Skip to content

Detect the PTY foreground process without shell integration - #723

Merged
dakra merged 2 commits into
mainfrom
feat/foreground-process
Oct 9, 2026
Merged

dakra merged 2 commits into
mainfrom
feat/foreground-process

Conversation

@dakra

@dakra dakra commented Oct 8, 2026

Copy link
Copy Markdown
Owner

Problem

Ghostel could only tell that "a command is running" through OSC 133 shell-integration markers, and never which program. Without shell integration, ghostel-query-before-killing's auto setting never asked, and even with it, an ssh client or nested shell sitting at its own prompt looked idle, so killing the buffer silently killed that session.

Approach

Query the PTY's foreground process group instead, the same signal terminal emulators use for close confirmation.

  • Native path: PosixPtyProcess gains a TIOCGPGRP query on the PTY primary, exposed through the NativeProcess facade under the backend-handoff mutex and registered as ghostel--pty-foreground-pgid. The ioctl must run on the primary: a re-opened replica fd is not the caller's controlling terminal and the kernel rejects it with ENOTTY. ConPTY has no foreground process-group concept and returns nil.
  • Emacs PTY path: Emacs owns the primary fd there, and process-running-child-p answers the same question (nil = shell at prompt, integer = child pgid, t = unknown). ghostel-foreground-pid dispatches between the two.
  • ghostel-command-running-p combines the probe with the OSC 133 C→D state. The OSC arm still covers remote (TRAMP) sessions, where the probe only sees the local client.
  • Baseline shell pgid: ghostel-macos-login-shell runs the shell under login(1), which forks, so ghostel--pid is login and the idle shell never matches it. Only with such a wrapper is a direct child of ghostel--pid adopted as the shell and cached; without one, a command the shell starts is a direct child too and would be adopted in its place.
  • New abnormal hook ghostel-foreground-change-functions (BUFFER PGID COMM), sampled after each redraw while non-nil and run outside the render inhibits. The identity is the (pgid . comm) pair: an early sample can catch a forked command before exec, still under the shell's name, and the pair comparison re-fires once the real name shows up. process-attributes is slow, so comm is re-read only when the pgid changed or while a command still carries the shell's comm; an idle prompt or an exec'ed command costs one ioctl per redraw.

User-visible changes

  • ghostel-query-before-killing auto works without shell integration and also asks while an ssh client or nested shell holds the foreground. This also gates the save-buffers-kill-emacs confirmation.
  • ghostel-bookmark and consult-ghostel's history insert move off the raw OSC 133 flag onto ghostel-command-running-p, so they also refuse while a nested session holds the terminal.
  • The predicate needs a shell to compare against, so it never fires in a buffer that runs a program directly (ghostel-exec, ghostel-compile). The defcustom, CHANGELOG and README say so.

Tests

test/ghostel-foreground-test.el covers both PTY backends live (probe tracking, hook firing through the redraw path, interrupt/reclaim, and a forked shell reading as idle) plus the predicate, child-state mapping, shell-pgid adoption, and the comm re-read gating with mocked probes.

Ghostel could only tell that "a command is running" through OSC 133
shell-integration markers, and never which program.  Query the PTY's
foreground process group instead — the same signal terminal emulators
use for close confirmation:

- PosixPtyProcess gains a TIOCGPGRP query on the PTY primary, exposed
  through the NativeProcess facade (under the backend-handoff mutex)
  and registered as ghostel--pty-foreground-pgid.  The ioctl must run
  on the primary: a re-opened replica fd is not the caller's
  controlling terminal, so the kernel rejects it with ENOTTY.  ConPTY
  has no foreground process-group concept and returns nil.
- ghostel-foreground-pid dispatches per PTY path: the native module
  owns the primary fd on the native path; Emacs owns it on the
  Emacs-PTY path, where process-running-child-p answers the same
  question (nil = shell at prompt, integer = child pgid, t = unknown).
- ghostel-command-running-p combines the probe with the OSC 133 C→D
  state; the OSC arm still covers remote (TRAMP) sessions, where the
  probe only sees the local client.
- ghostel-query-before-killing's auto setting now defers to
  ghostel-command-running-p, so it works without shell integration and
  also asks while an ssh client or nested shell holds the foreground —
  killing the buffer would kill that session.
- ghostel--shell-pgid supplies the baseline: ghostel-macos-login-shell
  runs the shell under login(1), which forks, so ghostel--pid is login
  and the idle shell never matches it.  Only with such a wrapper
  (ghostel--shell-forked) is a direct child of ghostel--pid adopted as
  the shell and cached; without one, a command the shell starts is a
  direct child too and would be adopted in its place.
- New abnormal hook ghostel-foreground-change-functions (buffer, pgid,
  comm), sampled after each redraw while non-nil and run outside the
  render inhibits.  The foreground identity is the (pgid . comm) pair:
  an early sample can catch a forked command before exec, still under
  the shell's name, and the pair comparison re-fires once the real
  name shows up with the program's next output.  process-attributes is
  slow, so comm is re-read only when the pgid changed or while a
  command still carries the shell's comm (remembered from samples where
  the shell holds the foreground); an idle prompt or an exec'ed command
  costs one ioctl per redraw.

- ghostel-bookmark--shell-idle-p and consult-ghostel--history-insert
  move off the raw OSC 133 flag onto ghostel-command-running-p, so they
  also refuse while an ssh client or nested shell holds the terminal,
  with no shell integration required.

The predicate needs a shell to compare the foreground against, so it
never fires in a buffer that runs a program directly (ghostel-exec,
ghostel-compile); the defcustom, CHANGELOG and README say so.

Tests cover both PTY backends live (probe tracking, hook firing
through the redraw path, interrupt/reclaim, and a forked shell reading
as idle) plus the predicate, child-state mapping, shell-pgid adoption,
and the comm re-read gating with mocked probes.
`ghostel-line-mode-send' runs `ghostel-line-mode-send-functions' with
the input text while it is still in the buffer.  The first function
returning non-nil takes over and the input is neither deleted nor sent.
The actual send lives in the new `ghostel-line-mode-send-input', which
such a function calls when it decides to send after all.
@dakra
dakra merged commit 0d7d8ac into main Oct 9, 2026
33 checks passed
@dakra
dakra deleted the feat/foreground-process branch October 9, 2026 07:55
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.

1 participant