Skip to content

Commit 3a7d020

Browse files
committed
fix: stop embedded shell selection from crashing Taskers
Recent desktop launches were aborting inside the Ghostty bridge during GTK snapshot, and the freshest coredumps pointed at the embedded terminal host path. The crash lined up with the shell chooser work: Taskers was forwarding shell-specific argv through Ghostty host options again. This change keeps the embedded Ghostty command stable at the wrapper entrypoint, moves bash/fish/zsh argv synthesis back into the wrapper, and exports shell profile state for fish/zsh so shell choice and clean/default modes still survive relaunch. Constraint: Embedded Ghostty host launch must avoid brittle shell-specific command overrides while still preserving user shell choice and profile Rejected: Keep forwarding shell-specific argv through Ghostty | recent coredumps showed that path aborting in the bridge during GTK snapshot Rejected: Drop shell/profile preservation entirely | that would fix the crash at the cost of regressing configured shell behavior Confidence: medium Scope-risk: moderate Reversibility: clean Directive: Keep embedded Ghostty on the env-driven wrapper contract; do not reintroduce per-shell argv overrides through host options without proving bridge stability first Tested: cargo test -p taskers-runtime --lib shell_wrapper Tested: cargo test -p taskers-runtime --lib fish_and_zsh_launch_specs_preserve_shell_profile_env Tested: cargo test -p taskers-ghostty --lib host_options Tested: cargo test -p taskers runtime_bootstrap_tests --bin taskers-gtk Tested: TASKERS_CONFIG_PATH=<temp fish config> timeout 20s taskers-gtk --diagnostic-log <temp log> --smoke-script baseline --quit-after-ms 1000 Tested: cargo install --path crates/taskers-app --force Not-tested: Manual desktop launch through the original vicinae launcher path Related: coredumpctl taskers-gtk PIDs 2021751 and 2029634 on 2026-04-14
1 parent ff528bd commit 3a7d020

3 files changed

Lines changed: 145 additions & 13 deletions

File tree

crates/taskers-ghostty/src/backend.rs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,18 @@ impl GhosttyHostOptions {
6868
pub fn from_shell_launch(shell_launch: &ShellLaunchSpec) -> Self {
6969
let mut env = BTreeMap::new();
7070
env.extend(shell_launch.env.clone());
71+
let command_argv = if shell_launch
72+
.program
73+
.file_name()
74+
.and_then(|value| value.to_str())
75+
== Some("taskers-shell-wrapper.sh")
76+
{
77+
vec![shell_launch.program.display().to_string()]
78+
} else {
79+
shell_launch.program_and_args()
80+
};
7181
Self {
72-
command_argv: shell_launch.program_and_args(),
82+
command_argv,
7383
env,
7484
embedded_terminal_appearance: EmbeddedTerminalAppearance::Taskers,
7585
}
@@ -266,6 +276,33 @@ mod tests {
266276
);
267277
}
268278

279+
#[test]
280+
fn host_options_collapse_wrapper_shell_args_for_embedded_ghostty() {
281+
let mut env = BTreeMap::new();
282+
env.insert("TASKERS_REAL_SHELL".into(), "/usr/bin/fish".into());
283+
env.insert("TASKERS_SHELL_PROFILE".into(), "default".into());
284+
let shell_launch = ShellLaunchSpec {
285+
program: PathBuf::from("/tmp/taskers-runtime/taskers-shell-wrapper.sh"),
286+
args: vec![
287+
"--interactive".into(),
288+
"--init-command".into(),
289+
r#"source "$TASKERS_SHELL_INTEGRATION_DIR/taskers-hooks.fish""#.into(),
290+
],
291+
env,
292+
};
293+
294+
let options = GhosttyHostOptions::from_shell_launch(&shell_launch);
295+
296+
assert_eq!(
297+
options.command_argv,
298+
vec!["/tmp/taskers-runtime/taskers-shell-wrapper.sh"]
299+
);
300+
assert_eq!(
301+
options.env.get("TASKERS_REAL_SHELL").map(String::as_str),
302+
Some("/usr/bin/fish")
303+
);
304+
}
305+
269306
#[test]
270307
fn host_options_allow_overriding_embedded_terminal_appearance() {
271308
let options = GhosttyHostOptions::default()

crates/taskers-runtime/assets/shell/taskers-shell-wrapper.sh

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
55
REAL_SHELL=${TASKERS_REAL_SHELL:-${SHELL:-/bin/sh}}
66
SHELL_NAME=${REAL_SHELL##*/}
77
SHELL_NAME=${SHELL_NAME#-}
8+
SHELL_PROFILE=${TASKERS_SHELL_PROFILE:-default}
89
export TASKERS_EMBEDDED=1
910
export TERM_PROGRAM=taskers
1011
current_tty=$(tty 2>/dev/null || true)
@@ -37,22 +38,43 @@ elif [ -n "${TASKERS_TERMINAL_SOCKET:-}" ] && [ -n "${TASKERS_TERMINAL_SESSION_I
3738
fi
3839

3940
if [ "${TASKERS_DISABLE_SHELL_INTEGRATION:-0}" = "1" ]; then
41+
if [ "$#" -eq 0 ]; then
42+
case "$SHELL_NAME" in
43+
bash)
44+
set -- --noprofile --norc -i
45+
;;
46+
fish)
47+
set -- --no-config --interactive
48+
;;
49+
zsh)
50+
set -- -d -f -i
51+
;;
52+
esac
53+
fi
54+
exec "$REAL_SHELL" "$@"
55+
fi
56+
57+
if [ "$#" -eq 0 ]; then
4058
case "$SHELL_NAME" in
4159
bash)
42-
exec "$REAL_SHELL" --noprofile --norc -i "$@"
60+
export TASKERS_USER_BASHRC="${TASKERS_USER_BASHRC:-$HOME/.bashrc}"
61+
set -- --rcfile "$SCRIPT_DIR/bash/taskers.bashrc" -i
4362
;;
44-
*)
45-
exec "$REAL_SHELL" "$@"
63+
fish)
64+
if [ "$SHELL_PROFILE" = "clean" ]; then
65+
set -- --no-config --interactive
66+
else
67+
set -- --interactive --init-command "source \"$TASKERS_SHELL_INTEGRATION_DIR/taskers-hooks.fish\""
68+
fi
69+
;;
70+
zsh)
71+
if [ "$SHELL_PROFILE" = "clean" ]; then
72+
set -- -d -i
73+
else
74+
set -- -i
75+
fi
4676
;;
4777
esac
4878
fi
4979

