system settings
git clone https://git.lucas.co/cce-system-interface.git
Implement Processors page and adopt clear-ui Section layout across pages
- Add new Processors page to display CPU model, usage, cores, and GPU info
- Update existing pages (Audio, Display, Input, Layout, Network, Power, Status, Storage, System Info) to use the new Section layout helper from clear-ui
- Integrate Spinbox widgets for volume control in Audio page
- Ignore .antigravitycli directory in .gitignore
.gitignore | 1 +
src/app.rs | 4 +
src/main.rs | 214 +++++++++++++++++++++++++++++++++++++++--------
src/pages/audio.rs | 127 ++++++++++++++--------------
src/pages/display.rs | 116 ++++++++++++-------------
src/pages/input.rs | 122 ++++++++++++---------------
src/pages/layout.rs | 51 +++++------
src/pages/mod.rs | 6 +-
src/pages/network.rs | 56 +++++++------
src/pages/power.rs | 77 +++++++++--------
src/pages/processors.rs | 89 ++++++++++++++++++++
src/pages/status.rs | 30 ++++---
src/pages/storage.rs | 39 +++++----
src/pages/system_info.rs | 81 ++----------------
14 files changed, 590 insertions(+), 423 deletions(-)
diff --git a/.gitignore b/.gitignore
index c4c6b78..d193af6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@ target/
*.swo
*~
.DS_Store
+.antigravitycli/
diff --git a/src/app.rs b/src/app.rs
index 9c3774d..4e98176 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -6,6 +6,7 @@ use crate::pages::input;
use crate::pages::layout;
use crate::pages::network;
use crate::pages::power;
+use crate::pages::processors;
use crate::pages::status;
use crate::pages::storage;
use crate::pages::system_info;
@@ -19,6 +20,7 @@ pub struct AppState {
pub network: network::NetworkState,
pub layout: layout::LayoutState,
pub input: input::InputState,
+ pub processors: processors::ProcessorsState,
pub system_info: system_info::SystemState,
pub status: status::StatusState,
pub storage: storage::StorageState,
@@ -34,6 +36,7 @@ impl Default for AppState {
network: network::NetworkState::default(),
layout: layout::LayoutState::default(),
input: input::InputState::default(),
+ processors: processors::ProcessorsState::default(),
system_info: system_info::SystemState::default(),
status: status::StatusState::default(),
storage: storage::StorageState::default(),
@@ -49,6 +52,7 @@ pub enum AppAction {
Radios(network::NetworkMessage),
Layout(layout::LayoutMessage),
Input(input::InputMessage),
+ Processors(processors::ProcessorsMessage),
SystemInfo(system_info::SystemMessage),
Status(status::StatusMessage),
Storage(storage::StorageMessage),
diff --git a/src/main.rs b/src/main.rs
index 47c2239..ceb52e1 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -6,7 +6,7 @@ use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
use winit::window::{Window, WindowAttributes};
use clear_ui::color;
-use clear_ui::widget::Widget;
+use clear_ui::widget::{Spinbox, Widget};
use glyphon::{
Attrs, Buffer, Cache, FontSystem, Metrics, Resolution, SwashCache, TextArea, TextAtlas,
TextBounds, TextRenderer, Viewport,
@@ -81,7 +81,7 @@ struct TextItem {
color: glyphon::Color,
}
-enum ColorPickerAction {
+enum ColorSelectorAction {
Background([u8; 3]),
Border([u8; 3]),
}
@@ -121,11 +121,12 @@ struct SystemInterface {
rx_network: std::sync::mpsc::Receiver<pages::network::NetworkState>,
rx_layout: std::sync::mpsc::Receiver<pages::layout::LayoutState>,
rx_input: std::sync::mpsc::Receiver<pages::input::InputState>,
+ rx_processors: std::sync::mpsc::Receiver<pages::processors::ProcessorsState>,
rx_system: std::sync::mpsc::Receiver<pages::system_info::SystemState>,
rx_status: std::sync::mpsc::Receiver<pages::status::StatusState>,
rx_storage: std::sync::mpsc::Receiver<pages::storage::StorageState>,
- tx_color_picker: std::sync::mpsc::Sender<ColorPickerAction>,
- rx_color_picker: std::sync::mpsc::Receiver<ColorPickerAction>,
+ tx_color_selector: std::sync::mpsc::Sender<ColorSelectorAction>,
+ rx_color_selector: std::sync::mpsc::Receiver<ColorSelectorAction>,
scale_factor: f64,
width: u32,
@@ -218,7 +219,7 @@ impl SystemInterface {
});
// ── Initial state (fetch all concurrently) ──
- let (power, audio, display, network, system_info, status, storage) = tokio::join!(
+ let (power, audio, display, network, system_info, status, storage, processors) = tokio::join!(
pages::power::fetch_power_state(),
pages::audio::fetch_audio_state(),
pages::display::fetch_display_state(),
@@ -226,19 +227,24 @@ impl SystemInterface {
pages::system_info::fetch_system_state(),
pages::status::fetch_status_state(),
pages::storage::fetch_storage_state(),
+ pages::processors::fetch_processors_state(),
);
- let app = AppState {
+ let mut app = AppState {
power,
audio,
display,
network,
layout: pages::layout::read_layout_config(),
input: pages::input::read_input_config(),
+ processors,
system_info,
status,
storage,
current_page: Page::ALL[0],
};
+ // Init spinbox vectors to match fetched sinks/sources
+ app.audio.sink_spinboxes.resize_with(app.audio.sinks.len(), || Spinbox::new(50, 0, 100, 1));
+ app.audio.source_spinboxes.resize_with(app.audio.sources.len(), || Spinbox::new(50, 0, 100, 1));
// ── Background refresh channels ──
fn spawn_bg<T, F>(period_secs: u64, f: fn() -> F) -> std::sync::mpsc::Receiver<T>
@@ -284,10 +290,11 @@ impl SystemInterface {
rx
};
let rx_system = spawn_bg(5, || pages::system_info::fetch_system_state());
+ let rx_processors = spawn_bg(3, || pages::processors::fetch_processors_state());
let rx_status = spawn_bg(10, || pages::status::fetch_status_state());
let rx_storage = spawn_bg(10, || pages::storage::fetch_storage_state());
- let (tx_color_picker, rx_color_picker) = std::sync::mpsc::channel();
+ let (tx_color_selector, rx_color_selector) = std::sync::mpsc::channel();
let scale_factor = (window.scale_factor() as f32).max(2.0) as f64;
let mut this = Self {
window, surface, device, queue, config, render_pipeline,
@@ -299,8 +306,8 @@ impl SystemInterface {
cursor_x: 0.0, cursor_y: 0.0,
scale_factor,
rx_power, rx_audio, rx_display, rx_network, rx_layout, rx_input,
- rx_system, rx_status, rx_storage,
- tx_color_picker, rx_color_picker,
+ rx_processors, rx_system, rx_status, rx_storage,
+ tx_color_selector, rx_color_selector,
width: size.width, height: size.height,
needs_rebuild: true,
};
@@ -405,9 +412,12 @@ impl SystemInterface {
hovering: false,
kind: WidgetKind::ActionButton(btn.action.clone()),
});
+ let buf = make_text_buffer(&mut self.font_system, &btn.label, btn.label_size * s);
+ let tw = buf.layout_runs().next().map(|r| r.line_w).unwrap_or(0.0);
+ let lh = btn.label_size * s * 1.4;
text_items.push(TextItem {
- buffer: make_text_buffer(&mut self.font_system, &btn.label, btn.label_size * s),
- x: btn.x * s + 10.0 * s, y: btn.y * s + 8.0 * s,
+ buffer: buf,
+ x: btn.x * s + (btn.w * s - tw) / 2.0, y: btn.y * s + (btn.h * s - lh) / 2.0,
color: glyphon::Color::rgb(
(btn.label_color[0] * 255.0) as u8,
(btn.label_color[1] * 255.0) as u8,
@@ -436,11 +446,12 @@ impl SystemInterface {
use pages::*;
match self.app.current_page {
Page::Power => power::view(&self.app.power, cx, cy, cw, ch),
- Page::Audio => audio::view(&self.app.audio, cx, cy, cw, ch),
- Page::Display => display::view(&self.app.display, cx, cy, cw, ch),
+ Page::Audio => audio::view(&mut self.app.audio, cx, cy, cw, ch),
+ Page::Display => display::view(&mut self.app.display, cx, cy, cw, ch),
Page::Radios => network::view(&self.app.network, cx, cy, cw, ch),
Page::Layout => layout::view(&mut self.app.layout, cx, cy, cw, ch),
- Page::Input => input::view(&self.app.input, cx, cy, cw, ch),
+ Page::Processors => processors::view(&self.app.processors, cx, cy, cw, ch),
+ Page::Input => input::view(&mut self.app.input, cx, cy, cw, ch),
Page::System => system_info::view(&self.app.system_info, cx, cy, cw, ch),
Page::Status => status::view(&self.app.status, cx, cy, cw, ch),
Page::Storage => storage::view(&self.app.storage, cx, cy, cw, ch),
@@ -534,6 +545,10 @@ impl SystemInterface {
system_info::update(&mut self.app.system_info, system_info::SystemMessage::Refreshed(s));
self.needs_rebuild = true;
}
+ while let Ok(s) = self.rx_processors.try_recv() {
+ processors::update(&mut self.app.processors, processors::ProcessorsMessage::Refreshed(s));
+ self.needs_rebuild = true;
+ }
while let Ok(s) = self.rx_status.try_recv() {
status::update(&mut self.app.status, status::StatusMessage::Refreshed(s));
self.needs_rebuild = true;
@@ -542,12 +557,12 @@ impl SystemInterface {
storage::update(&mut self.app.storage, storage::StorageMessage::Refreshed(s));
self.needs_rebuild = true;
}
- while let Ok(action) = self.rx_color_picker.try_recv() {
+ while let Ok(action) = self.rx_color_selector.try_recv() {
match action {
- ColorPickerAction::Background(rgb) => {
+ ColorSelectorAction::Background(rgb) => {
layout::update(&mut self.app.layout, layout::LayoutMessage::SetBackground(rgb));
}
- ColorPickerAction::Border(rgb) => {
+ ColorSelectorAction::Border(rgb) => {
layout::update(&mut self.app.layout, layout::LayoutMessage::SetBorderColor(rgb));
}
}
@@ -561,7 +576,7 @@ impl SystemInterface {
AppAction::Layout(m) => match m {
layout::LayoutMessage::PickBackgroundColor => {
let color = self.app.layout.background_color;
- let tx = self.tx_color_picker.clone();
+ let tx = self.tx_color_selector.clone();
tokio::spawn(async move {
let hex = format!("#{:02X}{:02X}{:02X}", color[0], color[1], color[2]);
if let Ok(output) = tokio::process::Command::new("clear-colors").arg(&hex).output().await {
@@ -570,14 +585,14 @@ impl SystemInterface {
let r = u8::from_str_radix(&s[1..3], 16).unwrap_or(color[0]);
let g = u8::from_str_radix(&s[3..5], 16).unwrap_or(color[1]);
let b = u8::from_str_radix(&s[5..7], 16).unwrap_or(color[2]);
- let _ = tx.send(ColorPickerAction::Background([r, g, b]));
+ let _ = tx.send(ColorSelectorAction::Background([r, g, b]));
}
}
});
}
layout::LayoutMessage::PickBorderColor => {
let color = self.app.layout.border_color;
- let tx = self.tx_color_picker.clone();
+ let tx = self.tx_color_selector.clone();
tokio::spawn(async move {
let hex = format!("#{:02X}{:02X}{:02X}", color[0], color[1], color[2]);
if let Ok(output) = tokio::process::Command::new("clear-colors").arg(&hex).output().await {
@@ -586,7 +601,7 @@ impl SystemInterface {
let r = u8::from_str_radix(&s[1..3], 16).unwrap_or(color[0]);
let g = u8::from_str_radix(&s[3..5], 16).unwrap_or(color[1]);
let b = u8::from_str_radix(&s[5..7], 16).unwrap_or(color[2]);
- let _ = tx.send(ColorPickerAction::Border([r, g, b]));
+ let _ = tx.send(ColorSelectorAction::Border([r, g, b]));
}
}
});
@@ -599,6 +614,7 @@ impl SystemInterface {
AppAction::Radios(m) => network::update(&mut self.app.network, m.clone()),
AppAction::Input(m) => input::update(&mut self.app.input, m.clone()),
AppAction::SystemInfo(m) => system_info::update(&mut self.app.system_info, m.clone()),
+ AppAction::Processors(m) => processors::update(&mut self.app.processors, m.clone()),
AppAction::Status(m) => status::update(&mut self.app.status, m.clone()),
AppAction::Storage(m) => storage::update(&mut self.app.storage, m.clone()),
}
@@ -626,12 +642,43 @@ impl SystemInterface {
changed = true;
}
}
- for cp in &mut self.app.layout.color_pickers {
+ for cp in &mut self.app.layout.color_selectors {
if cp.cursor_moved(self.cursor_x / s, self.cursor_y / s) {
changed = true;
}
}
}
+ if self.app.current_page == Page::Input {
+ let s = self.scale_factor as f32;
+ if self.app.input.rate_spinbox.cursor_moved(self.cursor_x / s, self.cursor_y / s) {
+ changed = true;
+ }
+ if self.app.input.delay_spinbox.cursor_moved(self.cursor_x / s, self.cursor_y / s) {
+ changed = true;
+ }
+ if self.app.input.tap_toggle.cursor_moved(self.cursor_x / s, self.cursor_y / s) {
+ changed = true;
+ }
+ }
+ if self.app.current_page == Page::Audio {
+ let s = self.scale_factor as f32;
+ for sb in &mut self.app.audio.sink_spinboxes {
+ if sb.cursor_moved(self.cursor_x / s, self.cursor_y / s) {
+ changed = true;
+ }
+ }
+ for sb in &mut self.app.audio.source_spinboxes {
+ if sb.cursor_moved(self.cursor_x / s, self.cursor_y / s) {
+ changed = true;
+ }
+ }
+ }
+ if self.app.current_page == Page::Display {
+ let s = self.scale_factor as f32;
+ if self.app.display.brightness_spinbox.cursor_moved(self.cursor_x / s, self.cursor_y / s) {
+ changed = true;
+ }
+ }
if changed { self.needs_rebuild = true; }
changed
}
@@ -644,7 +691,7 @@ impl SystemInterface {
changed = true;
}
}
- for (i, cp) in self.app.layout.color_pickers.iter_mut().enumerate() {
+ for (i, cp) in self.app.layout.color_selectors.iter_mut().enumerate() {
let old = cp.color;
if cp.keyboard_input(event) {
if cp.color != old {
@@ -664,6 +711,59 @@ impl SystemInterface {
return true;
}
}
+ if self.app.current_page == Page::Input {
+ if self.app.input.rate_spinbox.keyboard_input(event) {
+ self.handle_action(&AppAction::Input(pages::input::InputMessage::ApplyRepeat));
+ self.needs_rebuild = true;
+ return true;
+ }
+ if self.app.input.delay_spinbox.keyboard_input(event) {
+ self.handle_action(&AppAction::Input(pages::input::InputMessage::ApplyRepeat));
+ self.needs_rebuild = true;
+ return true;
+ }
+ }
+ if self.app.current_page == Page::Audio {
+ let mut actions = Vec::new();
+ for (i, sb) in self.app.audio.sink_spinboxes.iter_mut().enumerate() {
+ let old = sb.value;
+ if sb.keyboard_input(event) {
+ if sb.value != old {
+ let id = self.app.audio.sinks[i].id;
+ actions.push(AppAction::Audio(pages::audio::AudioMessage::SinkVolume(id, sb.value as f32 / 100.0)));
+ }
+ }
+ }
+ for (i, sb) in self.app.audio.source_spinboxes.iter_mut().enumerate() {
+ let old = sb.value;
+ if sb.keyboard_input(event) {
+ if sb.value != old {
+ let id = self.app.audio.sources[i].id;
+ actions.push(AppAction::Audio(pages::audio::AudioMessage::SourceVolume(id, sb.value as f32 / 100.0)));
+ }
+ }
+ }
+ for a in &actions {
+ self.handle_action(a);
+ }
+ if !actions.is_empty() {
+ self.needs_rebuild = true;
+ return true;
+ }
+ }
+ if self.app.current_page == Page::Display {
+ let sb = &mut self.app.display.brightness_spinbox;
+ let old = sb.value;
+ if sb.keyboard_input(event) {
+ let new_val = sb.value;
+ drop(sb);
+ if new_val != old {
+ self.handle_action(&AppAction::Display(pages::display::DisplayMessage::BrightnessSet(new_val as u32)));
+ }
+ self.needs_rebuild = true;
+ return true;
+ }
+ }
false
}
WindowEvent::MouseInput { state, button, .. } => {
@@ -689,11 +789,11 @@ impl SystemInterface {
}
}
}
+ let s = self.scale_factor as f32;
+ let lx = self.cursor_x / s;
+ let ly = self.cursor_y / s;
+ let mut actions = Vec::new();
if *state == ElementState::Pressed && self.app.current_page == Page::Layout {
- let s = self.scale_factor as f32;
- let lx = self.cursor_x / s;
- let ly = self.cursor_y / s;
- let mut actions = Vec::new();
for (i, sb) in self.app.layout.spinboxes.iter_mut().enumerate() {
if !sb.hit_test(lx, ly) { sb.unfocus(); }
let old = sb.value;
@@ -706,7 +806,7 @@ impl SystemInterface {
));
}
}
- for (i, cp) in self.app.layout.color_pickers.iter_mut().enumerate() {
+ for (i, cp) in self.app.layout.color_selectors.iter_mut().enumerate() {
let old = cp.color;
if !cp.hit_test(lx, ly) { cp.unfocus(); }
cp.mouse_input(*button, *state, lx, ly);
@@ -723,14 +823,62 @@ impl SystemInterface {
}));
}
}
- for a in &actions {
- self.handle_action(a);
+ }
+ if *state == ElementState::Pressed && self.app.current_page == Page::Input {
+ let sb = &mut self.app.input.rate_spinbox;
+ if !sb.hit_test(lx, ly) { sb.unfocus(); }
+ let old = sb.value;
+ if sb.mouse_input(*button, *state, lx, ly) && sb.value != old {
+ actions.push(AppAction::Input(pages::input::InputMessage::ApplyRepeat));
}
- if !actions.is_empty() {
- self.needs_rebuild = true;
- return true;
+ let sb = &mut self.app.input.delay_spinbox;
+ if !sb.hit_test(lx, ly) { sb.unfocus(); }
+ let old = sb.value;
+ if sb.mouse_input(*button, *state, lx, ly) && sb.value != old {
+ actions.push(AppAction::Input(pages::input::InputMessage::ApplyRepeat));
+ }
+ }
+ if self.app.current_page == Page::Input {
+ let toggle = &mut self.app.input.tap_toggle;
+ toggle.mouse_input(*button, *state, lx, ly);
+ if toggle.take_click() {
+ actions.push(AppAction::Input(pages::input::InputMessage::ToggleTapToClick));
+ }
+ }
+ if *state == ElementState::Pressed && self.app.current_page == Page::Audio {
+ for (i, sb) in self.app.audio.sink_spinboxes.iter_mut().enumerate() {
+ if !sb.hit_test(lx, ly) { sb.unfocus(); }
+ let old = sb.value;
+ if sb.mouse_input(*button, *state, lx, ly) && sb.value != old {
+ let id = self.app.audio.sinks[i].id;
+ actions.push(AppAction::Audio(pages::audio::AudioMessage::SinkVolume(id, sb.value as f32 / 100.0)));
+ }
+ }
+ for (i, sb) in self.app.audio.source_spinboxes.iter_mut().enumerate() {
+ if !sb.hit_test(lx, ly) { sb.unfocus(); }
+ let old = sb.value;
+ if sb.mouse_input(*button, *state, lx, ly) && sb.value != old {
+ let id = self.app.audio.sources[i].id;
+ actions.push(AppAction::Audio(pages::audio::AudioMessage::SourceVolume(id, sb.value as f32 / 100.0)));
+ }
}
}
+ if *state == ElementState::Pressed && self.app.current_page == Page::Display {
+ let sb = &mut self.app.display.brightness_spinbox;
+ if !sb.hit_test(lx, ly) { sb.unfocus(); }
+ let old = sb.value;
+ if sb.mouse_input(*button, *state, lx, ly) && sb.value != old {
+ actions.push(AppAction::Display(pages::display::DisplayMessage::BrightnessSet(sb.value as u32)));
+ }
+ }
+ for a in &actions {
+ self.handle_action(a);
+ }
+ if !actions.is_empty() {
+ self.needs_rebuild = true;
+ return true;
+ }
+ self.needs_rebuild = true;
self.needs_rebuild = true;
true
}
diff --git a/src/pages/audio.rs b/src/pages/audio.rs
index 6608676..4af8283 100644
--- a/src/pages/audio.rs
+++ b/src/pages/audio.rs
@@ -1,4 +1,6 @@
use crate::app::{AppAction, PageContent};
+use clear_ui::layout::{render_widget, Section};
+use clear_ui::widget::Spinbox;
#[derive(Debug, Clone)]
pub struct AudioSink {
@@ -22,6 +24,8 @@ pub struct AudioSource {
pub struct AudioState {
pub sinks: Vec<AudioSink>,
pub sources: Vec<AudioSource>,
+ pub sink_spinboxes: Vec<Spinbox>,
+ pub source_spinboxes: Vec<Spinbox>,
}
#[derive(Debug, Clone)]
@@ -113,7 +117,7 @@ pub async fn fetch_audio_state() -> AudioState {
let connected = drm_connected_ports();
let sinks = fetch_sinks(&connected).await;
let sources = fetch_sources(&connected).await;
- AudioState { sinks, sources }
+ AudioState { sinks, sources, sink_spinboxes: Vec::new(), source_spinboxes: Vec::new() }
}
async fn fetch_sinks(connected_ports: &[String]) -> Vec<AudioSink> {
@@ -195,29 +199,26 @@ async fn fetch_sources(connected_ports: &[String]) -> Vec<AudioSource> {
const TEXT_FG: [f32; 4] = [0.83, 0.83, 0.83, 1.0];
const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
const MUTED_BG: [f32; 4] = [0.33, 0.20, 0.20, 1.0];
-const BTN_ACTIVE: [f32; 4] = [0.20, 0.40, 0.22, 1.0];
const BTN_INACTIVE: [f32; 4] = [0.13, 0.18, 0.14, 1.0];
const BTN_HOVER: [f32; 4] = [0.25, 0.30, 0.26, 1.0];
const BLANK_BAR: [f32; 4] = [0.15, 0.15, 0.24, 1.0];
const FILL_BAR: [f32; 4] = [0.30, 0.50, 0.32, 1.0];
-const SECTION_BORDER: [f32; 4] = [0.18, 0.18, 0.27, 1.0];
const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
const RED: [f32; 4] = [1.0, 0.33, 0.33, 1.0];
-pub fn view(state: &AudioState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
+pub fn view(state: &mut AudioState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
let mut pc = PageContent::new();
let mut y = cy + 12.0;
// ── Output section ──
- pc.text("Output", cx + 12.0, y, 14.0, TEXT_FG);
- y += 22.0;
+ let mut sec = Section::new(&mut pc, cx, y, cw, "Output");
if state.sinks.is_empty() {
- pc.text("No output devices found", cx + 12.0, y, 12.0, TEXT_DIM);
- y += 18.0;
+ sec.text(&mut pc, "No output devices found", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(18.0);
}
- for sink in &state.sinks {
+ for (idx, sink) in state.sinks.iter().enumerate() {
let label = if !sink.active {
format!("{} (inactive)", sink.name)
} else if sink.muted {
@@ -226,57 +227,49 @@ pub fn view(state: &AudioState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageCont
format!("{} {:.0}%", sink.name, sink.volume * 100.0)
};
let lc = if sink.muted { RED } else { TEXT_FG };
- pc.text(&label, cx + 14.0, y, 13.0, lc);
- y += 18.0;
+ sec.text(&mut pc, &label, 14.0, 0.0, 13.0, lc);
+ sec.spacing(18.0);
if sink.active {
- // Volume bar (visual only — we use +/- buttons)
let bar_w = cw - 100.0;
- let bar_x = cx + 14.0;
- pc.rect(BLANK_BAR, bar_x, y, bar_w, 8.0);
- pc.rect(FILL_BAR, bar_x, y, bar_w * sink.volume, 8.0);
- pc.text(&format!("{:.0}%", sink.volume * 100.0), bar_x + bar_w + 8.0, y - 2.0, 11.0, TEXT_DIM);
+ let bar_x = 14.0;
+ let yt = sec.ay();
+ pc.rect(BLANK_BAR, sec.ax(bar_x), yt, bar_w, 8.0);
+ pc.rect(FILL_BAR, sec.ax(bar_x), yt, bar_w * sink.volume, 8.0);
+ sec.text(&mut pc, &format!("{:.0}%", sink.volume * 100.0), bar_x + bar_w + 8.0, -2.0, 11.0, TEXT_DIM);
- // Buttons
- let btn_h = 28.0;
- let btn_y = y + 12.0;
- let mut bx = cx + 14.0;
+ let row_y = sec.ay() + 12.0;
+ let sb_w = 100.0;
+ let sb_h = 26.0;
+ let mute_w = 60.0;
+ let gap = 8.0;
- pc.button("-10", bx, btn_y, 36.0, btn_h,
- BTN_INACTIVE, BTN_HOVER, WHITE,
- AppAction::Audio(AudioMessage::SinkVolume(sink.id, (sink.volume - 0.10).max(0.0))));
- bx += 40.0;
+ state.sink_spinboxes[idx].value = (sink.volume * 100.0).round() as i32;
+ render_widget(&mut pc, &mut state.sink_spinboxes[idx], sec.ax(bar_x), row_y, sb_w, sb_h);
let mute_label = if sink.muted { "Unmute" } else { "Mute" };
- let mute_bg = if sink.muted { MUTED_BG } else { BTN_INACTIVE };
- pc.button(mute_label, bx, btn_y, 52.0, btn_h,
- mute_bg, BTN_HOVER, WHITE,
+ let mute_col = if sink.muted { MUTED_BG } else { BTN_INACTIVE };
+ pc.button(mute_label, sec.ax(bar_x) + sb_w + gap, row_y, mute_w, sb_h,
+ mute_col, BTN_HOVER, WHITE,
AppAction::Audio(AudioMessage::SinkMute(sink.id)));
- bx += 56.0;
- pc.button("+10", bx, btn_y, 36.0, btn_h,
- BTN_ACTIVE, BTN_HOVER, WHITE,
- AppAction::Audio(AudioMessage::SinkVolume(sink.id, (sink.volume + 0.10).min(1.0))));
-
- y = btn_y + btn_h + 6.0;
+ sec.content_y += 12.0 + sb_h + 6.0;
} else {
- y += 6.0;
+ sec.content_y += 6.0;
}
}
+ y = sec.finish(&mut pc);
+
// ── Input section ──
- y += 8.0;
- pc.rect(SECTION_BORDER, cx + 8.0, y, cw - 16.0, 1.0);
- y += 8.0;
- pc.text("Input", cx + 12.0, y, 14.0, TEXT_FG);
- y += 22.0;
+ let mut sec = Section::new(&mut pc, cx, y, cw, "Input");
if state.sources.is_empty() {
- pc.text("No input devices found", cx + 12.0, y, 12.0, TEXT_DIM);
- y += 18.0;
+ sec.text(&mut pc, "No input devices found", 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(18.0);
}
- for src in &state.sources {
+ for (idx, src) in state.sources.iter().enumerate() {
let label = if !src.active {
format!("{} (inactive)", src.name)
} else if src.muted {
@@ -285,48 +278,50 @@ pub fn view(state: &AudioState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageCont
format!("{} {:.0}%", src.name, src.volume * 100.0)
};
let lc = if src.muted { RED } else { TEXT_FG };
- pc.text(&label, cx + 14.0, y, 13.0, lc);
- y += 18.0;
+ sec.text(&mut pc, &label, 14.0, 0.0, 13.0, lc);
+ sec.spacing(18.0);
if src.active {
let bar_w = cw - 100.0;
- let bar_x = cx + 14.0;
- pc.rect(BLANK_BAR, bar_x, y, bar_w, 8.0);
- pc.rect(FILL_BAR, bar_x, y, bar_w * src.volume, 8.0);
- pc.text(&format!("{:.0}%", src.volume * 100.0), bar_x + bar_w + 8.0, y - 2.0, 11.0, TEXT_DIM);
+ let bar_x = 14.0;
+ let yt = sec.ay();
+ pc.rect(BLANK_BAR, sec.ax(bar_x), yt, bar_w, 8.0);
+ pc.rect(FILL_BAR, sec.ax(bar_x), yt, bar_w * src.volume, 8.0);
+ sec.text(&mut pc, &format!("{:.0}%", src.volume * 100.0), bar_x + bar_w + 8.0, -2.0, 11.0, TEXT_DIM);
- let btn_h = 28.0;
- let btn_y = y + 12.0;
- let mut bx = cx + 14.0;
+ let row_y = sec.ay() + 12.0;
+ let sb_w = 100.0;
+ let sb_h = 26.0;
+ let mute_w = 60.0;
+ let gap = 8.0;
- pc.button("-10", bx, btn_y, 36.0, btn_h,
- BTN_INACTIVE, BTN_HOVER, WHITE,
- AppAction::Audio(AudioMessage::SourceVolume(src.id, (src.volume - 0.10).max(0.0))));
- bx += 40.0;
+ state.source_spinboxes[idx].value = (src.volume * 100.0).round() as i32;
+ render_widget(&mut pc, &mut state.source_spinboxes[idx], sec.ax(bar_x), row_y, sb_w, sb_h);
let mute_label = if src.muted { "Unmute" } else { "Mute" };
- let mute_bg = if src.muted { MUTED_BG } else { BTN_INACTIVE };
- pc.button(mute_label, bx, btn_y, 52.0, btn_h,
- mute_bg, BTN_HOVER, WHITE,
+ let mute_col = if src.muted { MUTED_BG } else { BTN_INACTIVE };
+ pc.button(mute_label, sec.ax(bar_x) + sb_w + gap, row_y, mute_w, sb_h,
+ mute_col, BTN_HOVER, WHITE,
AppAction::Audio(AudioMessage::SourceMute(src.id)));
- bx += 56.0;
-
- pc.button("+10", bx, btn_y, 36.0, btn_h,
- BTN_ACTIVE, BTN_HOVER, WHITE,
- AppAction::Audio(AudioMessage::SourceVolume(src.id, (src.volume + 0.10).min(1.0))));
- y = btn_y + btn_h + 6.0;
+ sec.content_y += 12.0 + sb_h + 6.0;
} else {
- y += 6.0;
+ sec.content_y += 6.0;
}
}
+ sec.finish(&mut pc);
+
pc
}
pub fn update(state: &mut AudioState, msg: AudioMessage) {
match msg {
- AudioMessage::Refreshed(new) => { *state = new; }
+ AudioMessage::Refreshed(new) => {
+ *state = new;
+ state.sink_spinboxes.resize_with(state.sinks.len(), || Spinbox::new(50, 0, 100, 1));
+ state.source_spinboxes.resize_with(state.sources.len(), || Spinbox::new(50, 0, 100, 1));
+ }
AudioMessage::SinkVolume(id, vol) => {
if let Some(sink) = state.sinks.iter_mut().find(|s| s.id == id) {
sink.volume = vol;
diff --git a/src/pages/display.rs b/src/pages/display.rs
index a23cb0f..cf47cdd 100644
--- a/src/pages/display.rs
+++ b/src/pages/display.rs
@@ -1,4 +1,6 @@
-use crate::app::{AppAction, PageContent};
+use crate::app::PageContent;
+use clear_ui::layout::{render_widget, Section};
+use clear_ui::widget::Spinbox;
#[derive(Debug, Clone)]
pub struct DisplayOutput {
@@ -9,26 +11,44 @@ pub struct DisplayOutput {
pub connected: bool,
}
-#[derive(Debug, Clone, Default)]
+#[derive(Debug, Clone)]
pub struct DisplayState {
pub brightness: f32,
pub max_brightness: f32,
pub outputs: Vec<DisplayOutput>,
pub night_light: bool,
+ pub brightness_spinbox: Spinbox,
+}
+
+impl Default for DisplayState {
+ fn default() -> Self {
+ Self {
+ brightness: 0.0,
+ max_brightness: 0.0,
+ outputs: Vec::new(),
+ night_light: false,
+ brightness_spinbox: Spinbox::new(50, 0, 100, 5),
+ }
+ }
}
#[derive(Debug, Clone)]
pub enum DisplayMessage {
Refreshed(DisplayState),
- BrightnessDecrement,
- BrightnessIncrement,
+ BrightnessSet(u32),
}
pub async fn fetch_display_state() -> DisplayState {
let (brightness, max_brightness) = fetch_brightness().await;
let outputs = fetch_outputs().await;
let night_light = is_night_light_on().await;
- DisplayState { brightness, max_brightness, outputs, night_light }
+ let pct = if max_brightness > 0.0 {
+ (brightness / max_brightness * 100.0).round() as i32
+ } else { 50 };
+ DisplayState {
+ brightness, max_brightness, outputs, night_light,
+ brightness_spinbox: Spinbox::new(pct, 0, 100, 5),
+ }
}
async fn fetch_brightness() -> (f32, f32) {
@@ -98,56 +118,43 @@ fn spawn_brightness(pct: u32) {
const TEXT_FG: [f32; 4] = [0.83, 0.83, 0.83, 1.0];
const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
-const BTN_ACTIVE: [f32; 4] = [0.20, 0.40, 0.22, 1.0];
-const BTN_INACTIVE: [f32; 4] = [0.13, 0.18, 0.14, 1.0];
-const BTN_HOVER: [f32; 4] = [0.25, 0.30, 0.26, 1.0];
-const SECTION_BORDER: [f32; 4] = [0.18, 0.18, 0.27, 1.0];
const BLANK_BAR: [f32; 4] = [0.15, 0.15, 0.24, 1.0];
const FILL_BAR: [f32; 4] = [0.30, 0.50, 0.32, 1.0];
-const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
-pub fn view(state: &DisplayState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
+pub fn view(state: &mut DisplayState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
let mut pc = PageContent::new();
let mut y = cy + 12.0;
// ── Brightness ──
- pc.text("Brightness", cx + 12.0, y, 14.0, TEXT_FG);
- y += 22.0;
+ let mut sec = Section::new(&mut pc, cx, y, cw, "Brightness");
let bright_pct = if state.max_brightness > 0.0 {
- (state.brightness / state.max_brightness * 100.0) as u32
+ (state.brightness / state.max_brightness * 100.0).round() as i32
} else { 0 };
let bar_w = cw - 100.0;
- pc.rect(BLANK_BAR, cx + 12.0, y, bar_w, 8.0);
- pc.rect(FILL_BAR, cx + 12.0, y, bar_w * bright_pct as f32 / 100.0, 8.0);
- pc.text(&format!("{}%", bright_pct), cx + 16.0 + bar_w, y - 2.0, 11.0, TEXT_DIM);
- y += 14.0;
-
- let btn_h = 28.0;
- let btn_y = y;
- pc.button("-10", cx + 12.0, btn_y, 36.0, btn_h,
- BTN_INACTIVE, BTN_HOVER, WHITE,
- AppAction::Display(DisplayMessage::BrightnessDecrement));
- pc.text("brightnessctl set", cx + 56.0, btn_y + 8.0, 10.0, TEXT_DIM);
- pc.button("+10", cx + 12.0 + bar_w - 36.0, btn_y, 36.0, btn_h,
- BTN_ACTIVE, BTN_HOVER, WHITE,
- AppAction::Display(DisplayMessage::BrightnessIncrement));
-
- y = btn_y + btn_h + 12.0;
+ let yt = sec.ay();
+ pc.rect(BLANK_BAR, sec.ax(12.0), yt, bar_w, 8.0);
+ pc.rect(FILL_BAR, sec.ax(12.0), yt, bar_w * bright_pct as f32 / 100.0, 8.0);
+ pc.text(&format!("{}%", bright_pct), sec.ax(16.0 + bar_w), yt - 2.0, 11.0, TEXT_DIM);
+ sec.content_y += 14.0;
+
+ let yt = sec.ay();
+ let sb_w = 100.0;
+ let sb_h = 26.0;
+ state.brightness_spinbox.value = bright_pct;
+ render_widget(&mut pc, &mut state.brightness_spinbox, sec.ax(12.0), yt, sb_w, sb_h);
+ sec.content_y += sb_h + 12.0;
+ y = sec.finish(&mut pc);
// ── Night Light ──
- pc.rect(SECTION_BORDER, cx + 8.0, y, cw - 16.0, 1.0);
- y += 8.0;
+ let mut sec = Section::new(&mut pc, cx, y, cw, "Night Light");
let nl_label = if state.night_light { "Night Light: ON" } else { "Night Light: OFF" };
- pc.text(nl_label, cx + 12.0, y, 13.0, TEXT_FG);
- y += 24.0;
+ sec.text(&mut pc, nl_label, 12.0, 0.0, 13.0, TEXT_FG);
+ y = sec.finish(&mut pc);
// ── Outputs ──
- pc.rect(SECTION_BORDER, cx + 8.0, y, cw - 16.0, 1.0);
- y += 8.0;
- pc.text("Outputs", cx + 12.0, y, 14.0, TEXT_FG);
- y += 22.0;
+ let mut sec = Section::new(&mut pc, cx, y, cw, "Outputs");
for out in &state.outputs {
if out.connected {
@@ -158,13 +165,14 @@ pub fn view(state: &DisplayState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageCo
} else { format!(" scale {:.0}x", out.scale) }
} else { String::new() }
} else { String::new() };
- pc.text(&format!("{} {} @ {}Hz{}", out.name, out.resolution, out.refresh, scale_info),
- cx + 14.0, y, 12.0, TEXT_FG);
+ sec.text(&mut pc, &format!("{} {} @ {}Hz{}", out.name, out.resolution, out.refresh, scale_info),
+ 14.0, 0.0, 12.0, TEXT_FG);
} else {
- pc.text(&format!("{} (disconnected)", out.name), cx + 14.0, y, 12.0, TEXT_DIM);
+ sec.text(&mut pc, &format!("{} (disconnected)", out.name), 14.0, 0.0, 12.0, TEXT_DIM);
}
- y += 18.0;
+ sec.spacing(18.0);
}
+ sec.finish(&mut pc);
pc
}
@@ -172,25 +180,11 @@ pub fn view(state: &DisplayState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageCo
pub fn update(state: &mut DisplayState, msg: DisplayMessage) {
match msg {
DisplayMessage::Refreshed(new) => { *state = new; }
- DisplayMessage::BrightnessDecrement => {
- let pct = if state.max_brightness > 0.0 {
- (state.brightness / state.max_brightness * 100.0) as u32
- } else { 0 };
- if pct > 0 {
- let new_pct = pct.saturating_sub(10).max(0);
- state.brightness = new_pct as f32 / 100.0 * state.max_brightness;
- spawn_brightness(new_pct);
- }
- }
- DisplayMessage::BrightnessIncrement => {
- let pct = if state.max_brightness > 0.0 {
- (state.brightness / state.max_brightness * 100.0) as u32
- } else { 0 };
- if pct < 100 {
- let new_pct = (pct + 10).min(100);
- state.brightness = new_pct as f32 / 100.0 * state.max_brightness;
- spawn_brightness(new_pct);
- }
+ DisplayMessage::BrightnessSet(pct) => {
+ let pct = pct.clamp(0, 100);
+ state.brightness = pct as f32 / 100.0 * state.max_brightness;
+ spawn_brightness(pct);
+ state.brightness_spinbox.value = pct as i32;
}
}
}
diff --git a/src/pages/input.rs b/src/pages/input.rs
index dafef86..116c73d 100644
--- a/src/pages/input.rs
+++ b/src/pages/input.rs
@@ -2,6 +2,8 @@ use std::fs;
use std::io::Write;
use crate::app::{AppAction, PageContent};
+use clear_ui::layout::{render_widget, Section};
+use clear_ui::widget::{Spinbox, Toggle};
const CONFIG_PATH: &str = "/home/lsgalante/.config/clearwm/config.toml";
const CLEARWM_SOCK: &str = "/tmp/clearwm.sock";
@@ -19,31 +21,45 @@ pub struct InputState {
pub tap_to_click: bool,
pub repeat_rate: u16,
pub repeat_delay: u16,
+ pub rate_spinbox: Spinbox,
+ pub delay_spinbox: Spinbox,
+ pub tap_toggle: Toggle,
pub keybinds: Vec<Keybind>,
}
impl Default for InputState {
fn default() -> Self {
- Self { tap_to_click: false, repeat_rate: 50, repeat_delay: 300, keybinds: Vec::new() }
+ Self {
+ tap_to_click: false,
+ repeat_rate: 50,
+ repeat_delay: 300,
+ rate_spinbox: Spinbox::new(50, 1, 100, 1).with_label("Repeat Rate"),
+ delay_spinbox: Spinbox::new(300, 100, 2000, 10).with_label("Repeat Delay"),
+ tap_toggle: Toggle::new().with_label("Tap to Click"),
+ keybinds: Vec::new(),
+ }
}
}
#[derive(Debug, Clone)]
pub enum InputMessage {
ToggleTapToClick,
- RepeatRateDown,
- RepeatRateUp,
- RepeatDelayDown,
- RepeatDelayUp,
+ ApplyRepeat,
Refreshed(InputState),
}
pub fn read_input_config() -> InputState {
let content = fs::read_to_string(CONFIG_PATH).unwrap_or_default();
+ let rate = parse_u16_key(&content, "rate", 50);
+ let delay = parse_u16_key(&content, "delay", 300);
+ let tap = parse_bool_from(&content, "tap_to_click");
InputState {
- tap_to_click: parse_bool_from(&content, "tap_to_click"),
- repeat_rate: parse_u16_key(&content, "rate", 50),
- repeat_delay: parse_u16_key(&content, "delay", 300),
+ tap_to_click: tap,
+ repeat_rate: rate,
+ repeat_delay: delay,
+ rate_spinbox: Spinbox::new(rate as i32, 1, 100, 1).with_label("Repeat Rate"),
+ delay_spinbox: Spinbox::new(delay as i32, 100, 2000, 10).with_label("Repeat Delay"),
+ tap_toggle: Toggle::new().with_label("Tap to Click"),
keybinds: parse_keybinds(&content),
}
}
@@ -147,62 +163,38 @@ fn apply_repeat_config(rate: u16, delay: u16) {
const TEXT_FG: [f32; 4] = [0.83, 0.83, 0.83, 1.0];
const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
const ACCENT: [f32; 4] = [0.36, 0.56, 0.38, 1.0];
-const BTN_ACTIVE: [f32; 4] = [0.20, 0.40, 0.22, 1.0];
-const BTN_INACTIVE: [f32; 4] = [0.13, 0.18, 0.14, 1.0];
const BTN_HOVER: [f32; 4] = [0.25, 0.30, 0.26, 1.0];
const TOGGLE_ON: [f32; 4] = [0.16, 0.41, 0.18, 1.0];
const TOGGLE_OFF: [f32; 4] = [0.16, 0.16, 0.24, 1.0];
-const SECTION_BORDER: [f32; 4] = [0.18, 0.18, 0.27, 1.0];
const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
-pub fn view(state: &InputState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
+pub fn view(state: &mut InputState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
let mut pc = PageContent::new();
let mut y = cy + 12.0;
// ── Touchpad ──
- pc.text("Touchpad", cx + 12.0, y, 14.0, TEXT_FG);
- y += 22.0;
-
- let tap_bg = if state.tap_to_click { TOGGLE_ON } else { TOGGLE_OFF };
- let tap_label = if state.tap_to_click { "Tap to Click: ON" } else { "Tap to Click: OFF" };
- pc.text(tap_label, cx + 14.0, y + 6.0, 13.0, if state.tap_to_click { ACCENT } else { TEXT_DIM });
- let btn_w = (cw - 32.0).min(100.0);
- pc.button(if state.tap_to_click { "ON" } else { "OFF" }, cx + cw - btn_w - 14.0, y, btn_w, 28.0,
- tap_bg, BTN_HOVER, WHITE,
- AppAction::Input(InputMessage::ToggleTapToClick));
- y += 36.0;
+ let mut sec = Section::new(&mut pc, cx, y, cw, "Touchpad");
+
+ let yt = sec.ay();
+ let toggle_w = 48.0;
+ let toggle_h = 24.0;
+ state.tap_toggle.set_toggled(state.tap_to_click);
+ render_widget(&mut pc, &mut state.tap_toggle, sec.ax(100.0), yt, toggle_w, toggle_h);
+ sec.content_y += toggle_h + 12.0;
+ y = sec.finish(&mut pc);
// ── Keyboard ──
- pc.rect(SECTION_BORDER, cx + 8.0, y, cw - 16.0, 1.0);
- y += 8.0;
- pc.text("Keyboard", cx + 12.0, y, 14.0, TEXT_FG);
- y += 22.0;
-
- // Repeat Rate
- pc.text(&format!("Repeat Rate: {} /sec", state.repeat_rate), cx + 14.0, y, 12.0, TEXT_DIM);
- y += 18.0;
- pc.button("-1", cx + 14.0, y, 36.0, 28.0, BTN_INACTIVE, BTN_HOVER, WHITE,
- AppAction::Input(InputMessage::RepeatRateDown));
- pc.text(&format!(" {} ", state.repeat_rate), cx + 58.0, y + 7.0, 13.0, TEXT_FG);
- pc.button("+1", cx + 14.0 + 36.0 + 8.0, y, 36.0, 28.0, BTN_ACTIVE, BTN_HOVER, WHITE,
- AppAction::Input(InputMessage::RepeatRateUp));
- y += 34.0;
-
- // Repeat Delay
- pc.text(&format!("Repeat Delay: {}ms", state.repeat_delay), cx + 14.0, y, 12.0, TEXT_DIM);
- y += 18.0;
- pc.button("-10", cx + 14.0, y, 36.0, 28.0, BTN_INACTIVE, BTN_HOVER, WHITE,
- AppAction::Input(InputMessage::RepeatDelayDown));
- pc.text(&format!(" {}ms ", state.repeat_delay), cx + 58.0, y + 7.0, 13.0, TEXT_FG);
- pc.button("+10", cx + 14.0 + 36.0 + 8.0, y, 36.0, 28.0, BTN_ACTIVE, BTN_HOVER, WHITE,
- AppAction::Input(InputMessage::RepeatDelayUp));
- y += 40.0;
+ let mut sec = Section::new(&mut pc, cx, y, cw, "Keyboard");
+
+ sec.widget(&mut pc, &mut state.rate_spinbox, 14.0, 200.0, 26.0);
+ sec.spacing(8.0);
+
+ sec.widget(&mut pc, &mut state.delay_spinbox, 14.0, 200.0, 26.0);
+ sec.spacing(8.0);
+ y = sec.finish(&mut pc);
// ── Keybindings ──
- pc.rect(SECTION_BORDER, cx + 8.0, y, cw - 16.0, 1.0);
- y += 8.0;
- pc.text("Keyboard Bindings", cx + 12.0, y, 14.0, TEXT_FG);
- y += 22.0;
+ let mut sec = Section::new(&mut pc, cx, y, cw, "Keyboard Bindings");
for kb in &state.keybinds {
let binding = if kb.mods.is_empty() {
@@ -215,11 +207,12 @@ pub fn view(state: &InputState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageCont
} else {
format!("{}: {}", kb.action, kb.command)
};
- pc.text(&binding, cx + 14.0, y, 12.0, TEXT_FG);
+ sec.text(&mut pc, &binding, 14.0, 0.0, 12.0, TEXT_FG);
let label_w = cw - 200.0;
- pc.text(&action_label, cx + 14.0 + label_w.min(180.0), y, 12.0, TEXT_DIM);
- y += 18.0;
+ sec.text(&mut pc, &action_label, 14.0 + label_w.min(180.0), 0.0, 12.0, TEXT_DIM);
+ sec.spacing(18.0);
}
+ sec.finish(&mut pc);
pc
}
@@ -230,21 +223,12 @@ pub fn update(state: &mut InputState, msg: InputMessage) {
state.tap_to_click = !state.tap_to_click;
write_tap_to_click(state.tap_to_click);
}
- InputMessage::RepeatRateDown => {
- if state.repeat_rate > 1 { state.repeat_rate -= 1; }
- apply_repeat_config(state.repeat_rate, state.repeat_delay);
- }
- InputMessage::RepeatRateUp => {
- if state.repeat_rate < 100 { state.repeat_rate += 1; }
- apply_repeat_config(state.repeat_rate, state.repeat_delay);
- }
- InputMessage::RepeatDelayDown => {
- if state.repeat_delay > 100 { state.repeat_delay -= 10; }
- apply_repeat_config(state.repeat_rate, state.repeat_delay);
- }
- InputMessage::RepeatDelayUp => {
- if state.repeat_delay < 2000 { state.repeat_delay += 10; }
- apply_repeat_config(state.repeat_rate, state.repeat_delay);
+ InputMessage::ApplyRepeat => {
+ let rate = state.rate_spinbox.value.max(1).min(100) as u16;
+ let delay = state.delay_spinbox.value.max(100).min(2000) as u16;
+ state.repeat_rate = rate;
+ state.repeat_delay = delay;
+ apply_repeat_config(rate, delay);
}
InputMessage::Refreshed(new) => { *state = new; }
}
diff --git a/src/pages/layout.rs b/src/pages/layout.rs
index e940f50..bf1c279 100644
--- a/src/pages/layout.rs
+++ b/src/pages/layout.rs
@@ -2,8 +2,8 @@ use std::fs;
use std::io::Write;
use crate::app::PageContent;
-use clear_ui::layout::Column;
-use clear_ui::widget::{ColorPicker, Spinbox};
+use clear_ui::layout::Section;
+use clear_ui::widget::{ColorSelector, Spinbox};
const CONFIG_PATH: &str = "/home/lsgalante/.config/clearwm/config.toml";
const CLEARWM_SOCK: &str = "/tmp/clearwm.sock";
@@ -63,7 +63,7 @@ pub struct LayoutState {
pub floating_border_width: u16,
pub color_options: Vec<(&'static str, [u8; 3])>,
pub spinboxes: Vec<Spinbox>,
- pub color_pickers: Vec<ColorPicker>,
+ pub color_selectors: Vec<ColorSelector>,
}
impl Default for LayoutState {
@@ -79,9 +79,9 @@ impl Default for LayoutState {
floating_border_width: 6,
color_options: preset_colors(),
spinboxes: make_spinboxes(0, 6, 6, 6, 6, 6),
- color_pickers: vec![
- ColorPicker::new([0x0a, 0x1a, 0x0e]).with_label("Desktop Background"),
- ColorPicker::new([0x3e, 0x3e, 0x3e]).with_label("Border Color"),
+ color_selectors: vec![
+ ColorSelector::new([0x0a, 0x1a, 0x0e]).with_label("Desktop Background"),
+ ColorSelector::new([0x3e, 0x3e, 0x3e]).with_label("Border Color"),
],
}
}
@@ -133,10 +133,10 @@ pub fn read_layout_config() -> LayoutState {
floating_border_width: fl,
color_options: preset_colors(),
spinboxes: make_spinboxes(fs, ca, g, v, h, fl),
- color_pickers: vec![
- ColorPicker::new(parse_color_from_key(&content, "background_color", [0x0a, 0x1a, 0x0e]))
+ color_selectors: vec![
+ ColorSelector::new(parse_color_from_key(&content, "background_color", [0x0a, 0x1a, 0x0e]))
.with_label("Desktop Background"),
- ColorPicker::new(parse_color_from_key(&content, "border_color", [0x3e, 0x3e, 0x3e]))
+ ColorSelector::new(parse_color_from_key(&content, "border_color", [0x3e, 0x3e, 0x3e]))
.with_label("Border Color"),
],
}
@@ -232,27 +232,28 @@ fn apply_all_widths(s: &LayoutState) {
w("floating_border_width", s.floating_border_width);
}
-const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
-
pub fn view(state: &mut LayoutState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
let mut pc = PageContent::new();
- let mut col = Column::new(&mut pc, cx, cy, 0.0, 28.0, cw);
+ let mut y = cy + 12.0;
+
+ let mut sec = Section::new(&mut pc, cx, y, cw, "Desktop Background");
+ state.color_selectors[0].color = state.background_color;
+ sec.widget(&mut pc, &mut state.color_selectors[0], 12.0, 220.0, 22.0);
+ y = sec.finish(&mut pc);
+
+ let mut sec = Section::new(&mut pc, cx, y, cw, "Border Color");
+ state.color_selectors[1].color = state.border_color;
+ sec.widget(&mut pc, &mut state.color_selectors[1], 12.0, 220.0, 22.0);
+ y = sec.finish(&mut pc);
- state.color_pickers[0].color = state.background_color;
- col.widget(&mut state.color_pickers[0], 12.0, 220.0, 22.0);
- col.spacing(16.0);
- col.separator();
- col.spacing(12.0);
- state.color_pickers[1].color = state.border_color;
- col.widget(&mut state.color_pickers[1], 12.0, 220.0, 22.0);
- col.separator();
- col.header("Border Width", 12.0);
+ let mut sec = Section::new(&mut pc, cx, y, cw, "Border Width");
+ sec.spacing(8.0);
for (i, param) in WidthParam::ALL.iter().enumerate() {
- col.row(30.0, |row| {
- row.text(param.label(), 14.0, 6.0, 12.0, TEXT_DIM);
- row.widget(&mut state.spinboxes[i], 110.0, 90.0, 26.0);
- });
+ state.spinboxes[i].set_label(param.label());
+ sec.widget(&mut pc, &mut state.spinboxes[i], 14.0, 200.0, 26.0);
+ sec.spacing(8.0);
}
+ sec.finish(&mut pc);
pc
}
diff --git a/src/pages/mod.rs b/src/pages/mod.rs
index 74d0cfd..bc0be2c 100644
--- a/src/pages/mod.rs
+++ b/src/pages/mod.rs
@@ -8,6 +8,7 @@ pub mod system_info;
pub mod keybindings;
pub mod input;
pub mod status;
+pub mod processors;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Page {
@@ -18,16 +19,18 @@ pub enum Page {
Display,
Layout,
System,
+ Processors,
Input,
Status,
}
impl Page {
- pub const ALL: [Page; 9] = [
+ pub const ALL: [Page; 10] = [
Page::Audio,
Page::Display,
Page::Input,
Page::Layout,
+ Page::Processors,
Page::Power,
Page::Radios,
Page::Status,
@@ -44,6 +47,7 @@ impl Page {
Page::Display => "Display",
Page::Layout => "Layout",
Page::System => "System",
+ Page::Processors => "Processors",
Page::Input => "Input",
Page::Status => "Status",
}
diff --git a/src/pages/network.rs b/src/pages/network.rs
index 54df5e3..8e8d31a 100644
--- a/src/pages/network.rs
+++ b/src/pages/network.rs
@@ -1,4 +1,5 @@
use crate::app::{AppAction, PageContent};
+use clear_ui::layout::Section;
#[derive(Debug, Clone)]
pub struct WifiNetwork {
@@ -200,7 +201,6 @@ const TOGGLE_OFF: [f32; 4] = [0.16, 0.16, 0.24, 1.0];
const BTN_HOVER: [f32; 4] = [0.25, 0.30, 0.26, 1.0];
const NET_BTN: [f32; 4] = [0.13, 0.20, 0.27, 1.0];
const ACT_BTN: [f32; 4] = [0.16, 0.29, 0.18, 1.0];
-const SECTION_BORDER: [f32; 4] = [0.18, 0.18, 0.27, 1.0];
const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
pub fn view(state: &NetworkState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
@@ -208,22 +208,24 @@ pub fn view(state: &NetworkState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageCo
let mut y = cy + 12.0;
// ── WiFi ──
- pc.text("WiFi", cx + 12.0, y, 14.0, TEXT_FG);
+ let mut sec = Section::new(&mut pc, cx, y, cw, "WiFi");
+
+ let yt = sec.ay();
pc.button(if state.wifi_enabled { "ON" } else { "OFF" },
- cx + cw - 80.0, y, 60.0, 28.0,
+ sec.ax(cw - 80.0), yt, 60.0, 28.0,
if state.wifi_enabled { TOGGLE_ON } else { TOGGLE_OFF }, BTN_HOVER, WHITE,
AppAction::Radios(NetworkMessage::ToggleWifi));
- y += 34.0;
+ sec.content_y += 34.0;
if !state.connected_ssid.is_empty() {
- pc.text(&format!("Connected: {}", state.connected_ssid), cx + 14.0, y, 13.0, ACCENT);
- y += 18.0;
- pc.text(&format!("Signal: {}% IP: {}", state.signal_strength, state.ip_address),
- cx + 14.0, y, 12.0, TEXT_DIM);
- y += 16.0;
+ sec.text(&mut pc, &format!("Connected: {}", state.connected_ssid), 14.0, 0.0, 13.0, ACCENT);
+ sec.spacing(18.0);
+ sec.text(&mut pc, &format!("Signal: {}% IP: {}", state.signal_strength, state.ip_address),
+ 14.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(16.0);
} else if state.wifi_enabled {
- pc.text("Not connected", cx + 14.0, y, 12.0, TEXT_DIM);
- y += 16.0;
+ sec.text(&mut pc, "Not connected", 14.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(16.0);
}
if state.wifi_enabled && !state.available.is_empty() {
@@ -231,52 +233,52 @@ pub fn view(state: &NetworkState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageCo
let prefix = if net.in_use { ">" } else { " " };
let label = format!("{} {} ({}%)", prefix, net.ssid, net.signal);
let active = net.in_use;
- pc.button(&label, cx + 14.0, y, cw - 28.0, 26.0,
+ let yt = sec.ay();
+ pc.button(&label, sec.ax(14.0), yt, cw - 28.0, 26.0,
if active { ACT_BTN } else { NET_BTN }, BTN_HOVER,
if active { ACCENT } else { TEXT_FG },
AppAction::Radios(NetworkMessage::ConnectWifi(net.ssid.clone())));
- y += 30.0;
+ sec.content_y += 30.0;
}
}
+ y = sec.finish(&mut pc);
+
// ── Bluetooth ──
- y += 4.0;
- pc.rect(SECTION_BORDER, cx + 8.0, y, cw - 16.0, 1.0);
- y += 8.0;
- pc.text("Bluetooth", cx + 12.0, y, 14.0, TEXT_FG);
+ let mut sec = Section::new(&mut pc, cx, y, cw, "Bluetooth");
- // BT toggle
+ let yt = sec.ay();
pc.button(if state.bt_enabled { "ON" } else { "OFF" },
- cx + cw - 140.0, y, 60.0, 28.0,
+ sec.ax(cw - 140.0), yt, 60.0, 28.0,
if state.bt_enabled { TOGGLE_ON } else { TOGGLE_OFF }, BTN_HOVER, WHITE,
AppAction::Radios(NetworkMessage::ToggleBluetooth));
-
- // Scan button
- pc.button("Scan", cx + cw - 72.0, y, 52.0, 28.0,
+ pc.button("Scan", sec.ax(cw - 72.0), yt, 52.0, 28.0,
TOGGLE_OFF, BTN_HOVER, WHITE,
AppAction::Radios(NetworkMessage::BtScan));
- y += 34.0;
+ sec.content_y += 34.0;
if state.bt_devices.is_empty() {
if state.bt_enabled {
- pc.text("No paired devices found", cx + 14.0, y, 12.0, TEXT_DIM);
+ sec.text(&mut pc, "No paired devices found", 14.0, 0.0, 12.0, TEXT_DIM);
}
} else {
for dev in &state.bt_devices {
let status = if dev.connected { ">" } else { " " };
let label = format!("{} {} ({})", status, dev.name, dev.mac);
let action_label = if dev.connected { "Disconnect" } else { "Connect" };
- pc.text(&label, cx + 14.0, y, 12.0, if dev.connected { ACCENT } else { TEXT_FG });
- pc.button(action_label, cx + cw - 90.0, y - 2.0, 70.0, 22.0,
+ let yt = sec.ay();
+ sec.text(&mut pc, &label, 14.0, 0.0, 12.0, if dev.connected { ACCENT } else { TEXT_FG });
+ pc.button(action_label, sec.ax(cw - 90.0), yt - 2.0, 70.0, 22.0,
if dev.connected { TOGGLE_OFF } else { TOGGLE_ON }, BTN_HOVER, WHITE,
if dev.connected {
AppAction::Radios(NetworkMessage::BtDisconnect(dev.mac.clone()))
} else {
AppAction::Radios(NetworkMessage::BtConnect(dev.mac.clone()))
});
- y += 24.0;
+ sec.content_y += 24.0;
}
}
+ sec.finish(&mut pc);
pc
}
diff --git a/src/pages/power.rs b/src/pages/power.rs
index bf16277..9dcab48 100644
--- a/src/pages/power.rs
+++ b/src/pages/power.rs
@@ -1,4 +1,5 @@
use crate::app::{AppAction, PageContent};
+use clear_ui::layout::Section;
#[derive(Debug, Clone, Default)]
pub struct BatteryInfo {
@@ -159,7 +160,6 @@ const BTN_INACTIVE: [f32; 4] = [0.13, 0.18, 0.14, 1.0];
const BTN_HOVER: [f32; 4] = [0.25, 0.30, 0.26, 1.0];
const DANGER_BG: [f32; 4] = [0.67, 0.20, 0.20, 1.0];
const SAFE_BG: [f32; 4] = [0.20, 0.33, 0.22, 1.0];
-const SECTION_BORDER: [f32; 4] = [0.18, 0.18, 0.27, 1.0];
const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
const RED: [f32; 4] = [1.0, 0.33, 0.33, 1.0];
const ORANGE: [f32; 4] = [1.0, 0.73, 0.20, 1.0];
@@ -169,6 +169,8 @@ pub fn view(state: &PowerState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageCont
let mut y = cy + 12.0;
// ── Battery section ──
+ let mut sec = Section::new(&mut pc, cx, y, cw, "Battery");
+
let bat = &state.battery;
let bat_icon = match bat.state.as_str() {
"charging" => "+",
@@ -181,13 +183,13 @@ pub fn view(state: &PowerState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageCont
else { ACCENT };
let pct_str = format!("{} {:.0}%", bat_icon, bat.percentage);
- pc.text(&pct_str, cx + 12.0, y, 24.0, pct_color);
- y += 30.0;
+ sec.text(&mut pc, &pct_str, 12.0, 0.0, 24.0, pct_color);
+ sec.spacing(30.0);
let state_str = format!("{} • {:.1}W • {:.1}/{:.1} Wh",
bat.state, bat.energy_rate, bat.energy, bat.energy_full);
- pc.text(&state_str, cx + 12.0, y, 12.0, TEXT_DIM);
- y += 18.0;
+ sec.text(&mut pc, &state_str, 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(18.0);
let time_str = if bat.time_to_empty > 0 {
format!("Time remaining: {}", format_duration(bat.time_to_empty))
@@ -195,27 +197,25 @@ pub fn view(state: &PowerState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageCont
format!("Time to full: {}", format_duration(bat.time_to_full))
} else { String::new() };
if !time_str.is_empty() {
- pc.text(&time_str, cx + 12.0, y, 12.0, TEXT_DIM);
- y += 18.0;
+ sec.text(&mut pc, &time_str, 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.spacing(18.0);
}
let detail_str = format!("{} {}", bat.vendor, bat.model);
- pc.text(&detail_str, cx + 12.0, y, 11.0, TEXT_DIM);
- y += 20.0;
+ sec.text(&mut pc, &detail_str, 12.0, 0.0, 11.0, TEXT_DIM);
+ sec.spacing(20.0);
let ac_str = if state.on_ac { "On AC Power" } else { "On Battery" };
- pc.text(ac_str, cx + 12.0, y, 14.0, TEXT_FG);
- y += 24.0;
+ sec.text(&mut pc, ac_str, 12.0, 0.0, 14.0, TEXT_FG);
+
+ y = sec.finish(&mut pc);
// ── CPU Governor section ──
- y += 8.0;
- pc.rect(SECTION_BORDER, cx + 8.0, y, cw - 16.0, 1.0);
- y += 8.0;
- pc.text("CPU Governor", cx + 12.0, y, 12.0, TEXT_DIM);
- y += 18.0;
+ let mut sec = Section::new(&mut pc, cx, y, cw, "CPU Governor");
let btn_w = (cw - 40.0) / 2.0;
let btn_h = 44.0;
+ let yt = sec.ay();
let perf_active = !state.cpu_powersave;
let (perf_bg, perf_desc, perf_desc_color) = if perf_active {
@@ -224,10 +224,10 @@ pub fn view(state: &PowerState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageCont
(BTN_INACTIVE, "Switch to performance governor", TEXT_DIM)
};
- pc.button("Performance", cx + 12.0, y, btn_w, btn_h,
+ pc.button("Performance", sec.ax(12.0), yt, btn_w, btn_h,
perf_bg, BTN_HOVER, WHITE,
AppAction::Power(PowerMessage::SetCpuPerformance));
- pc.text(perf_desc, cx + 16.0, y + 26.0, 10.0, perf_desc_color);
+ sec.text(&mut pc, perf_desc, 16.0, 26.0, 10.0, perf_desc_color);
let (save_bg, save_desc, save_desc_color) = if state.cpu_powersave {
(BTN_ACTIVE, "Governor set to powersave — lower power, slower burst", ACCENT)
@@ -235,19 +235,18 @@ pub fn view(state: &PowerState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageCont
(BTN_INACTIVE, "Switch to powersave governor (requires auth)", TEXT_DIM)
};
- let save_x = cx + 16.0 + btn_w;
- pc.button("Powersave", save_x, y, btn_w, btn_h,
+ let save_x = 16.0 + btn_w;
+ pc.button("Powersave", sec.ax(save_x), yt, btn_w, btn_h,
save_bg, BTN_HOVER, WHITE,
AppAction::Power(PowerMessage::SetCpuPowersave));
- pc.text(save_desc, save_x + 4.0, y + 26.0, 10.0, save_desc_color);
- y += btn_h + 12.0;
+ sec.text(&mut pc, save_desc, save_x + 4.0, 26.0, 10.0, save_desc_color);
+ sec.content_y += btn_h + 12.0;
+ y = sec.finish(&mut pc);
// ── GPU Power section ──
- pc.rect(SECTION_BORDER, cx + 8.0, y, cw - 16.0, 1.0);
- y += 8.0;
- pc.text("GPU Power", cx + 12.0, y, 12.0, TEXT_DIM);
- y += 18.0;
+ let mut sec = Section::new(&mut pc, cx, y, cw, "GPU Power");
+ let yt = sec.ay();
let gpu_def_active = !state.gpu_powersave;
let (gpu_def_bg, gpu_def_desc, gpu_def_desc_c) = if gpu_def_active {
(BTN_ACTIVE, "NVIDIA running at default power limit", ACCENT)
@@ -255,10 +254,10 @@ pub fn view(state: &PowerState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageCont
(BTN_INACTIVE, "Restore default power limit (requires auth)", TEXT_DIM)
};
- pc.button("80W Default", cx + 12.0, y, btn_w, btn_h,
+ pc.button("80W Default", sec.ax(12.0), yt, btn_w, btn_h,
gpu_def_bg, BTN_HOVER, WHITE,
AppAction::Power(PowerMessage::SetGpuDefault));
- pc.text(gpu_def_desc, cx + 16.0, y + 26.0, 10.0, gpu_def_desc_c);
+ sec.text(&mut pc, gpu_def_desc, 16.0, 26.0, 10.0, gpu_def_desc_c);
let (gpu_cap_bg, gpu_cap_desc, gpu_cap_desc_c) = if state.gpu_powersave {
(BTN_ACTIVE, "NVIDIA power limit capped at 5W — minimal draw", ACCENT)
@@ -266,29 +265,29 @@ pub fn view(state: &PowerState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageCont
(BTN_INACTIVE, "Cap NVIDIA to 5W power limit (requires auth)", TEXT_DIM)
};
- pc.button("5W Cap", save_x, y, btn_w, btn_h,
+ pc.button("5W Cap", sec.ax(save_x), yt, btn_w, btn_h,
gpu_cap_bg, BTN_HOVER, WHITE,
AppAction::Power(PowerMessage::SetGpuPowersave));
- pc.text(gpu_cap_desc, save_x + 4.0, y + 26.0, 10.0, gpu_cap_desc_c);
- y += btn_h + 12.0;
+ sec.text(&mut pc, gpu_cap_desc, save_x + 4.0, 26.0, 10.0, gpu_cap_desc_c);
+ sec.content_y += btn_h + 12.0;
+ y = sec.finish(&mut pc);
// ── System Actions section ──
- pc.rect(SECTION_BORDER, cx + 8.0, y, cw - 16.0, 1.0);
- y += 8.0;
- pc.text("System Actions", cx + 12.0, y, 12.0, TEXT_DIM);
- y += 18.0;
+ let mut sec = Section::new(&mut pc, cx, y, cw, "System Actions");
+ let yt = sec.ay();
let act_btn_w = (cw - 48.0) / 4.0;
let act_btn_h = 32.0;
- pc.button("Suspend", cx + 12.0, y, act_btn_w, act_btn_h,
+ pc.button("Suspend", sec.ax(12.0), yt, act_btn_w, act_btn_h,
SAFE_BG, BTN_HOVER, WHITE, AppAction::Power(PowerMessage::Suspend));
- pc.button("Hibernate", cx + 16.0 + act_btn_w, y, act_btn_w, act_btn_h,
+ pc.button("Hibernate", sec.ax(16.0 + act_btn_w), yt, act_btn_w, act_btn_h,
SAFE_BG, BTN_HOVER, WHITE, AppAction::Power(PowerMessage::Hibernate));
- pc.button("Reboot", cx + 20.0 + 2.0 * act_btn_w, y, act_btn_w, act_btn_h,
+ pc.button("Reboot", sec.ax(20.0 + 2.0 * act_btn_w), yt, act_btn_w, act_btn_h,
DANGER_BG, BTN_HOVER, WHITE, AppAction::Power(PowerMessage::Reboot));
- pc.button("Power Off", cx + 24.0 + 3.0 * act_btn_w, y, act_btn_w, act_btn_h,
+ pc.button("Power Off", sec.ax(24.0 + 3.0 * act_btn_w), yt, act_btn_w, act_btn_h,
DANGER_BG, BTN_HOVER, WHITE, AppAction::Power(PowerMessage::PowerOff));
+ sec.finish(&mut pc);
pc
}
diff --git a/src/pages/processors.rs b/src/pages/processors.rs
new file mode 100644
index 0000000..ec5963f
--- /dev/null
+++ b/src/pages/processors.rs
@@ -0,0 +1,89 @@
+use crate::app::PageContent;
+use clear_ui::layout::Section;
+
+#[derive(Debug, Clone, Default)]
+pub struct ProcessorsState {
+ pub cpu_model: String,
+ pub cpu_usage: f32,
+ pub cpu_cores: u32,
+ pub gpu: String,
+}
+
+#[derive(Debug, Clone)]
+pub enum ProcessorsMessage {
+ Refreshed(ProcessorsState),
+}
+
+pub async fn fetch_processors_state() -> ProcessorsState {
+ let (cpu_model, cpu_cores) = {
+ let lscpu = tokio::process::Command::new("lscpu")
+ .output().await.ok()
+ .map(|o| String::from_utf8_lossy(&o.stdout).to_string())
+ .unwrap_or_default();
+ let model = lscpu.lines()
+ .find(|l| l.contains("Model name"))
+ .and_then(|l| l.split(':').nth(1))
+ .map(|s| s.trim().to_string())
+ .unwrap_or_default();
+ let cores = lscpu.lines()
+ .find(|l| l.contains("CPU(s)"))
+ .and_then(|l| {
+ let rest = l.split(':').nth(1).unwrap_or("").trim();
+ rest.split_whitespace().next().and_then(|n| n.parse::<u32>().ok())
+ })
+ .unwrap_or(0);
+ (model, cores)
+ };
+
+ let cpu_usage = {
+ let read_stat = || -> Option<(u64, u64)> {
+ let stat = std::fs::read_to_string("/proc/stat").ok()?;
+ let first = stat.lines().next()?;
+ let vals: Vec<u64> = first.split_whitespace().skip(1).filter_map(|v| v.parse().ok()).collect();
+ if vals.len() < 3 { return None; }
+ let total: u64 = vals.iter().sum();
+ let idle = vals.get(3).copied().unwrap_or(0);
+ Some((idle, total))
+ };
+ let (idle1, total1) = read_stat().unwrap_or((0, 1));
+ tokio::time::sleep(std::time::Duration::from_millis(100)).await;
+ let (idle2, total2) = read_stat().unwrap_or((0, 1));
+ let d_idle = idle2.saturating_sub(idle1);
+ let d_total = total2.saturating_sub(total1);
+ if d_total > 0 {
+ (1.0 - d_idle as f64 / d_total as f64) * 100.0
+ } else { 0.0 }
+ } as f32;
+
+ let gpu = tokio::process::Command::new("lspci")
+ .output().await.ok()
+ .and_then(|o| {
+ String::from_utf8_lossy(&o.stdout).lines()
+ .find(|l| l.contains("VGA") || l.contains("3D"))
+ .and_then(|l| l.split(':').nth(2))
+ .map(|s| s.trim().to_string())
+ })
+ .unwrap_or_default();
+
+ ProcessorsState { cpu_model, cpu_usage, cpu_cores, gpu }
+}
+
+const TEXT_FG: [f32; 4] = [0.83, 0.83, 0.83, 1.0];
+
+pub fn view(state: &ProcessorsState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
+ let mut pc = PageContent::new();
+ let y = cy + 12.0;
+
+ let mut sec = Section::new(&mut pc, cx, y, cw, "Processors");
+ sec.text(&mut pc,
+ &format!("CPU {} ({} cores) — {:.0}%", state.cpu_model, state.cpu_cores, state.cpu_usage),
+ 12.0, 0.0, 12.0, TEXT_FG,
+ );
+ sec.spacing(10.0);
+ sec.text(&mut pc, &format!("GPU {}", state.gpu), 12.0, 0.0, 12.0, TEXT_FG);
+ sec.finish(&mut pc);
+
+ pc
+}
+
+pub fn update(_state: &mut ProcessorsState, _msg: ProcessorsMessage) {}
diff --git a/src/pages/status.rs b/src/pages/status.rs
index f3d247c..117c1de 100644
--- a/src/pages/status.rs
+++ b/src/pages/status.rs
@@ -1,4 +1,5 @@
use crate::app::{AppAction, PageContent};
+use clear_ui::layout::Section;
#[derive(Debug, Clone, Default)]
pub struct StatusState {
@@ -69,34 +70,39 @@ const WHITE: [f32; 4] = [1.0, 1.0, 1.0, 1.0];
pub fn view(state: &StatusState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
let mut pc = PageContent::new();
- let mut y = cy + 12.0;
+ let y = cy + 12.0;
- // Waybar status header
+ let mut sec = Section::new(&mut pc, cx, y, cw, "Waybar");
+
+ // Status
let status_color = if state.running { ACCENT } else { [0.67, 0.20, 0.20, 1.0] };
let status_text = if state.running { "Running" } else { "Stopped" };
- pc.text("Waybar", cx + 12.0, y, 14.0, TEXT_FG);
- pc.text(status_text, cx + 80.0, y, 14.0, status_color);
- y += 22.0;
+ sec.text(&mut pc, "Waybar", 12.0, 0.0, 14.0, TEXT_FG);
+ sec.text(&mut pc, status_text, 80.0, 0.0, 14.0, status_color);
+ sec.spacing(22.0);
// Font size
- pc.text(&format!("Font size: {}px", state.font_size), cx + 12.0, y, 13.0, TEXT_FG);
- y += 20.0;
+ sec.text(&mut pc, &format!("Font size: {}px", state.font_size), 12.0, 0.0, 13.0, TEXT_FG);
+ sec.spacing(20.0);
let btn_h = 28.0;
- pc.button("-1", cx + 12.0, y, 36.0, btn_h,
+ let yt = sec.ay();
+ pc.button("-1", sec.ax(12.0), yt, 36.0, btn_h,
BTN_INACTIVE, BTN_HOVER, WHITE,
AppAction::Status(StatusMessage::FontSizeDown));
- pc.text(&format!(" {}px ", state.font_size), cx + 56.0, y + 7.0, 13.0, TEXT_FG);
- pc.button("+1", cx + 12.0 + 36.0 + 8.0, y, 36.0, btn_h,
+ pc.text(&format!(" {}px ", state.font_size), sec.ax(56.0), yt + 7.0, 13.0, TEXT_FG);
+ pc.button("+1", sec.ax(12.0 + 36.0 + 8.0), yt, 36.0, btn_h,
BTN_ACTIVE, BTN_HOVER, WHITE,
AppAction::Status(StatusMessage::FontSizeUp));
- y += btn_h + 12.0;
+ sec.content_y += btn_h + 12.0;
// Reload button
+ let yt = sec.ay();
let btn_w = (cw - 24.0).min(200.0);
- pc.button("Reload Waybar", cx + cw / 2.0 - btn_w / 2.0, y, btn_w, 32.0,
+ pc.button("Reload Waybar", cx + cw / 2.0 - btn_w / 2.0, yt, btn_w, 32.0,
BTN_INACTIVE, BTN_HOVER, WHITE,
AppAction::Status(StatusMessage::ReloadWaybar));
+ sec.finish(&mut pc);
pc
}
diff --git a/src/pages/storage.rs b/src/pages/storage.rs
index 74df39f..604caf8 100644
--- a/src/pages/storage.rs
+++ b/src/pages/storage.rs
@@ -1,4 +1,5 @@
use crate::app::PageContent;
+use clear_ui::layout::Section;
#[derive(Debug, Clone, Default)]
pub struct StorageState {
@@ -60,12 +61,11 @@ fn parse_mem(info: &str) -> (f64, f64) {
const LABEL_FG: [f32; 4] = [0.56, 0.83, 0.56, 1.0];
const TEXT_FG: [f32; 4] = [0.83, 0.83, 0.83, 1.0];
-pub fn view(state: &StorageState, cx: f32, cy: f32, _cw: f32, _ch: f32) -> PageContent {
+pub fn view(state: &StorageState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
let mut pc = PageContent::new();
- let mut y = cy + 12.0;
+ let y = cy + 12.0;
- pc.text("Local Storage", cx + 12.0, y, 16.0, LABEL_FG);
- y += 26.0;
+ let mut sec = Section::new(&mut pc, cx, y, cw, "Local Storage");
let disk_pct = if state.disk_total > 0.0 {
state.disk_used / state.disk_total * 100.0
@@ -73,19 +73,20 @@ pub fn view(state: &StorageState, cx: f32, cy: f32, _cw: f32, _ch: f32) -> PageC
0.0
};
- pc.text("Disk", cx + 12.0, y, 12.0, LABEL_FG);
- pc.text(
+ sec.text(&mut pc, "Disk", 12.0, 0.0, 12.0, LABEL_FG);
+ sec.text(&mut pc,
&format!("{:.0} / {:.0} GiB ({:.0}%)", state.disk_used, state.disk_total, disk_pct),
- cx + 100.0, y, 12.0, TEXT_FG,
+ 100.0, 0.0, 12.0, TEXT_FG,
);
- y += 18.0;
+ sec.spacing(18.0);
- let bar_w = _cw - 24.0;
- pc.rect([0.15, 0.15, 0.25, 1.0], cx + 12.0, y, bar_w, 8.0);
+ let bar_w = cw - 24.0;
+ let yt = sec.ay();
+ pc.rect([0.15, 0.15, 0.25, 1.0], sec.ax(12.0), yt, bar_w, 8.0);
if disk_pct > 0.0 {
- pc.rect([0.36, 0.60, 0.36, 1.0], cx + 12.0, y, bar_w * (disk_pct as f32 / 100.0).min(1.0), 8.0);
+ pc.rect([0.36, 0.60, 0.36, 1.0], sec.ax(12.0), yt, bar_w * (disk_pct as f32 / 100.0).min(1.0), 8.0);
}
- y += 20.0;
+ sec.content_y += 20.0;
let ram_pct = if state.ram_total > 0.0 {
state.ram_used / state.ram_total * 100.0
@@ -93,17 +94,19 @@ pub fn view(state: &StorageState, cx: f32, cy: f32, _cw: f32, _ch: f32) -> PageC
0.0
};
- pc.text("RAM", cx + 12.0, y, 12.0, LABEL_FG);
- pc.text(
+ sec.text(&mut pc, "RAM", 12.0, 0.0, 12.0, LABEL_FG);
+ sec.text(&mut pc,
&format!("{:.1} / {:.1} GiB ({:.0}%)", state.ram_used, state.ram_total, ram_pct),
- cx + 100.0, y, 12.0, TEXT_FG,
+ 100.0, 0.0, 12.0, TEXT_FG,
);
- y += 18.0;
+ sec.spacing(18.0);
- pc.rect([0.15, 0.15, 0.25, 1.0], cx + 12.0, y, bar_w, 8.0);
+ let yt = sec.ay();
+ pc.rect([0.15, 0.15, 0.25, 1.0], sec.ax(12.0), yt, bar_w, 8.0);
if ram_pct > 0.0 {
- pc.rect([0.50, 0.50, 0.65, 1.0], cx + 12.0, y, bar_w * (ram_pct as f32 / 100.0).min(1.0), 8.0);
+ pc.rect([0.50, 0.50, 0.65, 1.0], sec.ax(12.0), yt, bar_w * (ram_pct as f32 / 100.0).min(1.0), 8.0);
}
+ sec.finish(&mut pc);
pc
}
diff --git a/src/pages/system_info.rs b/src/pages/system_info.rs
index e18a4dd..eb20492 100644
--- a/src/pages/system_info.rs
+++ b/src/pages/system_info.rs
@@ -1,14 +1,11 @@
use crate::app::PageContent;
+use clear_ui::layout::Section;
#[derive(Debug, Clone, Default)]
pub struct SystemState {
pub hostname: String,
pub kernel: String,
- pub cpu_model: String,
- pub cpu_usage: f32,
- pub cpu_cores: u32,
pub uptime: String,
- pub gpu: String,
}
#[derive(Debug, Clone)]
@@ -27,86 +24,26 @@ pub async fn fetch_system_state() -> SystemState {
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_default();
- let (cpu_model, cpu_cores) = {
- let lscpu = tokio::process::Command::new("lscpu")
- .output().await.ok()
- .map(|o| String::from_utf8_lossy(&o.stdout).to_string())
- .unwrap_or_default();
- let model = lscpu.lines()
- .find(|l| l.contains("Model name"))
- .and_then(|l| l.split(':').nth(1))
- .map(|s| s.trim().to_string())
- .unwrap_or_default();
- let cores = lscpu.lines()
- .find(|l| l.contains("CPU(s)"))
- .and_then(|l| {
- let rest = l.split(':').nth(1).unwrap_or("").trim();
- rest.split_whitespace().next().and_then(|n| n.parse::<u32>().ok())
- })
- .unwrap_or(0);
- (model, cores)
- };
-
- let cpu_usage = {
- let read_stat = || -> Option<(u64, u64)> {
- let stat = std::fs::read_to_string("/proc/stat").ok()?;
- let first = stat.lines().next()?;
- let vals: Vec<u64> = first.split_whitespace().skip(1).filter_map(|v| v.parse().ok()).collect();
- if vals.len() < 3 { return None; }
- let total: u64 = vals.iter().sum();
- let idle = vals.get(3).copied().unwrap_or(0);
- Some((idle, total))
- };
- let (idle1, total1) = read_stat().unwrap_or((0, 1));
- tokio::time::sleep(std::time::Duration::from_millis(100)).await;
- let (idle2, total2) = read_stat().unwrap_or((0, 1));
- let d_idle = idle2.saturating_sub(idle1);
- let d_total = total2.saturating_sub(total1);
- if d_total > 0 {
- (1.0 - d_idle as f64 / d_total as f64) * 100.0
- } else { 0.0 }
- } as f32;
-
let uptime = tokio::process::Command::new("uptime")
.args(["-p"]).output().await.ok()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().trim_start_matches("up ").to_string())
.unwrap_or_default();
- let gpu = tokio::process::Command::new("lspci")
- .output().await.ok()
- .and_then(|o| {
- String::from_utf8_lossy(&o.stdout).lines()
- .find(|l| l.contains("VGA") || l.contains("3D"))
- .and_then(|l| l.split(':').nth(2))
- .map(|s| s.trim().to_string())
- })
- .unwrap_or_default();
-
- SystemState { hostname, kernel, cpu_model, cpu_usage, cpu_cores, uptime, gpu }
+ SystemState { hostname, kernel, uptime }
}
const TEXT_FG: [f32; 4] = [0.83, 0.83, 0.83, 1.0];
const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
-pub fn view(state: &SystemState, cx: f32, cy: f32, _cw: f32, _ch: f32) -> PageContent {
+pub fn view(state: &SystemState, cx: f32, cy: f32, cw: f32, _ch: f32) -> PageContent {
let mut pc = PageContent::new();
- let mut y = cy + 12.0;
-
- // Hostname / kernel
- pc.text(&format!("{} — Linux {}", state.hostname, state.kernel), cx + 12.0, y, 14.0, TEXT_FG);
- y += 22.0;
-
- // Uptime
- pc.text(&format!("Uptime: {}", state.uptime), cx + 12.0, y, 12.0, TEXT_DIM);
- y += 18.0;
-
- // CPU
- pc.text(&format!("CPU {} ({} cores) — {:.0}%", state.cpu_model, state.cpu_cores, state.cpu_usage),
- cx + 12.0, y, 12.0, TEXT_FG);
- y += 18.0;
+ let y = cy + 12.0;
- // GPU
- pc.text(&format!("GPU {}", state.gpu), cx + 12.0, y, 12.0, TEXT_FG);
+ let mut sec = Section::new(&mut pc, cx, y, cw, "System");
+ sec.text(&mut pc, &format!("{} — Linux {}", state.hostname, state.kernel), 12.0, 0.0, 14.0, TEXT_FG);
+ sec.spacing(10.0);
+ sec.text(&mut pc, &format!("Uptime: {}", state.uptime), 12.0, 0.0, 12.0, TEXT_DIM);
+ sec.finish(&mut pc);
pc
}