From b167d2c980f6b2b6a19aeda42cf0847e78ad8855 Mon Sep 17 00:00:00 2001 From: SW Date: Mon, 15 Jun 2026 11:09:42 +0200 Subject: [PATCH] Add configurable terminal (Ghostty, iTerm, Terminal) via config.toml --- Cargo.lock | 55 ++++++++++++++++++++++ Cargo.toml | 1 + src/app.rs | 5 +- src/commands.rs | 118 ++++++++++++++++++++++++++++++++++++++---------- src/config.rs | 43 +++++++++++++++++- src/events.rs | 4 +- src/main.rs | 2 +- 7 files changed, 198 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 37a1137..4619fdb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -763,6 +763,7 @@ dependencies = [ "crossterm", "directories", "ratatui", + "toml", "walkdir", ] @@ -1388,6 +1389,15 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "sha2" version = "0.10.9" @@ -1627,6 +1637,45 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + [[package]] name = "typenum" version = "1.20.1" @@ -1938,6 +1987,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" + [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/Cargo.toml b/Cargo.toml index e85f2b5..9ea8788 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,4 +8,5 @@ clap = { version = "4.6.1", features = ["derive"] } crossterm = "0.29.0" directories = "6.0.0" ratatui = "0.30.1" +toml = "1.1.2" walkdir = "2.5.0" diff --git a/src/app.rs b/src/app.rs index 9ebb02e..4b162cb 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,5 +1,6 @@ use std::path::PathBuf; +use crate::config::Terminal; use crate::parser::{load_groups, CommandFile, Group, Section}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -11,6 +12,7 @@ pub enum Focus { pub struct App { pub data_dir: PathBuf, + pub terminal: Terminal, pub groups: Vec, pub group_index: usize, pub file_index: usize, @@ -20,10 +22,11 @@ pub struct App { } impl App { - pub fn new(data_dir: PathBuf) -> Self { + pub fn new(data_dir: PathBuf, terminal: Terminal) -> Self { let groups = load_groups(&data_dir); App { data_dir, + terminal, groups, group_index: 0, file_index: 0, diff --git a/src/commands.rs b/src/commands.rs index b351170..92a7fb6 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -3,9 +3,10 @@ use std::os::unix::fs::PermissionsExt; use std::path::Path; use std::process::Command; +use crate::config::Terminal; use crate::parser::{CommandFile, Section}; -pub fn run_section(section: &Section, file: &CommandFile) { +pub fn run_section(section: &Section, file: &CommandFile, terminal: &Terminal) { let commands = if section.is_run_all() { file.sections .iter() @@ -21,14 +22,14 @@ pub fn run_section(section: &Section, file: &CommandFile) { } let script = build_script(&commands); - execute_in_terminal(&script); + execute_in_terminal(&script, terminal); } fn build_script(commands: &[String]) -> String { commands.join("\n") } -fn execute_in_terminal(script: &str) { +fn execute_in_terminal(script: &str, terminal: &Terminal) { let tmp_dir = std::env::temp_dir(); let script_path = tmp_dir.join(format!("lines_run_{}.sh", random_suffix())); @@ -37,22 +38,28 @@ fn execute_in_terminal(script: &str) { Err(_) => return, }; - if file.write_all(script.as_bytes()).is_err() { + let script_with_cleanup = format!("{}\nrm -f {}", script, escape_shell(&script_path.to_string_lossy())); + + if file.write_all(script_with_cleanup.as_bytes()).is_err() { return; } - // Best-effort attempt to make executable; Terminal.app can run `bash script.sh` anyway. let _ = std::fs::set_permissions(&script_path, std::fs::Permissions::from_mode(0o755)); - let path_str = script_path.to_string_lossy(); + match terminal { + Terminal::Ghostty => run_in_ghostty(&script_path), + Terminal::Iterm => run_in_iterm(&script_path), + Terminal::Builtin => run_in_terminal_app(&script_path), + } +} - // Open a new Terminal.app window and run the script. +fn run_in_terminal_app(script_path: &Path) { + let path_str = script_path.to_string_lossy(); let applescript = format!( r#"tell application "Terminal" - do script "clear; bash {}; rm {}" + do script "clear; bash {}" activate end tell"#, - escape_applescript(&path_str), escape_applescript(&path_str) ); @@ -62,29 +69,90 @@ end tell"#, .spawn(); } +fn run_in_ghostty(script_path: &Path) { + let path_str = script_path.to_string_lossy(); + let _ = Command::new("open") + .arg("-na") + .arg("Ghostty") + .arg("--args") + .arg("-e") + .arg("bash") + .arg(path_str.as_ref()) + .spawn(); +} + +fn run_in_iterm(script_path: &Path) { + let path_str = script_path.to_string_lossy(); + let applescript = format!( + r#"tell application "iTerm" + create window with default profile + tell current session of current window + write text "clear; bash {}" + end tell + activate +end tell"#, + escape_applescript(&path_str) + ); + + let _ = Command::new("osascript") + .arg("-e") + .arg(applescript) + .spawn(); +} + +pub fn edit_file(path: &Path, terminal: &Terminal) { + let editor = std::env::var("EDITOR").unwrap_or_else(|_| "nano".to_string()); + + match terminal { + Terminal::Ghostty => { + let _ = Command::new("open") + .arg("-na") + .arg("Ghostty") + .arg("--args") + .arg("-e") + .arg("bash") + .arg("-c") + .arg(format!( + "cd {} && {} {}; exec bash", + escape_shell(&std::env::current_dir().unwrap_or_default().to_string_lossy()), + escape_shell(&editor), + escape_shell(&path.to_string_lossy()) + )) + .spawn(); + } + _ => { + let app = match terminal { + Terminal::Iterm => "iTerm", + _ => "Terminal", + }; + let _ = Command::new("osascript") + .arg("-e") + .arg(format!( + r#"tell application "{}" + do script "cd {}; {} {}; exit" + activate +end tell"#, + app, + escape_applescript(&std::env::current_dir().unwrap_or_default().to_string_lossy()), + escape_applescript(&editor), + escape_applescript(&path.to_string_lossy()) + )) + .spawn(); + } + } +} + fn escape_applescript(s: &str) -> String { s.replace('\\', "\\\\").replace('"', "\\\"") } +fn escape_shell(s: &str) -> String { + s.replace('\\', "\\\\").replace('"', "\\\"").replace('\'', "'\"'\"'") +} + fn random_suffix() -> u64 { std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_secs()) .unwrap_or(0) } - -pub fn edit_file(path: &Path) { - let editor = std::env::var("EDITOR").unwrap_or_else(|_| "nano".to_string()); - let _ = Command::new("osascript") - .arg("-e") - .arg(format!( - r#"tell application "Terminal" - do script "cd {}; {} {}; exit" - activate -end tell"#, - escape_applescript(&std::env::current_dir().unwrap_or_default().to_string_lossy()), - escape_applescript(&editor), - escape_applescript(&path.to_string_lossy()) - )) - .spawn(); -} diff --git a/src/config.rs b/src/config.rs index 7ee1ed6..eef3915 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,13 +1,37 @@ +use std::fs; +use std::path::Path; use std::path::PathBuf; +#[derive(Debug, Clone)] pub struct Config { pub data_dir: PathBuf, + pub terminal: Terminal, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub enum Terminal { + #[default] + Builtin, + Ghostty, + Iterm, +} + +impl Terminal { + pub fn from_str(s: &str) -> Option { + match s.to_lowercase().as_str() { + "terminal" | "terminal.app" => Some(Terminal::Builtin), + "ghostty" => Some(Terminal::Ghostty), + "iterm" | "iterm2" => Some(Terminal::Iterm), + _ => None, + } + } } impl Config { pub fn new(data_dir: Option) -> Self { let data_dir = data_dir.unwrap_or_else(default_data_dir); - Config { data_dir } + let terminal = load_terminal_preference(&data_dir); + Config { data_dir, terminal } } } @@ -16,3 +40,20 @@ fn default_data_dir() -> PathBuf { .map(|d| d.home_dir().join(".lines")) .unwrap_or_else(|| PathBuf::from(".lines")) } + +fn load_terminal_preference(data_dir: &Path) -> Terminal { + let config_path = data_dir.join("config.toml"); + let content = match fs::read_to_string(&config_path) { + Ok(c) => c, + Err(_) => return Terminal::default(), + }; + + match toml::from_str::(&content) { + Ok(value) => value + .get("terminal") + .and_then(|v| v.as_str()) + .and_then(Terminal::from_str) + .unwrap_or_default(), + Err(_) => Terminal::default(), + } +} diff --git a/src/events.rs b/src/events.rs index 98ac2c2..639eb66 100644 --- a/src/events.rs +++ b/src/events.rs @@ -38,12 +38,12 @@ pub fn handle_event(app: &mut App, event: AppEvent) -> bool { KeyCode::Char('r') => app.reload(), KeyCode::Char('e') => { if let Some(file) = app.current_file() { - edit_file(&file.path); + edit_file(&file.path, &app.terminal); } } KeyCode::Enter => { if let (Some(section), Some(file)) = (app.current_section(), app.current_file()) { - run_section(section, file); + run_section(section, file, &app.terminal); } } _ => {} diff --git a/src/main.rs b/src/main.rs index b3903dd..0e5d4ae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -43,7 +43,7 @@ fn main() -> io::Result<()> { eprintln!("Nepodařilo se vytvořit datovou složku {}: {}", config.data_dir.display(), e); } - let app = App::new(config.data_dir); + let app = App::new(config.data_dir, config.terminal); enable_raw_mode()?; let mut stdout = io::stdout();