50-
case "$SHELL_NAME" in
51-
bash)
52-
export TASKERS_USER_BASHRC="${TASKERS_USER_BASHRC:-$HOME/.bashrc}"
53-
exec "$REAL_SHELL" --rcfile "$SCRIPT_DIR/bash/taskers.bashrc" -i "$@"
54-
;;
55-
*)
56-
exec "$REAL_SHELL" "$@"
57-
;;
58-
esac
80+
exec "$REAL_SHELL" "$@"

crates/taskers-runtime/src/shell.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ impl ShellIntegration {
127127
"TASKERS_REAL_SHELL".into(),
128128
self.real_shell.display().to_string(),
129129
);
130+
env.insert("TASKERS_SHELL_PROFILE".into(), profile.clone());
130131

131132
let mut args = Vec::new();
132133
if profile == "clean" {
@@ -153,6 +154,7 @@ impl ShellIntegration {
153154
"TASKERS_REAL_SHELL".into(),
154155
self.real_shell.display().to_string(),
155156
);
157+
env.insert("TASKERS_SHELL_PROFILE".into(), profile.clone());
156158
env.insert(
157159
"ZDOTDIR".into(),
158160
zsh_runtime_dir(&self.root).display().to_string(),
@@ -759,6 +761,77 @@ mod tests {
759761
);
760762
}
761763

764+
#[test]
765+
fn shell_wrapper_handles_fish_and_zsh_default_launch_modes() {
766+
let wrapper = include_str!(concat!(
767+
env!("CARGO_MANIFEST_DIR"),
768+
"/assets/shell/taskers-shell-wrapper.sh"
769+
));
770+
assert!(
771+
wrapper.contains("SHELL_PROFILE=${TASKERS_SHELL_PROFILE:-default}"),
772+
"expected wrapper to honor TASKERS_SHELL_PROFILE when synthesizing default shell args"
773+
);
774+
assert!(
775+
wrapper.contains("--init-command"),
776+
"expected wrapper to synthesize fish init-command integration when no explicit args are passed"
777+
);
778+
assert!(
779+
wrapper.contains("set -- -d -i"),
780+
"expected wrapper to synthesize zsh default launch flags when no explicit args are passed"
781+
);
782+
}
783+
784+
#[test]
785+
fn fish_and_zsh_launch_specs_preserve_shell_profile_env() {
786+
let _guard = ENV_LOCK.lock().unwrap_or_else(|poison| poison.into_inner());
787+
let original_profile = std::env::var_os("TASKERS_SHELL_PROFILE");
788+
let original_disabled = std::env::var_os("TASKERS_DISABLE_SHELL_INTEGRATION");
789+
unsafe {
790+
std::env::set_var("TASKERS_SHELL_PROFILE", "clean");
791+
std::env::remove_var("TASKERS_DISABLE_SHELL_INTEGRATION");
792+
}
793+
794+
let fish_integration = ShellIntegration {
795+
root: PathBuf::from("/tmp/taskers-runtime"),
796+
wrapper_path: PathBuf::from("/tmp/taskers-runtime/taskers-shell-wrapper.sh"),
797+
real_shell: PathBuf::from("/usr/bin/fish"),
798+
};
799+
let zsh_integration = ShellIntegration {
800+
root: PathBuf::from("/tmp/taskers-runtime"),
801+
wrapper_path: PathBuf::from("/tmp/taskers-runtime/taskers-shell-wrapper.sh"),
802+
real_shell: PathBuf::from("/usr/bin/zsh"),
803+
};
804+
805+
let fish_spec = fish_integration.launch_spec();
806+
let zsh_spec = zsh_integration.launch_spec();
807+
808+
assert_eq!(
809+
fish_spec
810+
.env
811+
.get("TASKERS_SHELL_PROFILE")
812+
.map(String::as_str),
813+
Some("clean")
814+
);
815+
assert_eq!(
816+
zsh_spec
817+
.env
818+
.get("TASKERS_SHELL_PROFILE")
819+
.map(String::as_str),
820+
Some("clean")
821+
);
822+
823+
unsafe {
824+
match original_profile {
825+
Some(value) => std::env::set_var("TASKERS_SHELL_PROFILE", value),
826+
None => std::env::remove_var("TASKERS_SHELL_PROFILE"),
827+
}
828+
match original_disabled {
829+
Some(value) => std::env::set_var("TASKERS_DISABLE_SHELL_INTEGRATION", value),
830+
None => std::env::remove_var("TASKERS_DISABLE_SHELL_INTEGRATION"),
831+
}
832+
}
833+
}
834+
762835
#[test]
763836
fn shell_hooks_and_proxy_require_surface_tty_identity() {
764837
let bash_hooks = include_str!(concat!(

0 commit comments

Comments
 (0)