From f162de16a5034b027dc47b2e154edfba7a7b3cd9 Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 27 Dec 2025 22:32:23 +0100 Subject: [PATCH] feat: add footer bar with disk space, item count, and app info - Add footer bar displaying disk space, item count, and selection info - Show default application for selected files - Add dependencies: sysinfo and mime_guess - Fix RefCell borrowing issues in path change callbacks - Footer updates dynamically on directory navigation and file selection --- Cargo.lock | 243 +++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 + src/files_panel.rs | 6 +- src/footer_bar.rs | 236 +++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 89 ++++++++++++++++- src/style.rs | 9 ++ 6 files changed, 581 insertions(+), 4 deletions(-) create mode 100644 src/footer_bar.rs diff --git a/Cargo.lock b/Cargo.lock index 6c7a38d..77464cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,6 +13,8 @@ name = "axfm" version = "0.1.0" dependencies = [ "gtk4", + "mime_guess", + "sysinfo", ] [[package]] @@ -54,6 +56,43 @@ dependencies = [ "target-lexicon", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "equivalent" version = "1.0.2" @@ -424,6 +463,31 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "ntapi" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c70f219e21142367c70c0b30c6a9e3a14d55b4d12a204d897fbec83a0363f081" +dependencies = [ + "winapi", +] + [[package]] name = "pango" version = "0.21.3" @@ -493,6 +557,26 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "rustc_version" version = "0.4.1" @@ -560,6 +644,20 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sysinfo" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c33cd241af0f2e9e3b5c32163b873b29956890b5342e6745b917ce9d490f4af" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows", +] + [[package]] name = "system-deps" version = "7.0.6" @@ -630,6 +728,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-ident" version = "1.0.19" @@ -642,12 +746,87 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core", + "windows-targets", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-targets", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -657,6 +836,70 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "winnow" version = "0.7.13" diff --git a/Cargo.toml b/Cargo.toml index 647b38f..f1c3c1a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,3 +5,5 @@ edition = "2024" [dependencies] gtk4 = "0.10.1" +sysinfo = "0.32" +mime_guess = "2.0" diff --git a/src/files_panel.rs b/src/files_panel.rs index 54be06a..939dd0e 100644 --- a/src/files_panel.rs +++ b/src/files_panel.rs @@ -9,7 +9,9 @@ use std::{ rc::Rc, }; -pub fn build_files_panel(fmstate: Rc>) -> (ScrolledWindow, StringList, ListView) { +pub fn build_files_panel( + fmstate: Rc>, +) -> (ScrolledWindow, StringList, ListView, SingleSelection) { let files_list = StringList::new(&[]); let files_selection = SingleSelection::new(Some(files_list.clone())); @@ -204,7 +206,7 @@ pub fn build_files_panel(fmstate: Rc>) -> (ScrolledWindow, Stri let list_view = ListView::new(Some(files_selection.clone()), Some(factory)); let scroll = ScrolledWindow::builder().child(&list_view).vexpand(true).hexpand(true).build(); - (scroll, files_list, list_view) + (scroll, files_list, list_view, files_selection) } pub fn populate_files_list(files_list: >k4::StringList, dir: &gio::File, show_hidden: &bool) { diff --git a/src/footer_bar.rs b/src/footer_bar.rs new file mode 100644 index 0000000..4c1cd70 --- /dev/null +++ b/src/footer_bar.rs @@ -0,0 +1,236 @@ +use gtk4::{Box as GtkBox, Label, Orientation, gio, prelude::*}; +use std::path::Path; +use sysinfo::Disks; + +pub struct FooterBarComponents { + pub left_label: Label, + pub center_label: Label, + pub right_label: Label, +} + +pub fn build_footer_bar() -> (GtkBox, FooterBarComponents) { + let footer_bar = GtkBox::new(Orientation::Horizontal, 0); + footer_bar.set_height_request(30); + footer_bar.add_css_class("footer-bar"); + + // Left label: Disk space + let left_label = Label::new(Some("")); + left_label.set_halign(gtk4::Align::Start); + left_label.add_css_class("footer-label"); + + // Center label: Item count or selection info + let center_label = Label::new(Some("")); + center_label.set_halign(gtk4::Align::Center); + center_label.set_hexpand(true); + center_label.add_css_class("footer-label"); + + // Right label: Default application + let right_label = Label::new(Some("")); + right_label.set_halign(gtk4::Align::End); + right_label.add_css_class("footer-label"); + + footer_bar.append(&left_label); + footer_bar.append(¢er_label); + footer_bar.append(&right_label); + + let components = FooterBarComponents { left_label, center_label, right_label }; + + (footer_bar, components) +} + +pub fn update_disk_space(label: &Label, path: &gio::File) { + if let Some(path_buf) = path.path() { + if let Some((available, total)) = get_disk_space(&path_buf) { + let available_gb = available as f64 / 1_073_741_824.0; // Convert to GB + let total_gb = total as f64 / 1_073_741_824.0; + let used_percent = ((total - available) as f64 / total as f64 * 100.0) as u32; + + label.set_text(&format!( + "{:.2} GB free of {:.2} GB ({}% used)", + available_gb, total_gb, used_percent + )); + } else { + label.set_text("N/A"); + } + } else { + // Remote URIs (trash:///, network paths, etc.) + label.set_text("N/A"); + } +} + +pub fn update_item_count(label: &Label, count: usize) { + let text = if count == 1 { "1 item".to_string() } else { format!("{} items", count) }; + label.set_text(&text); +} + +pub fn update_selection_info(label: &Label, file: &gio::File) { + if let Ok(info) = file.query_info( + "standard::size,standard::type,standard::content-type", + gio::FileQueryInfoFlags::NONE, + None::<&gio::Cancellable>, + ) { + let file_type = info.file_type(); + + if file_type == gio::FileType::Directory { + label.set_text("Directory"); + } else { + let size = info.size(); + let size_str = format_size(size as u64); + + // Get file type description + let type_desc = get_file_type_description(file); + + label.set_text(&format!("{} - {}", type_desc, size_str)); + } + } else { + label.set_text(""); + } +} + +pub fn update_default_app(label: &Label, file: &gio::File) { + // Only show default app for regular files, not directories + if let Ok(info) = file.query_info( + "standard::type,standard::content-type", + gio::FileQueryInfoFlags::NONE, + None::<&gio::Cancellable>, + ) { + let file_type = info.file_type(); + + if file_type == gio::FileType::Regular { + if let Some(app_name) = get_default_app(file) { + label.set_text(&format!("Opens with: {}", app_name)); + } else { + label.set_text(""); + } + } else { + label.set_text(""); + } + } else { + label.set_text(""); + } +} + +pub fn count_items(dir: &gio::File, show_hidden: bool) -> usize { + let mut count = 0; + + if let Ok(enumerator) = dir.enumerate_children( + "standard::name", + gio::FileQueryInfoFlags::NONE, + None::<&gio::Cancellable>, + ) { + while let Some(info) = enumerator.next_file(None::<&gio::Cancellable>).unwrap_or(None) { + let name = info.name(); + let name_str = name.to_string_lossy(); + + if !show_hidden && name_str.starts_with('.') { + continue; + } + + count += 1; + } + } + + count +} + +fn get_disk_space(path: &Path) -> Option<(u64, u64)> { + let disks = Disks::new_with_refreshed_list(); + + for disk in disks.list() { + if path.starts_with(disk.mount_point()) { + return Some((disk.available_space(), disk.total_space())); + } + } + + None +} + +fn get_file_type_description(file: &gio::File) -> String { + // Try to get MIME type from gio first + if let Ok(info) = file.query_info( + "standard::content-type", + gio::FileQueryInfoFlags::NONE, + None::<&gio::Cancellable>, + ) { + if let Some(content_type) = info.content_type() { + let content_str = content_type.to_string(); + + // Convert MIME type to friendly name + return match content_str.as_str() { + "application/pdf" => "PDF Document".to_string(), + "text/plain" => "Text File".to_string(), + "text/html" => "HTML Document".to_string(), + "image/jpeg" | "image/jpg" => "JPEG Image".to_string(), + "image/png" => "PNG Image".to_string(), + "image/gif" => "GIF Image".to_string(), + "image/svg+xml" => "SVG Image".to_string(), + "video/mp4" => "MP4 Video".to_string(), + "video/x-matroska" => "MKV Video".to_string(), + "audio/mpeg" => "MP3 Audio".to_string(), + "audio/ogg" => "OGG Audio".to_string(), + "application/zip" => "ZIP Archive".to_string(), + "application/x-tar" => "TAR Archive".to_string(), + "application/gzip" => "GZIP Archive".to_string(), + _ => { + // Extract main type (e.g., "image" from "image/png") + if let Some(main_type) = content_str.split('/').next() { + format!("{} file", main_type.to_uppercase()) + } else { + content_str + } + } + }; + } + } + + // Fallback to mime_guess if gio doesn't work + if let Some(path) = file.path() { + if let Some(mime) = mime_guess::from_path(&path).first() { + let mime_str = mime.essence_str(); + return match mime_str { + "application/pdf" => "PDF Document".to_string(), + "text/plain" => "Text File".to_string(), + _ => mime_str.to_string(), + }; + } + } + + "Unknown type".to_string() +} + +fn get_default_app(file: &gio::File) -> Option { + if let Ok(info) = file.query_info( + "standard::content-type", + gio::FileQueryInfoFlags::NONE, + None::<&gio::Cancellable>, + ) { + if let Some(content_type) = info.content_type() { + if let Some(app) = gio::AppInfo::default_for_type(&content_type, false) { + return Some(app.name().to_string()); + } + } + } + + None +} + +fn format_size(bytes: u64) -> String { + const KB: u64 = 1024; + const MB: u64 = KB * 1024; + const GB: u64 = MB * 1024; + const TB: u64 = GB * 1024; + + if bytes >= TB { + format!("{:.2} TB", bytes as f64 / TB as f64) + } else if bytes >= GB { + format!("{:.2} GB", bytes as f64 / GB as f64) + } else if bytes >= MB { + format!("{:.2} MB", bytes as f64 / MB as f64) + } else if bytes >= KB { + format!("{:.2} KB", bytes as f64 / KB as f64) + } else if bytes == 1 { + "1 byte".to_string() + } else { + format!("{} bytes", bytes) + } +} diff --git a/src/main.rs b/src/main.rs index 8e04351..b38a48e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod files_panel; +mod footer_bar; mod headerbar; mod pathbar; mod popup_menu; @@ -37,7 +38,8 @@ fn build_fm(app: &Application) { let home_path = gio::File::for_path(glib::home_dir()); let fmstate = Rc::new(RefCell::new(state::FmState::new(home_path.clone()))); - let (files_scroll, files_list, list_view) = files_panel::build_files_panel(fmstate.clone()); + let (files_scroll, files_list, list_view, files_selection) = + files_panel::build_files_panel(fmstate.clone()); let (sidebar_box, sidebar_selection) = sidebar::build_sidebar(fmstate.clone(), &files_list); let path_bar = pathbar::build_pathbar(&mut fmstate.borrow_mut()); @@ -208,6 +210,89 @@ fn build_fm(app: &Application) { paned.set_resize_start_child(false); paned.set_shrink_start_child(false); - window.set_child(Some(&paned)); + // Build footer bar + let (footer_bar, footer_components) = footer_bar::build_footer_bar(); + + // Create main vertical box to hold paned and footer + let main_vbox = GtkBox::new(Orientation::Vertical, 0); + main_vbox.append(&paned); + main_vbox.append(&footer_bar); + + // Store labels in local variables for cloning + let left_label = footer_components.left_label.clone(); + let center_label = footer_components.center_label.clone(); + let right_label = footer_components.right_label.clone(); + let files_list_path = files_list.clone(); + + // Connect footer updates for path changes + fmstate.borrow_mut().connect_path_changed(glib::clone!( + #[weak] + left_label, + #[weak] + center_label, + #[weak] + right_label, + #[weak] + files_list_path, + move |new_path| { + // Update disk space + footer_bar::update_disk_space(&left_label, new_path); + + // Update item count based on what's actually displayed in the list + let count = files_list_path.n_items() as usize; + footer_bar::update_item_count(¢er_label, count); + + // Clear selection info and default app + right_label.set_text(""); + } + )); + + // Clone labels again for selection callback + let center_label_sel = footer_components.center_label.clone(); + let right_label_sel = footer_components.right_label.clone(); + let files_list_sel = files_list.clone(); + + // Connect footer updates for selection changes + files_selection.connect_selected_notify(glib::clone!( + #[weak] + center_label_sel, + #[weak] + right_label_sel, + #[weak] + files_list_sel, + move |sel| { + let idx = sel.selected(); + + if idx == gtk4::INVALID_LIST_POSITION { + // No selection - show item count based on displayed items + let count = files_list_sel.n_items() as usize; + footer_bar::update_item_count(¢er_label_sel, count); + right_label_sel.set_text(""); + } else { + // Selection - show file info + if let Some(obj) = files_list_sel.item(idx) { + let string_obj = obj.downcast::().unwrap(); + let file_str = string_obj.string(); + + let file = if std::path::Path::new(&file_str).exists() { + gio::File::for_path(&file_str) + } else { + gio::File::for_uri(&file_str) + }; + + footer_bar::update_selection_info(¢er_label_sel, &file); + footer_bar::update_default_app(&right_label_sel, &file); + } + } + } + )); + + // Initialize footer with current state + let current_path = fmstate.borrow().current_path.clone(); + footer_bar::update_disk_space(&footer_components.left_label, ¤t_path); + let count = footer_bar::count_items(¤t_path, fmstate.borrow().settings.show_hidden); + footer_bar::update_item_count(&footer_components.center_label, count); + + window.set_child(Some(&main_vbox)); window.present(); } diff --git a/src/style.rs b/src/style.rs index 07313b2..2d85bab 100644 --- a/src/style.rs +++ b/src/style.rs @@ -10,6 +10,15 @@ pub fn load_css() { .pathbar { margin: 5px; } + .footer-bar { + background-color: #f0f0f0; + border-top: 1px solid #d0d0d0; + padding: 5px 10px; + font-size: 12px; + } + .footer-label { + margin: 0 10px; + } ", );