use crate::Args;
use crate::app::Action::{
CommandCompleted, Debounced, ResetHighlight, StdinRead, StdinReadFailed, UserInput,
};
use crate::cmd_runner::{CmdRunner, Output};
use crate::completable_input::CompletableInput;
use crate::config::{KeyBindingsConfig, ThemeConfig};
use crate::debouncer::debouncer_task;
use crate::help_widget::HelpWidget;
use crate::history::History;
use crate::output_widget::{ErrorDisplayMode, ErrorPanePlacement, OutputWidget};
use crate::rura::ExecuteType;
use crate::rura_widget::RuraWidget;
use crate::save_to_file_widget::SaveToFileWidget;
use crate::search_widget::SearchWidget;
use crate::theme::Theme;
use crate::uicmd::{KeyBindings, UiCmd, to_ui_command};
use KeyCode::{Enter, Esc, F};
use anyhow::Result;
use crossterm::event::KeyCode::Char;
use crossterm::event::{KeyCode, KeyModifiers};
use crossterm::tty::IsTty;
use log::{debug, error, info};
use ratatui::crossterm::event;
use ratatui::crossterm::event::Event;
use ratatui::layout::{Constraint, Direction, Layout, Margin, Rect};
use ratatui::prelude::Span;
use ratatui::prelude::Stylize;
use ratatui::style::Color::Yellow;
use ratatui::style::Style;
use ratatui::text::{Line, Text};
use ratatui::widgets::{Block, BorderType, Widget};
use ratatui::{DefaultTerminal, Frame};
use serde::{Deserialize, Serialize};
use std::io::{Read, stdin};
use std::sync::mpsc::{Receiver, Sender};
use std::thread;
use std::time::Duration;
use tui_popup::Popup;
pub struct App {
rura_widget: RuraWidget,
output_widget: OutputWidget,
search_widget: SearchWidget,
help_widget: HelpWidget,
save_output_widget: SaveToFileWidget,
save_command_widget: SaveToFileWidget,
stdin: String,
shell: String,
action_rx: Receiver<Action>,
command_tx: Sender<(String, String)>,
key_bindings: KeyBindings,
command_line_placement: CommandLinePlacement,
input_mode: InputMode,
debouncer_tx: Sender<()>,
active_mode: ActiveMode,
active_modal: ActiveModal,
exit: bool,
}
impl App {
pub fn new(
args: Args,
theme_config: &ThemeConfig,
kb_config: KeyBindingsConfig,
command_line_placement: CommandLinePlacement,
error_display_mode: ErrorDisplayMode,
highlight_duration_ms: u64,
debounce_duration_ms: u64,
shell: String,
) -> Self {
let (action_tx, action_rx) = std::sync::mpsc::channel::<Action>();
let (command_tx, command_rx) = std::sync::mpsc::channel::<(String, String)>();
let (highlight_reset_tx, highlight_reset_rx) = std::sync::mpsc::channel::<()>();
let (debouncer_tx, debouncer_rx) = std::sync::mpsc::channel::<()>();
let s1 = action_tx.clone();
thread::spawn(move || handle_input_task(s1).unwrap());
let value = shell.clone();
let s2 = action_tx.clone();
thread::spawn(move || handle_command_task(CmdRunner::new(&value), command_rx, s2).unwrap());
let s3 = action_tx.clone();
thread::spawn(move || {
if let Some(file) = args.file {
read_file_task(file, s3).unwrap()
} else {
read_stdin_task(s3).unwrap()
}
});
let s4 = action_tx.clone();
thread::spawn(move || {
reset_highlight_task(highlight_reset_rx, s4, highlight_duration_ms).unwrap()
});
thread::spawn(move || {
debouncer_task(
debouncer_rx,
Duration::from_millis(debounce_duration_ms),
move || {
action_tx
.send(Debounced)
.expect("Sending to channel failed");
},
)
.unwrap()
});
Self {
rura_widget: RuraWidget {
command_input: CompletableInput::from(&args.command.unwrap_or_default(), &shell),
highlight_until: None,
theme: Theme::from_config(theme_config),
history: History::using_file(),
highlight_reset_tx,
},
output_widget: OutputWidget::new(
theme_config,
match command_line_placement {
CommandLinePlacement::Top => ErrorPanePlacement::Top,
CommandLinePlacement::Bottom => ErrorPanePlacement::Bottom,
},
error_display_mode,
),
search_widget: SearchWidget::default(),
save_output_widget: SaveToFileWidget::new(
" Save output to file ".to_string(),
shell.clone(),
),
save_command_widget: SaveToFileWidget::new(
" Save command to file ".to_string(),
shell.clone(),
),
stdin: "".into(),
shell,
action_rx,
command_tx,
debouncer_tx,
key_bindings: KeyBindings::from_config(&kb_config),
command_line_placement,
help_widget: HelpWidget::new(kb_config),
input_mode: InputMode::Normal,
active_mode: ActiveMode::default(),
active_modal: ActiveModal::default(),
exit: false,
}
}
pub fn run(mut self, terminal: &mut DefaultTerminal) -> Result<String> {
while !self.exit {
terminal.draw(|frame| self.render(frame, frame.area()))?;
let action = self.action_rx.recv()?;
self.handle_action(action);
}
Ok(self.rura_widget.command_input.value().to_string())
}
fn handle_action(&mut self, action: Action) {
match action {
UserInput(event) => self.handle_event(&event),
CommandCompleted(output) => {
if output.ok {
if let Some(c) = &output.command {
self.rura_widget.history.push(c)
}
}
self.output_widget.handle_command_output(output)
}
ResetHighlight => self.rura_widget.highlight_until = None,
StdinRead(output) => {
self.output_widget
.handle_command_output(Output::ok_stdin(&output));
self.stdin = output;
}
StdinReadFailed(output) => {
self.output_widget
.handle_command_output(Output::err_stdin(&output));
}
Debounced => {
match self.input_mode {
InputMode::Normal => {
}
InputMode::LiveFull => self.handle_execute(ExecuteType::FullLive),
InputMode::LiveUntilCursor => {
self.handle_execute(ExecuteType::UntilCurrentLive)
}
}
}
}
}
pub fn handle_event(&mut self, event: &Event) {
match event {
Event::Key(key_event) => {
let code = key_event.code;
let mods = key_event.modifiers;
match &self.active_modal {
ActiveModal::None => match &self.active_mode {
ActiveMode::Normal => self.handle_event_normal(event, code, mods),
ActiveMode::Search => self.handle_event_search(event, code, mods),
},
ActiveModal::LiveConfirmation(input_mode) => {
self.handle_event_live_confirmation(code, mods, input_mode.clone())
}
ActiveModal::Help => self.handle_event_help(code, mods),
ActiveModal::SaveOutput => self.handle_event_save_output(event, code, mods),
ActiveModal::SaveCommand => self.handle_event_save_command(event, code, mods),
}
}
_ => {}
}
}
fn handle_event_save_command(&mut self, event: &Event, code: KeyCode, mods: KeyModifiers) {
match (code, mods) {
(Esc, KeyModifiers::NONE) => {
self.active_modal = ActiveModal::default();
}
(Enter, KeyModifiers::NONE) => match self.save_command_to_file() {
Ok(()) => {
debug!(
"Output saved to file: {}",
self.save_command_widget.file_path_input.value()
);
self.active_modal = ActiveModal::default();
}
Err(e) => {
self.save_command_widget.error_message = Some(e.to_string());
error!("Error saving output to file: {}", e);
}
},
_ => match to_ui_command(&self.key_bindings, code, mods) {
Some(UiCmd::Quit) => {
self.exit = true;
}
Some(UiCmd::Complete) => {
self.save_command_widget.file_path_input.complete(true);
}
Some(UiCmd::CompletePrev) => {
self.save_command_widget.file_path_input.complete(false);
}
_ => {
self.save_command_widget.file_path_input.handle_event(event);
}
},
}
}
fn handle_event_save_output(&mut self, event: &Event, code: KeyCode, mods: KeyModifiers) {
match (code, mods) {
(Esc, KeyModifiers::NONE) => {
self.active_modal = ActiveModal::default();
}
(Enter, KeyModifiers::NONE) => match self.save_output_to_file() {
Ok(()) => {
debug!(
"Output saved to file: {}",
self.save_output_widget.file_path_input.value()
);
self.active_modal = ActiveModal::default();
}
Err(e) => {
self.save_output_widget.error_message = Some(e.to_string());
error!("Error saving output to file: {}", e);
}
},
_ => match to_ui_command(&self.key_bindings, code, mods) {
Some(UiCmd::Quit) => {
self.exit = true;
}
Some(UiCmd::Complete) => {
self.save_output_widget.file_path_input.complete(true);
}
Some(UiCmd::CompletePrev) => {
self.save_output_widget.file_path_input.complete(false);
}
_ => {
self.save_output_widget.file_path_input.handle_event(event);
}
},
}
}
fn handle_event_help(&mut self, code: KeyCode, mods: KeyModifiers) {
match (code, mods) {
(Esc, KeyModifiers::NONE) => {
self.active_modal = ActiveModal::default();
}
(F(1), KeyModifiers::NONE) => {
self.active_modal = ActiveModal::default();
}
_ => match to_ui_command(&self.key_bindings, code, mods) {
Some(UiCmd::Quit) => {
self.exit = true;
}
_ => {}
},
}
}
fn handle_event_live_confirmation(
&mut self,
code: KeyCode,
mods: KeyModifiers,
input_mode: InputMode,
) {
match (code, mods) {
(Esc | Char('n'), KeyModifiers::NONE) => self.active_modal = ActiveModal::default(),
(Char('y'), KeyModifiers::NONE) => {
self.input_mode = input_mode.clone();
self.active_modal = ActiveModal::default();
}
_ => match to_ui_command(&self.key_bindings, code, mods) {
Some(UiCmd::Quit) => {
self.exit = true;
}
_ => {}
},
}
}
fn handle_event_search(&mut self, event: &Event, code: KeyCode, mods: KeyModifiers) {
match (code, mods) {
(Esc, KeyModifiers::NONE) => {
self.active_mode = ActiveMode::Normal;
}
(F(1), KeyModifiers::NONE) => {
self.active_modal = ActiveModal::Help;
}
(Char('c'), KeyModifiers::ALT) => {
self.search_widget.toggle_case_sensitive();
self.output_widget.highlight(
self.search_widget.input.value(),
self.search_widget.case_sensitive,
self.search_widget.regex,
);
self.search_widget
.update_highlight_info(self.output_widget.highlight_info());
}
(Char('x'), KeyModifiers::ALT) => {
self.search_widget.toggle_regex();
self.output_widget.highlight(
self.search_widget.input.value(),
self.search_widget.case_sensitive,
self.search_widget.regex,
);
self.search_widget
.update_highlight_info(self.output_widget.highlight_info());
}
(Enter, KeyModifiers::NONE) => {
self.output_widget.highlight(
self.search_widget.input.value(),
self.search_widget.case_sensitive,
self.search_widget.regex,
);
self.search_widget
.update_highlight_info(self.output_widget.highlight_info());
}
_ => match to_ui_command(&self.key_bindings, code, mods) {
Some(ui_cmd) => match ui_cmd {
UiCmd::Quit => {
self.exit = true;
}
UiCmd::SearchNext => {
self.output_widget.highlight_next();
self.search_widget
.update_highlight_info(self.output_widget.highlight_info());
}
UiCmd::SearchPrev => {
self.output_widget.highlight_prev();
self.search_widget
.update_highlight_info(self.output_widget.highlight_info());
}
UiCmd::ScrollDown => {
self.output_widget.scroll_down();
}
UiCmd::ScrollDownPage => {
self.output_widget.scroll_page_down();
}
UiCmd::ScrollUp => {
self.output_widget.scroll_up();
}
UiCmd::ScrollUpPage => {
self.output_widget.scroll_page_up();
}
UiCmd::ScrollLeft => {
self.output_widget.scroll_left();
}
UiCmd::ScrollRight => {
self.output_widget.scroll_right();
}
UiCmd::ToggleWrap => {
self.output_widget.toggle_wrap();
}
UiCmd::SaveOutput => {
self.active_modal = ActiveModal::SaveOutput;
}
UiCmd::SaveCommand => {
self.active_modal = ActiveModal::SaveCommand;
}
_ => {}
},
_ => {
if self.search_widget.handle_event(event) {
self.output_widget.highlight(
self.search_widget.input.value(),
self.search_widget.case_sensitive,
self.search_widget.regex,
);
self.search_widget
.update_highlight_info(self.output_widget.highlight_info());
};
}
},
}
}
fn handle_event_normal(&mut self, event: &Event, code: KeyCode, mods: KeyModifiers) {
match (code, mods) {
(Esc, KeyModifiers::NONE) => {
self.output_widget.clear_highlight();
}
(F(1), KeyModifiers::NONE) => {
self.active_modal = ActiveModal::Help;
}
(F(11), KeyModifiers::NONE) => match self.input_mode {
InputMode::Normal => {
self.active_modal = ActiveModal::LiveConfirmation(InputMode::LiveUntilCursor);
}
InputMode::LiveFull => {
self.input_mode = InputMode::LiveUntilCursor;
}
InputMode::LiveUntilCursor => {
self.input_mode = InputMode::Normal;
}
},
(F(12), KeyModifiers::NONE) => match self.input_mode {
InputMode::Normal => {
self.active_modal = ActiveModal::LiveConfirmation(InputMode::LiveFull);
}
InputMode::LiveFull => {
self.input_mode = InputMode::Normal;
}
InputMode::LiveUntilCursor => {
self.input_mode = InputMode::LiveFull;
}
},
_ => match to_ui_command(&self.key_bindings, code, mods) {
Some(ui_cmd) => match ui_cmd {
UiCmd::Quit => {
self.exit = true;
}
UiCmd::SearchNext | UiCmd::SearchPrev => {
self.active_mode = ActiveMode::Search;
}
UiCmd::ExecuteFull => {
self.handle_execute(ExecuteType::Full);
}
UiCmd::ExecuteUntilCurrent => self.handle_execute(ExecuteType::UntilCurrent),
UiCmd::ExecuteUntilPrev => self.handle_execute(ExecuteType::UntilCurrentPrev),
UiCmd::ResetInput => {
self.output_widget
.handle_command_output(Output::ok_stdin(&self.stdin));
}
UiCmd::SubcommandNext => {
self.rura_widget.subcommand_next();
}
UiCmd::SubcommandPrev => {
self.rura_widget.subcommand_prev();
}
UiCmd::HistoryNext => {
if matches!(self.input_mode, InputMode::Normal) {
self.rura_widget.history_next();
}
}
UiCmd::HistoryPrev => {
if matches!(self.input_mode, InputMode::Normal) {
self.rura_widget.history_prev();
}
}
UiCmd::Complete => {
if matches!(self.input_mode, InputMode::Normal) {
self.rura_widget.command_input.complete(true);
}
}
UiCmd::CompletePrev => {
if matches!(self.input_mode, InputMode::Normal) {
self.rura_widget.command_input.complete(false);
}
}
UiCmd::ScrollDown => {
self.output_widget.scroll_down();
}
UiCmd::ScrollDownPage => {
self.output_widget.scroll_page_down();
}
UiCmd::ScrollUp => {
self.output_widget.scroll_up();
}
UiCmd::ScrollUpPage => {
self.output_widget.scroll_page_up();
}
UiCmd::ScrollLeft => {
self.output_widget.scroll_left();
}
UiCmd::ScrollRight => {
self.output_widget.scroll_right();
}
UiCmd::ToggleWrap => {
self.output_widget.toggle_wrap();
}
UiCmd::SaveOutput => {
self.active_modal = ActiveModal::SaveOutput;
}
UiCmd::SaveCommand => {
self.active_modal = ActiveModal::SaveCommand;
}
},
_ => {
if self.rura_widget.handle_event(event) {
match self.input_mode {
InputMode::Normal => {}
InputMode::LiveFull | InputMode::LiveUntilCursor => {
self.debouncer_tx.send(()).unwrap();
}
}
}
}
},
}
}
fn save_output_to_file(&mut self) -> Result<()> {
self.save_output_widget
.save(&self.output_widget.output.lines.join("\n"))
}
fn save_command_to_file(&mut self) -> Result<()> {
self.save_command_widget.save_executable(&format!(
"#!/usr/bin/env {}\n\n{}",
self.shell,
self.rura_widget.command_input.value()
))
}
fn handle_execute(&mut self, kind: ExecuteType) {
match self.rura_widget.execute(kind) {
Ok(Some(c)) => self.command_tx.send((c, self.stdin.clone())).unwrap(),
Ok(None) => self
.output_widget
.handle_command_output(Output::ok_stdin(&self.stdin)),
Err(_) => {}
}
}
pub fn render(&mut self, frame: &mut Frame, area: Rect) {
let margin = Margin::new(1, 1);
let inner_area = area.inner(margin);
let (command_input_area, search_input_area, output_area, status_area) = {
let search_height = if matches!(self.active_mode, ActiveMode::Search) {
self.search_widget.height(inner_area.width) + 2
} else {
0
};
match self.command_line_placement {
CommandLinePlacement::Top => {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
Constraint::Length(self.rura_widget.height(inner_area.width) + 2), Constraint::Length(search_height), Constraint::Fill(1), Constraint::Length(1), ])
.split(area);
(layout[0], layout[1], layout[2], layout[3])
}
CommandLinePlacement::Bottom => {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
Constraint::Fill(1), Constraint::Length(search_height), Constraint::Length(self.rura_widget.height(inner_area.width) + 2), Constraint::Length(1), ])
.split(area);
(layout[2], layout[1], layout[0], layout[3])
}
}
};
let command_input_block = if matches!(self.input_mode, InputMode::Normal) {
Block::bordered()
} else {
Block::bordered()
.border_style(Style::default().fg(Yellow))
.border_type(BorderType::Thick)
};
frame.render_widget(command_input_block, command_input_area);
frame.render_widget(&self.rura_widget, command_input_area.inner(margin));
match self.active_mode {
ActiveMode::Normal => {
let inner_rect = command_input_area.inner(margin);
let (x, y) = self.rura_widget.cursor(inner_rect.width);
frame.set_cursor_position((
command_input_area.x + 1 + x,
command_input_area.y + 1 + y,
));
}
ActiveMode::Search => {
self.search_widget
.render(search_input_area, frame.buffer_mut());
frame.render_widget(
Block::default().reversed(),
command_input_area.inner(margin),
);
let inner_rect = search_input_area.inner(margin);
let (x, y) = self.search_widget.cursor(inner_rect.width);
frame.set_cursor_position((
search_input_area.x + 1 + x,
search_input_area.y + 1 + y,
));
}
}
frame.render_widget(&mut self.output_widget, output_area);
let status_text = match self.output_widget.error_display_mode {
ErrorDisplayMode::Inline => {
if self.output_widget.main_output().ok {
" OK ".white().on_green()
} else {
match self.output_widget.main_output().status_code {
None => " ERR ".white().on_red(),
Some(code) => format!(" ERR({code}) ").white().on_red(),
}
}
}
ErrorDisplayMode::Pane => Span::from(""),
};
let [_, exit_code_area, hints_area, lines_area, _] = Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![
Constraint::Length(1),
Constraint::Length(status_text.width() as u16 + 1),
Constraint::Fill(1),
Constraint::Length(self.output_widget.output_len().to_string().len() as u16 + 3),
Constraint::Length(1),
])
.areas(status_area);
frame.render_widget(self.hints_widget(), hints_area);
match self.output_widget.error_display_mode {
ErrorDisplayMode::Pane => (),
ErrorDisplayMode::Inline => frame.render_widget(status_text, exit_code_area),
}
frame.render_widget(
format!("L:{}", self.output_widget.output_len())
.bold()
.into_right_aligned_line(),
lines_area,
);
match self.active_modal {
ActiveModal::LiveConfirmation(_) => {
self.render_live_confirm(frame);
}
ActiveModal::Help => {
frame.render_widget(&self.help_widget, frame.area());
}
ActiveModal::SaveOutput => {
self.save_output_widget
.render(frame.area(), frame.buffer_mut());
frame.set_cursor_position(self.save_output_widget.cursor)
}
ActiveModal::SaveCommand => {
self.save_command_widget
.render(frame.area(), frame.buffer_mut());
frame.set_cursor_position(self.save_command_widget.cursor)
}
_ => {}
}
}
fn render_live_confirm(&self, frame: &mut Frame) {
let body = Text::from(vec![
Line::from("").centered(),
Line::from(" Warning: This might be dangerous! ")
.centered()
.bold(),
Line::from("").centered(),
Line::from(" Commands will be executed automatically as you type. ").centered(),
Line::from("").centered(),
Line::from("[Y]es / [N]o").centered(),
Line::from("").centered(),
]);
let popup = Popup::new(body)
.title(" Confirm entering LIVE mode ")
.style(Style::new().white().on_yellow());
frame.render_widget(popup, frame.area());
}
fn hints_widget(&self) -> Line<'_> {
let mut spans: Vec<Span> = vec![];
spans.push(" ".into());
spans.push("^C".bold());
spans.push(" Quit ".into());
spans.push("Enter".bold());
spans.push(" Execute ".into());
spans.push("F1".bold());
spans.push(" Help ".into());
spans.push("F11 ".bold());
match self.input_mode {
InputMode::Normal | InputMode::LiveFull => {
spans.push("Live UC".into());
}
InputMode::LiveUntilCursor => {
spans.push("Live UC".reversed());
}
}
spans.push(" ".into());
spans.push("F12 ".bold());
match self.input_mode {
InputMode::Normal | InputMode::LiveUntilCursor => {
spans.push("Live".into());
}
InputMode::LiveFull => {
spans.push("Live".reversed());
}
}
Line::from_iter(spans).centered().dim()
}
}
fn handle_command_task(
cmd_runner: CmdRunner,
command_rx: Receiver<(String, String)>,
action_tx: Sender<Action>,
) -> Result<()> {
loop {
if let Ok((command, stdin)) = command_rx.recv() {
match cmd_runner.run(&command, &stdin) {
Ok(output) => {
let _ = action_tx.send(CommandCompleted(output));
}
Err(e) => {
action_tx.send(CommandCompleted(Output::err_stdin(
"Failed running command, check logs",
)))?;
error!("{}", e)
}
}
}
}
}
fn handle_input_task(tx: Sender<Action>) -> Result<()> {
loop {
if let Ok(event) = event::read() {
tx.send(UserInput(event))?
}
}
}
fn read_stdin_task(action_tx: Sender<Action>) -> Result<()> {
let mut buff = vec![];
let tty = stdin().is_tty();
if !tty {
let result = stdin().read_to_end(&mut buff);
match result {
Ok(_) => {
let content = String::from_utf8_lossy(&buff);
action_tx.send(StdinRead(content.into_owned()))?;
}
Err(e) => {
action_tx.send(StdinReadFailed(format!(
"Failed reading stdin: {}",
e.to_string()
)))?;
}
}
Ok(())
} else {
Ok(())
}
}
fn read_file_task(file: String, action_tx: Sender<Action>) -> Result<()> {
info!("reading input file {file}");
match std::fs::read(file.clone()) {
Ok(content) => {
let content = String::from_utf8_lossy(&content);
action_tx.send(StdinRead(content.into_owned()))?;
}
Err(e) => {
action_tx.send(StdinReadFailed(format!(
"Failed reading input file {}: {}",
file,
e.to_string()
)))?;
}
}
Ok(())
}
fn reset_highlight_task(rx: Receiver<()>, tx: Sender<Action>, duration_ms: u64) -> Result<()> {
loop {
if let Ok(_) = rx.recv() {
thread::sleep(Duration::from_millis(duration_ms));
tx.send(ResetHighlight)?
}
}
}
enum Action {
UserInput(Event),
CommandCompleted(Output),
StdinRead(String),
StdinReadFailed(String),
ResetHighlight,
Debounced,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum CommandLinePlacement {
Top,
#[default]
Bottom,
}
#[derive(Clone)]
enum InputMode {
Normal,
LiveFull,
LiveUntilCursor,
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::Event::Key;
use crossterm::event::{KeyEvent, KeyEventKind, KeyEventState};
use insta::assert_snapshot;
use ratatui::Terminal;
use ratatui::backend::TestBackend;
use std::collections::VecDeque;
struct TestTerminal(Terminal<TestBackend>);
impl Default for TestTerminal {
fn default() -> Self {
TestTerminal(Terminal::new(TestBackend::new(100, 30)).unwrap())
}
}
impl Default for App {
fn default() -> Self {
let (_, action_rx) = std::sync::mpsc::channel::<Action>();
let (command_tx, _) = std::sync::mpsc::channel::<(String, String)>();
let (highlight_reset_tx, _) = std::sync::mpsc::channel::<()>();
let (debouncer_tx, _) = std::sync::mpsc::channel::<()>();
let theme_config = ThemeConfig::default();
let kb_config = KeyBindingsConfig::default();
Self {
rura_widget: RuraWidget {
command_input: CompletableInput::from("", ""),
highlight_until: None,
theme: Theme::from_config(&theme_config),
history: History::in_mem(),
highlight_reset_tx,
},
output_widget: OutputWidget::new(
&theme_config,
ErrorPanePlacement::Bottom,
ErrorDisplayMode::Pane,
),
save_output_widget: SaveToFileWidget::new(
" Save output to file ".into(),
"".into(),
),
save_command_widget: SaveToFileWidget::new(
" Save command to file ".into(),
"".into(),
),
search_widget: SearchWidget::default(),
stdin: "".into(),
shell: "sh".into(),
action_rx,
command_tx,
debouncer_tx,
exit: false,
key_bindings: KeyBindings::from_config(&kb_config),
command_line_placement: CommandLinePlacement::Bottom,
help_widget: HelpWidget::new(kb_config),
input_mode: InputMode::Normal,
active_mode: ActiveMode::default(),
active_modal: ActiveModal::default(),
}
}
}
#[test]
fn main_screen() {
let mut app = App::default();
let mut terminal = TestTerminal::default().0;
terminal
.draw(|frame| app.render(frame, frame.area()))
.unwrap();
assert_snapshot!(terminal.backend());
}
#[test]
fn main_screen_help() {
let mut app = App::default();
input_key(&mut app, F(1), KeyModifiers::NONE);
let mut terminal = TestTerminal::default().0;
terminal
.draw(|frame| app.render(frame, frame.area()))
.unwrap();
assert_snapshot!(terminal.backend());
}
#[test]
fn live_mode_confirm() {
let mut app = App::default();
input_key(&mut app, F(11), KeyModifiers::NONE);
let mut terminal = TestTerminal::default().0;
terminal
.draw(|frame| app.render(frame, frame.area()))
.unwrap();
assert_snapshot!(terminal.backend());
}
#[test]
fn live_mode_full_confirm() {
let mut app = App::default();
input_key(&mut app, F(12), KeyModifiers::NONE);
let mut terminal = TestTerminal::default().0;
terminal
.draw(|frame| app.render(frame, frame.area()))
.unwrap();
assert_snapshot!(terminal.backend());
}
#[test]
fn command_input() {
let mut app = App::default();
input_text(&mut app, "ls -la | grep a");
let mut terminal = TestTerminal::default().0;
terminal
.draw(|frame| app.render(frame, frame.area()))
.unwrap();
assert_snapshot!(terminal.backend());
}
#[test]
fn saving_to_history_only_ok_outputs() {
let mut app = App::default();
app.handle_action(CommandCompleted(Output::err_command("g", "", None)));
app.handle_action(CommandCompleted(Output::err_command("gr", "", None)));
app.handle_action(CommandCompleted(Output::err_command("gre", "", None)));
app.handle_action(CommandCompleted(Output::ok_command("grep", "")));
app.handle_action(CommandCompleted(Output::ok_command("grep 'abc'", "")));
app.handle_action(CommandCompleted(Output::err_command("gp 'abc'", "", None)));
assert_eq!(
*app.rura_widget.history.history(),
VecDeque::from(vec!["grep 'abc'".into(), "grep".into(),])
);
}
#[test]
fn save_output_popup() {
let mut app = App::default();
input_key(&mut app, Char('s'), KeyModifiers::CONTROL);
input_text(&mut app, "output-file.txt");
let mut terminal = TestTerminal::default().0;
terminal
.draw(|frame| app.render(frame, frame.area()))
.unwrap();
assert_snapshot!(terminal.backend());
}
#[test]
fn save_command_popup() {
let mut app = App::default();
input_key(
&mut app,
Char('s'),
KeyModifiers::CONTROL | KeyModifiers::ALT,
);
input_text(&mut app, "command-file.txt");
let mut terminal = TestTerminal::default().0;
terminal
.draw(|frame| app.render(frame, frame.area()))
.unwrap();
assert_snapshot!(terminal.backend());
}
fn input_text(app: &mut App, text: &str) {
for c in text.chars() {
app.handle_event(&Key(KeyEvent {
code: Char(c),
modifiers: KeyModifiers::NONE,
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
}))
}
}
fn input_key(app: &mut App, code: KeyCode, modifiers: KeyModifiers) {
app.handle_event(&Key(KeyEvent {
code,
modifiers,
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
}))
}
}
#[derive(Default)]
enum ActiveMode {
#[default]
Normal,
Search,
}
#[derive(Default)]
enum ActiveModal {
#[default]
None,
LiveConfirmation(InputMode),
Help,
SaveOutput,
SaveCommand,
}