Files
RedBear-OS/local/docs/redbear-power-improvement-plan.md
T
vasilito ed92bce14b redbear-power: v1.1 — full Phase A→D implementation
Comprehensive implementation per local/docs/redbear-power-improvement-plan.md.

Source: 2376 LoC across 10 modules (was 1396/6 in v0.6, +980 LoC).
Cross-compile: 2.8 MB stripped Redox ELF binary.
SHA256: 1b6f9db6ce79e77957bbb1fd606c430516015d5f02f3b64cb6f395e2f63b8e04

Modules:
- main.rs     (376) — event loop, key + mouse dispatch, render orchestration
- app.rs      (421) — App, CpuRow, Governor, ThrottleMode, PackageThermal, HybridInfo
- render.rs   (498) — header/table/controls/help/snapshot rendering
- acpi.rs     (166) — CPU enumeration, ACPI _PSS, CPUID fallback
- cpuid.rs    (350) — CPUID leaf decoding (vendor, family, model, SIMD, cache, hybrid)
- bench.rs    (123) — prime-sieve stress benchmark for thermal response testing
- dbus.rs     (202) — D-Bus export via zbus 5 (org.redbear.Power, --dbus flag)
- msr.rs      (127) — MSR constants + PackageThermal decoder
- cpufreq.rs  (50)  — governor hint read/write
- theme.rs    (72)  — central color palette (const Style)

Phase A — bug fixes:
- R1: PROCHOT pulse bug — Instant::now() math always ~0, pulse never toggled.
  Replaced with Frame::count() so the bar pulses at a frame-rate-stable rate.
- R5: removed duplicate comment block in snapshot().
- C2: PackageThermal struct + 13 PKG_THERM_* bit constants; full decode of
  IA32_PACKAGE_THERM_STATUS (PL1/PL2/CRIT/TT1/TT2/HFI/temp) surfaced in header.

Phase B — quality:
- R3: input poll decoupled from refresh cadence (50ms vs 250-2000ms).
- R4: Rect::centered replaces hand-rolled centered_rect helper.
- R6: area.layout(&Layout) destructuring with compile-time size check.
- O2: theme.rs central color palette (LABEL, BORDER_*, STATUS_*).
- C9: ratatui 0.30 Stylize shorthand across all renders.

Phase C — features:
- C1/C8: cpuid.rs reads leaves 0/1/4/7/0x80000000+/0x1A/0x8000001E.
- C3: SIMD display header line.
- C5: cache hierarchy header line.
- C7: dynamic refresh interval via / key (typed input 50..60000ms + Enter).
- C6: prime-sieve benchmark via b/B keys (one thread per core, AtomicU64
  counter, run/stop/status).

Phase D remaining (was deferred per plan s23):
- C4: hybrid CPU detection (CoreType enum, Intel leaf 0x1A, AMD leaf 0x8000001E),
  per-CPU row prefixed with type label, Hybrid: 8P + 16E header.
- O1: termion MouseTerminal wrapper enables xterm mouse protocols.
  Wheel = scroll, Left-click = select/toggle, Right-click = expand P-state.
  hit_test() maps (x, y) to panel rects cached after every render.
- O3: dbus.rs publishes org.redbear.Power on session bus (opt-in via --dbus).
  Properties: cpu_count, avg_freq_khz, max_temp_c, avg_load_pct, governor,
  throttle_mode, prochot_asserted. Background thread owns the tokio runtime
  + zbus Connection; main thread sends snapshots via mpsc channel. Graceful
  degradation if redbear-sessiond is unreachable.

Verification:
- cargo build --release (host): 0 errors, 21 warnings.
- ./redbear-power --once (Linux host, AMD 24-core): renders all features.
- ./redbear-power --dbus (via script(1)): registers on session bus,
  emits xterm mouse capture sequences.
- cook redbear-power (Redox target): 2.8 MB stripped binary at
  local/recipes/system/redbear-power/target/x86_64-unknown-redox/stage/usr/bin/redbear-power.

ISO rebuild status: blocked by pre-existing upstream nix-0.30.1 vs Redox
relibc SaFlags incompatibility in uutils (recipes/core/uutils). The v1.1
binary IS staged and will be packaged into the next successful ISO build
once that issue is resolved (separate scope).

Docs:
- local/docs/CONSOLE-TO-KDE-DESKTOP-PLAN.md s3.3.2 - v1.0 + v1.1 sections.
- local/docs/redbear-power-improvement-plan.md s24 - full status update.
- local/docs/RATATUI-APP-PATTERNS.md - canonical ratatui 0.30 guide
  (1161 lines), includes s13 ratatui 0.30 Best-Practices Update + s14
  Cross-Reference redbear-power as a Reference Implementation.

Cargo.toml: new dependencies zbus = "5" (tokio feature) and tokio = "1"
(rt + rt-multi-thread + macros) for the D-Bus export.
2026-06-20 13:26:24 +03:00

45 KiB
Raw Blame History

Red Bear Power — Improvement Plan v1.0 (Phase 3 Roadmap)

Target tool: local/recipes/system/redbear-power/ (Redox-native Rust ratatui TUI) Current version: v0.6 (2026-06-20, 1396 lines, 6 modules) Scope: Phase 1 (correctness/bug fixes) and Phase 2 (comprehensive quality expansion) Cross-references: ratatui 0.30.2 best-practices survey + cpu-x v4.7 architectural study

Reading guide: This document is intentionally long. Each section is self-contained. Use the Executive Summary for the prioritized action list, then drill down into specific sections as needed.


Executive Summary

This plan synthesizes:

  1. ratatui 0.30.2 best-practices audit — official docs, demo2 reference app, and the latest widgets crate (released 2026-06-19). Head: e665c36c.
  2. cpu-x v4.7 architectural study/tmp/cpu-x-src/, a 7000+ LoC C++17 mature CPU monitor (Linux). Established 2014, recently maintained, both ncurses and GTK UIs.

Headline findings

