| 1 | //! Host contracts around the shared native posture renderer. |
| 2 | |
| 3 | use codewhale_palette::{ChromeInk, UI_THEME, UiTheme}; |
| 4 | use ratatui::{ |
| 5 | buffer::Buffer, |
| 6 | layout::Rect, |
| 7 | style::{Color, Modifier, Style}, |
| 8 | }; |
| 9 | |
| 10 | use super::{TidelineFooter, render_tideline_footer}; |
| 11 | |
| 12 | fn custom_theme() -> UiTheme { |
| 13 | UiTheme { |
| 14 | status_working: Color::Rgb(1, 2, 3), |
| 15 | permission_ask: Color::Rgb(2, 3, 4), |
| 16 | permission_auto_review: Color::Rgb(3, 4, 5), |
| 17 | permission_full_access: Color::Rgb(4, 5, 6), |
| 18 | accent_action: Color::Rgb(5, 6, 7), |
| 19 | warning: Color::Rgb(6, 7, 8), |
| 20 | accent_primary: Color::Rgb(7, 8, 9), |
| 21 | text_soft: Color::Rgb(8, 9, 10), |
| 22 | text_muted: Color::Rgb(9, 10, 11), |
| 23 | text_hint: Color::Rgb(10, 11, 12), |
| 24 | text_dim: Color::Rgb(11, 12, 13), |
| 25 | error_fg: Color::Rgb(12, 13, 14), |
| 26 | ..UI_THEME |
| 27 | } |
| 28 | } |
| 29 | |
| 30 | #[test] |
| 31 | fn posture_retains_every_host_ink_including_the_pinned_right_fact() { |
| 32 | let theme = custom_theme(); |
| 33 | for ink in [ |
| 34 | ChromeInk::Outcome, |
| 35 | ChromeInk::PermissionAsk, |
| 36 | ChromeInk::PermissionAutoReview, |
| 37 | ChromeInk::PermissionFullAccess, |
| 38 | ChromeInk::Waiting, |
| 39 | ChromeInk::Attention, |
| 40 | ChromeInk::Active, |
| 41 | ChromeInk::PolicyAct, |
| 42 | ChromeInk::PolicyPlan, |
| 43 | ChromeInk::PolicyOperate, |
| 44 | ChromeInk::Identity, |
| 45 | ChromeInk::Info, |
| 46 | ChromeInk::MetadataValue, |
| 47 | ChromeInk::Metadata, |
| 48 | ChromeInk::MetadataHint, |
| 49 | ChromeInk::MetadataDim, |
| 50 | ChromeInk::Failure, |
| 51 | ] { |
| 52 | let counts = [("C (Ctrl+])".into(), ink)]; |
| 53 | let footer = TidelineFooter::new(&theme, ("P", ink)) |
| 54 | .permission_key(Some("K")) |
| 55 | .mode_chip(Some(("M", ink))) |
| 56 | .mode_key(Some("Tab")) |
| 57 | .turn_clock(Some(("W", ink))) |
| 58 | .counts(&counts) |
| 59 | .session_clock(Some(("S", ink))) |
| 60 | .hint(Some(("H", ink))) |
| 61 | .right(Some(("R", ink))); |
| 62 | let area = Rect::new(3, 2, 100, 2); |
| 63 | let mut buf = Buffer::empty(Rect::new(0, 0, 110, 5)); |
| 64 | buf.set_style( |
| 65 | buf.area, |
| 66 | Style::default() |
| 67 | .fg(crate::tui::infoline::source_theme(false) |
| 68 | .color(codewhale_ratatui::Role::Primary) |
| 69 | .unwrap()) |
| 70 | .bg(Color::Rgb(30, 40, 50)) |
| 71 | .add_modifier(Modifier::ITALIC), |
| 72 | ); |
| 73 | let before = buf.clone(); |
| 74 | let targets = render_tideline_footer(area, &mut buf, &footer); |
| 75 | let column = |symbol: &str| { |
| 76 | (area.left()..area.right()) |
| 77 | .find(|x| buf[(*x, area.y)].symbol() == symbol) |
| 78 | .unwrap_or_else(|| panic!("{ink:?}: {symbol} did not paint")) |
| 79 | }; |
| 80 | for symbol in ["P", "M", "W", "C", "S", "H", "R"] { |
| 81 | let cell = &buf[(column(symbol), area.y)]; |
| 82 | assert_eq!(cell.fg, ink.color(&theme), "{ink:?}: {symbol}"); |
| 83 | assert_eq!(cell.bg, Color::Rgb(30, 40, 50)); |
| 84 | assert!(cell.modifier.contains(Modifier::ITALIC)); |
| 85 | assert_eq!(cell.modifier.contains(Modifier::BOLD), symbol == "P"); |
| 86 | } |
| 87 | assert_eq!(buf[(column("K"), area.y)].fg, theme.text_hint); |
| 88 | assert_eq!(buf[(column("M") + 2, area.y)].fg, theme.text_hint); |
| 89 | assert_eq!(targets, [(0, Rect::new(column("C"), area.y, 10, 1))]); |
| 90 | assert_eq!(buf[(column("C") + 2, area.y)].fg, theme.text_hint); |
| 91 | assert_eq!(buf[(column("M") - 1, area.y)].fg, theme.text_dim); |
| 92 | for position in [(3, 2), (102, 2), (60, 2), (3, 3), (109, 4)] { |
| 93 | assert_eq!(buf[position], before[position], "{ink:?}: {position:?}"); |
| 94 | } |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | #[test] |
| 99 | fn posture_counts_keep_cjk_combining_and_ascii_pointer_geometry_at_every_width() { |
| 100 | let counts = [ |
| 101 | ("模型e\u{301}↓\0 (Ctrl+])".into(), ChromeInk::Active), |
| 102 | ("作業 2".into(), ChromeInk::Waiting), |
| 103 | ]; |
| 104 | for ascii in [false, true] { |
| 105 | for width in 0..=45 { |
| 106 | let area = Rect::new(3, 2, width, 2); |
| 107 | let mut buf = Buffer::empty(Rect::new(0, 0, 55, 5)); |
| 108 | buf.set_style(buf.area, Style::default().bg(Color::Rgb(30, 40, 50))); |
| 109 | let before = buf.clone(); |
| 110 | let footer = TidelineFooter::new(&UI_THEME, ("許可\u{202e}", ChromeInk::PermissionAsk)) |
| 111 | .counts(&counts) |
| 112 | .ascii_safe(ascii); |
| 113 | let targets = render_tideline_footer(area, &mut buf, &footer); |
| 114 | assert_eq!(buf[(2, 2)], before[(2, 2)]); |
| 115 | assert_eq!(buf[(area.right(), 2)], before[(area.right(), 2)]); |
| 116 | assert_eq!(buf[(3, 3)], before[(3, 3)], "one reserved row"); |
| 117 | if width >= 34 { |
| 118 | assert_eq!( |
| 119 | targets, |
| 120 | [(0, Rect::new(13, 2, 15, 1)), (1, Rect::new(30, 2, 6, 1))] |
| 121 | ); |
| 122 | for (x, symbol) in [ |
| 123 | (13, "模"), |
| 124 | (15, "型"), |
| 125 | (17, "e\u{301}"), |
| 126 | (20, "("), |
| 127 | (30, "作"), |
| 128 | (32, "業"), |
| 129 | (35, "2"), |
| 130 | ] { |
| 131 | assert_eq!(buf[(x, 2)].symbol(), symbol, "{width}: {symbol}"); |
| 132 | } |
| 133 | assert_eq!(buf[(18, 2)].symbol(), if ascii { "v" } else { "↓" }); |
| 134 | } else { |
| 135 | assert!(targets.is_empty(), "whole count group sheds at {width}"); |
| 136 | } |
| 137 | if width >= 8 { |
| 138 | assert_eq!(buf[(4, 2)].symbol(), if ascii { "." } else { "●" }); |
| 139 | assert_eq!(buf[(6, 2)].symbol(), "許"); |
| 140 | assert_eq!(buf[(8, 2)].symbol(), "可"); |
| 141 | } |
| 142 | } |
| 143 | } |
| 144 | } |
| 145 | |
| 146 | #[test] |
| 147 | fn posture_cap_warning_keeps_its_live_attention_ink_and_priority() { |
| 148 | let theme = custom_theme(); |
| 149 | for percent in [79, 80, 100, 255] { |
| 150 | let area = Rect::new(0, 0, 100, 1); |
| 151 | let mut buf = Buffer::empty(area); |
| 152 | let footer = TidelineFooter::new(&theme, ("full access", ChromeInk::PermissionFullAccess)) |
| 153 | .hint(Some(("hint", ChromeInk::Failure))) |
| 154 | .context_percent(percent) |
| 155 | .right(Some(("failed", ChromeInk::Failure))); |
| 156 | render_tideline_footer(area, &mut buf, &footer); |
| 157 | let row: String = buf.content().iter().map(|cell| cell.symbol()).collect(); |
| 158 | if percent < 80 { |
| 159 | assert!(row.contains("hint")); |
| 160 | assert!(!row.contains("surface soon")); |
| 161 | } else { |
| 162 | assert!(!row.contains("hint")); |
| 163 | assert!(row.contains("▲ surface soon — /compact")); |
| 164 | let warning = (0..area.width) |
| 165 | .find(|x| buf[(*x, 0)].symbol() == "▲") |
| 166 | .unwrap(); |
| 167 | assert_eq!(buf[(warning, 0)].fg, theme.warning); |
| 168 | } |
| 169 | assert_eq!(buf[(1, 0)].fg, theme.permission_full_access); |
| 170 | assert_eq!(buf[(area.right() - 2, 0)].fg, theme.error_fg); |
| 171 | } |
| 172 | } |
| 173 |