Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
966 changes: 596 additions & 370 deletions tools/scxtop/src/app.rs

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions tools/scxtop/src/keymap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ impl Default for KeyMap {
bindings.insert(Key::Code(KeyCode::Enter), Action::Enter);
bindings.insert(Key::Code(KeyCode::Esc), Action::Esc);
bindings.insert(Key::Code(KeyCode::Backspace), Action::Backspace);
bindings.insert(Key::Code(KeyCode::Tab), Action::FocusNext);

Self { bindings }
}
Expand Down Expand Up @@ -404,6 +405,7 @@ pub fn parse_action(action_str: &str) -> Result<Action> {
"PageDown" => Ok(Action::PageDown),
"PageUp" => Ok(Action::PageUp),
"Enter" => Ok(Action::Enter),
"FocusNext" => Ok(Action::FocusNext),
"Esc" => Ok(Action::Esc),
"Backspace" => Ok(Action::Backspace),
_ => Err(anyhow!("Invalid action: {}", action_str)),
Expand Down
10 changes: 10 additions & 0 deletions tools/scxtop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub mod mcp;
mod mem_stats;
pub mod network_stats;
mod node_data;
pub mod pane;
mod perfetto_trace;
mod power_data;
mod proc_data;
Expand Down Expand Up @@ -52,6 +53,7 @@ pub use llc_data::LlcData;
pub use mem_stats::MemStatSnapshot;
pub use network_stats::NetworkStatSnapshot;
pub use node_data::NodeData;
pub use pane::{PaneFocusManager, PaneScrollState};
pub use perfetto_trace::PerfettoTraceManager;
pub use power_data::{
CStateInfo, CorePowerData, PowerDataCollector, PowerSnapshot, SystemPowerData,
Expand Down Expand Up @@ -459,6 +461,7 @@ pub enum Action {
Exec(ExecAction),
Exit(ExitAction),
Filter,
FocusNext,
Fork(ForkAction),
Kprobe(KprobeAction),
GpuMem(GpuMemAction),
Expand All @@ -469,6 +472,9 @@ pub enum Action {
InputEntry(String),
IPI(IPIAction),
MangoApp(MangoAppAction),
MouseClick { col: u16, row: u16 },
MouseScrollDown { col: u16, row: u16 },
MouseScrollUp { col: u16, row: u16 },
NextEvent,
NextViewState,
PageDown,
Expand Down Expand Up @@ -829,6 +835,10 @@ impl std::fmt::Display for Action {
Action::Down => write!(f, "Down"),
Action::Up => write!(f, "Up"),
Action::Enter => write!(f, "Enter"),
Action::FocusNext => write!(f, "FocusNext"),
Action::MouseClick { .. } => write!(f, "MouseClick"),
Action::MouseScrollUp { .. } => write!(f, "MouseScrollUp"),
Action::MouseScrollDown { .. } => write!(f, "MouseScrollDown"),
_ => write!(f, "{self:?}"),
}
}
Expand Down
13 changes: 13 additions & 0 deletions tools/scxtop/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1167,6 +1167,19 @@ fn run_tui(tui_args: &TuiArgs) -> Result<()> {
let action = get_action(&app, &keymap, ev);
action_tx.send(action)?;
}
Event::Mouse(mouse_event) => {
use ratatui::crossterm::event::{MouseEventKind, MouseButton};
let action = match mouse_event.kind {
MouseEventKind::Down(MouseButton::Left) =>
Action::MouseClick { col: mouse_event.column, row: mouse_event.row },
MouseEventKind::ScrollUp =>
Action::MouseScrollUp { col: mouse_event.column, row: mouse_event.row },
MouseEventKind::ScrollDown =>
Action::MouseScrollDown { col: mouse_event.column, row: mouse_event.row },
_ => Action::None,
};
action_tx.send(action)?;
}
_ => {}
}}

Expand Down
6 changes: 5 additions & 1 deletion tools/scxtop/src/mcp/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,10 @@ pub fn action_to_mcp_event(action: &Action) -> Option<Value> {
| Action::ToggleLocalization
| Action::ToggleHwPressure
| Action::ToggleUncoreFreq
| Action::Up => None,
| Action::Up
| Action::FocusNext
| Action::MouseClick { .. }
| Action::MouseScrollDown { .. }
| Action::MouseScrollUp { .. } => None,
}
}
150 changes: 150 additions & 0 deletions tools/scxtop/src/pane.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// This software may be used and distributed according to the terms of the
// GNU General Public License version 2.

use ratatui::layout::Rect;

/// Scroll state for a single pane.
#[derive(Debug, Clone, Default)]
pub struct PaneScrollState {
/// Current scroll offset (number of items scrolled past).
pub offset: usize,
/// Total number of items in the pane's content.
pub content_len: usize,
/// Number of items that fit in the visible area.
pub visible_len: usize,
}