# Finding Severity Source
R1 PROCHOT pulse bugnow.elapsed() is always ~0 because now is constructed at every call. Pulse never changes phase. bug §1, ratatui audit §4
R2 Use Frame::count() instead of Instant math for frame-rate-stable animations. minor ratatui audit §4
R3 Decouple input poll (50ms) from refresh cadence (250-2000ms) for snappy UX. minor ratatui audit §8
R4 Replace hand-rolled centered_rect with Rect::centered (0.30 idiom). cosmetic ratatui audit §9
R5 Duplicate comment in snapshot() (lines 514-518 and 519-523). cosmetic ratatui audit §11
R6 Use area.layout(&layout) destructuring (compile-time size check). cosmetic ratatui audit §10
C1 Missing: chip/architecture detection (cpu-x tracks 30+ vendors, we track only AMD/Intel from CPUID). gap cpu-x §3
C2 Missing: package-level thermal sensor alongside per-core. We have it via IA32_PACKAGE_THERM_STATUS in app.rs:221 but only use the PROCHOT bit; full readout is discarded. gap cpu-x §4, §6
C3 Missing: instruction-set listing (SSE/AVX/AVX-512/AES/etc.) in header. cpu-x renders this as a multi-line label. gap cpu-x §3
C4 Missing: CPU purpose breakdown (Performance-cores vs Efficiency-cores on hybrid Intel CPUs). cpu-x splits into multiple cpu_types. gap cpu-x §3
C5 Missing: cache hierarchy display (L1d/L1i/L2/L3). cpu-x shows this in its own panel. gap cpu-x §3
C6 Missing: benchmark tab — cpu-x runs prime-number benchmarks for stress tests. Useful when monitoring throttling. gap (low priority) cpu-x §12
C7 Missing: dynamic refresh — we have fixed [250, 500, 1000, 2000] step. cpu-x allows user-typed interval. minor cpu-x §7
C8 Missing: cache awareness — cpu-x libcpuid does full CPU identification with raw cpuid dump. We only read 0. gap cpu-x §3
C9 Pattern: chip abstraction — cpu-x's Label { name, value, ext } is a tidy way to attach format strings to typed values. We use ad-hoc string formatting. pattern cpu-x §11
C10 Pattern: dynamic layout constants — cpu-x's SizeInfo::width/height is a static struct of terminal dimensions. We hardcode HEADER_LINES = 6, CONTROLS_LINES = 21. pattern cpu-x §11
C11 Pattern: pause/freeze — cpu-x uses ERR (no input) to drive refresh; we use std::thread::sleep. Same effect, but the canonical pattern uses non-blocking poll. pattern ratatui audit §8
O1 No mouse support — official ratatui examples include this as Tier 4. feature ratatui audit (Tier 4)
O2 No color theme / config file — colors are hardcoded throughout render.rs. maintainability cpu-x Pairs::init, ratatui Theme pattern
O3 No sysinfo dumpredbear-info exists in the recipe catalog but doesn't expose package power data. integration cpu-x §11

Prioritized Action List (Phased)

Phase A (Immediate, 1-2 hours): Correctness fixes

  • R1: Fix PROCHOT pulse — replace Instant::now() math with Frame::count(). Estimated: 5 min.
  • R5: Remove duplicate comment in snapshot(). Estimated: 1 min.
  • C2 (partial): Surface full package thermal readout in header (read bit fields of IA32_PACKAGE_THERM_STATUS instead of just PROCHOT). Estimated: 15 min.

Phase B (This Week, 3-4 hours): Quality improvements aligned with ratatui 0.30 + cpu-x patterns

  • R3: Decouple input poll from refresh cadence. Estimated: 10 min.
  • R4: Replace centered_rect with Rect::centered. Estimated: 5 min.
  • R6: Use area.layout(&layout) destructuring. Estimated: 5 min.
  • C10: Introduce SizeInfo consts struct + Theme consts. Estimated: 30 min.
  • O2: Wire Theme constants for color management. Estimated: 1 hour.
  • C9: Wrap CpuRow and per-field labels in a structured Label pattern for cleaner display logic. Estimated: 30 min.

Phase C (This Month, 6-8 hours): Feature additions

  • C1: Multi-vendor CPU identification (parse CPUID leaf 0 correctly, recognize 30+ vendors). Estimated: 2 hours.
  • C3: Instruction-set display in header (SSE/AVX flags from CPUID leaf 1 ECX/EDX, leaf 7 EBX/ECX). Estimated: 1 hour.
  • C5: Cache hierarchy panel (read via CPUID leaf 4 for L1/L2/L3). Estimated: 1 hour.
  • C7: Dynamic refresh interval (typed input via crossterm/termion raw mode). Estimated: 1 hour.
  • C8: Full cpuid raw dump (read leaves 0, 1, 4, 7, 0x80000000-0x80000008). Estimated: 1 hour.

Phase D (Next Quarter, Optional / Tier 4 features)

  • O1: Mouse support for row selection + scrolling. Estimated: 2 hours.
  • C4: Hybrid CPU detection (P-cores vs E-cores on Intel 12th+). Estimated: 2 hours.
  • C6: Lightweight benchmark (one-shot CPU burn to validate thermal response). Estimated: 2 hours.
  • O3: D-Bus export (publish to org.redbear.Power for KWin/system tray). Estimated: 4 hours.

1. PROCHOT Pulse Bug (R1, R2)

Problem

render.rs:118-140 (render_prochot_alert):

pub fn render_prochot_alert(app: &App, width: u16, now: std::time::Instant) -> Option<Paragraph<'static>> {
    let any_prochot = app.cpus.iter().any(|c| c.prochot);
    if !any_prochot {
        return None;
    }
    // 500 ms period: first half filled, second half empty + indicator.
    let elapsed_ms = now.elapsed().as_millis() as u64;  // ← BUG: ~0 every call
    let phase = (elapsed_ms / 250) % 2;
    let bar_char = if phase == 0 { '█' } else { ' ' };
    let indicator = if phase == 0 { ' ' } else { '▌' };
    // ...
}

main.rs:131 constructs Instant::now() immediately before calling render_prochot_alert:

