Skip to content

[Bug]: Help char after a prefix key with no binding (e.g. C-c C-h) exits Neomacs #576

Closed

Description

@kaede-0323

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

  1. Run neomacs -Q -nw (the GUI behaves the same).
  2. In *scratch*, type C-c C-h.
  3. "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":

// 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:

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:

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

  1. self-assigned this
    on Oct 16, 2026
  2. added this to the v0.0.20 milestone on Oct 16, 2026
  3. eval-exec commented on Oct 16, 2026

    @eval-exec
    Owner

    Thank you for the report. Reproducing and investigating.

  4. eval-exec commented on Oct 17, 2026

    @eval-exec
    Owner

    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, and C-c ? against GNU Emacs 31.1, with unit, oracle, TUI, and GUI coverage on Linux.

    Please try the latest main when convenient, and let us know if you still see any problems. Thanks again for helping improve Neomacs’s compatibility with GNU Emacs!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingstatus: needs-triageNot yet reviewed by a maintainer

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions