Skip to content

Commit 5280db2

Browse files
committed
Add ghostel-compile and ghostel-recompile
Adds compile.el-style integration on top of ghostel: `M-x ghostel-compile' runs a shell command in a ghostel terminal buffer and presents the result like `M-x compile' — header, footer, error highlighting, and `next-error' navigation — but backed by a real TTY so programs that probe `isatty(3)' (coloured output, progress bars, curses tools) behave as they do in a normal shell. Architecture: - `ghostel-command-start-functions' / `ghostel-command-finish- functions' hooks expose OSC 133 C / D semantic prompt markers from the parser. `ghostel-compile' uses both to track a pending → armed → fired state machine so that prompt redraws (which can emit a stale D) don't get treated as the user command's exit. - `ghostel-compile-mode' (minor) wires the OSC 133 hooks into a live ghostel buffer. When the user's command finishes the shell process and ghostel renderer are torn down, header and footer text are inserted (matching compile.el byte-for-byte), the buffer's major mode is switched to a new `ghostel-compile-view-mode' (derived from `compilation-mode'), and `compilation--ensure-parse' populates the error list. The buffer becomes a regular read-only Emacs buffer that never transitions back to an interactive terminal — recompile discards it and starts fresh. - `mode-line-process' shows :run / :exit [N] using the same `compilation-mode-line-{run,exit,fail}' faces `M-x compile' uses. Compile parity: - `compile-command' / `compile-history' shared with \[compile] - `compilation-read-command' (prefix arg forces the prompt) - `compilation-ask-about-save' - `compilation-auto-jump-to-first-error' - `compilation-finish-functions' Ghostel-specific options: `ghostel-compile-buffer-name', `ghostel-compile-finished-major-mode', `ghostel-compile-hide-prompts', `ghostel-compile-clear-buffer', `ghostel-compile-finish-functions', `ghostel-compile-debug'. Keys in the finished view-mode buffer: g - ghostel-recompile n / p - compilation-next-error / -previous-error RET / mouse-2 - compile-goto-error M-g n / M-g p - standard next-error / previous-error Recompile preserves the original `default-directory' (captured at `ghostel-compile' invocation) so pressing `g' from any buffer re-runs the command in the right directory. Bash integration fix: the PROMPT_COMMAND wrapper in `etc/ghostel.bash' captures `$?' on its first line via `local $?'. The previous code began with the bare assignment `__ghostel_in_prompt_command=1' which resets `$?' to 0 in bash, so OSC 133 D always carried 0 and any consumer reported `:exit [0]' for every command. 107 new ERT tests across the elisp test suite cover the hook plumbing, the view-mode switch, error parsing without double counting, header/footer insertion, prompt hiding, the C-gating state machine, the bash integration round-trip, default-directory preservation, and shared `compile-command' / `compile-history' behaviour.
1 parent bfb6e7c commit 5280db2

6 files changed

Lines changed: 1793 additions & 14 deletions

File tree

Makefile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ XDG_CACHE_HOME ?= $(HOME)/.cache
44
MELPAZOID_DIR ?= $(XDG_CACHE_HOME)/melpazoid
55
EVIL_DIR ?= $(XDG_CACHE_HOME)/evil
66

7-
ELC := ghostel.elc ghostel-debug.elc
7+
ELC := ghostel.elc ghostel-debug.elc ghostel-compile.elc
88

99
.PHONY: all build test test-native test-all test-evil lint melpazoid byte-compile bench bench-quick clean
1010

@@ -53,7 +53,7 @@ checkdoc:
5353
(checkdoc-proper-noun-list nil) \
5454
(checkdoc-verb-check-experimental-flag nil) \
5555
(ok t)) \
56-
(dolist (f '(\"ghostel.el\" \"ghostel-debug.el\")) \
56+
(dolist (f '(\"ghostel.el\" \"ghostel-debug.el\" \"ghostel-compile.el\")) \
5757
(ignore-errors (kill-buffer \"*Warnings*\")) \
5858
(let ((inhibit-message t)) \
5959
(checkdoc-file f)) \
@@ -67,7 +67,7 @@ melpazoid:
6767
@if [ ! -d "$(MELPAZOID_DIR)" ]; then \
6868
git clone https://github.com/riscy/melpazoid.git "$(MELPAZOID_DIR)"; \
6969
fi
70-
RECIPE='(ghostel :fetcher github :repo "dakra/ghostel" :files ("ghostel.el" "ghostel-debug.el" "ghostel-module.*"))' \
70+
RECIPE='(ghostel :fetcher github :repo "dakra/ghostel" :files ("ghostel.el" "ghostel-debug.el" "ghostel-compile.el" "ghostel-module.*"))' \
7171
LOCAL_REPO=$(CURDIR) \
7272
make -C "$(MELPAZOID_DIR)"
7373

README.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ process, keymap, and buffer.
1919
- [TRAMP (Remote Terminals)](#tramp-remote-terminals)
2020
- [Configuration](#configuration)
2121
- [Commands](#commands)
22+
- [Compilation mode](#compilation-mode)
2223
- [Running Tests](#running-tests)
2324
- [Performance](#performance)
2425
- [Ghostel vs vterm](#ghostel-vs-vterm)
@@ -457,6 +458,110 @@ with a project-prefixed buffer name. To make it available from
457458
(add-to-list 'project-switch-commands '(ghostel-project "Ghostel") t)
458459
```
459460

461+
### Compilation mode
462+
463+
`ghostel-compile` runs a shell command in a ghostel buffer and presents
464+
the result like `M-x compile``compilation-mode`-style header,
465+
footer, error highlighting, and `next-error` navigation — but backed by
466+
a real TTY so programs that probe `isatty(3)` (coloured output, progress
467+
bars, curses tools) behave as they do in a normal shell.
468+
469+
```elisp
470+
(require 'ghostel-compile)
471+
472+
(global-set-key (kbd "C-c c") #'ghostel-compile)
473+
```
474+
475+
Commands:
476+
477+
| Command | Description |
478+
|-------------------------|-----------------------------------------------------|
479+
| `M-x ghostel-compile` | Prompt for a command and run it (uses `compile-command`) |
480+
| `M-x ghostel-recompile` | Re-run the last command in its original directory |
481+
482+
What a run looks like — the buffer text matches `M-x compile`:
483+
484+
```
485+
-*- mode: ghostel-compile -*-
486+
Compilation started at Wed Apr 15 08:30:11
487+
488+
make -j4 test
489+
490+
...command output (live, with full TTY)...
491+
492+
Compilation finished at Wed Apr 15 08:30:19, duration 8.20 s
493+
```
494+
495+
When the command finishes, the live shell and ghostel renderer are torn
496+
down and the buffer's major mode is switched to `ghostel-compile-view-mode`
497+
(derived from `compilation-mode`). The buffer becomes a regular,
498+
read-only Emacs buffer with compile-mode's coloured error / line-number
499+
faces; the buffer never returns to an interactive ghostel terminal —
500+
a recompile discards it and starts fresh in the original directory.
501+
Point stays at the end of the output (where the renderer left it) so
502+
you see the latest output and the footer rather than jumping to the
503+
top. `mode-line-process` shows `:run` while the command is running
504+
and `:exit [N]` afterwards, using the same faces `M-x compile` uses.
505+
506+
Keybindings (in `ghostel-compile-view-mode`):
507+
508+
| Key | Action |
509+
|-----------------|---------------------------------------------------------|
510+
| `g` | Re-run via `ghostel-recompile` |
511+
| `n` / `p` | Move point to next / previous error (no auto-open) |
512+
| `RET` / `mouse-2` | Jump to the source of the error under point |
513+
| `M-g n` / `M-g p` | Standard `next-error` / `previous-error` |
514+
| `C-c C-c` | `compile-goto-error` (same as RET) |
515+
516+
These standard `compile` options are honoured:
517+
518+
- **`compile-command` / `compile-history`** — shared with `M-x compile`.
519+
The prompt defaults to `compile-command`, the chosen command is
520+
written back, and the history list is `compile-history`, so recent
521+
commands round-trip between the two commands.
522+
- **`compilation-read-command`** — when nil, `ghostel-compile` runs
523+
`compile-command` silently; pass a prefix arg to force the prompt.
524+
- **`compilation-ask-about-save`** — modified buffers are offered for
525+
saving before launching.
526+
- **`compilation-auto-jump-to-first-error`** — jumps to the first error
527+
after parsing.
528+
- **`compilation-finish-functions`** — runs with `(buffer msg)` just
529+
like with `M-x compile`.
530+
- Output scrolling is always on (terminal behaviour — equivalent to
531+
`compilation-scroll-output` non-nil).
532+
533+
`ghostel-recompile` runs in the directory the original `ghostel-compile`
534+
was invoked from, regardless of which buffer you're in when you press
535+
`g`.
536+
537+
Ghostel-specific customisation:
538+
539+
| Option | Effect |
540+
|---------------------------------------|-----------------------------------------------------------------------------------------------------------------|
541+
| `ghostel-compile-buffer-name` | Buffer name (default `*ghostel-compile*`) |
542+
| `ghostel-compile-finished-major-mode` | Major mode to switch to after each run (default `ghostel-compile-view-mode`; set to nil to stay in `ghostel-mode`) |
543+
| `ghostel-compile-hide-prompts` | Hide surrounding shell prompts (default `t`) |
544+
| `ghostel-compile-clear-buffer` | Clear the buffer before each run (default `t`) |
545+
| `ghostel-compile-finish-functions` | Ghostel-specific finish hook (runs alongside `compilation-finish-functions`) |
546+
| `ghostel-compile-debug` | Log every OSC 133 C/D event to `*Messages*` (default `nil`) |
547+
548+
Completion is detected via the OSC 133 `D;<exit>` semantic prompt
549+
marker, so shell integration (`ghostel-shell-integration`, enabled by
550+
default) must be active.
551+
552+
#### Hooks for your own integrations
553+
554+
Outside of a compile buffer, two hooks let you react to *any* shell
555+
command in *any* ghostel buffer:
556+
557+
- `ghostel-command-start-functions` — called with `(BUFFER)` when the
558+
shell emits OSC 133 `C` (a command starts running).
559+
- `ghostel-command-finish-functions` — called with `(BUFFER EXIT-STATUS)`
560+
when the shell emits OSC 133 `D` (a command finishes).
561+
562+
Errors raised by individual hook functions are caught and logged so
563+
one bad consumer can't break the rest.
564+
460565
## Running Tests
461566

462567
Tests use ERT. The Makefile provides convenient targets:

etc/ghostel.bash

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,6 @@ __ghostel_osc7() {
1717

1818
# --- Semantic prompt markers (OSC 133) ---
1919

20-
# Save exit status before PROMPT_COMMAND overwrites $?.
21-
__ghostel_save_status() {
22-
__ghostel_last_status="$?"
23-
}
24-
2520
# Emit "command finished" (D) for the previous command, then "prompt start" (A).
2621
# D is skipped on the very first prompt (no previous command).
2722
__ghostel_prompt_start() {
@@ -46,8 +41,15 @@ __ghostel_preexec() {
4641
}
4742

4843
__ghostel_wrapped_prompt_command() {
44+
# Capture $? FIRST. A bare assignment such as
45+
# `__ghostel_in_prompt_command=1' on its own line counts as a
46+
# successful command and resets $? to 0 in bash, so any later
47+
# `$?' read here would always see 0 and we'd report `:exit [0]'
48+
# for every command. `local' with `=$?' evaluates `$?' before
49+
# invoking the local builtin, preserving the real exit status.
50+
local __ghostel_status=$?
4951
__ghostel_in_prompt_command=1
50-
__ghostel_save_status
52+
__ghostel_last_status=$__ghostel_status
5153
__ghostel_prompt_start
5254
__ghostel_osc7
5355
eval "${__ghostel_original_prompt_command:-}"

0 commit comments

Comments
 (0)