together_rs/
kb.rs

1use std::ops::ControlFlow;
2
3use crate::{
4    config::{self, StartTogetherOptions},
5    errors::TogetherResult,
6    log, log_err,
7    manager::{self, ProcessAction},
8    process, t_println,
9    terminal::Terminal,
10    terminal_ext::TerminalExt,
11};
12
13#[derive(Default)]
14struct InputState {
15    requested_quit: bool,
16    awaiting_quit_command: bool,
17    last_command: Option<BufferedCommand>,
18}
19
20enum BufferedCommand {
21    Start(String),
22    Restart(String, process::ProcessId),
23}
24
25enum Key {
26    #[cfg(feature = "termion")]
27    CtrlC,
28    Char(char),
29}
30
31#[cfg(feature = "termion")]
32impl TryFrom<termion::event::Key> for Key {
33    type Error = ();
34
35    fn try_from(key: termion::event::Key) -> Result<Self, Self::Error> {
36        match key {
37            termion::event::Key::Ctrl('c') => Ok(Self::CtrlC),
38            termion::event::Key::Char(c) => Ok(Self::Char(c)),
39            _ => Err(()),
40        }
41    }
42}
43
44impl From<char> for Key {
45    fn from(c: char) -> Self {
46        Self::Char(c)
47    }
48}
49
50#[cfg(feature = "termion")]
51pub fn block_for_user_input(
52    start_opts: &StartTogetherOptions,
53    sender: manager::ProcessManagerHandle,
54) -> TogetherResult<()> {
55    use std::io::Write;
56    // use termion::event::Key;
57    use termion::input::TermRead;
58
59    let mut state = InputState::default();
60
61    // let mut stdout = std::io::stdout().into_raw_mode().unwrap();
62    let mut stdout = std::io::stdout();
63    let stdin = std::io::stdin();
64
65    for k in stdin.keys() {
66        let Ok(k): Result<Key, ()> = k?.try_into() else {
67            continue;
68        };
69
70        match handle_key_press(k, &mut state, start_opts, &sender) {
71            Ok(ControlFlow::Continue(_)) => {
72                write!(stdout, "{}", termion::cursor::Show).unwrap();
73                stdout.flush().unwrap();
74            }
75            Ok(ControlFlow::Break(_)) => break,
76            Err(e) => {
77                log_err!("Unexpected error: {:?}", e);
78            }
79        }
80    }
81
82    drop(stdout);
83    Ok(())
84}
85
86#[cfg(not(feature = "termion"))]
87pub fn block_for_user_input(
88    start_opts: &StartTogetherOptions,
89    sender: manager::ProcessManagerHandle,
90) -> TogetherResult<()> {
91    let mut state = InputState::default();
92    let mut input = String::new();
93    loop {
94        std::io::stdin().read_line(&mut input)?;
95        let Some(key) = input.trim().chars().next() else {
96            continue;
97        };
98
99        match handle_key_press(key.into(), &mut state, start_opts, &sender) {
100            Ok(ControlFlow::Continue(_)) => {}
101            Ok(ControlFlow::Break(_)) => break,
102            Err(e) => {
103                log_err!("Unexpected error: {:?}", e);
104            }
105        }
106
107        input.clear();
108    }
109    Ok(())
110}
111
112fn handle_key_press(
113    key: Key,
114    state: &mut InputState,
115    start_opts: &StartTogetherOptions,
116    sender: &manager::ProcessManagerHandle,
117) -> TogetherResult<ControlFlow<()>> {
118    if state.requested_quit {
119        state.requested_quit = false;
120        state.awaiting_quit_command = true;
121    }
122
123    match key {
124        #[cfg(feature = "termion")]
125        Key::CtrlC => {
126            log!("Ctrl-C pressed, stopping all processes...");
127            sender
128                .send(ProcessAction::KillAll)
129                .expect("Could not send signal on channel.");
130        }
131        Key::Char('h') | Key::Char('?') => {
132            log!("[help]");
133            t_println!("together is a tool to run multiple commands in parallel selectively by an interactive prompt.");
134
135            t_println!();
136            t_println!("Press 't' to trigger a one-time run");
137            t_println!("Press '.' to re-trigger the last one-time run or restart action");
138            if let Some(last) = &state.last_command {
139                t_println!(
140                    "  (last command: [{}] {})",
141                    match last {
142                        BufferedCommand::Start(_) => "start",
143                        BufferedCommand::Restart(_, _) => "restart",
144                    },
145                    match last {
146                        BufferedCommand::Start(command) => command,
147                        BufferedCommand::Restart(command, _) => command,
148                    }
149                );
150            }
151            t_println!("Press 'b' to batch trigger commands by recipe");
152            t_println!("Press 'z' to switch to running a single recipe");
153            t_println!("Press 'k' to kill a running command");
154            t_println!("Press 'r' to restart a running command");
155            t_println!("Press 'l' to list all running commands");
156            t_println!("Press 'd' to dump the current configuration");
157            t_println!("Press 'h' or '?' to show this help message");
158            t_println!("Press 'q' to stop");
159            t_println!();
160
161            t_println!();
162            log!("[status]");
163            match sender.list() {
164                Ok(list) => {
165                    t_println!("together is running {} commands in parallel:", list.len());
166                    for command in list {
167                        t_println!("  {}", command);
168                    }
169                }
170                Err(_) => {
171                    t_println!("together is running in an unknown state");
172                }
173            }
174        }
175        Key::Char('q') => {
176            if state.awaiting_quit_command {
177                log!("Quitting together...");
178                sender.send(ProcessAction::KillAll)?;
179                return Ok(ControlFlow::Break(()));
180            }
181
182            log!("Press 'q' again to quit together");
183            state.requested_quit = true;
184            return Ok(ControlFlow::Break(()));
185        }
186        Key::Char('l') => {
187            for command in sender.list()? {
188                t_println!("{}", command);
189            }
190        }
191        Key::Char('d') => {
192            let list = sender.list()?;
193            let running: Vec<_> = list.iter().map(|c| c.command()).collect();
194            let config = start_opts.config.clone();
195            let config = config.with_running(&running);
196            config::dump(&config)?;
197        }
198        Key::Char('k') => {
199            let list = sender.list()?;
200            let command = Terminal::select_single_process(
201                "Pick command to kill, or press 'q' to cancel",
202                &sender,
203                &list,
204            )?;
205            if let Some(command) = command {
206                sender.kill(command.clone())?;
207            }
208        }
209        Key::Char('K') => {
210            let list = sender.list()?;
211            let command = Terminal::select_single_process(
212                "Pick command to kill, or press 'q' to cancel",
213                &sender,
214                &list,
215            )?;
216            let signal = command.and_then(|_| {
217                Terminal::select_single(
218                    "Pick signal to send, or press 'q' to cancel",
219                    &["SIGINT", "SIGTERM", "SIGKILL"],
220                )
221            });
222            let target = signal
223                .and_then(|signal| match *signal {
224                    "SIGINT" => Some(process::ProcessSignal::SIGINT),
225                    "SIGTERM" => Some(process::ProcessSignal::SIGTERM),
226                    "SIGKILL" => Some(process::ProcessSignal::SIGKILL),
227                    _ => None,
228                })
229                .and_then(|signal| command.map(|command| (command, signal)));
230            if let Some((command, signal)) = target {
231                sender.send(ProcessAction::KillAdvanced(command.clone(), signal))?;
232            }
233        }
234        Key::Char('r') => {
235            let list = sender.list()?;
236            let command = Terminal::select_single_process(
237                "Pick command to restart, or press 'q' to cancel",
238                &sender,
239                &list,
240            )?;
241            if let Some(command) = command {
242                sender.send(ProcessAction::Kill(command.clone()))?;
243                let process_id = sender.spawn(command.command())?;
244                state.last_command = Some(BufferedCommand::Restart(
245                    command.command().to_string(),
246                    process_id,
247                ));
248            }
249        }
250        Key::Char('t') => {
251            let list = sender.list()?;
252            let command = Terminal::select_single_command_with_running(
253                "Pick command to run, or press 'q' to cancel",
254                &sender,
255                &start_opts.config.start_options.commands,
256                &list,
257            )?;
258            if let Some(command) = command {
259                sender.spawn(command)?;
260                state.last_command = Some(BufferedCommand::Start(command.to_string()));
261            }
262        }
263        Key::Char('.') => match &state.last_command {
264            Some(BufferedCommand::Start(command)) => {
265                sender.spawn(command)?;
266            }
267            Some(BufferedCommand::Restart(command, process_id)) => {
268                match sender.restart(process_id.clone(), &command)? {
269                    Some(id) => {
270                        let command = command.clone();
271                        state.last_command = Some(BufferedCommand::Restart(command, id))
272                    }
273                    None => {
274                        log_err!("Could not find process to restart");
275                    }
276                };
277            }
278            _ => {
279                log!("No last command to re-trigger");
280            }
281        },
282        Key::Char('b') => {
283            let all_recipes = config::get_unique_recipes(&start_opts.config.start_options);
284            let all_recipes = all_recipes.into_iter().cloned().collect::<Vec<_>>();
285            let recipes = Terminal::select_multiple_recipes(
286                "Select one or more recipes to start running, or press 'q' to cancel",
287                &sender,
288                &all_recipes,
289            )?;
290            let commands =
291                config::collect_commands_by_recipes(&start_opts.config.start_options, &recipes);
292            for command in commands {
293                sender.send(ProcessAction::Create(command.clone()))?;
294            }
295        }
296        Key::Char('z') => {
297            let all_recipes = config::get_unique_recipes(&start_opts.config.start_options);
298            let all_recipes = all_recipes.into_iter().cloned().collect::<Vec<_>>();
299            let recipe = Terminal::select_single_recipe(
300                "Select a recipe to start running, or press 'q' to cancel (note: this will stop all other commands)",
301                &sender,
302                &all_recipes,
303            )?;
304            if let Some(recipe) = recipe {
305                let recipe = recipe.clone();
306                let recipe_commands = config::collect_commands_by_recipes(
307                    &start_opts.config.start_options,
308                    &[recipe],
309                );
310                let list = sender.list()?;
311                let kill_commands: Vec<_> = list
312                    .iter()
313                    .filter(|c| !recipe_commands.contains(&c.command().to_string()))
314                    .collect();
315
316                for command in kill_commands {
317                    sender.kill(command.clone())?;
318                }
319                for command in recipe_commands {
320                    sender.spawn(&command)?;
321                }
322            }
323        }
324        Key::Char('\n') => {}
325        Key::Char(c) => {
326            log_err!("Unknown command: '{}'", c);
327            log!("Press 'h' or '?' for help");
328        }
329    }
330    state.awaiting_quit_command = false;
331
332    Ok(ControlFlow::Continue(()))
333}