summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorOrhun Parmaksız <orhunparmaksiz@gmail.com>2021-11-04 00:23:09 +0300
committerOrhun Parmaksız <orhunparmaksiz@gmail.com>2021-11-04 00:23:09 +0300
commit8a8ad9e7f0fc5458c567276d47c1f75fa85ab29e (patch)
treeb99637bae2881aac59164833949fb23d7799d769
parent20bb8bbd0203ae57fc923b45e7fed06f4a073693 (diff)
feat: Add options menu for managing the modules (#26)
-rw-r--r--src/app.rs127
-rw-r--r--src/kernel/cmd.rs14
-rw-r--r--src/main.rs138
-rw-r--r--src/widgets.rs74
4 files changed, 314 insertions, 39 deletions
diff --git a/src/app.rs b/src/app.rs
index 06494e1..dba445f 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -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());
+ }
+}