if let Some(alert) = render_prochot_alert(&app, area.width, Instant::now()) {

So now.elapsed() is always ~0 at every render, phase is always 0, and the bar never toggles. The PROCHOT alert appears static (filled bar) instead of pulsing.

This was flagged by the ratatui best-practices audit (Section §4) but with even more detail — the audit correctly identifies the API to fix it.

Fix (canonical 0.30 idiom)

Replace the time-based animation with Frame::count(), which is the canonical pattern in the official sparkline.rs example:

pub fn render_prochot_alert(app: &App, frame: &Frame) -> Option<Paragraph<'static>> {
    let any_prochot = app.cpus.iter().any(|c| c.prochot);
    if !any_prochot {
        return None;
    }
    // Pulse period: 2 frames on, 2 frames off (~1 Hz at 4 FPS, ~30 Hz at 60 FPS).
    let phase = (frame.count() / 2) % 2;
    let (bar_char, indicator) = if phase == 0 {
        ('█', ' ')
    } else {
        (' ', '▌')
    };
    let width = frame.area().width as usize;
    let line = format!(
        "{}{}{}{}",
        bar_char,
        indicator,
        bar_char.to_string().repeat(width.saturating_sub(2)),
        bar_char
    );
    Some(
        Paragraph::new(line)
            .style(Style::new().red().bold()),  // also see Stylize shorthand §2
    )
}

