Skip to content

Commit 85c53f2

Browse files
authored
feat: support color on task run (#75)
### TL;DR Add automatic color support detection for subprocess output by adding the `supports-color` crate and setting the `FORCE_COLOR` environment variable. ### What changed? - Added the `supports-color` crate as a dependency - Added automatic detection of terminal color support capabilities - Set the `FORCE_COLOR` environment variable for subprocesses based on detected color support level: - `3` for true color (16 million colors) - `2` for 256 colors - `1` for basic ANSI colors - `0` for no color support - Added `FORCE_COLOR` to the default passthrough environment variables - Added tests to verify the color detection and environment variable handling ### How to test? 1. Run a task that outputs colored text (like a test runner or linter) 2. Verify that the subprocess correctly displays colored output 3. Test with different terminal capabilities to ensure the correct color level is detected ### Why make this change? Many CLI tools support colored output but disable it when running in a subprocess or CI environment. By automatically detecting color support and setting the `FORCE_COLOR` environment variable, we ensure that subprocess output maintains proper coloring, improving readability and user experience.
1 parent 085f66e commit 85c53f2

4 files changed

Lines changed: 106 additions & 6 deletions

File tree

Cargo.lock

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,18 +45,18 @@ dashmap = "6.1.0"
4545
diff-struct = "0.5.3"
4646
directories = "6.0.0"
4747
edit = "0.1.5"
48-
fspy_shared = { path = "crates/fspy_shared" }
4948
fspy = { path = "crates/fspy" }
50-
fspy_shared_unix = { path = "crates/fspy_shared_unix"}
51-
fspy_seccomp_unotify = { path = "crates/fspy_seccomp_unotify" }
5249
fspy_preload_unix = { path = "crates/fspy_preload_unix", artifact = "cdylib" }
5350
fspy_preload_windows = { path = "crates/fspy_preload_windows", artifact = "cdylib" }
54-
passfd = { git = "https://github.com/polachok/passfd", rev = "d55881752c16aced1a49a75f9c428d38d3767213", default-features = false }
51+
fspy_seccomp_unotify = { path = "crates/fspy_seccomp_unotify" }
52+
fspy_shared = { path = "crates/fspy_shared" }
53+
fspy_shared_unix = { path = "crates/fspy_shared_unix" }
5554
futures = "0.3.31"
5655
futures-core = "0.3.31"
5756
futures-util = "0.3.31"
5857
itertools = "0.14.0"
5958
nix = { version = "0.30.1", features = ["dir"] }
59+
passfd = { git = "https://github.com/polachok/passfd", rev = "d55881752c16aced1a49a75f9c428d38d3767213", default-features = false }
6060
petgraph = "0.8.2"
6161
portable-pty = "0.9.0"
6262
ratatui = "0.29.0"
@@ -69,6 +69,7 @@ serde = "1.0.219"
6969
serde_json = "1.0.140"
7070
serde_yml = "0.0.12"
7171
shell-escape = "0.1.5"
72+
supports-color = "3.0.1"
7273
tempfile = "3.14.0"
7374
thiserror = "2"
7475
tokio = "1.46.1"
@@ -80,9 +81,9 @@ tui-term = "0.2.0"
8081
twox-hash = "2.1.1"
8182
vite_error = { path = "crates/vite_error" }
8283
vite_package_manager = { path = "crates/vite_package_manager" }
83-
vite_task = { path = "crates/vite_task" }
84-
vite_str = { path = "crates/vite_str" }
8584
vite_path = { path = "crates/vite_path" }
85+
vite_str = { path = "crates/vite_str" }
86+
vite_task = { path = "crates/vite_task" }
8687
wax = "0.6.0"
8788
wildmatch = "2.4.0"
8889

crates/vite_task/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ rusqlite = { workspace = true, features = ["bundled"] }
3636
serde = { workspace = true, features = ["derive", "rc"] }
3737
serde_json = { workspace = true }
3838
shell-escape = { workspace = true }
39+
supports-color = { workspace = true }
3940
tokio = { workspace = true, features = ["rt-multi-thread", "io-std", "macros"] }
4041
tracing = { workspace = true }
4142
tracing-subscriber = { workspace = true }

crates/vite_task/src/execute.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use std::{
1111
use anyhow::Context;
1212
use bincode::{Decode, Encode};
1313
use fspy::{AccessMode, Spy, TrackedChild};
14+
use supports_color::{Stream, on};
1415

1516
use futures_util::future::try_join4;
1617
use serde::{Deserialize, Serialize};
@@ -159,6 +160,7 @@ fn is_default_passthrough_env(name: &str) -> bool {
159160
"TERM",
160161
"TERM_PROGRAM",
161162
"DISPLAY",
163+
"FORCE_COLOR",
162164
// Temporary directories
163165
"TMP",
164166
"TEMP",
@@ -245,6 +247,27 @@ impl TaskEnvs {
245247
)?
246248
.into();
247249

250+
// Automatically add FORCE_COLOR environment variable if not already set
251+
// This enables color output in subprocesses when color is supported
252+
// TODO: will remove this temporarily until we have a better solution
253+
if !all_envs.contains_key("FORCE_COLOR") {
254+
if let Some(support) = on(Stream::Stdout) {
255+
let force_color_value = if support.has_16m {
256+
"3" // True color (16 million colors)
257+
} else if support.has_256 {
258+
"2" // 256 colors
259+
} else if support.has_basic {
260+
"1" // Basic ANSI colors
261+
} else {
262+
"0" // No color support
263+
};
264+
all_envs.insert(
265+
"FORCE_COLOR".into(),
266+
Arc::<OsStr>::from(OsStr::new(force_color_value)),
267+
);
268+
}
269+
}
270+
248271
Ok(Self { all_envs, envs_without_pass_through })
249272
}
250273
}
@@ -436,6 +459,9 @@ mod tests {
436459
assert!(!is_default_passthrough_env("RANDOM_ENV"));
437460
assert!(!is_default_passthrough_env("MY_SECRET"));
438461

462+
// Test FORCE_COLOR is a passthrough env
463+
assert!(is_default_passthrough_env("FORCE_COLOR"));
464+
439465
// Test edge cases
440466
assert!(!is_default_passthrough_env("VSCODE")); // Should not match without underscore
441467
assert!(!is_default_passthrough_env("DOCKER")); // Should not match without underscore
@@ -510,4 +536,60 @@ mod tests {
510536
std::env::remove_var("BETA_VAR");
511537
}
512538
}
539+
540+
#[test]
541+
fn test_force_color_auto_detection() {
542+
use crate::collections::HashSet;
543+
use crate::config::{ResolvedTaskConfig, TaskCommand, TaskConfig};
544+
use std::path::Path;
545+
546+
let task_config = TaskConfig {
547+
command: TaskCommand::ShellScript("echo test".into()),
548+
cwd: ".".into(),
549+
cacheable: true,
550+
inputs: HashSet::new(),
551+
envs: HashSet::new(),
552+
pass_through_envs: HashSet::new(),
553+
};
554+
555+
let resolved_task_config =
556+
ResolvedTaskConfig { config_dir: ".".into(), config: task_config };
557+
558+
// Test when FORCE_COLOR is not already set
559+
unsafe {
560+
std::env::remove_var("FORCE_COLOR");
561+
}
562+
563+
let result = TaskEnvs::resolve(Path::new("."), &resolved_task_config).unwrap();
564+
565+
// FORCE_COLOR should be automatically added if color is supported
566+
// Note: This test might vary based on the test environment
567+
let force_color_present = result.all_envs.contains_key("FORCE_COLOR");
568+
if force_color_present {
569+
let force_color_value = result.all_envs.get("FORCE_COLOR").unwrap();
570+
let force_color_str = force_color_value.to_str().unwrap();
571+
// Should be a valid FORCE_COLOR level
572+
assert!(matches!(force_color_str, "0" | "1" | "2" | "3"));
573+
}
574+
575+
// Test when FORCE_COLOR is already set - should not be overridden
576+
unsafe {
577+
std::env::set_var("FORCE_COLOR", "2");
578+
}
579+
580+
let result2 = TaskEnvs::resolve(Path::new("."), &resolved_task_config).unwrap();
581+
582+
// Should contain the original FORCE_COLOR value
583+
assert!(result2.all_envs.contains_key("FORCE_COLOR"));
584+
let force_color_value = result2.all_envs.get("FORCE_COLOR").unwrap();
585+
assert_eq!(force_color_value.to_str().unwrap(), "2");
586+
587+
// FORCE_COLOR should not be in envs_without_pass_through since it's a passthrough env
588+
assert!(!result2.envs_without_pass_through.contains_key("FORCE_COLOR"));
589+
590+
// Clean up
591+
unsafe {
592+
std::env::remove_var("FORCE_COLOR");
593+
}
594+
}
513595
}

0 commit comments

Comments
 (0)