tlc: add post-copy verification to recipe install loop

This commit is contained in:
2026-06-20 19:51:35 +03:00
parent 933336180f
commit 714f7c2115
8 changed files with 237 additions and 18 deletions
@@ -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].")
@@ -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<Instant>,
@@ -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,
}
}
@@ -48,6 +48,7 @@ mod network;
mod platform;
mod render;
mod sensor;
mod storage;
mod theme;
use crate::app::{App, POLL_MS, TabId};
@@ -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)
@@ -0,0 +1,175 @@
//! Block device storage info via `sysfs` (`/sys/block/<dev>/`).
//!
//! 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/<dev>/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/<dev>/stat` (single line,
/// 15 fields per Documentation/block/stat.txt).
pub fn parse(line: &str) -> Self {
let fields: Vec<u64> = line
.split_whitespace()
.filter_map(|f| f.parse::<u64>().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<String>,
pub vendor: Option<String>,
pub size_bytes: u64,
pub rotational: bool,
pub removable: bool,
pub scheduler: Option<String>,
pub queue_depth: Option<u32>,
pub stats: DiskStats,
pub partitions: Vec<String>,
}
fn read_sysfs(path: &Path) -> Option<String> {
fs::read_to_string(path).ok().map(|s| s.trim().to_string()).filter(|s| !s.is_empty())
}
fn read_sysfs_u64(path: &Path) -> Option<u64> {
read_sysfs(path)?.parse::<u64>().ok()
}
fn read_sysfs_u32(path: &Path) -> Option<u32> {
read_sysfs(path)?.parse::<u32>().ok()
}
fn read_disk(name: &str, path: &Path) -> Option<DiskInfo> {
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<DiskInfo>,
}
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()
}
}
+4
View File
@@ -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}"
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
@@ -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 {
@@ -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;