Skip to content

Commit 629be95

Browse files
authoredJun 2, 2024··
Merge pull request #4 from ynqa/v0.1.2/main
v0.1.2
2 parents 62725ec + 0db6dda commit 629be95

File tree

9 files changed

+269
-55
lines changed

9 files changed

+269
-55
lines changed
 

‎Cargo.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "sigrs"
3-
version = "0.1.1"
3+
version = "0.1.2"
44
authors = ["ynqa <un.pensiero.vano@gmail.com>"]
55
edition = "2021"
66
description = "Interactive grep (for streaming)"

‎README.md

+39-1
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,16 @@ Interactive grep
1212
- Interactive grep (for streaming)
1313
- *sig* allows users to interactively search through (streaming) data,
1414
updating results in real-time.
15+
- Re-execute command
16+
- If `--cmd` is specified instread of piping data to *sig*,
17+
the command will be executed on initial and retries.
18+
- This feature is designed to address the issue where data streams
19+
past while the user is fine-tuning the search criteria.
20+
In other words, even if the data has already passed,
21+
executing the command again allows
22+
the retrieval of the data for re-evaluation.
1523
- Archived mode
16-
- In Archived mode, since there is no seeking capability
24+
- In archived mode, since there is no seeking capability
1725
for streaming data received through a pipe,
1826
it is not possible to search backwards without exiting the process.
1927
Therefore, in *sig*, the latest N entries of streaming data are saved,
@@ -79,11 +87,28 @@ in
7987
}
8088
```
8189

90+
## Examples
91+
92+
```bash
93+
stern --context kind-kind etcd |& sig
94+
# or
95+
sig --cmd "stern --context kind-kind etcd" # this is able to retry command by ctrl+r.
96+
```
97+
98+
### Archived mode
99+
100+
```bash
101+
cat README.md |& sig -a
102+
# or
103+
sig -a --cmd "cat README.md"
104+
```
105+
82106
## Keymap
83107

84108
| Key | Action
85109
| :- | :-
86110
| <kbd>Ctrl + C</kbd> | Exit `sig`
111+
| <kbd>Ctrl + R</kbd> | Retry command if `--cmd` is specified
87112
| <kbd>Ctrl + F</kbd> | Enter Archived mode
88113
| <kbd>←</kbd> | Move the cursor one character to the left
89114
| <kbd>→</kbd> | Move the cursor one character to the right
@@ -111,6 +136,17 @@ Interactive grep (for streaming)
111136

112137
Usage: sig [OPTIONS]
113138

139+
Examples:
140+
141+
$ stern --context kind-kind etcd |& sig
142+
Or the method to retry command by pressing ctrl+r:
143+
$ sig --cmd "stern --context kind-kind etcd"
144+
145+
Archived mode:
146+
$ cat README.md |& sig -a
147+
Or
148+
$ sig -a --cmd "cat README.md"
149+
114150
Options:
115151
--retrieval-timeout <RETRIEVAL_TIMEOUT_MILLIS>
116152
Timeout to read a next line from the stream in milliseconds. [default: 10]
@@ -122,6 +158,8 @@ Options:
122158
Archived mode to grep through static data.
123159
-i, --ignore-case
124160
Case insensitive search.
161+
--cmd <CMD>
162+
Command to execute on initial and retries.
125163
-h, --help
126164
Print help (see more with '--help')
127165
-V, --version

‎src/archived.rs

+9-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ struct Archived {
2020
lines: Snapshot<listbox::State>,
2121
highlight_style: ContentStyle,
2222
case_insensitive: bool,
23+
cmd: Option<String>,
2324
}
2425

2526
impl promkit::Finalizer for Archived {
@@ -39,7 +40,12 @@ impl promkit::Renderer for Archived {
3940
}
4041

4142
fn evaluate(&mut self, event: &Event) -> anyhow::Result<PromptSignal> {
42-
let signal = self.keymap.get()(event, &mut self.text_editor_snapshot, &mut self.lines);
43+
let signal = self.keymap.get()(
44+
event,
45+
&mut self.text_editor_snapshot,
46+
&mut self.lines,
47+
self.cmd.clone(),
48+
);
4349
if self
4450
.text_editor_snapshot
4551
.after()
@@ -85,6 +91,7 @@ pub fn run(
8591
lines: listbox::State,
8692
highlight_style: ContentStyle,
8793
case_insensitive: bool,
94+
cmd: Option<String>,
8895
) -> anyhow::Result<()> {
8996
Prompt {
9097
renderer: Archived {
@@ -93,6 +100,7 @@ pub fn run(
93100
lines: Snapshot::new(lines),
94101
highlight_style,
95102
case_insensitive,
103+
cmd,
96104
},
97105
}
98106
.run()

‎src/archived/keymap.rs

+16
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,33 @@ pub type Keymap = fn(
99
&Event,
1010
&mut Snapshot<text_editor::State>,
1111
&mut Snapshot<listbox::State>,
12+
Option<String>,
1213
) -> anyhow::Result<PromptSignal>;
1314

1415
pub fn default(
1516
event: &Event,
1617
text_editor_snapshot: &mut Snapshot<text_editor::State>,
1718
logs_snapshot: &mut Snapshot<listbox::State>,
19+
cmd: Option<String>,
1820
) -> anyhow::Result<PromptSignal> {
1921
let text_editor_state = text_editor_snapshot.after_mut();
2022
let logs_state = logs_snapshot.after_mut();
2123

2224
match event {
25+
Event::Key(KeyEvent {
26+
code: KeyCode::Char('r'),
27+
modifiers: KeyModifiers::CONTROL,
28+
kind: KeyEventKind::Press,
29+
state: KeyEventState::NONE,
30+
}) => {
31+
if cmd.is_some() {
32+
// Exiting archive mode here allows
33+
// the caller to re-enter streaming mode,
34+
// as it is running in an infinite loop.
35+
return Ok(PromptSignal::Quit);
36+
}
37+
}
38+
2339
Event::Key(KeyEvent {
2440
code: KeyCode::Char('c'),
2541
modifiers: KeyModifiers::CONTROL,

‎src/cmd.rs

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
use std::process::Stdio;
2+
3+
use tokio::{
4+
io::{AsyncBufReadExt, BufReader},
5+
process::Command,
6+
sync::mpsc,
7+
time::{timeout, Duration},
8+
};
9+
use tokio_util::sync::CancellationToken;
10+
11+
pub async fn execute(
12+
cmdstr: &str,
13+
tx: mpsc::Sender<String>,
14+
retrieval_timeout: Duration,
15+
canceled: CancellationToken,
16+
) -> anyhow::Result<()> {
17+
let args: Vec<&str> = cmdstr.split_whitespace().collect();
18+
let mut child = Command::new(args[0])
19+
.args(&args[1..])
20+
.stdout(Stdio::piped())
21+
.stderr(Stdio::piped())
22+
.spawn()?;
23+
24+
let stdout = child
25+
.stdout
26+
.take()
27+
.ok_or_else(|| anyhow::anyhow!("stdout is not available"))?;
28+
let stderr = child
29+
.stderr
30+
.take()
31+
.ok_or_else(|| anyhow::anyhow!("stderr is not available"))?;
32+
let mut stdout_reader = BufReader::new(stdout).lines();
33+
let mut stderr_reader = BufReader::new(stderr).lines();
34+
35+
while !canceled.is_cancelled() {
36+
tokio::select! {
37+
stdout_res = timeout(retrieval_timeout, stdout_reader.next_line()) => {
38+
if let Ok(Ok(Some(line))) = stdout_res {
39+
let escaped = strip_ansi_escapes::strip_str(line.replace(['\n', '\t'], " "));
40+
tx.send(escaped).await?;
41+
}
42+
},
43+
stderr_res = timeout(retrieval_timeout, stderr_reader.next_line()) => {
44+
if let Ok(Ok(Some(line))) = stderr_res {
45+
let escaped = strip_ansi_escapes::strip_str(line.replace(['\n', '\t'], " "));
46+
tx.send(escaped).await?;
47+
}
48+
}
49+
}
50+
}
51+
52+
child.kill().await?;
53+
Ok(())
54+
}

‎src/main.rs

+115-39
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,44 @@ use promkit::{
1919
};
2020

2121
mod archived;
22+
mod cmd;
2223
mod sig;
2324
mod stdin;
2425
mod terminal;
2526

27+
#[derive(Eq, PartialEq)]
28+
pub enum Signal {
29+
Continue,
30+
GotoArchived,
31+
GotoStreaming,
32+
}
33+
2634
/// Interactive grep (for streaming)
2735
#[derive(Parser)]
2836
#[command(name = "sig", version)]
37+
#[command(
38+
name = "sig",
39+
version,
40+
help_template = "
41+
{about}
42+
43+
Usage: {usage}
44+
45+
Examples:
46+
47+
$ stern --context kind-kind etcd |& sig
48+
Or the method to retry command by pressing ctrl+r:
49+
$ sig --cmd \"stern --context kind-kind etcd\"
50+
51+
Archived mode:
52+
$ cat README.md |& sig -a
53+
Or
54+
$ sig -a --cmd \"cat README.md\"
55+
56+
Options:
57+
{options}
58+
"
59+
)]
2960
pub struct Args {
3061
#[arg(
3162
long = "retrieval-timeout",
@@ -71,6 +102,14 @@ pub struct Args {
71102
help = "Case insensitive search."
72103
)]
73104
pub case_insensitive: bool,
105+
106+
#[arg(
107+
long = "cmd",
108+
help = "Command to execute on initial and retries.",
109+
long_help = "This command is invoked initially and
110+
whenever a retry is triggered according to key mappings."
111+
)]
112+
pub cmd: Option<String>,
74113
}
75114

76115
impl Drop for Args {
@@ -92,14 +131,26 @@ async fn main() -> anyhow::Result<()> {
92131
if args.archived {
93132
let (tx, mut rx) = mpsc::channel(1);
94133

95-
tokio::spawn(async move {
96-
stdin::streaming(
97-
tx,
98-
Duration::from_millis(args.retrieval_timeout_millis),
99-
CancellationToken::new(),
100-
)
101-
.await
102-
});
134+
if let Some(cmd) = args.cmd.clone() {
135+
tokio::spawn(async move {
136+
cmd::execute(
137+
&cmd,
138+
tx,
139+
Duration::from_millis(args.retrieval_timeout_millis),
140+
CancellationToken::new(),
141+
)
142+
.await
143+
});
144+
} else {
145+
tokio::spawn(async move {
146+
stdin::streaming(
147+
tx,
148+
Duration::from_millis(args.retrieval_timeout_millis),
149+
CancellationToken::new(),
150+
)
151+
.await
152+
});
153+
}
103154

104155
let mut queue = VecDeque::with_capacity(args.queue_capacity);
105156
loop {
@@ -149,9 +200,11 @@ async fn main() -> anyhow::Result<()> {
149200
},
150201
highlight_style,
151202
args.case_insensitive,
203+
// In archived mode, command for retry is meaningless.
204+
None,
152205
)?;
153206
} else {
154-
let queue = sig::run(
207+
while let Ok((signal, queue)) = sig::run(
155208
text_editor::State {
156209
texteditor: Default::default(),
157210
history: Default::default(),
@@ -169,39 +222,62 @@ async fn main() -> anyhow::Result<()> {
169222
Duration::from_millis(args.render_interval_millis),
170223
args.queue_capacity,
171224
args.case_insensitive,
225+
args.cmd.clone(),
172226
)
173-
.await?;
227+
.await
228+
{
229+
crossterm::execute!(
230+
io::stdout(),
231+
crossterm::terminal::Clear(crossterm::terminal::ClearType::All),
232+
crossterm::terminal::Clear(crossterm::terminal::ClearType::Purge),
233+
cursor::MoveTo(0, 0),
234+
)?;
174235

175-
crossterm::execute!(
176-
io::stdout(),
177-
crossterm::terminal::Clear(crossterm::terminal::ClearType::All),
178-
crossterm::terminal::Clear(crossterm::terminal::ClearType::Purge),
179-
cursor::MoveTo(0, 0),
180-
)?;
236+
match signal {
237+
Signal::GotoArchived => {
238+
archived::run(
239+
text_editor::State {
240+
texteditor: Default::default(),
241+
history: Default::default(),
242+
prefix: String::from("❯❯❯ "),
243+
mask: Default::default(),
244+
prefix_style: StyleBuilder::new().fgc(Color::DarkBlue).build(),
245+
active_char_style: StyleBuilder::new().bgc(Color::DarkCyan).build(),
246+
inactive_char_style: StyleBuilder::new().build(),
247+
edit_mode: Default::default(),
248+
word_break_chars: Default::default(),
249+
lines: Default::default(),
250+
},
251+
listbox::State {
252+
listbox: listbox::Listbox::from_iter(queue),
253+
cursor: String::from("❯ "),
254+
active_item_style: None,
255+
inactive_item_style: None,
256+
lines: Default::default(),
257+
},
258+
highlight_style,
259+
args.case_insensitive,
260+
args.cmd.clone(),
261+
)?;
181262

182-
archived::run(
183-
text_editor::State {
184-
texteditor: Default::default(),
185-
history: Default::default(),
186-
prefix: String::from("❯❯❯ "),
187-
mask: Default::default(),
188-
prefix_style: StyleBuilder::new().fgc(Color::DarkBlue).build(),
189-
active_char_style: StyleBuilder::new().bgc(Color::DarkCyan).build(),
190-
inactive_char_style: StyleBuilder::new().build(),
191-
edit_mode: Default::default(),
192-
word_break_chars: Default::default(),
193-
lines: Default::default(),
194-
},
195-
listbox::State {
196-
listbox: listbox::Listbox::from_iter(queue),
197-
cursor: String::from("❯ "),
198-
active_item_style: None,
199-
inactive_item_style: None,
200-
lines: Default::default(),
201-
},
202-
highlight_style,
203-
args.case_insensitive,
204-
)?;
263+
// Re-enable raw mode and hide the cursor again here
264+
// because they are disabled and shown, respectively, by promkit.
265+
enable_raw_mode()?;
266+
execute!(io::stdout(), cursor::Hide)?;
267+
268+
crossterm::execute!(
269+
io::stdout(),
270+
crossterm::terminal::Clear(crossterm::terminal::ClearType::All),
271+
crossterm::terminal::Clear(crossterm::terminal::ClearType::Purge),
272+
cursor::MoveTo(0, 0),
273+
)?;
274+
}
275+
Signal::GotoStreaming => {
276+
continue;
277+
}
278+
_ => {}
279+
}
280+
}
205281
}
206282

207283
Ok(())

‎src/sig.rs

+13-8
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ use promkit::{
1515
crossterm::{self, event, style::ContentStyle},
1616
grapheme::StyledGraphemes,
1717
switch::ActiveKeySwitcher,
18-
text_editor, PaneFactory, PromptSignal,
18+
text_editor, PaneFactory,
1919
};
2020

2121
mod keymap;
22-
use crate::{stdin, terminal::Terminal};
22+
use crate::{cmd, stdin, terminal::Terminal, Signal};
2323

2424
fn matched(queries: &[&str], line: &str, case_insensitive: bool) -> anyhow::Result<Vec<Match>> {
2525
let mut matched = Vec::new();
@@ -78,7 +78,8 @@ pub async fn run(
7878
render_interval: Duration,
7979
queue_capacity: usize,
8080
case_insensitive: bool,
81-
) -> anyhow::Result<VecDeque<String>> {
81+
cmd: Option<String>,
82+
) -> anyhow::Result<(Signal, VecDeque<String>)> {
8283
let keymap = ActiveKeySwitcher::new("default", keymap::default);
8384
let size = crossterm::terminal::size()?;
8485

@@ -95,8 +96,11 @@ pub async fn run(
9596
let canceler = CancellationToken::new();
9697

9798
let canceled = canceler.clone();
98-
let streaming =
99-
tokio::spawn(async move { stdin::streaming(tx, retrieval_timeout, canceled).await });
99+
let streaming = if let Some(cmd) = cmd.clone() {
100+
tokio::spawn(async move { cmd::execute(&cmd, tx, retrieval_timeout, canceled).await })
101+
} else {
102+
tokio::spawn(async move { stdin::streaming(tx, retrieval_timeout, canceled).await })
103+
};
100104

101105
let keeping: JoinHandle<anyhow::Result<VecDeque<String>>> = tokio::spawn(async move {
102106
let mut queue = VecDeque::with_capacity(queue_capacity);
@@ -135,11 +139,12 @@ pub async fn run(
135139
Ok(queue)
136140
});
137141

142+
let mut signal: Signal;
138143
loop {
139144
let event = event::read()?;
140145
let mut text_editor = shared_text_editor.write().await;
141-
let signal = keymap.get()(&event, &mut text_editor)?;
142-
if signal == PromptSignal::Quit {
146+
signal = keymap.get()(&event, &mut text_editor, cmd.clone())?;
147+
if signal == Signal::GotoArchived || signal == Signal::GotoStreaming {
143148
break;
144149
}
145150

@@ -152,5 +157,5 @@ pub async fn run(
152157
canceler.cancel();
153158
let _: anyhow::Result<(), anyhow::Error> = streaming.await?;
154159

155-
keeping.await?
160+
Ok((signal, keeping.await??))
156161
}

‎src/sig/keymap.rs

+21-4
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,33 @@
11
use promkit::{
22
crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers},
3-
text_editor, PromptSignal,
3+
text_editor,
44
};
55

6-
pub fn default(event: &Event, state: &mut text_editor::State) -> anyhow::Result<PromptSignal> {
6+
use crate::Signal;
7+
8+
pub fn default(
9+
event: &Event,
10+
state: &mut text_editor::State,
11+
cmd: Option<String>,
12+
) -> anyhow::Result<Signal> {
713
match event {
814
Event::Key(KeyEvent {
915
code: KeyCode::Char('f'),
1016
modifiers: KeyModifiers::CONTROL,
1117
kind: KeyEventKind::Press,
1218
state: KeyEventState::NONE,
13-
}) => return Ok(PromptSignal::Quit),
19+
}) => return Ok(Signal::GotoArchived),
20+
21+
Event::Key(KeyEvent {
22+
code: KeyCode::Char('r'),
23+
modifiers: KeyModifiers::CONTROL,
24+
kind: KeyEventKind::Press,
25+
state: KeyEventState::NONE,
26+
}) => {
27+
if cmd.is_some() {
28+
return Ok(Signal::GotoStreaming);
29+
}
30+
}
1431

1532
Event::Key(KeyEvent {
1633
code: KeyCode::Char('c'),
@@ -82,5 +99,5 @@ pub fn default(event: &Event, state: &mut text_editor::State) -> anyhow::Result<
8299

83100
_ => (),
84101
}
85-
Ok(PromptSignal::Continue)
102+
Ok(Signal::Continue)
86103
}

0 commit comments

Comments
 (0)
Please sign in to comment.