tlc: add post-copy verification to recipe install loop
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user