Add configurable shell (default from /bin/bash) for command execution

This commit is contained in:
SW 2026-06-15 13:04:01 +02:00
parent 1da2553b92
commit d495d23afe
5 changed files with 68 additions and 33 deletions

View file

@ -13,6 +13,7 @@ pub enum Focus {
pub struct App { pub struct App {
pub data_dir: PathBuf, pub data_dir: PathBuf,
pub terminal: Terminal, pub terminal: Terminal,
pub shell: String,
pub groups: Vec<Group>, pub groups: Vec<Group>,
pub group_index: usize, pub group_index: usize,
pub file_index: usize, pub file_index: usize,
@ -22,11 +23,12 @@ pub struct App {
} }
impl App { impl App {
pub fn new(data_dir: PathBuf, terminal: Terminal) -> Self { pub fn new(data_dir: PathBuf, terminal: Terminal, shell: String) -> Self {
let groups = load_groups(&data_dir); let groups = load_groups(&data_dir);
App { App {
data_dir, data_dir,
terminal, terminal,
shell,
groups, groups,
group_index: 0, group_index: 0,
file_index: 0, file_index: 0,

View file

@ -9,7 +9,13 @@ use base64::{engine::general_purpose::STANDARD, Engine};
use crate::config::Terminal; use crate::config::Terminal;
use crate::parser::{CommandFile, Section}; use crate::parser::{CommandFile, Section};
pub fn run_section(section: &Section, file: &CommandFile, terminal: &Terminal, data_dir: &Path) { pub fn run_section(
section: &Section,
file: &CommandFile,
terminal: &Terminal,
shell: &str,
data_dir: &Path,
) {
let commands = if section.is_run_all() { let commands = if section.is_run_all() {
file.sections file.sections
.iter() .iter()
@ -25,12 +31,12 @@ pub fn run_section(section: &Section, file: &CommandFile, terminal: &Terminal, d
} }
match terminal { match terminal {
Terminal::Ghostty => run_in_ghostty(&commands), Terminal::Ghostty => run_in_ghostty(&commands, shell),
_ => run_via_script(&commands, terminal, data_dir), _ => run_via_script(&commands, terminal, shell, data_dir),
} }
} }
fn run_via_script(commands: &[String], terminal: &Terminal, data_dir: &Path) { fn run_via_script(commands: &[String], terminal: &Terminal, shell: &str, data_dir: &Path) {
let script = commands.join("\n"); let script = commands.join("\n");
let run_dir = data_dir.join(".run"); let run_dir = data_dir.join(".run");
if fs::create_dir_all(&run_dir).is_err() { if fs::create_dir_all(&run_dir).is_err() {
@ -57,35 +63,36 @@ fn run_via_script(commands: &[String], terminal: &Terminal, data_dir: &Path) {
let _ = fs::set_permissions(&script_path, fs::Permissions::from_mode(0o755)); let _ = fs::set_permissions(&script_path, fs::Permissions::from_mode(0o755));
match terminal { match terminal {
Terminal::Iterm => run_in_iterm(&script_path), Terminal::Iterm => run_in_iterm(&script_path, shell),
Terminal::Builtin => run_in_terminal_app(&script_path), Terminal::Builtin => run_in_terminal_app(&script_path, shell),
Terminal::Ghostty => unreachable!(), Terminal::Ghostty => unreachable!(),
} }
} }
fn run_in_ghostty(commands: &[String]) { fn run_in_ghostty(commands: &[String], shell: &str) {
let script = commands.join("\n"); let script = commands.join("\n");
let encoded = STANDARD.encode(script); let encoded = STANDARD.encode(script);
let shell_command = format!("echo {} | base64 -D | bash", encoded); let shell_command = format!("echo {} | base64 -D | {}", encoded, shell);
let _ = Command::new("open") let _ = Command::new("open")
.arg("-na") .arg("-na")
.arg("Ghostty") .arg("Ghostty")
.arg("--args") .arg("--args")
.arg("-e") .arg("-e")
.arg("bash") .arg(shell)
.arg("-c") .arg("-c")
.arg(shell_command) .arg(shell_command)
.spawn(); .spawn();
} }
fn run_in_terminal_app(script_path: &Path) { fn run_in_terminal_app(script_path: &Path, shell: &str) {
let path_str = script_path.to_string_lossy(); 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 {}" do script "clear; {} {}"
activate activate
end tell"#, end tell"#,
shell,
escape_applescript(&path_str) escape_applescript(&path_str)
); );
@ -95,16 +102,17 @@ end tell"#,
.spawn(); .spawn();
} }
fn run_in_iterm(script_path: &Path) { fn run_in_iterm(script_path: &Path, shell: &str) {
let path_str = script_path.to_string_lossy(); let path_str = script_path.to_string_lossy();
let applescript = format!( let applescript = format!(
r#"tell application "iTerm" r#"tell application "iTerm"
create window with default profile create window with default profile
tell current session of current window tell current session of current window
write text "clear; bash {}" write text "clear; {} {}"
end tell end tell
activate activate
end tell"#, end tell"#,
shell,
escape_applescript(&path_str) escape_applescript(&path_str)
); );
@ -114,7 +122,7 @@ end tell"#,
.spawn(); .spawn();
} }
pub fn edit_file(path: &Path, terminal: &Terminal) { pub fn edit_file(path: &Path, terminal: &Terminal, shell: &str) {
let editor = std::env::var("EDITOR").unwrap_or_else(|_| "nano".to_string()); let editor = std::env::var("EDITOR").unwrap_or_else(|_| "nano".to_string());
match terminal { match terminal {
@ -124,13 +132,14 @@ pub fn edit_file(path: &Path, terminal: &Terminal) {
.arg("Ghostty") .arg("Ghostty")
.arg("--args") .arg("--args")
.arg("-e") .arg("-e")
.arg("bash") .arg(shell)
.arg("-c") .arg("-c")
.arg(format!( .arg(format!(
"cd {} && {} {}; exec bash", "cd {} && {} {}; exec {}",
escape_shell(&std::env::current_dir().unwrap_or_default().to_string_lossy()), escape_shell(&std::env::current_dir().unwrap_or_default().to_string_lossy()),
escape_shell(&editor), escape_shell(&editor),
escape_shell(&path.to_string_lossy()) escape_shell(&path.to_string_lossy()),
shell
)) ))
.spawn(); .spawn();
} }

View file

@ -1,11 +1,11 @@
use std::fs; use std::fs;
use std::path::Path; use std::path::{Path, PathBuf};
use std::path::PathBuf;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Config { pub struct Config {
pub data_dir: PathBuf, pub data_dir: PathBuf,
pub terminal: Terminal, pub terminal: Terminal,
pub shell: String,
} }
#[derive(Debug, Clone, PartialEq, Eq, Default)] #[derive(Debug, Clone, PartialEq, Eq, Default)]
@ -30,8 +30,12 @@ impl Terminal {
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);
let terminal = load_terminal_preference(&data_dir); let (terminal, shell) = load_preferences(&data_dir);
Config { data_dir, terminal } Config {
data_dir,
terminal,
shell,
}
} }
} }
@ -41,19 +45,39 @@ fn default_data_dir() -> PathBuf {
.unwrap_or_else(|| PathBuf::from(".lines")) .unwrap_or_else(|| PathBuf::from(".lines"))
} }
fn load_terminal_preference(data_dir: &Path) -> Terminal { fn default_shell() -> String {
std::env::var("SHELL")
.ok()
.and_then(|s| {
Path::new(&s)
.file_name()
.and_then(|n| n.to_str())
.map(String::from)
})
.unwrap_or_else(|| "zsh".to_string())
}
fn load_preferences(data_dir: &Path) -> (Terminal, String) {
let config_path = data_dir.join("config.toml"); let config_path = data_dir.join("config.toml");
let content = match fs::read_to_string(&config_path) { let content = match fs::read_to_string(&config_path) {
Ok(c) => c, Ok(c) => c,
Err(_) => return Terminal::default(), Err(_) => return (Terminal::default(), default_shell()),
}; };
match toml::from_str::<toml::Value>(&content) { match toml::from_str::<toml::Value>(&content) {
Ok(value) => value Ok(value) => {
.get("terminal") let terminal = value
.and_then(|v| v.as_str()) .get("terminal")
.and_then(Terminal::from_str) .and_then(|v| v.as_str())
.unwrap_or_default(), .and_then(Terminal::from_str)
Err(_) => Terminal::default(), .unwrap_or_default();
let shell = value
.get("shell")
.and_then(|v| v.as_str())
.map(String::from)
.unwrap_or_else(default_shell);
(terminal, shell)
}
Err(_) => (Terminal::default(), default_shell()),
} }
} }

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, &app.terminal); edit_file(&file.path, &app.terminal, &app.shell);
} }
} }
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, &app.terminal, &app.data_dir); run_section(section, file, &app.terminal, &app.shell, &app.data_dir);
} }
} }
_ => {} _ => {}

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, config.terminal); let app = App::new(config.data_dir, config.terminal, config.shell);
enable_raw_mode()?; enable_raw_mode()?;
let mut stdout = io::stdout(); let mut stdout = io::stdout();