Skip to main content

DevelopmentNodeEnvironment_MicrosoftVSCodeDependency_22NodeVersion_Bundle_Clean_Debug_ElectronProfile_EsbuildCompiler_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	/// Extra arguments to prepend to the shell's argument list.
38	pub PrependArgs:Vec<String>,
39	/// Extra arguments to append to the shell's argument list.
40	pub AppendArgs:Vec<String>,
41}
42
43/// Returns the resource-dir path for a named integration script.
44fn ScriptPath(AppHandle:&AppHandle, Name:&str) -> Option<PathBuf> {
45	let Base = AppHandle.path().resource_dir().ok()?;
46
47	let Candidate = Base.join("scripts/shell-integration").join(Name);
48
49	if Candidate.exists() {
50		Some(Candidate)
51	} else {
52		dev_log!(
53			"terminal",
54			"[ShellIntegration] script not found at {} (bundled .app only)",
55			Candidate.display()
56		);
57		None
58	}
59}
60
61/// Returns the shell binary name (lowercase) extracted from a full path.
62fn ShellName(ShellPath:&str) -> &str { Path::new(ShellPath).file_name().and_then(|N| N.to_str()).unwrap_or("") }
63
64/// Computes the `Injection` for `shell_path`, or `None` if the shell is
65/// unsupported or integration is explicitly disabled via
66/// `LAND_SHELL_INTEGRATION=0`.
67pub fn Compute(AppHandle:&AppHandle, ShellPath:&str) -> Option<Injection> {
68	if std::env::var("LAND_SHELL_INTEGRATION").as_deref() == Ok("0") {
69		dev_log!("terminal", "[ShellIntegration] disabled via LAND_SHELL_INTEGRATION=0");
70		return None;
71	}
72
73	let Shell = ShellName(ShellPath);
74	dev_log!("terminal", "[ShellIntegration] shell={} path={}", Shell, ShellPath);
75
76	match Shell {
77		"bash" => {
78			let Script = ScriptPath(AppHandle, "bash.sh")?;
79			dev_log!("terminal", "[ShellIntegration] bash: --init-file {}", Script.display());
80			Some(Injection {
81				EnvVars:vec![("VSCODE_SHELL_INTEGRATION".into(), "1".into())],
82				PrependArgs:Vec::new(),
83				AppendArgs:vec!["--init-file".into(), Script.to_string_lossy().into_owned()],
84			})
85		},
86
87		"zsh" => {
88			let Script = ScriptPath(AppHandle, "zsh.zsh")?;
89			dev_log!(
90				"terminal",
91				"[ShellIntegration] zsh: ZDOTDIR injection script={}",
92				Script.display()
93			);
94
95			// Create a temporary ZDOTDIR containing a .zshrc that sources our
96			// script. Preserve the user's original ZDOTDIR so the integration
97			// script can re-source their config.
98			let TmpDir = std::env::temp_dir().join(format!("land-zsh-integration-{}", std::process::id()));
99
100			if std::fs::create_dir_all(&TmpDir).is_err() {
101				return None;
102			}
103
104			let OrigZdotDir = std::env::var("ZDOTDIR").unwrap_or_else(|_| std::env::var("HOME").unwrap_or_default());
105
106			// Write a minimal .zshrc that forwards to our integration script.
107			let ZshRcContent = format!(
108				"export LAND_ORIG_ZDOTDIR=\"{}\"\nexport LAND_SHELL_INTEGRATION_ACTIVE=1\nsource \"{}\"\n",
109				OrigZdotDir.replace('"', "\\\""),
110				Script.to_string_lossy().replace('"', "\\\""),
111			);
112
113			let ZshRcPath = TmpDir.join(".zshrc");
114
115			if std::fs::write(&ZshRcPath, ZshRcContent).is_err() {
116				return None;
117			}
118
119			Some(Injection {
120				EnvVars:vec![
121					("ZDOTDIR".into(), TmpDir.to_string_lossy().into_owned()),
122					("VSCODE_SHELL_INTEGRATION".into(), "1".into()),
123				],
124				PrependArgs:Vec::new(),
125				AppendArgs:Vec::new(),
126			})
127		},
128
129		"fish" => {
130			let Script = ScriptPath(AppHandle, "fish.fish")?;
131			dev_log!(
132				"terminal",
133				"[ShellIntegration] fish: --init-command source {}",
134				Script.display()
135			);
136			Some(Injection {
137				EnvVars:vec![("VSCODE_SHELL_INTEGRATION".into(), "1".into())],
138				PrependArgs:Vec::new(),
139				AppendArgs:vec![
140					"--init-command".into(),
141					format!("source \"{}\"", Script.to_string_lossy().replace('"', "\\\"")),
142				],
143			})
144		},
145
146		Other => {
147			dev_log!("terminal", "[ShellIntegration] unsupported shell '{}' - no injection", Other);
148			None
149		},
150	}
151}