impl PaneScrollState {
/// Scrolls down by `amount` items, clamping to the maximum offset.
pub fn scroll_down(&mut self, amount: usize) {
let max = self.max_offset();
self.offset = (self.offset + amount).min(max);
}

/// Scrolls up by `amount` items, clamping to 0.
pub fn scroll_up(&mut self, amount: usize) {
self.offset = self.offset.saturating_sub(amount);
}

/// Scrolls down by one page.
pub fn page_down(&mut self) {
self.scroll_down(self.visible_len.saturating_sub(1).max(1));
}

/// Scrolls up by one page.
pub fn page_up(&mut self) {
self.scroll_up(self.visible_len.saturating_sub(1).max(1));
}

/// Returns the maximum scroll offset.
pub fn max_offset(&self) -> usize {
self.content_len.saturating_sub(self.visible_len)
}

/// Returns true if the content exceeds the visible area.
pub fn needs_scroll(&self) -> bool {
self.content_len > self.visible_len
}

/// Updates the content and visible lengths, clamping offset if needed.
pub fn set_content_and_visible(&mut self, content_len: usize, visible_len: usize) {
self.content_len = content_len;
self.visible_len = visible_len;
// Clamp offset to valid range
let max = self.max_offset();
if self.offset > max {
self.offset = max;
}
}
}

/// Manages pane focus and per-pane scroll state within a view.
#[derive(Debug, Clone)]
pub struct PaneFocusManager {
/// Total number of panes in the current view.
pub pane_count: usize,
/// Index of the currently focused pane (0-based).
pub focused: usize,
/// Screen area of each pane (updated each render frame).
pub areas: Vec<Rect>,
/// Per-pane scroll state.
pub scroll_states: Vec<PaneScrollState>,
}

impl PaneFocusManager {
/// Creates a new PaneFocusManager with `pane_count` panes.
pub fn new(pane_count: usize) -> Self {
Self {
pane_count,
focused: 0,
areas: vec![Rect::default(); pane_count],
scroll_states: (0..pane_count).map(|_| PaneScrollState::default()).collect(),
}
}

/// Cycles focus to the next pane (wraps around).
pub fn focus_next(&mut self) {
if self.pane_count > 0 {
self.focused = (self.focused + 1) % self.pane_count;
}
}

/// Sets focus to a specific pane index.
pub fn focus_pane(&mut self, index: usize) {
if index < self.pane_count {
self.focused = index;
}
}

/// Hit-tests a screen position and focuses the pane under it.
/// Returns true if a pane was found at the position.
pub fn focus_at_position(&mut self, col: u16, row: u16) -> bool {
for (i, area) in self.areas.iter().enumerate() {
if col >= area.x
&& col < area.x + area.width
&& row >= area.y
&& row < area.y + area.height
{
self.focused = i;
return true;
}
}
false
}

/// Registers the screen area for a pane during rendering.
pub fn register_area(&mut self, pane_index: usize, area: Rect) {
if pane_index < self.areas.len() {
self.areas[pane_index] = area;
}
}

/// Returns a reference to the focused pane's scroll state.
pub fn focused_scroll(&self) -> &PaneScrollState {
&self.scroll_states[self.focused]
}

/// Returns a mutable reference to the focused pane's scroll state.
pub fn focused_scroll_mut(&mut self) -> &mut PaneScrollState {
&mut self.scroll_states[self.focused]
}

/// Returns true if the pane at `index` is focused.
pub fn is_focused(&self, index: usize) -> bool {
self.focused == index
}

/// Reconfigures for a new view with `pane_count` panes.
/// Resets focus to pane 0 and clears all scroll offsets.
pub fn reconfigure(&mut self, pane_count: usize) {
self.pane_count = pane_count;
self.focused = 0;
self.areas.clear();
self.areas.resize(pane_count, Rect::default());
self.scroll_states.clear();
self.scroll_states
.resize_with(pane_count, PaneScrollState::default);
}
}
6 changes: 4 additions & 2 deletions tools/scxtop/src/render/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ impl ProcessRenderer {
render_tick_rate: bool,
theme: &AppTheme,
_events_list_size: u16,
border_style_override: Option<ratatui::style::Style>,
) -> Result<(Option<i32>, u16)> {
let [scroll_area, data_area] =
Layout::horizontal(vec![Constraint::Min(1), Constraint::Percentage(100)]).areas(area);
Expand All @@ -73,7 +74,7 @@ impl ProcessRenderer {

let block = Block::bordered()
.border_type(BorderType::Rounded)
.border_style(theme.border_style())
.border_style(border_style_override.unwrap_or_else(|| theme.border_style()))
.title_top(
Line::from(format!("Processes (total: {})", proc_data.len()))
.style(theme.title_style())
Expand Down Expand Up @@ -176,6 +177,7 @@ impl ProcessRenderer {
render_tick_rate: bool,
theme: &AppTheme,
_events_list_size: u16,
border_style_override: Option<ratatui::style::Style>,
) -> Result<u16> {
let [scroll_area, data_area] =
Layout::horizontal(vec![Constraint::Min(1), Constraint::Percentage(100)]).areas(area);
Expand All @@ -192,7 +194,7 @@ impl ProcessRenderer {

let block = Block::bordered()
.border_type(BorderType::Rounded)
.border_style(theme.border_style())
.border_style(border_style_override.unwrap_or_else(|| theme.border_style()))
.title_top(
Line::from(format!(
"Process: {:.15} [{}] (total threads: {})",
Expand Down
Loading
Loading