use std::io::{self, stdout};
use std::rc::Rc;
use log::*;
use crate::uistate::{
UIBackend,
UIMode,
THREAD_APPLY_TIME_DEFAULT
};
use crate::system::{
ThreadOrder,
System,
};
use crate::strings;
use crossterm::{
event::{
self,
Event,
KeyEvent,
KeyCode,
KeyModifiers,
DisableMouseCapture,
EnableMouseCapture
},
terminal::{
disable_raw_mode,
enable_raw_mode,
EnterAlternateScreen,
LeaveAlternateScreen,
},
};
use crate::topologycache::{
TopologyObjectID,
TopologyObjectInfo,
TopologyObjectLabelStyle
};
use crate::util;
use ratatui::{prelude::*, widgets::*};
use ratatui_textarea::{Input, Key, TextArea};
fn chunks_vertical(rect: Rect, constraints: &[Constraint]) -> Rc<[Rect]> {
Layout::default().direction(Direction::Vertical).constraints(constraints).split(rect)
}
fn chunks_horizontal(rect: Rect, constraints: &[Constraint]) -> Rc<[Rect]> {
Layout::default().direction(Direction::Horizontal).constraints(constraints).split(rect)
}
fn scrollbar(frame: &mut Frame, rect: Rect, state: &mut ScrollbarState) {
Scrollbar::default()
.begin_symbol(None)
.end_symbol(None)
.track_symbol(None)
.thumb_symbol("▐")
.render(rect, frame.buffer_mut(), state);
}
fn fill_rect(frame: &mut Frame, rect: Rect) {
for fix_y in rect.y .. rect.y + rect.height {
for fix_x in rect.x .. rect.x + rect.width {
frame.buffer_mut().get_mut(fix_x, fix_y).set_char(' ');
}
}
}
fn checkbox_base(frame: &mut Frame, rect: Rect, selected: bool, label: &str, hint: Option<&str>, check: &str) {
let hint_part = if let Some(hint) = hint {format!("<{}> ", hint)} else {"".to_string()};
let checkbox_line = Line::from(vec![
Span::styled("[", Style::default().fg(Color::Gray)),
if selected {
Span::styled(check, Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD))
} else {
Span::styled(" ", Style::default())
},
Span::styled("]", Style::default().fg(Color::Gray)),
Span::styled(format!(" {}{}", hint_part, label), Style::default().fg(Color::White)),
]);
frame.render_widget(Paragraph::new(checkbox_line), rect);
}
fn radio_value(frame: &mut Frame, rect: Rect, selected: bool, label: &str) {
checkbox_base(frame, rect, selected, label, None, "*");
}
fn checkbox(frame: &mut Frame, rect: Rect, selected: bool, label: &str, hint: &str) {
checkbox_base(frame, rect, selected, label, Some(hint), "x");
}
fn set_active_textarea_style<'a>(textarea: &mut TextArea<'a>) {
textarea.set_block(
Block::default()
.borders(Borders::RIGHT | Borders::LEFT)
.style(Style::default().add_modifier(Modifier::BOLD))
.bg(Color::Yellow)
.fg(Color::Black),
);
}
fn set_inactive_textarea_style<'a>(textarea: &mut TextArea<'a>) {
textarea.set_cursor_line_style(Style::default());
textarea.set_cursor_style(Style::default());
textarea.set_block(
Block::default()
.borders(Borders::RIGHT | Borders::LEFT)
.bg(Color::Indexed(234)),
);
}
enum TextInputType {
String,
UInt,
UFloat
}
struct TextInput<'a> {
textarea: TextArea<'a>,
input_type: TextInputType,
received_input: bool
}
impl TextInput<'_> {
fn input(&mut self, input: Input) {
let key = input.key;
let first_line = &self.textarea.lines()[0];
let filter = match (&self.input_type, key) {
(TextInputType::String, _) => true,
(TextInputType::UFloat, Key::Char(c)) =>
c.is_digit(10) || (c == '.' && !first_line.is_empty() && !first_line.contains('.')),
(TextInputType::UInt, Key::Char(c)) =>
c.is_digit(10) && first_line.len() <= 9,
(_, Key::Backspace) => true,
(_, _) => false
};
if filter {
self.textarea.input(input);
}
}
}
pub struct RatatuiFrontend {
active_textinput: Option<usize>,
process_prevnext_pressed: Option<bool>,
thread_updown_pressed: Option<bool>,
update_idx: usize,
current_right_tab_index: usize,
current_help_page_index: usize,
selected_object: Option<TopologyObjectID>,
threads_current_row_index: usize,
selected_thread: Option<usize>,
process_is_focused: bool,
debug_blink: bool,
rect_debug: Option<Rect>
}
const TEXTINPUT_FILTER: usize = 0;
const TEXTINPUT_RUN: usize = 1;
const TEXTINPUT_SUBPROCESS: usize = 2;
const TEXTINPUT_THREAD_APPLY_TIME: usize = 3;
const TEXTINPUT_THREAD_COUNT: usize = 4;
const RIGHT_TAB_INDEX_HELP: usize = 0;
const RIGHT_TAB_INDEX_CPUINFO: usize = 1;
const HELP_PAGE_COUNT: usize = 2;
const TUI_FRAMERATE: u64 = 12;
impl RatatuiFrontend {
pub fn new() -> Self {
Self {
active_textinput: None,
process_prevnext_pressed: None,
thread_updown_pressed: None,
update_idx: 0,
current_right_tab_index: 0,
current_help_page_index: 0,
selected_object: None,
threads_current_row_index: 0,
selected_thread: None,
process_is_focused: true,
debug_blink: false,
rect_debug: None
}
}
pub fn run(&mut self, backend: &mut UIBackend, system: &mut System)
-> anyhow::Result<()>
{
enable_raw_mode()?;
crossterm::execute!(stdout(), EnterAlternateScreen, EnableMouseCapture)?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
let mut text_inputs = [Self::text_input("</>,<?> Process filter text ...", TextInputType::String),
Self::text_input("<:> Command...", TextInputType::String),
Self::text_input("<!> subprocess (optional)...", TextInputType::String),
Self::text_input(format!("{}", THREAD_APPLY_TIME_DEFAULT ), TextInputType::UFloat),
Self::text_input(format!("<t> {}", strings::THREADS_COUNT), TextInputType::UInt),
];
let mut should_quit = false;
while !should_quit {
let mut ui = |frame: &mut Frame| {
self.update(backend, system, frame, &mut text_inputs);
};
terminal.draw(&mut ui)?;
backend.handle_events(system);
should_quit = self.handle_events_term(backend, system, &mut text_inputs).unwrap();
}
disable_raw_mode()?;
crossterm::execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
Ok(())
}
fn update<'a>(&mut self,
backend: &mut UIBackend,
system: &mut System,
frame: &mut Frame,
text_inputs: &mut [TextInput<'a>])
{
if backend.is_first_update() {
self.selected_object = Some(TopologyObjectID::from_cached(system.cache().root()));
}
if self.debug_blink {
frame.render_widget(Block::default().style(Style::default().add_modifier(Modifier::REVERSED)), frame.size());
self.debug_blink = false;
}
let chunks = chunks_vertical(frame.size(), &[Constraint::Min(1), Constraint::Length(1)]);
let rect_main = chunks[0];
let rect_statusbar = chunks[1];
let chunks_bottom = chunks_horizontal(rect_statusbar, &[Constraint::Length(60), Constraint::Min(0)]);
self.rect_debug = Some(chunks_bottom[1]);
self.statusbar(backend, frame, rect_statusbar);
let show_left_panel = !backend.get_hide_process_selection_panel();
let show_right_panel = frame.size().width > 180;
let chunks_main = chunks_horizontal(rect_main, &[
if show_left_panel {
match frame.size().width {
0 ..=180 => Constraint::Percentage(40),
181 ..=360 => Constraint::Percentage(35),
361 .. => Constraint::Percentage(30)
}
} else {
Constraint::Length(0)
},
Constraint::Min(60),
if show_right_panel {Constraint::Length(36)} else {Constraint::Length(0)},
]);
let rect_left = chunks_main[0];
let rect_center = chunks_main[1];
let rect_right = chunks_main[2];
if show_left_panel {
self.process_selection_panel(frame, rect_left, backend, system, text_inputs);
}
backend.update_current_process_data(system);
self.topology_view(frame, rect_center, backend, system, text_inputs);
if show_right_panel {
self.right_panel(frame, rect_right, backend, system);
}
backend.handle_timeouts(system);
if backend.end_update() {
self.threads_current_row_index = 0;
}
self.process_prevnext_pressed = None;
self.thread_updown_pressed = None;
self.update_idx += 1;
}
fn activate_text_input<'a>(&mut self, text_inputs: &mut [TextInput<'a>], index: usize, clear: bool) {
self.active_textinput = Some(index);
let input: &mut TextInput = &mut text_inputs[index];
if clear {
input.textarea = TextArea::default();
}
input.textarea.set_cursor_line_style(Style::default().add_modifier(Modifier::UNDERLINED));
input.textarea.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
set_active_textarea_style(&mut input.textarea);
}
fn deactivate_text_input<'a>(&mut self, text_inputs: &mut [TextInput<'a>]) {
if let Some(index) = self.active_textinput {
set_inactive_textarea_style(&mut text_inputs[index].textarea);
self.active_textinput = None;
}
}
fn text_input(placeholder: impl Into<String>, input_type: TextInputType) -> TextInput<'static> {
let mut textarea = TextArea::default();
set_inactive_textarea_style(&mut textarea);
textarea.insert_str(placeholder);
TextInput{ textarea, input_type, received_input: false }
}
fn handle_events_textinput<'a>(&mut self, input_index: usize, text_inputs: &mut [TextInput<'a>]) -> io::Result<bool> {
let event = event::read()?;
if let Event::Key(KeyEvent{kind: event::KeyEventKind::Press, code, modifiers, .. }) = event {
if code == KeyCode::Char('c') && modifiers.contains(KeyModifiers::CONTROL) {
return Ok(true)
}
}
match event.into() {
Input { key: Key::Esc, .. } | Input { key: Key::Enter, ..} => {
self.deactivate_text_input(text_inputs)
},
input => {
let text = &mut text_inputs[input_index];
if !text.received_input {
text.textarea.undo();
text.received_input = true;
}
text.input(input);
}
}
Ok(false)
}
fn handle_events_filter<'a>(&mut self,
backend: &mut UIBackend,
system: &mut System,
text_inputs: &mut [TextInput<'a>],
clear_textinput: bool)
{
self.activate_text_input(text_inputs, TEXTINPUT_FILTER, clear_textinput);
backend.try_set_next_mode_view_anyprocess(system);
}
fn handle_events_mode_edit<'a>(&mut self,
backend: &mut UIBackend,
system: &mut System,
text_inputs: &mut [TextInput<'a>])
{
match backend.get_mode() {
UIMode::Run(..) => {
let processes = backend.get_filtered_user_processes( system );
if let Some((pid, _process)) = processes.first() {
backend.set_next_mode_edit(**pid, system.affinity_full(), None );
}
self.activate_text_input(text_inputs, TEXTINPUT_FILTER, true);
},
UIMode::View(ref view) => {
backend.set_next_mode_edit(view.get_selected_process(), view.get_process_affinity().clone(), None);
},
UIMode::Edit(..) =>{}
}
}
fn handle_events_apply<'a>(&mut self,
backend: &mut UIBackend,
system: &mut System)
{
let current_order = backend.get_current_order();
match backend.get_mode() {
UIMode::Run(ref run) => {
if run.is_run_enabled(current_order) {
backend.run_shell_command_with_process_affinity(system);
}
}
UIMode::View(..) => {},
UIMode::Edit(ref edit) => {
if edit.is_apply_enabled() {
backend.edit_apply_clicked(system);
}
}
}
}
fn handle_events_rotate_thread_order(&mut self,
backend: &mut UIBackend,
system: &mut System,
ignore_main_thread: bool)
{
let mut current_order = backend.get_current_order();
match (backend.get_mode_mut(), ThreadOrder::rotate(&mut current_order)) {
(UIMode::Run(ref mut run), Some(order)) => {
run.apply_thread_order(&order, system, ignore_main_thread)
},
(UIMode::View(ref view), Some(order)) => {
let selected_process = view.get_selected_process();
let (selected_cores_threads, selected_cores_process) = view.apply_thread_order(order, system, ignore_main_thread);
backend.set_next_mode_edit_hashmap(selected_process, selected_cores_process, selected_cores_threads);
},
(UIMode::View(ref view), None) => {
let selected_process = view.get_selected_process();
let selected_cores_process = view.get_process_affinity().clone();
backend.set_next_mode_edit(selected_process, selected_cores_process, None);
},
(UIMode::Edit(ref mut edit), Some(order)) => {
edit.apply_thread_order(order, system, ignore_main_thread)
},
(_, None) => {}
}
*backend.get_current_order_mut() = current_order;
}
fn handle_thread_change(&mut self,
backend: &UIBackend,
previous: bool,
jump_size: usize)
{
info!("handle_thread_change: selected_thread is {:?}, moving {} by {}",
self.selected_thread, if previous { "back" } else { "forward" }, jump_size );
let thread_count = backend.get_thread_count();
self.selected_thread = match (previous, self.selected_thread, thread_count) {
(true, Some(n), c @ 1..) if n >= jump_size => Some((c - 1).min(n - jump_size)),
(true, _, 1..) => None, (false, None, 1..) => Some(0), (false, Some(n), c @ 1..) => Some((c - 1).min(n + jump_size)),
(_, _, 0) => None, };
self.process_is_focused = self.selected_thread.is_none();
info!("selected thread changed to {:?}", self.selected_thread)
}
fn handle_events_term<'a>(&mut self,
backend: &mut UIBackend,
system: &mut System,
text_inputs: &mut [TextInput<'a>])
-> io::Result<bool>
{
if !event::poll(std::time::Duration::from_millis(1000/TUI_FRAMERATE))?
{
return Ok(false);
}
if let Some(index) = self.active_textinput {
return self.handle_events_textinput(index, text_inputs);
}
if let Event::Key(KeyEvent{kind: event::KeyEventKind::Press, code, modifiers, .. }) = event::read()? {
let left_hidden = backend.get_hide_process_selection_panel() ;
let ignore_main_thread = backend.get_thread_order_ignore_main_thread();
match code {
KeyCode::Char('c') if modifiers.contains(KeyModifiers::CONTROL) =>
return Ok(true),
KeyCode::Char('<') =>
*backend.get_hide_process_selection_panel_mut() = true ,
KeyCode::Char('>') =>
*backend.get_hide_process_selection_panel_mut() = false,
KeyCode::F(10) =>
*backend.get_hide_process_selection_panel_mut() = !left_hidden,
KeyCode::F(1) => {
if self.current_right_tab_index != RIGHT_TAB_INDEX_HELP {
self.current_right_tab_index = RIGHT_TAB_INDEX_HELP;
self.current_help_page_index = 0;
} else {
self.current_help_page_index = (self.current_help_page_index + 1) % HELP_PAGE_COUNT;
}
},
KeyCode::F(2) =>
self.current_right_tab_index = RIGHT_TAB_INDEX_CPUINFO,
KeyCode::F(3) => {
if let UIMode::Run(..) = backend.get_mode() {
} else {
backend.set_next_mode_run(system);
}
},
KeyCode::F(4) => {
backend.try_set_next_mode_view_anyprocess(system);
if let UIMode::Run(..) = backend.get_mode() {
self.activate_text_input(text_inputs, TEXTINPUT_FILTER, true);
}
},
KeyCode::F(5) => {
self.handle_events_mode_edit(backend, system, text_inputs);
},
KeyCode::Char('/') | KeyCode::Char('f') => {
self.handle_events_filter(backend, system, text_inputs, true);
},
KeyCode::Char('?') | KeyCode::Char('F') => {
self.handle_events_filter(backend, system, text_inputs, false);
},
KeyCode::Char(':') | KeyCode::Char('e') => {
self.activate_text_input(text_inputs, TEXTINPUT_RUN, false);
},
KeyCode::Char('!') | KeyCode::Char('p') => {
self.activate_text_input(text_inputs, TEXTINPUT_SUBPROCESS, false);
},
KeyCode::Char('-') => {
*backend.get_thread_order_ignore_main_thread_mut() =
!backend.get_thread_order_ignore_main_thread();
},
KeyCode::Char('+') => {
self.handle_events_rotate_thread_order(backend, system, ignore_main_thread);
},
KeyCode::Char('n') => {
backend.disable_affinity_smt(system);
},
KeyCode::Char('r') => {
if let UIMode::Run(ref _run) = backend.get_mode() {
self.activate_text_input(text_inputs, TEXTINPUT_THREAD_APPLY_TIME, true);
}
},
key @ KeyCode::Down | key @ KeyCode::Up =>
self.process_prevnext_pressed = Some(key == KeyCode::Up),
KeyCode::Char('a') if modifiers.contains(KeyModifiers::CONTROL) => {
self.handle_events_apply(backend, system);
},
KeyCode::Enter if modifiers.is_empty() => {
let id = self.selected_object.clone().unwrap();
let obj = system.cache().get_by_id(id);
let mut current_children = system.cache().get_children(obj);
while let Some(child) = current_children.peek() {
current_children = system.cache().get_children(child);
if child.has_siblings || child.child_count() == 0 {
self.selected_object = Some(child.id());
break;
}
}
},
KeyCode::Esc => {
let id = self.selected_object.clone().unwrap();
let obj = system.cache().get_by_id(id);
let mut current_parent = system.cache().get_parent(obj);
while let Some(parent) = current_parent {
current_parent = system.cache().get_parent(parent);
if parent.has_siblings || current_parent.is_none() {
self.selected_object = Some(parent.id());
break;
}
}
},
key @ KeyCode::Char('w') | key @ KeyCode::Char('s') | key @ KeyCode::Char('a') | key @ KeyCode::Char('d') => {
if self.selected_object.as_ref().unwrap().is_root() {
match key {
KeyCode::Char('w') => self.thread_updown_pressed = Some(true),
KeyCode::Char('s') => self.thread_updown_pressed = Some(false),
KeyCode::Char('a') => self.handle_thread_change(backend, true, 1),
KeyCode::Char('d') => self.handle_thread_change(backend, false, 1),
_ => panic!("unreachable")
}
} else {
let id = self.selected_object.clone().unwrap();
self.selected_object = match key {
KeyCode::Char('w') => Some(system.cache().sibling_up(&id)),
KeyCode::Char('s') => Some(system.cache().sibling_down(&id)),
KeyCode::Char('a') => system.cache().previous_by_type(id).or(self.selected_object.clone()),
KeyCode::Char('d') => system.cache().next_by_type(id).or(self.selected_object.clone()),
_ => panic!("unreachable")
}
}
},
key @ KeyCode::Char('k') | key @ KeyCode::Char('j') =>
self.thread_updown_pressed = Some(key == KeyCode::Char('k')),
KeyCode::Char('h') => self.handle_thread_change(backend, true, 1),
KeyCode::Char('l') => self.handle_thread_change(backend, false, 1),
KeyCode::Char(' ') => {
let id = self.selected_object.clone().unwrap();
let thread_id = self.selected_thread.and_then(|t| backend.get_mode_mut().thread_index_to_id(t, system));
backend.get_mode_mut().set_currently_rendered_thread(thread_id);
backend.handle_group_click(system, system.cache().get_by_id(id));
backend.get_mode_mut().set_currently_rendered_thread(None);
},
KeyCode::Backspace => {
if let UIMode::Run(ref mut run) = backend.get_mode_mut() {
run.clear_threads();
} else if let UIMode::Edit(ref mut edit) = backend.get_mode_mut() {
edit.clear_threads();
}
},
KeyCode::Char('t') => {
if let UIMode::Run(_) = backend.get_mode_mut() {
self.activate_text_input(text_inputs, TEXTINPUT_THREAD_COUNT, true);
}
},
KeyCode::Tab => {
self.process_is_focused = if self.process_is_focused == true {
if self.selected_thread == None {
self.selected_thread = Some(0)
}
false
} else {
true
};
}
_ => {}
}
}
Ok(false)
}
fn statusbar(&self, backend: &UIBackend, frame: &mut Frame, rect: Rect) {
let is_error = backend.get_status().to_lowercase().contains("error");
frame.render_widget(
Paragraph::new(Text::styled(
backend.get_status().as_str(),
Style::default().bg(Color::Indexed(235)).fg(if is_error { Color::Red } else { Color::Green }),
))
.block(Block::default().borders(Borders::NONE).bg(Color::Indexed(235))),
rect
);
}
fn process_selection_panel_line<'a>(
pid: sysinfo::Pid,
process: &'a sysinfo::Process,
system: &System)
-> ListItem<'a>
{
let cmd = UIBackend::get_process_cmd(pid, system);
ListItem::new(Line::from(vec![
Span::styled(format!("{:>8} ", pid.as_u32()), Style::default().fg(Color::Gray)),
Span::styled(System::get_process_name(process), Style::default().fg(Color::White)),
Span::styled(format!(" {}", cmd), Style::default().fg(Color::DarkGray)),
]))
}
fn process_selection_panel<'a>(&self,
frame: &mut Frame,
rect: Rect,
backend: &mut UIBackend,
system: &System,
text_inputs: &[TextInput<'a>])
{
let chunks_panel = chunks_vertical(rect, &[Constraint::Length(1), Constraint::Length(1), Constraint::Min(1)]);
let (rect_filter, rect_count, rect_list) = (chunks_panel[0], chunks_panel[1], chunks_panel[2]);
let chunks_list = chunks_horizontal(rect_list, &[Constraint::Min(1), Constraint::Length(1)]);
let (rect_list, rect_scrollbar) = (chunks_list[0], chunks_list[1]);
let filter = &text_inputs[TEXTINPUT_FILTER];
frame.render_widget(filter.textarea.widget(), rect_filter);
let empty = String::from("");
*backend.get_filter_mut() = if filter.received_input {
filter.textarea.lines().get(0).unwrap_or(&empty).clone()
} else {
empty
};
let processes = backend.get_filtered_user_processes( system );
frame.render_widget(
Paragraph::new(Text::styled(
strings::processes(processes.len()),
Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)
)),
rect_count);
frame.render_widget(Paragraph::new("<↓>,<↑> next/previous")
.style(Style::default().fg(Color::White)).alignment(Alignment::Right), rect_count);
let process_list_items: Vec<_> = processes
.iter()
.map(|p| { Self::process_selection_panel_line(*p.0, p.1, system) })
.collect();
let selected_pid = backend.get_selected_pid();
let selected_index = if let UIMode::Run(..) = backend.get_mode() {
None
} else {
processes.iter().position(|(pid, _process)| **pid == selected_pid)
};
if let Some(index) = selected_index {
let mut scrollbar_state = ScrollbarState::default()
.content_length(process_list_items.len())
.viewport_content_length(rect_list.height.into())
.position(index);
scrollbar(frame, rect_scrollbar, &mut scrollbar_state);
}
let mut process_list_state = ListState::default().with_selected(selected_index);
frame.render_stateful_widget(
List::new(process_list_items)
.highlight_style(Style::default().bg(Color::Indexed(233))
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::UNDERLINED))
.highlight_symbol(">"),
rect_list,
&mut process_list_state,
);
backend.process_selection_panel_keyboard_control(self.process_prevnext_pressed, system);
}
fn right_panel( &self,
frame: &mut Frame,
rect: Rect,
backend: &mut UIBackend,
system: &System)
{
let chunks_panel = chunks_vertical(rect, &[Constraint::Length(1), Constraint::Min(1)]);
let (rect_tabs, rect_content) = (chunks_panel[0], chunks_panel[1]);
let tabs = Tabs::new(vec!["<F1> HELP", "<F2> CPU"])
.block(Block::default().borders(Borders::NONE))
.style(Style::default().fg(Color::White))
.highlight_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::UNDERLINED | Modifier::BOLD))
.select(self.current_right_tab_index)
.divider("");
frame.render_widget(tabs, rect_tabs);
match self.current_right_tab_index {
RIGHT_TAB_INDEX_HELP => {
self.help(frame, rect_content, backend);
},
RIGHT_TAB_INDEX_CPUINFO => {
self.cpu_info(frame, rect_content, system);
},
_ => {
panic!("Unsupported current_right_tab_index: {}", self.current_right_tab_index)
}
}
}
fn help(&self,
frame: &mut Frame,
rect: Rect,
backend: &mut UIBackend)
{
let (mode, mode_shortcuts, is_run) = backend.get_current_mode_help();
let text = match self.current_help_page_index {
0 => {
format!("{}{}{}\n\
", strings::HELP_MAIN, if is_run {strings::HELP_MAIN_RUN} else {""}
, strings::HELP_MAIN_SHORTCUTS_TUI)
},
1 => {
format!("{}\n{}\n", mode, mode_shortcuts)
},
_ => {
panic!("Unsupported current_help_page_index: {}", self.current_help_page_index)
}
};
let chunks_help = chunks_vertical(rect, &[Constraint::Length(1), Constraint::Min(1), Constraint::Length(1)]);
let (rect_help, rect_next) = (chunks_help[1], chunks_help[2]);
frame.render_widget(
Paragraph::new(text).block(Block::default().fg(Color::LightRed)),
rect_help,
);
frame.render_widget(
Paragraph::new(Text::styled(
"<F1> next page",
Style::default().fg(Color::Yellow)
)),
rect_next);
}
fn cpu_info(&self,
frame: &mut Frame,
rect: Rect,
system: &System)
{
let chunks_cpulist = chunks_vertical(rect, &[Constraint::Length(1), Constraint::Min(1)]);
let rect_list = chunks_cpulist[1];
let list_items: Vec<_> = system.cpu_info()
.iter()
.map(|info| {
ListItem::new(format!("{}", info.name))
})
.collect();
let mut list_state = ListState::default();
frame.render_stateful_widget(
List::new(list_items),
rect_list,
&mut list_state,
);
}
fn topology_view<'a>(&mut self,
frame: &mut Frame,
rect: Rect,
backend: &mut UIBackend,
system: &System,
text_inputs: &mut [TextInput<'a>])
{
frame.render_widget(Block::default().bg(Color::Indexed(232)), rect);
let chunks_center = chunks_vertical(rect, &[Constraint::Length(2), Constraint::Min(1)]);
let (rect_header, rect_grid) = (chunks_center[0], chunks_center[1]);
self.topology_header(frame, rect_header, backend, text_inputs);
let topology_rect = self.topology_grid(frame, rect_grid, backend, system).intersection(rect);
let chunks_topbottom =
chunks_vertical(rect_grid, &[Constraint::Length(topology_rect.height), Constraint::Min(1)]);
let (rect_top, rect_bottom) = (chunks_topbottom[0], chunks_topbottom[1]);
let rect_grid_sidebar =
chunks_horizontal(rect_top, &[Constraint::Min(topology_rect.width), Constraint::Length(24)])[1];
self.topology_sidebar(frame, rect_grid_sidebar, backend);
let chunks_bottom =
chunks_vertical(rect_bottom, &[Constraint::Length(1), Constraint::Min(1), Constraint::Length(1)]);
let (rect_grid_buttons_bottom, rect_threads, rect_footer) =
(chunks_bottom[0], chunks_bottom[1], chunks_bottom[2]);
self.topology_buttons_process(frame, rect_grid_buttons_bottom, backend, text_inputs);
self.topology_threads(frame, rect_threads, backend, system, text_inputs);
self.topology_footer(frame, rect_footer, backend);
}
fn thread_name(&self, frame: &mut Frame, rect: Rect, name: &str, edited: bool) {
let chunks = chunks_horizontal(rect, &[Constraint::Length(1), Constraint::Min(1)]);
let (rect_edited, rect_name) = (chunks[0], chunks[1]);
if edited {
let indicator = Text::styled(format!("*"), Style::default().fg(Color::Yellow));
frame.render_widget(Paragraph::new(indicator), rect_edited);
}
let name_render = if name.len() <= rect_name.width.into() {
String::from(name)
} else {
name[0..(rect_name.width.max(1) - 1).into()].to_string() + "…"
};
frame.render_widget(Paragraph::new(name_render).style(Style::default().fg(Color::Red)), rect_name);
}
fn threads_scrollbar(&self,
frame: &mut Frame,
backend: &UIBackend,
rect_threads: Rect,
rect_topo: Rect,
rect_scrollbar: Rect)
-> (usize, usize)
{
let threads_in_row: usize = (rect_threads.width / rect_topo.width).into();
let row_count = backend.get_thread_count().div_ceil(threads_in_row);
let row_count_viewport: usize = rect_threads.height.div_ceil(rect_topo.height).into();
let threads_current_row_index = self.threads_current_row_index.min(row_count - 1);
let mut scrollbar_state = ScrollbarState::default()
.content_length(row_count)
.viewport_content_length(row_count_viewport)
.position(threads_current_row_index);
scrollbar(frame, rect_scrollbar, &mut scrollbar_state);
(threads_current_row_index, threads_in_row)
}
fn topology_thread(&self,
frame: &mut Frame,
backend: &mut UIBackend,
system: &System,
name: &String,
edited: bool,
bounds: Rect) -> Rect
{
let chunks = chunks_vertical(bounds, &[Constraint::Length(1), Constraint::Min(1)]);
let (rect_name, rect_topo) = (chunks[0], chunks[1]);
self.thread_name(frame, rect_name, name.as_str(), edited);
let rect_grid = self.topology_grid(frame, rect_topo, backend, system).intersection(rect_topo);
Rect::new(rect_name.x, rect_name.y, rect_grid.width, rect_grid.height + 1)
}
fn topology_thread_bounds(&self,
row: &mut u16,
col: &mut u16,
rect_threads: Rect,
rect_topo: Rect) -> Rect
{
let max_x_offset = (*col + 1) * rect_topo.width;
if max_x_offset > rect_threads.width {
*col = 0;
*row += 1;
}
let (x, y) = (rect_threads.x + *col * rect_topo.width, rect_threads.y + *row * rect_topo.height);
*col += 1;
let bounds = Rect::new(x, y, rect_topo.width, rect_topo.height);
if bounds.intersects(rect_threads) {
bounds.intersection(rect_threads)
} else {
Rect::new(rect_threads.x, rect_threads.y, 0, 0)
}
}
fn topology_threads_sidebar(&self, frame: &mut Frame, rect: Rect)
{
let rect = Rect::new(rect.x, rect.y - 1, rect.width, rect.height + 1);
let chunks_side = chunks_vertical(rect, &[Constraint::Length(4), Constraint::Min(1)]);
let rect_legend = chunks_side[0];
let block_legend =
Block::default().title(format!("thread selection")).borders(Borders::ALL);
let legend_text = "🅷 🅹 🅺 🅻 navigation\n<Tab> toggle process";
frame.render_widget(
Paragraph::new(legend_text).style(Style::default().fg(Color::White)).block(block_legend), rect_legend);
}
fn topology_threads<'a>(&mut self,
frame: &mut Frame,
rect: Rect,
backend: &mut UIBackend,
system: &System,
text_inputs: &mut [TextInput<'a>])
{
let chunks = chunks_vertical(rect, &[Constraint::Length(1), Constraint::Min(1)]);
let (rect_header, rect_body) = (chunks[0], chunks[1]);
let chunks = chunks_horizontal(rect_body, &[Constraint::Min(1), Constraint::Length(1), Constraint::Length(24)]);
let (rect_threads, rect_scrollbar, rect_sidebar) = (chunks[0], chunks[1], chunks[2]);
self.topology_threads_sidebar(frame, rect_sidebar);
self.topology_threads_header(frame, rect_header, backend, system, text_inputs);
let thread_ids = backend.get_mode().get_all_thread_ids(system);
if backend.get_thread_count() == 0 {
return;
}
let (thread_id, name) = (thread_ids[0].0, &thread_ids[0].1);
let edited = backend.get_mode().is_thread_edited(thread_id);
backend.get_mode_mut().set_currently_rendered_thread(Some(thread_id));
let rect_topo = self.topology_thread(frame, backend, system, name, edited, rect_threads);
let (threads_current_row_index, threads_in_row) =
self.threads_scrollbar(frame, backend, rect_threads, rect_topo, rect_scrollbar);
if let Some(up) = self.thread_updown_pressed {
self.handle_thread_change(backend, up, threads_in_row) ;
}
let threads_current_row_index = if let Some(thread_idx) = self.selected_thread {
thread_idx / threads_in_row
} else {
threads_current_row_index
};
let (mut row, mut col) = if threads_current_row_index == 0 {
(0, 1)
} else {
fill_rect(frame, rect_topo);
(0, 0)
};
let skip_count: usize = (threads_current_row_index * threads_in_row).max(1) - 1;
for (thread_id, name) in thread_ids.iter().skip(skip_count+1) {
let edited = backend.get_mode().is_thread_edited(*thread_id);
backend.get_mode_mut().set_currently_rendered_thread(Some(*thread_id));
let bounds = self.topology_thread_bounds(&mut row, &mut col, rect_threads, rect_topo);
self.topology_thread(frame, backend, system, &name, edited, bounds);
}
backend.get_mode_mut().set_currently_rendered_thread(None);
self.threads_current_row_index = threads_current_row_index;
}
fn topology_threads_header<'a>(&self,
frame: &mut Frame,
rect: Rect,
backend: &mut UIBackend,
system: &System,
text_inputs: &mut [TextInput<'a>])
{
let count_len = if let UIMode::Run(_) = backend.get_mode() {
11
} else {
let count_str = backend.get_thread_count().to_string();
u16::try_from(count_str.len()).unwrap() + 1
};
let chunks = chunks_horizontal( rect, &[
Constraint::Length(count_len), Constraint::Length(8), Constraint::Length(12) ]);
let (rect_count, rect_label, rect_clear) = (chunks[0], chunks[1], chunks[2]);
let thread_count_input = &mut text_inputs[TEXTINPUT_THREAD_COUNT];
let label_style = Style::default().fg(Color::Green).add_modifier(Modifier::BOLD);
let label_paragraph = Paragraph::new(Text::styled(strings::THREADS, label_style));
let clear_style = Style::default().fg(Color::White);
let clear_paragraph = Paragraph::new(Text::styled("<⌫ > ".to_owned() + strings::CLEAR, clear_style));
if let UIMode::Run(..) = backend.get_mode_mut() {
let count_str = if thread_count_input.received_input {
thread_count_input.textarea.lines().get(0)
} else {
None
};
if count_str.is_some_and(|s| !s.is_empty()) {
let count = count_str.unwrap().parse::<usize>().expect("input validation checks this");
backend.run_update_thread_count(count, system);
}
frame.render_widget(thread_count_input.textarea.widget(), rect_count);
frame.render_widget(label_paragraph, rect_label);
frame.render_widget(clear_paragraph, rect_clear);
} else {
frame.render_widget(Paragraph::new(Text::styled(backend.get_thread_count().to_string(), label_style)), rect_count);
frame.render_widget(label_paragraph, rect_label);
if let UIMode::Edit(_) = backend.get_mode_mut() {
frame.render_widget(clear_paragraph, rect_clear);
}
}
}
fn topology_grid(&self,
frame: &mut Frame,
rect: Rect,
backend: &mut UIBackend,
system: &System) -> Rect
{
self.topology_object(frame, rect, rect.x, rect.y, backend, system, system.cache().root())
}
fn topology_object_pu(&self,
frame: &mut Frame,
rect_grid: Rect,
x: u16,
y: u16,
backend: &mut UIBackend,
system: &System,
obj: &TopologyObjectInfo) -> Rect
{
let pu_id = obj.os_index;
let pu_color = self.topology_pu_color(backend, pu_id);
let pu_text = format!("{:0>2}", pu_id);
let pu_rect = Rect::new(x, y, pu_text.len().try_into().unwrap(), 1);
let pu_base_style = Style::default().bg(pu_color).fg(Color::Indexed(16));
let pu_style = self.highlight_style(backend, system, obj, pu_base_style);
if rect_grid.intersects(pu_rect) {
frame.render_widget(
Paragraph::new(Text::styled(pu_text, pu_style)),
rect_grid.intersection(pu_rect));
}
pu_rect
}
fn topology_pu_color(&self, backend: &mut UIBackend, pu_id: u32) -> Color {
backend.pu_color(pu_id, (Color::White, Color::Green, Color::LightYellow, Color::Indexed(172)))
}
fn topology_object_child_rect_fixup(&self,
frame: &mut Frame,
child: &TopologyObjectInfo,
mut rect_child: Rect,
rect_grid: Rect,
row: usize,
col: usize,
rows: usize,
cols: usize) -> Rect
{
if !child.use_frame {
if col + 1 < cols {
rect_child.width += 1;
}
if row + 1 < rows {
rect_child.height += 1;
}
} else {
if col + 1 < cols {
rect_child.width -= 1;
}
if row + 1 < rows {
rect_child.height -= 1;
}
if rect_grid.intersection(rect_child) == rect_child {
if row == 0 && col >= 1 {
let (fix_x, fix_y) = (rect_child.x, rect_child.y);
frame.buffer_mut().set_string(fix_x, fix_y, "┬", Style::default());
}
if row > 0 && col == 0 {
let (fix_x, fix_y) = (rect_child.x, rect_child.y);
frame.buffer_mut().set_string(fix_x, fix_y, "├", Style::default());
}
if row > 0 && col > 0 {
let (fix_x, fix_y) = (rect_child.x, rect_child.y);
frame.buffer_mut().set_string(fix_x, fix_y, "┼", Style::default());
}
if row > 0 && col + 1 == cols {
let (fix_x, fix_y) = (rect_child.right() - 1, rect_child.y);
frame.buffer_mut().set_string(fix_x, fix_y, "┤", Style::default());
}
if row + 1 == rows && col > 0{
let (fix_x, fix_y) = (rect_child.x, rect_child.bottom()-1);
frame.buffer_mut().set_string(fix_x, fix_y, "┴", Style::default());
}
}
}
rect_child
}
fn is_highlighted(&self, backend: &UIBackend, system: &System, obj: &TopologyObjectInfo) -> bool {
let highlight_object = || {
let is_root = self.selected_object.as_ref().unwrap().is_root();
if is_root && obj.is_package() {
system.cache().is_recursive_child_of(obj, self.selected_object.as_ref().unwrap())
} else {
self.selected_object == Some(TopologyObjectID::from_cached(obj))
}
};
if backend.get_mode().currently_rendering_a_thread() {
let selected_thread_id = self.selected_thread.and_then(|t| backend.get_mode().thread_index_to_id(t, system));
let current_thread_selected = selected_thread_id == backend.get_mode().currently_rendering_thread_id();
(!self.process_is_focused) && current_thread_selected && highlight_object()
} else {
(self.process_is_focused || self.selected_thread.is_none()) && highlight_object()
}
}
fn highlight_style(&self, backend: &UIBackend, system: &System, obj: &TopologyObjectInfo, base: Style) -> Style {
if obj.is_pu() && self.is_highlighted(backend, system, obj) {
base.bg(Color::Cyan)
}
else if self.is_highlighted(backend, system, obj) {
base.fg(Color::Cyan).add_modifier(Modifier::BOLD)
} else {
base
}
}
fn topology_object_label(&self,
frame: &mut Frame,
rect_grid: Rect,
compact: bool,
x: u16,
y: u16,
obj: &TopologyObjectInfo)
-> (u16, u16)
{
let Some(label) = &obj.label else {unreachable!();};
let base_style = match &label.style_override {
Some(TopologyObjectLabelStyle::SmallCache) => Style::default().fg(Color::Indexed(248)),
None => Style::default()
};
let horizontal = obj.use_horizontal;
if compact {
let label_text = label.lines.join(" ");
(label_text.len() as u16, 0)
} else {
let label_text: Vec<Line> = if horizontal && label.lines.len() > 1 {
label.lines.iter().map(|s|{Line::from(s.clone())}).collect()
} else {
vec![Line::from(label.lines.join(" "))]
};
let max_length = label_text.iter().map(|s| s.width()).max().unwrap_or(0);
let label_rect = Rect::new(x, y, max_length.try_into().unwrap(), label_text.len().try_into().unwrap());
if rect_grid.intersects(label_rect) {
frame.render_widget(
Paragraph::new(label_text).style(base_style),
rect_grid.intersection(label_rect));
}
(label_rect.width, label_rect.height)
}
}
fn topology_object(&self,
frame: &mut Frame,
rect_grid: Rect,
x: u16,
y: u16,
backend: &mut UIBackend,
system: &System,
obj: &TopologyObjectInfo) -> Rect
{
let (x_inner, mut y_inner) = if obj.use_frame { (x + 1, y + 1) } else { (x, y) };
let compact = obj.use_frame && obj.object_type == hwloc2::ObjectType::Package;
if obj.object_type == hwloc2::ObjectType::PU {
return self.topology_object_pu( frame, rect_grid, x, y, backend, system, obj);
}
let children_iter_base = system.cache().get_children(obj);
let mut first_group = true;
let mut x_grid_start = x_inner;
let mut max_group_x = 0;
let children_iter = children_iter_base.clone();
children_iter.by_group(|children, _last|{
let horizontal = obj.use_horizontal;
let draw_label = first_group && !backend.get_mode().currently_rendering_a_thread();
if draw_label && obj.label.is_some() {
let (label_w, label_h) =
self.topology_object_label(frame, rect_grid, compact, x_inner, y_inner, obj);
if horizontal {
x_grid_start += label_w;
} else {
y_inner += label_h;
max_group_x = max_group_x.max(x_grid_start + label_w);
}
}
let (rows, cols) = util::squarish_grid_dimensions(children.len(), horizontal);
for row in 0..rows {
let mut x_grid = x_grid_start;
let mut max_height = 0;
for (col, child) in children.subiter(row * cols, (row + 1) * cols).enumerate() {
let rect_child = self.topology_object(frame, rect_grid, x_grid, y_inner,
backend, system, child);
let rect_child = if rect_child.intersects(rect_grid) {
self.topology_object_child_rect_fixup(frame, child, rect_child,
rect_grid, row, col, rows, cols)
} else {
Rect::new(rect_child.x, rect_child.y, 0, 0)
};
x_grid += rect_child.width;
max_height = max_height.max(rect_child.height);
}
y_inner += max_height;
max_group_x = max_group_x.max(x_grid);
}
first_group = false;
});
let mut object_rect = Rect::new(x, y, max_group_x - x, y_inner - y);
if obj.use_frame {
object_rect.width += 1;
object_rect.height += 1;
if rect_grid.intersects(object_rect) {
let highlight_style = self.highlight_style(backend, system, obj, Style::default());
let block = if compact && obj.label.is_some() {
let label_text = obj.label.as_ref().unwrap().lines.join(" ");
Block::default().border_style(highlight_style).borders(Borders::ALL).title(label_text)
} else {
Block::default().border_style(highlight_style).borders(Borders::ALL)
};
frame.render_widget(block, rect_grid.intersection(object_rect));
}
}
return object_rect;
}
fn topology_header<'a>(&self,
frame: &mut Frame,
rect: Rect,
backend: &mut UIBackend,
text_inputs: &[TextInput<'a>])
{
let chunks_header = chunks_vertical(rect, &[Constraint::Length(1), Constraint::Length(1)]);
let (rect_tabs, rect_mode) = (chunks_header[0], chunks_header[1]);
let current_tab_index = match backend.get_mode() {
UIMode::Run(..) => 0,
UIMode::View(..) => 1,
UIMode::Edit(..) => 2,
};
let tabs = Tabs::new(vec!["<F3> RUN", "<F4> VIEW", "<F5> EDIT"])
.block(Block::default().borders(Borders::NONE))
.style(Style::default().fg(Color::White))
.highlight_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::UNDERLINED | Modifier::BOLD))
.select(current_tab_index)
.divider("");
frame.render_widget(tabs, rect_tabs);
if let UIMode::Run(ref mut run) = backend.get_mode_mut() {
let chunks_mode = chunks_horizontal(rect_mode, &[Constraint::Percentage(60), Constraint::Percentage(40)]);
let (rect_run, rect_subprocess) = (chunks_mode[0], chunks_mode[1]);
let mut draw_input = |rect, input: &TextInput<'a>, target: &mut String| {
frame.render_widget(input.textarea.widget(), rect);
if input.received_input == false {
return;
}
if let Some(text) = input.textarea.lines().get(0) {
*target = text.clone();
}
};
draw_input( rect_run, &text_inputs[TEXTINPUT_RUN], run.get_command_mut());
draw_input( rect_subprocess, &text_inputs[TEXTINPUT_SUBPROCESS], run.get_subprocess_pattern_mut());
}
}
fn topology_buttons_process<'a>(&self,
frame: &mut Frame,
rect: Rect,
backend: &mut UIBackend,
text_inputs: &mut[TextInput<'a>])
{
let chunks = chunks_horizontal(rect, &[
Constraint::Length(15), Constraint::Length(30), Constraint::Length(10), Constraint::Length(1), Constraint::Min(1)
]);
let (rect_nosmt, rect_apply_label, rect_apply_time, rect_s) =
(chunks[0], chunks[1], chunks[2], chunks[3]);
frame.render_widget(Block::default().style(Style::default().add_modifier(Modifier::UNDERLINED)), rect);
frame.render_widget(
Paragraph::new(Text::styled(format!("<n> {}", strings::NO_SMT), Style::default().fg(Color::White))),
rect_nosmt);
let current_order = backend.get_current_order();
if let UIMode::Run(ref mut run) = backend.get_mode_mut() {
if run.have_thread_affinities(current_order) {
frame.render_widget(
Paragraph::new(Text::styled(format!("<r> {}", strings::APPLY_THREAD_AFFINITIES), Style::default().fg(Color::White))),
rect_apply_label);
let thread_apply_time = &mut text_inputs[TEXTINPUT_THREAD_APPLY_TIME];
let time_str = if thread_apply_time.received_input {
thread_apply_time.textarea.lines().get(0)
} else {
None
};
if time_str.is_some_and(|s| !s.is_empty()) {
let time = time_str.unwrap().parse::<f64>().expect("input validation checks this");
set_active_textarea_style(&mut thread_apply_time.textarea);
*run.get_thread_apply_time_mut() = time
}
frame.render_widget(thread_apply_time.textarea.widget(), rect_apply_time);
frame.render_widget(
Paragraph::new(Text::styled("s", Style::default().fg(Color::White))),
rect_s);
}
}
}
fn topology_sidebar(&self,
frame: &mut Frame,
rect: Rect,
backend: &mut UIBackend)
{
let rect = Rect::new(rect.x, rect.y, rect.width, rect.height + 1);
let compact = rect.height < 14;
let chunks_side = chunks_vertical(rect, &[
Constraint::Length(7),
if compact { Constraint::Length(6) } else { Constraint::Length(7) }
]);
let (rect_side_buttons_block, rect_legend) = (chunks_side[0], chunks_side[1]);
let buttons_borders = Borders::LEFT | Borders::RIGHT | Borders::TOP; let block_side_buttons = Block::default().title(format!("<+> {}", strings::THREAD_ORDER))
.borders(buttons_borders);
let rect_side_buttons = block_side_buttons.inner(rect_side_buttons_block);
frame.render_widget(block_side_buttons, rect_side_buttons_block);
let chunks_side_buttons = chunks_vertical(rect_side_buttons,
&[Constraint::Min(1), Constraint::Length(1)]);
let (rect_order_radios, rect_ignore_main_thread) = (chunks_side_buttons[0], chunks_side_buttons[1]);
let ignore_main_thread = backend.get_thread_order_ignore_main_thread();
self.thread_order_radios(frame, rect_order_radios, backend.get_current_order());
checkbox(frame, rect_ignore_main_thread, ignore_main_thread, strings::IGNORE_MAIN, "-");
let block_legend =
Block::default().title(format!("core control")).borders(Borders::ALL);
let wasd_text = format!("{} navigation\n<Enter> select/in\n<Esc> out\n<Space> toggle cores",
if compact {"🆆 🅰 🆂 🅳"} else {" 🆆 \n🅰 🆂 🅳 "});
frame.render_widget(
Paragraph::new(wasd_text).style(Style::default().fg(Color::White)).block(block_legend), rect_legend);
frame.buffer_mut().set_string(rect_legend.x, rect_legend.y, "├", Style::default());
frame.buffer_mut().set_string(rect_legend.right() - 1, rect_legend.y, "┤", Style::default());
}
fn thread_order_radios(&self, frame: &mut Frame, rect: Rect, order: Option<ThreadOrder>) {
let chunks_radios = chunks_vertical(rect, &[
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(1),
]);
radio_value(frame, chunks_radios[0], order == Some(ThreadOrder::ByLCore), "HW threads");
radio_value(frame, chunks_radios[1], order == Some(ThreadOrder::ByCore), "cores");
radio_value(frame, chunks_radios[2], order == Some(ThreadOrder::ByL3), "L3");
radio_value(frame, chunks_radios[3], order == None, "none");
}
fn topology_footer(&self,
frame: &mut Frame,
rect: Rect,
backend: &mut UIBackend)
{
let current_order = backend.get_current_order();
let mut draw_run = |enabled, label| {
let modifier = if enabled { Modifier::empty() } else {Modifier::CROSSED_OUT};
let text = Text::styled(format!("<C-a> {}", label), Style::default().add_modifier(modifier));
let block = Block::default().fg(if enabled { Color::Yellow } else {Color::DarkGray});
frame.render_widget(Paragraph::new(text).alignment(Alignment::Right).block(block), rect);
};
match backend.get_mode() {
UIMode::Run(ref run) => {
if let Some(duration_ratio) = run.wait_for_thread_start_ratio() {
frame.render_widget(Gauge::default().ratio(duration_ratio.min(1.0).into()).use_unicode(true), rect);
} else {
draw_run(run.is_run_enabled(current_order), "Run");
}
},
UIMode::View(..) => {},
UIMode::Edit(ref edit) => {
draw_run(edit.is_apply_enabled(), "Apply");
},
}
}
}