Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src-tauri/crates/infra/scripts/default_init_fish.fish
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# 2code common init (fish)
# The hook/wrapper files are written by 2code at session start via Rust.
# This script only exports env vars and updates PATH for the running shell.
set -gx _2CODE_HOME "$HOME/.2code"
set -gx _2CODE_BIN "$_2CODE_HOME/bin"
set -gx _2CODE_HOOKS "$_2CODE_HOME/hooks"
set -gx _2CODE_NOTIFY "$_2CODE_HOOKS/notify.sh"
set -gx _2CODE_SETTINGS "$_2CODE_HOOKS/claude-settings.json"

fish_add_path -gP $_2CODE_BIN
20 changes: 20 additions & 0 deletions src-tauri/crates/infra/scripts/default_init_pwsh.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# 2code common init (PowerShell)
# The hook/wrapper files are written by 2code at session start via Rust.
# This script only exports env vars and updates PATH for the running shell.
$env:_2CODE_HOME = "$HOME\.2code"
$env:_2CODE_BIN = "$env:_2CODE_HOME\bin"
$env:_2CODE_HOOKS = "$env:_2CODE_HOME\hooks"
$env:_2CODE_NOTIFY = "$env:_2CODE_HOOKS\notify.sh"
$env:_2CODE_SETTINGS = "$env:_2CODE_HOOKS\claude-settings.json"

