Finally works, now the commands it's launched

main
kirbylife 2026-07-25 01:20:38 -06:00
parent e76881005c
commit 31d13507fe
4 changed files with 123 additions and 9 deletions

1
Cargo.lock generated
View File

@ -1231,6 +1231,7 @@ checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
name = "sshmngrs"
version = "0.1.0"
dependencies = [
"libc",
"ratatui",
"serde",
"toml",

View File

@ -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"

View File

@ -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<String>,
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
}

54
src/misc.rs 100644
View File

@ -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) {}