File size: 7,259 Bytes
d90101d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | use std::io::{IsTerminal, Read};
use std::panic;
use std::path::PathBuf;
use anyhow::{Context, Result};
use clap::Parser;
use forge_api::ForgeAPI;
use forge_config::ForgeConfig;
use forge_domain::TitleFormat;
use forge_main::{Cli, Sandbox, TitleDisplayExt, TopLevelCommand, UI, tracker};
/// Enables ENABLE_VIRTUAL_TERMINAL_PROCESSING on the stdout console handle.
///
/// The `enable_ansi_support` crate sets VT processing on the `CONOUT$` handle,
/// but console mode flags are **per-handle** on Windows. The `CONOUT$` flag may
/// not propagate to the individual `STD_OUTPUT_HANDLE` handle on all Windows
/// configurations (e.g. older builds, cmd.exe launched in certain ways, or
/// when handles have been duplicated).
///
/// Without VT processing on stdout, ANSI escape codes from forge's markdown
/// renderer (bold, colors, inline code styling) are displayed as raw text
/// like `←[33m` instead of being interpreted as formatting.
///
/// We intentionally do NOT set VT processing on stderr. The `console` crate
/// (used by `indicatif`) uses `GetConsoleMode` to detect VT support and
/// switches between Win32 Console APIs and ANSI escapes accordingly. The
/// Win32 Console API path (`FillConsoleOutputCharacterA` /
/// `SetConsoleCursorPosition`) modifies the screen buffer in-place, which
/// produces clean scrollback when clearing spinner lines. Enabling VT
/// processing on stderr would cause `console` to use ANSI escapes instead,
/// leaving spinner artifacts in the terminal scrollback buffer.
#[cfg(windows)]
fn enable_stdout_vt_processing() {
use windows_sys::Win32::System::Console::{
ENABLE_VIRTUAL_TERMINAL_PROCESSING, GetConsoleMode, GetStdHandle, STD_OUTPUT_HANDLE,
SetConsoleMode,
};
unsafe {
let handle = GetStdHandle(STD_OUTPUT_HANDLE);
let mut mode = 0;
if GetConsoleMode(handle, &mut mode) != 0 {
let _ = SetConsoleMode(handle, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
}
}
}
#[tokio::main]
async fn main() {
if let Err(err) = run().await {
eprintln!("{}", TitleFormat::error(format!("{err}")).display());
if let Some(cause) = err.chain().nth(1) {
eprintln!("{cause}");
}
std::process::exit(1);
}
}
async fn run() -> Result<()> {
// Enable ANSI escape code support on Windows console.
// `enable_ansi_support` sets VT processing on the `CONOUT$` screen buffer
// handle. We additionally set it on `STD_OUTPUT_HANDLE` directly, since
// console mode flags are per-handle and `CONOUT$` may not propagate to
// individual handles on all Windows configurations.
#[cfg(windows)]
{
let _ = enable_ansi_support::enable_ansi_support();
enable_stdout_vt_processing();
}
// Install default rustls crypto provider (ring) before any TLS connections
// This is required for rustls 0.23+ when multiple crypto providers are
// available
let _ = rustls::crypto::ring::default_provider().install_default();
// Set up panic hook for better error display
panic::set_hook(Box::new(|panic_info| {
let message = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
s.clone()
} else {
"Unexpected error occurred".to_string()
};
println!("{}", TitleFormat::error(message.to_string()).display());
tracker::error_blocking(message);
std::process::exit(1);
}));
// Initialize and run the UI
let mut cli = Cli::parse();
// Check if there's piped input, but skip for `forge select` since that
// command uses stdin for its item list.
let is_select = matches!(cli.subcommands, Some(TopLevelCommand::Select(_)));
if !is_select && !std::io::stdin().is_terminal() {
let mut stdin_content = String::new();
std::io::stdin().read_to_string(&mut stdin_content)?;
let trimmed_content = stdin_content.trim();
if !trimmed_content.is_empty() {
cli.piped_input = Some(trimmed_content.to_string());
}
}
// Read and validate configuration at startup so any errors are surfaced
// immediately rather than silently falling back to defaults at runtime.
let config =
ForgeConfig::read().context("Failed to read Forge configuration from .forge.toml")?;
// Handle worktree creation if specified
let cwd: PathBuf = match (&cli.sandbox, &cli.directory) {
(Some(sandbox), Some(cli)) => {
let mut sandbox = Sandbox::new(sandbox).create()?;
sandbox.push(cli);
sandbox
}
(Some(sandbox), _) => Sandbox::new(sandbox).create()?,
(_, Some(cli)) => match cli.canonicalize() {
Ok(cwd) => cwd,
Err(_) => panic!("Invalid path: {}", cli.display()),
},
(_, _) => std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
};
let mut ui = UI::init(cli, config, move |config| {
ForgeAPI::init(cwd.clone(), config)
})?;
ui.run().await;
Ok(())
}
#[cfg(test)]
mod tests {
use forge_main::TopLevelCommand;
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_stdin_detection_logic() {
// This test verifies that the logic for detecting stdin is correct
// We can't easily test the actual stdin reading in a unit test,
// but we can verify the logic flow
// Test that when prompt is provided, it remains independent of piped input
let cli_with_prompt = Cli::parse_from(["forge", "--prompt", "existing prompt"]);
let original_prompt = cli_with_prompt.prompt.clone();
// The prompt should remain as provided
assert_eq!(original_prompt, Some("existing prompt".to_string()));
// Test that when no prompt is provided, piped_input field exists
let cli_no_prompt = Cli::parse_from(["forge"]);
assert_eq!(cli_no_prompt.prompt, None);
assert_eq!(cli_no_prompt.piped_input, None);
}
#[test]
fn test_cli_parsing_with_short_flag() {
// Test that the short flag -p also works correctly
let cli_with_short_prompt = Cli::parse_from(["forge", "-p", "short flag prompt"]);
assert_eq!(
cli_with_short_prompt.prompt,
Some("short flag prompt".to_string())
);
}
#[test]
fn test_cli_parsing_other_flags_work_with_piping() {
// Test that other CLI flags still work when expecting stdin input
let cli_with_flags = Cli::parse_from(["forge", "--verbose"]);
assert_eq!(cli_with_flags.prompt, None);
assert_eq!(cli_with_flags.verbose, true);
}
#[test]
fn test_commit_command_diff_field_initially_none() {
// Test that the diff field in CommitCommandGroup starts as None
let cli = Cli::parse_from(["forge", "commit", "--preview"]);
if let Some(TopLevelCommand::Commit(commit_group)) = cli.subcommands {
assert_eq!(commit_group.preview, true);
assert_eq!(commit_group.diff, None);
} else {
panic!("Expected Commit command");
}
}
}
|