Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled
Single-commit orphan branch: full zed-industries/zed @ 8c74db0 source tree with a 3-file patch applied (no upstream history). Patch (crates/gpui_linux/src/linux/wayland/): - serial.rs: add SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; add latest_serial_of() - window.rs: activate() uses keyboard-enter serial (Mutter focus gate) Mutter honors window activation only when the token carries the keyboard- focus serial from wl_keyboard.enter; GPUI used a stale mouse-press serial. See docs/tray-window-focus-wayland.md in logiguard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
652 lines
20 KiB
Rust
652 lines
20 KiB
Rust
use std::{
|
|
borrow::Cow,
|
|
collections::HashMap,
|
|
io::{IsTerminal, Write},
|
|
sync::{Arc, Mutex, OnceLock},
|
|
time::{Duration, Instant},
|
|
};
|
|
|
|
use crate::paths::RUN_DIR;
|
|
|
|
use log::{Level, Log, Metadata, Record};
|
|
|
|
pub struct Progress {
|
|
inner: Mutex<ProgressInner>,
|
|
}
|
|
|
|
struct ProgressInner {
|
|
completed: Vec<CompletedTask>,
|
|
in_progress: HashMap<String, InProgressTask>,
|
|
is_tty: bool,
|
|
terminal_width: usize,
|
|
max_example_name_len: usize,
|
|
status_lines_displayed: usize,
|
|
total_examples: usize,
|
|
completed_examples: usize,
|
|
failed_examples: usize,
|
|
last_line_is_logging: bool,
|
|
ticker: Option<std::thread::JoinHandle<()>>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct InProgressTask {
|
|
step: Step,
|
|
started_at: Instant,
|
|
substatus: Option<String>,
|
|
info: Option<(String, InfoStyle)>,
|
|
}
|
|
|
|
struct CompletedTask {
|
|
step: Step,
|
|
example_name: String,
|
|
duration: Duration,
|
|
info: Option<(String, InfoStyle)>,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
pub enum Step {
|
|
LoadProject,
|
|
Context,
|
|
FormatPrompt,
|
|
Predict,
|
|
Score,
|
|
Qa,
|
|
Repair,
|
|
Synthesize,
|
|
PullExamples,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum InfoStyle {
|
|
Normal,
|
|
Warning,
|
|
}
|
|
|
|
impl Step {
|
|
pub fn label(&self) -> &'static str {
|
|
match self {
|
|
Step::LoadProject => "Load",
|
|
Step::Context => "Context",
|
|
Step::FormatPrompt => "Format",
|
|
Step::Predict => "Predict",
|
|
Step::Score => "Score",
|
|
Step::Qa => "QA",
|
|
Step::Repair => "Repair",
|
|
Step::Synthesize => "Synthesize",
|
|
Step::PullExamples => "Pull",
|
|
}
|
|
}
|
|
|
|
fn color_code(&self) -> &'static str {
|
|
match self {
|
|
Step::LoadProject => "\x1b[33m",
|
|
Step::Context => "\x1b[35m",
|
|
Step::FormatPrompt => "\x1b[34m",
|
|
Step::Predict => "\x1b[32m",
|
|
Step::Score => "\x1b[31m",
|
|
Step::Qa => "\x1b[36m",
|
|
Step::Repair => "\x1b[95m",
|
|
Step::Synthesize => "\x1b[36m",
|
|
Step::PullExamples => "\x1b[36m",
|
|
}
|
|
}
|
|
}
|
|
|
|
static GLOBAL: OnceLock<Arc<Progress>> = OnceLock::new();
|
|
static LOGGER: ProgressLogger = ProgressLogger;
|
|
|
|
const MARGIN: usize = 4;
|
|
const MAX_STATUS_LINES: usize = 10;
|
|
const STATUS_TICK_INTERVAL: Duration = Duration::from_millis(300);
|
|
|
|
impl Progress {
|
|
/// Returns the global Progress instance, initializing it if necessary.
|
|
pub fn global() -> Arc<Progress> {
|
|
GLOBAL
|
|
.get_or_init(|| {
|
|
let progress = Arc::new(Self {
|
|
inner: Mutex::new(ProgressInner {
|
|
completed: Vec::new(),
|
|
in_progress: HashMap::new(),
|
|
is_tty: std::env::var("COLOR").is_ok()
|
|
|| (std::env::var("NO_COLOR").is_err()
|
|
&& std::io::stderr().is_terminal()),
|
|
terminal_width: get_terminal_width(),
|
|
max_example_name_len: 0,
|
|
status_lines_displayed: 0,
|
|
total_examples: 0,
|
|
completed_examples: 0,
|
|
failed_examples: 0,
|
|
last_line_is_logging: false,
|
|
ticker: None,
|
|
}),
|
|
});
|
|
let _ = log::set_logger(&LOGGER);
|
|
log::set_max_level(log::LevelFilter::Info);
|
|
progress
|
|
})
|
|
.clone()
|
|
}
|
|
|
|
pub fn start_group(self: &Arc<Self>, example_name: &str) -> ExampleProgress {
|
|
ExampleProgress {
|
|
progress: self.clone(),
|
|
example_name: example_name.to_string(),
|
|
}
|
|
}
|
|
|
|
fn increment_completed(&self) {
|
|
let mut inner = self.inner.lock().unwrap();
|
|
inner.completed_examples += 1;
|
|
}
|
|
|
|
pub fn set_total_examples(&self, total: usize) {
|
|
let mut inner = self.inner.lock().unwrap();
|
|
inner.total_examples = total;
|
|
}
|
|
|
|
pub fn set_max_example_name_len(&self, example_names: impl Iterator<Item = impl AsRef<str>>) {
|
|
let mut inner = self.inner.lock().unwrap();
|
|
let max_name_width = inner
|
|
.terminal_width
|
|
.saturating_sub(MARGIN * 2)
|
|
.saturating_div(3)
|
|
.max(1);
|
|
inner.max_example_name_len = example_names
|
|
.map(|name| name.as_ref().len().min(max_name_width))
|
|
.max()
|
|
.unwrap_or(0);
|
|
}
|
|
|
|
pub fn increment_failed(&self) {
|
|
let mut inner = self.inner.lock().unwrap();
|
|
inner.failed_examples += 1;
|
|
}
|
|
|
|
/// Prints a message to stderr, clearing and redrawing status lines to avoid corruption.
|
|
/// This should be used for any output that needs to appear above the status lines.
|
|
fn log(&self, message: &str) {
|
|
let mut inner = self.inner.lock().unwrap();
|
|
Self::clear_status_lines(&mut inner);
|
|
|
|
if !inner.last_line_is_logging {
|
|
let reset = "\x1b[0m";
|
|
let dim = "\x1b[2m";
|
|
let divider = "─".repeat(inner.terminal_width.saturating_sub(MARGIN));
|
|
eprintln!("{dim}{divider}{reset}");
|
|
inner.last_line_is_logging = true;
|
|
}
|
|
|
|
let max_width = inner.terminal_width.saturating_sub(MARGIN);
|
|
for line in message.lines() {
|
|
let truncated = truncate_to_visible_width(line, max_width);
|
|
if truncated.len() < line.len() {
|
|
eprintln!("{}…", truncated);
|
|
} else {
|
|
eprintln!("{}", truncated);
|
|
}
|
|
}
|
|
|
|
Self::print_status_lines(&mut inner);
|
|
}
|
|
|
|
pub fn start(self: &Arc<Self>, step: Step, example_name: &str) -> StepProgress {
|
|
let mut inner = self.inner.lock().unwrap();
|
|
|
|
Self::clear_status_lines(&mut inner);
|
|
|
|
// Update max_example_name_len if not already set via set_max_example_name_len
|
|
if inner.max_example_name_len == 0 {
|
|
let max_name_width = inner
|
|
.terminal_width
|
|
.saturating_sub(MARGIN * 2)
|
|
.saturating_div(3)
|
|
.max(1);
|
|
inner.max_example_name_len = example_name.len().min(max_name_width);
|
|
}
|
|
inner.in_progress.insert(
|
|
example_name.to_string(),
|
|
InProgressTask {
|
|
step,
|
|
started_at: Instant::now(),
|
|
substatus: None,
|
|
info: None,
|
|
},
|
|
);
|
|
|
|
if inner.is_tty && inner.ticker.is_none() {
|
|
let progress = self.clone();
|
|
inner.ticker = Some(std::thread::spawn(move || {
|
|
loop {
|
|
std::thread::sleep(STATUS_TICK_INTERVAL);
|
|
|
|
let mut inner = progress.inner.lock().unwrap();
|
|
if inner.in_progress.is_empty() {
|
|
break;
|
|
}
|
|
|
|
Progress::clear_status_lines(&mut inner);
|
|
Progress::print_status_lines(&mut inner);
|
|
}
|
|
}));
|
|
}
|
|
|
|
Self::print_status_lines(&mut inner);
|
|
|
|
StepProgress {
|
|
progress: self.clone(),
|
|
step,
|
|
example_name: example_name.to_string(),
|
|
}
|
|
}
|
|
|
|
fn finish(&self, step: Step, example_name: &str) {
|
|
let mut inner = self.inner.lock().unwrap();
|
|
|
|
let Some(task) = inner.in_progress.remove(example_name) else {
|
|
return;
|
|
};
|
|
|
|
if task.step == step {
|
|
let duration = task.started_at.elapsed();
|
|
|
|
// Skip logging for tasks that complete quickly (under 500ms)
|
|
let should_print = duration >= Duration::from_millis(500);
|
|
|
|
inner.completed.push(CompletedTask {
|
|
step: task.step,
|
|
example_name: example_name.to_string(),
|
|
duration,
|
|
info: task.info,
|
|
});
|
|
|
|
Self::clear_status_lines(&mut inner);
|
|
if should_print {
|
|
Self::print_logging_closing_divider(&mut inner);
|
|
if let Some(last_completed) = inner.completed.last() {
|
|
Self::print_completed(&inner, last_completed);
|
|
}
|
|
}
|
|
Self::print_status_lines(&mut inner);
|
|
} else {
|
|
inner.in_progress.insert(example_name.to_string(), task);
|
|
}
|
|
}
|
|
|
|
fn print_logging_closing_divider(inner: &mut ProgressInner) {
|
|
if inner.last_line_is_logging {
|
|
let reset = "\x1b[0m";
|
|
let dim = "\x1b[2m";
|
|
let divider = "─".repeat(inner.terminal_width.saturating_sub(MARGIN));
|
|
eprintln!("{dim}{divider}{reset}");
|
|
inner.last_line_is_logging = false;
|
|
}
|
|
}
|
|
|
|
fn clear_status_lines(inner: &mut ProgressInner) {
|
|
if inner.is_tty && inner.status_lines_displayed > 0 {
|
|
// Move up and clear each line we previously displayed
|
|
for _ in 0..inner.status_lines_displayed {
|
|
eprint!("\x1b[A\x1b[K");
|
|
}
|
|
inner.status_lines_displayed = 0;
|
|
}
|
|
}
|
|
|
|
fn print_completed(inner: &ProgressInner, task: &CompletedTask) {
|
|
let duration = format_duration(task.duration);
|
|
let name_width = inner.max_example_name_len;
|
|
let truncated_name = truncate_with_ellipsis(&task.example_name, name_width);
|
|
|
|
if inner.is_tty {
|
|
let reset = "\x1b[0m";
|
|
let bold = "\x1b[1m";
|
|
let dim = "\x1b[2m";
|
|
|
|
let yellow = "\x1b[33m";
|
|
let info_part = task
|
|
.info
|
|
.as_ref()
|
|
.map(|(s, style)| {
|
|
if *style == InfoStyle::Warning {
|
|
format!("{yellow}{s}{reset}")
|
|
} else {
|
|
s.to_string()
|
|
}
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
let prefix = format!(
|
|
"{bold}{color}{label:>12}{reset} {name:<name_width$} {dim}│{reset} {info_part}",
|
|
color = task.step.color_code(),
|
|
label = task.step.label(),
|
|
name = truncated_name,
|
|
);
|
|
|
|
let duration_with_margin = format!("{duration} ");
|
|
let padding_needed = inner
|
|
.terminal_width
|
|
.saturating_sub(MARGIN)
|
|
.saturating_sub(duration_with_margin.len())
|
|
.saturating_sub(strip_ansi_len(&prefix));
|
|
let padding = " ".repeat(padding_needed);
|
|
|
|
eprintln!("{prefix}{padding}{dim}{duration_with_margin}{reset}");
|
|
} else {
|
|
let info_part = task
|
|
.info
|
|
.as_ref()
|
|
.map(|(s, _)| format!(" | {}", s))
|
|
.unwrap_or_default();
|
|
|
|
eprintln!(
|
|
"{label:>12} {name:<name_width$}{info_part} {duration}",
|
|
label = task.step.label(),
|
|
name = truncate_with_ellipsis(&task.example_name, name_width),
|
|
);
|
|
}
|
|
}
|
|
|
|
fn print_status_lines(inner: &mut ProgressInner) {
|
|
if !inner.is_tty || inner.in_progress.is_empty() {
|
|
inner.status_lines_displayed = 0;
|
|
return;
|
|
}
|
|
|
|
let reset = "\x1b[0m";
|
|
let bold = "\x1b[1m";
|
|
let dim = "\x1b[2m";
|
|
|
|
// Build the done/in-progress/total label
|
|
let done_count = inner.completed_examples;
|
|
let in_progress_count = inner.in_progress.len();
|
|
let failed_count = inner.failed_examples;
|
|
|
|
let failed_label = if failed_count > 0 {
|
|
format!(" {} failed ", failed_count)
|
|
} else {
|
|
String::new()
|
|
};
|
|
|
|
let range_label = format!(
|
|
" {}/{}/{} ",
|
|
done_count, in_progress_count, inner.total_examples
|
|
);
|
|
|
|
// Print a divider line with failed count on left, range label on right
|
|
let failed_visible_len = strip_ansi_len(&failed_label);
|
|
let range_visible_len = range_label.len();
|
|
let middle_divider_len = inner
|
|
.terminal_width
|
|
.saturating_sub(MARGIN * 2)
|
|
.saturating_sub(failed_visible_len)
|
|
.saturating_sub(range_visible_len);
|
|
let left_divider = "─".repeat(MARGIN);
|
|
let middle_divider = "─".repeat(middle_divider_len);
|
|
let right_divider = "─".repeat(MARGIN);
|
|
eprintln!(
|
|
"{dim}{left_divider}{reset}{failed_label}{dim}{middle_divider}{reset}{range_label}{dim}{right_divider}{reset}"
|
|
);
|
|
|
|
let mut tasks: Vec<_> = inner.in_progress.iter().collect();
|
|
tasks.sort_by_key(|(name, _)| *name);
|
|
|
|
let total_tasks = tasks.len();
|
|
let mut lines_printed = 0;
|
|
|
|
for (name, task) in tasks.iter().take(MAX_STATUS_LINES) {
|
|
let elapsed = format_duration(task.started_at.elapsed());
|
|
let substatus_part = task
|
|
.substatus
|
|
.as_ref()
|
|
.map(|s| truncate_with_ellipsis(s, 30))
|
|
.unwrap_or_default();
|
|
|
|
let step_label = task.step.label();
|
|
let step_color = task.step.color_code();
|
|
let name_width = inner.max_example_name_len;
|
|
let truncated_name = truncate_with_ellipsis(name, name_width);
|
|
|
|
let prefix = format!(
|
|
"{bold}{step_color}{step_label:>12}{reset} {name:<name_width$} {dim}│{reset} {substatus_part}",
|
|
name = truncated_name,
|
|
);
|
|
|
|
let duration_with_margin = format!("{elapsed} ");
|
|
let padding_needed = inner
|
|
.terminal_width
|
|
.saturating_sub(MARGIN)
|
|
.saturating_sub(duration_with_margin.len())
|
|
.saturating_sub(strip_ansi_len(&prefix));
|
|
let padding = " ".repeat(padding_needed);
|
|
|
|
eprintln!("{prefix}{padding}{dim}{duration_with_margin}{reset}");
|
|
lines_printed += 1;
|
|
}
|
|
|
|
// Show "+N more" on its own line if there are more tasks
|
|
if total_tasks > MAX_STATUS_LINES {
|
|
let remaining = total_tasks - MAX_STATUS_LINES;
|
|
eprintln!("{:>12} +{remaining} more", "");
|
|
lines_printed += 1;
|
|
}
|
|
|
|
inner.status_lines_displayed = lines_printed + 1; // +1 for the divider line
|
|
let _ = std::io::stderr().flush();
|
|
}
|
|
|
|
pub fn finalize(&self) {
|
|
let ticker = {
|
|
let mut inner = self.inner.lock().unwrap();
|
|
inner.ticker.take()
|
|
};
|
|
|
|
if let Some(ticker) = ticker {
|
|
let _ = ticker.join();
|
|
}
|
|
|
|
let mut inner = self.inner.lock().unwrap();
|
|
Self::clear_status_lines(&mut inner);
|
|
|
|
// Print summary if there were failures
|
|
if inner.failed_examples > 0 {
|
|
let total_examples = inner.total_examples;
|
|
let percentage = if total_examples > 0 {
|
|
inner.failed_examples as f64 / total_examples as f64 * 100.0
|
|
} else {
|
|
0.0
|
|
};
|
|
let failed_jsonl_path = RUN_DIR.join("failed.jsonl");
|
|
eprintln!(
|
|
"\n{} of {} examples failed ({:.1}%)\nFailed examples: {}",
|
|
inner.failed_examples,
|
|
total_examples,
|
|
percentage,
|
|
failed_jsonl_path.display()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct ExampleProgress {
|
|
progress: Arc<Progress>,
|
|
example_name: String,
|
|
}
|
|
|
|
impl ExampleProgress {
|
|
pub fn start(&self, step: Step) -> StepProgress {
|
|
self.progress.start(step, &self.example_name)
|
|
}
|
|
}
|
|
|
|
impl Drop for ExampleProgress {
|
|
fn drop(&mut self) {
|
|
self.progress.increment_completed();
|
|
}
|
|
}
|
|
|
|
pub struct StepProgress {
|
|
progress: Arc<Progress>,
|
|
step: Step,
|
|
example_name: String,
|
|
}
|
|
|
|
impl StepProgress {
|
|
pub fn set_substatus(&self, substatus: impl Into<Cow<'static, str>>) {
|
|
let mut inner = self.progress.inner.lock().unwrap();
|
|
if let Some(task) = inner.in_progress.get_mut(&self.example_name) {
|
|
task.substatus = Some(substatus.into().into_owned());
|
|
Progress::clear_status_lines(&mut inner);
|
|
Progress::print_status_lines(&mut inner);
|
|
}
|
|
}
|
|
|
|
pub fn clear_substatus(&self) {
|
|
let mut inner = self.progress.inner.lock().unwrap();
|
|
if let Some(task) = inner.in_progress.get_mut(&self.example_name) {
|
|
task.substatus = None;
|
|
Progress::clear_status_lines(&mut inner);
|
|
Progress::print_status_lines(&mut inner);
|
|
}
|
|
}
|
|
|
|
pub fn set_info(&self, info: impl Into<String>, style: InfoStyle) {
|
|
let mut inner = self.progress.inner.lock().unwrap();
|
|
if let Some(task) = inner.in_progress.get_mut(&self.example_name) {
|
|
task.info = Some((info.into(), style));
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for StepProgress {
|
|
fn drop(&mut self) {
|
|
self.progress.finish(self.step, &self.example_name);
|
|
}
|
|
}
|
|
|
|
struct ProgressLogger;
|
|
|
|
impl Log for ProgressLogger {
|
|
fn enabled(&self, metadata: &Metadata) -> bool {
|
|
metadata.level() <= Level::Info
|
|
}
|
|
|
|
fn log(&self, record: &Record) {
|
|
if !self.enabled(record.metadata()) {
|
|
return;
|
|
}
|
|
|
|
let level_color = match record.level() {
|
|
Level::Error => "\x1b[31m",
|
|
Level::Warn => "\x1b[33m",
|
|
Level::Info => "\x1b[32m",
|
|
Level::Debug => "\x1b[34m",
|
|
Level::Trace => "\x1b[35m",
|
|
};
|
|
let reset = "\x1b[0m";
|
|
let bold = "\x1b[1m";
|
|
|
|
let level_label = match record.level() {
|
|
Level::Error => "Error",
|
|
Level::Warn => "Warn",
|
|
Level::Info => "Info",
|
|
Level::Debug => "Debug",
|
|
Level::Trace => "Trace",
|
|
};
|
|
|
|
let message = format!(
|
|
"{bold}{level_color}{level_label:>12}{reset} {}",
|
|
record.args()
|
|
);
|
|
|
|
if let Some(progress) = GLOBAL.get() {
|
|
progress.log(&message);
|
|
} else {
|
|
eprintln!("{}", message);
|
|
}
|
|
}
|
|
|
|
fn flush(&self) {
|
|
let _ = std::io::stderr().flush();
|
|
}
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn get_terminal_width() -> usize {
|
|
unsafe {
|
|
let mut winsize: libc::winsize = std::mem::zeroed();
|
|
if libc::ioctl(libc::STDERR_FILENO, libc::TIOCGWINSZ, &mut winsize) == 0
|
|
&& winsize.ws_col > 0
|
|
{
|
|
winsize.ws_col as usize
|
|
} else {
|
|
80
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(not(unix))]
|
|
fn get_terminal_width() -> usize {
|
|
80
|
|
}
|
|
|
|
fn strip_ansi_len(s: &str) -> usize {
|
|
let mut len = 0;
|
|
let mut in_escape = false;
|
|
for c in s.chars() {
|
|
if c == '\x1b' {
|
|
in_escape = true;
|
|
} else if in_escape {
|
|
if c == 'm' {
|
|
in_escape = false;
|
|
}
|
|
} else {
|
|
len += 1;
|
|
}
|
|
}
|
|
len
|
|
}
|
|
|
|
fn truncate_with_ellipsis(s: &str, max_len: usize) -> Cow<'_, str> {
|
|
if s.len() <= max_len {
|
|
Cow::Borrowed(s)
|
|
} else {
|
|
Cow::Owned(format!("{}…", &s[..max_len.saturating_sub(1)]))
|
|
}
|
|
}
|
|
|
|
fn truncate_to_visible_width(s: &str, max_visible_len: usize) -> &str {
|
|
let mut visible_len = 0;
|
|
let mut in_escape = false;
|
|
let mut last_byte_index = 0;
|
|
for (byte_index, c) in s.char_indices() {
|
|
if c == '\x1b' {
|
|
in_escape = true;
|
|
} else if in_escape {
|
|
if c == 'm' {
|
|
in_escape = false;
|
|
}
|
|
} else {
|
|
if visible_len >= max_visible_len {
|
|
return &s[..last_byte_index];
|
|
}
|
|
visible_len += 1;
|
|
}
|
|
last_byte_index = byte_index + c.len_utf8();
|
|
}
|
|
s
|
|
}
|
|
|
|
fn format_duration(duration: Duration) -> String {
|
|
const MINUTE_IN_MILLIS: f32 = 60. * 1000.;
|
|
|
|
let millis = duration.as_millis() as f32;
|
|
if millis < 1000.0 {
|
|
format!("{}ms", millis)
|
|
} else if millis < MINUTE_IN_MILLIS {
|
|
format!("{:.1}s", millis / 1_000.0)
|
|
} else {
|
|
format!("{:.1}m", millis / MINUTE_IN_MILLIS)
|
|
}
|
|
}
|