Skip to content

Commit ecbffad

Browse files
committed
refactor: enhance scrolling logic and add unique+sorted completions, replace String stdin with Output type
1 parent d61ecfb commit ecbffad

8 files changed

Lines changed: 117 additions & 70 deletions

src/app.rs

Lines changed: 48 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::Args;
2-
use crate::app::Action::{CommandCompleted, ResetHighlight, StdinRead, UserInput};
2+
use crate::app::Action::{CommandCompleted, Debounced, ResetHighlight, StdinRead, UserInput};
3+
use crate::completion::ShCompleter;
34
use crate::config::{KeyBindingsConfig, ThemeConfig, history_path};
45
use crate::debouncer::debouncer_task;
56
use crate::history::History;
@@ -8,11 +9,10 @@ use crate::rura::ExecuteType;
89
use crate::rura_widget::RuraWidget;
910
use crate::theme::Theme;
1011
use crate::uicmd::{KeyBindings, UiCmd, to_ui_command};
11-
use Action::Debounced;
1212
use crossterm::event::KeyCode::Char;
1313
use crossterm::event::{KeyCode, KeyModifiers};
1414
use crossterm::tty::IsTty;
15-
use log::debug;
15+
use log::{debug, info};
1616
use ratatui::crossterm::event;
1717
use ratatui::crossterm::event::Event;
1818
use ratatui::layout::{Constraint, Direction, Layout, Margin, Rect};
@@ -33,12 +33,11 @@ use std::thread;
3333
use std::time::Duration;
3434
use tui_input::Input;
3535
use tui_popup::Popup;
36-
use crate::completion::BashCompleter;
3736

