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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.zsh eol=lf
30 changes: 23 additions & 7 deletions crates/forge_main/src/zsh/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ use include_dir::{Dir, include_dir};

use crate::cli::Cli;

/// Normalizes CRLF to LF for scripts embedded via `include_str!` on Windows
fn normalize_line_endings(s: &str) -> String {
s.replace("\r\n", "\n")
}

/// Embeds shell plugin files for zsh integration
static ZSH_PLUGIN_LIB: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/../../shell-plugin/lib");

Expand Down Expand Up @@ -51,7 +56,8 @@ pub fn generate_zsh_plugin() -> Result<String> {

/// Generates the ZSH theme for Forge
pub fn generate_zsh_theme() -> Result<String> {
let mut content = include_str!("../../../../shell-plugin/forge.theme.zsh").to_string();
let mut content =
normalize_line_endings(include_str!("../../../../shell-plugin/forge.theme.zsh"));

// Set environment variable to indicate theme is loaded (with timestamp)
content.push_str("\n_FORGE_THEME_LOADED=$(date +%s)\n");
Expand All @@ -71,10 +77,15 @@ pub fn generate_zsh_theme() -> Result<String> {
/// Returns error if the script cannot be executed, if output streaming fails,
/// or if the script exits with a non-zero status code
fn execute_zsh_script_with_streaming(script_content: &str, script_name: &str) -> Result<()> {
// Write script to a temp file to avoid Windows command-line escaping issues
let temp_dir = std::env::temp_dir();
let script_path = temp_dir.join(format!("forge_{}.zsh", script_name));
fs::write(&script_path, script_content)
.context(format!("Failed to write temp script for {}", script_name))?;

// Execute the script in a zsh subprocess with piped output
let mut child = std::process::Command::new("zsh")
.arg("-c")
.arg(script_content)
.arg(script_path.to_str().unwrap())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential panic on non-UTF8 paths: Calling unwrap() on to_str() will panic if the temp path contains non-UTF8 characters. While rare with system temp directories, this could crash the application in certain environments.

Fix by handling the error:

.arg(script_path.to_str()
    .ok_or_else(|| anyhow::anyhow!("Temp path contains invalid UTF-8"))?)

Or use as_os_str() if zsh command accepts OsStr (though Command::arg() already accepts AsRef):

.arg(&script_path)
Suggested change
.arg(script_path.to_str().unwrap())
.arg(&script_path)

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
Expand Down Expand Up @@ -114,6 +125,9 @@ fn execute_zsh_script_with_streaming(script_content: &str, script_name: &str) ->
.wait()
.context(format!("Failed to wait for zsh {} script", script_name))?;

// Clean up temp script file
let _ = fs::remove_file(&script_path);

if !status.success() {
let exit_code = status
.code()
Expand All @@ -135,8 +149,9 @@ fn execute_zsh_script_with_streaming(script_content: &str, script_name: &str) ->
///
/// Returns error if the doctor script cannot be executed
pub fn run_zsh_doctor() -> Result<()> {
let script_content = include_str!("../../../../shell-plugin/doctor.zsh");
execute_zsh_script_with_streaming(script_content, "doctor")
let script_content =
normalize_line_endings(include_str!("../../../../shell-plugin/doctor.zsh"));
execute_zsh_script_with_streaming(&script_content, "doctor")
}

/// Shows ZSH keyboard shortcuts with streaming output
Expand All @@ -145,8 +160,9 @@ pub fn run_zsh_doctor() -> Result<()> {
///
/// Returns error if the keyboard script cannot be executed
pub fn run_zsh_keyboard() -> Result<()> {
let script_content = include_str!("../../../../shell-plugin/keyboard.zsh");
execute_zsh_script_with_streaming(script_content, "keyboard")
let script_content =
normalize_line_endings(include_str!("../../../../shell-plugin/keyboard.zsh"));
execute_zsh_script_with_streaming(&script_content, "keyboard")
}

/// Represents the state of markers in a file
Expand Down
Loading