diff --git a/assets/shell/bash_integration.sh b/assets/shell/bash_integration.sh new file mode 100644 index 000000000..abf58a158 --- /dev/null +++ b/assets/shell/bash_integration.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# Copyright (c) 2026 AI Venture Holdings LLC +# Licensed under the Business Source License 1.1 +# +# CX Terminal Shell Integration for Bash +# Captures stderr of failed commands for 'cx fix' + +# Only run in interactive shells +if [[ $- != *i* ]]; then + return +fi + +__cx_err_capture() { + local exit_code=$? + + if [ $exit_code -ne 0 ]; then + if [ -s "$HOME/.cx/stderr.tmp" ]; then + # Get the last command from history + local last_cmd=$(history 1 | sed 's/^[ ]*[0-9]*[ ]*//') + + # Don't capture if it was 'cx fix' or similar + if [[ "$last_cmd" =~ ^cx\ fix ]]; then + : > "$HOME/.cx/stderr.tmp" + return + fi + + { + echo "Command: $last_cmd" + echo "Exit Code: $exit_code" + echo "--- Stderr ---" + cat "$HOME/.cx/stderr.tmp" + } > "$HOME/.cx/last_error" + fi + fi + # Clear for next command + : > "$HOME/.cx/stderr.tmp" +} + +# Inject into PROMPT_COMMAND +if [[ ! "$PROMPT_COMMAND" =~ __cx_err_capture ]]; then + if [[ -z "$PROMPT_COMMAND" ]]; then + PROMPT_COMMAND="__cx_err_capture" + else + PROMPT_COMMAND="__cx_err_capture; $PROMPT_COMMAND" + fi +fi + +# Ensure .cx directory exists +mkdir -p "$HOME/.cx" + +# Global stderr capture +# Note: we use 'tee' to ensure the user still sees the error on their screen. +# We redirect stderr (2) through tee which also writes to stderr.tmp +exec 2> >(tee "$HOME/.cx/stderr.tmp" >&2) diff --git a/assets/shell/zsh_integration.sh b/assets/shell/zsh_integration.sh new file mode 100644 index 000000000..28c35a3b2 --- /dev/null +++ b/assets/shell/zsh_integration.sh @@ -0,0 +1,47 @@ +#!/bin/zsh +# Copyright (c) 2026 AI Venture Holdings LLC +# Licensed under the Business Source License 1.1 +# +# CX Terminal Shell Integration for Zsh +# Captures stderr of failed commands for 'cx fix' + +# Only run in interactive shells +if [[ ! -o interactive ]]; then + return +fi + +__cx_err_capture_precmd() { + local exit_code=$? + + if [[ $exit_code -ne 0 ]]; then + if [[ -s "$HOME/.cx/stderr.tmp" ]]; then + # In Zsh, the last command is available via fc + local last_cmd=$(fc -ln -1) + + # Don't capture if it was 'cx fix' or similar + if [[ "$last_cmd" =~ "cx fix" ]]; then + : > "$HOME/.cx/stderr.tmp" + return + fi + + { + echo "Command: ${last_cmd# }" + echo "Exit Code: $exit_code" + echo "--- Stderr ---" + cat "$HOME/.cx/stderr.tmp" + } > "$HOME/.cx/last_error" + fi + fi + # Clear for next command + : > "$HOME/.cx/stderr.tmp" +} + +# Setup hooks +autoload -Uz add-zsh-hook +add-zsh-hook precmd __cx_err_capture_precmd + +# Ensure .cx directory exists +mkdir -p "$HOME/.cx" + +# Global stderr capture +exec 2> >(tee "$HOME/.cx/stderr.tmp" >&2) diff --git a/wezterm/src/cli/shortcuts.rs b/wezterm/src/cli/shortcuts.rs index 96fc3fdc7..3715f9ff0 100644 --- a/wezterm/src/cli/shortcuts.rs +++ b/wezterm/src/cli/shortcuts.rs @@ -14,6 +14,7 @@ You may not use this file except in compliance with the License. use anyhow::Result; use clap::Parser; +use std::io::Write; use super::ask::AskCommand; @@ -76,6 +77,10 @@ pub struct SetupCommand { impl SetupCommand { pub fn run(&self) -> Result<()> { + if self.description.len() == 1 && self.description[0] == "shell" { + return self.setup_shell(); + } + let query = format!("setup {}", self.description.join(" ")); let ask = AskCommand { @@ -89,6 +94,64 @@ impl SetupCommand { ask.run() } + + fn setup_shell(&self) -> Result<()> { + let home = std::env::var("HOME").unwrap_or_default(); + if home.is_empty() { + anyhow::bail!("HOME environment variable not set"); + } + let cx_dir = std::path::Path::new(&home).join(".cx"); + let shell_dir = cx_dir.join("shell"); + std::fs::create_dir_all(&shell_dir)?; + + let bash_script = shell_dir.join("bash_integration.sh"); + let zsh_script = shell_dir.join("zsh_integration.sh"); + + // We embed the script contents here so they are available in the binary + let bash_content = include_str!("../../../assets/shell/bash_integration.sh"); + let zsh_content = include_str!("../../../assets/shell/zsh_integration.sh"); + + std::fs::write(&bash_script, bash_content)?; + std::fs::write(&zsh_script, zsh_content)?; + + // Update .bashrc + self.update_rc_file(&home, ".bashrc", &bash_script)?; + // Update .zshrc + self.update_rc_file(&home, ".zshrc", &zsh_script)?; + + println!("\n✅ CX Terminal shell integration installed!"); + println!("The integration will capture errors and allow 'cx fix' to read them automatically."); + println!("\nTo activate now, run:"); + println!(" source ~/.bashrc # if using Bash"); + println!(" source ~/.zshrc # if using Zsh"); + + Ok(()) + } + + fn update_rc_file(&self, home: &str, rc_name: &str, script_path: &std::path::Path) -> Result<()> { + let rc_path = std::path::Path::new(home).join(rc_name); + if !rc_path.exists() { + return Ok(()); // Skip if shell is not present + } + + let content = std::fs::read_to_string(&rc_path)?; + let source_line = format!("source ~/.cx/shell/{}", script_path.file_name().unwrap().to_str().unwrap()); + + if content.contains(&source_line) { + println!(" ℹ️ Shell integration already present in {}", rc_name); + return Ok(()); + } + + let mut file = std::fs::OpenOptions::new() + .append(true) + .open(&rc_path)?; + + writeln!(file, "\n# CX Terminal Shell Integration")?; + writeln!(file, "{}", source_line)?; + println!(" ✅ Added integration to {}", rc_name); + + Ok(()) + } } /// Ask questions about the system