§9 acceptance: grep-zero sweep — every internal x.ai/grok identifier renamed

The PRD's first acceptance gate now holds: grep -RinE '\bx\.ai\b|grok'
crates/ --include='*.rs' → 0 matches (exempt: NOTICE and third-party
license archives, README provenance, and the required 'Based on Grok
Build Open Source' attribution, now sourced from version_attribution.txt).

Wire-visible renames (both sides in this repo, changed in lockstep):
- Auth method id 'grok.com' → 'kimi-code' (AuthMethodKind::KimiCode).
- Every x.ai/* and _x.ai/* ACP ext method and meta key → kigi/* /
  _kigi/* (~200 names; grokShell → kigiShell). Session-file replay keeps
  a read-side alias for the legacy '_x.ai/session/update' method so
  existing updates.jsonl histories load; writes emit only the new name
  (both directions test-pinned).
- Agent types grok-build* → kigi* with a documented legacy-prefix alias
  at resolution time so persisted sessions keep resolving.
- ToolNamespace/BuiltinAgentName GrokBuild* → Kigi* (wire snake_case
  kigi/kigi_concise/kigi_hashline; schema regenerated); grok_build
  implementation dirs renamed to kigi*.
- x-grok-* headers → x-kigi-*, __GROK_* sentinels → __KIGI_*, themes
  grokday/groknight → kigiday/kiginight (old persisted values fall back
  to the default theme), web_fetch allowlist xAI hosts → kimi.com +
  moonshot platforms, changelog CDN → this repo, grok-build changelog
  archives deleted.
- BYOK default endpoint removed: [endpoints] api_base_url is now truly
  optional with NO default — consumers fail fast with the flag name when
  unset (no silent x.ai egress). Mock harnesses inject it explicitly.
- System-prompt identity fixed: 'released by xAI' → 'an unofficial
  community CLI for Kimi' (template + regenerated encrypted form).

Also repaired pre-existing grok-era test debt found by the sweep: the
stale trace_classify default-model pin, the grok-pager UA label test,
pty-harness stale-binary reuse and non-hermetic moonshot routing (a PTY
test could previously reach the real api.moonshot.cn), and the outdated
oauth fixture scope key.

Gates: §9 grep 0; fmt clean; workspace check/clippy 0/0 (-D warnings);
FULL cargo test --workspace: 234 suites, 21,961 passed, 0 failed;
deny advisories ok.
This commit is contained in:
2026-07-18 02:48:46 -04:00
parent 86e3724310
commit 6f31415ed6
1056 changed files with 8410 additions and 18307 deletions
@@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>name</key>
<string>Grok Day</string>
<string>Kigi Day</string>
<key>settings</key>
<array>
<dict>
@@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>name</key>
<string>Grok Night</string>
<string>Kigi Night</string>
<key>settings</key>
<array>
<dict>
@@ -1715,7 +1715,7 @@ fn parse_hex_color(hex: &str) -> Result<Color, String> {
}
fn lookup_named_color(name: &str) -> Result<Color, String> {
// Named colors use the GrokNight RGB palette. They are quantized via
// Named colors use the KigiNight RGB palette. They are quantized via
// `parse_color_string` → `quantize()` to match the terminal's capabilities.
let color = match name.to_uppercase().as_str() {
// Background colors
@@ -1787,10 +1787,10 @@ impl RawAppearanceConfig {
let toml_str = toml_edit::ser::to_string_pretty(&config).expect("serialize default");
let mut doc: DocumentMut = toml_str.parse().expect("parse toml");
let pager_path = crate::util::display_user_grok_path("pager.toml");
let pager_path = crate::util::display_user_kigi_path("pager.toml");
let header = format!(
"\
# Grok Pager Appearance Configuration ({pager_path})
# Kigi Pager Appearance Configuration ({pager_path})
# Every value below is a commented-out built-in default: uncomment a line and
# save to override it. Values left commented track future default changes.
# Delete the file to regenerate this template.
@@ -1935,7 +1935,7 @@ pub fn persist_respect_manual_folds(enabled: bool) -> std::io::Result<()> {
if kigi_config::user_kigi_home().is_none() {
return Err(Error::new(
ErrorKind::NotFound,
"no user grok home resolved; refusing to write a cwd-relative pager.toml \
"no user kigi home resolved; refusing to write a cwd-relative pager.toml \
that startup would never read",
));
}
@@ -27,18 +27,18 @@ fn is_container_no_display() -> bool {
/// Cached result of the "an upstream OSC 52 sink is capturing our output" check.
///
/// `grok wrap` runs a command inside a local PTY, scans its output for OSC 52
/// `kigi wrap` runs a command inside a local PTY, scans its output for OSC 52
/// clipboard sequences, and writes their payload to the *real* (local) system
/// clipboard (see `kigi-tui`'s `pty_wrap` module). It advertises this to
/// the wrapped program via an environment variable so the
/// inner `grok` knows its OSC 52 writes are reliably intercepted and copied,
/// inner `kigi` knows its OSC 52 writes are reliably intercepted and copied,
/// even when the inner terminal brand is misdetected (e.g. over SSH, where only
/// `TERM` propagates and Apple Terminal / unknown brands look OSC-52-incapable).
///
/// Two names are accepted: the canonical `KIGI_OSC52_SINK` (inherited by local
/// children) and the `LC_`-prefixed `LC_KIGI_OSC52_SINK`, which the default
/// OpenSSH client/server configs forward (`SendEnv LANG LC_*` /
/// `AcceptEnv LANG LC_*`) so the signal survives the hop into a remote `grok`.
/// `AcceptEnv LANG LC_*`) so the signal survives the hop into a remote `kigi`.
pub fn osc52_sink_active() -> bool {
static SINK: OnceLock<bool> = OnceLock::new();
*SINK.get_or_init(|| {
@@ -124,7 +124,7 @@ pub fn resolve_clipboard_route(ctx: &TerminalContext) -> ClipboardRoute {
// Linux: always emit OSC 52 as a safety net. This matches other
// terminal agent CLIs which emit OSC 52 on every copy.
// macOS/Windows: only in tmux/SSH/container contexts, or when an
// upstream `grok wrap` sink is capturing our output and will forward
// upstream `kigi wrap` sink is capturing our output and will forward
// the sequence to the real clipboard.
osc52: cfg!(target_os = "linux")
|| is_tmux
@@ -37,7 +37,7 @@ pub(crate) fn trusted_native(
/// Normally this requires the detected terminal brand to natively apply OSC 52
/// to the system pasteboard (fail closed). Two overrides widen the brand gate:
///
/// - `osc52_sink`: when `grok wrap` is capturing this process's output (see
/// - `osc52_sink`: when `kigi wrap` is capturing this process's output (see
/// [`super::osc52_sink_active`]) the escape sequence is intercepted upstream
/// and copied to the *local* clipboard regardless of the (often misdetected,
/// e.g. over SSH) inner terminal brand, so the copy is trusted.
@@ -550,7 +550,7 @@ mod tests {
);
}
// `grok wrap` sink: a brand that does NOT natively support OSC 52 (the
// `kigi wrap` sink: a brand that does NOT natively support OSC 52 (the
// common SSH case where the inner terminal is misdetected as Vte/Unknown)
// is still trusted when an upstream OSC 52 sink is capturing our output.
#[test]
@@ -665,7 +665,7 @@ mod tests {
}
// Unknown brand over SSH (not container) keeps failing closed — the
// container override is deliberately narrow; `grok wrap` is the SSH path.
// container override is deliberately narrow; `kigi wrap` is the SSH path.
#[test]
fn ssh_unknown_brand_osc_only_still_fails() {
let l = legs(true, false, false, false, true, "");
@@ -666,7 +666,7 @@ mod tests {
#[test]
#[ignore]
fn dump_frames_for_visual_inspection() {
let dir = std::env::temp_dir().join("grok-gboom-frames");
let dir = std::env::temp_dir().join("kigi-gboom-frames");
std::fs::create_dir_all(&dir).unwrap();
let dump = |name: &str, state: &mut GboomState| {
state.sim_gen += 1;
@@ -554,7 +554,7 @@ fn decide_legacy_windows_console(host: HostOs, brand: TerminalName) -> bool {
| TerminalName::Alacritty
| TerminalName::Ghostty
| TerminalName::Rio
| TerminalName::GrokDesktop
| TerminalName::KigiDesktop
)
}
@@ -797,7 +797,7 @@ mod tests {
TerminalName::Alacritty,
TerminalName::Ghostty,
TerminalName::Rio,
TerminalName::GrokDesktop,
TerminalName::KigiDesktop,
] {
assert!(!decide_legacy_windows_console(HostOs::Windows, brand));
}
@@ -1,6 +1,6 @@
//! Shared URL-opening and scheme validation utilities.
//!
//! Extracted from the `OpenSupergrokUrl` dispatch handler so that any
//! Extracted from the `OpenSubscriptionUrl` dispatch handler so that any
//! code path (keyboard navigation, mouse click, action dispatch) can
//! open a link safely without duplicating platform-specific logic.
@@ -217,7 +217,7 @@ mod tests {
#[test]
fn open_path_command_passes_path_as_a_single_arg() {
// Path with spaces must be one argument, never shell-interpolated.
let path = std::path::Path::new("/tmp/grok session/image 1.jpg");
let path = std::path::Path::new("/tmp/kigi session/image 1.jpg");
let command = build_open_path_command(path);
let args: Vec<_> = command.get_args().map(|a| a.to_os_string()).collect();
assert!(args.contains(&path.as_os_str().to_os_string()));
@@ -453,7 +453,7 @@ pub fn extract_poster_frame(path: &std::path::Path) -> Option<(Vec<u8>, u32, u32
/// Create a unique temp directory path for frame extraction.
fn make_temp_dir() -> PathBuf {
std::env::temp_dir().join(format!(
"grok-video-{}-{}",
"kigi-video-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
@@ -2764,7 +2764,7 @@ mod tests {
// time would still satisfy this assertion because the
// anchor gate now rejects single-slash strings *before* any
// URL parsing runs.
let pasted = "file:/tmp/should_not_exist_abc_grok_pager.png";
let pasted = "file:/tmp/should_not_exist_abc_kigi_pager.png";
assert!(try_read_image_from_path(pasted).is_none());
}
@@ -3583,7 +3583,7 @@ mod tests {
// user might just have typed `/etc/passwd` as part of prose.
// (The image branch already filters on file existence; this
// mirrors that for the non-image branch.)
let entries = dropped_paths("/tmp/definitely_does_not_exist_xyz_grok_pager.txt");
let entries = dropped_paths("/tmp/definitely_does_not_exist_xyz_kigi_pager.txt");
assert!(
entries.is_empty(),
"bare nonexistent path must fall through to prose; got {entries:?}"
@@ -91,7 +91,7 @@ fn link_finder() -> &'static LinkFinder {
}
/// One path segment without spaces (`main.rs`, `.kigi`, `@scope`). Leading `.`
/// matches dot-directories and `%` matches percent-encoded segments — grok
/// matches dot-directories and `%` matches percent-encoded segments — kigi
/// session media lives under `~/.kigi/sessions/%2F…/images/1.jpg`.
const PATH_SEGMENT: &str = r"[a-zA-Z0-9_@.%][a-zA-Z0-9._+@%\-]*";
@@ -565,7 +565,7 @@ mod tests {
std::fs::write(dir.path().join("images/1.jpg"), b"x").unwrap();
let media = vec![dir.path().join("images/1.jpg")];
assert!(local_link_to_file_url("https://x.ai", &media).is_none());
assert!(local_link_to_file_url("https://kimi.com", &media).is_none());
assert!(local_link_to_file_url("mailto:a@b.c", &media).is_none());
assert!(local_link_to_file_url("#section", &media).is_none());
// Relative path that isn't a known generated media file.
@@ -786,13 +786,14 @@ mod tests {
#[test]
fn scan_content_x_offset_applied() {
let line = make_line("https://x.ai");
let line = make_line("https://kimi.com");
let mut overlay = LinkOverlay::new();
scan_unjoined(std::iter::once((0, &line)), 10, &[], &mut overlay);
assert_eq!(overlay.links().len(), 1);
assert_eq!(overlay.links()[0].col_start, 10);
assert_eq!(overlay.links()[0].col_end, 10 + 12);
// 16 == "https://kimi.com".len()
assert_eq!(overlay.links()[0].col_end, 10 + 16);
}
// ── File path detection ──
@@ -856,7 +857,7 @@ mod tests {
}
#[test]
fn scan_detects_grok_session_media_path() {
fn scan_detects_kigi_session_media_path() {
// Dot-directory (`.kigi`), percent-encoded session segment, and a
// trailing sentence period — the shape of media-tool output prose.
let line = make_line("Saved to /Users/alice/.kigi/sessions/%2Fabc/00000000/images/1.jpg.");
@@ -1097,12 +1098,12 @@ mod tests {
#[test]
fn scan_file_path_with_dots_and_hyphens() {
let line = make_line("Reading /tmp/grok-impl-summary.md now.");
let line = make_line("Reading /tmp/kigi-impl-summary.md now.");
let mut overlay = LinkOverlay::new();
scan_unjoined(std::iter::once((0, &line)), 0, &[], &mut overlay);
assert_eq!(overlay.links().len(), 1);
assert_eq!(&*overlay.links()[0].url, "file:///tmp/grok-impl-summary.md");
assert_eq!(&*overlay.links()[0].url, "file:///tmp/kigi-impl-summary.md");
}
#[test]
@@ -1188,7 +1189,7 @@ mod tests {
#[test]
fn scan_detects_tilde_file_path() {
// The user's example: a `~/Desktop/…md` path in agent output.
let raw = "~/Desktop/grok-pager-retention-findings-2026-06-06.md";
let raw = "~/Desktop/kigi-pager-retention-findings-2026-06-06.md";
// Skip when no home directory is resolvable (e.g. minimal sandbox):
// the tilde stays unexpanded and cannot form an absolute file URL.
let Ok(expected) = url::Url::from_file_path(shellexpand::tilde(raw).as_ref()) else {
@@ -1,8 +1,8 @@
//! Syntax highlighting initialization.
//!
//! Provides lazily-initialized `Syntect` instances for code highlighting.
//! Dark themes (GrokNight, TokyoNight) share `grok-night.tmTheme`;
//! GrokDay uses `grok-day.tmTheme` with deepened colors for light backgrounds.
//! Dark themes (KigiNight, TokyoNight) share `kigi-night.tmTheme`;
//! KigiDay uses `kigi-day.tmTheme` with deepened colors for light backgrounds.
use std::sync::OnceLock;
@@ -10,9 +10,9 @@ pub use kigi_markdown::Syntect;
use crate::theme::ThemeKind;
static SYNTECT_GROKNIGHT: OnceLock<Syntect> = OnceLock::new();
static SYNTECT_KIGINIGHT: OnceLock<Syntect> = OnceLock::new();
static SYNTECT_TOKYONIGHT: OnceLock<Syntect> = OnceLock::new();
static SYNTECT_GROKDAY: OnceLock<Syntect> = OnceLock::new();
static SYNTECT_KIGIDAY: OnceLock<Syntect> = OnceLock::new();
/// Convert syntect style to ratatui foreground-only style, quantized for terminal color support.
pub fn syntect_to_ratatui_fg(style: syntect::highlighting::Style) -> ratatui::style::Style {
@@ -66,14 +66,14 @@ pub fn highlight_line(
/// Returns the syntect instance matching the active theme.
pub fn get_syntect() -> &'static Syntect {
match crate::theme::Theme::current_kind() {
ThemeKind::GrokNight
ThemeKind::KigiNight
| ThemeKind::RosePineMoon
| ThemeKind::OscuraMidnight
| ThemeKind::Auto => SYNTECT_GROKNIGHT
.get_or_init(|| Syntect::new(include_bytes!("../assets/grok-night.tmTheme"))),
| ThemeKind::Auto => SYNTECT_KIGINIGHT
.get_or_init(|| Syntect::new(include_bytes!("../assets/kigi-night.tmTheme"))),
ThemeKind::TokyoNight => SYNTECT_TOKYONIGHT
.get_or_init(|| Syntect::new(include_bytes!("../assets/tokyo-night.tmTheme"))),
ThemeKind::GrokDay => SYNTECT_GROKDAY
.get_or_init(|| Syntect::new(include_bytes!("../assets/grok-day.tmTheme"))),
ThemeKind::KigiDay => SYNTECT_KIGIDAY
.get_or_init(|| Syntect::new(include_bytes!("../assets/kigi-day.tmTheme"))),
}
}
@@ -1,4 +1,4 @@
//! Detects whether grok is running inside an editor's embedded `:terminal`
//! Detects whether kigi is running inside an editor's embedded `:terminal`
//! (Neovim/Vim `:terminal`, Emacs `vterm`).
//!
//! WHY this matters: inside an editor `:terminal` the *immediate* terminal
@@ -13,7 +13,7 @@ use std::collections::HashMap;
use super::env_get;
/// Which embedded editor `:terminal` grok is running inside.
/// Which embedded editor `:terminal` kigi is running inside.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum EmbeddedEditor {
/// Neovim `:terminal` (sets `NVIM`, or legacy `NVIM_LISTEN_ADDRESS`).
@@ -178,7 +178,7 @@ pub fn hyperlink_capabilities(brand: TerminalName) -> HyperlinkCapabilities {
native_plain_url_open: false,
},
// Electron app; behavior undocumented.
TerminalName::GrokDesktop => HyperlinkCapabilities {
TerminalName::KigiDesktop => HyperlinkCapabilities {
osc8: Unknown,
id_param: false,
scheme_filter: SchemeFilter::Standard,
@@ -54,7 +54,7 @@ static GRAPHICS_PROTOCOL: OnceLock<GraphicsProtocol> = OnceLock::new();
/// When set, scrollback inline-media overlays are forced **off** process-wide,
/// regardless of the terminal's graphics capability. The scrollback-native
/// minimal mode (`grok --minimal`) sets this once at startup: it never runs the
/// minimal mode (`kigi --minimal`) sets this once at startup: it never runs the
/// interactive draw loop that paints inline images, so committed media blocks
/// must always fall back to the `[Open …]` text affordance — and must not
/// reserve blank image rows. See [`set_inline_overlay_force_off`].
@@ -281,8 +281,8 @@ fn convert_via_sips(image_data: &[u8]) -> Option<Vec<u8>> {
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let src = tmp_dir.join(format!("grok-sips-{id}-{ts}.dat"));
let dst = tmp_dir.join(format!("grok-sips-{id}-{ts}.png"));
let src = tmp_dir.join(format!("kigi-sips-{id}-{ts}.dat"));
let dst = tmp_dir.join(format!("kigi-sips-{id}-{ts}.png"));
// Write source bytes to temp file.
let mut f = std::fs::File::create(&src).ok()?;
@@ -117,7 +117,7 @@ fn macos_capabilities(brand: TerminalName) -> KeyboardCapabilities {
// Apple Terminal: Cmd+Bsp captured by the window manager.
// Opt+Bsp and modified Enter are dropped; CG can rescue both.
TerminalName::AppleTerminal => (Unrecoverable, Dropped, Dropped),
TerminalName::GrokDesktop => (Unknown, Unknown, Unknown),
TerminalName::KigiDesktop => (Unknown, Unknown, Unknown),
// VTE-based terminals (incl. Terminator) on macOS are unusual;
// classify when we have evidence rather than guessing.
TerminalName::Vte | TerminalName::Terminator => (Unknown, Unknown, Unknown),
@@ -201,7 +201,7 @@ mod tests {
fn unknown_brands_skip_rescue() {
for brand in [
TerminalName::Unknown,
TerminalName::GrokDesktop,
TerminalName::KigiDesktop,
TerminalName::Vte,
TerminalName::JetBrains,
] {
@@ -129,9 +129,9 @@ pub enum TerminalName {
/// indistinguishable. All capabilities are conservative/Unknown.
#[strum(to_string = "JetBrains")]
JetBrains,
/// Grok Desktop (Electron app).
#[strum(to_string = "Grok Desktop")]
GrokDesktop,
/// Kigi Desktop (Electron app).
#[strum(to_string = "Kigi Desktop")]
KigiDesktop,
/// VTE-based terminal (GNOME Terminal, kgx/GNOME Console, Tilix, etc.).
#[strum(to_string = "VTE")]
Vte,
@@ -276,7 +276,7 @@ pub struct TerminalContext {
pub multiplexer: MultiplexerKind,
/// Whether Byobu is wrapping the session, and which backend it uses.
pub byobu: Option<ByobuBackend>,
/// Which embedded editor `:terminal` grok is running inside, if any.
/// Which embedded editor `:terminal` kigi is running inside, if any.
pub embedded_editor: Option<EmbeddedEditor>,
/// tmux client metadata (populated only when `multiplexer == Tmux`).
pub tmux_meta: TmuxClientMeta,
@@ -323,7 +323,7 @@ impl TerminalContext {
/// Whether an outer layer (embedded-editor :terminal or multiplexer) can
/// repaint our pane out of band, stranding rows until a full clear. A heal
/// keyed off this only fires when a FocusGained actually reaches grok, which
/// keyed off this only fires when a FocusGained actually reaches kigi, which
/// needs focus reporting enabled upstream (e.g. tmux `focus-events on`, off
/// by default).
pub fn repaints_pane_out_of_band(&self) -> bool {
@@ -645,7 +645,7 @@ fn detect_terminal_context() -> TerminalContext {
// per-pane vars don't survive) and over SSH (not forwarded), except
// brands with SSH-surviving markers (the VS Code family, and iTerm2
// via LC_TERMINAL). tmux -g global env is stale (reflects the server's
// first client, not the current one). Revisit when `grok ssh` can
// first client, not the current one). Revisit when `kigi ssh` can
// forward env vars.
let mut ctx = build_terminal_context_from_env(&env);
ctx.brand = refine_unknown_brand_for_host(ctx.brand, HostOs::current());
@@ -980,7 +980,7 @@ fn terminal_name_from_term_program(value: &str) -> Option<TerminalName> {
"rio" => Some(TerminalName::Rio),
"terminator" => Some(TerminalName::Terminator),
"zed" => Some(TerminalName::Zed),
"grokdesktop" => Some(TerminalName::GrokDesktop),
"kigidesktop" => Some(TerminalName::KigiDesktop),
"windowsterminal" => Some(TerminalName::WindowsTerminal),
"otty" => Some(TerminalName::Otty),
_ => None,
@@ -19,7 +19,7 @@ use super::system_appearance;
/// In-memory theme kind, encoded as a `u8` matching the
/// `ThemeKind` discriminants. Loaded from disk once at startup via
/// `load_from_disk()`, then kept in sync by `set()`.
static CURRENT: AtomicU8 = AtomicU8::new(ThemeKind::GrokNight as u8);
static CURRENT: AtomicU8 = AtomicU8::new(ThemeKind::KigiNight as u8);
static LOADED: AtomicBool = AtomicBool::new(false);
#[cfg(any(test, feature = "test-support"))]
static TEST_LOCK: Mutex<()> = Mutex::new(());
@@ -36,19 +36,19 @@ static AUTO_MODE: AtomicBool = AtomicBool::new(false);
static TERMINAL_NATIVE_LOCK: AtomicBool = AtomicBool::new(false);
/// Decode the u8 stored in `CURRENT` back to a `ThemeKind`. Falls
/// back to `GrokNight` if the byte is somehow out of range (which
/// back to `KigiNight` if the byte is somehow out of range (which
/// can't happen via `set` — the discriminant is always a valid
/// variant — but defends against a future variant addition that
/// forgot to extend this match).
fn theme_kind_from_u8(byte: u8) -> ThemeKind {
match byte {
x if x == ThemeKind::GrokNight as u8 => ThemeKind::GrokNight,
x if x == ThemeKind::GrokDay as u8 => ThemeKind::GrokDay,
x if x == ThemeKind::KigiNight as u8 => ThemeKind::KigiNight,
x if x == ThemeKind::KigiDay as u8 => ThemeKind::KigiDay,
x if x == ThemeKind::TokyoNight as u8 => ThemeKind::TokyoNight,
x if x == ThemeKind::RosePineMoon as u8 => ThemeKind::RosePineMoon,
x if x == ThemeKind::OscuraMidnight as u8 => ThemeKind::OscuraMidnight,
x if x == ThemeKind::Auto as u8 => ThemeKind::Auto,
_ => ThemeKind::GrokNight,
_ => ThemeKind::KigiNight,
}
}
@@ -63,7 +63,7 @@ static AUTO_THEME_CONFIG: Mutex<Option<AutoThemeConfig>> = Mutex::new(None);
///
/// `dark_theme` and `light_theme` are the user-configured overrides read
/// from `[ui].auto_dark_theme` and `[ui].auto_light_theme` in `config.toml`.
/// When `None`, `to_theme_kind()` defaults to `GrokNight` / `GrokDay`.
/// When `None`, `to_theme_kind()` defaults to `KigiNight` / `KigiDay`.
#[derive(Debug, Clone, Copy, Default)]
pub struct AutoThemeConfig {
pub dark_theme: Option<ThemeKind>,
@@ -78,7 +78,7 @@ pub struct AutoThemeConfig {
pub fn current_kind() -> ThemeKind {
// Locked: return a constant nominal kind without seeding from disk.
if terminal_native_locked() {
return ThemeKind::GrokNight;
return ThemeKind::KigiNight;
}
if !LOADED.load(Ordering::Acquire) {
// Two threads racing into the seed path is harmless — the
@@ -161,7 +161,7 @@ pub fn invalidate_auto_theme_config() {
/// Precedence:
/// 1. Environment variable (`KIGI_THEME`)
/// 2. Config file (`[ui].theme`)
/// 3. Default: `GrokNight`
/// 3. Default: `KigiNight`
#[must_use]
pub fn resolve_initial_theme() -> ThemeKind {
// 1. Environment variable (for desktop app integration)
@@ -185,8 +185,8 @@ fn resolve_from_config(config_theme: Option<ThemeKind>, osc11_fallback: bool) ->
return kind;
}
// Default: GrokNight
ThemeKind::GrokNight
// Default: KigiNight
ThemeKind::KigiNight
}
/// Map an optional appearance detection result to a concrete `ThemeKind`.
@@ -194,13 +194,13 @@ fn resolve_from_appearance(appearance: Option<system_appearance::SystemAppearanc
let config = auto_theme_config();
appearance
.map(|a| system_appearance::to_theme_kind(a, config.dark_theme, config.light_theme))
.unwrap_or(ThemeKind::GrokNight)
.unwrap_or(ThemeKind::KigiNight)
}
/// Resolve "auto" by detecting system appearance and mapping via config.
///
/// Returns the concrete `ThemeKind` based on the current system appearance
/// and the user's dark/light theme mapping. Falls back to `GrokNight`
/// and the user's dark/light theme mapping. Falls back to `KigiNight`
/// when detection fails.
///
/// Uses desktop APIs only (no OSC 11) — safe to call at runtime while
@@ -274,7 +274,7 @@ fn load_auto_theme_config() -> AutoThemeConfig {
pub fn reset_for_test() {
// Tests are serialized via TEST_LOCK so the AtomicU8/AtomicBool
// pair is safe to reset without any cross-thread coordination.
CURRENT.store(ThemeKind::GrokNight as u8, Ordering::Relaxed);
CURRENT.store(ThemeKind::KigiNight as u8, Ordering::Relaxed);
LOADED.store(false, Ordering::Release);
AUTO_MODE.store(false, Ordering::Relaxed);
set_terminal_native_lock(false);
@@ -303,7 +303,7 @@ pub fn test_lock() -> &'static Mutex<()> {
#[cfg(any(test, feature = "test-support"))]
pub fn pin_theme() -> std::sync::MutexGuard<'static, ()> {
let guard = test_lock().lock().unwrap_or_else(|e| e.into_inner());
set(ThemeKind::GrokNight);
set(ThemeKind::KigiNight);
// Color level is a write-once `OnceLock`; tests run without a TTY so it
// resolves to `TrueColor` anyway. Pin it explicitly (best-effort: ignore the
// already-initialized `Err`) so the measure path that reads it stays fixed.
@@ -322,7 +322,7 @@ mod tests {
reset_for_test();
seed_auto_theme_defaults_for_test();
// Set LOADED=true so current_kind() doesn't read from disk.
set(ThemeKind::GrokNight);
set(ThemeKind::KigiNight);
system_appearance::clear_mock();
f();
system_appearance::clear_mock();
@@ -339,19 +339,19 @@ mod tests {
#[test]
fn terminal_native_lock_pins_kind_and_blocks_apply_kind() {
with_test_env(|| {
set(ThemeKind::GrokDay);
set(ThemeKind::KigiDay);
set_terminal_native_lock(true);
assert!(terminal_native_locked());
assert_eq!(current_kind(), ThemeKind::GrokNight, "nominal kind");
assert_eq!(current_kind(), ThemeKind::KigiNight, "nominal kind");
let applied = super::super::Theme::apply_kind(ThemeKind::GrokDay);
assert_eq!(applied, ThemeKind::GrokNight, "apply_kind must no-op");
assert_eq!(current_kind(), ThemeKind::GrokNight);
let applied = super::super::Theme::apply_kind(ThemeKind::KigiDay);
assert_eq!(applied, ThemeKind::KigiNight, "apply_kind must no-op");
assert_eq!(current_kind(), ThemeKind::KigiNight);
set_terminal_native_lock(false);
assert_eq!(
current_kind(),
ThemeKind::GrokDay,
ThemeKind::KigiDay,
"unlocking restores the cached kind"
);
});
@@ -360,7 +360,7 @@ mod tests {
#[test]
fn terminal_native_lock_serves_terminal_default_palette() {
with_test_env(|| {
set(ThemeKind::GrokDay);
set(ThemeKind::KigiDay);
set_terminal_native_lock(true);
let theme = super::super::Theme::current();
let native = super::super::Theme::terminal_default();
@@ -369,8 +369,8 @@ mod tests {
assert_eq!(theme.accent_user, native.accent_user);
assert_ne!(
theme.text_primary,
super::super::Theme::grokday().text_primary,
"must not serve the cached (GrokDay) theme"
super::super::Theme::kigiday().text_primary,
"must not serve the cached (KigiDay) theme"
);
});
}
@@ -393,7 +393,7 @@ mod tests {
set_terminal_native_lock(true);
assert!(color_support::detect() <= color_support::ColorLevel::Basic);
for input in [
Color::Rgb(0x26, 0x26, 0x26), // grokday text_primary
Color::Rgb(0x26, 0x26, 0x26), // kigiday text_primary
Color::Rgb(122, 162, 247),
Color::Indexed(141),
] {
@@ -411,19 +411,19 @@ mod tests {
fn resolve_no_osc11_explicit_auto_and_default() {
with_test_env(|| {
assert_eq!(
resolve_from_config(Some(ThemeKind::GrokDay), false),
ThemeKind::GrokDay
resolve_from_config(Some(ThemeKind::KigiDay), false),
ThemeKind::KigiDay
);
assert!(!is_auto_mode());
system_appearance::set_mock(Some(system_appearance::SystemAppearance::Light));
assert_eq!(
resolve_from_config(Some(ThemeKind::Auto), false),
ThemeKind::GrokDay
ThemeKind::KigiDay
);
assert!(is_auto_mode(), "auto must arm the appearance watcher");
assert_eq!(resolve_from_config(None, false), ThemeKind::GrokNight);
assert_eq!(resolve_from_config(None, false), ThemeKind::KigiNight);
});
}
@@ -458,29 +458,29 @@ mod tests {
// -- resolve_auto --------------------------------------------------------
#[test]
fn resolve_auto_dark_system_returns_groknight() {
fn resolve_auto_dark_system_returns_kiginight() {
with_test_env(|| {
system_appearance::set_mock(Some(system_appearance::SystemAppearance::Dark));
let result = resolve_auto();
assert_eq!(result, ThemeKind::GrokNight);
assert_eq!(result, ThemeKind::KigiNight);
});
}
#[test]
fn resolve_auto_light_system_returns_grokday() {
fn resolve_auto_light_system_returns_kigiday() {
with_test_env(|| {
system_appearance::set_mock(Some(system_appearance::SystemAppearance::Light));
let result = resolve_auto();
assert_eq!(result, ThemeKind::GrokDay);
assert_eq!(result, ThemeKind::KigiDay);
});
}
#[test]
fn resolve_auto_detection_failure_returns_groknight() {
fn resolve_auto_detection_failure_returns_kiginight() {
with_test_env(|| {
system_appearance::set_mock(None);
let result = resolve_auto();
assert_eq!(result, ThemeKind::GrokNight);
assert_eq!(result, ThemeKind::KigiNight);
});
}
@@ -509,10 +509,10 @@ mod tests {
// -- resolve_from_config (resolve_initial_theme inner logic) ---------------
#[test]
fn resolve_from_config_no_config_returns_groknight() {
fn resolve_from_config_no_config_returns_kiginight() {
with_test_env(|| {
let result = resolve_from_config(None, true);
assert_eq!(result, ThemeKind::GrokNight);
assert_eq!(result, ThemeKind::KigiNight);
assert!(!is_auto_mode());
});
}
@@ -520,8 +520,8 @@ mod tests {
#[test]
fn resolve_from_config_explicit_theme_returns_it() {
with_test_env(|| {
let result = resolve_from_config(Some(ThemeKind::GrokDay), true);
assert_eq!(result, ThemeKind::GrokDay);
let result = resolve_from_config(Some(ThemeKind::KigiDay), true);
assert_eq!(result, ThemeKind::KigiDay);
assert!(
!is_auto_mode(),
"explicit theme should not enable auto mode"
@@ -534,7 +534,7 @@ mod tests {
with_test_env(|| {
system_appearance::set_mock(Some(system_appearance::SystemAppearance::Dark));
let result = resolve_from_config(Some(ThemeKind::Auto), true);
assert_eq!(result, ThemeKind::GrokNight);
assert_eq!(result, ThemeKind::KigiNight);
assert!(is_auto_mode(), "auto config must enable auto mode");
});
}
@@ -544,7 +544,7 @@ mod tests {
with_test_env(|| {
system_appearance::set_mock(Some(system_appearance::SystemAppearance::Light));
let result = resolve_from_config(Some(ThemeKind::Auto), true);
assert_eq!(result, ThemeKind::GrokDay);
assert_eq!(result, ThemeKind::KigiDay);
assert!(is_auto_mode());
});
}
@@ -554,7 +554,7 @@ mod tests {
with_test_env(|| {
system_appearance::set_mock(None);
let result = resolve_from_config(Some(ThemeKind::Auto), true);
assert_eq!(result, ThemeKind::GrokNight);
assert_eq!(result, ThemeKind::KigiNight);
assert!(is_auto_mode(), "auto mode is set before detection");
});
}
@@ -617,8 +617,8 @@ mod tests {
with_test_env(|| {
set(ThemeKind::TokyoNight);
assert_eq!(current_kind(), ThemeKind::TokyoNight);
set(ThemeKind::GrokDay);
assert_eq!(current_kind(), ThemeKind::GrokDay);
set(ThemeKind::KigiDay);
assert_eq!(current_kind(), ThemeKind::KigiDay);
});
}
@@ -638,14 +638,14 @@ mod tests {
!LOADED.load(Ordering::Acquire),
"LOADED must be false for this test"
);
set(ThemeKind::GrokDay);
set(ThemeKind::KigiDay);
assert!(
LOADED.load(Ordering::Acquire),
"set must flip LOADED to true"
);
// Subsequent current_kind read returns the set value (no
// disk re-seed).
assert_eq!(current_kind(), ThemeKind::GrokDay);
assert_eq!(current_kind(), ThemeKind::KigiDay);
assert!(
LOADED.load(Ordering::Acquire),
"current_kind must NOT flip LOADED back to false"
@@ -1,8 +1,8 @@
//! GrokDay theme — neutral gray base (light) with deepened accent colors.
//! KigiDay theme — neutral gray base (light) with deepened accent colors.
//!
//! Light counterpart to GrokNight. Backgrounds and text use a neutral
//! Light counterpart to KigiNight. Backgrounds and text use a neutral
//! grayscale ramp (no blue/warm tint). Accent colors are the same hue
//! family as GrokNight but deepened for contrast on light backgrounds.
//! family as KigiNight but deepened for contrast on light backgrounds.
use ratatui::style::{Color, Modifier};
@@ -52,7 +52,7 @@ mod palette {
use palette::*;
impl Theme {
pub const fn grokday() -> Self {
pub const fn kigiday() -> Self {
Self {
bg_base: BG_STORM,
bg_light: BG_HIGHLIGHT,
@@ -1,4 +1,4 @@
//! GrokNight theme — neutral gray base with TokyoNight accent colors.
//! KigiNight theme — neutral gray base with TokyoNight accent colors.
//!
//! The canonical palette is defined in RGB (`Color::Rgb`). At startup the
//! theme is run through [`Theme::quantized`] which downgrades every color
@@ -13,7 +13,7 @@ const fn rgb(r: u8, g: u8, b: u8) -> Color {
Color::Rgb(r, g, b)
}
// GrokNight palette — neutral gray base + TokyoNight accent colors.
// KigiNight palette — neutral gray base + TokyoNight accent colors.
//
// Backgrounds and text use a custom grayscale ramp anchored at:
// • bg = #141414 (20)
@@ -60,11 +60,11 @@ mod palette {
use palette::*;
impl Theme {
/// GrokNight theme — neutral gray base with TokyoNight accents.
/// KigiNight theme — neutral gray base with TokyoNight accents.
///
/// Colors are defined in RGB. Call [`Theme::quantized`] to downgrade
/// them to the terminal's supported color level before rendering.
pub const fn groknight() -> Self {
pub const fn kiginight() -> Self {
Self {
bg_base: BG_STORM,
bg_light: BG_HIGHLIGHT,
@@ -157,8 +157,8 @@ mod tests {
#[test]
#[ignore = "known broken: expected accent values drift from runtime theme"]
fn test_groknight_theme() {
let theme = Theme::groknight();
fn test_kiginight_theme() {
let theme = Theme::kiginight();
assert!(matches!(theme.bg_base, Color::Rgb(20, 20, 20)));
assert!(matches!(theme.accent_user, Color::Rgb(225, 225, 225)));
assert!(matches!(theme.text_primary, Color::Rgb(225, 225, 225)));
@@ -1,19 +1,19 @@
//! Theming for the pager.
//!
//! All colors come from the `Theme` struct. No hardcoded colors elsewhere.
//! The default theme is GrokNight (neutral gray base with TokyoNight accents).
//! The default theme is KigiNight (neutral gray base with TokyoNight accents).
//!
//! ## Color support
//!
//! GrokNight is defined in `Color::Rgb` (truecolor). At startup,
//! KigiNight is defined in `Color::Rgb` (truecolor). At startup,
//! [`Theme::current()`] quantizes every color to the terminal's detected
//! capability level via [`Theme::quantized`]. Runtime-generated colors (syntax
//! highlighting, blending) are also quantized via [`color_support::quantize`].
pub mod cache;
pub mod color_support;
mod grokday;
mod groknight;
mod kigiday;
mod kiginight;
pub mod md_style;
pub mod osc11;
mod oscura;
@@ -28,8 +28,8 @@ pub use tokyonight::{Theme, pulse_brightness, wave_brightness};
/// Available theme variants.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ThemeKind {
GrokNight = 0,
GrokDay = 1,
KigiNight = 0,
KigiDay = 1,
TokyoNight = 2,
RosePineMoon = 3,
OscuraMidnight = 5,
@@ -46,8 +46,8 @@ pub enum ThemeKind {
impl ThemeKind {
/// All theme kinds (including those that may not work on the current terminal).
pub const ALL: &[ThemeKind] = &[
ThemeKind::GrokNight,
ThemeKind::GrokDay,
ThemeKind::KigiNight,
ThemeKind::KigiDay,
ThemeKind::TokyoNight,
ThemeKind::RosePineMoon,
ThemeKind::OscuraMidnight,
@@ -61,7 +61,7 @@ impl ThemeKind {
// Two possible results — pick the right const slice based on
// the detected color level. No heap allocation needed.
const ALL: &[ThemeKind] = ThemeKind::ALL;
const NO_TRUECOLOR: &[ThemeKind] = &[ThemeKind::GrokNight, ThemeKind::GrokDay];
const NO_TRUECOLOR: &[ThemeKind] = &[ThemeKind::KigiNight, ThemeKind::KigiDay];
if color_support::detect().has_truecolor() {
ALL
@@ -73,9 +73,9 @@ impl ThemeKind {
/// Human-readable display name.
pub fn display_name(self) -> &'static str {
match self {
Self::GrokNight => "groknight",
Self::KigiNight => "kiginight",
Self::TokyoNight => "tokyonight",
Self::GrokDay => "grokday",
Self::KigiDay => "kigiday",
Self::RosePineMoon => "rosepine-moon",
Self::OscuraMidnight => "oscura-midnight",
Self::Auto => "auto",
@@ -85,13 +85,13 @@ impl ThemeKind {
/// Whether this theme requires truecolor (24-bit RGB) to look correct.
///
/// TokyoNight uses blue-tinted backgrounds that lose their character
/// when quantized to 256 or 16 colors. GrokNight uses neutral grays
/// when quantized to 256 or 16 colors. KigiNight uses neutral grays
/// that survive quantization cleanly.
pub fn requires_truecolor(self) -> bool {
match self {
Self::GrokNight => false,
Self::KigiNight => false,
Self::TokyoNight => true,
Self::GrokDay => false,
Self::KigiDay => false,
Self::RosePineMoon => true,
Self::OscuraMidnight => true,
// Auto is resolved to a concrete theme before rendering.
@@ -105,9 +105,9 @@ impl ThemeKind {
let lower = name.to_lowercase();
match lower.as_str() {
"auto" | "system" => Some(Self::Auto),
"groknight" | "grok-night" | "dark" => Some(Self::GrokNight),
"kiginight" | "kigi-night" | "dark" => Some(Self::KigiNight),
"tokyonight" | "tokyo-night" | "tokyo" => Some(Self::TokyoNight),
"grokday" | "grok-day" | "light" | "day" => Some(Self::GrokDay),
"kigiday" | "kigi-day" | "light" | "day" => Some(Self::KigiDay),
"rosepine" | "rose-pine" | "rosepine-moon" | "rose-pine-moon" => {
Some(Self::RosePineMoon)
}
@@ -139,12 +139,12 @@ pub fn canonical_name(value: &str) -> Option<&'static str> {
}
/// Human-friendly display name for a canonical theme value (e.g.
/// `"groknight"` → `"Kigi Night"`). Falls back to `value` verbatim.
/// `"kiginight"` → `"Kigi Night"`). Falls back to `value` verbatim.
pub fn display_name_for_canonical(value: &str) -> &str {
match value {
"auto" => "Auto",
"groknight" => "Kigi Night",
"grokday" => "Kigi Day",
"kiginight" => "Kigi Night",
"kigiday" => "Kigi Day",
"tokyonight" => "Tokyo Night",
"rosepine-moon" => "Rose Pine Moon",
other => other,
@@ -153,7 +153,7 @@ pub fn display_name_for_canonical(value: &str) -> &str {
impl Default for Theme {
fn default() -> Self {
Self::groknight()
Self::kiginight()
}
}
@@ -270,14 +270,14 @@ impl Theme {
return Self::terminal_default().quantized(level);
}
let base = match cache::current_kind() {
ThemeKind::GrokNight => Self::groknight(),
ThemeKind::KigiNight => Self::kiginight(),
ThemeKind::TokyoNight => Self::tokyonight(),
ThemeKind::GrokDay => Self::grokday(),
ThemeKind::KigiDay => Self::kigiday(),
ThemeKind::RosePineMoon => Self::rosepine_moon(),
ThemeKind::OscuraMidnight => Self::oscura_midnight(),
// Auto is resolved to a concrete theme before being stored;
// if reached, fall back to GrokNight.
ThemeKind::Auto => Self::groknight(),
// if reached, fall back to KigiNight.
ThemeKind::Auto => Self::kiginight(),
};
// Sample polarity pre-quantization — post-quantize `bg_base` may
// land on a named/indexed entry whose luminance is host-palette-
@@ -342,7 +342,7 @@ impl Theme {
/// Clamp a theme kind to what the terminal supports.
fn clamp_to_terminal(kind: ThemeKind) -> ThemeKind {
if kind.requires_truecolor() && !color_support::detect().has_truecolor() {
ThemeKind::GrokNight
ThemeKind::KigiNight
} else {
kind
}
@@ -431,7 +431,7 @@ impl Theme {
/// 2. **Semantic accents (running/error/success/etc.).** Naive
/// RGB-distance quantization collapses pastel theme hues onto
/// the gray ramp (audit: 18/27 fields became DarkGray or silver
/// on groknight, erasing every state signal). We pin each
/// on kiginight, erasing every state signal). We pin each
/// semantic field to a hue-preserving ANSI16 slot, polarity-aware:
/// bright variants (`Light*`, idx 915) on a dark canvas; normal
/// variants (idx 17, ~50% luminance) on a light canvas. This
@@ -447,7 +447,7 @@ impl Theme {
/// theme's polarity (Black for dark themes, White for light themes)
/// instead of `Color::Reset` — using `Reset` would defer to the
/// user's terminal profile, which can disagree with the selected
/// theme (e.g., GrokNight on a white terminal would show white
/// theme (e.g., KigiNight on a white terminal would show white
/// "holes" through every sunken surface).
fn ansi16_chrome_overrides(self, dark: bool) -> Self {
use ratatui::style::Color;
@@ -505,7 +505,7 @@ impl Theme {
// ── Elevated surfaces: one step off the canvas ──────────────
// Hover/highlight/visual-selection rows need to read as a
// distinct "raised" band against the body. Without this every
// GrokNight bg field quantizes to Color::Black and these
// KigiNight bg field quantizes to Color::Black and these
// become invisible.
bg_light: elevated_bg,
bg_highlight: elevated_bg,
@@ -679,8 +679,8 @@ mod tests {
#[test]
fn is_auto_returns_false_for_concrete_variants() {
assert!(!ThemeKind::GrokNight.is_auto());
assert!(!ThemeKind::GrokDay.is_auto());
assert!(!ThemeKind::KigiNight.is_auto());
assert!(!ThemeKind::KigiDay.is_auto());
assert!(!ThemeKind::TokyoNight.is_auto());
assert!(!ThemeKind::RosePineMoon.is_auto());
assert!(!ThemeKind::OscuraMidnight.is_auto());
@@ -699,17 +699,17 @@ mod tests {
#[test]
fn is_dark_classifies_built_in_themes() {
// Sanity-check the polarity sampler against the theme catalog.
assert!(Theme::groknight().is_dark());
assert!(Theme::kiginight().is_dark());
assert!(Theme::tokyonight().is_dark());
assert!(Theme::rosepine_moon().is_dark());
assert!(Theme::oscura_midnight().is_dark());
assert!(!Theme::grokday().is_dark());
assert!(!Theme::kigiday().is_dark());
}
#[test]
fn ansi16_overrides_dark_uses_bright_white_high_contrast() {
use ratatui::style::Color;
let t = Theme::groknight().ansi16_chrome_overrides(true);
let t = Theme::kiginight().ansi16_chrome_overrides(true);
assert_eq!(t.bg_light, Color::DarkGray);
assert_eq!(t.bg_highlight, Color::DarkGray);
// Idle prompt border sits at `dim_fg` (DarkGray on dark canvas);
@@ -732,7 +732,7 @@ mod tests {
// flips to silver — see
// `ansi16_overrides_gray_hierarchy_collapses_to_two_slots`.
use ratatui::style::Color;
let t = Theme::grokday().ansi16_chrome_overrides(false);
let t = Theme::kigiday().ansi16_chrome_overrides(false);
assert_eq!(t.bg_light, Color::Gray);
assert_eq!(t.bg_highlight, Color::Gray);
assert_eq!(t.prompt_border, Color::Gray);
@@ -749,7 +749,7 @@ mod tests {
// (`bg_dark`, `md_code_bg`, `paste_bg`, `scrollbar_bg`) are
// tested separately in
// `ansi16_overrides_canvas_matching_surfaces_use_theme_polarity`.
let base = Theme::groknight();
let base = Theme::kiginight();
let t = base.ansi16_chrome_overrides(true);
assert_eq!(t.bg_base, base.bg_base);
}
@@ -762,12 +762,12 @@ mod tests {
// pastel RGBs collapse onto silver/DarkGray and every state
// signal becomes the same gray.
use ratatui::style::Color;
let t_dark = Theme::groknight().ansi16_chrome_overrides(true);
let t_dark = Theme::kiginight().ansi16_chrome_overrides(true);
assert_eq!(t_dark.accent_error, Color::LightRed);
assert_eq!(t_dark.accent_success, Color::LightGreen);
assert_eq!(t_dark.accent_running, Color::LightMagenta);
let t_light = Theme::grokday().ansi16_chrome_overrides(false);
let t_light = Theme::kigiday().ansi16_chrome_overrides(false);
assert_eq!(t_light.accent_error, Color::Red);
assert_eq!(t_light.accent_success, Color::Green);
assert_eq!(t_light.accent_running, Color::Magenta);
@@ -781,7 +781,7 @@ mod tests {
// onto it together — they live in different surfaces so the
// collision doesn't cause confusion.
use ratatui::style::Color;
let t = Theme::groknight().ansi16_chrome_overrides(true);
let t = Theme::kiginight().ansi16_chrome_overrides(true);
assert_eq!(t.accent_assistant, Color::LightMagenta);
assert_eq!(t.accent_thinking, Color::LightMagenta);
assert_eq!(t.accent_running, Color::LightMagenta);
@@ -796,7 +796,7 @@ mod tests {
// semantic is more important than per-accent differentiation
// that the palette cannot represent.
use ratatui::style::Color;
let t_dark = Theme::groknight().ansi16_chrome_overrides(true);
let t_dark = Theme::kiginight().ansi16_chrome_overrides(true);
for f in [
t_dark.command,
t_dark.warning,
@@ -806,7 +806,7 @@ mod tests {
assert_eq!(f, Color::LightYellow);
}
let t_light = Theme::grokday().ansi16_chrome_overrides(false);
let t_light = Theme::kigiday().ansi16_chrome_overrides(false);
for f in [
t_light.command,
t_light.warning,
@@ -824,7 +824,7 @@ mod tests {
// from the magenta `accent_running` used for subagents) also
// lives here.
use ratatui::style::Color;
let t = Theme::groknight().ansi16_chrome_overrides(true);
let t = Theme::kiginight().ansi16_chrome_overrides(true);
assert_eq!(t.accent_feedback, Color::LightCyan);
assert_eq!(t.accent_model, Color::LightCyan);
assert_eq!(t.running, Color::LightCyan);
@@ -835,12 +835,12 @@ mod tests {
// System messages, skill invocations, and fuzzy-search matches
// all carry the same blue family in truecolor.
use ratatui::style::Color;
let t_dark = Theme::groknight().ansi16_chrome_overrides(true);
let t_dark = Theme::kiginight().ansi16_chrome_overrides(true);
assert_eq!(t_dark.accent_system, Color::LightBlue);
assert_eq!(t_dark.accent_skill, Color::LightBlue);
assert_eq!(t_dark.fuzzy_accent, Color::LightBlue);
let t_light = Theme::grokday().ansi16_chrome_overrides(false);
let t_light = Theme::kigiday().ansi16_chrome_overrides(false);
assert_eq!(t_light.accent_system, Color::Blue);
assert_eq!(t_light.accent_skill, Color::Blue);
assert_eq!(t_light.fuzzy_accent, Color::Blue);
@@ -852,11 +852,11 @@ mod tests {
// ANSI16 (the subtle pastel bg tints don't survive quantization).
// Pin fg to red / green so deletes and inserts stay legible.
use ratatui::style::Color;
let t_dark = Theme::groknight().ansi16_chrome_overrides(true);
let t_dark = Theme::kiginight().ansi16_chrome_overrides(true);
assert_eq!(t_dark.diff_delete_fg, Color::LightRed);
assert_eq!(t_dark.diff_insert_fg, Color::LightGreen);
let t_light = Theme::grokday().ansi16_chrome_overrides(false);
let t_light = Theme::kigiday().ansi16_chrome_overrides(false);
assert_eq!(t_light.diff_delete_fg, Color::Red);
assert_eq!(t_light.diff_insert_fg, Color::Green);
}
@@ -869,10 +869,10 @@ mod tests {
// - dark canvas → Color::White
// - light canvas → Color::Black
use ratatui::style::Color;
let t_dark = Theme::groknight().ansi16_chrome_overrides(true);
let t_dark = Theme::kiginight().ansi16_chrome_overrides(true);
assert_eq!(t_dark.accent_user, Color::White);
let t_light = Theme::grokday().ansi16_chrome_overrides(false);
let t_light = Theme::kigiday().ansi16_chrome_overrides(false);
assert_eq!(t_light.accent_user, Color::Black);
}
@@ -881,7 +881,7 @@ mod tests {
// Without these pins, every dark RGB bg quantizes to Color::Black
// and the hover/visual/highlight bands collapse onto the canvas.
use ratatui::style::Color;
let t = Theme::groknight().ansi16_chrome_overrides(true);
let t = Theme::kiginight().ansi16_chrome_overrides(true);
assert_eq!(t.bg_hover, Color::DarkGray);
assert_eq!(t.bg_visual, Color::DarkGray);
}
@@ -889,7 +889,7 @@ mod tests {
#[test]
fn ansi16_overrides_extended_light_pins_elevated_bg_to_gray() {
use ratatui::style::Color;
let t = Theme::grokday().ansi16_chrome_overrides(false);
let t = Theme::kigiday().ansi16_chrome_overrides(false);
assert_eq!(t.bg_hover, Color::Gray);
assert_eq!(t.bg_visual, Color::Gray);
}
@@ -900,16 +900,16 @@ mod tests {
// must match the theme polarity (Black for dark themes, White
// for light) — NOT Color::Reset, which would defer to the
// user's terminal canvas and create polarity mismatches when
// the theme disagrees with the terminal profile (e.g. GrokNight
// the theme disagrees with the terminal profile (e.g. KigiNight
// running on a white-canvas terminal).
use ratatui::style::Color;
let t_dark = Theme::groknight().ansi16_chrome_overrides(true);
let t_dark = Theme::kiginight().ansi16_chrome_overrides(true);
assert_eq!(t_dark.bg_dark, Color::Black);
assert_eq!(t_dark.md_code_bg, Color::Black);
assert_eq!(t_dark.paste_bg, Color::Black);
assert_eq!(t_dark.scrollbar_bg, Color::Black);
let t_light = Theme::grokday().ansi16_chrome_overrides(false);
let t_light = Theme::kigiday().ansi16_chrome_overrides(false);
assert_eq!(t_light.bg_dark, Color::White);
assert_eq!(t_light.md_code_bg, Color::White);
assert_eq!(t_light.paste_bg, Color::White);
@@ -928,14 +928,14 @@ mod tests {
// both take `DarkGray`. The selection-vs-active distinction
// survives in both polarities via `prompt_border_active`.
use ratatui::style::Color;
let t_dark = Theme::groknight().ansi16_chrome_overrides(true);
let t_dark = Theme::kiginight().ansi16_chrome_overrides(true);
assert_eq!(t_dark.hover_border, Color::DarkGray);
assert_eq!(t_dark.prompt_border, Color::DarkGray);
assert_eq!(t_dark.selection_border, Color::Gray);
assert_eq!(t_dark.prompt_border_active, Color::White);
assert_ne!(t_dark.selection_border, t_dark.prompt_border_active);
let t_light = Theme::grokday().ansi16_chrome_overrides(false);
let t_light = Theme::kigiday().ansi16_chrome_overrides(false);
assert_eq!(t_light.hover_border, Color::DarkGray);
assert_eq!(t_light.prompt_border, Color::Gray);
assert_eq!(t_light.selection_border, Color::DarkGray);
@@ -948,11 +948,11 @@ mod tests {
// scrollbar_fg must not equal scrollbar_bg or the thumb is
// invisible against the canvas-pinned track.
use ratatui::style::Color;
let t_dark = Theme::groknight().ansi16_chrome_overrides(true);
let t_dark = Theme::kiginight().ansi16_chrome_overrides(true);
assert_eq!(t_dark.scrollbar_fg, Color::Gray);
assert_ne!(t_dark.scrollbar_fg, t_dark.scrollbar_bg);
let t_light = Theme::grokday().ansi16_chrome_overrides(false);
let t_light = Theme::kigiday().ansi16_chrome_overrides(false);
assert_eq!(t_light.scrollbar_fg, Color::DarkGray);
assert_ne!(t_light.scrollbar_fg, t_light.scrollbar_bg);
}
@@ -980,8 +980,8 @@ mod tests {
};
for &kind in ThemeKind::ALL {
let theme = match kind {
ThemeKind::GrokNight => Theme::groknight(),
ThemeKind::GrokDay => Theme::grokday(),
ThemeKind::KigiNight => Theme::kiginight(),
ThemeKind::KigiDay => Theme::kigiday(),
ThemeKind::TokyoNight => Theme::tokyonight(),
ThemeKind::RosePineMoon => Theme::rosepine_moon(),
ThemeKind::OscuraMidnight => Theme::oscura_midnight(),
@@ -1007,16 +1007,16 @@ mod tests {
}
#[test]
fn ansi16_quantize_without_override_collapses_groknight_backgrounds() {
fn ansi16_quantize_without_override_collapses_kiginight_backgrounds() {
// Regression-ratchet for the override gate in `Theme::current`:
// naive `Basic` quantization maps every dark GrokNight bg field
// naive `Basic` quantization maps every dark KigiNight bg field
// to `Color::Black`, erasing the hierarchy. This test exists to
// make the motivation for `ansi16_chrome_overrides` explicit —
// if quantization later gains a "dark gray" mid-tone (e.g., via
// an ANSI24/ANSI32 level), this test will start failing and the
// override scope should be revisited.
use ratatui::style::Color;
let q = Theme::groknight().quantized(color_support::ColorLevel::Basic);
let q = Theme::kiginight().quantized(color_support::ColorLevel::Basic);
for (name, color) in [
("bg_base", q.bg_base),
("bg_light", q.bg_light),
@@ -1052,13 +1052,13 @@ mod tests {
// brighter slot keeps secondary text legible while still
// separating "dim" from "muted".
use ratatui::style::Color;
let t_dark = Theme::groknight().ansi16_chrome_overrides(true);
let t_dark = Theme::kiginight().ansi16_chrome_overrides(true);
assert_eq!(t_dark.gray, Color::Gray);
assert_eq!(t_dark.gray_bright, Color::Gray);
assert_eq!(t_dark.gray_dim, Color::DarkGray);
assert_ne!(t_dark.gray, t_dark.gray_dim);
let t_light = Theme::grokday().ansi16_chrome_overrides(false);
let t_light = Theme::kigiday().ansi16_chrome_overrides(false);
assert_eq!(t_light.gray, Color::DarkGray);
assert_eq!(t_light.gray_bright, Color::DarkGray);
assert_eq!(t_light.gray_dim, Color::Gray);
@@ -1111,12 +1111,12 @@ mod tests {
#[test]
fn from_name_concrete_variants_still_work() {
assert_eq!(
ThemeKind::from_name("groknight"),
Some(ThemeKind::GrokNight)
ThemeKind::from_name("kiginight"),
Some(ThemeKind::KigiNight)
);
assert_eq!(ThemeKind::from_name("dark"), Some(ThemeKind::GrokNight));
assert_eq!(ThemeKind::from_name("grokday"), Some(ThemeKind::GrokDay));
assert_eq!(ThemeKind::from_name("light"), Some(ThemeKind::GrokDay));
assert_eq!(ThemeKind::from_name("dark"), Some(ThemeKind::KigiNight));
assert_eq!(ThemeKind::from_name("kigiday"), Some(ThemeKind::KigiDay));
assert_eq!(ThemeKind::from_name("light"), Some(ThemeKind::KigiDay));
assert_eq!(
ThemeKind::from_name("tokyonight"),
Some(ThemeKind::TokyoNight)
@@ -1142,16 +1142,16 @@ mod tests {
let cases = [
("auto", ThemeKind::Auto),
("system", ThemeKind::Auto),
("groknight", ThemeKind::GrokNight),
("grok-night", ThemeKind::GrokNight),
("dark", ThemeKind::GrokNight),
("kiginight", ThemeKind::KigiNight),
("kigi-night", ThemeKind::KigiNight),
("dark", ThemeKind::KigiNight),
("tokyonight", ThemeKind::TokyoNight),
("tokyo-night", ThemeKind::TokyoNight),
("tokyo", ThemeKind::TokyoNight),
("grokday", ThemeKind::GrokDay),
("grok-day", ThemeKind::GrokDay),
("light", ThemeKind::GrokDay),
("day", ThemeKind::GrokDay),
("kigiday", ThemeKind::KigiDay),
("kigi-day", ThemeKind::KigiDay),
("light", ThemeKind::KigiDay),
("day", ThemeKind::KigiDay),
("rosepine", ThemeKind::RosePineMoon),
("rose-pine", ThemeKind::RosePineMoon),
("rosepine-moon", ThemeKind::RosePineMoon),
@@ -87,7 +87,7 @@ fn mock_override() -> Option<Option<SystemAppearance>> {
///
/// `dark_theme` and `light_theme` are the user-configured themes for each
/// appearance mode, read from `[ui].auto_dark_theme` and `[ui].auto_light_theme`
/// in `config.toml`. When `None`, defaults to `GrokNight` / `GrokDay`.
/// in `config.toml`. When `None`, defaults to `KigiNight` / `KigiDay`.
///
/// This function is the single mapping point for appearance -> theme.
/// All callers go through it, making the mapping trivially extensible.
@@ -98,8 +98,8 @@ pub fn to_theme_kind(
light_theme: Option<ThemeKind>,
) -> ThemeKind {
match appearance {
SystemAppearance::Light => light_theme.unwrap_or(ThemeKind::GrokDay),
SystemAppearance::Dark => dark_theme.unwrap_or(ThemeKind::GrokNight),
SystemAppearance::Light => light_theme.unwrap_or(ThemeKind::KigiDay),
SystemAppearance::Dark => dark_theme.unwrap_or(ThemeKind::KigiNight),
}
}
@@ -214,15 +214,15 @@ mod tests {
}
#[test]
fn to_theme_kind_dark_defaults_to_groknight() {
fn to_theme_kind_dark_defaults_to_kiginight() {
let result = to_theme_kind(SystemAppearance::Dark, None, None);
assert_eq!(result, ThemeKind::GrokNight);
assert_eq!(result, ThemeKind::KigiNight);
}
#[test]
fn to_theme_kind_light_defaults_to_grokday() {
fn to_theme_kind_light_defaults_to_kigiday() {
let result = to_theme_kind(SystemAppearance::Light, None, None);
assert_eq!(result, ThemeKind::GrokDay);
assert_eq!(result, ThemeKind::KigiDay);
}
#[test]
@@ -242,30 +242,30 @@ mod tests {
let result = to_theme_kind(
SystemAppearance::Dark,
Some(ThemeKind::RosePineMoon),
Some(ThemeKind::GrokNight),
Some(ThemeKind::KigiNight),
);
assert_eq!(result, ThemeKind::RosePineMoon);
let result = to_theme_kind(
SystemAppearance::Light,
Some(ThemeKind::RosePineMoon),
Some(ThemeKind::GrokNight),
Some(ThemeKind::KigiNight),
);
assert_eq!(result, ThemeKind::GrokNight);
assert_eq!(result, ThemeKind::KigiNight);
}
#[test]
fn to_theme_kind_dark_ignores_light_override() {
let result = to_theme_kind(SystemAppearance::Dark, None, Some(ThemeKind::TokyoNight));
// Dark appearance should use the dark default, not the light override.
assert_eq!(result, ThemeKind::GrokNight);
assert_eq!(result, ThemeKind::KigiNight);
}
#[test]
fn to_theme_kind_light_ignores_dark_override() {
let result = to_theme_kind(SystemAppearance::Light, Some(ThemeKind::TokyoNight), None);
// Light appearance should use the light default, not the dark override.
assert_eq!(result, ThemeKind::GrokDay);
assert_eq!(result, ThemeKind::KigiDay);
}
#[test]
@@ -285,8 +285,8 @@ mod tests {
#[test]
fn rgb_theme_muted_keeps_explicit_gray_without_forced_dim() {
use ratatui::style::Modifier;
// GrokNight paints real RGB grays; muted/dim must not invent DIM.
let theme = Theme::groknight();
// KigiNight paints real RGB grays; muted/dim must not invent DIM.
let theme = Theme::kiginight();
assert!(!matches!(theme.gray, Color::Reset));
let muted = theme.muted();
assert_eq!(muted.fg, Some(theme.gray));
+7 -7
View File
@@ -11,7 +11,7 @@ pub fn pager_toml_path() -> PathBuf {
kigi_home().join("pager.toml")
}
/// User-facing label for the user grok directory (``~/.kigi`` or ``$KIGI_SHARE_DIR``).
/// User-facing label for the user kigi directory (``~/.kigi`` or ``$KIGI_SHARE_DIR``).
///
/// Derived from resolved [`kigi_home()`] vs `kigi_config::default_kigi_home()`,
/// not from whether `KIGI_SHARE_DIR` is set in the environment.
@@ -24,7 +24,7 @@ pub fn display_kigi_home_prefix() -> String {
}
/// User-facing path under [`kigi_home()`], e.g. ``~/.kigi/config.toml``.
pub fn display_user_grok_path(relative: impl AsRef<Path>) -> String {
pub fn display_user_kigi_path(relative: impl AsRef<Path>) -> String {
let rel = relative.as_ref();
let prefix = display_kigi_home_prefix();
if rel.as_os_str().is_empty() {
@@ -36,8 +36,8 @@ pub fn display_user_grok_path(relative: impl AsRef<Path>) -> String {
/// Abbreviate an absolute path for display: prefer [`kigi_home()`], then `$HOME`.
pub fn abbreviate_path(path: &str) -> Cow<'_, str> {
let path_buf = Path::new(path);
let grok = kigi_home();
if let Ok(rest) = path_buf.strip_prefix(&grok) {
let kigi = kigi_home();
if let Ok(rest) = path_buf.strip_prefix(&kigi) {
let prefix = display_kigi_home_prefix();
if rest.as_os_str().is_empty() {
return Cow::Owned(prefix);
@@ -404,14 +404,14 @@ mod tests {
}
#[test]
fn display_user_grok_path_joins_relative() {
let path = display_user_grok_path("config.toml");
fn display_user_kigi_path_joins_relative() {
let path = display_user_kigi_path("config.toml");
assert!(path.ends_with("/config.toml") || path.ends_with("\\config.toml"));
assert!(path.contains(".kigi") || path.contains("$KIGI_SHARE_DIR"));
}
#[test]
fn abbreviate_path_uses_home_when_under_default_grok() {
fn abbreviate_path_uses_home_when_under_default_kigi() {
if let Ok(home) = std::env::var("HOME") {
if home.is_empty() {
return;