Files
ghostty-rs/crates/ghostty-input/src/helpgen_actions.rs
T
ddidderr 3afe938b95 feat(input): port keyboard, keybinding and mouse input handling
Ports ZIG/src/input (except macOS keymaps) into ghostty-input:
- key, key_mods (incl. RemapSet), the physical keycode table
- key_encode: legacy, modifyOtherKeys and kitty keyboard protocol
  encoders, function key tables
- Binding: all keybinding actions with their user-visible docs (for
  +list-actions), triggers, sets with sequences, chains, leader keys and
  key tables, and formatting back to config strings
- command palette defaults, mouse, paste safety checks, kitty helpers,
  links, helpgen action docs

Deviations (docs/rewrite/notes/ghostty-input.md): Mods.super is named
super_; case folding uses char::to_lowercase plus Ghostty's special
cases instead of uucode tables; chain parents are trigger paths instead
of pointers; a Zig bug where chained leaves inside sequences corrupted the
shared prefix when formatting is fixed (covered by a test).

Integration left: building encode/paste Options from terminal modes,
and implementing the EntryFormatter trait in ghostty-config.

Test Plan:
- cargo test -p ghostty-input (220 tests; 8 macOS-only skipped)
- cargo clippy -p ghostty-input --all-targets -- -D warnings
2026-10-11 21:56:07 +02:00

149 lines
4.3 KiB
Rust

//! This module is a help generator for keybind actions documentation.
//! It can generate documentation in different formats (plaintext for CLI,
//! markdown for website) while maintaining consistent content.
//! (ZIG/src/input/helpgen_actions.zig)
//!
//! The docs themselves live in [`ActionKey::docs`] (the Zig version reads
//! them from the build-time generated `help_strings`).
use crate::binding::ActionKey;
/// Format options for generating keybind actions documentation
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Format {
/// Plain text output with indentation
Plaintext,
/// Markdown formatted output
Markdown,
}
impl Format {
fn format_field_name(self, w: &mut String, field_name: &str) {
match self {
Format::Plaintext => {
w.push_str(field_name);
w.push_str(":\n");
}
Format::Markdown => {
w.push_str("## `");
w.push_str(field_name);
w.push_str("`\n");
}
}
}
fn format_doc_line(self, w: &mut String, line: &str) {
match self {
Format::Plaintext => {
w.push_str(" ");
w.push_str(line);
w.push('\n');
}
Format::Markdown => {
w.push_str(line);
w.push('\n');
}
}
}
fn header(self) -> Option<&'static str> {
match self {
Format::Plaintext => None,
Format::Markdown => Some(
"---\n\
title: Keybinding Action Reference\n\
description: Reference of all Ghostty keybinding actions.\n\
editOnGithubLink: https://github.com/ghostty-org/ghostty/edit/main/src/input/Binding.zig\n\
---\n\
\n\
This is a reference of all Ghostty keybinding actions.\n\
\n\
\n",
),
}
}
}
/// Generate keybind actions documentation with the specified format
pub fn generate(writer: &mut String, format: Format, show_docs: bool) {
if let Some(header) = format.header() {
writer.push_str(header);
}
let mut stream = String::new();
for &key in ActionKey::ALL {
let name = key.name();
if name.starts_with('_') {
continue;
}
// Write previously stored doc comment below all related actions
// (every action has a doc comment).
if show_docs {
writer.push_str(&stream);
writer.push('\n');
stream.clear();
}
if show_docs {
format.format_field_name(writer, name);
} else {
writer.push_str(name);
writer.push('\n');
}
if show_docs {
let mut iter = key.docs().split('\n').peekable();
while let Some(s) = iter.next() {
// If it is the last line and empty, then skip it.
if iter.peek().is_none() && s.is_empty() {
continue;
}
format.format_doc_line(&mut stream, s);
}
}
}
// Write any remaining buffered documentation
if !stream.is_empty() {
writer.push_str(&stream);
}
}
/// Convenience: generate into a fresh string.
pub fn generate_string(format: Format, show_docs: bool) -> String {
let mut s = String::new();
generate(&mut s, format, show_docs);
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plaintext_with_docs_lists_all_actions() {
let s = generate_string(Format::Plaintext, true);
assert!(s.starts_with("\nignore:\n"));
assert!(s.contains("ignore:\n Ignore this key combination.\n"));
for &k in ActionKey::ALL {
assert!(s.contains(&format!("{}:\n", k.name())));
}
}
#[test]
fn names_only() {
let s = generate_string(Format::Plaintext, false);
assert_eq!(s.lines().count(), ActionKey::ALL.len());
assert_eq!(s.lines().next(), Some("ignore"));
}
#[test]
fn markdown_header_and_field() {
let s = generate_string(Format::Markdown, true);
assert!(s.starts_with("---\ntitle: Keybinding Action Reference\n"));
assert!(s.contains("## `unbind`\n"));
}
}