$pathSep = [System.IO.Path]::PathSeparator
$alreadyOnPath = $false
foreach ($entry in ($env:PATH -split [regex]::Escape($pathSep))) {
if ([string]::Equals($entry, $env:_2CODE_BIN, [System.StringComparison]::OrdinalIgnoreCase)) {
$alreadyOnPath = $true
break
}
}
if (-not $alreadyOnPath) {
$env:PATH = "$env:_2CODE_BIN$pathSep$env:PATH"
}
7 changes: 5 additions & 2 deletions src-tauri/crates/infra/scripts/shellIntegration-bash.sh
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,11 @@ fi
if [ -z "${VSCODE_PYTHON_AUTOACTIVATE_GUARD:-}" ]; then
export VSCODE_PYTHON_AUTOACTIVATE_GUARD=1
if [ -n "${VSCODE_PYTHON_BASH_ACTIVATE:-}" ] && [ "$TERM_PROGRAM" = "vscode" ]; then
# Prevent crashing by negating exit code
if ! builtin eval "$VSCODE_PYTHON_BASH_ACTIVATE"; then
# Capture the eval's real exit status (the previous `if ! ...; then $?` form
# captured the negated pipeline's status, which is always 0 on failure).
if builtin eval "$VSCODE_PYTHON_BASH_ACTIVATE"; then
:
else
__vsc_activation_status=$?
builtin printf '\x1b[0m\x1b[7m * \x1b[0;103m VS Code Python bash activation failed with exit code %d \x1b[0m' "$__vsc_activation_status"
fi
Expand Down
26 changes: 26 additions & 0 deletions src-tauri/crates/infra/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,32 @@ pub fn init_db(app_data_dir: &std::path::Path) -> Result<DbPool, String> {
{
tracing::warn!("Failed to set foreign_keys=ON: {e}");
}
// Performance: synchronous=NORMAL is the recommended companion for WAL mode.
// FULL (default) causes excessive fsync calls, especially slow on Windows NTFS.
if let Err(e) =
diesel::sql_query("PRAGMA synchronous=NORMAL;").execute(&mut conn)
{
tracing::warn!("Failed to set synchronous=NORMAL: {e}");
}
// Allow SQLite to wait up to 5s when the database is locked by another
// connection, instead of failing immediately with SQLITE_BUSY.
if let Err(e) =
diesel::sql_query("PRAGMA busy_timeout=5000;").execute(&mut conn)
{
tracing::warn!("Failed to set busy_timeout=5000: {e}");
}
// 64 MB page cache — the default 2 MB is too small for PTY output BLOB workloads.
if let Err(e) =
diesel::sql_query("PRAGMA cache_size=-65536;").execute(&mut conn)
{
tracing::warn!("Failed to set cache_size: {e}");
}
// Keep temp tables in memory instead of writing to disk.
if let Err(e) =
diesel::sql_query("PRAGMA temp_store=MEMORY;").execute(&mut conn)
{
tracing::warn!("Failed to set temp_store=MEMORY: {e}");
}

conn.run_pending_migrations(MIGRATIONS)
.map_err(|e| format!("Failed to run migrations: {e}"))?;
Expand Down
1 change: 1 addition & 0 deletions src-tauri/crates/infra/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod git;
pub mod logger;
pub mod no_window;
pub mod pty;
pub mod shell_detect;
pub mod shell_init;
pub mod slug;
pub mod watcher;
8 changes: 8 additions & 0 deletions src-tauri/crates/infra/src/no_window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ use std::process::Command;
///
/// On non-Windows platforms this is identical to `Command::new`.
pub fn command_without_windows_console(program: &str) -> Command {
// Alias for shell_detect.rs
silent_command(program)
}

/// Create a `Command` that won't open a console window on Windows.
///
/// On non-Windows platforms this is identical to `Command::new`.
pub fn silent_command(program: &str) -> Command {
#[cfg(target_os = "windows")]
{
windows_no_window_command(program)
Expand Down
104 changes: 82 additions & 22 deletions src-tauri/crates/infra/src/pty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ pub fn create_session(
// Common env vars for all shells
cmd.env("TERM", "xterm-256color");
cmd.env("TERM_PROGRAM", "vscode"); // Makes VS Code's shell integration scripts work
cmd.env("VSCODE_INJECTION", "1"); // Tells scripts they were injected (not manually installed)
cmd.env("VSCODE_INJECTION", "1"); // Tells scripts they were injected (not manually installed)

// Inject helper env vars for CLI sidecar communication
if let Some(url) = options.helper_url {
Expand Down Expand Up @@ -134,21 +134,46 @@ pub fn create_session(
}

/// Parse a shell command string into (program, args), handling paths with spaces.
/// e.g. `"C:\\Program Files\\PowerShell\\7-preview\\pwsh.exe -NoLogo -NoProfile"`
/// → `("C:\\Program Files\\PowerShell\\7-preview\\pwsh.exe", ["-NoLogo", "-NoProfile"])`
///
/// Accepts three shapes:
/// 1. Quoted program: `"C:\Program Files\PowerShell\7\pwsh.exe" -NoLogo`
/// → program is the quoted span (quotes stripped), args split by whitespace.
/// 2. Bare path with spaces ending in a known extension or matching an existing
/// file: `C:\Program Files\PowerShell\7\pwsh.exe -NoLogo` → joined back into
/// a single program via the extension/exists fallback below.
/// 3. Simple `program [args...]`.
fn parse_shell_command(shell: &str) -> (String, Vec<String>) {
let shell = shell.trim();
if shell.is_empty() {
return (String::new(), vec![]);
}

// Shape 1: quoted program. Preserves spaces and escaping intent the user
// expressed explicitly — no heuristic guesswork needed.
let first_char = shell.as_bytes()[0];
if first_char == b'"' || first_char == b'\'' {
let quote = first_char as char;
if let Some(end) = shell[1..].find(quote) {
let program = shell[1..=end].to_string();
let rest = shell[end + 2..].trim();
let args = rest.split_whitespace().map(|s| s.to_string()).collect();
return (program, args);
}
}

let parts: Vec<&str> = shell.split_whitespace().collect();
if parts.is_empty() {
return (shell.to_string(), vec![]);
}
let first = parts[0];
let looks_like_path =
first.len() >= 2 && first.as_bytes()[1] == b':' // C: D: etc
let looks_like_path = first.len() >= 2 && first.as_bytes()[1] == b':' // C: D: etc
|| first.contains('/') || first.contains('\\');

if !looks_like_path || parts.len() == 1 {
return (first.to_string(), parts[1..].iter().map(|s| s.to_string()).collect());
return (
first.to_string(),
parts[1..].iter().map(|s| s.to_string()).collect(),
);
}

// Reconstruct the path by joining tokens until we hit one ending with a
Expand All @@ -167,34 +192,50 @@ fn parse_shell_command(shell: &str) -> (String, Vec<String>) {
break;
}
end_idx += 1;
if end_idx > 6 { break; }
if end_idx > 6 {
break;
}
}
if end_idx > parts.len() {
end_idx = parts.iter().position(|p| p.starts_with('-')).unwrap_or(parts.len());
if end_idx == 0 { end_idx = 1; }
end_idx = parts
.iter()
.position(|p| p.starts_with('-'))
.unwrap_or(parts.len());
if end_idx == 0 {
end_idx = 1;
}
}
let program = parts[..end_idx].join(" ");
let args = parts[end_idx..].iter().map(|s| s.to_string()).collect();
(program, args)
}

/// Build a CommandBuilder with the right executable and args for the given injection type.
fn build_injected_command(shell: &str, injection: &ShellInjection) -> CommandBuilder {
fn build_injected_command(
shell: &str,
injection: &ShellInjection,
) -> CommandBuilder {
// Parse the shell command, handling paths with spaces like
// "C:\Program Files\PowerShell\7-preview\pwsh.exe -NoLogo -NoProfile"
let (program, existing_args) = parse_shell_command(shell);

match injection {
ShellInjection::Bash { init_file } => {
// bash --init-file /path/to/shellIntegration-bash.sh
// bash [user args] --init-file /path/to/shellIntegration-bash.sh
let mut cmd = CommandBuilder::new(program);
for arg in &existing_args {
cmd.arg(arg.as_str());
}
cmd.arg("--init-file");
cmd.arg(init_file.to_string_lossy().as_ref());
cmd
}
ShellInjection::Zsh { .. } => {
// zsh -i (ZDOTDIR is set via env var, scripts are in the dir)
// zsh [user args] -i (ZDOTDIR is set via env var, scripts are in the dir)
let mut cmd = CommandBuilder::new(program);
for arg in &existing_args {
cmd.arg(arg.as_str());
}
cmd.arg("-i");
cmd
}
Expand All @@ -205,10 +246,7 @@ fn build_injected_command(shell: &str, injection: &ShellInjection) -> CommandBui
cmd.arg(arg.as_str());
}
cmd.arg("--init-command");
cmd.arg(format!(
"source \"{}\"",
init_script.to_string_lossy()
));
cmd.arg(format!("source \"{}\"", init_script.to_string_lossy()));
cmd
}
ShellInjection::Pwsh { init_script } => {
Expand All @@ -223,10 +261,7 @@ fn build_injected_command(shell: &str, injection: &ShellInjection) -> CommandBui
}
cmd.arg("-noexit");
cmd.arg("-command");
cmd.arg(format!(
". \"{}\"",
init_script.to_string_lossy()
));
cmd.arg(format!(". \"{}\"", init_script.to_string_lossy()));
cmd
}
ShellInjection::None => {
Expand Down Expand Up @@ -384,7 +419,8 @@ mod tests {

#[test]
fn parse_shell_with_args() {
let (prog, args) = parse_shell_command("powershell.exe -NoLogo -NoProfile");
let (prog, args) =
parse_shell_command("powershell.exe -NoLogo -NoProfile");
assert_eq!(prog, "powershell.exe");
assert_eq!(args, vec!["-NoLogo".to_string(), "-NoProfile".to_string()]);
}
Expand All @@ -400,8 +436,32 @@ mod tests {

#[test]
fn parse_shell_git_bash() {
let (prog, args) = parse_shell_command(r"C:\Program Files\Git\bin\bash.exe");
let (prog, args) =
parse_shell_command(r"C:\Program Files\Git\bin\bash.exe");
assert_eq!(prog, r"C:\Program Files\Git\bin\bash.exe");
assert!(args.is_empty());
}

#[test]
fn parse_shell_quoted_path() {
let (prog, args) = parse_shell_command(
r#""C:\Program Files\PowerShell\7\pwsh.exe" -NoLogo -NoProfile"#,
);
assert_eq!(prog, r"C:\Program Files\PowerShell\7\pwsh.exe");
assert_eq!(args, vec!["-NoLogo".to_string(), "-NoProfile".to_string()]);
}

#[test]
fn parse_shell_quoted_path_no_args() {
let (prog, args) = parse_shell_command(r#""/usr/local/bin/my shell""#);
assert_eq!(prog, "/usr/local/bin/my shell");
assert!(args.is_empty());
}

#[test]
fn parse_shell_empty() {
let (prog, args) = parse_shell_command("");
assert!(prog.is_empty());
assert!(args.is_empty());
}
}
Loading
Loading