Add scrollbar to CPU per core area.
This commit is contained in:
parent
9b1643afac
commit
a4f69a5f7d
@ -7,21 +7,25 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
event::{self, Event, KeyCode},
|
event::{self, Event, KeyCode, EnableMouseCapture, DisableMouseCapture},
|
||||||
execute,
|
execute,
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
};
|
};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::CrosstermBackend,
|
backend::CrosstermBackend,
|
||||||
layout::{Constraint, Direction},
|
layout::{Constraint, Direction, Rect}, // add Rect
|
||||||
Terminal,
|
Terminal,
|
||||||
};
|
};
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
|
||||||
use crate::history::{push_capped, PerCoreHistory};
|
use crate::history::{push_capped, PerCoreHistory};
|
||||||
use crate::types::Metrics;
|
use crate::types::Metrics;
|
||||||
|
use crate::ui::cpu::{
|
||||||
|
draw_cpu_avg_graph, draw_per_core_bars, per_core_clamp, per_core_content_area,
|
||||||
|
per_core_handle_key, per_core_handle_mouse, per_core_handle_scrollbar_mouse,
|
||||||
|
PerCoreScrollDrag,
|
||||||
|
};
|
||||||
use crate::ui::{
|
use crate::ui::{
|
||||||
cpu::{draw_cpu_avg_graph, draw_per_core_bars},
|
|
||||||
disks::draw_disks,
|
disks::draw_disks,
|
||||||
header::draw_header,
|
header::draw_header,
|
||||||
mem::draw_mem,
|
mem::draw_mem,
|
||||||
@ -50,6 +54,9 @@ pub struct App {
|
|||||||
|
|
||||||
// Quit flag
|
// Quit flag
|
||||||
should_quit: bool,
|
should_quit: bool,
|
||||||
|
|
||||||
|
pub per_core_scroll: usize,
|
||||||
|
pub per_core_drag: Option<PerCoreScrollDrag>, // new: drag state
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
@ -64,6 +71,8 @@ impl App {
|
|||||||
rx_peak: 0,
|
rx_peak: 0,
|
||||||
tx_peak: 0,
|
tx_peak: 0,
|
||||||
should_quit: false,
|
should_quit: false,
|
||||||
|
per_core_scroll: 0,
|
||||||
|
per_core_drag: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,7 +83,7 @@ impl App {
|
|||||||
// Terminal setup
|
// Terminal setup
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
let mut stdout = io::stdout();
|
let mut stdout = io::stdout();
|
||||||
execute!(stdout, EnterAlternateScreen)?;
|
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||||
let backend = CrosstermBackend::new(stdout);
|
let backend = CrosstermBackend::new(stdout);
|
||||||
let mut terminal = Terminal::new(backend)?;
|
let mut terminal = Terminal::new(backend)?;
|
||||||
terminal.clear()?;
|
terminal.clear()?;
|
||||||
@ -85,7 +94,7 @@ impl App {
|
|||||||
// Teardown
|
// Teardown
|
||||||
disable_raw_mode()?;
|
disable_raw_mode()?;
|
||||||
let backend = terminal.backend_mut();
|
let backend = terminal.backend_mut();
|
||||||
execute!(backend, LeaveAlternateScreen)?;
|
execute!(backend, DisableMouseCapture, LeaveAlternateScreen)?;
|
||||||
terminal.show_cursor()?;
|
terminal.show_cursor()?;
|
||||||
|
|
||||||
res
|
res
|
||||||
@ -99,13 +108,90 @@ impl App {
|
|||||||
loop {
|
loop {
|
||||||
// Input (non-blocking)
|
// Input (non-blocking)
|
||||||
while event::poll(Duration::from_millis(10))? {
|
while event::poll(Duration::from_millis(10))? {
|
||||||
if let Event::Key(k) = event::read()? {
|
match event::read()? {
|
||||||
if matches!(
|
Event::Key(k) => {
|
||||||
k.code,
|
if matches!(k.code, KeyCode::Char('q') | KeyCode::Char('Q') | KeyCode::Esc) {
|
||||||
KeyCode::Char('q') | KeyCode::Char('Q') | KeyCode::Esc
|
|
||||||
) {
|
|
||||||
self.should_quit = true;
|
self.should_quit = true;
|
||||||
}
|
}
|
||||||
|
// Per-core scroll via keys (Up/Down/PageUp/PageDown/Home/End)
|
||||||
|
let sz = terminal.size()?;
|
||||||
|
let area = Rect::new(0, 0, sz.width, sz.height);
|
||||||
|
let rows = ratatui::layout::Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Length(1),
|
||||||
|
Constraint::Ratio(1, 3),
|
||||||
|
Constraint::Length(3),
|
||||||
|
Constraint::Length(3),
|
||||||
|
Constraint::Min(10),
|
||||||
|
])
|
||||||
|
.split(area);
|
||||||
|
let top = ratatui::layout::Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([Constraint::Percentage(66), Constraint::Percentage(34)])
|
||||||
|
.split(rows[1]);
|
||||||
|
let content = per_core_content_area(top[1]);
|
||||||
|
|
||||||
|
per_core_handle_key(&mut self.per_core_scroll, k, content.height as usize);
|
||||||
|
|
||||||
|
let total_rows = self
|
||||||
|
.last_metrics
|
||||||
|
.as_ref()
|
||||||
|
.map(|mm| mm.cpu_per_core.len())
|
||||||
|
.unwrap_or(0);
|
||||||
|
per_core_clamp(&mut self.per_core_scroll, total_rows, content.height as usize);
|
||||||
|
}
|
||||||
|
Event::Mouse(m) => {
|
||||||
|
// Layout to get areas
|
||||||
|
let sz = terminal.size()?;
|
||||||
|
let area = Rect::new(0, 0, sz.width, sz.height);
|
||||||
|
let rows = ratatui::layout::Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Length(1),
|
||||||
|
Constraint::Ratio(1, 3),
|
||||||
|
Constraint::Length(3),
|
||||||
|
Constraint::Length(3),
|
||||||
|
Constraint::Min(10),
|
||||||
|
])
|
||||||
|
.split(area);
|
||||||
|
let top = ratatui::layout::Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([Constraint::Percentage(66), Constraint::Percentage(34)])
|
||||||
|
.split(rows[1]);
|
||||||
|
|
||||||
|
// Content wheel scrolling
|
||||||
|
let content = per_core_content_area(top[1]);
|
||||||
|
per_core_handle_mouse(
|
||||||
|
&mut self.per_core_scroll,
|
||||||
|
m,
|
||||||
|
content,
|
||||||
|
content.height as usize,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Scrollbar clicks/drag
|
||||||
|
let total_rows = self
|
||||||
|
.last_metrics
|
||||||
|
.as_ref()
|
||||||
|
.map(|mm| mm.cpu_per_core.len())
|
||||||
|
.unwrap_or(0);
|
||||||
|
per_core_handle_scrollbar_mouse(
|
||||||
|
&mut self.per_core_scroll,
|
||||||
|
&mut self.per_core_drag,
|
||||||
|
m,
|
||||||
|
top[1],
|
||||||
|
total_rows,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clamp to bounds
|
||||||
|
per_core_clamp(
|
||||||
|
&mut self.per_core_scroll,
|
||||||
|
total_rows,
|
||||||
|
content.height as usize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Event::Resize(_, _) => {}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if self.should_quit {
|
if self.should_quit {
|
||||||
@ -179,7 +265,13 @@ impl App {
|
|||||||
.split(rows[1]);
|
.split(rows[1]);
|
||||||
|
|
||||||
draw_cpu_avg_graph(f, top[0], &self.cpu_hist, self.last_metrics.as_ref());
|
draw_cpu_avg_graph(f, top[0], &self.cpu_hist, self.last_metrics.as_ref());
|
||||||
draw_per_core_bars(f, top[1], self.last_metrics.as_ref(), &self.per_core_hist);
|
draw_per_core_bars(
|
||||||
|
f,
|
||||||
|
top[1],
|
||||||
|
self.last_metrics.as_ref(),
|
||||||
|
&self.per_core_hist,
|
||||||
|
self.per_core_scroll,
|
||||||
|
);
|
||||||
|
|
||||||
draw_mem(f, rows[2], self.last_metrics.as_ref());
|
draw_mem(f, rows[2], self.last_metrics.as_ref());
|
||||||
draw_swap(f, rows[3], self.last_metrics.as_ref());
|
draw_swap(f, rows[3], self.last_metrics.as_ref());
|
||||||
@ -225,3 +317,21 @@ impl App {
|
|||||||
draw_top_processes(f, bottom[1], self.last_metrics.as_ref());
|
draw_top_processes(f, bottom[1], self.last_metrics.as_ref());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for App {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
last_metrics: None,
|
||||||
|
cpu_hist: VecDeque::with_capacity(600),
|
||||||
|
per_core_hist: PerCoreHistory::new(60),
|
||||||
|
last_net_totals: None,
|
||||||
|
rx_hist: VecDeque::with_capacity(600),
|
||||||
|
tx_hist: VecDeque::with_capacity(600),
|
||||||
|
rx_peak: 0,
|
||||||
|
tx_peak: 0,
|
||||||
|
should_quit: false,
|
||||||
|
per_core_scroll: 0,
|
||||||
|
per_core_drag: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -7,10 +7,236 @@ use ratatui::{
|
|||||||
text::{Line, Span},
|
text::{Line, Span},
|
||||||
widgets::{Block, Borders, Paragraph, Sparkline},
|
widgets::{Block, Borders, Paragraph, Sparkline},
|
||||||
};
|
};
|
||||||
|
use crossterm::event::{KeyCode, KeyEvent, MouseButton, MouseEvent, MouseEventKind}; // + MouseButton
|
||||||
|
|
||||||
use crate::history::PerCoreHistory;
|
use crate::history::PerCoreHistory;
|
||||||
use crate::types::Metrics;
|
use crate::types::Metrics;
|
||||||
|
|
||||||
|
/// Subtle grey theme for the custom scrollbar
|
||||||
|
const SB_ARROW: Color = Color::Rgb(170,170,180);
|
||||||
|
const SB_TRACK: Color = Color::Rgb(170,170,180);
|
||||||
|
const SB_THUMB: Color = Color::Rgb(170,170,180);
|
||||||
|
|
||||||
|
/// State for dragging the scrollbar thumb
|
||||||
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
pub struct PerCoreScrollDrag {
|
||||||
|
pub active: bool,
|
||||||
|
pub start_y: u16, // mouse row where drag started
|
||||||
|
pub start_top: usize, // thumb top (in track rows) at drag start
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the content area for per-core CPU bars, excluding borders and reserving space for scrollbar.
|
||||||
|
pub fn per_core_content_area(area: Rect) -> Rect {
|
||||||
|
// Inner minus borders
|
||||||
|
let inner = Rect {
|
||||||
|
x: area.x + 1,
|
||||||
|
y: area.y + 1,
|
||||||
|
width: area.width.saturating_sub(2),
|
||||||
|
height: area.height.saturating_sub(2),
|
||||||
|
};
|
||||||
|
// Reserve 1 column on the right for a gutter and 1 for the scrollbar
|
||||||
|
Rect {
|
||||||
|
x: inner.x,
|
||||||
|
y: inner.y,
|
||||||
|
width: inner.width.saturating_sub(2),
|
||||||
|
height: inner.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles key events for per-core CPU bars.
|
||||||
|
pub fn per_core_handle_key(scroll_offset: &mut usize, key: KeyEvent, page_size: usize) {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Up => *scroll_offset = scroll_offset.saturating_sub(1),
|
||||||
|
KeyCode::Down => *scroll_offset = scroll_offset.saturating_add(1),
|
||||||
|
KeyCode::PageUp => {
|
||||||
|
let step = page_size.max(1);
|
||||||
|
*scroll_offset = scroll_offset.saturating_sub(step);
|
||||||
|
}
|
||||||
|
KeyCode::PageDown => {
|
||||||
|
let step = page_size.max(1);
|
||||||
|
*scroll_offset = scroll_offset.saturating_add(step);
|
||||||
|
}
|
||||||
|
KeyCode::Home => *scroll_offset = 0,
|
||||||
|
KeyCode::End => *scroll_offset = usize::MAX, // draw() clamps to max
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles mouse wheel over the content.
|
||||||
|
pub fn per_core_handle_mouse(
|
||||||
|
scroll_offset: &mut usize,
|
||||||
|
mouse: MouseEvent,
|
||||||
|
content_area: Rect,
|
||||||
|
page_size: usize,
|
||||||
|
) {
|
||||||
|
let inside = mouse.column >= content_area.x
|
||||||
|
&& mouse.column < content_area.x + content_area.width
|
||||||
|
&& mouse.row >= content_area.y
|
||||||
|
&& mouse.row < content_area.y + content_area.height;
|
||||||
|
|
||||||
|
if !inside {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match mouse.kind {
|
||||||
|
MouseEventKind::ScrollUp => *scroll_offset = scroll_offset.saturating_sub(1),
|
||||||
|
MouseEventKind::ScrollDown => *scroll_offset = scroll_offset.saturating_add(1),
|
||||||
|
// Optional paging via horizontal wheel
|
||||||
|
MouseEventKind::ScrollLeft => {
|
||||||
|
let step = page_size.max(1);
|
||||||
|
*scroll_offset = scroll_offset.saturating_sub(step);
|
||||||
|
}
|
||||||
|
MouseEventKind::ScrollRight => {
|
||||||
|
let step = page_size.max(1);
|
||||||
|
*scroll_offset = scroll_offset.saturating_add(step);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles mouse interaction with the scrollbar itself (click arrows/page/drag).
|
||||||
|
pub fn per_core_handle_scrollbar_mouse(
|
||||||
|
scroll_offset: &mut usize,
|
||||||
|
drag: &mut Option<PerCoreScrollDrag>,
|
||||||
|
mouse: MouseEvent,
|
||||||
|
per_core_area: Rect,
|
||||||
|
total_rows: usize,
|
||||||
|
) {
|
||||||
|
// Geometry
|
||||||
|
let inner = Rect {
|
||||||
|
x: per_core_area.x + 1,
|
||||||
|
y: per_core_area.y + 1,
|
||||||
|
width: per_core_area.width.saturating_sub(2),
|
||||||
|
height: per_core_area.height.saturating_sub(2),
|
||||||
|
};
|
||||||
|
if inner.height < 3 || inner.width < 1 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let content = Rect {
|
||||||
|
x: inner.x,
|
||||||
|
y: inner.y,
|
||||||
|
width: inner.width.saturating_sub(2),
|
||||||
|
height: inner.height,
|
||||||
|
};
|
||||||
|
let scroll_area = Rect {
|
||||||
|
x: inner.x + inner.width.saturating_sub(1),
|
||||||
|
y: inner.y,
|
||||||
|
width: 1,
|
||||||
|
height: inner.height,
|
||||||
|
};
|
||||||
|
let viewport_rows = content.height as usize;
|
||||||
|
let total = total_rows.max(1);
|
||||||
|
let view = viewport_rows.clamp(1, total);
|
||||||
|
let max_off = total.saturating_sub(view);
|
||||||
|
let mut offset = (*scroll_offset).min(max_off);
|
||||||
|
|
||||||
|
// Track and current thumb
|
||||||
|
let track = (scroll_area.height - 2) as usize;
|
||||||
|
if track == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let thumb_len = ((track * view + total - 1) / total).max(1).min(track);
|
||||||
|
let top_for_offset = |off: usize| -> usize {
|
||||||
|
if max_off == 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
((track - thumb_len) * off + max_off / 2) / max_off
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let thumb_top = top_for_offset(offset);
|
||||||
|
|
||||||
|
let inside_scrollbar = mouse.column == scroll_area.x
|
||||||
|
&& mouse.row >= scroll_area.y
|
||||||
|
&& mouse.row < scroll_area.y + scroll_area.height;
|
||||||
|
|
||||||
|
// Helper to page
|
||||||
|
let page_up = || offset.saturating_sub(view.max(1));
|
||||||
|
let page_down = || offset.saturating_add(view.max(1));
|
||||||
|
|
||||||
|
match mouse.kind {
|
||||||
|
MouseEventKind::Down(MouseButton::Left) if inside_scrollbar => {
|
||||||
|
// Where within the track?
|
||||||
|
let row = mouse.row;
|
||||||
|
if row == scroll_area.y {
|
||||||
|
// Top arrow
|
||||||
|
offset = offset.saturating_sub(1);
|
||||||
|
} else if row + 1 == scroll_area.y + scroll_area.height {
|
||||||
|
// Bottom arrow
|
||||||
|
offset = offset.saturating_add(1);
|
||||||
|
} else {
|
||||||
|
// Inside track
|
||||||
|
let rel = (row - (scroll_area.y + 1)) as usize;
|
||||||
|
let thumb_end = thumb_top + thumb_len;
|
||||||
|
if rel < thumb_top {
|
||||||
|
// Page up
|
||||||
|
offset = page_up();
|
||||||
|
} else if rel >= thumb_end {
|
||||||
|
// Page down
|
||||||
|
offset = page_down();
|
||||||
|
} else {
|
||||||
|
// Start dragging
|
||||||
|
*drag = Some(PerCoreScrollDrag {
|
||||||
|
active: true,
|
||||||
|
start_y: row,
|
||||||
|
start_top: thumb_top,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MouseEventKind::Drag(MouseButton::Left) => {
|
||||||
|
if let Some(mut d) = drag.take() {
|
||||||
|
if d.active {
|
||||||
|
let dy = (mouse.row as i32) - (d.start_y as i32);
|
||||||
|
let new_top = (d.start_top as i32 + dy)
|
||||||
|
.clamp(0, (track.saturating_sub(thumb_len)) as i32) as usize;
|
||||||
|
// Inverse mapping top -> offset
|
||||||
|
if track > thumb_len {
|
||||||
|
let denom = track - thumb_len;
|
||||||
|
offset = if max_off == 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
(new_top * max_off + denom / 2) / denom
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
offset = 0;
|
||||||
|
}
|
||||||
|
// Keep dragging
|
||||||
|
d.start_top = new_top;
|
||||||
|
d.start_y = mouse.row;
|
||||||
|
*drag = Some(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MouseEventKind::Up(MouseButton::Left) => {
|
||||||
|
// End drag
|
||||||
|
*drag = None;
|
||||||
|
}
|
||||||
|
// Also allow wheel scrolling when cursor is over the scrollbar
|
||||||
|
MouseEventKind::ScrollUp if inside_scrollbar => {
|
||||||
|
offset = offset.saturating_sub(1);
|
||||||
|
}
|
||||||
|
MouseEventKind::ScrollDown if inside_scrollbar => {
|
||||||
|
offset = offset.saturating_add(1);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp and write back
|
||||||
|
if offset > max_off {
|
||||||
|
offset = max_off;
|
||||||
|
}
|
||||||
|
*scroll_offset = offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clamp scroll offset to the valid range given content and viewport.
|
||||||
|
pub fn per_core_clamp(scroll_offset: &mut usize, total_rows: usize, viewport_rows: usize) {
|
||||||
|
let max_offset = total_rows.saturating_sub(viewport_rows);
|
||||||
|
if *scroll_offset > max_offset {
|
||||||
|
*scroll_offset = max_offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws the CPU average sparkline graph.
|
||||||
pub fn draw_cpu_avg_graph(
|
pub fn draw_cpu_avg_graph(
|
||||||
f: &mut ratatui::Frame<'_>,
|
f: &mut ratatui::Frame<'_>,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
@ -33,11 +259,13 @@ pub fn draw_cpu_avg_graph(
|
|||||||
f.render_widget(spark, area);
|
f.render_widget(spark, area);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Draws the per-core CPU bars with sparklines and trends.
|
||||||
pub fn draw_per_core_bars(
|
pub fn draw_per_core_bars(
|
||||||
f: &mut ratatui::Frame<'_>,
|
f: &mut ratatui::Frame<'_>,
|
||||||
area: Rect,
|
area: Rect,
|
||||||
m: Option<&Metrics>,
|
m: Option<&Metrics>,
|
||||||
per_core_hist: &PerCoreHistory,
|
per_core_hist: &PerCoreHistory,
|
||||||
|
scroll_offset: usize,
|
||||||
) {
|
) {
|
||||||
f.render_widget(
|
f.render_widget(
|
||||||
Block::default().borders(Borders::ALL).title("Per-core"),
|
Block::default().borders(Borders::ALL).title("Per-core"),
|
||||||
@ -47,45 +275,51 @@ pub fn draw_per_core_bars(
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Compute inner rect and content area
|
||||||
let inner = Rect {
|
let inner = Rect {
|
||||||
x: area.x + 1,
|
x: area.x + 1,
|
||||||
y: area.y + 1,
|
y: area.y + 1,
|
||||||
width: area.width.saturating_sub(2),
|
width: area.width.saturating_sub(2),
|
||||||
height: area.height.saturating_sub(2),
|
height: area.height.saturating_sub(2),
|
||||||
};
|
};
|
||||||
if inner.height == 0 {
|
if inner.height == 0 || inner.width <= 2 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let content = Rect {
|
||||||
|
x: inner.x,
|
||||||
|
y: inner.y,
|
||||||
|
width: inner.width.saturating_sub(2),
|
||||||
|
height: inner.height,
|
||||||
|
};
|
||||||
|
|
||||||
|
let total_rows = mm.cpu_per_core.len();
|
||||||
|
let viewport_rows = content.height as usize;
|
||||||
|
let max_offset = total_rows.saturating_sub(viewport_rows);
|
||||||
|
let offset = scroll_offset.min(max_offset);
|
||||||
|
let show_n = total_rows.saturating_sub(offset).min(viewport_rows);
|
||||||
|
|
||||||
let rows = inner.height as usize;
|
|
||||||
let show_n = rows.min(mm.cpu_per_core.len());
|
|
||||||
let constraints: Vec<Constraint> = (0..show_n).map(|_| Constraint::Length(1)).collect();
|
let constraints: Vec<Constraint> = (0..show_n).map(|_| Constraint::Length(1)).collect();
|
||||||
let vchunks = Layout::default()
|
let vchunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints(constraints)
|
.constraints(constraints)
|
||||||
.split(inner);
|
.split(content);
|
||||||
|
|
||||||
for i in 0..show_n {
|
for i in 0..show_n {
|
||||||
|
let idx = offset + i;
|
||||||
let rect = vchunks[i];
|
let rect = vchunks[i];
|
||||||
let hchunks = Layout::default()
|
let hchunks = Layout::default()
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
.constraints([Constraint::Min(6), Constraint::Length(12)])
|
.constraints([Constraint::Min(6), Constraint::Length(12)])
|
||||||
.split(rect);
|
.split(rect);
|
||||||
|
|
||||||
let curr = mm.cpu_per_core[i].clamp(0.0, 100.0);
|
let curr = mm.cpu_per_core[idx].clamp(0.0, 100.0);
|
||||||
let older = per_core_hist
|
let older = per_core_hist
|
||||||
.deques
|
.deques
|
||||||
.get(i)
|
.get(idx)
|
||||||
.and_then(|d| d.iter().rev().nth(20).copied())
|
.and_then(|d| d.iter().rev().nth(20).copied())
|
||||||
.map(|v| v as f32)
|
.map(|v| v as f32)
|
||||||
.unwrap_or(curr);
|
.unwrap_or(curr);
|
||||||
let trend = if curr > older + 0.2 {
|
let trend = if curr > older + 0.2 { "↑" } else if curr + 0.2 < older { "↓" } else { "╌" };
|
||||||
"↑"
|
|
||||||
} else if curr + 0.2 < older {
|
|
||||||
"↓"
|
|
||||||
} else {
|
|
||||||
"╌"
|
|
||||||
};
|
|
||||||
|
|
||||||
let fg = match curr {
|
let fg = match curr {
|
||||||
x if x < 25.0 => Color::Green,
|
x if x < 25.0 => Color::Green,
|
||||||
@ -95,7 +329,7 @@ pub fn draw_per_core_bars(
|
|||||||
|
|
||||||
let hist: Vec<u64> = per_core_hist
|
let hist: Vec<u64> = per_core_hist
|
||||||
.deques
|
.deques
|
||||||
.get(i)
|
.get(idx)
|
||||||
.map(|d| {
|
.map(|d| {
|
||||||
let max_points = hchunks[0].width as usize;
|
let max_points = hchunks[0].width as usize;
|
||||||
let start = d.len().saturating_sub(max_points);
|
let start = d.len().saturating_sub(max_points);
|
||||||
@ -103,17 +337,49 @@ pub fn draw_per_core_bars(
|
|||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let spark = Sparkline::default()
|
let spark = Sparkline::default().data(&hist).max(100).style(Style::default().fg(fg));
|
||||||
.data(&hist)
|
|
||||||
.max(100)
|
|
||||||
.style(Style::default().fg(fg));
|
|
||||||
f.render_widget(spark, hchunks[0]);
|
f.render_widget(spark, hchunks[0]);
|
||||||
|
|
||||||
let label = format!("cpu{:<2}{}{:>5.1}%", i, trend, curr);
|
let label = format!("cpu{:<2}{}{:>5.1}%", idx, trend, curr);
|
||||||
let line = Line::from(Span::styled(
|
let line = Line::from(Span::styled(
|
||||||
label,
|
label,
|
||||||
Style::default().fg(fg).add_modifier(Modifier::BOLD),
|
Style::default().fg(fg).add_modifier(Modifier::BOLD),
|
||||||
));
|
));
|
||||||
f.render_widget(Paragraph::new(line).right_aligned(), hchunks[1]);
|
f.render_widget(Paragraph::new(line).right_aligned(), hchunks[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Custom 1-col scrollbar with arrows, track, and exact mapping
|
||||||
|
let scroll_area = Rect {
|
||||||
|
x: inner.x + inner.width.saturating_sub(1),
|
||||||
|
y: inner.y,
|
||||||
|
width: 1,
|
||||||
|
height: inner.height,
|
||||||
|
};
|
||||||
|
if scroll_area.height >= 3 {
|
||||||
|
let track = (scroll_area.height - 2) as usize;
|
||||||
|
let total = total_rows.max(1);
|
||||||
|
let view = viewport_rows.clamp(1, total);
|
||||||
|
let max_off = total.saturating_sub(view);
|
||||||
|
|
||||||
|
let thumb_len = ((track * view + total - 1) / total).max(1).min(track);
|
||||||
|
let thumb_top = if max_off == 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
((track - thumb_len) * offset + max_off / 2) / max_off
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build lines: top arrow, track (with thumb), bottom arrow
|
||||||
|
let mut lines: Vec<Line> = Vec::with_capacity(scroll_area.height as usize);
|
||||||
|
lines.push(Line::from(Span::styled("▲", Style::default().fg(SB_ARROW))));
|
||||||
|
for i in 0..track {
|
||||||
|
if i >= thumb_top && i < thumb_top + thumb_len {
|
||||||
|
lines.push(Line::from(Span::styled("█", Style::default().fg(SB_THUMB))));
|
||||||
|
} else {
|
||||||
|
lines.push(Line::from(Span::styled("│", Style::default().fg(SB_TRACK))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines.push(Line::from(Span::styled("▼", Style::default().fg(SB_ARROW))));
|
||||||
|
|
||||||
|
f.render_widget(Paragraph::new(lines), scroll_area);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user