Frame::count() (ratatui-core/src/terminal/frame.rs#L211-L237) is a monotonic frame counter that increments on each successful render. This makes the pulse rate frame-rate-stable: slow terminals pulse slower; fast terminals pulse faster. The user's visual perception is consistent because the absolute number of frames per cycle is fixed (4 frames = 4 renders).

Caller change (main.rs:131)

if let Some(alert) = render_prochot_alert(&app, f) {
    let alert_area = Rect::new(0, f.area().bottom() - 1, f.area().width, 1);
    f.render_widget(alert, alert_area);
}

Or restructure the layout to include a dedicated alert row in the vertical split.


2. Stylize Shorthand (R7)

Audit finding

render.rs uses verbose Style::default().fg(Color::X) chains at least 30 times (audit §5). The 0.30 release stabilized Stylize trait, allowing:

Style::new().red().bold()     // instead of Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)
"Vendor: ".cyan()             // for `Cow<'_, str>` and `&str`
42.green()                    // for primitives via `Styled`

This is purely cosmetic — no functional change. But it would shorten render.rs by ~50-80 lines and make color intent more visible.

Example refactor

Before (render.rs:103):

let border_style = if focused {
    Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
} else {
    Style::default().fg(Color::DarkGray)
};

After:

let border_style = if focused {
    Style::new().yellow().bold()
} else {
    Style::new().dark_gray()
};

Before (render.rs:170):

ThrottleMode::Auto => Span::styled("AUTO", Style::default().fg(Color::Green)),

After:

ThrottleMode::Auto => "AUTO".green().into(),

Import change

Add use ratatui::style::Stylize; at top of render.rs.

Recommendation

Apply across the entire render.rs in one focused PR. Low risk — purely visual.


3. centered_rectRect::centered (R4)

Audit finding

render.rs:92-98 defines centered_rect(percent_x, percent_y, r) by hand. The 0.30 release added Rect::centered(Constraint, Constraint) and friends.

Before (main.rs:135-139):

if show_help {
    let area = centered_rect(70, 80, f.area());
    f.render_widget(Clear, area);
    f.render_widget(render_help(), area);
}

After:

if show_help {
    let area = f.area().centered(
        Constraint::Percentage(70),
        Constraint::Percentage(80),
    );
    f.render_widget(Clear, area);
    f.render_widget(render_help(), area);
}

The helper function becomes dead code — remove it.


4. Decoupled Input Poll vs Refresh Cadence (R3)

Audit finding

main.rs:93-94, 142-198 uses std::thread::sleep(poll) with poll ranging from 250ms to 2000ms. This means the event loop blocks for up to 2 seconds before checking for input, producing a sluggish feel even though our event polling machinery is correct.

The canonical pattern (ratatui demo2/app.rs#L52-L57) uses a fixed short timeout (20-50ms) for input poll and a separate timer for refresh:

// Pseudo-code for the decoupled pattern
loop {
    let elapsed = last_refresh.elapsed();
    if elapsed >= poll_duration {
        app.refresh();
        last_refresh = Instant::now();
    }
    terminal.draw(|f| render(f, &app))?;

    if event::poll(Duration::from_millis(INPUT_POLL_MS))? {
        // handle event
    }
}

This decouples input latency (20ms, snappy) from refresh cadence (250-2000ms, configurable). User changes will feel instantaneous while data still updates at the chosen rate.

Concrete change

In main.rs:

const INPUT_POLL_MS: u64 = 50;  // 20 Hz input check
let poll = Duration::from_millis(POLL_MS);  // existing refresh cadence
let mut last_refresh = Instant::now();
let input_timeout = Duration::from_millis(INPUT_POLL_MS);

'render_loop: loop {
    if last_refresh.elapsed() >= poll {
        app.refresh();
        last_refresh = Instant::now();
    }
    terminal.draw(|f| render(f, &app))?;

    if let Some(Ok(event)) = events.next() {
        if let Event::Key(k) = event {
            match handle_key(&mut app, k, &mut show_help) {
                Action::Quit => break 'render_loop,
                Action::Render => {} // already rendered
            }
        }
    }
    std::thread::sleep(input_timeout);
}

Note: termion::async_stdin().events().next() is non-blocking by design, but the current code's thread::sleep(poll) is what blocks input. Removing the thread::sleep(poll) and adding a fixed thread::sleep(INPUT_POLL_MS) fixes the responsiveness without changing the refresh model.


5. Layout Destructuring (R6)

Audit finding

main.rs:116-123 uses the 0.29 idiom with Layout::default().split(...) returning chunks:

let chunks = Layout::default()
    .direction(Direction::Vertical)
    .constraints([
        Constraint::Length(render::HEADER_LINES),
        Constraint::Min(6),
        Constraint::Length(render::CONTROLS_LINES),
    ])
    .split(f.area());
f.render_widget(render_header(&app, focused_panel == 0), chunks[0]);

The 0.30 idiom uses area.layout(&layout) which destructures with compile-time size checking:

let [header_area, table_area, controls_area] = f.area().layout(
    &Layout::vertical([
        Constraint::Length(render::HEADER_LINES),
        Constraint::Min(6),
        Constraint::Length(render::CONTROLS_LINES),
    ]),
);

The compile-time check (the destructuring pattern enforces exact 3-tuple) prevents silent index-misalignment bugs.


6. Snapshot Duplicate Comment (R5)

Audit finding

render.rs:511-545 (snapshot) has the same 5-line comment twice:

pub fn snapshot(app: &App, width: u16, height: u16) -> String {
    let backend = TestBackend::new(width, height);
    let mut terminal = Terminal::new(backend).expect("test terminal");
    // Copy the live table state for the snapshot — the TestBackend
    // doesn't share buffers with the interactive terminal, so we
    // can't pass `&mut app.table_state` (still borrowed by the
    // render call). A clone keeps the snapshot stable when the
    // interactive loop continues scrolling.
    let mut state = app.table_state;
    // Copy the live table state for the snapshot — the TestBackend   ← DUPLICATE
    // doesn't share buffers with the interactive terminal, so we     ← DUPLICATE
    // can't pass `&mut app.table_state` (still borrowed by the        ← DUPLICATE
    // render call). A clone keeps the snapshot stable when the        ← DUPLICATE
    // interactive loop continues scrolling.                           ← DUPLICATE
    terminal
        .draw(|f| {
            // ...
        })

Trivial cleanup — delete the second copy.


7. Multi-Vendor CPU Identification (C1, C8)

cpu-x reference pattern

cpu-x's libcpuid.cpp parses cpu_vendor_t (CPUID leaf 0) into a 30+ vendor table: Intel, AMD, Cyrix, NexGen, Transmeta, UMC, Centaur, Rise, SiS, NSC, Hygon, ARM Holdings, Broadcom, Cavium, DEC, Fujitsu, HiSilicon, Infineon, Freescale, NVIDIA, APM, Qualcomm, Samsung, Marvell, Apple, Faraday, Microsoft, Phytium, Ampere Computing.

Our current acpi.rs:read_cpu_id is hardcoded to read the vendor string from leaf 0 (12-byte ASCII string) and model from cpuid(1).eax family/model bits. This works for AMD/Intel but not ARM (which uses different leaf structure).

Proposed implementation

Add a new module cpuid.rs (alongside acpi.rs) with:

// cpuid.rs

pub struct CpuId {
    pub vendor_id: [u32; 4],      // leaf 0 EAX, EBX, ECX, EDX
    pub vendor: String,            // parsed from vendor_id
    pub family: u8,                // leaf 1 EAX bits 27:20 + 11:8
    pub model: u8,                 // leaf 1 EAX bits 19:16 + 7:4
    pub stepping: u8,              // leaf 1 EAX bits 3:0
    pub brand: String,             // leaves 0x80000002-4
    pub features: CpuFeatures,
    pub cache_l1d: Option<CacheInfo>,
    pub cache_l1i: Option<CacheInfo>,
    pub cache_l2: Option<CacheInfo>,
    pub cache_l3: Option<CacheInfo>,
}

pub struct CpuFeatures {
    pub mmx: bool,
    pub sse: bool, sse2: bool, sse3: bool, ssse3: bool,
    pub sse4_1: bool, sse4_2: bool, sse4a: bool,
    pub avx: bool, avx2: bool, avx512f: bool, avx512dq: bool,
    pub aes: bool, pclmulqdq: bool, sha_ni: bool,
    pub fma3: bool,
    pub vmx: bool, svm: bool,             // virtualization
    pub hypervisor: bool,
    // ... (full list from cpu-x data.cpp)
}

pub struct CacheInfo {
    pub level: u8,           // 1, 2, 3
    pub size_kb: u32,
    pub line_bytes: u8,
    pub associativity: u8,   // 0xFF = fully associative
    pub sets: u32,
    pub shared_cores: u32,
}

Then acpi.rs:read_cpu_id becomes a thin wrapper that calls cpuid::identify().

For Redox, we need a cpuid scheme or a /scheme/cpuid syscall. If not yet available, fall back to the existing string-based heuristic but emit a warning in the header: "cpuid scheme not available — using /scheme/cpuinfo fallback".


8. Package Thermal Sensor Full Readout (C2)

Problem

app.rs:221-237 reads IA32_PACKAGE_THERM_STATUS (MSR 0x1b1) but only uses the PROCHOT bit:

if let Some(pkg) = read_package_thermal_status(self.cpus[0].id) {
    self.throttle = if pkg & THERM_STATUS_PROCHOT != 0 {
        if matches!(self.throttle, ThrottleMode::Auto) {
            ThrottleMode::ForcedMin
        } else {
            self.throttle
        }
    } else if matches!(self.throttle, ThrottleMode::ForcedMin) {
        self.throttle
    } else {
        self.throttle
    };
}

The MSR has more useful bits (cpu-x shows all of these):

Bit Name Meaning
0 PROCHOT Package-level PROCHOT (any core asserted)
1 Reserved -
2 Reserved -
3 Reserved -
4 HFI Status History-Firmware Interrupt raised
5 Reserved -
6 Critical Temperature Package has hit T_CRIT
7 PROCHOT Log Log of past PROCHOT
8 PROCHOT Log2 Multi-bit PROCHOT Log
9 PROCHOT Log3 -
10 Reserved -
11 Power Limit #1 Package-level PL1 active
12 Power Limit #2 Package-level PL2 active
13 Power Limit Log PL history
14 Critical Temperature Log T_CRIT history
15 Thermal Threshold #1 Log TT1 history
16 Thermal Threshold #2 Log TT2 history
17-22 Temperature Readout Digital thermometer (in 1°C units)
23 Readout Valid Temperature bits are valid
24-31 Reserved -

Proposed implementation

Add a new struct in app.rs:

#[derive(Default, Clone, Copy)]
pub struct PackageThermal {
    pub temp_c: Option<u32>,        // bits 22:16
    pub valid: bool,                // bit 23
    pub prochot: bool,              // bit 0
    pub prochot_log: bool,          // bit 7
    pub crit_temp: bool,            // bit 6
    pub crit_temp_log: bool,        // bit 14
    pub power_limit_1: bool,        // bit 11
    pub power_limit_2: bool,        // bit 12
    pub thermal_throttle_1: bool,   // bit 15
    pub thermal_throttle_2: bool,   // bit 16
}

Parse in refresh() and store in App. Add to header line 3 alongside per-CPU max temp:

Pkg: 75°C  PkgFlags: PL1 (95°C max)     MSR: available     P-state source: ACPI _PSS

Or as a dedicated icon row:

Pkg: 75°C  ⚠ PL1  ⚠ PkgCrit  │  Cores: 24/24 online

9. Instruction-Set Display (C3)

cpu-x reference pattern

cpu-x's Processor struct has an instructions: Label that lists supported SIMD extensions:

Instructions: SSE(1, 2, 3, 3S, 4.1, 4.2, 4A), AVX(1, 2), FMA(3, 4), AES, SHA

This is highly useful for users who want to know what optimizations can run on the CPU.

Proposed implementation

Add an instructions: String field to App, formatted once in App::new() (instructions don't change at runtime):

// In cpuid.rs
pub fn format_instructions(features: &CpuFeatures) -> String {
    let mut parts = Vec::new();
    if features.sse || features.sse2 || features.sse3 || features.sse4_1 || features.sse4_2 {
        let mut sse = String::from("SSE(");
        let mut first = true;
        if features.sse { sse.push_str("1"); first = false; }
        if features.sse2 { if !first { sse.push(','); } sse.push_str("2"); first = false; }
        if features.sse3 { if !first { sse.push(','); } sse.push_str("3"); first = false; }
        if features.ssse3 { if !first { sse.push(','); } sse.push_str("3S"); first = false; }
        if features.sse4_1 { if !first { sse.push(','); } sse.push_str("4.1"); first = false; }
        if features.sse4_2 { if !first { sse.push(','); } sse.push_str("4.2"); first = false; }
        if features.sse4a { if !first { sse.push(','); } sse.push_str("4A"); }
        sse.push(')');
        parts.push(sse);
    }
    // ... AVX, FMA, AES, SHA, etc.
    parts.join(", ")
}

Display in header as a new line (collapsible if terminal is short):

SIMD: SSE(1,2,3,3S,4.1,4.2), AVX(1,2), FMA3, AES, SHA

Or wrap onto existing header if width allows.


10. Cache Hierarchy Display (C5)

cpu-x reference pattern

cpu-x's Caches Tab shows four separate labels (one per level):

Caches:
  L1 Data:   32 KiB (8 instances)
  L1 Inst.:  32 KiB (8 instances)
  Level 2:   256 KiB (8 instances)
  Level 3:   16 MiB (1 instance)

Proposed implementation

Add a caches: Vec<CacheInfo> field to App populated once at startup from CPUID leaf 4 (intel-style) or extended leaf 0x80000005/6 (AMD-style).

Display as a separate header line:

Cache: L1d 32KB×8 | L1i 32KB×8 | L2 256KB×8 | L3 16MB

Or as a new panel below the per-CPU table (when terminal is tall enough):

┌─ Cache Hierarchy ─────────────────┐
│  L1 Data:   32 KiB / 8-way         │
│  L1 Inst.:  32 KiB / 8-way         │
│  L2:       256 KiB / 8-way         │
│  L3:        16 MiB / 16-way        │
└───────────────────────────────────┘

11. Hybrid CPU Detection (C4)

cpu-x reference pattern

cpu-x's cpu_types vector supports heterogeneous core types: P-cores vs E-cores, big.LITTLE clusters, AMD CCDs. Each type has its own frequency table and bench score.

Proposed implementation

For Intel 12th+ hybrid CPUs:

  1. Read CPUID leaf 0x1A (native model ID) per logical processor.
  2. Group cores by CoreType::P (Performance) vs CoreType::E (Efficiency).
  3. Display as separate rows in the per-CPU table:
CPU Type  CPU  Freq/MHz   PkgW  Temp°C   P-state  State  Flags  Load % (30s)
─────────  ───  ────────  ────  ──────  ────────  ─────  ─────  ─────────────
P-core     0   3200       15.0   72 ██▌·  P2        mid    -      ▁▂▃▄▅▆▇█▆▅ 78%
P-core     1   3100       14.5   71 ██▎·  P2        mid    -      ▂▃▄▅▆▇█▇▆▅ 75%
...
E-core     8   2200        3.2   65 █▎··  P5        mid    -      ▁▁▂▂▃▃▄▄▅▅ 32%
E-core     9   2300        3.5   66 █▎··  P5        mid    -      ▁▁▂▂▃▃▄▄▅▅ 30%
...

For AMD CCDs: similar grouping by CPUID leaf 0x8000001E (Core/Thread ID).


12. Theme/Color Centralization (O2)

Problem

render.rs has 30+ ad-hoc Style::default().fg(Color::X) chains and 10+ Span::styled("...", Style::default().fg(Color::Cyan)) for label names. There's no single source of truth.

Proposed implementation

Create a new module theme.rs:

// theme.rs
use ratatui::style::{Color, Modifier, Style};
use ratatui::style::Stylize;

pub struct Theme;

impl Theme {
    pub const LABEL: Style = Style::new().cyan();
    pub const LABEL_BOLD: Style = Style::new().cyan().bold();
    pub const VALUE: Style = Style::new();
    pub const VALUE_HOT: Style = Style::new().red().bold();
    pub const VALUE_WARM: Style = Style::new().yellow();
    pub const VALUE_OK: Style = Style::new().green();
    pub const VALUE_OFF: Style = Style::new().dark_gray();

    pub const BORDER_FOCUSED: Style = Style::new().yellow().bold();
    pub const BORDER_DIM: Style = Style::new().dark_gray();

    pub const HEADER_GOVERNOR: Style = Style::new().magenta().bold();
    pub const HEADER_THROTTLE_AUTO: Style = Style::new().green();
    pub const HEADER_THROTTLE_USER: Style = Style::new().blue();
    pub const HEADER_THROTTLE_FORCED: Style = Style::new().red().bold();

    pub const STATUS_OK: Style = Style::new().green().bold();
    pub const STATUS_WARN: Style = Style::new().yellow().bold();
    pub const STATUS_ERR: Style = Style::new().red().bold();

    pub const PROCHOT_PULSE: Style = Style::new().red().bold();
}

Then in render.rs:

// Before
Span::styled("Vendor: ", Style::default().fg(Color::Cyan))
// After
"Vendor: ".set_style(Theme::LABEL)

Or with Stylize shorthand:

"Vendor: ".cyan()

For dark/light mode support, Theme can become &'static Theme injected at startup, allowing runtime theme switching via a config file (~/.config/redbear-power/theme.toml).

Benefit

  • One file controls all visual style
  • Easy theme switching (dark, light, colorblind)
  • Reduces render.rs line count by ~30%
  • Matches ratatui demo2 Theme pattern exactly

13. Dynamic Refresh Interval (C7)

Current limitation

We cycle through fixed [250, 500, 1000, 2000] ms with [ and ]. Users with specific monitoring needs (debugging thermal issues, capturing traces) may want finer control.

Proposed implementation

Add a new key : to enter "interval input mode" — captures a number followed by Enter:

Current: 500ms
Press : to set: 200<Enter>    → 200ms refresh

Or simpler: use the / key to bring up a small input prompt at the bottom of the screen that takes a numeric input and validates (must be >= 50ms, <= 60000ms).

Implementation sketch

// In main.rs
let mut interval_input_mode = false;
let mut interval_input_buf = String::new();

// On ':' key
interval_input_mode = true;
interval_input_buf.clear();

// In input handling during interval_input_mode
Key::Char(c) if interval_input_mode => {
    if c.is_ascii_digit() && interval_input_buf.len() < 5 {
        interval_input_buf.push(c);
    }
}
Key::Enter if interval_input_mode => {
    if let Ok(ms) = interval_input_buf.parse::<u64>() {
        if (50..=60_000).contains(&ms) {
            POLL_MS = ms;
            app.flash_status(format!("refresh → {ms}ms"));
        }
    }
    interval_input_mode = false;
}
Key::Esc if interval_input_mode => interval_input_mode = false,

Render the input prompt as an overlay in the status area:

┌─ Controls ────────────────────────┐
│  ...                              │
│  Refresh interval (ms): 200█       │ ← editable
│  ...                              │
└───────────────────────────────────┘

14. Mouse Support (O1)

Ratatui 0.30 support

MouseCapture is enabled per-backend (termion has MouseTerminal opt-in). The events are delivered via the same event::poll() cycle.

Proposed interactions

Mouse event Action
Scroll up on table page_selection(-1)
Scroll down on table page_selection(+1)
Click on CPU row table_state.select(Some(row_idx)) + toggle_expand()
Click on governor chip cycle_governor()
Click on throttle chip toggle_throttle_mode()
Right click Show context menu for selected CPU

Implementation sketch

// In main.rs
match event {
    MouseEvent::ScrollUp => app.page_selection(-1),
    MouseEvent::ScrollDown => app.page_selection(1),
    MouseEvent::Down(MouseButton::Left) => {
        // hit-test: figure out which panel was clicked
        // if table: select row + maybe expand
    }
}

Requires:

  1. Enable mouse capture on terminal startup: terminal.show_cursor()?.enable_raw_mode() etc.
  2. Add hit-testing logic in render closure that maps (x, y) → panel
  3. Handle MouseEvent in main loop

15. Configuration File (O2 partial)

Use case

User customizes:

  • Color theme (dark, light, colorblind)
  • Refresh interval default (override 500ms)
  • Displayed columns (per-CPU: which fields to show)
  • Key bindings (vim vs emacs style)

Format

TOML at /etc/redbear-power.toml (system) or ~/.config/redbear-power.toml (user):

[theme]
mode = "dark"  # dark | light | solarized | high-contrast

[display]
refresh_ms = 500
show_per_cpu_columns = ["freq", "pkgw", "temp", "pstate", "state", "flags", "load"]
show_cache_panel = true
show_simd_panel = true

[keybindings]
quit = "q"
cycle_governor = "g"
page_up = "PageUp"
page_down = "PageDown"
help = "?"

Implementation

Add local/recipes/system/redbear-power/source/src/config.rs:

// config.rs
use serde::Deserialize;

#[derive(Deserialize)]
pub struct Config {
    #[serde(default)]
    pub theme: ThemeConfig,
    #[serde(default)]
    pub display: DisplayConfig,
    #[serde(default)]
    pub keybindings: KeyBindings,
}

#[derive(Deserialize)]
pub struct ThemeConfig {
    #[serde(default = "default_theme_mode")]
    pub mode: String,  // "dark" | "light" | ...
}
// ... etc.

impl Config {
    pub fn load() -> Self {
        // Try /etc/redbear-power.toml, then ~/.config/redbear-power.toml,
        // then fall back to defaults.
        let paths = [
            PathBuf::from("/etc/redbear-power.toml"),
            dirs_home().map(|h| h.join(".config/redbear-power.toml")),
        ];
        for path in paths.into_iter().flatten() {
            if let Ok(content) = fs::read_to_string(&path) {
                if let Ok(cfg) = toml::from_str(&content) {
                    return cfg;
                }
            }
        }
        Self::default()
    }
}

Cargo dependency: toml = "0.8" and dirs = "5".


16. Tab System (cpu-x parity)

cpu-x reference

cpu-x has 8 tabs (CPU, Caches, Motherboard, Memory, System, Graphics, Bench, About) with a top-of-screen tab bar that highlights the active tab.

redbear-power extension

For now, our one-screen layout is appropriate for the power/thermal focus. But we could introduce:

  • Tab 1: Per-CPU (current view)
  • Tab 2: System (memory, cache hierarchy, uptime — like cpu-x System tab)
  • Tab 3: Info (vendor/model, SIMD, microcode, BIOS date — like cpu-x About tab)

Use ratatui's Tabs widget (which has a stateful mode) for the tab bar:

use ratatui::widgets::Tabs;

let tab_titles = vec!["Per-CPU", "System", "Info"];
let tabs = Tabs::new(tab_titles)
    .select(active_tab)
    .style(Theme::BORDER_DIM)
    .highlight_style(Theme::BORDER_FOCUSED)
    .divider(" │ ");

f.render_widget(tabs, tab_bar_area);

Hotkey: 1, 2, 3 to switch tabs directly.


17. D-Bus Export (O3)

Use case

System tray (KDE Plasma's StatusNotifierItem) or KWin's compositor wants to display the package temperature as a panel widget. Currently this requires polling — but a D-Bus interface would allow push updates.

Interface sketch

Service: org.redbear.Power
Path:    /org/redbear/Power/CPU0
Iface:   org.redbear.Power.CPU

Properties:
  uint32 Id                          (read-only)
  uint32 FreqKhz                     (read-only, PropertyChanged signal on update)
  uint32 TempCelsius                 (read-only)
  uint32 PowerMilliwatts             (read-only)
  uint32 LoadPercent                 (read-only)
  string Governor                    (read-write)
  uint32 TargetPstate                (read-write)
  string ThrottleMode                (read-write)

Signals:
  PropertiesChanged(dict)
  ThermalAlert(uint32 cpu, string level)   // WARN/THROTTLE/CRITICAL

This would require adding zbus to Cargo.toml and wiring the refresh() method to also publish changes.

Implementation

Add local/recipes/system/redbear-power/source/src/dbus.rs:

// dbus.rs
use zbus::{interface, ConnectionBuilder, SignalContext};

struct CpuPowerInterface {
    app: Arc<Mutex<App>>,
}

#[interface(name = "org.redbear.Power.CPU")]
impl CpuPowerInterface {
    #[zbus(property)]
    async fn id(&self) -> u32 { /* ... */ }
    #[zbus(property)]
    async fn freq_khz(&self) -> u32 { /* ... */ }
    // ... etc.
}

pub async fn run(app: Arc<Mutex<App>>) -> zbus::Result<()> {
    let conn = ConnectionBuilder::session()?
        .serve_at("/org/redbear/Power/CPU0", CpuPowerInterface { app })?
        .build()
        .await?;
    // ...
}

Caveat

D-Bus integration requires redbear-sessiond (session bus broker) and redbear-dbus-services to be running, which are themselves a Phase 4 deliverable. This work is most valuable once the desktop stack is operational.


18. Lightweight Stress Benchmark (C6)

Use case

When thermal issues are suspected, a stress test loads the CPU to 100% across all cores, letting the user see:

  • How quickly the thermal headroom runs out
  • Whether thermald / cpufreqd responds appropriately
  • Whether the CPU throttles (PROCHOT asserted)
  • Recovery time when stress is released

Implementation

Two new keys:

  • b — Start 30-second prime-sieve benchmark on all cores
  • B — Stop the benchmark

Algorithm: same as cpu-x's slow prime sieve (a fixed-bound sieve, simpler than the multi-threaded version). Spawn one thread per core.

// bench.rs
pub struct BenchState {
    pub running: bool,
    pub started_at: Option<Instant>,
    pub duration_s: u32,
    pub primes_found: AtomicU64,
    pub threads: Vec<JoinHandle<()>>,
}

impl BenchState {
    pub fn start(&mut self, duration_s: u32) {
        self.running = true;
        self.started_at = Some(Instant::now());
        self.duration_s = duration_s;
        self.primes_found.store(0, Ordering::Relaxed);
        // Spawn per-CPU threads
        for _ in 0..num_cpus() {
            self.threads.push(thread::spawn(|| {
                // ... sieve
            }));
        }
    }
    pub fn stop(&mut self) {
        self.running = false;
        for h in self.threads.drain(..) {
            let _ = h.join();
        }
    }
}

Display in header line 3:

Bench: 30s prime sieve (12.3s elapsed, 87,234 primes, 24 threads)

When active, color the bench number red (for emphasis). When finished, show a final score flash status for 5 seconds.


19. Pattern: Hit-Testing for Mouse Support

For mouse support to work cleanly, we need a function that maps a (x, y) coordinate to a PanelId:

// mouse.rs (or in render.rs)
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PanelId {
    Header,
    Table,
    Controls,
    StatusBar,
}

pub fn hit_test(area: Rect, x: u16, y: u16, layout: &LayoutDims) -> Option<PanelId> {
    let within = |r: Rect| x >= r.x && x < r.x + r.width && y >= r.y && y < r.y + r.height;
    if within(layout.header) { return Some(PanelId::Header); }
    if within(layout.table) { return Some(PanelId::Table); }
    if within(layout.controls) { return Some(PanelId::Controls); }
    if within(layout.status) { return Some(PanelId::StatusBar); }
    None
}

pub struct LayoutDims {
    pub header: Rect,
    pub table: Rect,
    pub controls: Rect,
    pub status: Rect,
}

This pairs with the destructuring layout pattern (§5) — build the LayoutDims once per render, use it both for rendering (passing Rect to each panel) and for mouse hit-testing.


20. Migration Notes

From v0.6 → v1.0 (Phase A complete)

cd local/recipes/system/redbear-power
# No new dependencies — pure refactor
cargo build --release

From v1.0 → v2.0 (Phase B+C complete)

cd local/recipes/system/redbear-power
# Add new dependencies in source/Cargo.toml:
#   serde = { version = "1", features = ["derive"] }
#   toml = "0.8"
#   dirs = "5"
#   zbus = { version = "4", features = ["async-io"] }  # for D-Bus export (Phase D)
cargo update
cargo build --release

ISO rebuild

unset REDBEAR_RELEASE
export REDBEAR_ALLOW_PROTECTED_FETCH=1
./local/scripts/build-redbear.sh redbear-mini

Backward compatibility

All new features are opt-in:

  • Existing keybindings unchanged
  • New keys (:, b, B, Tab→1/2/3) have no conflict with existing controls
  • New header lines appear only if data is available (feature-detected)
  • Configuration file is fully optional (defaults match v0.6)

21. Risk Assessment

Change Risk Mitigation
R1 (PROCHOT pulse fix) None — pure timing change Test on hardware with active PROCHOT
R2 (Stylize shorthand) Cosmetic only Visual diff
R3 (decoupled poll) Could increase CPU usage slightly Set INPUT_POLL_MS = 50 (20 Hz, well within budget)
R4 (Rect::centered) None Visual diff
R5 (duplicate comment) None Trivial
R6 (layout destructure) Low — compile-time check protects Compile-test
Theme constants (O2) None Cosmetic
Multi-vendor cpuid (C1, C8) Low — fallback to existing path Test on non-x86
Package thermal full (C2) Low — new struct field Visual diff
SIMD display (C3) Low — read-only at startup Unit test cpuid parsing
Cache hierarchy (C5) Low — read-only at startup Unit test
Hybrid CPU (C4) Medium — Intel 12th+ only, AMD CCD similar Fall back to flat list
Dynamic refresh (C7) Low — input validation Min/max check
Mouse (O1) Medium — termion mouse support is finicky on terminals Test in QEMU + bare metal
Config file (O2) Low — optional, defaults safe Validate TOML
D-Bus (O3) High — depends on redbear-sessiond being up Make opt-in via --dbus flag
Benchmark (C6) Medium — long-running, could leave zombie threads Ensure stop() joins all

22. References

ratatui 0.30.2 audit

cpu-x v4.7 reference

  • Repository: https://github.com/X0rg/CPU-X
  • Local clone: /tmp/cpu-x-src/
  • Architecture: CMake + C++17
  • Modules:
    • data.{hpp,cpp} (CPU/mobo/memory/graphics/bench data model) — /tmp/cpu-x-src/src/data.hpp
    • core/libsystem.cpp (uptime/memory from libprocps) — /tmp/cpu-x-src/src/core/libsystem.cpp
    • core/libpci.cpp (PCI device scanning + GPU hwmon) — /tmp/cpu-x-src/src/core/libpci.cpp
    • core/libcpuid.cpp (vendor/family/model/features) — /tmp/cpu-x-src/src/core/libcpuid.cpp
    • core/benchmarks.cpp (prime-sieve stress test) — /tmp/cpu-x-src/src/core/benchmarks.cpp
    • ui/ncurses.cpp (ncurses TUI) — /tmp/cpu-x-src/src/ui/ncurses.cpp
    • ui/gtk.cpp (GTK GUI) — /tmp/cpu-x-src/src/ui/gtk.cpp

redbear-power current state

  • Source: local/recipes/system/redbear-power/source/src/
    • main.rs — event loop, key dispatch, render orchestration
    • app.rsApp, CpuRow, Governor, ThrottleMode
    • render.rsrender_header, render_cpu_table, render_controls, render_prochot_alert, snapshot, buffer_to_string
    • acpi.rs — CPU enumeration, ACPI _PSS reading, CPUID, load calculation
    • cpufreq.rs — governor state read/write
    • msr.rs — MSR address constants and read/write helpers
  • Recipe: local/recipes/system/redbear-power/recipe.toml
  • Config inclusion: config/redbear-mini.toml:56, config/redbear-full.toml:137
  • Catalog entry: local/recipes/AGENTS.md (system section)
  • Top-level crates: AGENTS.md (item 8)

23. Decision Time

This plan is comprehensive. Before implementation, the user must decide:

  1. Phase scope: All of Phase A (immediate), Phase B (quality), Phase C (features)?
  2. Phase D deferral: D-Bus export and Stress Benchmark — implement now or wait for desktop stack?
  3. Mouse support priority: Tier 4 — defer to after Phase C? Or ship with Phase B?
  4. Config file format: TOML (matches Redox convention) or INI (simpler)?

The recommendation is:

  • Approve Phase A immediately — bug fixes are non-controversial.
  • Approve Phase B in next session — quality work, no risk.
  • Phase C — implement C1, C2, C3, C5 first (data-layer features, no UX change). Defer C4, C6, C7, C8.
  • Phase D — defer until desktop stack is operational (Q3 2026).

User's call.

24. Status Update — All Phases Implemented (2026-06-20)

Per the user's "go on, implement comprehensively" directive, all four phases (A → D, including previously-deferred items) have been implemented.

Delivered

Item Phase Status
R1: PROCHOT pulse bug A
R5: Duplicate comment A
C2: Package thermal full readout A
R3: Decoupled input poll B
R4: Rect::centered B
R6: Layout destructuring B
O2: Theme constants B
C9: Stylize shorthand B
C1, C8: Multi-vendor CPUID C
C3: SIMD display C
C5: Cache hierarchy C
C7: Dynamic refresh interval C
C6: Prime-sieve benchmark C
C4: Hybrid CPU detection D
O1: Mouse support D
O3: D-Bus export D

Implementation order (chronological)

  1. Phase A (2026-06-20 morning): bug fixes — PROCHOT pulse, duplicate comment, package thermal full readout (PL1/PL2/CRIT/TT1/TT2/HFI).
  2. Phase B (2026-06-20 morning): quality — theme.rs module, Stylize shorthand, Rect::centered, layout destructuring, decoupled input poll.
  3. Phase C (2026-06-20 late morning): features — cpuid.rs module (vendor/family/model/SIMD/cache), bench.rs module (prime-sieve benchmark), dynamic refresh interval.
  4. Phase D remaining (2026-06-20 noon):
    • cpuid.rs extended with CoreType enum + HybridInfo struct (Intel leaf 0x1A + AMD leaf 0x8000001E).
    • main.rs updated to use MouseTerminal and handle MouseEvent.
    • New dbus.rs module using zbus = "5" + tokio = "1" (opt-in via --dbus flag).

Final state

  • Source: 2376 lines across 10 modules (local/recipes/system/redbear-power/source/src/)
  • Cross-compile: 2.8 MB stripped Redox ELF binary
  • Build: cook redbear-power - successful (sha256 1b6f9db6...)
  • Smoke test: --once renders all features; --dbus registers on session bus
  • ISO rebuild: blocked by pre-existing upstream uutils/nix-0.30.1 vs Redox relibc incompatibility (out of scope; documented in local/docs/CONSOLE-TO-KDE-DESKTOP-PLAN.md §3.3.2 v1.1)

Remaining work (post-v1.1)

  • Fix uutils/nix-0.30.1 incompatibility so the redbear-mini ISO rebuild can complete (separate issue).
  • AMD Zen CCD topology via leaf 0x80000026 — currently AMD reports Unknown core type (work deferred; Zen 4+ only).
  • D-Bus methods beyond properties (e.g. cycle_governor() method invocation) — currently the TUI receives the keystroke; D-Bus clients read state but cannot mutate.
  • Config file (TOML at /etc/redbear-power.toml + ~/.config/redbear-power.toml) — still deferred.
  • Mouse-driven header/controls sub-panel navigation — currently left-click on header toggles throttle, on controls cycles governor (single-action per panel).

See Also

  • local/docs/RATATUI-APP-PATTERNS.md §13 — the canonical ratatui 0.30 best-practices update that this plan is derived from. Includes the modular crate split, WidgetRef/StatefulWidgetRef notes, Frame::count(), Stylize, Rect::centered, custom widget patterns, layout destructuring, Tabs widget, async event handling (crossterm only), and the migration status table. Use this as the implementation guide while this doc is the roadmap.
  • local/docs/CONSOLE-TO-KDE-DESKTOP-PLAN.md — the desktop stack plan that Phase D (D-Bus export) depends on.
  • local/recipes/system/redbear-power/ — the source code under analysis/improvement.
  • local/recipes/system/redbear-power/source/src/render.rs:118-140 — the PROCHOT pulse bug location (R1, immediate fix).
  • https://github.com/X0rg/CPU-X — cpu-x v4.7 reference (cloned at /tmp/cpu-x-src/ for this audit).