use crossterm::style::Color as CtColor;
pub mod arbiter;
pub mod caps;
pub mod frames;
mod modal;
pub mod progress_sink;
#[cfg(all(test, unix))]
mod pty_notice_test;
pub mod raw_mode;
mod row;
pub mod spinner;
pub mod theme;
pub mod widgets;
pub mod width;
pub use arbiter::{
blank_rows, place_below_cursor, prompt_stdin_active, prompt_windows_constructed,
try_watch_stdin, Ephemeral, EphemeralRegistration, LineLease, LineWriter, OnCollision,
Placement, PromptWindow, Region, RegionLease, Sink, Terminal, TerminalTaker, WatcherStdinGuard,
};
pub use caps::{enter_protocol_mode, protocol_mode, LineCaps};
pub use frames::{format_spinner, SPINNER_FRAMES};
pub use modal::{
modal_prompt_controls, read_prompt_window_line, ControlReader, Echo, PromptControlReader,
PromptLine, MODAL_CONTROL_HINT, MODAL_INPUT_GLYPH,
};
pub use progress_sink::TerminalProgressSink;
pub use spinner::{
interrupt_pending, interrupt_presses, note_interrupt_press, set_interrupt_pending,
with_spinner, Spinner,
};
pub use widgets::{Action, Level, Notice, Question};
pub use width::{ch_width, str_width, wrap_line};
pub const NEWT_ORANGE_CT: CtColor = CtColor::Rgb {
r: 220,
g: 60,
b: 20,
};
pub const ACTIVE_INPUT_CT: CtColor = CtColor::Rgb {
r: 255,
g: 165,
b: 90,
};
pub const FADE_CT: CtColor = CtColor::Rgb {
r: 90,
g: 90,
b: 90,
};
pub fn term_cols() -> usize {
crossterm::terminal::size()
.map(|(c, _)| c as usize)
.unwrap_or(80)
.max(8)
}
#[derive(Debug, PartialEq, Eq)]
pub struct FittedLine {
pub head: String,
pub fade: String,
pub ellipsis: &'static str,
}
pub fn fit_line(s: &str, max_cols: usize) -> FittedLine {
if str_width(s) <= max_cols {
return FittedLine {
head: s.to_string(),
fade: String::new(),
ellipsis: "",
};
}
let budget = max_cols.saturating_sub(1).max(1);
let mut kept: Vec<char> = Vec::new();
let mut used = 0usize;
for c in s.chars() {
let cw = ch_width(c);
if used + cw > budget {
break;
}
used += cw;
kept.push(c);
}
if kept.is_empty() {
kept.extend(s.chars().next());
}
let fade_n = 2.min(kept.len());
let split = kept.len() - fade_n;
FittedLine {
head: kept[..split].iter().collect(),
fade: kept[split..].iter().collect(),
ellipsis: "…",
}
}
#[cfg(test)]
mod tests {
use super::{fit_line, term_cols};
#[test]
fn fit_line_passes_through_when_it_fits() {
let f = fit_line("hello", 10);
assert_eq!(f.head, "hello");
assert_eq!(f.fade, "");
assert_eq!(f.ellipsis, "");
let exact = fit_line("hello", 5);
assert_eq!(exact.ellipsis, "");
assert_eq!(exact.head, "hello");
}
#[test]
fn fit_line_truncates_with_faded_tail_and_ellipsis() {
let f = fit_line("abcdefghijk", 6);
assert_eq!(f.ellipsis, "…");
assert_eq!(f.head, "abc");
assert_eq!(f.fade, "de");
assert!(f.head.chars().count() + f.fade.chars().count() < 6);
}
#[test]
fn fit_line_handles_tiny_budgets() {
let f = fit_line("abcdef", 1);
assert_eq!(f.ellipsis, "…");
assert_eq!(f.head, "");
assert_eq!(f.fade, "a");
}
#[test]
fn term_cols_never_returns_a_degenerate_width() {
assert!(term_cols() >= 8);
}
#[test]
fn fit_line_measures_columns_so_a_cjk_label_is_actually_cut() {
use super::width::str_width;
let label = "日本語です";
assert_eq!(label.chars().count(), 5, "5 chars…");
assert_eq!(str_width(label), 10, "…but 10 columns");
let f = fit_line(label, 6);
assert_eq!(
f.ellipsis, "…",
"the old char-count result — the whole 10-column string, untruncated \
— was wrong: it overflows a 6-column row"
);
assert_eq!(f.head, "");
assert_eq!(f.fade, "日本");
assert!(
str_width(&f.head) + str_width(&f.fade) + str_width(f.ellipsis) <= 6,
"the fitted row must not exceed its budget"
);
}
#[test]
fn fit_line_keeps_a_glyph_wider_than_the_whole_budget() {
let f = fit_line("日本", 1);
assert_eq!(f.head, "");
assert_eq!(f.fade, "日");
assert_eq!(f.ellipsis, "…");
}
}