Add configurable terminal (Ghostty, iTerm, Terminal) via config.toml
This commit is contained in:
parent
d4150757ca
commit
b167d2c980
7 changed files with 198 additions and 30 deletions
55
Cargo.lock
generated
55
Cargo.lock
generated
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
118
src/commands.rs
118
src/commands.rs
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue