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