use std::collections::HashSet;
use super::focus::Focus;
use super::keybind::{self, Action, BindingEntry};
#[derive(Debug, Clone)]
pub struct Entry {
pub key: String,
pub desc: String,
pub is_header: bool,
}
fn entry(key: impl Into<String>, desc: impl Into<String>) -> Entry {
Entry {
key: key.into(),
desc: desc.into(),
is_header: false,
}
}
fn header(label: impl Into<String>) -> Entry {
Entry {
key: label.into(),
desc: String::new(),
is_header: true,
}
}
fn blank() -> Entry {
Entry {
key: String::new(),
desc: String::new(),
is_header: true,
}
}
pub fn entries_for(focus: Focus) -> Vec<Entry> {
let mut out: Vec<Entry> = Vec::new();
out.extend(global_entries());
out.push(blank());
match focus {
Focus::Tree | Focus::SearchBar => {
out.push(header("─── Tree ───"));
out.extend(tree_entries());
}
Focus::Editor => {
out.push(header("─── Editor ───"));
out.extend(editor_entries());
}
Focus::Ai | Focus::AiPrompt => {
out.push(header("─── AI ───"));
out.extend(ai_entries());
}
}
out.push(blank());
out.push(header("─── F-keys + top-level (live keymap) ───"));
out.extend(live_chord_entries(keybind::Layer::TopLevel, focus));
out.push(blank());
out.push(header("─── Meta chords (live keymap) ───"));
out.extend(live_chord_entries(keybind::Layer::MetaSub, focus));
out.push(blank());
out.push(header("─── Bund chords (live keymap) ───"));
out.extend(live_chord_entries(keybind::Layer::BundSub, focus));
out.push(blank());
out.push(header("─── View chords (live keymap) ───"));
out.extend(live_chord_entries(keybind::Layer::ViewSub, focus));
out
}
fn global_entries() -> Vec<Entry> {
vec![
header("─── Global ───"),
entry("Ctrl+Q", "Quit (autosaves dirty paragraph)"),
entry("Tab / Shift+Tab", "Cycle Tree → Editor → AI"),
entry("Ctrl+1/2/3/4/5", "Focus Editor/Tree/AI/Search/AI-prompt"),
entry("Ctrl+T", "Focus Tree (Ctrl+2 alternative)"),
entry("Ctrl+S", "Save current paragraph"),
entry("Ctrl+/", "Focus Search bar"),
entry("Ctrl+I", "Focus AI prompt"),
entry("Ctrl+B", "Meta prefix (next key = action — see live section below)"),
entry("Ctrl+Z", "Bund prefix (next key = bund action — see live section below)"),
entry("F1", "Help-manual question (RAG over the Help book)"),
entry("F7", "Grammar check the open paragraph (→ AI pane)"),
entry("F9", "Cycle AI scope: None→Sel→Para→Sub→Chap→Book→None"),
entry("F10", "Toggle inference: Local↔Full (Help locked to Local)"),
entry("Esc", "Close overlay / cancel"),
]
}
fn tree_entries() -> Vec<Entry> {
vec![
entry("↑ / ↓ / Home / End", "Navigate"),
entry("PageUp / PageDown", "Jump by 10"),
entry("← / →", "Collapse / expand branch (← steps to parent)"),
entry("Enter", "Open paragraph (autosaves the previous one)"),
entry("F2", "Rename current node"),
entry("F3", "File picker — load file or import directory tree"),
blank(),
header("─ Hierarchy edits ─"),
entry("B / C / A / +", "Add book / chapter / subchapter / paragraph"),
entry("V / S / P", "Insert chapter / subchapter / paragraph after current"),
entry("D", "Delete branch at cursor (asks for confirmation)"),
entry("-", "Delete paragraph at cursor"),
blank(),
header("─ Reorder ─"),
entry("U", "Move current node up among siblings"),
entry("J", "Move current node down among siblings"),
blank(),
header("─ Folding ─"),
entry("← / →", "Collapse / expand cursor's branch"),
entry("Z", "Collapse cursor's enclosing subchapter"),
entry("X", "Collapse every expanded branch"),
entry("q", "Quit (autosaves if dirty)"),
]
}
fn editor_entries() -> Vec<Entry> {
vec![
entry("arrows", "Move cursor"),
entry("Ctrl+← / →", "Word back / forward"),
entry("Ctrl+Home / End", "File top / bottom"),
entry("Home / End", "Start / end of line"),
entry("PageUp / PageDown", "By paragraph"),
entry("Shift+arrows", "Extend linear selection"),
entry("Ctrl+A", "Select all"),
entry("Esc", "Clear in-buffer search (first press); cycle to Tree"),
blank(),
header("─ Clipboard ─"),
entry("Ctrl+C", "Copy selection (system clipboard)"),
entry("Ctrl+K", "Cut selection"),
entry("Ctrl+P", "Paste at cursor"),
blank(),
header("─ Edit ─"),
entry("Ctrl+U", "Undo"),
entry("Ctrl+Y", "Redo"),
entry("Ctrl+D", "Delete current line"),
entry("Ctrl+E", "Delete cursor → end of line"),
entry("Ctrl+W", "Delete cursor → start of line"),
entry("Ctrl+Backspace", "Delete previous word"),
blank(),
header("─ Find / Replace (regex) ─"),
entry("Ctrl+F", "Open Find (regex)"),
entry("Ctrl+X", "Repeat — next match / replace+next"),
entry("Ctrl+R", "Open Replace · or replace all (while in replace mode)"),
blank(),
header("─ Block selection ─"),
entry("Alt+arrows", "Extend rectangular selection"),
entry("Alt+C", "Copy rectangular block"),
blank(),
header("─ Files & snapshots ─"),
entry("F3", "Load file → replace buffer (Ctrl+B F also toggles split)"),
entry("F4 / Ctrl+F4", "Toggle split / accept split snapshot"),
entry("F5", "Snapshot the current paragraph (== Ctrl+B N)"),
entry(
"F6",
"Snapshot picker — ↑↓ navigate · Enter load · D / Del delete · Esc close",
),
entry("Ctrl+H / Ctrl+J", "(split only) scroll lower pane up / down"),
]
}
fn ai_entries() -> Vec<Entry> {
vec![
header("─ AI pane (apply a finished inference) ─"),
entry("r / R", "Replace editor selection with AI text"),
entry("i / I", "Insert AI text at cursor"),
entry("t / T", "Prepend AI text to top of paragraph"),
entry("g / G", "Grammar apply: extract corrected text, overwrite buffer"),
entry("b / B", "Append AI text to bottom"),
entry("c / C", "Copy AI text to clipboard"),
entry("Esc", "Bounce to AI prompt (mirror of AI prompt's Esc)"),
entry("q / Q", "Quit (autosaves if dirty)"),
blank(),
header("─ AI prompt input ─"),
entry("/", "Show prompt library picker (Tab / Enter to commit)"),
entry("Help! …", "Help-manual question (same as F1, RAG over Help)"),
entry("Enter", "Send (chat history is replayed automatically)"),
entry("Esc", "Bounce to AI pane to read the answer"),
blank(),
header("─ Chat session ─"),
entry("F9", "Cycle scope: None / Sel / Para / Sub / Chap / Book"),
entry("F10", "Toggle inference: Local ↔ Full (Help locked to Local)"),
]
}
fn live_chord_entries(layer: keybind::Layer, focus: Focus) -> Vec<Entry> {
let bindings = keybind::read();
let table: &Vec<BindingEntry> = match layer {
keybind::Layer::MetaSub => &bindings.meta_sub,
keybind::Layer::BundSub => &bindings.bund_sub,
keybind::Layer::ViewSub => &bindings.view_sub,
keybind::Layer::TopLevel => &bindings.top_level,
};
let prefix = match layer {
keybind::Layer::MetaSub => bindings.meta_prefix.to_display_string(),
keybind::Layer::BundSub => bindings
.bund_prefix
.map(|c| c.to_display_string())
.unwrap_or_else(|| "(bund prefix disabled)".to_string()),
keybind::Layer::ViewSub => bindings
.view_prefix
.map(|c| c.to_display_string())
.unwrap_or_else(|| "(view prefix disabled)".to_string()),
keybind::Layer::TopLevel => String::new(),
};
let mut out: Vec<Entry> = Vec::new();
let mut seen: HashSet<String> = HashSet::new();
for be in table.iter() {
if !be.scope.matches(focus) {
continue;
}
if matches!(be.action, Action::None) {
continue;
}
let label = be.action.label();
if label.is_empty() {
continue;
}
if !seen.insert(label.clone()) {
continue;
}
let chord = if prefix.is_empty() {
be.chord.to_display_string()
} else {
format!("{} {}", prefix, be.chord.to_display_string())
};
let desc = be.action.description();
let desc = if desc.is_empty() { label } else { desc };
out.push(entry(chord, desc));
}
if out.is_empty() {
out.push(entry("—", "no chords active in this pane"));
}
out
}