Lyrica initial </3
new file: .gitignore new file: Cargo.lock new file: Cargo.toml new file: src/main.rs new file: src/util.rs
This commit is contained in:
commit
26d2b2c325
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
target/
|
1031
Cargo.lock
generated
Normal file
1031
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
Cargo.toml
Normal file
15
Cargo.toml
Normal file
@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "lyrica"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "AGPLv3"
|
||||
authors = ["Michael Wain <alterwain@protonmail.com>"]
|
||||
|
||||
[dependencies]
|
||||
rusb = "0.9.4"
|
||||
regex = "1.11.1"
|
||||
ratatui = { version = "0.29.0", features = ["all-widgets"] }
|
||||
color-eyre = "0.6.3"
|
||||
crossterm = "0.28.1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-util = { version = "0.7.12", features = ["codec"] }
|
157
src/main.rs
Normal file
157
src/main.rs
Normal file
@ -0,0 +1,157 @@
|
||||
use std::io;
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
|
||||
use ratatui::{buffer::Buffer, layout::Rect, style::Stylize, symbols::border, text::{Line, Text}, widgets::{Block, Paragraph, Widget}, DefaultTerminal, Frame};
|
||||
use tokio::sync::mpsc::{self, Receiver, Sender, UnboundedReceiver, UnboundedSender};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
mod util;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum AppState {
|
||||
IPodWait,
|
||||
MainScreen,
|
||||
SoundCloud,
|
||||
Youtube,
|
||||
Preferences
|
||||
}
|
||||
|
||||
enum AppEvent {
|
||||
SearchIPod,
|
||||
IPodFound(String),
|
||||
IPodNotFound,
|
||||
}
|
||||
|
||||
fn initialize_async_service(sender: Sender<AppEvent>, receiver: UnboundedReceiver<AppEvent>, token: CancellationToken) {
|
||||
tokio::spawn(async move {
|
||||
let mut receiver = receiver;
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = token.cancelled() => { return; }
|
||||
r = receiver.recv() => {
|
||||
if let Some(request) = r {
|
||||
match request {
|
||||
AppEvent::SearchIPod => {
|
||||
if let Some(p) = util::search_ipod() {
|
||||
let _ = sender.send(AppEvent::IPodFound(p)).await;
|
||||
} else {
|
||||
let _ = sender.send(AppEvent::IPodNotFound).await;
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct App {
|
||||
state: AppState,
|
||||
receiver: Receiver<AppEvent>,
|
||||
sender: UnboundedSender<AppEvent>,
|
||||
token: CancellationToken,
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
fn default() -> Self {
|
||||
let (tx, mut rx) = mpsc::channel(1);
|
||||
let (jx, mut jr) = mpsc::unbounded_channel();
|
||||
let token = CancellationToken::new();
|
||||
initialize_async_service(tx, jr, token.clone());
|
||||
let _ = jx.send(AppEvent::SearchIPod);
|
||||
Self { state: AppState::IPodWait, receiver: rx, sender: jx, token }
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
|
||||
while !self.token.is_cancelled() {
|
||||
terminal.draw(|frame| self.draw(frame))?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(&self, frame: &mut Frame) {
|
||||
frame.render_widget(self, frame.area());
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> io::Result<()> {
|
||||
match event::read()? {
|
||||
Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
|
||||
self.handle_key_event(key_event)
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
if let Ok(event) = self.receiver.try_recv() {
|
||||
match event {
|
||||
AppEvent::IPodFound(path) => {
|
||||
self.state = AppState::MainScreen;
|
||||
},
|
||||
AppEvent::IPodNotFound => {
|
||||
let _ = self.sender.send(AppEvent::SearchIPod);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_key_event(&mut self, key_event: KeyEvent) {
|
||||
if key_event.code == KeyCode::Char('q') {
|
||||
self.exit();
|
||||
}
|
||||
}
|
||||
|
||||
fn exit(&mut self) {
|
||||
self.token.cancel();
|
||||
}
|
||||
|
||||
fn render_waiting_screen(&self, area: Rect, buf: &mut Buffer) {
|
||||
let title = Line::from(" Lyrica ".bold());
|
||||
let instructions = Line::from(vec![
|
||||
" Quit ".into(),
|
||||
"<Q> ".red().bold(),
|
||||
]);
|
||||
let block = Block::bordered()
|
||||
.title(title.centered())
|
||||
.title_bottom(instructions.centered())
|
||||
.border_set(border::ROUNDED);
|
||||
|
||||
let counter_text = Text::from(
|
||||
vec![
|
||||
Line::from(
|
||||
vec![
|
||||
"Searching for iPod...".into()
|
||||
]
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
Paragraph::new(counter_text)
|
||||
.centered()
|
||||
.block(block)
|
||||
.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
match self.state {
|
||||
AppState::IPodWait => self.render_waiting_screen(area, buf),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
let mut terminal = ratatui::init();
|
||||
let app_result = App::default().run(&mut terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
100
src/util.rs
Normal file
100
src/util.rs
Normal file
@ -0,0 +1,100 @@
|
||||
use std::{str, process::Command, error::Error, str::FromStr};
|
||||
use regex::Regex;
|
||||
|
||||
const VENDOR_ID: u16 = 1452;
|
||||
const PRODUCT_ID: u16 = 4617;
|
||||
|
||||
pub fn search_ipod() -> Option<String> {
|
||||
for device in rusb::devices().unwrap().iter() {
|
||||
let device_desc = device.device_descriptor().unwrap();
|
||||
if VENDOR_ID == device_desc.vendor_id() && PRODUCT_ID == device_desc.product_id() {
|
||||
return get_ipod_path()
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn list() -> Result<Vec<String>, Box<dyn Error>> {
|
||||
let mut disks = Vec::new();
|
||||
let r = match Command::new("diskutil").arg("list").output() {
|
||||
Ok(s) => s,
|
||||
Err(e) => return Err(Box::new(e))
|
||||
};
|
||||
if !r.status.success() { return Ok(disks); }
|
||||
let rg = Regex::new(r"\d:.+ [a-zA-Z0-9].+").unwrap();
|
||||
let a = match str::from_utf8(&r.stdout) {
|
||||
Ok(r) => r,
|
||||
Err(e) => return Err(Box::new(e))
|
||||
};
|
||||
for cap in Regex::new(r"\/dev\/.+\(external\, physical\):").unwrap().find_iter(a) {
|
||||
let mut b = &a[cap.end()..];
|
||||
let i = match b.find("\n\n") {
|
||||
Some(r) => r,
|
||||
None => return Ok(disks)
|
||||
};
|
||||
b = &b[..i];
|
||||
for gap in rg.find_iter(b) {
|
||||
let j = match gap.as_str().rfind(" ") {
|
||||
Some(r) => r + 1,
|
||||
None => return Ok(disks)
|
||||
};
|
||||
|
||||
let g= &gap.as_str()[j..];
|
||||
disks.push(String::from_str(g).unwrap());
|
||||
}
|
||||
}
|
||||
Ok(disks)
|
||||
}
|
||||
|
||||
fn is_ipod(name: &str) -> bool {
|
||||
let r = match Command::new("diskutil").arg("info").arg(name).output() {
|
||||
Ok(s) => s,
|
||||
Err(_e) => return false
|
||||
};
|
||||
if !r.status.success() { return false; }
|
||||
let a = match str::from_utf8(&r.stdout) {
|
||||
Ok(r) => r,
|
||||
Err(_e) => return false
|
||||
};
|
||||
let cap = Regex::new(r"Media Type:.+\n").unwrap().find(a);
|
||||
if let Some(g) = cap {
|
||||
let mut b = g.as_str();
|
||||
let f = b.rfind(" ").unwrap() + 1;
|
||||
b = &b[f..b.len()-1];
|
||||
return b == "iPod";
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn get_mount_point(name: &str) -> Option<String> {
|
||||
let r = match Command::new("diskutil").arg("info").arg(name).output() {
|
||||
Ok(s) => s,
|
||||
Err(_e) => return None
|
||||
};
|
||||
if !r.status.success() { return None; }
|
||||
let a = match str::from_utf8(&r.stdout) {
|
||||
Ok(r) => r,
|
||||
Err(_e) => return None
|
||||
};
|
||||
let cap = Regex::new(r"Mount Point:.+\n").unwrap().find(a);
|
||||
match cap {
|
||||
Some(g) => {
|
||||
let i = g.as_str();
|
||||
let j = i.rfind(" ").unwrap() + 1;
|
||||
Some(i[j..i.len()-1].to_string())
|
||||
},
|
||||
None => None
|
||||
}
|
||||
}
|
||||
|
||||
fn get_ipod_path() -> Option<String> {
|
||||
match list() {
|
||||
Ok(l) => l.iter()
|
||||
.filter(|d| is_ipod(d))
|
||||
.map(|d| get_mount_point(d))
|
||||
.filter(|d| d.is_some())
|
||||
.map(|d| d.unwrap())
|
||||
.last(),
|
||||
Err(_e) => None
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user