Skip to main content

Mountain/Environment/Terminal/
ShellIntegration.rs

1//! Shell integration injection for the integrated terminal.
2//!
3//! When `LAND_SHELL_INTEGRATION` is not explicitly set to `"0"`, this module
4//! finds the appropriate shell integration script (bash, zsh, or fish) in the
5//! app resource directory and injects it into the shell's startup sequence
6//! so the workbench receives OSC 633 command-tracking sequences.
7//!
8//! ## OSC 633 sequence meanings
9//!
10//! | Code | Meaning              |
11//! |------|----------------------|
12//! | A    | Prompt start         |
13//! | B    | Prompt end           |
14//! | C    | Command start        |
15//! | D;N  | Command end (exit N) |
16//! | P;cwd=<path> | Current working directory |
17//!
18//! ## Injection strategy per shell
19//!
20//! - **bash**: `--init-file <script>` - replaces `.bashrc`; script sources the
21//!   original before setting hooks.
22//! - **zsh**: set `ZDOTDIR` to a temp dir whose `.zshrc` sources the script
23//!   then `LAND_ORIG_ZDOTDIR/.zshrc`; avoids touching `--rcs`.
24//! - **fish**: `--init-command 'source <script>'`
25//! - All others: no injection; integration unavailable for that shell.
26
27use std::path::{Path, PathBuf};
28
29use tauri::{AppHandle, Manager};
30
31use crate::dev_log;
32
33/// Describes how a shell integration script should be injected.
34pub struct Injection {
35	/// Additional environment variables to set before spawning the shell.
36	pub EnvVars:Vec<(String, String)>,
37
38	/// Extra arguments to prepend to the shell's argument list.
39	pub PrependArgs:Vec<String>,
40
41	/// Extra arguments to append to the shell's argument list.
42	pub AppendArgs:Vec<String>,
43}
44
45/// Returns the resource-dir path for a named integration script.
46fn ScriptPath(AppHandle:&AppHandle, Name:&str) -> Option<PathBuf> {
47	let Base = AppHandle.path().resource_dir().ok()?;
48
49	let Candidate = Base.join("scripts/shell-integration").join(Name);
50
51	if Candidate.exists() {
52		Some(Candidate)
53	} else {
54		dev_log!(
55			"terminal",
56			"[ShellIntegration] script not found at {} (bundled .app only)",
57			Candidate.display()
58		);
59
60		None
61	}
62}
63
64/// Returns the shell binary name (lowercase) extracted from a full path.
65fn ShellName(ShellPath:&str) -> &str { Path::new(ShellPath).file_name().and_then(|N| N.to_str()).unwrap_or("") }
66
67/// Computes the `Injection` for `shell_path`, or `None` if the shell is
68/// unsupported or integration is explicitly disabled via
69/// `LAND_SHELL_INTEGRATION=0`.
70pub fn Compute(AppHandle:&AppHandle, ShellPath:&str) -> Option<Injection> {
71	if std::env::var("LAND_SHELL_INTEGRATION").as_deref() == Ok("0") {
72		dev_log!("terminal", "[ShellIntegration] disabled via LAND_SHELL_INTEGRATION=0");
73
74		return None;
75	}
76
77	let Shell = ShellName(ShellPath);
78
79	dev_log!("terminal", "[ShellIntegration] shell={} path={}", Shell, ShellPath);
80
81	match Shell {
82		"bash" => {
83			let Script = ScriptPath(AppHandle, "bash.sh")?;
84
85			dev_log!("terminal", "[ShellIntegration] bash: --init-file {}", Script.display());
86
87			Some(Injection {
88				EnvVars:vec![("VSCODE_SHELL_INTEGRATION".into(), "1".into())],
89				PrependArgs:Vec::new(),
90				AppendArgs:vec!["--init-file".into(), Script.to_string_lossy().into_owned()],
91			})
92		},
93
94		"zsh" => {
95			let Script = ScriptPath(AppHandle, "zsh.zsh")?;
96
97			dev_log!(
98				"terminal",
99				"[ShellIntegration] zsh: ZDOTDIR injection script={}",
100				Script.display()
101			);
102
103			// Create a temporary ZDOTDIR containing a .zshrc that sources our
104			// script. Preserve the user's original ZDOTDIR so the integration
105			// script can re-source their config.
106			let TmpDir = std::env::temp_dir().join(format!("land-zsh-integration-{}", std::process::id()));
107
108			if std::fs::create_dir_all(&TmpDir).is_err() {
109				return None;
110			}
111
112			let OrigZdotDir = std::env::var("ZDOTDIR").unwrap_or_else(|_| std::env::var("HOME").unwrap_or_default());
113
114			// Write a minimal .zshrc that forwards to our integration script.
115			let ZshRcContent = format!(
116				"export LAND_ORIG_ZDOTDIR=\"{}\"\nexport LAND_SHELL_INTEGRATION_ACTIVE=1\nsource \"{}\"\n",
117				OrigZdotDir.replace('"', "\\\""),
118				Script.to_string_lossy().replace('"', "\\\""),
119			);
120
121			let ZshRcPath = TmpDir.join(".zshrc");
122
123			if std::fs::write(&ZshRcPath, ZshRcContent).is_err() {
124				return None;
125			}
126
127			Some(Injection {
128				EnvVars:vec![
129					("ZDOTDIR".into(), TmpDir.to_string_lossy().into_owned()),
130					("VSCODE_SHELL_INTEGRATION".into(), "1".into()),
131				],
132				PrependArgs:Vec::new(),
133				AppendArgs:Vec::new(),
134			})
135		},
136
137		"fish" => {
138			let Script = ScriptPath(AppHandle, "fish.fish")?;
139
140			dev_log!(
141				"terminal",
142				"[ShellIntegration] fish: --init-command source {}",
143				Script.display()
144			);
145
146			Some(Injection {
147				EnvVars:vec![("VSCODE_SHELL_INTEGRATION".into(), "1".into())],
148				PrependArgs:Vec::new(),
149				AppendArgs:vec![
150					"--init-command".into(),
151					format!("source \"{}\"", Script.to_string_lossy().replace('"', "\\\"")),
152				],
153			})
154		},
155
156		Other => {
157			dev_log!("terminal", "[ShellIntegration] unsupported shell '{}' - no injection", Other);
158
159			None
160		},
161	}
162}