Skip to content

Commit b04df75

Browse files
committed
feat: add support for saving commands and output to files
- Added `SaveToFileWidget` for saving commands and outputs. - Introduced `CompletableInput` for improved completion handling.
1 parent 5065195 commit b04df75

13 files changed

Lines changed: 692 additions & 180 deletions

src/app.rs

Lines changed: 210 additions & 64 deletions
Large diffs are not rendered by default.

src/completable_input.rs

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
use crate::completion::FileOnlyShCompleter;
2+
use crate::completion::ShCompleter;
3+
use crate::completion::{Completer, CompletionResult};
4+
use crossterm::event::Event;
5+
use tui_input::backend::crossterm::to_input_request;
6+
use tui_input::{Input, InputRequest, InputResponse, StateChanged};
7+
8+
pub struct CompletableInput {
9+
input: Input,
10+
completions: Option<(CompletionResult, usize)>,
11+
completer: Box<dyn Completer>,
12+
}
13+
14+
impl From<String> for CompletableInput {
15+
fn from(value: String) -> Self {
16+
Self {
17+
input: Input::new(value),
18+
completions: None,
19+
completer: Box::new(ShCompleter {}),
20+
}
21+
}
22+
}
23+
24+
impl From<&str> for CompletableInput {
25+
fn from(value: &str) -> Self {
26+
Self {
27+
input: Input::new(value.to_string()),
28+
completions: None,
29+
completer: Box::new(ShCompleter {}),
30+
}
31+
}
32+
}
33+
34+
impl CompletableInput {
35+
pub fn file_only(str: &str) -> Self {
36+
Self {
37+
input: Input::new(str.to_string()),
38+
completions: None,
39+
completer: Box::new(FileOnlyShCompleter {}),
40+
}
41+
}
42+
43+
pub fn cursor(&self) -> usize {
44+
self.input.cursor()
45+
}
46+
47+
pub fn handle(&mut self, req: InputRequest) -> InputResponse {
48+
self.input.handle(req)
49+
}
50+
51+
pub fn handle_event(&mut self, evt: &Event) -> Option<StateChanged> {
52+
self.completions = None;
53+
to_input_request(evt).and_then(|req| self.input.handle(req))
54+
}
55+
56+
pub fn value(&self) -> &str {
57+
self.input.value()
58+
}
59+
60+
pub fn visual_cursor(&self) -> usize {
61+
self.input.visual_cursor()
62+
}
63+
64+
pub fn clear_completions(&mut self) {
65+
self.completions = None;
66+
}
67+
68+
pub fn complete(&mut self, next: bool) {
69+
let current_value = self.input.value().to_string();
70+
let cursor_pos = self.input.visual_cursor();
71+
72+
if let Some((res, index)) = self.completions.as_mut() {
73+
if next {
74+
*index = (*index + 1) % res.completions.len();
75+
} else {
76+
*index = if *index == 0 {
77+
res.completions.len() - 1
78+
} else {
79+
*index - 1
80+
};
81+
}
82+
let completion = &res.completions[*index];
83+
let new_value = format!(
84+
"{}{}{}",
85+
&current_value[..res.word_start],
86+
completion,
87+
&current_value[cursor_pos..]
88+
);
89+
self.input = Input::from(new_value);
90+
self.input
91+
.handle(InputRequest::SetCursor(res.word_start + completion.len()));
92+
} else if let Some(res) = self.completer.completions(&current_value, cursor_pos) {
93+
let index = if next { 0 } else { res.completions.len() - 1 };
94+
let word_start = res.word_start;
95+
let completion = res.completions[index].clone();
96+
let new_value = format!(
97+
"{}{}{}",
98+
&current_value[..word_start],
99+
completion,
100+
&current_value[cursor_pos..]
101+
);
102+
self.completions = Some((res, index));
103+
self.input = Input::from(new_value);
104+
self.input
105+
.handle(InputRequest::SetCursor(word_start + completion.len()));
106+
}
107+
}
108+
}
109+
110+
#[cfg(test)]
111+
mod tests {
112+
use super::*;
113+
use crossterm::event::KeyCode::Char;
114+
use crossterm::event::{Event, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers};
115+
use tui_input::Input;
116+
117+
struct TestCompleter;
118+
119+
impl Completer for TestCompleter {
120+
fn completions(&self, _input: &str, _cursor_pos: usize) -> Option<CompletionResult> {
121+
Some(CompletionResult {
122+
completions: vec!["command".to_string(), "command_other".to_string()],
123+
word_start: 0,
124+
})
125+
}
126+
}
127+
128+
impl Default for CompletableInput {
129+
fn default() -> Self {
130+
CompletableInput {
131+
input: Input::from(""),
132+
completions: None,
133+
completer: Box::new(TestCompleter {}),
134+
}
135+
}
136+
}
137+
138+
#[test]
139+
fn completer() {
140+
let mut input = CompletableInput::default();
141+
142+
input_text(&mut input, "co");
143+
144+
input.complete(true);
145+
assert_eq!(input.value(), "command");
146+
147+
input.complete(true);
148+
assert_eq!(input.value(), "command_other");
149+
150+
input.complete(false);
151+
assert_eq!(input.value(), "command");
152+
}
153+
154+
fn input_text(app: &mut CompletableInput, text: &str) {
155+
for c in text.chars() {
156+
app.handle_event(&Event::Key(KeyEvent {
157+
code: Char(c),
158+
modifiers: KeyModifiers::NONE,
159+
kind: KeyEventKind::Press,
160+
state: KeyEventState::NONE,
161+
}));
162+
}
163+
}
164+
}

