feat: update splash (#3631)
- Update splash styling. - Add center truncation for long paths. (Uses new `center_truncate_path` utility.) - Update the suggested commands. ## New splash <img width="560" height="326" alt="image" src="https://github.com/user-attachments/assets/b80d7075-f376-4019-a464-b96a78b0676d" /> ## Example with truncation: <img width="524" height="317" alt="image" src="https://github.com/user-attachments/assets/b023c5cc-0bf0-4d21-9b98-bfea85546eda" />
This commit is contained in:
1
codex-rs/Cargo.lock
generated
1
codex-rs/Cargo.lock
generated
@@ -921,6 +921,7 @@ dependencies = [
|
|||||||
"color-eyre",
|
"color-eyre",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"diffy",
|
"diffy",
|
||||||
|
"dirs",
|
||||||
"image",
|
"image",
|
||||||
"insta",
|
"insta",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ crossterm = { version = "0.28.1", features = [
|
|||||||
"bracketed-paste",
|
"bracketed-paste",
|
||||||
"event-stream",
|
"event-stream",
|
||||||
] }
|
] }
|
||||||
|
dirs = "6"
|
||||||
diffy = "0.4.2"
|
diffy = "0.4.2"
|
||||||
image = { version = "^0.25.8", default-features = false, features = [
|
image = { version = "^0.25.8", default-features = false, features = [
|
||||||
"jpeg",
|
"jpeg",
|
||||||
|
|||||||
@@ -781,8 +781,7 @@ async fn binary_size_transcript_snapshot() {
|
|||||||
// Consider content only after the last session banner marker. Skip the transient
|
// Consider content only after the last session banner marker. Skip the transient
|
||||||
// 'thinking' header if present, and start from the first non-empty content line
|
// 'thinking' header if present, and start from the first non-empty content line
|
||||||
// that follows. This keeps the snapshot stable across sessions.
|
// that follows. This keeps the snapshot stable across sessions.
|
||||||
const MARKER_PREFIX: &str =
|
const MARKER_PREFIX: &str = "To get started, describe a task or try one of these commands:";
|
||||||
"Describe a task to get started or try one of the following commands:";
|
|
||||||
let last_marker_line_idx = lines
|
let last_marker_line_idx = lines
|
||||||
.iter()
|
.iter()
|
||||||
.rposition(|l| l.trim_start().starts_with(MARKER_PREFIX))
|
.rposition(|l| l.trim_start().starts_with(MARKER_PREFIX))
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use dirs::home_dir;
|
||||||
use shlex::try_join;
|
use shlex::try_join;
|
||||||
|
|
||||||
pub(crate) fn escape_command(command: &[String]) -> String {
|
pub(crate) fn escape_command(command: &[String]) -> String {
|
||||||
@@ -27,13 +28,9 @@ where
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(home_dir) = std::env::var_os("HOME").map(PathBuf::from)
|
let home_dir = home_dir()?;
|
||||||
&& let Ok(rel) = path.strip_prefix(&home_dir)
|
let rel = path.strip_prefix(&home_dir).ok()?;
|
||||||
{
|
Some(rel.to_path_buf())
|
||||||
return Some(rel.to_path_buf());
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -574,7 +574,7 @@ impl HistoryCell for CompletedMcpToolCallWithImageOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const TOOL_CALL_MAX_LINES: usize = 5;
|
const TOOL_CALL_MAX_LINES: usize = 5;
|
||||||
const SESSION_HEADER_MAX_INNER_WIDTH: usize = 70;
|
const SESSION_HEADER_MAX_INNER_WIDTH: usize = 56; // Just an eyeballed value
|
||||||
|
|
||||||
fn title_case(s: &str) -> String {
|
fn title_case(s: &str) -> String {
|
||||||
if s.is_empty() {
|
if s.is_empty() {
|
||||||
@@ -628,24 +628,29 @@ pub(crate) fn new_session_info(
|
|||||||
|
|
||||||
// Help lines below the header (new copy and list)
|
// Help lines below the header (new copy and list)
|
||||||
let help_lines: Vec<Line<'static>> = vec![
|
let help_lines: Vec<Line<'static>> = vec![
|
||||||
"Describe a task to get started or try one of the following commands:"
|
" To get started, describe a task or try one of these commands:"
|
||||||
.dim()
|
.dim()
|
||||||
.into(),
|
.into(),
|
||||||
Line::from("".dim()),
|
Line::from(""),
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
"1. ".into(),
|
" ".into(),
|
||||||
"/status".bold(),
|
"/init".into(),
|
||||||
" - show current session configuration and token usage".dim(),
|
" - create an AGENTS.md file with instructions for Codex".dim(),
|
||||||
]),
|
]),
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
"2. ".into(),
|
" ".into(),
|
||||||
"/compact".bold(),
|
"/status".into(),
|
||||||
" - compact the chat history to avoid context limits".dim(),
|
" - show current session configuration".dim(),
|
||||||
]),
|
]),
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
"3. ".into(),
|
" ".into(),
|
||||||
"/prompts".bold(),
|
"/approvals".into(),
|
||||||
" - explore starter prompts to get to know Codex".dim(),
|
" - choose what Codex can do without approval".dim(),
|
||||||
|
]),
|
||||||
|
Line::from(vec![
|
||||||
|
" ".into(),
|
||||||
|
"/model".into(),
|
||||||
|
" - choose what model and reasoning effort to use".dim(),
|
||||||
]),
|
]),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -715,16 +720,31 @@ impl SessionHeaderHistoryCell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_directory(&self) -> String {
|
fn format_directory(&self, max_width: Option<usize>) -> String {
|
||||||
if let Some(rel) = relativize_to_home(&self.directory) {
|
Self::format_directory_inner(&self.directory, max_width)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_directory_inner(directory: &Path, max_width: Option<usize>) -> String {
|
||||||
|
let formatted = if let Some(rel) = relativize_to_home(directory) {
|
||||||
if rel.as_os_str().is_empty() {
|
if rel.as_os_str().is_empty() {
|
||||||
"~".to_string()
|
"~".to_string()
|
||||||
} else {
|
} else {
|
||||||
format!("~{}{}", std::path::MAIN_SEPARATOR, rel.display())
|
format!("~{}{}", std::path::MAIN_SEPARATOR, rel.display())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.directory.display().to_string()
|
directory.display().to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(max_width) = max_width {
|
||||||
|
if max_width == 0 {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
if UnicodeWidthStr::width(formatted.as_str()) > max_width {
|
||||||
|
return crate::text_formatting::center_truncate_path(&formatted, max_width);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
formatted
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reasoning_label(&self) -> Option<&'static str> {
|
fn reasoning_label(&self) -> Option<&'static str> {
|
||||||
@@ -753,79 +773,93 @@ impl HistoryCell for SessionHeaderHistoryCell {
|
|||||||
top.push('╭');
|
top.push('╭');
|
||||||
top.push_str(&"─".repeat(inner_width));
|
top.push_str(&"─".repeat(inner_width));
|
||||||
top.push('╮');
|
top.push('╮');
|
||||||
out.push(Line::from(top));
|
out.push(Line::from(top.dim()));
|
||||||
|
|
||||||
// Title line rendered inside the box: " >_ OpenAI Codex (vX)"
|
// Title line rendered inside the box: " >_ OpenAI Codex (vX)"
|
||||||
let title_text = format!(" >_ OpenAI Codex (v{})", self.version);
|
let title_text = format!(" >_ OpenAI Codex (v{})", self.version);
|
||||||
let title_w = UnicodeWidthStr::width(title_text.as_str());
|
let title_w = UnicodeWidthStr::width(title_text.as_str());
|
||||||
let pad_w = inner_width.saturating_sub(title_w);
|
let pad_w = inner_width.saturating_sub(title_w);
|
||||||
let mut title_spans: Vec<Span<'static>> = vec![
|
let mut title_spans: Vec<Span<'static>> = vec![
|
||||||
"│".into(),
|
Span::from("│").dim(),
|
||||||
" ".into(),
|
Span::from(" ").dim(),
|
||||||
">_ ".into(),
|
Span::from(">_ ").dim(),
|
||||||
"OpenAI Codex".bold(),
|
Span::from("OpenAI Codex").bold(),
|
||||||
" ".into(),
|
Span::from(" ").dim(),
|
||||||
format!("(v{})", self.version).dim(),
|
Span::from(format!("(v{})", self.version)).dim(),
|
||||||
];
|
];
|
||||||
if pad_w > 0 {
|
if pad_w > 0 {
|
||||||
title_spans.push(" ".repeat(pad_w).into());
|
title_spans.push(Span::from(" ".repeat(pad_w)).dim());
|
||||||
}
|
}
|
||||||
title_spans.push("│".into());
|
title_spans.push(Span::from("│").dim());
|
||||||
out.push(Line::from(title_spans));
|
out.push(Line::from(title_spans));
|
||||||
|
|
||||||
// Spacer row between title and details
|
// Spacer row between title and details
|
||||||
out.push(Line::from(vec![
|
out.push(Line::from(vec![
|
||||||
"│".into(),
|
Span::from(format!("│{}│", " ".repeat(inner_width))).dim(),
|
||||||
" ".repeat(inner_width).into(),
|
|
||||||
"│".into(),
|
|
||||||
]));
|
]));
|
||||||
|
|
||||||
// Model line: " Model: <model> <reasoning_label> (change with /model)"
|
// Model line: " model: <model> <reasoning_label> (change with /model)"
|
||||||
const CHANGE_MODEL_HINT: &str = "(change with /model)";
|
const CHANGE_MODEL_HINT_COMMAND: &str = "/model";
|
||||||
|
const CHANGE_MODEL_HINT_EXPLANATION: &str = " to change";
|
||||||
|
const DIR_LABEL: &str = "directory:";
|
||||||
|
let label_width = DIR_LABEL.len();
|
||||||
|
let model_label = format!(
|
||||||
|
"{model_label:<label_width$}",
|
||||||
|
model_label = "model:",
|
||||||
|
label_width = label_width
|
||||||
|
);
|
||||||
let reasoning_label = self.reasoning_label();
|
let reasoning_label = self.reasoning_label();
|
||||||
let mut model_text = format!(" Model: {}", self.model);
|
let mut model_value_for_width = self.model.clone();
|
||||||
if let Some(reasoning) = reasoning_label {
|
if let Some(reasoning) = reasoning_label {
|
||||||
model_text.push(' ');
|
model_value_for_width.push(' ');
|
||||||
model_text.push_str(reasoning);
|
model_value_for_width.push_str(reasoning);
|
||||||
}
|
}
|
||||||
model_text.push(' ');
|
let model_text_for_width_calc = format!(
|
||||||
model_text.push_str(CHANGE_MODEL_HINT);
|
" {model_label} {model_value_for_width} {CHANGE_MODEL_HINT_COMMAND}{CHANGE_MODEL_HINT_EXPLANATION}",
|
||||||
let model_w = UnicodeWidthStr::width(model_text.as_str());
|
);
|
||||||
|
let model_w = UnicodeWidthStr::width(model_text_for_width_calc.as_str());
|
||||||
let pad_w = inner_width.saturating_sub(model_w);
|
let pad_w = inner_width.saturating_sub(model_w);
|
||||||
let mut spans: Vec<Span<'static>> = vec![
|
let mut spans: Vec<Span<'static>> = vec![
|
||||||
"│".into(),
|
Span::from(format!("│ {model_label} ")).dim(),
|
||||||
" ".into(),
|
Span::from(self.model.clone()),
|
||||||
"Model: ".bold(),
|
|
||||||
self.model.clone().into(),
|
|
||||||
];
|
];
|
||||||
if let Some(reasoning) = reasoning_label {
|
if let Some(reasoning) = reasoning_label {
|
||||||
spans.push(" ".into());
|
spans.push(Span::from(" "));
|
||||||
spans.push(reasoning.into());
|
spans.push(Span::from(reasoning));
|
||||||
}
|
}
|
||||||
spans.push(" ".into());
|
spans.push(Span::from(" ").dim());
|
||||||
spans.push(CHANGE_MODEL_HINT.dim());
|
spans.push(Span::from(CHANGE_MODEL_HINT_COMMAND).cyan());
|
||||||
|
spans.push(Span::from(CHANGE_MODEL_HINT_EXPLANATION).dim());
|
||||||
if pad_w > 0 {
|
if pad_w > 0 {
|
||||||
spans.push(" ".repeat(pad_w).into());
|
spans.push(Span::from(" ".repeat(pad_w)).dim());
|
||||||
}
|
}
|
||||||
spans.push("│".into());
|
spans.push(Span::from("│").dim());
|
||||||
out.push(Line::from(spans));
|
out.push(Line::from(spans));
|
||||||
|
|
||||||
// Directory line: " Directory: <cwd>"
|
// Directory line: " Directory: <cwd>"
|
||||||
let dir = self.format_directory();
|
let dir_label = format!("{DIR_LABEL:<label_width$}");
|
||||||
let dir_text = format!(" Directory: {dir}");
|
let dir_prefix = format!(" {dir_label} ");
|
||||||
|
let dir_max_width = inner_width.saturating_sub(UnicodeWidthStr::width(dir_prefix.as_str()));
|
||||||
|
let dir = self.format_directory(Some(dir_max_width));
|
||||||
|
let dir_text = format!(" {dir_label} {dir}");
|
||||||
let dir_w = UnicodeWidthStr::width(dir_text.as_str());
|
let dir_w = UnicodeWidthStr::width(dir_text.as_str());
|
||||||
let pad_w = inner_width.saturating_sub(dir_w);
|
let pad_w = inner_width.saturating_sub(dir_w);
|
||||||
let mut spans: Vec<Span<'static>> =
|
let mut spans: Vec<Span<'static>> = vec![
|
||||||
vec!["│".into(), " ".into(), "Directory: ".bold(), dir.into()];
|
Span::from("│").dim(),
|
||||||
|
Span::from(" ").dim(),
|
||||||
|
Span::from(dir_label).dim(),
|
||||||
|
Span::from(" ").dim(),
|
||||||
|
Span::from(dir),
|
||||||
|
];
|
||||||
if pad_w > 0 {
|
if pad_w > 0 {
|
||||||
spans.push(" ".repeat(pad_w).into());
|
spans.push(Span::from(" ".repeat(pad_w)).dim());
|
||||||
}
|
}
|
||||||
spans.push("│".into());
|
spans.push(Span::from("│").dim());
|
||||||
out.push(Line::from(spans));
|
out.push(Line::from(spans));
|
||||||
|
|
||||||
// Bottom border
|
// Bottom border
|
||||||
let bottom = format!("╰{}╯", "─".repeat(inner_width));
|
let bottom = format!("╰{}╯", "─".repeat(inner_width));
|
||||||
out.push(Line::from(bottom));
|
out.push(Line::from(bottom.dim()));
|
||||||
|
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
@@ -1523,6 +1557,7 @@ mod tests {
|
|||||||
use codex_core::config::Config;
|
use codex_core::config::Config;
|
||||||
use codex_core::config::ConfigOverrides;
|
use codex_core::config::ConfigOverrides;
|
||||||
use codex_core::config::ConfigToml;
|
use codex_core::config::ConfigToml;
|
||||||
|
use dirs::home_dir;
|
||||||
|
|
||||||
fn test_config() -> Config {
|
fn test_config() -> Config {
|
||||||
Config::load_from_base_config_with_overrides(
|
Config::load_from_base_config_with_overrides(
|
||||||
@@ -1561,11 +1596,35 @@ mod tests {
|
|||||||
let lines = render_lines(&cell.display_lines(80));
|
let lines = render_lines(&cell.display_lines(80));
|
||||||
let model_line = lines
|
let model_line = lines
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|line| line.contains("Model:"))
|
.find(|line| line.contains("model:"))
|
||||||
.expect("model line");
|
.expect("model line");
|
||||||
|
|
||||||
assert!(model_line.contains("Model: gpt-4o high"));
|
assert!(model_line.contains("gpt-4o high"));
|
||||||
assert!(model_line.contains("(change with /model)"));
|
assert!(model_line.contains("/model to change"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn session_header_directory_center_truncates() {
|
||||||
|
let mut dir = home_dir().expect("home directory");
|
||||||
|
for part in ["hello", "the", "fox", "is", "very", "fast"] {
|
||||||
|
dir.push(part);
|
||||||
|
}
|
||||||
|
|
||||||
|
let formatted = SessionHeaderHistoryCell::format_directory_inner(&dir, Some(24));
|
||||||
|
let sep = std::path::MAIN_SEPARATOR;
|
||||||
|
let expected = format!("~{sep}hello{sep}the{sep}…{sep}very{sep}fast");
|
||||||
|
assert_eq!(formatted, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn session_header_directory_front_truncates_long_segment() {
|
||||||
|
let mut dir = home_dir().expect("home directory");
|
||||||
|
dir.push("supercalifragilisticexpialidocious");
|
||||||
|
|
||||||
|
let formatted = SessionHeaderHistoryCell::format_directory_inner(&dir, Some(18));
|
||||||
|
let sep = std::path::MAIN_SEPARATOR;
|
||||||
|
let expected = format!("~{sep}…cexpialidocious");
|
||||||
|
assert_eq!(formatted, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
use unicode_width::UnicodeWidthChar;
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
/// Truncate a tool result to fit within the given height and width. If the text is valid JSON, we format it in a compact way before truncating.
|
/// Truncate a tool result to fit within the given height and width. If the text is valid JSON, we format it in a compact way before truncating.
|
||||||
/// This is a best-effort approach that may not work perfectly for text where 1 grapheme is rendered as multiple terminal cells.
|
/// This is a best-effort approach that may not work perfectly for text where 1 grapheme is rendered as multiple terminal cells.
|
||||||
@@ -100,6 +102,219 @@ pub(crate) fn truncate_text(text: &str, max_graphemes: usize) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Truncate a path-like string to the given display width, keeping leading and trailing segments
|
||||||
|
/// where possible and inserting a single Unicode ellipsis between them. If an individual segment
|
||||||
|
/// cannot fit, it is front-truncated with an ellipsis.
|
||||||
|
pub(crate) fn center_truncate_path(path: &str, max_width: usize) -> String {
|
||||||
|
if max_width == 0 {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
if UnicodeWidthStr::width(path) <= max_width {
|
||||||
|
return path.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let sep = std::path::MAIN_SEPARATOR;
|
||||||
|
let has_leading_sep = path.starts_with(sep);
|
||||||
|
let has_trailing_sep = path.ends_with(sep);
|
||||||
|
let mut raw_segments: Vec<&str> = path.split(sep).collect();
|
||||||
|
if has_leading_sep && !raw_segments.is_empty() && raw_segments[0].is_empty() {
|
||||||
|
raw_segments.remove(0);
|
||||||
|
}
|
||||||
|
if has_trailing_sep
|
||||||
|
&& !raw_segments.is_empty()
|
||||||
|
&& raw_segments.last().is_some_and(|last| last.is_empty())
|
||||||
|
{
|
||||||
|
raw_segments.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw_segments.is_empty() {
|
||||||
|
if has_leading_sep {
|
||||||
|
let root = sep.to_string();
|
||||||
|
if UnicodeWidthStr::width(root.as_str()) <= max_width {
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "…".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Segment<'a> {
|
||||||
|
original: &'a str,
|
||||||
|
text: String,
|
||||||
|
truncatable: bool,
|
||||||
|
is_suffix: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
let assemble = |leading: bool, segments: &[Segment<'_>]| -> String {
|
||||||
|
let mut result = String::new();
|
||||||
|
if leading {
|
||||||
|
result.push(sep);
|
||||||
|
}
|
||||||
|
for segment in segments {
|
||||||
|
if !result.is_empty() && !result.ends_with(sep) {
|
||||||
|
result.push(sep);
|
||||||
|
}
|
||||||
|
result.push_str(segment.text.as_str());
|
||||||
|
}
|
||||||
|
result
|
||||||
|
};
|
||||||
|
|
||||||
|
let front_truncate = |original: &str, allowed_width: usize| -> String {
|
||||||
|
if allowed_width == 0 {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
if UnicodeWidthStr::width(original) <= allowed_width {
|
||||||
|
return original.to_string();
|
||||||
|
}
|
||||||
|
if allowed_width == 1 {
|
||||||
|
return "…".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut kept: Vec<char> = Vec::new();
|
||||||
|
let mut used_width = 1; // reserve space for leading ellipsis
|
||||||
|
for ch in original.chars().rev() {
|
||||||
|
let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0);
|
||||||
|
if used_width + ch_width > allowed_width {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
used_width += ch_width;
|
||||||
|
kept.push(ch);
|
||||||
|
}
|
||||||
|
kept.reverse();
|
||||||
|
let mut truncated = String::from("…");
|
||||||
|
for ch in kept {
|
||||||
|
truncated.push(ch);
|
||||||
|
}
|
||||||
|
truncated
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut combos: Vec<(usize, usize)> = Vec::new();
|
||||||
|
let segment_count = raw_segments.len();
|
||||||
|
for left in 1..=segment_count {
|
||||||
|
let min_right = if left == segment_count { 0 } else { 1 };
|
||||||
|
for right in min_right..=(segment_count - left) {
|
||||||
|
combos.push((left, right));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let desired_suffix = if segment_count > 1 {
|
||||||
|
std::cmp::min(2, segment_count - 1)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
let mut prioritized: Vec<(usize, usize)> = Vec::new();
|
||||||
|
let mut fallback: Vec<(usize, usize)> = Vec::new();
|
||||||
|
for combo in combos {
|
||||||
|
if combo.1 >= desired_suffix {
|
||||||
|
prioritized.push(combo);
|
||||||
|
} else {
|
||||||
|
fallback.push(combo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let sort_combos = |items: &mut Vec<(usize, usize)>| {
|
||||||
|
items.sort_by(|(left_a, right_a), (left_b, right_b)| {
|
||||||
|
left_b
|
||||||
|
.cmp(left_a)
|
||||||
|
.then_with(|| right_b.cmp(right_a))
|
||||||
|
.then_with(|| (left_b + right_b).cmp(&(left_a + right_a)))
|
||||||
|
});
|
||||||
|
};
|
||||||
|
sort_combos(&mut prioritized);
|
||||||
|
sort_combos(&mut fallback);
|
||||||
|
|
||||||
|
let fit_segments =
|
||||||
|
|segments: &mut Vec<Segment<'_>>, allow_front_truncate: bool| -> Option<String> {
|
||||||
|
loop {
|
||||||
|
let candidate = assemble(has_leading_sep, segments);
|
||||||
|
let width = UnicodeWidthStr::width(candidate.as_str());
|
||||||
|
if width <= max_width {
|
||||||
|
return Some(candidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !allow_front_truncate {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut indices: Vec<usize> = Vec::new();
|
||||||
|
for (idx, seg) in segments.iter().enumerate().rev() {
|
||||||
|
if seg.truncatable && seg.is_suffix {
|
||||||
|
indices.push(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (idx, seg) in segments.iter().enumerate().rev() {
|
||||||
|
if seg.truncatable && !seg.is_suffix {
|
||||||
|
indices.push(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if indices.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut changed = false;
|
||||||
|
for idx in indices {
|
||||||
|
let original_width = UnicodeWidthStr::width(segments[idx].original);
|
||||||
|
if original_width <= max_width && segment_count > 2 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let seg_width = UnicodeWidthStr::width(segments[idx].text.as_str());
|
||||||
|
let other_width = width.saturating_sub(seg_width);
|
||||||
|
let allowed_width = max_width.saturating_sub(other_width).max(1);
|
||||||
|
let new_text = front_truncate(segments[idx].original, allowed_width);
|
||||||
|
if new_text != segments[idx].text {
|
||||||
|
segments[idx].text = new_text;
|
||||||
|
changed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !changed {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (left_count, right_count) in prioritized.into_iter().chain(fallback.into_iter()) {
|
||||||
|
let mut segments: Vec<Segment<'_>> = raw_segments[..left_count]
|
||||||
|
.iter()
|
||||||
|
.map(|seg| Segment {
|
||||||
|
original: seg,
|
||||||
|
text: (*seg).to_string(),
|
||||||
|
truncatable: true,
|
||||||
|
is_suffix: false,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let need_ellipsis = left_count + right_count < segment_count;
|
||||||
|
if need_ellipsis {
|
||||||
|
segments.push(Segment {
|
||||||
|
original: "…",
|
||||||
|
text: "…".to_string(),
|
||||||
|
truncatable: false,
|
||||||
|
is_suffix: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if right_count > 0 {
|
||||||
|
segments.extend(
|
||||||
|
raw_segments[segment_count - right_count..]
|
||||||
|
.iter()
|
||||||
|
.map(|seg| Segment {
|
||||||
|
original: seg,
|
||||||
|
text: (*seg).to_string(),
|
||||||
|
truncatable: true,
|
||||||
|
is_suffix: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let allow_front_truncate = need_ellipsis || segment_count <= 2;
|
||||||
|
if let Some(candidate) = fit_segments(&mut segments, allow_front_truncate) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
front_truncate(path, max_width)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -203,6 +418,49 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_center_truncate_doesnt_truncate_short_path() {
|
||||||
|
let sep = std::path::MAIN_SEPARATOR;
|
||||||
|
let path = format!("{sep}Users{sep}codex{sep}Public");
|
||||||
|
let truncated = center_truncate_path(&path, 40);
|
||||||
|
|
||||||
|
assert_eq!(truncated, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_center_truncate_truncates_long_path() {
|
||||||
|
let sep = std::path::MAIN_SEPARATOR;
|
||||||
|
let path = format!("~{sep}hello{sep}the{sep}fox{sep}is{sep}very{sep}fast");
|
||||||
|
let truncated = center_truncate_path(&path, 24);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
truncated,
|
||||||
|
format!("~{sep}hello{sep}the{sep}…{sep}very{sep}fast")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_center_truncate_truncates_long_windows_path() {
|
||||||
|
let sep = std::path::MAIN_SEPARATOR;
|
||||||
|
let path = format!(
|
||||||
|
"C:{sep}Users{sep}codex{sep}Projects{sep}super{sep}long{sep}windows{sep}path{sep}file.txt"
|
||||||
|
);
|
||||||
|
let truncated = center_truncate_path(&path, 36);
|
||||||
|
|
||||||
|
let expected = format!("C:{sep}Users{sep}codex{sep}…{sep}path{sep}file.txt");
|
||||||
|
|
||||||
|
assert_eq!(truncated, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_center_truncate_handles_long_segment() {
|
||||||
|
let sep = std::path::MAIN_SEPARATOR;
|
||||||
|
let path = format!("~{sep}supercalifragilisticexpialidocious");
|
||||||
|
let truncated = center_truncate_path(&path, 18);
|
||||||
|
|
||||||
|
assert_eq!(truncated, format!("~{sep}…cexpialidocious"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_format_json_compact_array() {
|
fn test_format_json_compact_array() {
|
||||||
let json = r#"[ 1, 2, { "key": "value" }, "string" ]"#;
|
let json = r#"[ 1, 2, { "key": "value" }, "string" ]"#;
|
||||||
|
|||||||
Reference in New Issue
Block a user