Initial upload with CI/CD workflow
This commit is contained in:
673
app/src/tui/mod.rs
Normal file
673
app/src/tui/mod.rs
Normal file
@@ -0,0 +1,673 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use crossterm::
|
||||||
|
event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
|
||||||
|
execute,
|
||||||
|
terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType},
|
||||||
|
};
|
||||||
|
use ratatui::
|
||||||
|
backend::CrosstermBackend,
|
||||||
|
layout::{Constraint, Direction, Layout, Rect},
|
||||||
|
style::{Color, Modifier, Style, Stylize},
|
||||||
|
text::{Line, Span, Text},
|
||||||
|
widgets::{
|
||||||
|
Block, Borders, Cell, HighlightSpacing, List, ListItem, ListState, Paragraph,
|
||||||
|
Row, Table, Tabs, Widget,
|
||||||
|
},
|
||||||
|
Terminal,
|
||||||
|
};
|
||||||
|
use std::io::{self, Stdout};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use crate::config::{AppConfig, Config};
|
||||||
|
use crate::parser::{Argument, CommandInfo, SubCommand};
|
||||||
|
|
||||||
|
enum ActiveTab {
|
||||||
|
Commands,
|
||||||
|
Arguments,
|
||||||
|
Wizard,
|
||||||
|
Help,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum InputMode {
|
||||||
|
Normal,
|
||||||
|
Editing,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WizardState {
|
||||||
|
selected_args: Vec<String>,
|
||||||
|
arg_values: Vec<(String, String)>,
|
||||||
|
current_arg_index: usize,
|
||||||
|
input_buffer: String,
|
||||||
|
input_mode: InputMode,
|
||||||
|
validation_errors: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WizardState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
selected_args: Vec::new(),
|
||||||
|
arg_values: Vec::new(),
|
||||||
|
current_arg_index: 0,
|
||||||
|
input_buffer: String::new(),
|
||||||
|
input_mode: InputMode::Normal,
|
||||||
|
validation_errors: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TuiApp {
|
||||||
|
command_info: CommandInfo,
|
||||||
|
config: AppConfig,
|
||||||
|
list_state: ListState,
|
||||||
|
tab_state: usize,
|
||||||
|
active_tab: ActiveTab,
|
||||||
|
search_query: String,
|
||||||
|
search_mode: bool,
|
||||||
|
wizard_state: WizardState,
|
||||||
|
filtered_commands: Vec<String>,
|
||||||
|
should_quit: bool,
|
||||||
|
last_tick: Instant,
|
||||||
|
help_visible: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TuiApp {
|
||||||
|
pub fn new(command_info: CommandInfo, config: Config) -> Self {
|
||||||
|
let subcommand_names: Vec<String> = command_info.subcommands
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.name.clone())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
command_info,
|
||||||
|
config: config.config,
|
||||||
|
list_state: ListState::default().with_selected(Some(0)),
|
||||||
|
tab_state: 0,
|
||||||
|
active_tab: ActiveTab::Commands,
|
||||||
|
search_query: String::new(),
|
||||||
|
search_mode: false,
|
||||||
|
wizard_state: WizardState::default(),
|
||||||
|
filtered_commands: subcommand_names.clone(),
|
||||||
|
should_quit: false,
|
||||||
|
last_tick: Instant::now(),
|
||||||
|
help_visible: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(&mut self) -> Result<()> {
|
||||||
|
enable_raw_mode()?;
|
||||||
|
let mut stdout = io::stdout();
|
||||||
|
execute!(stdout, Clear(ClearType::All))?;
|
||||||
|
|
||||||
|
let backend = CrosstermBackend::new(stdout);
|
||||||
|
let mut terminal = Terminal::new(backend)?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
terminal.draw(|f| self.render(f))?;
|
||||||
|
|
||||||
|
if event::poll(Duration::from_millis(50))? {
|
||||||
|
if let Event::Key(key) = event::read()? {
|
||||||
|
self.handle_key(key)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.should_quit {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if Instant::now().duration_since(self.last_tick) > Duration::from_secs(300) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disable_raw_mode()?;
|
||||||
|
execute!(terminal.backend_mut(), Clear(ClearType::All))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_key(&mut self, key: KeyEvent) -> Result<()> {
|
||||||
|
match self.active_tab {
|
||||||
|
ActiveTab::Commands => self.handle_commands_tab(key),
|
||||||
|
ActiveTab::Arguments => self.handle_arguments_tab(key),
|
||||||
|
ActiveTab::Wizard => self.handle_wizard_tab(key),
|
||||||
|
ActiveTab::Help => self.handle_help_tab(key),
|
||||||
|
}
|
||||||
|
|
||||||
|
if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) {
|
||||||
|
self.should_quit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if key.code == KeyCode::Esc {
|
||||||
|
if self.search_mode {
|
||||||
|
self.search_mode = false;
|
||||||
|
} else if self.help_visible {
|
||||||
|
self.help_visible = false;
|
||||||
|
} else {
|
||||||
|
self.should_quit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if key.code == KeyCode::Char('/') && !self.search_mode {
|
||||||
|
self.search_mode = true;
|
||||||
|
self.search_query.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if key.code == KeyCode::Char('?') {
|
||||||
|
self.help_visible = !self.help_visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.search_mode {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Tab => {
|
||||||
|
self.tab_state = (self.tab_state + 1) % 4;
|
||||||
|
self.active_tab = match self.tab_state {
|
||||||
|
0 => ActiveTab::Commands,
|
||||||
|
1 => ActiveTab::Arguments,
|
||||||
|
2 => ActiveTab::Wizard,
|
||||||
|
3 => ActiveTab::Help,
|
||||||
|
_ => ActiveTab::Commands,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_commands_tab(&mut self, key: KeyEvent) {
|
||||||
|
if self.search_mode {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Enter => {
|
||||||
|
self.search_mode = false;
|
||||||
|
}
|
||||||
|
KeyCode::Backspace => {
|
||||||
|
self.search_query.pop();
|
||||||
|
self.update_filtered_commands();
|
||||||
|
}
|
||||||
|
KeyCode::Char(c) => {
|
||||||
|
self.search_query.push(c);
|
||||||
|
self.update_filtered_commands();
|
||||||
|
}
|
||||||
|
KeyCode::Esc => {
|
||||||
|
self.search_mode = false;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Up => {
|
||||||
|
if let Some(selected) = self.list_state.selected() {
|
||||||
|
let new_selected = if selected == 0 {
|
||||||
|
self.filtered_commands.len().saturating_sub(1)
|
||||||
|
} else {
|
||||||
|
selected - 1
|
||||||
|
};
|
||||||
|
self.list_state.select(Some(new_selected));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Down => {
|
||||||
|
if let Some(selected) = self.list_state.selected() {
|
||||||
|
let new_selected = (selected + 1) % self.filtered_commands.len();
|
||||||
|
self.list_state.select(Some(new_selected));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
if let Some(selected) = self.list_state.selected() {
|
||||||
|
if let Some(cmd_name) = self.filtered_commands.get(selected) {
|
||||||
|
self.build_command(cmd_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_arguments_tab(&mut self, key: KeyEvent) {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Up => {
|
||||||
|
if let Some(selected) = self.list_state.selected() {
|
||||||
|
let len = self.command_info.arguments.len();
|
||||||
|
if len > 0 {
|
||||||
|
let new_selected = if selected == 0 {
|
||||||
|
len - 1
|
||||||
|
} else {
|
||||||
|
selected - 1
|
||||||
|
};
|
||||||
|
self.list_state.select(Some(new_selected));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Down => {
|
||||||
|
if let Some(selected) = self.list_state.selected() {
|
||||||
|
let len = self.command_info.arguments.len();
|
||||||
|
if len > 0 {
|
||||||
|
let new_selected = (selected + 1) % len;
|
||||||
|
self.list_state.select(Some(new_selected));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_wizard_tab(&mut self, key: KeyEvent) {
|
||||||
|
match self.wizard_state.input_mode {
|
||||||
|
InputMode::Normal => match key.code {
|
||||||
|
KeyCode::Up => {
|
||||||
|
self.wizard_state.current_arg_index =
|
||||||
|
self.wizard_state.current_arg_index.saturating_sub(1);
|
||||||
|
}
|
||||||
|
KeyCode::Down => {
|
||||||
|
let len = self.command_info.arguments.len();
|
||||||
|
if len > 0 {
|
||||||
|
self.wizard_state.current_arg_index =
|
||||||
|
(self.wizard_state.current_arg_index + 1) % len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
self.wizard_state.input_mode = InputMode::Editing;
|
||||||
|
self.wizard_state.input_buffer.clear();
|
||||||
|
}
|
||||||
|
KeyCode::Backspace => {
|
||||||
|
if let Some(pos) = self.wizard_state.selected_args.iter().position(|s| {
|
||||||
|
s == self.command_info.arguments
|
||||||
|
.get(self.wizard_state.current_arg_index)
|
||||||
|
.map(|a| a.name.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
}) {
|
||||||
|
self.wizard_state.selected_args.remove(pos);
|
||||||
|
self.wizard_state.arg_values.remove(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
InputMode::Editing => match key.code {
|
||||||
|
KeyCode::Enter => {
|
||||||
|
self.wizard_state.input_mode = InputMode::Normal;
|
||||||
|
if !self.wizard_state.input_buffer.is_empty() {
|
||||||
|
let arg = &self.command_info.arguments[self.wizard_state.current_arg_index];
|
||||||
|
self.wizard_state.selected_args.push(arg.name.clone());
|
||||||
|
self.wizard_state
|
||||||
|
.arg_values
|
||||||
|
.push((arg.name.clone(), self.wizard_state.input_buffer.clone()));
|
||||||
|
}
|
||||||
|
self.wizard_state.input_buffer.clear();
|
||||||
|
}
|
||||||
|
KeyCode::Esc => {
|
||||||
|
self.wizard_state.input_mode = InputMode::Normal;
|
||||||
|
self.wizard_state.input_buffer.clear();
|
||||||
|
}
|
||||||
|
KeyCode::Backspace => {
|
||||||
|
self.wizard_state.input_buffer.pop();
|
||||||
|
}
|
||||||
|
KeyCode::Char(c) => {
|
||||||
|
self.wizard_state.input_buffer.push(c);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_help_tab(&mut self, key: KeyEvent) {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Up => {}
|
||||||
|
KeyCode::Down => {}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_filtered_commands(&mut self) {
|
||||||
|
if self.search_query.is_empty() {
|
||||||
|
self.filtered_commands = self
|
||||||
|
.command_info
|
||||||
|
.subcommands
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.name.clone())
|
||||||
|
.collect();
|
||||||
|
} else {
|
||||||
|
self.filtered_commands = self
|
||||||
|
.command_info
|
||||||
|
.subcommands
|
||||||
|
.iter()
|
||||||
|
.filter(|s| s.name.contains(&self.search_query))
|
||||||
|
.map(|s| s.name.clone())
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_command(&self, subcommand: &str) -> String {
|
||||||
|
let mut cmd = vec![self.command_info.name.clone(), subcommand.to_string()];
|
||||||
|
|
||||||
|
for (arg, value) in &self.wizard_state.arg_values {
|
||||||
|
if let Some(long) = &self.command_info.arguments.iter().find(|a| &a.name == arg).and_then(|a| a.long.clone()) {
|
||||||
|
cmd.push(format!("--{}", long));
|
||||||
|
cmd.push(value.clone());
|
||||||
|
} else {
|
||||||
|
cmd.push(value.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, f: &mut ratatui::Frame) {
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Length(3),
|
||||||
|
Constraint::Min(0),
|
||||||
|
Constraint::Length(1),
|
||||||
|
])
|
||||||
|
.split(f.size());
|
||||||
|
|
||||||
|
self.render_header(f, chunks[0]);
|
||||||
|
self.render_content(f, chunks[1]);
|
||||||
|
self.render_status_bar(f, chunks[2]);
|
||||||
|
|
||||||
|
if self.help_visible {
|
||||||
|
self.render_help_popup(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.search_mode {
|
||||||
|
self.render_search(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_header(&self, f: &mut ratatui::Frame, area: Rect) {
|
||||||
|
let tabs = vec![
|
||||||
|
"Commands",
|
||||||
|
"Arguments",
|
||||||
|
"Wizard",
|
||||||
|
"Help",
|
||||||
|
];
|
||||||
|
|
||||||
|
let tab_titles: Vec<Line> = tabs
|
||||||
|
.iter()
|
||||||
|
.map(|t| Line::from(*t).centered())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let tabs_widget = Tabs::new(tab_titles)
|
||||||
|
.select(self.tab_state)
|
||||||
|
.style(Style::default().fg(Color::Gray))
|
||||||
|
.highlight_style(
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::Cyan)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
)
|
||||||
|
.divider(Span::raw(" | "));
|
||||||
|
|
||||||
|
f.render_widget(tabs_widget, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_content(&mut self, f: &mut ratatui::Frame, area: Rect) {
|
||||||
|
match self.active_tab {
|
||||||
|
ActiveTab::Commands => self.render_commands_tab(f, area),
|
||||||
|
ActiveTab::Arguments => self.render_arguments_tab(f, area),
|
||||||
|
ActiveTab::Wizard => self.render_wizard_tab(f, area),
|
||||||
|
ActiveTab::Help => self.render_help_tab(f, area),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_commands_tab(&self, f: &mut ratatui::Frame, area: Rect) {
|
||||||
|
let items: Vec<ListItem> = self
|
||||||
|
.filtered_commands
|
||||||
|
.iter()
|
||||||
|
.map(|cmd_name| {
|
||||||
|
let subcmd = self
|
||||||
|
.command_info
|
||||||
|
.subcommands
|
||||||
|
.iter()
|
||||||
|
.find(|s| &s.name == cmd_name);
|
||||||
|
|
||||||
|
let description = subcmd.map(|s| s.description.clone()).unwrap_or_default();
|
||||||
|
ListItem::new(Line::from(vec![
|
||||||
|
Span::styled(cmd_name, Style::default().fg(Color::Cyan).bold()),
|
||||||
|
Span::raw(" - "),
|
||||||
|
Span::styled(description, Style::default().fg(Color::Gray)),
|
||||||
|
]))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let list = List::new(items)
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.title(format!("{} Commands", self.command_info.name))
|
||||||
|
.borders(Borders::ALL),
|
||||||
|
)
|
||||||
|
.highlight_style(Style::default().bg(Color::DarkGray).fg(Color::White))
|
||||||
|
.highlight_spacing(HighlightSpacing::Always);
|
||||||
|
|
||||||
|
f.render_stateful_widget(list, area, &mut self.list_state.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_arguments_tab(&self, f: &mut ratatui::Frame, area: Rect) {
|
||||||
|
let rows: Vec<Row> = self
|
||||||
|
.command_info
|
||||||
|
.arguments
|
||||||
|
.iter()
|
||||||
|
.map(|arg| {
|
||||||
|
let flags = vec![
|
||||||
|
arg.short.map(|s| format!("-{}", s)).unwrap_or_default(),
|
||||||
|
arg.long.clone().unwrap_or_default(),
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
Row::new(vec![
|
||||||
|
Cell::from(flags),
|
||||||
|
Cell::from(arg.description.clone()),
|
||||||
|
Cell::from(if arg.required { "Yes" } else { "No" }),
|
||||||
|
Cell::from(arg.default_value.clone().unwrap_or_default()),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let table = Table::new(rows)
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.title("Arguments")
|
||||||
|
.borders(Borders::ALL),
|
||||||
|
)
|
||||||
|
.header(
|
||||||
|
Row::new(vec!["Flags", "Description", "Required", "Default"])
|
||||||
|
.style(Style::default().fg(Color::Yellow).bold()),
|
||||||
|
)
|
||||||
|
.widths(&[
|
||||||
|
Constraint::Percentage(30),
|
||||||
|
Constraint::Percentage(45),
|
||||||
|
Constraint::Percentage(10),
|
||||||
|
Constraint::Percentage(15),
|
||||||
|
]);
|
||||||
|
|
||||||
|
f.render_widget(table, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_wizard_tab(&mut self, f: &mut ratatui::Frame, area: Rect) {
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Min(0), Constraint::Length(3)])
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
let args_list: Vec<ListItem> = self
|
||||||
|
.command_info
|
||||||
|
.arguments
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, arg)| {
|
||||||
|
let is_selected = i == self.wizard_state.current_arg_index;
|
||||||
|
let style = if is_selected {
|
||||||
|
if let InputMode::Editing = self.wizard_state.input_mode {
|
||||||
|
Style::default().bg(Color::Blue).fg(Color::White)
|
||||||
|
} else {
|
||||||
|
Style::default().bg(Color::DarkGray).fg(Color::White)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Style::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let value = self
|
||||||
|
.wizard_state
|
||||||
|
.arg_values
|
||||||
|
.iter()
|
||||||
|
.find(|(n, _)| n == &arg.name)
|
||||||
|
.map(|(_, v)| format!(": {}", v))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
ListItem::new(Line::from(vec![Span::styled(
|
||||||
|
format!("{} {}{}", arg.name, arg.description, value),
|
||||||
|
style,
|
||||||
|
)]))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let list = List::new(args_list)
|
||||||
|
.block(Block::default().title("Command Arguments").borders(Borders::ALL))
|
||||||
|
.highlight_spacing(HighlightSpacing::Always);
|
||||||
|
|
||||||
|
f.render_stateful_widget(list, chunks[0], &mut self.list_state.clone());
|
||||||
|
|
||||||
|
let input_block = Block::default()
|
||||||
|
.title("Input")
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.style(Style::default().bg(Color::Black));
|
||||||
|
|
||||||
|
let input_text = match self.wizard_state.input_mode {
|
||||||
|
InputMode::Normal => Paragraph::new("Press Enter to edit").style(Style::default().fg(Color::Gray)),
|
||||||
|
InputMode::Editing => {
|
||||||
|
Paragraph::new(&self.wizard_state.input_buffer).style(Style::default().fg(Color::Green))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
f.render_widget(input_block, chunks[1]);
|
||||||
|
f.render_widget(input_text, chunks[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_help_tab(&self, f: &mut ratatui::Frame, area: Rect) {
|
||||||
|
let content = vec![
|
||||||
|
Line::from(vec![Span::styled("Keyboard Shortcuts", Style::default().bold().fg(Color::Cyan))]),
|
||||||
|
Line::from(vec![Span::raw("")]),
|
||||||
|
Line::from(vec![Span::raw(" Tab - Switch between tabs")]),
|
||||||
|
Line::from(vec![Span::raw(" Up/Down - Navigate items")]),
|
||||||
|
Line::from(vec![Span::raw(" Enter - Select/Confirm")]),
|
||||||
|
Line::from(vec![Span::raw(" Esc - Exit/Close")]),
|
||||||
|
Line::from(vec![Span::raw(" / - Search")]),
|
||||||
|
Line::from(vec![Span::raw(" ? - Toggle help")]),
|
||||||
|
Line::from(vec![Span::raw(" Ctrl+C - Quit")]),
|
||||||
|
Line::from(vec![Span::raw("")]),
|
||||||
|
Line::from(vec![Span::styled("Examples", Style::default().bold().fg(Color::Cyan))]),
|
||||||
|
];
|
||||||
|
|
||||||
|
for example in &self.command_info.examples {
|
||||||
|
content.push(Line::from(vec![Span::raw(format!(" {}", example))]));
|
||||||
|
}
|
||||||
|
|
||||||
|
let paragraph = Paragraph::new(content)
|
||||||
|
.block(Block::default().title("Help & Documentation").borders(Borders::ALL))
|
||||||
|
.wrap(ratatui::widgets::Wrap { trim: true });
|
||||||
|
|
||||||
|
f.render_widget(paragraph, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_status_bar(&self, f: &mut ratatui::Frame, area: Rect) {
|
||||||
|
let status_text = match self.active_tab {
|
||||||
|
ActiveTab::Commands => "Commands Tab - Use / to search",
|
||||||
|
ActiveTab::Arguments => "Arguments Tab - View all available arguments",
|
||||||
|
ActiveTab::Wizard => "Wizard Tab - Build commands interactively",
|
||||||
|
ActiveTab::Help => "Help Tab - Documentation and shortcuts",
|
||||||
|
};
|
||||||
|
|
||||||
|
let left = Span::styled(status_text, Style::default().fg(Color::Green));
|
||||||
|
let right = Span::styled("Press ? for help", Style::default().fg(Color::Gray));
|
||||||
|
|
||||||
|
let line = Line::from(vec![left, Span::raw(" ").repeat(area.width as usize - status_text.len() - 18), right]);
|
||||||
|
|
||||||
|
f.render_widget(Paragraph::new(line).style(Style::default().bg(Color::Black)), area);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_help_popup(&self, f: &mut ratatui::Frame) {
|
||||||
|
let area = centered_rect(50, 60, f.size());
|
||||||
|
let block = Block::default()
|
||||||
|
.title("Keyboard Shortcuts")
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.style(Style::default().bg(Color::DarkGray));
|
||||||
|
|
||||||
|
let content = vec![
|
||||||
|
Line::from(vec![Span::styled("Navigation", Style::default().bold())]),
|
||||||
|
Line::from(vec![Span::raw(" ↑/↓ Navigate lists")]),
|
||||||
|
Line::from(vec![Span::raw(" Tab Switch tabs")]),
|
||||||
|
Line::from(vec![Span::raw(" Enter Select item / Confirm")]),
|
||||||
|
Line::from(vec![Span::raw(" Esc Close / Back")]),
|
||||||
|
Line::from(vec![Span::raw("")]),
|
||||||
|
Line::from(vec![Span::styled("Search", Style::default().bold())]),
|
||||||
|
Line::from(vec![Span::raw(" / Start search")]),
|
||||||
|
Line::from(vec![Span::raw(" Type Filter results")]),
|
||||||
|
Line::from(vec![Span::raw(" Enter Exit search")]),
|
||||||
|
Line::from(vec![Span::raw("")]),
|
||||||
|
Line::from(vec![Span::styled("Wizard", Style::default().bold())]),
|
||||||
|
Line::from(vec![Span::raw(" Enter Edit argument value")]),
|
||||||
|
Line::from(vec![Span::raw(" Esc Cancel editing")]),
|
||||||
|
Line::from(vec![Span::raw(" Bksp Remove argument")]),
|
||||||
|
];
|
||||||
|
|
||||||
|
let paragraph = Paragraph::new(content)
|
||||||
|
.block(block)
|
||||||
|
.wrap(ratatui::widgets::Wrap { trim: true });
|
||||||
|
|
||||||
|
f.render_widget(paragraph, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_search(&self, f: &mut ratatui::Frame) {
|
||||||
|
let area = Layout::default()
|
||||||
|
.constraints([Constraint::Length(3)])
|
||||||
|
.split(f.size())[0];
|
||||||
|
|
||||||
|
let block = Block::default()
|
||||||
|
.title("Search")
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.style(Style::default().bg(Color::Black));
|
||||||
|
|
||||||
|
let input = Paragraph::new(format!("> {}", self.search_query))
|
||||||
|
.style(Style::default().fg(Color::Yellow));
|
||||||
|
|
||||||
|
f.render_widget(block, area);
|
||||||
|
f.render_widget(input, area);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
|
||||||
|
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),
|
||||||
|
])
|
||||||
|
.split(r)[1];
|
||||||
|
|
||||||
|
Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Percentage((100 - percent_x) / 2),
|
||||||
|
Constraint::Percentage(percent_x),
|
||||||
|
Constraint::Percentage((100 - percent_x) / 2),
|
||||||
|
])
|
||||||
|
.split(popup_layout)[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_wizard_state_default() {
|
||||||
|
let state = WizardState::default();
|
||||||
|
assert!(state.selected_args.is_empty());
|
||||||
|
assert!(state.arg_values.is_empty());
|
||||||
|
assert_eq!(state.current_arg_index, 0);
|
||||||
|
assert!(state.input_buffer.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user