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:
Michael Wain 2025-02-09 17:57:04 +03:00
commit 26d2b2c325
5 changed files with 1304 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
target/

1031
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

15
Cargo.toml Normal file
View 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
View 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
View 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
}
}