Add configurable terminal (Ghostty, iTerm, Terminal) via config.toml

This commit is contained in:
SW 2026-06-15 11:09:42 +02:00
parent d4150757ca
commit b167d2c980
7 changed files with 198 additions and 30 deletions

55
Cargo.lock generated
View file

@ -763,6 +763,7 @@ dependencies = [
"crossterm", "crossterm",
"directories", "directories",
"ratatui", "ratatui",
"toml",
"walkdir", "walkdir",
] ]
@ -1388,6 +1389,15 @@ dependencies = [
"zmij", "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]] [[package]]
name = "sha2" name = "sha2"
version = "0.10.9" version = "0.10.9"
@ -1627,6 +1637,45 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" 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]] [[package]]
name = "typenum" name = "typenum"
version = "1.20.1" version = "1.20.1"
@ -1938,6 +1987,12 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "winnow"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
[[package]] [[package]]
name = "wit-bindgen" name = "wit-bindgen"
version = "0.51.0" version = "0.51.0"

View file

@ -8,4 +8,5 @@ clap = { version = "4.6.1", features = ["derive"] }
crossterm = "0.29.0" crossterm = "0.29.0"
directories = "6.0.0" directories = "6.0.0"
ratatui = "0.30.1" ratatui = "0.30.1"
toml = "1.1.2"
walkdir = "2.5.0" walkdir = "2.5.0"

View file

@ -1,5 +1,6 @@
use std::path::PathBuf; use std::path::PathBuf;
use crate::config::Terminal;
use crate::parser::{load_groups, CommandFile, Group, Section}; use crate::parser::{load_groups, CommandFile, Group, Section};
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -11,6 +12,7 @@ pub enum Focus {
pub struct App { pub struct App {
pub data_dir: PathBuf, pub data_dir: PathBuf,
pub terminal: Terminal,
pub groups: Vec<Group>, pub groups: Vec<Group>,
pub group_index: usize, pub group_index: usize,
pub file_index: usize, pub file_index: usize,
@ -20,10 +22,11 @@ pub struct App {
} }
impl 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); let groups = load_groups(&data_dir);
App { App {
data_dir, data_dir,
terminal,
groups, groups,
group_index: 0, group_index: 0,
file_index: 0, file_index: 0,

View file

@ -3,9 +3,10 @@ use std::os::unix::fs::PermissionsExt;
use std::path::Path; use std::path::Path;
use std::process::Command; use std::process::Command;
use crate::config::Terminal;
use crate::parser::{CommandFile, Section}; 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() { let commands = if section.is_run_all() {
file.sections file.sections
.iter() .iter()
@ -21,14 +22,14 @@ pub fn run_section(section: &Section, file: &CommandFile) {
} }
let script = build_script(&commands); let script = build_script(&commands);
execute_in_terminal(&script); execute_in_terminal(&script, terminal);
} }
fn build_script(commands: &[String]) -> String { fn build_script(commands: &[String]) -> String {
commands.join("\n") 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 tmp_dir = std::env::temp_dir();
let script_path = tmp_dir.join(format!("lines_run_{}.sh", random_suffix())); let script_path = tmp_dir.join(format!("lines_run_{}.sh", random_suffix()));
@ -37,22 +38,28 @@ fn execute_in_terminal(script: &str) {
Err(_) => return, 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; 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 _ = 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!( let applescript = format!(
r#"tell application "Terminal" r#"tell application "Terminal"
do script "clear; bash {}; rm {}" do script "clear; bash {}"
activate activate
end tell"#, end tell"#,
escape_applescript(&path_str),
escape_applescript(&path_str) escape_applescript(&path_str)
); );
@ -62,29 +69,90 @@ end tell"#,
.spawn(); .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 { fn escape_applescript(s: &str) -> String {
s.replace('\\', "\\\\").replace('"', "\\\"") s.replace('\\', "\\\\").replace('"', "\\\"")
} }
fn escape_shell(s: &str) -> String {
s.replace('\\', "\\\\").replace('"', "\\\"").replace('\'', "'\"'\"'")
}
fn random_suffix() -> u64 { fn random_suffix() -> u64 {
std::time::SystemTime::now() std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH) .duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs()) .map(|d| d.as_secs())
.unwrap_or(0) .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();
}

View file

@ -1,13 +1,37 @@
use std::fs;
use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct Config { pub struct Config {
pub data_dir: PathBuf, 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<Self> {
match s.to_lowercase().as_str() {
"terminal" | "terminal.app" => Some(Terminal::Builtin),
"ghostty" => Some(Terminal::Ghostty),
"iterm" | "iterm2" => Some(Terminal::Iterm),
_ => None,
}
}
} }
impl Config { impl Config {
pub fn new(data_dir: Option<PathBuf>) -> Self { pub fn new(data_dir: Option<PathBuf>) -> Self {
let data_dir = data_dir.unwrap_or_else(default_data_dir); 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")) .map(|d| d.home_dir().join(".lines"))
.unwrap_or_else(|| PathBuf::from(".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::<toml::Value>(&content) {
Ok(value) => value
.get("terminal")
.and_then(|v| v.as_str())
.and_then(Terminal::from_str)
.unwrap_or_default(),
Err(_) => Terminal::default(),
}
}

View file

@ -38,12 +38,12 @@ pub fn handle_event(app: &mut App, event: AppEvent) -> bool {
KeyCode::Char('r') => app.reload(), KeyCode::Char('r') => app.reload(),
KeyCode::Char('e') => { KeyCode::Char('e') => {
if let Some(file) = app.current_file() { if let Some(file) = app.current_file() {
edit_file(&file.path); edit_file(&file.path, &app.terminal);
} }
} }
KeyCode::Enter => { KeyCode::Enter => {
if let (Some(section), Some(file)) = (app.current_section(), app.current_file()) { if let (Some(section), Some(file)) = (app.current_section(), app.current_file()) {
run_section(section, file); run_section(section, file, &app.terminal);
} }
} }
_ => {} _ => {}

View file

@ -43,7 +43,7 @@ fn main() -> io::Result<()> {
eprintln!("Nepodařilo se vytvořit datovou složku {}: {}", config.data_dir.display(), e); 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()?; enable_raw_mode()?;
let mut stdout = io::stdout(); let mut stdout = io::stdout();