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

View file

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

View file

@ -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<Group>,
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,

View file

@ -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();
}

View file

@ -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<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 {
pub fn new(data_dir: Option<PathBuf>) -> 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::<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('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);
}
}
_ => {}

View file

@ -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();