3837
pub struct App {
3938
rura_widget: RuraWidget,
4039
output_widget: OutputWidget,
41-
stdin: String,
40+
stdin: Output,
4241
exit: bool,
4342
action_rx: Receiver<Action>,
4443
command_tx: Sender<(String, String)>,
@@ -113,7 +112,7 @@ impl App {
113112
key_bindings: KeyBindings::from_config(&kb_config),
114113
highlight_reset_tx,
115114
completions: None,
116-
completer: Box::new(BashCompleter{}),
115+
completer: Box::new(ShCompleter {}),
117116
},
118117
output_widget: OutputWidget::new(
119118
theme_config,
@@ -124,7 +123,7 @@ impl App {
124123
},
125124
error_display_mode,
126125
),
127-
stdin: "".to_string(),
126+
stdin: Output::ok(""),
128127
action_rx,
129128
command_tx,
130129
debouncer_tx,
@@ -154,10 +153,13 @@ impl App {
154153
UserInput(event) => self.handle_event(&event),
155154
CommandCompleted(output) => self.output_widget.handle_command_output(output),
156155
ResetHighlight => self.rura_widget.highlight_until = None,
157-
StdinRead(stdin) => {
158-
self.stdin = stdin;
159-
self.output_widget
160-
.handle_command_output(Output::ok(&self.stdin))
156+
StdinRead(output) => {
157+
if output.ok {
158+
self.stdin = output.clone();
159+
} else {
160+
self.stdin = Output::ok("");
161+
}
162+
self.output_widget.handle_command_output(output)
161163
}
162164
Debounced => {
163165
match self.input_mode {
@@ -256,8 +258,8 @@ impl App {
256258
self.handle_execute(ExecuteType::UntilCurrentPrev)
257259
}
258260
UiCmd::ResetInput => {
259-
let new_output = Output::ok(&self.stdin);
260-
self.output_widget.handle_command_output(new_output);
261+
let stdin = self.stdin.clone();
262+
self.output_widget.handle_command_output(stdin);
261263
}
262264
UiCmd::HistoryNext => {
263265
// disable history for live mode
@@ -297,10 +299,14 @@ impl App {
297299

298300
fn handle_execute(&mut self, kind: ExecuteType) {
299301
match self.rura_widget.execute(kind) {
300-
Some(command) if command.is_empty() => self
301-
.output_widget
302-
.handle_command_output(Output::ok(&self.stdin)),
303-
Some(c) => self.command_tx.send((c, self.stdin.clone())).unwrap(),
302+
Some(command) if command.is_empty() => {
303+
let stdin = self.stdin.clone();
304+
self.output_widget.handle_command_output(stdin)
305+
}
306+
Some(c) => self
307+
.command_tx
308+
.send((c, self.stdin.lines.join("\n")))
309+
.unwrap(),
304310
None => {}
305311
}
306312
}
@@ -495,7 +501,7 @@ fn handle_command_task(
495501
) -> Result<(), Box<dyn Error>> {
496502
loop {
497503
if let Ok((command, stdin)) = command_rx.recv() {
498-
debug!("executing command: {command}");
504+
info!("executing command: {command}");
499505

500506
let mut cmd = Command::new("sh");
501507
cmd.args(["-c", &command]);
@@ -538,32 +544,41 @@ fn handle_command_task(
538544
fn handle_input_task(tx: Sender<Action>) -> Result<(), Box<dyn Error>> {
539545
loop {
540546
if let Ok(event) = event::read() {
541-
// debug!("event: {:?}", event);
547+
debug!("event: {:?}", event);
542548
tx.send(UserInput(event))?
543549
}
544550
}
545551
}
546552

547553
fn read_stdin_task(file_opt: Option<String>, tx: Sender<Action>) -> Result<(), Box<dyn Error>> {
548554
if let Some(file) = file_opt {
549-
debug!("reading file {file}");
550-
let file_content = std::fs::read_to_string(file).expect("Failed to read file");
551-
tx.send(StdinRead(file_content))?;
555+
info!("reading file {file}");
556+
let file_content = std::fs::read_to_string(file);
557+
match file_content {
558+
Ok(content) => {
559+
tx.send(StdinRead(Output::ok(&content)))?;
560+
}
561+
Err(err) => {
562+
tx.send(StdinRead(Output::err(&err.to_string(), None)))?;
563+
}
564+
}
552565
Ok(())
553566
} else {
554-
let mut input = String::new();
567+
let mut buff = String::new();
555568
let tty = stdin().is_tty();
556-
debug!("tty? {tty}");
557569
if !tty {
558-
debug!("reading input");
559-
stdin()
560-
.read_to_string(&mut input)
561-
.expect("Failed to read input");
570+
let result = stdin().read_to_string(&mut buff);
562571

563-
tx.send(StdinRead(input))?;
572+
match result {
573+
Ok(_) => {
574+
tx.send(StdinRead(Output::ok(&buff)))?;
575+
}
576+
Err(e) => {
577+
tx.send(StdinRead(Output::err(e.to_string().as_str(), None)))?;
578+
}
579+
}
564580
Ok(())
565581
} else {
566-
debug!("skipping input");
567582
Ok(())
568583
}
569584
}
@@ -585,7 +600,7 @@ fn reset_highlight_task(
585600
enum Action {
586601
UserInput(Event),
587602
CommandCompleted(Output),
588-
StdinRead(String),
603+
StdinRead(Output),
589604
ResetHighlight,
590605
Debounced,
591606
}
@@ -641,15 +656,15 @@ mod tests {
641656
key_bindings: KeyBindings::from_config(&kb_config),
642657
highlight_reset_tx,
643658
completions: None,
644-
completer: Box::new(BashCompleter{}),
659+
completer: Box::new(ShCompleter {}),
645660
},
646661
output_widget: OutputWidget::new(
647662
&theme_config,
648663
&kb_config,
649664
ErrorPanePlacement::Bottom,
650665
ErrorDisplayMode::Pane,
651666
),
652-
stdin: "".into(),
667+
stdin: Output::ok(""),
653668
action_rx,
654669
command_tx,
655670
debouncer_tx,

src/completion.rs

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use itertools::Itertools;
12
use log::{debug, error};
23
use std::process::Command;
34

@@ -11,12 +12,12 @@ pub struct CompletionResult {
1112
pub word_start: usize,
1213
}
1314

14-
pub struct BashCompleter;
15+
pub struct ShCompleter;
1516

16-
impl Completer for BashCompleter {
17+
impl Completer for ShCompleter {
1718
fn completions(&self, input: &str, cursor_pos: usize) -> Option<CompletionResult> {
1819
let (prefix, completion_type, word_start) =
19-
BashCompleter::find_completion_prefix(input, cursor_pos);
20+
ShCompleter::find_completion_prefix(input, cursor_pos);
2021

2122
debug!(
2223
"Completion prefix for '{}' @ {}: '{}', type: {:?}, word start: {}",
@@ -28,17 +29,27 @@ impl Completer for BashCompleter {
2829
CompletionType::File => "-f",
2930
};
3031

31-
let output = Command::new("bash")
32+
let output = Command::new("sh")
3233
.arg("-c")
3334
.arg(format!("compgen {} -- \"{}\"", comp_type_str, prefix))
3435
.output();
3536

3637
match output {
3738
Ok(output) => {
3839
let stdout = String::from_utf8_lossy(&output.stdout);
39-
let completions: Vec<String> = stdout.lines().map(|s| s.to_string()).collect();
40-
41-
debug!("Completion results: {:?}", completions);
40+
let completions: Vec<String> = stdout
41+
.lines()
42+
.map(|s| s.to_string())
43+
.unique()
44+
.sorted_by(|a, b| a.len().cmp(&b.len()))
45+
.sorted()
46+
.collect();
47+
48+
debug!(
49+
"completion results [{}]: {:?}",
50+
completions.len(),
51+
completions
52+
);
4253

4354
if completions.is_empty() {
4455
None
@@ -57,7 +68,7 @@ impl Completer for BashCompleter {
5768
}
5869
}
5970

60-
impl BashCompleter {
71+
impl ShCompleter {
6172
fn find_completion_prefix(input: &str, cursor_pos: usize) -> (String, CompletionType, usize) {
6273
let input_up_to_cursor = &input[..cursor_pos];
6374

@@ -93,31 +104,31 @@ mod tests {
93104
#[test]
94105
fn test_find_completion_prefix() {
95106
assert_eq!(
96-
BashCompleter::find_completion_prefix("grep ", 5),
107+
ShCompleter::find_completion_prefix("grep ", 5),
97108
("".to_string(), CompletionType::File, 5)
98109
);
99110
assert_eq!(
100-
BashCompleter::find_completion_prefix("grep f", 6),
111+
ShCompleter::find_completion_prefix("grep f", 6),
101112
("f".to_string(), CompletionType::File, 5)
102113
);
103114
assert_eq!(
104-
BashCompleter::find_completion_prefix("ls|gr", 5),
115+
ShCompleter::find_completion_prefix("ls|gr", 5),
105116
("gr".to_string(), CompletionType::Command, 3)
106117
);
107118
assert_eq!(
108-
BashCompleter::find_completion_prefix("ls | gr", 7),
119+
ShCompleter::find_completion_prefix("ls | gr", 7),
109120
("gr".to_string(), CompletionType::Command, 5)
110121
);
111122
assert_eq!(
112-
BashCompleter::find_completion_prefix("ls | ", 5),
123+
ShCompleter::find_completion_prefix("ls | ", 5),
113124
("".to_string(), CompletionType::Command, 5)
114125
);
115126
assert_eq!(
116-
BashCompleter::find_completion_prefix("grep foo", 8),
127+
ShCompleter::find_completion_prefix("grep foo", 8),
117128
("foo".to_string(), CompletionType::File, 5)
118129
);
119130
assert_eq!(
120-
BashCompleter::find_completion_prefix("grep foo ", 9),
131+
ShCompleter::find_completion_prefix("grep foo ", 9),
121132
("".to_string(), CompletionType::File, 9)
122133
);
123134
}

src/config.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,14 +119,20 @@ impl Default for KeyBindingsConfig {
119119
subcommand_next: vec!["alt+right".into()],
120120
subcommand_prev: vec!["alt+left".into()],
121121
complete: vec!["tab".into()],
122-
complete_prev: vec!["shift+tab".into(), "backtab".into()],
122+
complete_prev: vec![
123+
"shift+tab".into(),
124+
"shift+backtab".into(),
125+
"alt+tab".into(),
126+
"backtab".into(),
127+
],
123128
}
124129
}
125130
}
126131

127132
#[derive(Debug, Deserialize, Serialize)]
128133
#[serde(default)]
129134
pub struct Config {
135+
pub log_level: Option<String>,
130136
pub theme: ThemeConfig,
131137
pub keybindings: KeyBindingsConfig,
132138
pub command_line_placement: CommandLinePlacement,
@@ -138,6 +144,7 @@ pub struct Config {
138144
impl Default for Config {
139145
fn default() -> Self {
140146
Config {
147+
log_level: None,
141148
theme: ThemeConfig::default(),
142149
keybindings: KeyBindingsConfig::default(),
143150
command_line_placement: CommandLinePlacement::default(),

src/main.rs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use props::APP_NAME;
2020
use std::error::Error;
2121
use std::fs::OpenOptions;
2222
use std::process::exit;
23+
use std::str::FromStr;
2324

2425
fn main() {
2526
let file = OpenOptions::new()
@@ -28,19 +29,26 @@ fn main() {
2829
.open(format!("/tmp/{APP_NAME}.log"))
2930
.expect("Failed to open log file");
3031

31-
Builder::new()
32-
.target(Target::Pipe(Box::new(file)))
33-
.filter_level(LevelFilter::Debug)
34-
.init();
35-
3632
let args = Args::parse();
3733

34+
let config = load_config(args.config.as_deref());
35+
3836
if args.last {
3937
println!("{}", History::load().previous(""));
4038
exit(0)
4139
}
4240

43-
let config = load_config(args.config.as_deref());
41+
let level_filter = match config.log_level {
42+
Some(ref level) => {
43+
LevelFilter::from_str(&level).expect("Invalid log level specified in config")
44+
}
45+
None => LevelFilter::Info,
46+
};
47+
48+
Builder::new()
49+
.target(Target::Pipe(Box::new(file)))
50+
.filter_level(level_filter)
51+
.init();
4452

4553
info!("{args:?}");
4654

src/output_widget.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,19 @@ impl OutputWidget {
9191
.lines
9292
.len()
9393
.saturating_sub(self.output_height as usize);
94-
self.offset.y = self.offset.y.saturating_add(10).min(max_offset as u16);
94+
let page_size = self.output_height / 2;
95+
self.offset.y = self
96+
.offset
97+
.y
98+
.saturating_add(page_size)
99+
.min(max_offset as u16);
95100
}
96101
UiCmd::ScrollUp => {
97102
self.offset.y = self.offset.y.saturating_sub(1);
98103
}
99104
UiCmd::ScrollUpPage => {
100-
self.offset.y = self.offset.y.saturating_sub(10);
105+
let page_size = self.output_height / 2;
106+
self.offset.y = self.offset.y.saturating_sub(page_size);
101107
}
102108
UiCmd::ScrollLeft => {
103109
self.offset.x = self.offset.x.saturating_sub(1);
@@ -258,7 +264,7 @@ pub enum ErrorPanePlacement {
258264
Bottom,
259265
}
260266

261-
#[derive(PartialEq, Eq)]
267+
#[derive(Clone, PartialEq, Eq)]
262268
pub struct Output {
263269
pub lines: Vec<String>,
264270
pub status_code: Option<i32>,

0 commit comments

Comments
 (0)