diff --git a/Cargo.lock b/Cargo.lock index 7704610..ba3021b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1231,6 +1231,7 @@ checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" name = "sshmngrs" version = "0.1.0" dependencies = [ + "libc", "ratatui", "serde", "toml", diff --git a/Cargo.toml b/Cargo.toml index 1803fe0..e063239 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,3 +9,6 @@ ratatui = "0.30.2" serde = { version = "1.0.229", features = ["derive"] } toml = "1.1.3" tui-input = "0.15.3" + +[target.'cfg(unix)'.dependencies] +libc = "0.2.189" diff --git a/src/main.rs b/src/main.rs index ad98c62..1cb96f7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,13 @@ +mod misc; + +use misc::{ + centered_rect, ignore_interrupt_signals, reset_child_interrupt_signals, + restore_interrupt_signals, wait_for_enter, +}; use ratatui::{ DefaultTerminal, Frame, crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}, - layout::{Constraint, Flex, Layout, Rect}, + layout::{Constraint, Layout, Rect}, style::{Color, Style}, widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph}, }; @@ -126,6 +132,7 @@ struct App { config: Config, selected: usize, mode: Mode, + pending_command: Option, should_quit: bool, } @@ -137,6 +144,7 @@ impl App { config, selected: 0, mode: Mode::Normal, + pending_command: None, should_quit: false, } } @@ -170,6 +178,7 @@ impl App { KeyCode::Char('a') => self.mode = Mode::Editing(CommandForm::new()), KeyCode::Char('e') => self.start_editing(), KeyCode::Char('d') => self.delete_selected(), + KeyCode::Enter => self.connect_selected(), KeyCode::Up | KeyCode::Char('k') => self.select_previous(), KeyCode::Down | KeyCode::Char('j') => self.select_next(), _ => {} @@ -192,6 +201,12 @@ impl App { } } + fn connect_selected(&mut self) { + if let Some(command) = self.config.commands.get(self.selected) { + self.pending_command = Some(command.command.clone()); + } + } + fn on_key_editing(&mut self, key: KeyEvent) { match key.code { KeyCode::Esc => self.mode = Mode::Normal, @@ -253,10 +268,59 @@ fn run(mut terminal: DefaultTerminal) -> std::io::Result<()> { } } } + + if let Some(command) = app.pending_command.take() { + launch_command(&mut terminal, &command)?; + } } Ok(()) } +fn launch_command(terminal: &mut DefaultTerminal, command: &str) -> std::io::Result<()> { + ratatui::restore(); + + println!("Running: {command}\n"); + + // In Unix, while the child runs the command is in cooked mode, so Ctrl+C sends + // SIGINT to the whole foreground process group (sshmngrs included). Ignore it in + // the parent so interrupting the child, for example, at an ssh password prompt, + // returns to sshmngrs instead of killing it. The child resets its own handlers to + // default below so it can still be interrupted. + ignore_interrupt_signals(); + let mut shell = shell_command(command); + reset_child_interrupt_signals(&mut shell); + + let status = shell.status(); + + match &status { + Ok(code) if !code.success() => eprintln!("\nCommand exited with {code}."), + Ok(code) => println!("\nCommand exited with {code}."), + Err(error) => eprintln!("\nCould not run command: {error}"), + } + + wait_for_enter("Press Enter to restore sshmngrs..."); + + restore_interrupt_signals(); + + *terminal = ratatui::init(); + terminal.clear()?; + Ok(()) +} + +fn shell_command(command: &str) -> std::process::Command { + let mut shell = if cfg!(target_os = "windows") { + let mut shell = std::process::Command::new("cmd"); + shell.arg("/C"); + shell + } else { + let mut shell = std::process::Command::new("sh"); + shell.arg("-c"); + shell + }; + shell.arg(command); + shell +} + fn draw(frame: &mut Frame, app: &App) { draw_main(frame, app, frame.area()); @@ -342,11 +406,3 @@ fn draw_field(frame: &mut Frame, area: Rect, title: &str, input: &Input, focused frame.set_cursor_position((cursor_x, cursor_y)); } } - -fn centered_rect(area: Rect, width: u16, height: u16) -> Rect { - let horizontal = Layout::horizontal([Constraint::Length(width)]).flex(Flex::Center); - let vertical = Layout::vertical([Constraint::Length(height)]).flex(Flex::Center); - let [area] = vertical.areas(area); - let [area] = horizontal.areas(area); - area -} diff --git a/src/misc.rs b/src/misc.rs new file mode 100644 index 0000000..6365e07 --- /dev/null +++ b/src/misc.rs @@ -0,0 +1,54 @@ +use ratatui::layout::{Constraint, Flex, Layout, Rect}; + +pub fn wait_for_enter(prompt: &str) { + println!("{prompt}"); + let mut discard = String::new(); + let _ = std::io::stdin().read_line(&mut discard); +} + +pub fn centered_rect(area: Rect, width: u16, height: u16) -> Rect { + let horizontal = Layout::horizontal([Constraint::Length(width)]).flex(Flex::Center); + let vertical = Layout::vertical([Constraint::Length(height)]).flex(Flex::Center); + let [area] = vertical.areas(area); + let [area] = horizontal.areas(area); + area +} + +#[cfg(unix)] +pub fn ignore_interrupt_signals() { + unsafe { + libc::signal(libc::SIGINT, libc::SIG_IGN); + libc::signal(libc::SIGQUIT, libc::SIG_IGN); + } +} + +#[cfg(unix)] +pub fn restore_interrupt_signals() { + unsafe { + libc::signal(libc::SIGINT, libc::SIG_DFL); + libc::signal(libc::SIGQUIT, libc::SIG_DFL); + } +} + +#[cfg(unix)] +pub fn reset_child_interrupt_signals(command: &mut std::process::Command) { + use std::os::unix::process::CommandExt; + // Safety: the closure only resets signal dispositions to their default, + // which is async-signal-safe and valid between fork and exec. + unsafe { + command.pre_exec(|| { + libc::signal(libc::SIGINT, libc::SIG_DFL); + libc::signal(libc::SIGQUIT, libc::SIG_DFL); + Ok(()) + }); + } +} + +#[cfg(not(unix))] +pub fn ignore_interrupt_signals() {} + +#[cfg(not(unix))] +pub fn restore_interrupt_signals() {} + +#[cfg(not(unix))] +pub fn reset_child_interrupt_signals(_command: &mut std::process::Command) {}