Skip to content

fix: widen eat pane in 1-session layout to fit heavy TUIs#59

Merged
phasetr merged 2 commits into
mainfrom
fix/window-layout-split-ratio
May 8, 2026
Merged

fix: widen eat pane in 1-session layout to fit heavy TUIs#59
phasetr merged 2 commits into
mainfrom
fix/window-layout-split-ratio

Conversation

@phasetr
Copy link
Copy Markdown
Owner

@phasetr phasetr commented May 8, 2026

Summary

  • The default 50/50 split in `enkan-repl-setup-1session-layout` left the eat pane too narrow for heavier TUI applications (e.g. claude code CLI), causing the TUI's renderer to lock up under repaints
  • Adjusts `split-window-right` to allocate ~35% of the frame width to the center file pane (left), giving the eat pane (right) ~65%

Test plan

  • Manual: 1-session layout with claude no longer freezes the eat buffer

🤖 Generated with Claude Code

phasetr and others added 2 commits May 9, 2026 07:42
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@phasetr phasetr merged commit 54d0d2f into main May 8, 2026
1 check failed
@phasetr phasetr deleted the fix/window-layout-split-ratio branch May 8, 2026 22:43
phasetr pushed a commit that referenced this pull request May 9, 2026
Several enkan-repl parser/lookup helpers in enkan-repl-utils.el did not
account for Emacs's UNIQUE flag on `rename-buffer', which appends <2>,
<3>, ... when multiple eat sessions are opened for the same project path.
This left those buffers undetectable by path extraction and lookup.

Changes:
- enkan-repl--buffer-name->path: regex extended to accept optional <N>
  suffix; also tightened to require non-empty path (prevents misparsing
  malformed "*ws:01 enkan:*").
- enkan-repl--buffer-name->instance: new helper returning 1-based instance
  index (1 when no suffix, N when "<N>", nil for non-enkan names).
- enkan-repl--path->buffer-name: optional INSTANCE argument; appends "<N>"
  when N >= 2.
- enkan-repl--buffer-matches-directory: optional INSTANCE filter; matches
  on extracted path so buffers with <N> suffix are recognized; falls back
  to "any instance" when INSTANCE is nil.

Tests:
- Four new ERTs covering: multi-instance path extraction, instance index
  extraction, instance-aware buffer-name generation, and directory match
  with optional instance filter.
