diff --git a/pkgs/flake-repos/Cargo.lock b/pkgs/flake-repos/Cargo.lock index 283a9ae..ac6a5a6 100644 --- a/pkgs/flake-repos/Cargo.lock +++ b/pkgs/flake-repos/Cargo.lock @@ -116,6 +116,17 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "comfy-table" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136c8c4c3823846e8ba6d4bda011b4e5d5827b8bb0ac26c31f9ab31abe9f54f2" +dependencies = [ + "crossterm", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "console" version = "0.16.4" @@ -128,6 +139,38 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags", + "crossterm_winapi", + "document-features", + "parking_lot", + "rustix", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "encode_unicode" version = "1.0.0" @@ -161,6 +204,7 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", + "comfy-table", "console", "fd-lock", "serde", @@ -197,6 +241,21 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "memchr" version = "2.8.3" @@ -209,6 +268,29 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -227,6 +309,15 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + [[package]] name = "rustix" version = "1.1.4" @@ -240,6 +331,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.229" @@ -283,6 +380,12 @@ dependencies = [ "zmij", ] +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + [[package]] name = "strsim" version = "0.11.1" @@ -306,6 +409,12 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + [[package]] name = "unicode-width" version = "0.2.2" @@ -318,6 +427,28 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-link" version = "0.2.1" diff --git a/pkgs/flake-repos/Cargo.toml b/pkgs/flake-repos/Cargo.toml index 8ab219d..abcfe77 100644 --- a/pkgs/flake-repos/Cargo.toml +++ b/pkgs/flake-repos/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [dependencies] anyhow = "1.0.104" clap = { version = "4.6.6", features = ["derive"] } +comfy-table = "8.0.0" console = "0.16.4" fd-lock = "4.0.4" serde = { version = "1.0.229", features = ["derive"] } diff --git a/pkgs/flake-repos/src/render.rs b/pkgs/flake-repos/src/render.rs index 5de3cec..ad191c7 100644 --- a/pkgs/flake-repos/src/render.rs +++ b/pkgs/flake-repos/src/render.rs @@ -1,6 +1,6 @@ use crate::model::{truncate, Repo}; use crate::status::{LocalState, RemoteStatus, RepoStatus, SyncState}; -use console::{measure_text_width, Style}; +use comfy_table::{presets::UTF8_FULL, Cell, Color, ContentArrangement, Table}; const LOCAL_NAME_MAX: usize = 30; const REMOTE_NAME_MAX: usize = 20; @@ -12,114 +12,106 @@ mod glyphs { pub const CONN_LAST: &str = "┗━>"; pub const CONN_STRAIGHT: &str = "━━━>"; - pub const COL_REPO: &str = "\u{f02a2}"; - pub const COL_LOCAL: &str = "\u{f0219}"; - pub const COL_SYNC: &str = "\u{f0450}"; - pub const COL_REMOTE: &str = "\u{f02a4}"; - - pub const ICON_CLEAN: &str = "\u{f0132}"; - pub const ICON_DIRTY: &str = "\u{f044a}"; - pub const ICON_MISSING: &str = "\u{f05de}"; - pub const ICON_IN_SYNC: &str = "\u{eab2}"; - pub const ICON_AHEAD: &str = "\u{f0575}"; - pub const ICON_BEHIND: &str = "\u{f0577}"; - pub const ICON_DIVERGED: &str = "\u{f0028}"; -} - -mod color { - use console::Style; - - pub fn frame() -> Style { - Style::new().color256(8) - } - pub fn content() -> Style { - Style::new().white() - } - pub fn connector() -> Style { - Style::new().cyan() - } - pub fn green() -> Style { - Style::new().green() - } - pub fn yellow() -> Style { - Style::new().yellow() - } - pub fn red() -> Style { - Style::new().red() + pub const COL_REPO: &str = "󰳐"; + pub const COL_LOCAL: &str = ""; + pub const COL_SYNC: &str = "󱓎"; + pub const COL_REMOTE: &str = ""; + + pub const ICON_CLEAN: &str = "󰚰"; + pub const ICON_DIRTY: &str = "󰃢"; + pub const ICON_MISSING: &str = ""; + pub const ICON_IN_SYNC: &str = "󱓏"; + pub const ICON_AHEAD: &str = ""; + pub const ICON_BEHIND: &str = ""; + pub const ICON_DIVERGED: &str = "󰦾"; +} + +#[derive(Clone, Copy)] +enum StatusColor { + White, + Green, + Yellow, + Red, +} + +impl StatusColor { + fn to_comfy(self) -> Color { + match self { + StatusColor::White => Color::White, + StatusColor::Green => Color::Green, + StatusColor::Yellow => Color::Yellow, + StatusColor::Red => Color::Red, + } } } -struct Cell { +/// Plain text + which color it should render in. +#[derive(Clone)] +struct StatusCell { text: String, - style: Style, + color: StatusColor, } -fn local_state_cell(state: &LocalState) -> Cell { +fn local_state_cell(state: &LocalState) -> StatusCell { match state { - LocalState::Clean => Cell { + LocalState::Clean => StatusCell { text: format!("{} clean", glyphs::ICON_CLEAN), - style: color::green(), + color: StatusColor::Green, }, - LocalState::Dirty(n) => Cell { + LocalState::Dirty(n) => StatusCell { text: format!("{} dirty ({n})", glyphs::ICON_DIRTY), - style: color::yellow(), + color: StatusColor::Yellow, }, } } -fn not_present_local_cell() -> Cell { - Cell { +fn not_present_local_cell() -> StatusCell { + StatusCell { text: format!("{} MISSING", glyphs::ICON_MISSING), - style: color::red(), + color: StatusColor::Red, } } -fn sync_state_cell(state: &SyncState) -> Cell { +fn sync_state_cell(state: &SyncState) -> StatusCell { match state { - SyncState::InSync => Cell { + SyncState::InSync => StatusCell { text: format!("{} in-sync", glyphs::ICON_IN_SYNC), - style: color::green(), + color: StatusColor::Green, }, - SyncState::Ahead => Cell { + SyncState::Ahead => StatusCell { text: format!("{} ahead", glyphs::ICON_AHEAD), - style: color::yellow(), + color: StatusColor::Yellow, }, - SyncState::Behind => Cell { + SyncState::Behind => StatusCell { text: format!("{} behind", glyphs::ICON_BEHIND), - style: color::yellow(), + color: StatusColor::Yellow, }, - SyncState::Diverged => Cell { + SyncState::Diverged => StatusCell { text: format!("{} diverged", glyphs::ICON_DIVERGED), - style: color::red(), + color: StatusColor::Red, }, - SyncState::Missing => Cell { + SyncState::Missing => StatusCell { text: format!("{} MISSING", glyphs::ICON_MISSING), - style: color::red(), + color: StatusColor::Red, }, } } +/// One fully laid-out line of the table body, still split into columns. +/// `repo_col`/`local_cell` are `Some` only on a repo-block's first row. struct Row { repo_col: Option, connector_left: &'static str, - local_cell: Option, + local_cell: Option, connector_mid: &'static str, - sync_cell: Cell, + sync_cell: StatusCell, connector_right: &'static str, - remote_cell: Cell, + remote_cell: StatusCell, } pub fn render_table(entries: &[(Repo, RepoStatus)]) -> String { let rows = build_rows(entries); - let widths = compute_widths(&rows); - draw(&rows, &widths) -} - -struct Widths { - repo: usize, - local: usize, - sync: usize, - remote: usize, + draw(&rows) } fn build_rows(entries: &[(Repo, RepoStatus)]) -> Vec { @@ -134,17 +126,17 @@ fn build_rows(entries: &[(Repo, RepoStatus)]) -> Vec { if remote_names.is_empty() { rows.push(Row { repo_col: Some(repo_name), - connector_left: glyphs::CONN_ROOT, + connector_left: glyphs::CONN_STRAIGHT, local_cell: Some(not_present_local_cell()), connector_mid: "", - sync_cell: Cell { + sync_cell: StatusCell { text: String::new(), - style: color::content(), + color: StatusColor::White, }, connector_right: "", - remote_cell: Cell { + remote_cell: StatusCell { text: String::new(), - style: color::content(), + color: StatusColor::White, }, }); continue; @@ -156,9 +148,9 @@ fn build_rows(entries: &[(Repo, RepoStatus)]) -> Vec { Some(not_present_local_cell()), remote_names.into_iter().map(|name| { ( - Cell { + StatusCell { text: format!("{} -", glyphs::ICON_MISSING), - style: color::red(), + color: StatusColor::Red, }, truncate(name, REMOTE_NAME_MAX), ) @@ -187,26 +179,20 @@ fn build_rows(entries: &[(Repo, RepoStatus)]) -> Vec { fn push_repo_block( rows: &mut Vec, repo_name: String, - local_cell: Option, - remotes: impl ExactSizeIterator, + local_cell: Option, + remotes: impl ExactSizeIterator, ) { let n = remotes.len(); for (i, (sync_cell, remote_name)) in remotes.enumerate() { let is_first = i == 0; let is_last = i == n - 1; - let connector_left = if is_first { - if n == 1 { - glyphs::CONN_STRAIGHT - } else { - glyphs::CONN_BRANCH - } - } else { - "" - }; + let connector_left = if is_first { glyphs::CONN_ROOT } else { "" }; let connector_mid = if n == 1 { - "" + glyphs::CONN_STRAIGHT + } else if is_first { + glyphs::CONN_BRANCH } else if is_last { glyphs::CONN_LAST } else { @@ -224,204 +210,43 @@ fn push_repo_block( connector_mid, sync_cell, connector_right: glyphs::CONN_STRAIGHT, - remote_cell: Cell { + remote_cell: StatusCell { text: remote_name, - style: color::content(), + color: StatusColor::White, }, }); } } -impl Clone for Cell { - fn clone(&self) -> Self { - Cell { - text: self.text.clone(), - style: self.style.clone(), - } - } -} - -fn compute_widths(rows: &[Row]) -> Widths { - let header_repo = measure_text_width(&format!("{} repo", glyphs::COL_REPO)); - let header_local = measure_text_width(&format!("{} local", glyphs::COL_LOCAL)); - let header_sync = measure_text_width(&format!("{} sync", glyphs::COL_SYNC)); - let header_remote = measure_text_width(&format!("{} remote", glyphs::COL_REMOTE)); - - let mut w = Widths { - repo: header_repo, - local: header_local, - sync: header_sync, - remote: header_remote, - }; +fn draw(rows: &[Row]) -> String { + let mut table = Table::new(); + table + .load_style(UTF8_FULL) + .set_content_arrangement(ContentArrangement::Dynamic) + .set_header(vec![ + Cell::new(format!("{} repo", glyphs::COL_REPO)), + Cell::new(""), + Cell::new(format!("{} local", glyphs::COL_LOCAL)), + Cell::new(""), + Cell::new(format!("{} sync", glyphs::COL_SYNC)), + Cell::new(""), + Cell::new(format!("{} remote", glyphs::COL_REMOTE)), + ]); for row in rows { - if let Some(name) = &row.repo_col { - w.repo = w.repo.max(measure_text_width(name)); - } - if let Some(cell) = &row.local_cell { - w.local = w.local.max(measure_text_width(&cell.text)); - } - w.sync = w.sync.max(measure_text_width(&row.sync_cell.text)); - w.remote = w.remote.max(measure_text_width(&row.remote_cell.text)); - } - - w -} - -fn pad(s: &str, width: usize) -> String { - let visible = measure_text_width(s); - if visible >= width { - s.to_string() - } else { - format!("{s}{}", " ".repeat(width - visible)) - } -} - -fn draw(rows: &[Row], w: &Widths) -> String { - let frame = color::frame(); - - let header_plain = format_header_plain(w); - let row_plains: Vec = rows.iter().map(|r| format_row_plain(r, w)).collect(); - - let inner_width = std::iter::once(&header_plain) - .chain(row_plains.iter()) - .map(|s| measure_text_width(s)) - .max() - .unwrap_or(0); - - let mut out = String::new(); - - out.push_str( - &frame - .apply_to(format!("┌{}┐", "─".repeat(inner_width + 2))) - .to_string(), - ); - out.push('\n'); - - out.push_str(&frame.apply_to("│ ").to_string()); - out.push_str(&pad(&colorize_header(w), inner_width)); - out.push_str(&frame.apply_to(" │").to_string()); - out.push('\n'); - - out.push_str( - &frame - .apply_to(format!("├{}┤", "─".repeat(inner_width + 2))) - .to_string(), - ); - out.push('\n'); - - for (row, plain) in rows.iter().zip(row_plains.iter()) { - let styled = colorize_row(row, w); - let pad_amount = inner_width.saturating_sub(measure_text_width(plain)); - - out.push_str(&frame.apply_to("│ ").to_string()); - out.push_str(&styled); - out.push_str(&" ".repeat(pad_amount)); - out.push_str(&frame.apply_to(" │").to_string()); - out.push('\n'); + table.add_row(vec![ + Cell::new(row.repo_col.clone().unwrap_or_default()), + Cell::new(row.connector_left).fg(Color::Cyan), + match &row.local_cell { + Some(c) => Cell::new(format!("[{}]", c.text)).fg(c.color.to_comfy()), + None => Cell::new(""), + }, + Cell::new(row.connector_mid).fg(Color::Cyan), + Cell::new(format!("[{}]", row.sync_cell.text)).fg(row.sync_cell.color.to_comfy()), + Cell::new(row.connector_right).fg(Color::Cyan), + Cell::new(format!("[{}]", row.remote_cell.text)).fg(row.remote_cell.color.to_comfy()), + ]); } - out.push_str( - &frame - .apply_to(format!("└{}┘", "─".repeat(inner_width + 2))) - .to_string(), - ); - out -} - -fn format_header_plain(w: &Widths) -> String { - format!( - "{} {}{} {} {}{} {} {}{} {} {}", - pad(&format!("{} repo", glyphs::COL_REPO), w.repo), - pad("", 4), - pad(&format!("{} local", glyphs::COL_LOCAL), w.local), - pad("", 4), - pad(&format!("{} sync", glyphs::COL_SYNC), w.sync), - pad("", 4), - pad(&format!("{} remote", glyphs::COL_REMOTE), w.remote), - "", - "", - "", - "", - ) -} - -fn format_row_plain(row: &Row, w: &Widths) -> String { - format!( - "{} {} [{}] {} [{}] {} [{}]", - pad(row.repo_col.as_deref().unwrap_or(""), w.repo), - pad(row.connector_left, 4), - pad( - row.local_cell - .as_ref() - .map(|c| c.text.as_str()) - .unwrap_or(""), - w.local - ), - pad(row.connector_mid, 4), - pad(&row.sync_cell.text, w.sync), - pad(row.connector_right, 4), - pad(&row.remote_cell.text, w.remote), - ) -} - -fn colorize_header(w: &Widths) -> String { - let content = color::content(); - format!( - "{} {} {} {} {} {} {} {}", - content.apply_to(pad(&format!("{} repo", glyphs::COL_REPO), w.repo)), - " ".repeat(4), - content.apply_to(pad(&format!("{} local", glyphs::COL_LOCAL), w.local)), - " ".repeat(4), - content.apply_to(pad(&format!("{} sync", glyphs::COL_SYNC), w.sync)), - " ".repeat(4), - content.apply_to(pad(&format!("{} remote", glyphs::COL_REMOTE), w.remote)), - "", - ) -} - -fn colorize_row(row: &Row, w: &Widths) -> String { - let content = color::content(); - let conn = color::connector(); - - let repo_field = content.apply_to(pad(row.repo_col.as_deref().unwrap_or(""), w.repo)); - - let local_field = match &row.local_cell { - Some(cell) => format!( - "{}{}{}", - content.apply_to("["), - cell.style.apply_to(pad(&cell.text, w.local)), - content.apply_to("]"), - ), - None => " ".repeat(w.local + 2), - }; - - let sync_field = format!( - "{}{}{}", - content.apply_to("["), - row.sync_cell - .style - .apply_to(pad(&row.sync_cell.text, w.sync)), - content.apply_to("]"), - ); - - let remote_field = format!( - "{}{}{}", - content.apply_to("["), - row.remote_cell - .style - .apply_to(pad(&row.remote_cell.text, w.remote)), - content.apply_to("]"), - ); - - format!( - "{} {} {} {} {} {} {}", - repo_field, - conn.apply_to(pad(row.connector_left, 4)), - local_field, - conn.apply_to(pad(row.connector_mid, 4)), - sync_field, - conn.apply_to(pad(row.connector_right, 4)), - remote_field, - ) + table.to_string() }