diff --git a/Cargo.lock b/Cargo.lock index b1efe8b..9ae4e29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -202,6 +202,26 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + [[package]] name = "bitflags" version = "2.6.0" @@ -272,6 +292,15 @@ dependencies = [ "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -284,6 +313,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "compact_str" version = "0.8.0" @@ -330,6 +370,7 @@ checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ "bitflags", "crossterm_winapi", + "futures-core", "mio", "parking_lot", "rustix", @@ -596,6 +637,12 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "hashbrown" version = "0.15.2" @@ -661,6 +708,15 @@ dependencies = [ "syn", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -682,12 +738,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.168" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" +[[package]] +name = "libloading" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +dependencies = [ + "cfg-if", + "windows-targets", +] + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -723,6 +795,17 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "loopdev-3" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90a97d7a5124296ee9124a815acdc3dc4a91f577b72812b3f1f99bb959b46e8d" +dependencies = [ + "bindgen", + "errno", + "libc", +] + [[package]] name = "lru" version = "0.12.5" @@ -756,6 +839,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.0" @@ -783,7 +872,9 @@ version = "0.1.0" dependencies = [ "crossterm", "ratatui", + "sys-mount", "tokio", + "tokio-stream", "udisks2", ] @@ -800,6 +891,16 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "objc" version = "0.2.7" @@ -1015,7 +1116,7 @@ dependencies = [ "crossterm", "indoc", "instability", - "itertools", + "itertools 0.13.0", "lru", "paste", "strum", @@ -1068,6 +1169,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustix" version = "0.38.42" @@ -1192,6 +1299,17 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "smart-default" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "socket2" version = "0.5.8" @@ -1247,6 +1365,20 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sys-mount" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6acb8bb63826062d5a44b68298cf2e25b84bc151bc0c31c35a83b61f818682a" +dependencies = [ + "bitflags", + "libc", + "loopdev-3", + "smart-default", + "thiserror", + "tracing", +] + [[package]] name = "temp-dir" version = "0.1.14" @@ -1266,6 +1398,26 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tokio" version = "1.42.0" @@ -1295,6 +1447,17 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml_datetime" version = "0.6.8" @@ -1391,7 +1554,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" dependencies = [ - "itertools", + "itertools 0.13.0", "unicode-segmentation", "unicode-width 0.1.14", ] diff --git a/Cargo.toml b/Cargo.toml index 3504a6e..e1a4a30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,9 @@ version = "0.1.0" edition = "2021" [dependencies] -crossterm = "0.28.1" +crossterm = { version = "0.28.1", features = ["event-stream"]} ratatui = "0.29.0" +sys-mount = "3.0.1" tokio = {version = "1.42.0", features = ["full"]} +tokio-stream = "0.1.17" udisks2 = "0.2.0" diff --git a/src/drives.rs b/src/drives.rs index c64b5ea..56f0acf 100644 --- a/src/drives.rs +++ b/src/drives.rs @@ -1,4 +1,5 @@ use std::{ + collections::HashMap, ffi::{OsStr, OsString}, os::unix::ffi::{OsStrExt, OsStringExt}, sync::{Arc, Mutex}, @@ -6,19 +7,20 @@ use std::{ use crate::mountpoints; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Block { - pub id: String, + pub object_path: String, pub dev: String, pub label: String, pub mount: Option, pub fstype: String, + pub mounted: bool, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Drive { pub id: String, - pub path: String, + pub object_path: String, pub model: String, pub ejectable: bool, pub blocks: Vec, @@ -44,14 +46,14 @@ pub async fn collect_drives_from_udisk() -> udisks2::Result> { let path = path.to_string(); if let Ok(drv) = i.drive().await { let drv = Drive { - path, + object_path: path, id: drv.id().await?, model: drv.model().await?, ejectable: drv.ejectable().await?, blocks: Vec::new(), }; - if let Some(d) = drives.iter_mut().find(|i| i.path == drv.path) { + if let Some(d) = drives.iter_mut().find(|i| i.object_path == drv.object_path) { d.model = drv.model; d.ejectable = drv.ejectable; d.id = drv.id; @@ -61,7 +63,7 @@ pub async fn collect_drives_from_udisk() -> udisks2::Result> { } else if let Ok(blk) = i.block().await { let drv_path = blk.drive().await?.to_string(); let block = Block { - id: blk.id().await?, + object_path: path, dev: String::from_utf8_lossy(&blk.device().await?) .chars() .filter(|c| c != &'\0') @@ -69,13 +71,14 @@ pub async fn collect_drives_from_udisk() -> udisks2::Result> { label: blk.id_label().await?, mount: None, fstype: blk.id_type().await?, + mounted: false, }; - if let Some(d) = drives.iter_mut().find(|i| i.path == drv_path) { + if let Some(d) = drives.iter_mut().find(|i| i.object_path == drv_path) { d.blocks.push(block); } else { drives.push(Drive { - path: drv_path, + object_path: drv_path, id: String::new(), model: String::new(), ejectable: false, @@ -94,7 +97,7 @@ pub async fn collect_all() -> udisks2::Result> { let mut fstab = Drive { id: "fstab".to_owned(), - path: "fstab".to_owned(), + object_path: "fstab".to_owned(), model: "fstab".to_owned(), ejectable: false, blocks: Vec::new(), @@ -107,22 +110,55 @@ pub async fn collect_all() -> udisks2::Result> { .and_then(|d| d.blocks.iter_mut().find(|b| b.dev == i.dev)); if let Some(block) = block { block.mount = i.path; + block.mounted = i.mounted; } else { fstab.blocks.push(Block { - id: i.dev.clone(), + object_path: String::new(), dev: i.dev, label: String::new(), mount: i.path, fstype: i.fs, + mounted: i.mounted, }); } } drives.push(fstab); - drives.sort_by_cached_key(|b| b.path.clone()); + drives.sort_by_cached_key(|b| b.object_path.clone()); for i in &mut drives { i.blocks.sort_by_cached_key(|b| b.dev.clone()); } - - return Ok(drives); + + Ok(drives) +} + +pub async fn mount(block: &Block) -> udisks2::Result<()> { + let mut drives: Vec = Vec::new(); + let client = udisks2::Client::new().await?; + + client + .object(block.object_path.clone())? + .filesystem() + .await? + .mount(HashMap::new()) + .await?; + + // client.part + + Ok(()) +} + +pub async fn unmount(block: &Block) -> udisks2::Result<()> { + let mut drives: Vec = Vec::new(); + let client = udisks2::Client::new().await?; + + client + .object(block.object_path.clone())? + .filesystem() + .await? + .unmount(HashMap::new()) + .await?; + // client.part + + Ok(()) } diff --git a/src/main.rs b/src/main.rs index b18ace5..f4fbbad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,48 +3,109 @@ mod mountpoints; use std::{ ffi::{OsStr, OsString}, + io::stderr, os::unix::ffi::{OsStrExt, OsStringExt}, - sync::{Arc, Mutex}, + sync::Arc, + time::Duration, }; -use crossterm::event::{self, Event, KeyCode, KeyEventKind}; +use crossterm::{ + event::{Event, EventStream, KeyCode, KeyEventKind}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use drives::Drive; use mountpoints::MountPoint; use ratatui::{ layout::{Alignment, Constraint, Layout}, + prelude::CrosstermBackend, style::Color, text::{Line, Text}, widgets::{ Block, BorderType, Padding, Paragraph, Row, StatefulWidget, Table, TableState, Widget, Wrap, }, - Frame, + Frame, Terminal, }; +use tokio::sync::Mutex; +use tokio_stream::StreamExt; + +enum Command { + None, + Mount(String), + Umount(String), +} #[tokio::main] async fn main() -> udisks2::Result<()> { - let mut terminal = ratatui::init(); + enable_raw_mode().unwrap(); + execute!(stderr(), EnterAlternateScreen).unwrap(); + let mut terminal = Terminal::new(CrosstermBackend::new(stderr())).unwrap(); let mut ts = TableState::new(); + + let period = Duration::from_secs_f32(1.0 / 10.0); + let mut interval = tokio::time::interval(period); + let mut events = EventStream::new(); + + let state: Arc>> = Arc::new(Mutex::new(Vec::new())); + let s = state.clone(); + tokio::spawn(async move { + loop { + let drv = drives::collect_all().await.unwrap(); + s.lock().await.clone_from(&drv); + tokio::time::sleep(Duration::from_millis(500)).await; + } + }); + + let mut output = String::new(); + let mut last_status = String::new(); + loop { - let drv = drives::collect_all().await?; + let drv = state.lock().await.clone(); + let mut selected: Option = None; terminal - .draw(|f| draw(f, &mut ts, &drv)) + .draw(|f| draw(f, &mut ts, &drv, &mut selected, &last_status)) .expect("failed to draw frame"); - if let Event::Key(key) = event::read().unwrap() { - if key.kind == KeyEventKind::Press { - match key.code { - KeyCode::Up => ts.select_previous(), - KeyCode::Down => ts.select_next(), - KeyCode::Esc => break, - _ => {} + tokio::select! { + _ = interval.tick() => { }, + Some(Ok(event)) = events.next() => { + if let Event::Key(key) = event { + if key.kind == KeyEventKind::Press { + match key.code { + KeyCode::Up | KeyCode::Char('k') => ts.select_previous(), + KeyCode::Down | KeyCode::Char('j') => ts.select_next(), + KeyCode::Char('m') => if let Some(b) = &selected { + last_status = format!("{:?}", drives::mount(b).await); + } + KeyCode::Char('u') => if let Some(b) = &selected { + last_status = format!("{:?}", drives::unmount(b).await); + } + KeyCode::Esc | KeyCode::Char('q') => break, + KeyCode::Enter => { + output = selected.unwrap().mount.unwrap(); + break; + } + _ => {} + } + } } - } + }, } } - ratatui::restore(); + + disable_raw_mode().unwrap(); + execute!(stderr(), LeaveAlternateScreen).unwrap(); + println!("{output}"); Ok(()) } -fn draw(frame: &mut Frame, state: &mut TableState, drv: &[drives::Drive]) { +fn draw( + frame: &mut Frame, + state: &mut TableState, + drv: &[drives::Drive], + selected: &mut Option, + last_status: &str, +) { let text = Text::raw("Hello World!"); frame.render_widget(text, frame.area()); @@ -61,12 +122,22 @@ fn draw(frame: &mut Frame, state: &mut TableState, drv: &[drives::Drive]) { .border_style(Color::Yellow); block.clone().render(layout[0], frame.buffer_mut()); - let rows = drv.iter().flat_map(|d| &d.blocks).map(|i| { + let rows: Vec = drv.iter().flat_map(|d| d.blocks.clone()).collect(); + + state + .selected() + .and_then(|n| rows.get(n).cloned()) + .clone_into(selected); + + let rows = rows.iter().map(|i| { Row::new(vec![ i.dev.clone(), i.label.clone(), i.mount.clone().unwrap_or_default(), - "M".to_owned(), + match i.mounted { + true => "M".to_owned(), + false => "O".to_owned(), + }, ]) }); let widths = [ @@ -81,8 +152,10 @@ fn draw(frame: &mut Frame, state: &mut TableState, drv: &[drives::Drive]) { StatefulWidget::render(table, block.inner(frame.area()), frame.buffer_mut(), state); frame.render_widget( - Paragraph::new("j - UP, k - DOWN, l - Goto mountpoint\nm - Mount, u - Unmount, e - Eject") - .wrap(Wrap { trim: true }), + Paragraph::new(format!( + "j - UP, k - DOWN, l - Goto mountpoint, m - Mount, u - Unmount, e - Eject\n{selected:?} {last_status:?}" + )) + .wrap(Wrap { trim: true }), layout[1], ); } diff --git a/src/mountpoints.rs b/src/mountpoints.rs index f19154c..aa53595 100644 --- a/src/mountpoints.rs +++ b/src/mountpoints.rs @@ -5,10 +5,22 @@ pub struct MountPoint { pub dev: String, pub path: Option, pub fs: String, + pub mounted: bool, } impl MountPoint { pub fn collect_from_file(path: &str) -> Vec { + const FSTYPE_IGNORE: [&str; 8] = [ + "tmpfs", + "swap", + "devtmpfs", + "devpts", + "hugetlbfs", + "mqueue", + "fuse.portal", + "fuse.gvfsd-fuse", + ]; + const PATH_IGNORE: [&str; 3] = ["/tmp", "/sys", "/proc"]; std::io::BufReader::new(std::fs::File::open(PathBuf::from(path)).unwrap()) .lines() .map_while(Result::ok) @@ -21,17 +33,16 @@ impl MountPoint { .into(), path: Some(parts.next()?.to_string()), fs: parts.next()?.into(), + mounted: false, }) }) + .filter(|p| !FSTYPE_IGNORE.contains(&p.fs.as_str())) .filter(|p| { - p.fs != "tmpfs" && p.fs != "swap" - && p.path.clone().is_some_and(|p| { - !p.starts_with("/sys") - && !p.starts_with("/tmp") - && !p.starts_with("/run") - && !p.starts_with("/proc") - && !p.starts_with("/dev") - }) + if let Some(p) = &p.path { + !PATH_IGNORE.iter().any(|ignore| p.starts_with(ignore)) + } else { + false + } }) .collect() } @@ -44,12 +55,14 @@ impl MountPoint { .into_iter() .filter(|p| !mnt.iter().any(|f| f.path == p.path)) .map(|p| MountPoint { - dev: p.dev, - path: None, - fs: p.fs, + mounted: false, + ..p }) .collect(); - mnt.into_iter().chain(fstab).collect() + mnt.into_iter() + .map(|m| MountPoint { mounted: true, ..m }) + .chain(fstab) + .collect() } }