//! Presentation primitives shared by the two places Koh prints: the command //! line and the full-screen history view. //! //! Colour lives here so both surfaces use one palette, and `NO_COLOR` / //! `KOH_NO_COLOR` turn it off for both at once. module fmt import os import time pub const glyph_ok = '✦' pub const glyph_fail = '✗' pub fn colour_enabled() bool { if os.getenv('NO_COLOR') != '' || os.getenv('KOH_NO_COLOR') != '' { return false } return os.is_atty(1) != 0 } fn paint(text string, code string) string { if text == '' || !colour_enabled() { return text } return '\x1b[${code}m${text}\x1b[0m' } pub fn bold(text string) string { return paint(text, '1') } pub fn reverse(text string) string { return paint(text, '7') } pub fn amber(text string) string { return paint(text, '38;5;214') } pub fn red(text string) string { return paint(text, '38;5;203') } pub fn green(text string) string { return paint(text, '38;5;114') } pub fn dim(text string) string { return paint(text, '38;5;245') } // The history view keeps Koh 2's softer palette: gold for the selected save, // muted grey for everything around it, blue for HEAD. pub fn gold(text string) string { return paint(text, '38;5;179') } pub fn muted(text string) string { return paint(text, '38;5;241') } pub fn blue(text string) string { return paint(text, '38;5;110') } // The manual keeps the rest of Koh 2's man-page palette. pub fn light(text string) string { return paint(text, '38;5;223') } pub fn warning(text string) string { return paint(text, '38;5;173') } // strip_ansi removes the colour codes, for measuring how wide a line really is. pub fn strip_ansi(text string) string { mut out := '' mut skipping := false for c in text.runes() { if c == `\x1b` { skipping = true continue } if skipping { if (c >= `a` && c <= `z`) || (c >= `A` && c <= `Z`) { skipping = false } continue } out += c.str() } return out } // visible_width counts characters, not bytes or colour codes; a double-width character counts as one. pub fn visible_width(text string) int { return strip_ansi(text).runes().len } // fit cuts a line to `width` visible columns and keeps its colour codes, so a // long line can never wrap and scroll a full-screen frame. pub fn fit(text string, width int) string { if visible_width(text) <= width { return text } mut out := []rune{} mut shown := 0 mut skipping := false for c in text.runes() { if c == `\x1b` { skipping = true } if skipping { out << c if (c >= `a` && c <= `z`) || (c >= `A` && c <= `Z`) { skipping = false } continue } if shown == width { break } out << c shown++ } cut := out.string() return if cut.contains('\x1b') { cut + '\x1b[0m' } else { cut } } pub fn stamp(unix_seconds i64) string { return time.unix(unix_seconds).local().format() } pub fn date_of(unix_seconds i64) string { return time.unix(unix_seconds).local().ymmdd() } pub fn hour_of(unix_seconds i64) string { return time.unix(unix_seconds).local().hhmm() } pub fn clock_of(unix_seconds i64) string { return time.unix(unix_seconds).local().hhmmss() } pub fn human_bytes(bytes i64) string { mut value := f64(bytes) mut unit := 'B' for suffix in ['KiB', 'MiB', 'GiB'] { if value < 1024 { break } value /= 1024 unit = suffix } if unit == 'B' { return '${bytes} B' } return '${value:.1f} ${unit}' } // plural renders "1 file" / "3 files". pub fn plural(count int, noun string) string { if count == 1 { return '1 ${noun}' } return '${count} ${noun}s' }