Files
1jehuang--jcode/crates/jcode-setup-hints/src/keymap/report.rs
T
wehub-resource-sync a789495a98
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:34 +08:00

191 lines
6.6 KiB
Rust

//! Human-readable rendering of the keymap snapshot and detected conflicts,
//! shared by the `/keys` command and the startup conflict hint.
use jcode_config_types::KeybindingsConfig;
use super::KeymapSnapshot;
use super::conflicts::{Conflict, detect_conflicts};
use super::source::KeySource;
/// Render a full diagnostic report: detected terminal, discovered binding
/// counts, and any conflicts with jcode's configured bindings.
pub fn render_report(cfg: &KeybindingsConfig, snapshot: &KeymapSnapshot) -> String {
let mut out = String::new();
out.push_str("Keymap diagnostics\n");
out.push_str(&format!(
"Terminal: {}{}\n",
snapshot.terminal,
if snapshot.terminal_version.is_empty() {
String::new()
} else {
format!(" {}", snapshot.terminal_version)
}
));
out.push_str(&format!("OS: {}\n", snapshot.os));
let term_count = snapshot.from_source(KeySource::Terminal).count();
let sys_count = snapshot.from_source(KeySource::MacosSystem).count();
let app_count = snapshot.from_source(KeySource::ExternalApp).count();
out.push_str(&format!(
"Discovered bindings: {term_count} terminal, {sys_count} macOS system, {app_count} app\n",
));
if app_count > 0 {
let mut tools: Vec<&str> = snapshot
.from_source(KeySource::ExternalApp)
.map(|b| b.tool.as_str())
.filter(|t| !t.is_empty())
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect();
tools.dedup();
if !tools.is_empty() {
out.push_str(&format!("Apps scanned: {}\n", tools.join(", ")));
}
}
if term_count == 0 && sys_count == 0 && app_count == 0 {
out.push_str(
"\nNo machine bindings were discovered. jcode can read Ghostty bindings, macOS\n\
system shortcuts, and a few window managers (OmniWM, AeroSpace, skhd); other\n\
terminals and tools are not yet inspected, so conflicts there will not be\n\
detected.\n",
);
}
let conflicts = detect_conflicts(cfg, snapshot);
out.push('\n');
if conflicts.is_empty() {
out.push_str("No conflicts found between your jcode keybindings and the machine.\n");
} else {
out.push_str(&format!(
"{} potential conflict{} found:\n\n",
conflicts.len(),
if conflicts.len() == 1 { "" } else { "s" }
));
for c in &conflicts {
out.push_str(&render_conflict_block(c));
out.push('\n');
}
out.push_str(
"These keys may be captured by your terminal, macOS, or another app (window\n\
manager, launcher) before jcode sees them.\n\
To fix: rebind the jcode action in ~/.jcode/config.toml under [keybindings],\n\
or change the conflicting shortcut in the other app's settings.\n",
);
}
out
}
fn render_conflict_block(c: &Conflict) -> String {
let interceptor_desc = match c.interceptor.source {
KeySource::MacosSystem => format!("macOS: {}", c.interceptor.action),
KeySource::Terminal => {
if c.interceptor.action.is_empty() {
"terminal action".to_string()
} else {
format!("terminal: {}", c.interceptor.action)
}
}
KeySource::ExternalApp => {
let tool = if c.interceptor.tool.is_empty() {
"app"
} else {
c.interceptor.tool.as_str()
};
if c.interceptor.action.is_empty() {
tool.to_string()
} else {
format!("{tool}: {}", c.interceptor.action)
}
}
};
format!(
" ⚠ {key}\n jcode: {action} ({field} = \"{raw}\")\n taken by {interceptor}\n",
key = c.jcode.chord.display(),
action = c.jcode.action,
field = c.jcode.field,
raw = c.jcode.raw,
interceptor = interceptor_desc,
)
}
/// A compact one-line status string suitable for a startup notice, or `None`
/// when there are no conflicts.
pub fn render_status_line(cfg: &KeybindingsConfig, snapshot: &KeymapSnapshot) -> Option<String> {
let conflicts = detect_conflicts(cfg, snapshot);
if conflicts.is_empty() {
return None;
}
let keys: Vec<String> = conflicts
.iter()
.map(|c| c.jcode.chord.display())
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect();
Some(format!(
"Keybinding conflict: {} may be intercepted by your terminal/OS/apps. Run /keys for details.",
keys.join(", ")
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::keymap::KeyChord;
use crate::keymap::source::DiscoveredBinding;
fn snapshot_with(bindings: Vec<DiscoveredBinding>) -> KeymapSnapshot {
KeymapSnapshot {
version: 1,
captured_at: "0".to_string(),
os: "macos".to_string(),
terminal: "Ghostty".to_string(),
terminal_version: "1.3.1".to_string(),
bindings,
}
}
fn term(keys: &str, action: &str) -> DiscoveredBinding {
DiscoveredBinding {
chord: KeyChord::parse(keys).unwrap(),
source: KeySource::Terminal,
action: action.to_string(),
raw: format!("{keys}={action}"),
tool: String::new(),
}
}
#[test]
fn report_lists_conflicts_with_field_names() {
let cfg = KeybindingsConfig::default();
let snap = snapshot_with(vec![term("ctrl+tab", "next_tab")]);
let report = render_report(&cfg, &snap);
assert!(report.contains("Ghostty 1.3.1"));
assert!(report.contains("keybindings.model_switch_next"));
assert!(report.contains("next_tab"));
assert!(report.contains("Ctrl+Tab"));
}
#[test]
fn report_says_clean_when_no_conflicts() {
let cfg = KeybindingsConfig::default();
let snap = snapshot_with(vec![term("cmd+t", "new_tab")]);
let report = render_report(&cfg, &snap);
assert!(report.contains("No conflicts found"));
}
#[test]
fn status_line_present_only_on_conflict() {
let cfg = KeybindingsConfig::default();
let clean = snapshot_with(vec![term("cmd+t", "new_tab")]);
assert!(render_status_line(&cfg, &clean).is_none());
let dirty = snapshot_with(vec![term("ctrl+tab", "next_tab")]);
let line = render_status_line(&cfg, &dirty).unwrap();
assert!(line.contains("Ctrl+Tab"));
assert!(line.contains("/keys"));
}
}