Skip to content
Open
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
5 changes: 5 additions & 0 deletions crates/vite_global_cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,11 @@ async fn main() -> ExitCode {
}
};

// Set terminal title to the project name from package.json
if let Some(project_name) = vite_shared::read_project_name(cwd.as_path().as_ref()) {
vite_shared::header::set_terminal_title(&project_name);
}

if args.len() == 1 {
match command_picker::pick_top_level_command_if_interactive(&cwd) {
Ok(command_picker::TopLevelCommandPick::Selected(selection)) => {
Expand Down
45 changes: 45 additions & 0 deletions crates/vite_shared/src/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,40 @@ fn render_header_variant(
format!("{}{}", bold(&vite_plus, prefix_bold), bold(&suffix, suffix_bold))
}

/// Set the terminal window title using OSC 0 escape sequence.
///
/// Writes `ESC ] 0 ; <title> BEL` only when stdout looks like a terminal with
/// ANSI/VT escape support. This is a best-effort hint: unsupported terminals
/// may ignore it, and environments without escape support are treated as no-op.
pub fn set_terminal_title(title: &str) {
use std::io::Write;

if !should_set_terminal_title() {
return;
}

let title = sanitize_terminal_title(title);
if title.is_empty() {
return;
}

let _ = write!(std::io::stdout(), "\x1b]0;{title}\x07");
let _ = std::io::stdout().flush();
}

fn should_set_terminal_title() -> bool {
let stdout = std::io::stdout();

stdout.is_terminal()
&& std::env::var_os("CI").is_none()
&& std::env::var_os("GITHUB_ACTIONS").is_none()
&& on(Stream::Stdout).is_some()
}

fn sanitize_terminal_title(title: &str) -> String {
title.chars().filter(|ch| !matches!(ch, '\u{0000}'..='\u{001f}' | '\u{007f}')).collect()
}

/// Render the Vite+ CLI header string with JS-parity coloring behavior.
#[must_use]
pub fn vite_plus_header() -> String {
Expand All @@ -545,6 +579,17 @@ mod tests {
query_terminal_colors, read_until_either, to_8bit,
};

#[test]
fn sanitize_terminal_title_strips_control_chars() {
assert_eq!(sanitize_terminal_title("my\x1b]2;bad\x07project"), "my]2;badproject");
assert_eq!(sanitize_terminal_title("vite-plus"), "vite-plus");
}

#[test]
fn sanitize_terminal_title_can_return_empty() {
assert_eq!(sanitize_terminal_title("\x1b\x07\n"), "");
}

#[test]
fn to_8bit_matches_js_rules() {
assert_eq!(to_8bit("ff"), Some(255));
Expand Down
23 changes: 23 additions & 0 deletions crates/vite_shared/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,26 @@ pub use path_env::{
prepend_to_path_env,
};
pub use tracing::init_tracing;

/// Read the project name from the nearest `package.json` in the given directory.
///
/// Walks up the directory tree from `start_dir` looking for a `package.json` file
/// with a `name` field. Returns `None` if no such file is found or if it cannot
/// be parsed.
pub fn read_project_name(start_dir: &std::path::Path) -> Option<String> {
let mut dir = Some(start_dir);
while let Some(current) = dir {
let pkg_path = current.join("package.json");
if let Ok(contents) = std::fs::read_to_string(&pkg_path) {
if let Ok(pkg) = serde_json::from_str::<PackageJson>(&contents) {
if let Some(name) = pkg.name {
if !name.is_empty() {
return Some(name.to_string());
}
}
}
}
dir = current.parent();
}
None
}
3 changes: 3 additions & 0 deletions crates/vite_shared/src/package_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ pub struct Engines {
#[derive(Deserialize, Default, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct PackageJson {
/// The package name
#[serde(default)]
pub name: Option<Str>,
/// The devEngines configuration
#[serde(default)]
pub dev_engines: Option<DevEngines>,
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/binding/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1311,6 +1311,11 @@ pub async fn main(
options: Option<CliOptions>,
args: Option<Vec<String>>,
) -> Result<ExitStatus, Error> {
// Set terminal title to the project name from package.json
if let Some(project_name) = vite_shared::read_project_name(cwd.as_path().as_ref()) {
vite_shared::header::set_terminal_title(&project_name);
}

let args_vec: Vec<String> = args.unwrap_or_else(|| env::args().skip(1).collect());
let args_vec = normalize_help_args(args_vec);
if should_print_help(&args_vec) {
Expand Down