Skip to content

Route ESC to terminal in alt-screen apps under evil-ghostel (#215)#216

Merged
dakra merged 1 commit into
mainfrom
esc-routing
May 1, 2026
Merged

Route ESC to terminal in alt-screen apps under evil-ghostel (#215)#216
dakra merged 1 commit into
mainfrom
esc-routing

Conversation

@dakra
Copy link
Copy Markdown
Owner

@dakra dakra commented May 1, 2026

Summary

Fixes #215. Pressing ESC in evil insert state inside a ghostel buffer ran evil-normal-state even when the inner app (vim, less, htop, nvim, etc.) needed the keystroke.

  • New buffer-local routing mode controlled by evil-ghostel-escape: auto (default) | terminal | evil. auto checks DECSET 1049 (alt-screen) to decide.
  • Interactive evil-ghostel-toggle-send-escape: no-arg cycles auto → terminal → evil → auto; numeric prefix 1/2/3 sets directly. Out-of-range numeric prefix signals user-error.
  • Terminal-bound ESC snaps the viewport via ghostel--snap-to-input to match the invariant every other key in ghostel-mode-map holds.
  • Evil-bound fallback lands on evil-force-normal-state when the user's <escape> binding is missing or a chord prefix (e.g. evil-escape's jk), so the keystroke is never silently dropped.

Test plan

  • make -j4 all — 218 ERT tests pass, 11 new in test/evil-ghostel-test.el:
    • dispatcher branches: terminal-always / evil-always / auto+altscreen / auto+no-altscreen
    • viewport snap on terminal-bound ESC
    • evil fallback when lookup-key returns nil (chord/rebound case)
    • toggle cycle, numeric prefix set, invalid prefix user-error
    • per-buffer isolation (sets terminal in buffer A, evil in buffer B, re-enters A)
    • mode init reads evil-ghostel-escape defcustom
  • Live verify in M-x ghostel:
    • vim insert mode → ESC leaves insert (auto path).
    • Toggle to evil mid-vim, ESC switches evil state instead.
    • opencode (alt-screen but user wants evil ESC) → toggle to evil works.
    • Shell prompt with no alt-screen → ESC switches evil to normal (auto path, default).
    • M-2 evil-ghostel-toggle-send-escape directly forces terminal.

Pressing ESC in evil insert state inside ghostel ran
`evil-normal-state' even when the inner app (vim, less, htop, …) needed
the keystroke.  Add a buffer-local routing mode `auto'/`terminal'/`evil'
controlled by `evil-ghostel-escape' and a toggle command with numeric
prefix support.  `auto' uses DECSET 1049 to decide.  Terminal-bound ESC
snaps to the live viewport like every other typed key; the evil-bound
fallback lands on `evil-force-normal-state' when the user's
`<escape>' binding is missing or a chord prefix.

Fixes #215
(setq evil-ghostel--escape-mode target)
(message "evil-ghostel ESC mode: %s" target)))

(evil-define-key* 'insert evil-ghostel-mode-map
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd imagine there's a scenario where you're in normal state in emacs but want ESC to be sent to the insert state in the nested emacs/vim or maybe vice versa. Not sure because we're dealing with 2 modal states that could be not synchronized.

Or maybe in a non normal/insert state (like visual/motion/etc?) state but wanting ESC to pass through.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since I'm not an evil user I'm not 100% I understand what you mean, can you rephrase or make a suggestion for a fix?

btw (maybe it helps), you can always C-c C-q <esc> to send the next key to the terminal whatever it is ( in this case).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

e.g. if you're in emacs (outer) and vim (inner) [start emacs -> start ghostel -> start vim inside ghostel

maybe using 'insert state is ok e.g. for the user to interact with ghostel, they need to be in insert state, which then triggers your function above

the cycling might be strange though

e.g. emacs+evil [normal state] -> go to insert state -> press i in vim to go to insert state -> press ESC. this cycles the ESC to the next thing.

But you want this instead:

emacs+evil [normal state] -> go to insert state in emacs -> press i in vim to go to insert state -> press ESC -> (normal state in vim) -> press i again to go to insert state in vim -> press ESC to go to normal state in vim -> press i again to go to insert state in vim -> and so on.

^ I think this case is more likely to happen.

If you do that though, you still need a way to get back to the outer emacs (e.g. to get back to normal state in emacs) but once the inner vim is taking ESC inputs, it really won't know which ESC you want to send to so you can't do it automagically.

suggestion for a fix?

No suggestion, the way we do it in vterm is kind of a "this is too complicated, just let the user decide when they want what". If there's a more clever way to handle this, I'm all for it.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

emacs+evil [normal state] -> go to insert state in emacs -> press i in vim to go to insert state -> press ESC -> (normal state in vim) -> press i again to go to insert state in vim -> press ESC to go to normal state in vim -> press i again to go to insert state in vim -> and so on.

^ I think this case is more likely to happen.

If you do that though, you still need a way to get back to the outer emacs (e.g. to get back to normal state in emacs) but once the inner vim is taking ESC inputs, it really won't know which ESC you want to send to so you can't do it automagically.

But doesn't the auto-mode kinda address that in that is sends the ESC so long to vim as long as vim is open. You close it to type something in the shell, ESC gets routed to evil?!

And if you want your first case or not have ESC forwardet to the terminal when an alt-screen app is running, you have to manually change the routing (like in your vterm solution right now)?!

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might want to leave the window/leave vim running but still be able to get back to Normal state in emacs. I think this is a relatively common use case so won't describe specific examples. :)

And if you want your first case or not have ESC forwardet to the terminal when an alt-screen app is running, you have to manually change the routing (like in your vterm solution right now)?!

Yeah, I think it's a more manual/intentional step.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might want to leave the window/leave vim running but still be able to get back to Normal state in emacs. I think this is a relatively common use case so won't describe specific examples. :)

ok.
But anyway, should we merge this PR as it provides at least a workaround (and with the auto-mode hopefully even a slightly better hack than vterm)?
(and this might be me not using it, but if you just want to get into normal state emacs, you
can of course just bind evil-force-normal-state so something like C-c esc or whatever.)

And if you or some other evil user has a better idea how that "workflow" could be implemented I'm open for issues or PRs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, sounds good.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, thanks.

And happy to discuss any ideas/PRs, especially evil related as I don't test this integration very much.

@dakra dakra merged commit fdd369d into main May 1, 2026
36 of 40 checks passed
@dakra dakra deleted the esc-routing branch May 3, 2026 07:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sending ESC to ghostel with evil-mode on

2 participants