Repository navigation
[Bug]: Help char after a prefix key with no binding (e.g. C-c C-h) exits Neomacs #576
Description
Bug summary
Pressing help-char (or any event in help-event-list, such as ? or <f1>) after a prefix key closes Neomacs when that combination has no binding of its own. describe-prefix-bindings runs and prepares the *Help* buffer, then the command loop exits and the process ends with status 0. Nothing is reported as an error; the log only says "Command loop exited normally" and "Neomacs exited cleanly".
Combinations that have a real binding are not affected, because they never reach the prefix-help path: C-h C-h (help-for-help), or C-c C-h after (global-set-key (kbd "C-c C-h") #'forward-char).
It happens with -Q, both in the GUI (X11) and with -nw.
Steps to reproduce
- Run
neomacs -Q -nw(the GUI behaves the same). - In
*scratch*, typeC-c C-h. - "Type C-x 1 to delete the help window." is logged, then Neomacs exits with status 0.
C-x 4 C-h and C-c ? do the same.
Expected behavior
Same as GNU Emacs: a *Help* window lists the bindings that start with C-c, and the session continues.
Neomacs --version output
Neomacs 0.0.19
Git commit: unknown
Source date: unknown
Build: release/bench for x86_64-unknown-linux-gnu with rustc 1.96.1
Standalone Rust binary for Neomacs (no C dependency)Platform
Linux (X11)
Install source
Built from source, some other way
Extra context
Cause, as far as I can tell
dispatch_prefix_help_command calls prefix-help-command itself, then returns an empty key sequence with a nil binding, expecting command_loop_1 to treat that as "nothing to dispatch":
neomacs/crates/neovm-core/src/keyboard.rs
Lines 3802 to 3807 in 6abedf0
| // Return an empty key sequence — command_loop_1 treats | |
| // that as "nothing to dispatch this tick" and immediately | |
| // reads the next key. | |
| self.command_loop | |
| .set_command_key_sequences(Vec::new(), Vec::new()); | |
| Ok(Some((Vec::new(), Value::NIL))) |
read_command_key_sequence_with_options treats exactly that result as end of input:
neomacs/crates/neovm-core/src/keyboard.rs
Lines 4540 to 4552 in 6abedf0
| let (keys, binding) = self.read_key_sequence_with_options(options)?; | |
| if keys.is_empty() && binding.is_nil() { | |
| // The zero-length result is a macro-iteration boundary when the | |
| // macro is (still) exhausted -- GNU's `at_end_of_macro_p` check | |
| // ran mid-read -- and an ordinary EOF from command input | |
| // otherwise. Keeping the typed distinction lets `command_loop_1` | |
| // end the iteration instead of dispatching an undefined sequence. | |
| let end = if self.command_input_kbd_macro_iteration_is_exhausted() { | |
| CommandKeySequenceEnd::KeyboardMacroIteration | |
| } else { | |
| CommandKeySequenceEnd::Input | |
| }; | |
| Ok(CommandKeySequenceRead::End(end)) |
so command_loop_1 returns:
neomacs/crates/neovm-core/src/emacs_core/runtime/eval/command_loop.rs
Lines 734 to 737 in 6abedf0
| if input_end.is_some() { | |
| self.assign("this-command", Value::NIL); | |
| return Ok(Value::NIL); | |
| } |
GNU does not run the help command inside read_key_sequence. It records prefix-help-command as the command for the sequence and returns the whole sequence, help char included:
https://github.com/emacs-mirror/emacs/blob/8f535370b9efbc91673b20c6987a5cae4f6dc562/src/keyboard.c#L11324-L11329
command_loop_1 then treats only a zero-length sequence as end of file, and runs the recorded command like any other, with pre-command-hook / post-command-hook and this-command:
https://github.com/emacs-mirror/emacs/blob/8f535370b9efbc91673b20c6987a5cae4f6dc562/src/keyboard.c#L1439-L1443
https://github.com/emacs-mirror/emacs/blob/8f535370b9efbc91673b20c6987a5cae4f6dc562/src/keyboard.c#L1469
describe-prefix-bindings drops the last event of (this-command-keys) itself. Returning the current sequence with prefix-help-command as its binding, instead of calling it and returning an empty sequence, would match GNU.
Debug log excerpt
RUST_LOG=debug NEOMACS_LOG=info NEOMACS_LOG_FILE=... neomacs -nw -Q, then C-c C-h. Span prefixes and timestamps are trimmed.
DEBUG neomacs_display_runtime::tty_input: tty_input: raw byte batch RawTtyBytes { bytes: [3], emacs_frame_id: 0 }
DEBUG neovm_core::keyboard: read_key_sequence: event=3 starting translation
DEBUG neovm_core::keyboard: read_key_sequence: looking up binding for ["3"]
DEBUG neovm_core::keyboard: read_key_sequence: binding=(keymap (2 . elisp-byte-compile-buffer) (5 . elisp-eval-region-or-buffer))
DEBUG neomacs_display_runtime::tty_input: tty_input: raw byte batch RawTtyBytes { bytes: [8], emacs_frame_id: 0 }
DEBUG neovm_core::keyboard: read_key_sequence: event=8 starting translation
DEBUG neovm_core::keyboard: read_key_sequence: looking up binding for ["3", "8"]
DEBUG neovm_core::keyboard: read_key_sequence: binding=nil
INFO neovm_core::emacs_core::load: load .../share/neomacs/lisp/help-fns.elc
...
INFO neovm_core::emacs_core::builtins::misc_pure: msg=Type C-x 1 to delete the help window.
DEBUG neovm_core::emacs_core::eval::command_loop: command_loop_inner: result=true, propagating
INFO neomacs: Command loop exited normally
INFO neomacs: Shutting down...
INFO neomacs::tty_init: TTY terminal restored
INFO neomacs: Neomacs exited cleanly duration_ms=5614 command_args=[..., "-nw", "-Q"]
A Lisp-side workaround that avoids the exit: (setq prefix-help-command nil), plus an advice on undefined that calls describe-prefix-bindings when the last event is a help event.
Activity
eval-exec commentedon Oct 16, 2026
Thank you for the report. Reproducing and investigating.
eval-exec commentedon Oct 17, 2026
Thanks for the clear reproduction and detailed analysis—you correctly identified the cause.
Fixed in #577, now merged. Prefix help now returns the complete key sequence for normal command-loop dispatch, including command hooks and shift-translation state.
I verified
C-c C-h,C-x 4 C-h, andC-c ?against GNU Emacs 31.1, with unit, oracle, TUI, and GUI coverage on Linux.Please try the latest
mainwhen convenient, and let us know if you still see any problems. Thanks again for helping improve Neomacs’s compatibility with GNU Emacs!