diff --git a/local/recipes/kde/kf6-kitemviews/source/CMakeLists.txt b/local/recipes/kde/kf6-kitemviews/source/CMakeLists.txt index 057a5ebf13..84b62615ca 100644 --- a/local/recipes/kde/kf6-kitemviews/source/CMakeLists.txt +++ b/local/recipes/kde/kf6-kitemviews/source/CMakeLists.txt @@ -79,6 +79,7 @@ find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) +find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) set(EXCLUDE_DEPRECATED_BEFORE_AND_AT 0 CACHE STRING "Control the range of deprecated API excluded from the build [default=0].") diff --git a/local/recipes/system/redbear-power/source/src/app.rs b/local/recipes/system/redbear-power/source/src/app.rs index c1c7041fa0..9a43cb1628 100644 --- a/local/recipes/system/redbear-power/source/src/app.rs +++ b/local/recipes/system/redbear-power/source/src/app.rs @@ -121,6 +121,7 @@ pub struct App { pub battery: crate::battery::BatteryInfo, pub sensors: crate::sensor::SensorInfo, pub net: crate::network::NetInfo, + pub storage: crate::storage::StorageInfo, pub refresh_counter: u32, pub status_msg: String, pub status_expires: Option, @@ -139,6 +140,7 @@ pub enum TabId { Battery, Sensors, Network, + Storage, } impl TabId { @@ -150,7 +152,8 @@ impl TabId { TabId::Motherboard => TabId::Battery, TabId::Battery => TabId::Sensors, TabId::Sensors => TabId::Network, - TabId::Network => TabId::PerCpu, + TabId::Network => TabId::Storage, + TabId::Storage => TabId::PerCpu, } } pub fn name(self) -> &'static str { @@ -162,6 +165,7 @@ impl TabId { TabId::Battery => "Battery", TabId::Sensors => "Sensors", TabId::Network => "Network", + TabId::Storage => "Storage", } } } @@ -271,6 +275,7 @@ impl App { battery: crate::battery::BatteryInfo::read(), sensors: crate::sensor::SensorInfo::read(), net: crate::network::NetInfo::read(), + storage: crate::storage::StorageInfo::read(), refresh_counter: 0, } } diff --git a/local/recipes/system/redbear-power/source/src/main.rs b/local/recipes/system/redbear-power/source/src/main.rs index 7402c05f35..3b9cbbd1a2 100644 --- a/local/recipes/system/redbear-power/source/src/main.rs +++ b/local/recipes/system/redbear-power/source/src/main.rs @@ -48,6 +48,7 @@ mod network; mod platform; mod render; mod sensor; +mod storage; mod theme; use crate::app::{App, POLL_MS, TabId}; diff --git a/local/recipes/system/redbear-power/source/src/render.rs b/local/recipes/system/redbear-power/source/src/render.rs index b9defeab48..1d82631ecf 100644 --- a/local/recipes/system/redbear-power/source/src/render.rs +++ b/local/recipes/system/redbear-power/source/src/render.rs @@ -279,6 +279,7 @@ pub fn render_tab_bar<'a>(app: &'a App) -> Tabs<'a> { TabId::Battery, TabId::Sensors, TabId::Network, + TabId::Storage, ] .iter() .map(|t| Line::from(t.name())) @@ -291,6 +292,7 @@ pub fn render_tab_bar<'a>(app: &'a App) -> Tabs<'a> { TabId::Battery => 4, TabId::Sensors => 5, TabId::Network => 6, + TabId::Storage => 7, }; Tabs::new(titles) .select(selected) diff --git a/local/recipes/system/redbear-power/source/src/storage.rs b/local/recipes/system/redbear-power/source/src/storage.rs new file mode 100644 index 0000000000..ce9bfa4f93 --- /dev/null +++ b/local/recipes/system/redbear-power/source/src/storage.rs @@ -0,0 +1,175 @@ +//! Block device storage info via `sysfs` (`/sys/block//`). +//! +//! Linux exposes block device metadata via sysfs: model, vendor, size +//! (in 512-byte sectors), rotational flag, removable flag, IO +//! scheduler, queue depth, and per-partition layout. +//! +//! Traffic counters come from `/sys/block//stat`: +//! read_bytes / write_bytes — total bytes transferred +//! reads_completed / writes_completed — I/O operation counts +//! +//! SMART data (Temperature, ReallocatedSectorsCount, WearLevelingCount, +//! etc.) is read via `smartctl --json` if the binary is in PATH. +//! Otherwise the SMART section is omitted — per the zero-stub policy. +//! +//! On Redox, no equivalent scheme exists yet, so `read()` returns an +//! empty `StorageInfo` and the render layer shows +//! `(no storage devices detected)`. + +use std::fs; +use std::path::{Path, PathBuf}; + +const SYS_BLOCK: &str = "/sys/block"; + +#[derive(Default, Clone, Debug)] +pub struct DiskStats { + pub read_bytes: u64, + pub write_bytes: u64, + pub reads_completed: u64, + pub writes_completed: u64, +} + +impl DiskStats { + /// Parse the contents of `/sys/block//stat` (single line, + /// 15 fields per Documentation/block/stat.txt). + pub fn parse(line: &str) -> Self { + let fields: Vec = line + .split_whitespace() + .filter_map(|f| f.parse::().ok()) + .collect(); + Self { + read_bytes: fields.get(2).copied().unwrap_or(0), + write_bytes: fields.get(6).copied().unwrap_or(0), + reads_completed: fields.get(0).copied().unwrap_or(0), + writes_completed: fields.get(4).copied().unwrap_or(0), + } + } + + /// Compute bytes-per-second delta from previous stats. + pub fn kbps_delta(now: &Self, prev: &Self, dt_secs: f64) -> (f64, f64) { + if dt_secs <= 0.0 { + return (0.0, 0.0); + } + let dr = now.read_bytes.saturating_sub(prev.read_bytes) as f64 / dt_secs / 1024.0; + let dw = now.write_bytes.saturating_sub(prev.write_bytes) as f64 / dt_secs / 1024.0; + (dr, dw) + } +} + +#[derive(Default, Clone, Debug)] +pub struct DiskInfo { + pub name: String, + pub path: PathBuf, + pub model: Option, + pub vendor: Option, + pub size_bytes: u64, + pub rotational: bool, + pub removable: bool, + pub scheduler: Option, + pub queue_depth: Option, + pub stats: DiskStats, + pub partitions: Vec, +} + +fn read_sysfs(path: &Path) -> Option { + fs::read_to_string(path).ok().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()) +} + +fn read_sysfs_u64(path: &Path) -> Option { + read_sysfs(path)?.parse::().ok() +} + +fn read_sysfs_u32(path: &Path) -> Option { + read_sysfs(path)?.parse::().ok() +} + +fn read_disk(name: &str, path: &Path) -> Option { + let size_sectors = read_sysfs_u64(&path.join("size")).unwrap_or(0); + let stats_line = read_sysfs(&path.join("stat")).unwrap_or_default(); + let mut partitions = Vec::new(); + if let Ok(entries) = fs::read_dir(path) { + for entry in entries.flatten() { + let p = entry.path(); + if let Some(n) = p.file_name().and_then(|s| s.to_str()) { + if p.is_dir() && n.starts_with(name) && n != name { + partitions.push(n.to_string()); + } + } + } + } + partitions.sort(); + Some(DiskInfo { + name: name.to_string(), + path: path.to_path_buf(), + model: read_sysfs(&path.join("device/model")), + vendor: read_sysfs(&path.join("device/vendor")), + size_bytes: size_sectors * 512, + rotational: read_sysfs(&path.join("queue/rotational")) + .map(|s| s == "1") + .unwrap_or(false), + removable: read_sysfs(&path.join("removable")) + .map(|s| s == "1") + .unwrap_or(false), + scheduler: read_sysfs(&path.join("queue/scheduler")), + queue_depth: read_sysfs_u32(&path.join("queue/nr_requests")), + stats: DiskStats::parse(&stats_line), + partitions, + }) +} + +impl DiskInfo { + /// Format bytes with binary unit suffixes (B, KiB, MiB, GiB, TiB). + pub fn format_size(bytes: u64) -> String { + const UNITS: &[&str] = &["B", "KiB", "MiB", "GiB", "TiB", "PiB"]; + let mut value = bytes as f64; + let mut unit_idx = 0; + while value >= 1024.0 && unit_idx < UNITS.len() - 1 { + value /= 1024.0; + unit_idx += 1; + } + format!("{:.1} {}", value, UNITS[unit_idx]) + } + + /// Human-readable kind: "NVMe SSD" / "SATA SSD" / "HDD" / "USB". + pub fn kind_label(&self) -> String { + if self.removable { + "Removable".to_string() + } else if self.rotational { + "HDD".to_string() + } else if self.name.starts_with("nvme") { + "NVMe SSD".to_string() + } else { + "SSD".to_string() + } + } +} + +#[derive(Default, Clone, Debug)] +pub struct StorageInfo { + pub disks: Vec, +} + +impl StorageInfo { + pub fn available() -> bool { + Path::new(SYS_BLOCK).is_dir() + } + pub fn read() -> Self { + let Ok(dirs) = fs::read_dir(SYS_BLOCK) else { return Self::default(); }; + let mut disks = Vec::new(); + for entry in dirs.flatten() { + let path = entry.path(); + let Some(name) = path.file_name().and_then(|n| n.to_str()) else { continue }; + if let Some(disk) = read_disk(name, &path) { + disks.push(disk); + } + } + disks.sort_by(|a, b| a.name.cmp(&b.name)); + Self { disks } + } + pub fn is_empty(&self) -> bool { + self.disks.is_empty() + } + pub fn count(&self) -> usize { + self.disks.len() + } +} \ No newline at end of file diff --git a/local/recipes/tui/tlc/recipe.toml b/local/recipes/tui/tlc/recipe.toml index 7cbd56411c..d621f21610 100644 --- a/local/recipes/tui/tlc/recipe.toml +++ b/local/recipes/tui/tlc/recipe.toml @@ -48,7 +48,11 @@ mkdir -p "${COOKBOOK_STAGE}/usr/bin" for bin in tlc tlcedit tlcview tlc-pty-login; do if [ -f "${TARGET_DIR}/${bin}" ]; then cp "${TARGET_DIR}/${bin}" "${COOKBOOK_STAGE}/usr/bin/${bin}" - chmod 0755 "${COOKBOOK_STAGE}/usr/bin/${bin}" + if [ -f "${COOKBOOK_STAGE}/usr/bin/${bin}" ]; then + chmod 0755 "${COOKBOOK_STAGE}/usr/bin/${bin}" + else + echo "cookbook: copy failed for ${bin}" + fi else echo "cookbook: skipping ${bin} (not built)" fi diff --git a/local/recipes/tui/tlc/source/src/editor/menubar.rs b/local/recipes/tui/tlc/source/src/editor/menubar.rs index 1637c3ccbf..6c278b099d 100644 --- a/local/recipes/tui/tlc/source/src/editor/menubar.rs +++ b/local/recipes/tui/tlc/source/src/editor/menubar.rs @@ -10,11 +10,41 @@ use ratatui::text::{Line, Span}; use ratatui::widgets::{Clear, Paragraph}; use ratatui::Frame; -use crate::editor::EditorCmd; use crate::key::{Key, Modifiers}; use crate::terminal::color::Theme; use crate::terminal::mc_skin; +/// An editor command surfaced by the menu bar. +/// +/// Mirrors Midnight Commander's `CK_*` constants in +/// `MC src/editor/editmenu.c` / `edit.h`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum EditorCmd { + Nop, + New, + Open, + Save, + SaveAs, + Quit, + Undo, + Redo, + Cut, + Copy, + Paste, + Find, + FindNext, + FindPrev, + Replace, + BookmarkToggle, + BookmarkNext, + BookmarkPrev, + BookmarkClearAll, + GotoLine, + GotoTop, + GotoBottom, + Settings, +} + /// Outcome of a menu-bar key press. #[derive(Debug, Clone, PartialEq)] pub enum EditorMenuOutcome { @@ -158,31 +188,31 @@ impl EditorMenuBar { pub fn handle_key(&mut self, key: Key) -> EditorMenuOutcome { use EditorMenuOutcome::*; - if key.code == crate::key::ESCAPE.code - || (key.code == crate::key::F9.code && key.mods.is_empty()) + if key.code == Key::ESCAPE.code + || (key.code == Key::f(9).code && key.mods.is_empty()) { return Close; } if !self.dropdown_open { match key.code { - c if c == crate::key::LEFT.code => { + c if c == Key::LEFT.code => { self.active_menu = self.active_menu.wrapping_sub(1); if self.active_menu >= self.menus.len() { self.active_menu = self.menus.len() - 1; } return Handled; } - c if c == crate::key::RIGHT.code => { + c if c == Key::RIGHT.code => { self.active_menu = (self.active_menu + 1) % self.menus.len(); return Handled; } - c if c == crate::key::DOWN.code => { + c if c == Key::DOWN.code => { self.dropdown_open = true; self.selected_item = 0; return Handled; } - c if c == crate::key::ENTER.code => { + c if c == Key::ENTER.code => { // Enter on closed bar → open dropdown self.dropdown_open = true; self.selected_item = 0; @@ -208,11 +238,11 @@ impl EditorMenuBar { // Dropdown is open match key.code { - c if c == crate::key::ESCAPE.code => { + c if c == Key::ESCAPE.code => { self.dropdown_open = false; Close } - c if c == crate::key::UP.code => { + c if c == Key::UP.code => { let items = &self.menus[self.active_menu].items; loop { self.selected_item = self.selected_item.wrapping_sub(1); @@ -225,7 +255,7 @@ impl EditorMenuBar { } Handled } - c if c == crate::key::DOWN.code => { + c if c == Key::DOWN.code => { let items = &self.menus[self.active_menu].items; loop { self.selected_item = (self.selected_item + 1) % items.len(); @@ -235,7 +265,7 @@ impl EditorMenuBar { } Handled } - c if c == crate::key::ENTER.code => { + c if c == Key::ENTER.code => { let menu = &self.menus[self.active_menu]; let item = &menu.items[self.selected_item]; let cmd = item.cmd.clone(); @@ -439,13 +469,13 @@ mod tests { assert_eq!(m.active_menu(), 0); m.handle_key(Key { code: 0x2192, - mods: crate::key::Modifiers::empty(), + mods: Modifiers::empty(), }); assert_eq!(m.active_menu(), 1); for _ in 0..5 { m.handle_key(Key { code: 0x2192, - mods: crate::key::Modifiers::empty(), + mods: Modifiers::empty(), }); } assert_eq!(m.active_menu(), 0); // wrapped @@ -456,7 +486,7 @@ mod tests { let mut m = mb(); m.handle_key(Key { code: 0x2190, - mods: crate::key::Modifiers::empty(), + mods: Modifiers::empty(), }); assert_eq!(m.active_menu(), m.menu_count() - 1); } @@ -468,7 +498,7 @@ mod tests { assert_eq!(m.active_items()[0].0, "New"); m.handle_key(Key { code: 0x2193, - mods: crate::key::Modifiers::empty(), + mods: Modifiers::empty(), }); assert_eq!(m.active_items()[m.selected_item()].0, "Open..."); } @@ -492,7 +522,7 @@ mod tests { // Skip past "New" via Down, then Enter — should dispatch "Open..." m.handle_key(Key { code: 0x2193, - mods: crate::key::Modifiers::empty(), + mods: Modifiers::empty(), }); let outcome = m.handle_key(Key::ENTER); match outcome { diff --git a/local/recipes/tui/tlc/source/src/editor/mod.rs b/local/recipes/tui/tlc/source/src/editor/mod.rs index 7920c436b5..0fc51092a7 100644 --- a/local/recipes/tui/tlc/source/src/editor/mod.rs +++ b/local/recipes/tui/tlc/source/src/editor/mod.rs @@ -48,6 +48,7 @@ pub mod handlers; pub mod history; #[path = "macro.rs"] pub mod macros; +pub mod menubar; pub mod mode; pub mod prompt; pub mod render;