feat(spaceshd): configurable default shell
Add a config.rs module that loads optional ~/.spacesh/config.toml and a default_shell() resolver: SPACESH_SHELL env -> config.toml default_shell -> login shell from the passwd DB -> $SHELL -> /bin/sh. The passwd lookup matters under launchd where $SHELL is absent. All plain-panel spawn sites in server.rs now use the resolver. Adds toml + libc deps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Generated
+83
@@ -246,6 +246,12 @@ version = "1.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.14"
|
version = "0.3.14"
|
||||||
@@ -382,6 +388,12 @@ dependencies = [
|
|||||||
"wasi",
|
"wasi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.17.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -403,6 +415,16 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "2.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ioctl-rs"
|
name = "ioctl-rs"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
@@ -736,6 +758,15 @@ dependencies = [
|
|||||||
"zmij",
|
"zmij",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "0.6.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serial"
|
name = "serial"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@@ -889,6 +920,7 @@ dependencies = [
|
|||||||
"dirs",
|
"dirs",
|
||||||
"fs2",
|
"fs2",
|
||||||
"futures",
|
"futures",
|
||||||
|
"libc",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"spacesh-core",
|
"spacesh-core",
|
||||||
@@ -897,6 +929,7 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -986,6 +1019,47 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.8.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_edit",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "0.6.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_edit"
|
||||||
|
version = "0.22.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_write",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_write"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
@@ -1200,6 +1274,15 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.7.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winreg"
|
name = "winreg"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
|
|||||||
@@ -26,5 +26,7 @@ portable-pty = "0.8"
|
|||||||
alacritty_terminal = "0.25"
|
alacritty_terminal = "0.25"
|
||||||
fs2 = "0.4"
|
fs2 = "0.4"
|
||||||
dirs = "5"
|
dirs = "5"
|
||||||
|
toml = "0.8"
|
||||||
|
libc = "0.2"
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
clap_complete = "4"
|
clap_complete = "4"
|
||||||
|
|||||||
@@ -22,3 +22,5 @@ thiserror.workspace = true
|
|||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
fs2.workspace = true
|
fs2.workspace = true
|
||||||
dirs.workspace = true
|
dirs.workspace = true
|
||||||
|
toml.workspace = true
|
||||||
|
libc.workspace = true
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
//! Daemon configuration loaded from `~/.spacesh/config.toml`.
|
||||||
|
//!
|
||||||
|
//! The file is optional; every field has a sane fallback so a missing or
|
||||||
|
//! partial config never breaks startup.
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Deserialize)]
|
||||||
|
pub struct Config {
|
||||||
|
/// Shell launched for plain (no-command) panels. When unset, the daemon
|
||||||
|
/// auto-detects the user's login shell.
|
||||||
|
#[serde(default)]
|
||||||
|
pub default_shell: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
/// Load `~/.spacesh/config.toml`. Any error (missing file, bad TOML) yields defaults.
|
||||||
|
pub fn load() -> Self {
|
||||||
|
let Ok(dir) = crate::lifecycle::spacesh_dir() else { return Self::default() };
|
||||||
|
Self::from_path(&dir.join("config.toml"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_path(path: &Path) -> Self {
|
||||||
|
match std::fs::read_to_string(path) {
|
||||||
|
Ok(s) => toml::from_str(&s).unwrap_or_default(),
|
||||||
|
Err(_) => Self::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve the shell to spawn for a plain panel.
|
||||||
|
///
|
||||||
|
/// Order: `SPACESH_SHELL` env → `config.toml` `default_shell` → login shell
|
||||||
|
/// from the passwd DB → `$SHELL` → `/bin/sh`. The passwd lookup matters under
|
||||||
|
/// launchd, where `$SHELL` is typically absent (so a bash fallback would win).
|
||||||
|
pub fn default_shell() -> String {
|
||||||
|
if let Ok(s) = std::env::var("SPACESH_SHELL") {
|
||||||
|
if !s.is_empty() { return s; }
|
||||||
|
}
|
||||||
|
if let Some(s) = Config::load().default_shell {
|
||||||
|
if !s.is_empty() { return s; }
|
||||||
|
}
|
||||||
|
if let Some(s) = login_shell() {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
if let Ok(s) = std::env::var("SHELL") {
|
||||||
|
if !s.is_empty() { return s; }
|
||||||
|
}
|
||||||
|
"/bin/sh".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The current user's login shell from the passwd database (`getpwuid`).
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn login_shell() -> Option<String> {
|
||||||
|
use std::ffi::CStr;
|
||||||
|
// SAFETY: getpwuid returns a pointer into a static buffer valid until the
|
||||||
|
// next libc passwd call; we copy the string out immediately on this thread.
|
||||||
|
unsafe {
|
||||||
|
let pw = libc::getpwuid(libc::getuid());
|
||||||
|
if pw.is_null() { return None; }
|
||||||
|
let shell = (*pw).pw_shell;
|
||||||
|
if shell.is_null() { return None; }
|
||||||
|
let s = CStr::from_ptr(shell).to_str().ok()?;
|
||||||
|
if s.is_empty() { None } else { Some(s.to_string()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
fn login_shell() -> Option<String> { None }
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_file_is_default() {
|
||||||
|
let c = Config::from_path(Path::new("/no/such/spacesh/config.toml"));
|
||||||
|
assert!(c.default_shell.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_default_shell() {
|
||||||
|
let dir = std::env::temp_dir().join("spacesh-cfg-test");
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
let path = dir.join("config.toml");
|
||||||
|
std::fs::write(&path, "default_shell = \"/bin/zsh\"\n").unwrap();
|
||||||
|
let c = Config::from_path(&path);
|
||||||
|
assert_eq!(c.default_shell.as_deref(), Some("/bin/zsh"));
|
||||||
|
let _ = std::fs::remove_file(&path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn env_override_wins() {
|
||||||
|
let _serial = crate::test_support::serial();
|
||||||
|
std::env::set_var("SPACESH_SHELL", "/tmp/fake-shell");
|
||||||
|
let s = default_shell();
|
||||||
|
std::env::remove_var("SPACESH_SHELL");
|
||||||
|
assert_eq!(s, "/tmp/fake-shell");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
mod config;
|
||||||
mod event_log;
|
mod event_log;
|
||||||
mod event_store;
|
mod event_store;
|
||||||
mod hooks;
|
mod hooks;
|
||||||
|
|||||||
@@ -298,7 +298,7 @@ async fn handle_request(
|
|||||||
let _ = out.send(err(id, "NOT_FOUND", "workspace")).await; return;
|
let _ = out.send(err(id, "NOT_FOUND", "workspace")).await; return;
|
||||||
};
|
};
|
||||||
let sid = reg.new_surface_id();
|
let sid = reg.new_surface_id();
|
||||||
let shell = command.clone().unwrap_or_else(|| std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".into()));
|
let shell = command.clone().unwrap_or_else(crate::config::default_shell);
|
||||||
let spec = SurfaceSpec {
|
let spec = SurfaceSpec {
|
||||||
command: shell, args: args.clone(), cwd: ws.path.clone(),
|
command: shell, args: args.clone(), cwd: ws.path.clone(),
|
||||||
agent_label: command, cols, rows, autostart: false,
|
agent_label: command, cols, rows, autostart: false,
|
||||||
@@ -333,7 +333,7 @@ async fn handle_request(
|
|||||||
};
|
};
|
||||||
let ws = reg.workspace(&ws_id).cloned().unwrap();
|
let ws = reg.workspace(&ws_id).cloned().unwrap();
|
||||||
let new_sid = reg.new_surface_id();
|
let new_sid = reg.new_surface_id();
|
||||||
let shell = command.clone().unwrap_or_else(|| std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".into()));
|
let shell = command.clone().unwrap_or_else(crate::config::default_shell);
|
||||||
let spec = SurfaceSpec { command: shell, args, cwd: ws.path.clone(), agent_label: command, cols: 80, rows: 24, autostart: false };
|
let spec = SurfaceSpec { command: shell, args, cwd: ws.path.clone(), agent_label: command, cols: 80, rows: 24, autostart: false };
|
||||||
let (env, hooks_active) = spawn_env(&new_sid, &spec);
|
let (env, hooks_active) = spawn_env(&new_sid, &spec);
|
||||||
match crate::surface::spawn_from_spec(new_sid.clone(), ws_id.clone(), &spec, env, hooks_active, state_tx.clone(), exit_tx.clone()) {
|
match crate::surface::spawn_from_spec(new_sid.clone(), ws_id.clone(), &spec, env, hooks_active, state_tx.clone(), exit_tx.clone()) {
|
||||||
@@ -406,7 +406,7 @@ async fn handle_request(
|
|||||||
let slot = slots.get(i);
|
let slot = slots.get(i);
|
||||||
let new_sid = reg.new_surface_id();
|
let new_sid = reg.new_surface_id();
|
||||||
let command = slot.and_then(|s| s.command.clone());
|
let command = slot.and_then(|s| s.command.clone());
|
||||||
let shell = command.clone().unwrap_or_else(|| std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".into()));
|
let shell = command.clone().unwrap_or_else(crate::config::default_shell);
|
||||||
let args = slot.map(|s| s.args.clone()).unwrap_or_default();
|
let args = slot.map(|s| s.args.clone()).unwrap_or_default();
|
||||||
let spec = SurfaceSpec { command: shell, args, cwd: ws.path.clone(), agent_label: command, cols: 80, rows: 24, autostart: false };
|
let spec = SurfaceSpec { command: shell, args, cwd: ws.path.clone(), agent_label: command, cols: 80, rows: 24, autostart: false };
|
||||||
let (env, hooks_active) = spawn_env(&new_sid, &spec);
|
let (env, hooks_active) = spawn_env(&new_sid, &spec);
|
||||||
|
|||||||
Reference in New Issue
Block a user