- Fixed pre-existing failing test test-workspace-window-recreation-after-switch
  (mock for split-window-right needed to accept optional SIZE arg added by
  PR #59 layout tweak).

Test suite: 162/162 pass (previously 157/158 with one unrelated failure).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
phasetr added a commit that referenced this pull request May 9, 2026
* chore: start feat/terminal-backend-abstraction

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(terminal): introduce backend dispatch layer (eat backend only)

New file enkan-repl-terminal.el centralizes terminal-backend-specific
operations behind a generic API selected by `enkan-repl-terminal-backend'
(default 'eat for full backward compatibility).

Public API:
- enkan-repl--terminal-start
- enkan-repl--terminal-send / send-key
- enkan-repl--terminal-alive-p
- enkan-repl--terminal-list / kill / display

Phase 1 ships only the eat backend, which wraps existing in-tree behavior
without changing semantics. tmux backend functions are stubs that raise
user-error until a later phase.

Existing call sites in enkan-repl.el are not yet rewired to the new
dispatch; that follows in subsequent commits.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(parser): handle multi-instance buffer name suffix <N>

Several enkan-repl parser/lookup helpers in enkan-repl-utils.el did not
account for Emacs's UNIQUE flag on `rename-buffer', which appends <2>,
<3>, ... when multiple eat sessions are opened for the same project path.
This left those buffers undetectable by path extraction and lookup.

Changes:
- enkan-repl--buffer-name->path: regex extended to accept optional <N>
  suffix; also tightened to require non-empty path (prevents misparsing
  malformed "*ws:01 enkan:*").
- enkan-repl--buffer-name->instance: new helper returning 1-based instance
  index (1 when no suffix, N when "<N>", nil for non-enkan names).
- enkan-repl--path->buffer-name: optional INSTANCE argument; appends "<N>"
  when N >= 2.
- enkan-repl--buffer-matches-directory: optional INSTANCE filter; matches
  on extracted path so buffers with <N> suffix are recognized; falls back
  to "any instance" when INSTANCE is nil.

Tests:
- Four new ERTs covering: multi-instance path extraction, instance index
  extraction, instance-aware buffer-name generation, and directory match
  with optional instance filter.
- Fixed pre-existing failing test test-workspace-window-recreation-after-switch
  (mock for split-window-right needed to accept optional SIZE arg added by
  PR #59 layout tweak).

Test suite: 162/162 pass (previously 157/158 with one unrelated failure).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(session): track multi-instance index in session-list entries

Session-list entries previously stored just a project-name string in the
cdr.  This made it impossible to distinguish multiple terminal instances
of the same project, which Emacs's UNIQUE rename-buffer flag silently
permits and which the user actually relies on.

Entry format:
- Single instance (default):   (session-number . "project-name")    [unchanged]
- Multi-instance (index >= 2): (session-number . ("project-name" . N))

The legacy string form is kept for instance=1 to preserve serialization
compatibility with existing state files and ERTs.  All readers go through
the new accessors so on-disk format can evolve transparently.

New helpers in enkan-repl-utils.el:
- enkan-repl--session-entry-project   (extract project name; both forms)
- enkan-repl--session-entry-instance  (extract instance index, defaults 1)
- enkan-repl--make-session-entry-value (constructor)

Updated:
- enkan-repl--register-session: optional INSTANCE argument
- enkan-repl--terminate-all-session-buffers: passes instance to lookup
- enkan-repl--get-buffer-for-directory: optional INSTANCE filter
- enkan-repl--get-current-session-state-info: shows <N> in display
- examples/window-layouts.el: setup-window-eat-buffer-pure honors instance
  in generated buffer name

Test fixes (mock signature compatibility):
- test-enkan-repl--get-buffer-for-directory-with-workspace
- test-enkan-repl-terminate-workspace-buffers

Test suite: 162/162 pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(enkan-repl): route eat call sites through terminal abstraction

enkan-repl-start-eat, enkan-repl--send-primitive-action, and
enkan-repl--send-text-to-buffer no longer call eat directly.  They go
through enkan-repl--terminal-* dispatch so the active backend (eat or
tmux, per `enkan-repl-terminal-backend') handles transport.

Changes:
- enkan-repl.el: require enkan-repl-terminal at load time.
- enkan-repl-start-eat: use enkan-repl--terminal-start to spawn the
  session, then enkan-repl--terminal-id-instance to capture the
  multi-instance index for register-session.
- enkan-repl--send-primitive-action: collapse to one
  enkan-repl--terminal-send call (newline=t for text actions, nil
  otherwise).
- enkan-repl--send-text-to-buffer: same dispatch, plus alive check via
  enkan-repl--terminal-alive-p.  Cursor positioning timer kept, gated on
  bufferp so it remains a no-op for non-buffer terminal ids.
- enkan-repl-terminal.el: add enkan-repl--terminal-id-instance dispatch
  (eat: parse buffer-name <N> suffix; tmux stub returns 1).

Test fixes:
- Add per-test cleanup of stale "*ws:NN enkan:...*" buffers in tests
  that exercise enkan-repl-start-eat: leftover same-named buffers from
  earlier tests would otherwise inflate the detected instance index.
- test-multiple-eat-sessions-in-workspace: assert second instance
  produces the new (proj . 2) cons form (correctness improvement: the
  function now actually distinguishes the second instance).

Test suite: 162/162 pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(terminal): implement tmux backend (start/send/alive/list/kill)

Adds the tmux backend implementations behind the existing
enkan-repl--terminal-* dispatch.  When `enkan-repl-terminal-backend' is
set to `tmux', enkan-repl spawns and drives an external tmux server
instead of running an in-Emacs eat session, so heavy TUIs (claude,
codex, ...) no longer compete with Emacs' main loop for repaint.

Mapping:
- workspace NN -> tmux session "enkan-NN"
- per project -> tmux window in that session, named after dir basename
- multi-instance same-dir -> window names "lat", "lat-2", "lat-3", ...

Configuration:
- enkan-repl-tmux-executable     (default "tmux")
- enkan-repl-tmux-session-prefix (default "enkan-")

Implementations:
- enkan-repl--terminal-tmux-start: ensure session, add window for dir
- enkan-repl--terminal-tmux-send: send-keys -l (literal) + optional Enter
- enkan-repl--terminal-tmux-send-key: maps escape/enter/digit
- enkan-repl--terminal-tmux-alive-p: has-session + window membership
- enkan-repl--terminal-tmux-list: window targets in current ws session
- enkan-repl--terminal-tmux-kill: tmux kill-window
- enkan-repl--terminal-tmux-id-instance: parse "-N" from window name
- enkan-repl--terminal-tmux-display: still stub (mirror buffer is later)

Tests:
- test/enkan-repl-terminal-test.el: 9 new ERTs covering pure helpers
  (id parsing, base-name derivation, session-name builder,
  next-instance-name with mocked list-windows, eat id-instance via the
  abstraction).  Tests requiring a real tmux server are skipped.

Test suite: 171/171 pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(state): persist workspace state to disk + tmux reconcile

New file enkan-repl-state.el adds atomic disk persistence for
`enkan-repl--workspaces' so tmux sessions can be reattached after
Emacs restart / crash.  Also valuable as bookkeeping for the eat
backend even though those terminals don't survive Emacs.

API:
- enkan-repl-state-save (&optional FILE)        atomic write via .tmp+rename
- enkan-repl-state-load (&optional FILE)        validated read, nil on error
- enkan-repl-state-tmux-reconcile (&optional FILE)
    classifies state vs live tmux server (A: both / B: state-only /
    C: tmux-only) and returns reattach plan governed by
    `enkan-repl-state-recovery-policy' (reattach / recreate / prompt /
    ignore).

Configuration:
- enkan-repl-state-file              (default ~/.emacs.d/enkan-repl-state.eld)
- enkan-repl-state-recovery-policy   (default 'reattach)

Integration:
- enkan-repl.el requires enkan-repl-state at load time when available.
- enkan-repl--save-workspace-state mirrors to disk on every save (best
  effort, ignored in noninteractive/batch mode so tests don't clobber
  the user's real state file).
- kill-emacs-hook performs a final flush so even ungraceful exits leave
  the latest state on disk.

Tests:
- test/enkan-repl-state-test.el: 8 ERTs covering payload build /
  validate, save+load round-trip, missing-file and corrupt-file
  handling, atomic write (no .tmp leftover), and reconcile
  classification with mocked tmux session listing.

Test suite: 179/179 pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(terminal/tmux): add mirror buffer + attach helper

Phase 7 of the terminal-backend abstraction work.  Two complementary
ways to surface tmux sessions inside Emacs:

1. Mirror buffer (in-Emacs, read-only)
   - enkan-repl-tmux-mirror               (defcustom, default t)
   - enkan-repl-tmux-mirror-interval       idle refresh seconds (1.0)
   - enkan-repl-tmux-mirror-history-lines  scrollback per refresh (500)
   - enkan-repl--terminal-tmux-display now creates / pops to a
     "*tmux <id>*" buffer driven by an idle timer that runs
     `tmux capture-pane -p -J -S -<lines> -t <id>'.
   - Buffer-local kill-buffer-hook cancels the timer so killing the
     mirror buffer cleanly stops polling.
   - When the underlying pane disappears, refresh stops itself and
     marks the buffer "[tmux pane closed]".

2. External attach
   - enkan-repl-tmux-attach (interactive)  ws optional, defaults to
     current workspace.
   - enkan-repl-tmux-attach-command  defcustom: nil (auto), or
     a string template with %s -> session, or a function of one arg.
   - Auto default per system-type:
       darwin -> osascript -> Terminal.app -> "tmux attach -t <session>"
       other  -> "$SHELL -ic 'tmux attach -t <session>'"

Tests:
- test/enkan-repl-terminal-test.el: 2 new ERTs (mirror buffer naming,
  default attach command shape per OS).

Test suite: 181/181 pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore(terminal): satisfy byte-compile/checkdoc

- Add declare-function for enkan-repl--terminal-* and enkan-repl-state-*
  to silence "not known to be defined" errors when enkan-repl.el is
  byte-compiled with -Q (no autoloads loaded yet).
- Add declare-function for enkan-repl--session-entry-* and
  enkan-repl--buffer-name->instance helpers introduced in earlier
  commits.
- Update declare-function arity for enkan-repl--buffer-matches-directory
  to reflect optional INSTANCE argument.
- Wrap a too-long docstring line in enkan-repl--register-session
  (>80 cols) to satisfy checkdoc.

Test suite: 181/181 pass.  byte-compile clean.  checkdoc clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(terminal/tmux): kill helpers + mirror-buffer name compat with eat

User-facing additions:
- enkan-repl-tmux-kill-session     interactive: kill one enkan-* session
                                   (auto-pick if only one, completing-read otherwise)
- enkan-repl-tmux-kill-all-enkan   interactive: confirm + kill all enkan-* sessions

Mirror buffer naming fix:
- enkan-repl--terminal-tmux--mirror-buffer-name now returns the same
  *ws:NN enkan:/path/* form as the eat backend (with optional <N>
  multi-instance suffix) by querying the tmux pane's current dir via
  `tmux display-message #{pane_current_path}`.  Falls back to
  "*tmux <id>*" when the path is unavailable.
- Aligns implementation with docs/terminal-backend-abstraction.md and
  lets existing buffer-list-based layout / lookup code discover tmux
  mirror buffers transparently.

Tests:
- Updated mirror-buffer-name ERT to cover the new format and the
  fallback path.

Test suite: 181/181 pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(terminal): make layout/lookup discover tmux mirror buffers

Closes the gap that caused enkan-repl-setup to "do nothing visible" on
tmux backend: tmux sessions were created externally but the in-Emacs
layout / count code (operating on buffer-list) had no way to find them.

Changes:
- enkan-repl-start-eat: when backend is tmux, eagerly create the mirror
  buffer via enkan-repl--terminal-tmux--mirror-make so it appears in
  buffer-list before layout setup runs.  No pop-up here; layout decides
  display.
- enkan-repl--buffer-alive-as-terminal-p: new backend-agnostic alive
  predicate.  Considers buffer "live as terminal" if any of:
    1. buffer-local eat--process is a live process (kephale eat)
    2. get-buffer-process returns a live process (akib eat / general)
    3. buffer-local enkan-repl--tmux-mirror-id binds a live tmux pane
- enkan-repl--get-workspace-buffer-count-pure (utils):
  delegates the per-buffer liveness check to the new predicate.
- enkan-repl--get-available-buffers (enkan-repl):
  same.
- declare-function / defvar additions for cross-file references so
  byte-compile -Q stays clean.

Net effect: enkan-repl-setup-current-project-layout (C-M-l) and the
send-line buffer resolution see tmux sessions as first-class siblings
of eat sessions, while existing eat behavior is preserved (the
predicate's first two branches cover the original eat criteria).

Test suite: 181/181 pass.  byte-compile clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(terminal/tmux): normalize mirror buffer path with trailing slash

send-line returned "No active enkan sessions found" on tmux backend
even after start, because the mirror buffer name and the resolve lookup
used different path normalizations:

- enkan-repl-target-directories entries usually carry a trailing "/",
  so `enkan-repl--path->buffer-name' yields ".../path/*" (with slash).
- `tmux display-message #{pane_current_path}' returns the shell cwd
  without a trailing slash, yielding "...path*" (no slash).

`enkan-repl--resolve-send-target' looked up the buffer via
`get-buffer (enkan-repl--path->buffer-name path)', which produced the
slash-form, while the mirror buffer's actual name was the no-slash
form -- so the lookup missed every time.

Fix in `enkan-repl--terminal-tmux--mirror-buffer-name': pass pane-cwd
through `file-name-as-directory' so the buffer name always carries the
trailing slash and matches what target-directories-derived lookups
produce.  Idempotent for paths that already end with "/".

Test:
- Updated mirror-buffer-name ERT to assert the trailing-slash form for
  both inputs (with and without trailing slash) and for the
  multi-instance suffix case.

Test suite: 181/181 pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(terminal): coerce buffer identifiers when dispatching to tmux backend

Send/kill/etc. paths frequently obtain a target by buffer-list lookup
(e.g. enkan-repl--resolve-send-target returns the mirror buffer).
Passing that Emacs buffer to enkan-repl--terminal-tmux-* failed with
"Wrong type argument: stringp #<buffer ...>" because the tmux helpers
build shell command argv lists that require string ids.

Introduce enkan-repl--terminal--coerce-id and apply it to the first
positional argument inside enkan-repl--terminal--dispatch:

- Under tmux backend, a buffer is reduced to its buffer-local
  enkan-repl--tmux-mirror-id (set when the mirror buffer was created),
  yielding the canonical "enkan-NN:window" form.
- Under eat backend, no conversion happens (eat operates on buffers).
- String ids are passed through unchanged in both backends.

Result: callers can pass either a buffer (legacy / convenient) or a
string id (precise) to any abstracted terminal op without caring.

Tests: 4 new ERTs cover all four (backend x id-shape) combinations.

Test suite: 185/185 pass.  byte-compile clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(terminal)!: make tmux the default backend; doc + lint cleanup

BREAKING: enkan-repl-terminal-backend now defaults to 'tmux instead of
'eat.  Users who prefer the in-Emacs eat experience can opt in with
(setq enkan-repl-terminal-backend 'eat).  Existing eat-based workflows
keep working unchanged when this opt-in is set; both backends share the
same user-facing send / layout / lookup commands.

Why tmux became the default:
- The latest claude / codex CLI welcome screens emit thousands of
  cursor / color escape sequences that eat--t-write processes one
  character at a time in pure elisp, locking Emacs's main loop.
  Confirmed via SIGUSR2 backtrace from a frozen session.
- Window geometry changes (narrow split, expanding minibuffer)
  trigger the same repaint storm.
- Delegating the terminal to tmux moves all of this work outside
  Emacs.  Sessions also survive Emacs restart.
- The eat backend is preserved as a documented rollback path.

Documentation:
- README.org: new "Quick note on the terminal backend" section near
  the top, expanded "Configuration / Terminal Backend" section
  (custom-id terminal-backend) covering switch, tmux specifics,
  multi-instance naming, mirror buffer, attach helper, kill helpers,
  state persistence + recovery policy, and migration history.
- README.org: "Dependency for a terminal emulator" rewritten to
  describe the abstraction with both backends.
- AGENTS.md: added Terminal backend / Workspace / Sessions modules
  to the Project Structure listing.

Tests:
- Default-backend ERT now asserts 'tmux.
- Eat-mocking ERTs now bind enkan-repl-terminal-backend to 'eat
  explicitly so they exercise the eat path regardless of the new
  default.

Lint cleanup:
- Capitalize first word of docstrings in eat / tmux backend dispatch
  helpers (checkdoc).
- Restructure two defcustom docstrings whose first line previously
  wrapped (checkdoc "First line is not a complete sentence").
- Quote-escape '%s' in attach-command docstring (avoid byte-compile
  unescaped-quote error).
- Remove unused lexical binding state-only in
  enkan-repl-state-tmux-reconcile.
- Capitalize user-error / user-message strings (checkdoc "Messages
  should start with a capital letter").
- Add forward declare-function for
  enkan-repl-state--list-live-tmux-sessions.

Verification:
- make test:     185/185 pass
- make compile:  clean
- make checkdoc: clean for enkan-repl.el / -terminal.el / -state.el

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Yoshitsugu Sekine <yoshitsugu.sekine@offisis.co.jp>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
github-actions Bot pushed a commit that referenced this pull request May 9, 2026
# [0.18.0](v0.17.2...v0.18.0) (2026-05-09)

### Bug Fixes

* widen eat pane in 1-session layout to fit heavy TUIs ([#59](#59)) ([54d0d2f](54d0d2f))

### Features

* abstract terminal backend; tmux becomes default ([#61](#61)) ([fab55c6](fab55c6)), closes [#59](#59)
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 9, 2026

🎉 This PR is included in version 0.18.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant