diff options
author | Orhun Parmaksız <orhunparmaksiz@gmail.com> | 2021-11-04 00:23:09 +0300 |
---|---|---|
committer | Orhun Parmaksız <orhunparmaksiz@gmail.com> | 2021-11-04 00:23:09 +0300 |
commit | 8a8ad9e7f0fc5458c567276d47c1f75fa85ab29e (patch) | |
tree | b99637bae2881aac59164833949fb23d7799d769 | |
parent | 20bb8bbd0203ae57fc923b45e7fed06f4a073693 (diff) |
feat: Add options menu for managing the modules (#26)
-rw-r--r-- | src/app.rs | 127 | ||||
-rw-r--r-- | src/kernel/cmd.rs | 14 | ||||
-rw-r--r-- | src/main.rs | 138 | ||||
-rw-r--r-- | src/widgets.rs | 74 |
4 files changed, 314 insertions, 39 deletions
@@ -5,6 +5,7 @@ use crate::kernel::log::KernelLogs; use crate::kernel::Kernel; use crate::style::{Style, StyledText, Symbol}; use crate::util; +use crate::widgets::StatefulList; use clipboard::{ClipboardContext, ClipboardProvider}; use enum_unitary::{enum_unitary, Bounded, EnumUnitary}; use std::error::Error; @@ -13,15 +14,29 @@ use std::slice::Iter; use std::sync::mpsc::Sender; use termion::event::Key; use tui::backend::Backend; -use tui::layout::{Alignment, Constraint, Rect}; +use tui::layout::{Alignment, Constraint, Direction, Layout, Rect}; use tui::style::Style as TuiStyle; use tui::text::{Span, Spans, Text}; -use tui::widgets::{Block as TuiBlock, Borders, Paragraph, Row, Table, Wrap}; +use tui::widgets::{ + Block as TuiBlock, Borders, Clear, List, ListItem, Paragraph, Row, Table, Wrap, +}; use tui::Frame; +use unicode_width::UnicodeWidthStr; /* Table header of the module table */ pub const TABLE_HEADER: &[&str] = &[" Module", "Size", "Used by"]; +/* Available options in the module management menu */ +const OPTIONS: &[(&str, &str)] = &[ + ("unload", "Unload the module"), + ("reload", "Reload the module"), + ("blacklist", "Blacklist the module"), + ("dependent", "Show the dependent modules"), + ("copy", "Copy the module name"), + ("load", "Load a kernel module"), + ("clear", "Clear the ring buffer"), +]; + /* Supported directions of scrolling */ #[derive(Clone, Copy, Debug, PartialEq)] pub enum ScrollDirection { @@ -125,6 +140,8 @@ pub struct App { pub block_index: u8, pub input_mode: InputMode, pub input_query: String, + pub options: StatefulList<(String, String)>, + pub show_options: bool, style: Style, } @@ -144,6 +161,15 @@ impl App { block_index: 0, input_mode: InputMode::None, input_query: String::new(), + options: StatefulList::with_items( + OPTIONS + .iter() + .map(|(option, text)| { + (String::from(*option), String::from(*text)) + }) + .collect(), + ), + show_options: false, style, } } @@ -155,6 +181,8 @@ impl App { self.block_index = 0; self.input_mode = InputMode::None; self.input_query = String::new(); + self.options.state.select(Some(0)); + self.show_options = false; } /** @@ -164,7 +192,9 @@ impl App { * @return TuiStyle */ pub fn block_style(&self, block: Block) -> TuiStyle { - if block == self.selected_block { + if self.show_options { + self.style.colored + } else if block == self.selected_block { self.style.default } else { self.style.colored @@ -402,7 +432,7 @@ impl App { * @param kernel_modules */ pub fn draw_kernel_modules<B>( - &self, + &mut self, frame: &mut Frame<'_, B>, area: Rect, kernel_modules: &mut KernelModules<'_>, @@ -494,6 +524,95 @@ impl App { ]), area, ); + if self.show_options { + self.draw_options_menu(frame, area, kernel_modules); + } + } + + /** + * Draws the options menu as a popup. + * + * @param frame + * @param area + */ + pub fn draw_options_menu<B>( + &mut self, + frame: &mut Frame<'_, B>, + area: Rect, + kernel_modules: &mut KernelModules<'_>, + ) where + B: Backend, + { + let block_title = format!( + "Options ({})", + kernel_modules.list[kernel_modules.index][0] + .split_whitespace() + .next() + .unwrap_or("?") + .trim() + .to_string() + ); + let items = self + .options + .items + .iter() + .map(|(_, text)| ListItem::new(Span::raw(format!(" {}", text)))) + .collect::<Vec<ListItem<'_>>>(); + let (mut percent_y, mut percent_x) = (40, 60); + let text_height = items.iter().map(|v| v.height() as f32).sum::<f32>() + 3.; + if area.height.checked_sub(5).unwrap_or(area.height) as f32 > text_height { + percent_y = ((text_height / area.height as f32) * 100.) as u16; + } + if let Some(text_width) = self + .options + .items + .iter() + .map(|(_, text)| text.width()) + .chain(vec![block_title.width()].into_iter()) + .max() + .map(|v| v as f32 + 7.) + { + if area.width.checked_sub(2).unwrap_or(area.width) as f32 > text_width { + percent_x = ((text_width / area.width as f32) * 100.) as u16; + } + } + let popup_layout = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Percentage((100 - percent_y) / 2), + Constraint::Percentage(percent_y), + Constraint::Percentage((100 - percent_y) / 2), + ] + .as_ref(), + ) + .split(area); + let popup_rect = Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Percentage((100 - percent_x) / 2), + Constraint::Percentage(percent_x), + Constraint::Percentage((100 - percent_x) / 2), + ] + .as_ref(), + ) + .split(popup_layout[1])[1]; + frame.render_widget(Clear, popup_rect); + frame.render_stateful_widget( + List::new(items) + .block( + TuiBlock::default() + .title(Span::styled(block_title, self.style.bold)) + .title_alignment(Alignment::Center) + .style(self.style.default) + .borders(Borders::ALL), + ) + .style(self.style.colored) + .highlight_style(self.style.default), + popup_rect, + &mut self.options.state, + ); } /** diff --git a/src/kernel/cmd.rs b/src/kernel/cmd.rs index 23f39da..ed2625b 100644 --- a/src/kernel/cmd.rs +++ b/src/kernel/cmd.rs @@ -53,6 +53,20 @@ pub enum ModuleCommand { Clear, } +impl TryFrom<String> for ModuleCommand { + type Error = (); + fn try_from(s: String) -> Result<Self, Self::Error> { + match s.as_ref() { + "load" => Ok(Self::Load), + "unload" => Ok(Self::Unload), + "reload" => Ok(Self::Reload), + "blacklist" => Ok(Self::Blacklist), + "clear" => Ok(Self::Clear), + _ => Err(()), + } + } +} + impl ModuleCommand { /** * Get Command struct from a enum element. diff --git a/src/main.rs b/src/main.rs index f261d0c..265bb87 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ mod app; mod event; mod kernel; +mod widgets; #[macro_use] mod util; mod style; @@ -110,6 +111,7 @@ where match events.rx.recv()? { /* Key input events. */ Event::Input(input) => { + let mut hide_options = true; if app.input_mode.is_none() { /* Default input mode. */ match input { @@ -119,7 +121,11 @@ where | Key::Ctrl('c') | Key::Ctrl('d') | Key::Esc => { - break; + if app.show_options { + app.show_options = false; + } else { + break; + } } /* Refresh. */ Key::Char('r') | Key::Char('R') | Key::F(5) => { @@ -130,48 +136,70 @@ where Key::Char('?') | Key::F(1) => { app.show_help_message(&mut kernel.modules); } + Key::Char('m') | Key::Char('o') => { + app.show_options = true; + hide_options = false; + } /* Scroll the selected block up. */ Key::Up | Key::Char('k') | Key::Char('K') | Key::Alt('k') - | Key::Alt('K') => match app.selected_block { - Block::ModuleTable => { - kernel.modules.scroll_list(ScrollDirection::Up) + | Key::Alt('K') => { + if app.show_options { + app.options.previous(); + continue; + } else { + app.options.state.select(Some(0)); } - Block::ModuleInfo => kernel.modules.scroll_mod_info( - ScrollDirection::Up, - input == Key::Alt('k') || input == Key::Alt('K'), - ), - Block::Activities => { - kernel.logs.scroll( + match app.selected_block { + Block::ModuleTable => { + kernel.modules.scroll_list(ScrollDirection::Up) + } + Block::ModuleInfo => kernel.modules.scroll_mod_info( ScrollDirection::Up, input == Key::Alt('k') || input == Key::Alt('K'), - ); + ), + Block::Activities => { + kernel.logs.scroll( + ScrollDirection::Up, + input == Key::Alt('k') + || input == Key::Alt('K'), + ); + } + _ => {} } - _ => {} - }, + } /* Scroll the selected block down. */ Key::Down | Key::Char('j') | Key::Char('J') | Key::Alt('j') - | Key::Alt('J') => match app.selected_block { - Block::ModuleTable => { - kernel.modules.scroll_list(ScrollDirection::Down) + | Key::Alt('J') => { + if app.show_options { + app.options.next(); + continue; + } else { + app.options.state.select(Some(0)); } - Block::ModuleInfo => kernel.modules.scroll_mod_info( - ScrollDirection::Down, - input == Key::Alt('j') || input == Key::Alt('J'), - ), - Block::Activities => { - kernel.logs.scroll( + match app.selected_block { + Block::ModuleTable => { + kernel.modules.scroll_list(ScrollDirection::Down) + } + Block::ModuleInfo => kernel.modules.scroll_mod_info( ScrollDirection::Down, input == Key::Alt('j') || input == Key::Alt('J'), - ); + ), + Block::Activities => { + kernel.logs.scroll( + ScrollDirection::Down, + input == Key::Alt('j') + || input == Key::Alt('J'), + ); + } + _ => {} } - _ => {} - }, + } /* Select the next terminal block. */ Key::Left | Key::Char('h') | Key::Char('H') => { app.selected_block = @@ -348,16 +376,53 @@ where | Key::Char('+') | Key::Char('/') | Key::Insert => { - app.selected_block = Block::UserInput; - app.input_mode = match input { - Key::Char('+') - | Key::Char('i') - | Key::Char('I') - | Key::Insert => InputMode::Load, - _ => InputMode::Search, - }; - if input != Key::Char('\n') { - app.input_query = String::new(); + if input == Key::Char('\n') && app.show_options { + if let Ok(command) = ModuleCommand::try_from( + app.options + .selected() + .map(|(v, _)| v.to_string()) + .unwrap_or_default(), + ) { + if command == ModuleCommand::Load { + events + .tx + .send(Event::Input(Key::Char('+'))) + .unwrap(); + } else { + kernel.modules.set_current_command( + command, + String::new(), + ); + } + } else { + match app + .options + .selected() + .map(|(v, _)| v.as_ref()) + { + Some("dependent") => { + app.show_dependent_modules( + &mut kernel.modules, + ); + } + Some("copy") => app.set_clipboard_contents( + &kernel.modules.current_name, + ), + _ => {} + } + } + } else { + app.selected_block = Block::UserInput; + app.input_mode = match input { + Key::Char('+') + | Key::Char('i') + | Key::Char('I') + | Key::Insert => InputMode::Load, + _ => InputMode::Search, + }; + if input != Key::Char('\n') { + app.input_query = String::new(); + } } } /* Other character input. */ @@ -479,6 +544,9 @@ where _ => {} } } + if hide_options { + app.show_options = false; + } } /* Kernel events. */ Event::Kernel(logs) => { diff --git a/src/widgets.rs b/src/widgets.rs new file mode 100644 index 0000000..0518032 --- /dev/null +++ b/src/widgets.rs @@ -0,0 +1,74 @@ +use tui::widgets::ListState; + +/// List widget with TUI controlled states. +#[derive(Debug)] +pub struct StatefulList<T> { + /// List items (states). + pub items: Vec<T>, + /// State that can be modified by TUI. + pub state: ListState, +} + +impl<T> StatefulList<T> { + /// Constructs a new instance of `StatefulList`. + pub fn new(items: Vec<T>, mut state: ListState) -> StatefulList<T> { + state.select(Some(0)); + Self { items, state } + } + + /// Construct a new `StatefulList` with given items. + pub fn with_items(items: Vec<T>) -> StatefulList<T> { + Self::new(items, ListState::default()) + } + + /// Returns the selected item. + pub fn selected(&self) -> Option<&T> { + self.items.get(self.state.selected()?) + } + + /// Selects the next item. + pub fn next(&mut self) { + let i = match self.state.selected() { + Some(i) => { + if i >= self.items.len() - 1 { + 0 + } else { + i + 1 + } + } + None => 0, + }; + self.state.select(Some(i)); + } + + /// Selects the previous item. + pub fn previous(&mut self) { + let i = match self.state.selected() { + Some(i) => { + if i == 0 { + self.items.len() - 1 + } else { + i - 1 + } + } + None => 0, + }; + self.state.select(Some(i)); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_stateful_list() { + let mut list = StatefulList::with_items(vec!["data1", "data2", "data3"]); + list.state.select(Some(1)); + assert_eq!(Some(&"data2"), list.selected()); + list.next(); + assert_eq!(Some(2), list.state.selected()); + list.previous(); + assert_eq!(Some(1), list.state.selected()); + } +} |