src/completion.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,71 @@ impl ShCompleter {
9191
}
9292
}
9393

94+
pub struct FileOnlyShCompleter;
95+
96+
impl Completer for FileOnlyShCompleter {
97+
fn completions(&self, input: &str, cursor_pos: usize) -> Option<CompletionResult> {
98+
let (prefix, completion_type, word_start) = {
99+
let input_up_to_cursor = &input[..cursor_pos];
100+
101+
let word_start = input_up_to_cursor
102+
.rfind(|c: char| c.is_whitespace())
103+
.map(|i| i + 1)
104+
.unwrap_or(0);
105+
let prefix = &input_up_to_cursor[word_start..];
106+
107+
(prefix.to_string(), CompletionType::File, word_start)
108+
};
109+
110+
debug!(
111+
"Completion prefix for '{}' @ {}: '{}', type: {:?}, word start: {}",
112+
input, cursor_pos, prefix, completion_type, word_start
113+
);
114+
115+
let comp_type_str = match completion_type {
116+
CompletionType::Command => "-c",
117+
CompletionType::File => "-f",
118+
};
119+
120+
let output = Command::new("sh")
121+
.arg("-c")
122+
.arg(format!("compgen {} -- \"{}\"", comp_type_str, prefix))
123+
.output();
124+
125+
match output {
126+
Ok(output) => {
127+
let stdout = String::from_utf8_lossy(&output.stdout);
128+
let completions: Vec<String> = stdout
129+
.lines()
130+
.map(|s| s.to_string())
131+
.unique()
132+
.sorted_by(|a, b| a.len().cmp(&b.len()))
133+
.sorted()
134+
.collect();
135+
136+
debug!(
137+
"completion results [{}]: {:?}",
138+
completions.len(),
139+
completions
140+
);
141+
142+
if completions.is_empty() {
143+
None
144+
} else {
145+
Some(CompletionResult {
146+
completions,
147+
word_start,
148+
})
149+
}
150+
}
151+
Err(e) => {
152+
error!("Failed fetching completions {}", e);
153+
None
154+
}
155+
}
156+
}
157+
}
158+
94159
#[derive(Debug, PartialEq)]
95160
enum CompletionType {
96161
Command,

src/config.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ pub struct KeyBindingsConfig {
9797
pub complete_prev: Vec<String>,
9898
pub search_next: Vec<String>,
9999
pub search_prev: Vec<String>,
100+
pub save_output: Vec<String>,
101+
pub save_command: Vec<String>,
100102
}
101103

102104
impl Default for KeyBindingsConfig {
@@ -132,6 +134,8 @@ impl Default for KeyBindingsConfig {
132134
complete_prev: vec!["shift+tab".into(), "alt+tab".into()],
133135
search_next: vec!["f3".into(), "ctrl+f".into()],
134136
search_prev: vec!["f4".into(), "ctrl+b".into()],
137+
save_output: vec!["ctrl+s".into()],
138+
save_command: vec!["ctrl+alt+s".into()],
135139
}
136140
}
137141
}

src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
mod app;
22
mod cmd_runner;
3+
mod completable_input;
34
mod completion;
45
mod config;
56
mod debouncer;
@@ -8,6 +9,7 @@ mod output_widget;
89
mod props;
910
mod rura;
1011
mod rura_widget;
12+
mod save_to_file_widget;
1113
mod search_widget;
1214
mod theme;
1315
mod uicmd;

src/output_widget.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ struct Viewport {
2424
}
2525

2626
pub struct OutputWidget {
27-
output: Output,
27+
pub output: Output,
2828
error_output_opt: Option<Output>,
2929
offset: Position,
3030
wrap: bool,

0 commit comments

Comments
 (0)