Skip to content

Commit 5453c22

Browse files
committed
Add public API for sending input to the terminal (#126)
Promote two thin wrappers — `ghostel-send-string' and `ghostel-send-key' — so external packages (agent integrations, custom keymaps) can drive a ghostel buffer without reaching into `ghostel--' internals. Both signal a `user-error' when called outside a ghostel-mode buffer. Rename the internal `ghostel--send-key' to `ghostel--send-string' to match what it does (writes raw bytes to the PTY) and avoid naming confusion with the new public `ghostel-send-key' (which takes a key name and mods and goes through the encoder). The old internal name is kept as an obsolete alias so third-party packages that reached into it keep working.
1 parent e7164ec commit 5453c22

4 files changed

Lines changed: 135 additions & 28 deletions

File tree

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,23 @@ When `evil-ghostel-mode` is active:
455455
| `M-x ghostel-download-module` | Download pre-built native module |
456456
| `M-x ghostel-module-compile` | Compile native module from source |
457457

458+
### Sending input from Lisp
459+
460+
For packages that need to inject input into a running ghostel buffer
461+
(agent integrations, custom keymaps, Swerty-style bindings, …) two
462+
public functions are provided:
463+
464+
```elisp
465+
(ghostel-send-string "ls -la\n") ; send raw bytes, newline included
466+
(ghostel-send-key "return") ; send a named key through the encoder
467+
(ghostel-send-key "a" "ctrl") ; C-a — respects the current terminal mode
468+
(ghostel-send-key "up" "shift,ctrl") ; modifiers are comma-separated
469+
```
470+
471+
Both operate on the current buffer; wrap in `with-current-buffer`
472+
when driving another ghostel buffer. Calling either outside a
473+
ghostel buffer signals a `user-error`.
474+
458475
### Project integration
459476

460477
`ghostel-project` opens a terminal in the current project's root directory

ghostel-debug.el

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Logs filter calls, key sends, resize events, redraw decisions
5252
(insert "=== Ghostel Debug Log ===\n\n"))
5353
;; Data path
5454
(advice-add 'ghostel--filter :before #'ghostel-debug--log-filter)
55-
(advice-add 'ghostel--send-key :before #'ghostel-debug--log-send)
55+
(advice-add 'ghostel--send-string :before #'ghostel-debug--log-send)
5656
(advice-add 'ghostel--send-encoded :before #'ghostel-debug--log-encoded)
5757
;; Render path
5858
(advice-add 'ghostel--delayed-redraw :around #'ghostel-debug--log-redraw)
@@ -66,7 +66,7 @@ Logs filter calls, key sends, resize events, redraw decisions
6666
"Stop logging."
6767
(interactive)
6868
(advice-remove 'ghostel--filter #'ghostel-debug--log-filter)
69-
(advice-remove 'ghostel--send-key #'ghostel-debug--log-send)
69+
(advice-remove 'ghostel--send-string #'ghostel-debug--log-send)
7070
(advice-remove 'ghostel--send-encoded #'ghostel-debug--log-encoded)
7171
(advice-remove 'ghostel--delayed-redraw #'ghostel-debug--log-redraw)
7272
(advice-remove 'ghostel--window-adjust-process-window-size
@@ -239,7 +239,7 @@ The latency breakdown shows:
239239
(erase-buffer)
240240
(insert "=== Ghostel Typing Latency Measurement ===\n")
241241
(insert (format "Type %d characters to collect measurements...\n\n" n)))
242-
(advice-add 'ghostel--send-key :before #'ghostel-debug--latency-on-send)
242+
(advice-add 'ghostel--send-string :before #'ghostel-debug--latency-on-send)
243243
(advice-add 'ghostel--filter :before #'ghostel-debug--latency-on-echo)
244244
(advice-add 'ghostel--delayed-redraw :after #'ghostel-debug--latency-on-render)
245245
(message "ghostel-debug: type %d characters to measure latency" n)))
@@ -275,7 +275,7 @@ The latency breakdown shows:
275275

276276
(defun ghostel-debug--latency-report ()
277277
"Generate and display the latency report."
278-
(advice-remove 'ghostel--send-key #'ghostel-debug--latency-on-send)
278+
(advice-remove 'ghostel--send-string #'ghostel-debug--latency-on-send)
279279
(advice-remove 'ghostel--filter #'ghostel-debug--latency-on-echo)
280280
(advice-remove 'ghostel--delayed-redraw #'ghostel-debug--latency-on-render)
281281
(setq ghostel-debug--latency-active nil)

ghostel.el

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ Set to 0 to disable immediate redraws."
151151

152152
(defcustom ghostel-immediate-redraw-interval 0.05
153153
"Maximum seconds since last keystroke for immediate redraw.
154-
Output arriving within this interval of a `ghostel--send-key'
154+
Output arriving within this interval of a `ghostel--send-string'
155155
call is considered interactive echo and redrawn immediately
156156
when the output size is below `ghostel-immediate-redraw-threshold'."
157157
:type 'number)
@@ -784,7 +784,7 @@ pixel-based trailing-space compensation is needed.")
784784

785785

786786
(defvar-local ghostel--last-send-time nil
787-
"Time of the last `ghostel--send-key' call, for immediate-redraw detection.")
787+
"Time of the last `ghostel--send-string' call, for immediate-redraw detection.")
788788

789789
(defvar-local ghostel--input-buffer nil
790790
"Accumulated keystrokes waiting to be flushed to the PTY.")
@@ -917,7 +917,7 @@ is non-nil.")
917917
(define-key map (kbd key-str)
918918
(let ((code (- c 96)))
919919
(lambda () (interactive)
920-
(ghostel--send-key (string code)))))))))
920+
(ghostel--send-string (string code)))))))))
921921
;; Meta keys — bind all M-<letter> so they reach the terminal
922922
;; instead of running Emacs commands like forward-word.
923923
(dolist (c (number-sequence ?a ?z))
@@ -926,7 +926,7 @@ is non-nil.")
926926
(define-key map (kbd key-str) #'ghostel--send-event))))
927927
;; C-@ (NUL, same as C-SPC) — used by programs like Emacs-in-terminal
928928
(define-key map (kbd "C-@")
929-
(lambda () (interactive) (ghostel--send-key "\x00")))
929+
(lambda () (interactive) (ghostel--send-string "\x00")))
930930
;; C-y: yank from Emacs kill ring into the terminal
931931
(define-key map (kbd "C-y") #'ghostel-yank)
932932
(when (eq system-type 'darwin)
@@ -978,13 +978,13 @@ of waiting for a continuation keystroke."
978978
(cond
979979
;; Control character (C-@=0, C-a=1 through C-_=31)
980980
((and (integerp event) (<= event 31))
981-
(ghostel--send-key (string event)))
981+
(ghostel--send-string (string event)))
982982
;; ASCII (32-127)
983983
((and (integerp event) (<= event 127))
984-
(ghostel--send-key (string event)))
984+
(ghostel--send-string (string event)))
985985
;; Non-ASCII character without modifier bits — send as UTF-8
986986
((and (integerp event) (< event #x400000))
987-
(ghostel--send-key (encode-coding-string (string event) 'utf-8)))
987+
(ghostel--send-string (encode-coding-string (string event) 'utf-8)))
988988
;; Modified key (M-x, C-M-a, etc.) or function key — use encoder
989989
(t
990990
(let* ((base (event-basic-type event))
@@ -1015,17 +1015,17 @@ of waiting for a continuation keystroke."
10151015
(ghostel--send-encoded key-name mod-str)
10161016
(message "ghostel: unrecognized key %S" event)))))))
10171017

1018-
(defun ghostel--send-key (key)
1019-
"Send KEY string to the terminal process.
1018+
(defun ghostel--send-string (string)
1019+
"Send STRING as raw bytes to the terminal process.
10201020
Records the send time for immediate-redraw detection and optionally
10211021
coalesces rapid keystrokes when `ghostel-input-coalesce-delay' > 0."
10221022
(when (and ghostel--process (process-live-p ghostel--process))
10231023
(setq ghostel--last-send-time (current-time))
10241024
(if (and (> ghostel-input-coalesce-delay 0)
1025-
(= (length key) 1))
1025+
(= (length string) 1))
10261026
;; Coalesce single-char keystrokes
10271027
(progn
1028-
(push key ghostel--input-buffer)
1028+
(push string ghostel--input-buffer)
10291029
(unless ghostel--input-timer
10301030
(setq ghostel--input-timer
10311031
(run-with-timer ghostel-input-coalesce-delay nil
@@ -1039,7 +1039,10 @@ coalesces rapid keystrokes when `ghostel-input-coalesce-delay' > 0."
10391039
(process-send-string ghostel--process
10401040
(apply #'concat (nreverse ghostel--input-buffer)))
10411041
(setq ghostel--input-buffer nil)))
1042-
(process-send-string ghostel--process key))))
1042+
(process-send-string ghostel--process string))))
1043+
1044+
(define-obsolete-function-alias 'ghostel--send-key
1045+
#'ghostel--send-string "0.16.0")
10431046

10441047
(defun ghostel--flush-input (buffer)
10451048
"Flush coalesced input in BUFFER to the PTY."
@@ -1064,7 +1067,7 @@ Falls back to raw escape sequences if the encoder doesn't produce output."
10641067
;; immediate-redraw detection (ghostel--flush-output doesn't do this).
10651068
(setq ghostel--last-send-time (current-time))
10661069
(let ((seq (ghostel--raw-key-sequence key-name mods)))
1067-
(when seq (ghostel--send-key seq))))))
1070+
(when seq (ghostel--send-string seq))))))
10681071

10691072
(defun ghostel--raw-key-sequence (key-name mods)
10701073
"Build a raw escape sequence for KEY-NAME with MODS.
@@ -1157,7 +1160,7 @@ paste, yank, drop."
11571160
(str (if (and (characterp char) (< char 128))
11581161
(string char)
11591162
(encode-coding-string (string char) 'utf-8))))
1160-
(ghostel--send-key str)))
1163+
(ghostel--send-string str)))
11611164

11621165
(defun ghostel--send-event ()
11631166
"Send the current key event to the terminal via the key encoder.
@@ -1200,6 +1203,30 @@ modes (application cursor keys, Kitty keyboard protocol, etc.)."
12001203
(ghostel--send-encoded key-name mod-str))))
12011204

12021205

1206+
;;; Public input API
1207+
1208+
(defun ghostel-send-string (string)
1209+
"Send STRING to the terminal process in the current ghostel buffer.
1210+
Signals a `user-error' when called outside a ghostel buffer. STRING
1211+
is passed through unchanged, including any embedded control
1212+
characters; callers are responsible for UTF-8 encoding if needed."
1213+
(unless (derived-mode-p 'ghostel-mode)
1214+
(user-error "Must be called from a ghostel buffer"))
1215+
(ghostel--send-string string))
1216+
1217+
(defun ghostel-send-key (key-name &optional mods)
1218+
"Send KEY-NAME with optional MODS to the terminal's key encoder.
1219+
KEY-NAME is a string like \"a\", \"return\", or \"up\". MODS is a
1220+
comma-separated modifier string like \"ctrl\" or \"shift,ctrl\", or
1221+
nil for no modifiers. The encoder respects the terminal's current
1222+
mode (application cursor keys, Kitty keyboard protocol, etc.).
1223+
1224+
Signals a `user-error' when called outside a ghostel buffer."
1225+
(unless (derived-mode-p 'ghostel-mode)
1226+
(user-error "Must be called from a ghostel buffer"))
1227+
(ghostel--send-encoded key-name (or mods "")))
1228+
1229+
12031230
;;; Terminal control commands (C-c prefix)
12041231

12051232
(defun ghostel-send-C-c ()
@@ -1215,7 +1242,7 @@ modes (application cursor keys, Kitty keyboard protocol, etc.)."
12151242
(defun ghostel-send-C-backslash ()
12161243
"Send C-\\ (quit) to the terminal."
12171244
(interactive)
1218-
(ghostel--send-key "\x1c"))
1245+
(ghostel--send-string "\x1c"))
12191246

12201247
(defun ghostel-send-C-d ()
12211248
"Send EOF to the terminal."
@@ -1228,7 +1255,7 @@ Clears `quit-flag' which Emacs sets when \\`C-g' is pressed with
12281255
`inhibit-quit' non-nil."
12291256
(interactive)
12301257
(setq quit-flag nil)
1231-
(ghostel--send-key (string 7)))
1258+
(ghostel--send-string (string 7)))
12321259

12331260

12341261
;;; Paste / yank
@@ -1305,7 +1332,7 @@ pasted using bracketed paste."
13051332
(let ((type (car arg))
13061333
(objects (cddr arg)))
13071334
(if (eq type 'file)
1308-
(ghostel--send-key
1335+
(ghostel--send-string
13091336
(mapconcat #'shell-quote-argument objects " "))
13101337
(ghostel--paste-text
13111338
(mapconcat #'identity objects "\n"))))))))

test/ghostel-test.el

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4361,7 +4361,7 @@ hand nil to the native module."
43614361
;; Return a fake timer but call function for test
43624362
'fake-timer)))
43634363
(setq ghostel--process 'fake)
4364-
(ghostel--send-key "a")
4364+
(ghostel--send-string "a")
43654365
;; Should be buffered, not sent
43664366
(should (equal ghostel--input-buffer '("a")))
43674367
(should-not sent)))))
@@ -4379,7 +4379,7 @@ hand nil to the native module."
43794379
((symbol-function 'process-send-string)
43804380
(lambda (_proc str) (push str sent))))
43814381
(setq ghostel--process 'fake)
4382-
(ghostel--send-key "a")
4382+
(ghostel--send-string "a")
43834383
(should (member "a" sent))
43844384
(should-not ghostel--input-buffer)))))
43854385

@@ -4447,7 +4447,7 @@ redraw and produce visible flicker, so point is left alone."
44474447
(sent-key nil))
44484448
(cl-letf (((symbol-function 'ghostel--scroll-bottom)
44494449
(lambda (_term) (setq scroll-bottom-called t)))
4450-
((symbol-function 'ghostel--send-key)
4450+
((symbol-function 'ghostel--send-string)
44514451
(lambda (str) (setq sent-key str))))
44524452
(with-temp-buffer
44534453
(insert "scrollback\nscrollback\nscrollback\n")
@@ -4488,7 +4488,7 @@ redraw and produce visible flicker, so point is left alone."
44884488
(scroll-bottom-called nil))
44894489
(cl-letf (((symbol-function 'ghostel--scroll-bottom)
44904490
(lambda (_term) (setq scroll-bottom-called t)))
4491-
((symbol-function 'ghostel--send-key)
4491+
((symbol-function 'ghostel--send-string)
44924492
(lambda (_str) nil)))
44934493
(with-temp-buffer
44944494
(insert "scrollback\nscrollback\nscrollback\n")
@@ -4796,7 +4796,7 @@ rather than the selected window's buffer."
47964796
(ert-deftest ghostel-test-send-next-key-control-x ()
47974797
"Send-next-key sends the prefix key as raw byte 24 (not intercepted by Emacs)."
47984798
(let (sent-key)
4799-
(cl-letf (((symbol-function 'ghostel--send-key)
4799+
(cl-letf (((symbol-function 'ghostel--send-string)
48004800
(lambda (str) (setq sent-key str))))
48014801
(let ((unread-command-events (list ?\C-x)))
48024802
(ghostel-send-next-key))
@@ -4805,7 +4805,7 @@ rather than the selected window's buffer."
48054805
(ert-deftest ghostel-test-send-next-key-control-h ()
48064806
"Send-next-key sends the help key as raw byte 8."
48074807
(let (sent-key)
4808-
(cl-letf (((symbol-function 'ghostel--send-key)
4808+
(cl-letf (((symbol-function 'ghostel--send-string)
48094809
(lambda (str) (setq sent-key str))))
48104810
(let ((unread-command-events (list ?\C-h)))
48114811
(ghostel-send-next-key))
@@ -4814,7 +4814,7 @@ rather than the selected window's buffer."
48144814
(ert-deftest ghostel-test-send-next-key-regular-char ()
48154815
"Send-next-key sends a regular character as-is."
48164816
(let (sent-key)
4817-
(cl-letf (((symbol-function 'ghostel--send-key)
4817+
(cl-letf (((symbol-function 'ghostel--send-string)
48184818
(lambda (str) (setq sent-key str))))
48194819
(let ((unread-command-events (list ?a)))
48204820
(ghostel-send-next-key))
@@ -4844,6 +4844,63 @@ rather than the selected window's buffer."
48444844
(should (equal "up" captured-key))
48454845
(should (equal "" captured-mods)))))
48464846

4847+
;; -----------------------------------------------------------------------
4848+
;; Test: public send-string / send-key API
4849+
;; -----------------------------------------------------------------------
4850+
4851+
(ert-deftest ghostel-test-send-string-routes-to-send-string ()
4852+
"`ghostel-send-string' forwards its argument to `ghostel--send-string'."
4853+
(with-temp-buffer
4854+
(ghostel-mode)
4855+
(let (sent)
4856+
(cl-letf (((symbol-function 'ghostel--send-string)
4857+
(lambda (str) (setq sent str))))
4858+
(ghostel-send-string "hello")
4859+
(should (equal sent "hello"))))))
4860+
4861+
(ert-deftest ghostel-test-send-string-errors-outside-ghostel-buffer ()
4862+
"`ghostel-send-string' signals `user-error' when not in a ghostel buffer."
4863+
(with-temp-buffer
4864+
(should-error (ghostel-send-string "x") :type 'user-error)))
4865+
4866+
(ert-deftest ghostel-test-send-key-routes-to-send-encoded ()
4867+
"`ghostel-send-key' forwards key-name and mods to `ghostel--send-encoded'."
4868+
(with-temp-buffer
4869+
(ghostel-mode)
4870+
(let (captured-key captured-mods)
4871+
(cl-letf (((symbol-function 'ghostel--send-encoded)
4872+
(lambda (key mods &optional _utf8)
4873+
(setq captured-key key captured-mods mods))))
4874+
(ghostel-send-key "return" "ctrl")
4875+
(should (equal captured-key "return"))
4876+
(should (equal captured-mods "ctrl"))))))
4877+
4878+
(ert-deftest ghostel-test-send-key-nil-mods-becomes-empty-string ()
4879+
"`ghostel-send-key' passes an empty string when MODS is omitted."
4880+
(with-temp-buffer
4881+
(ghostel-mode)
4882+
(let (captured-mods)
4883+
(cl-letf (((symbol-function 'ghostel--send-encoded)
4884+
(lambda (_key mods &optional _utf8)
4885+
(setq captured-mods mods))))
4886+
(ghostel-send-key "up")
4887+
(should (equal captured-mods ""))))))
4888+
4889+
(ert-deftest ghostel-test-send-key-errors-outside-ghostel-buffer ()
4890+
"`ghostel-send-key' signals `user-error' when not in a ghostel buffer."
4891+
(with-temp-buffer
4892+
(should-error (ghostel-send-key "a") :type 'user-error)))
4893+
4894+
(ert-deftest ghostel-test-send-key-obsolete-alias-still-works ()
4895+
"The obsolete `ghostel--send-key' alias routes to `ghostel--send-string'.
4896+
External packages may still call the old internal name."
4897+
(let (sent)
4898+
(cl-letf (((symbol-function 'ghostel--send-string)
4899+
(lambda (str) (setq sent str))))
4900+
(with-no-warnings
4901+
(ghostel--send-key "payload"))
4902+
(should (equal sent "payload")))))
4903+
48474904
;; -----------------------------------------------------------------------
48484905
;; Test: TRAMP integration
48494906
;; -----------------------------------------------------------------------
@@ -5342,6 +5399,12 @@ while :; do sleep 0.1; done'\n")
53425399
ghostel-test-send-next-key-regular-char
53435400
ghostel-test-send-next-key-meta-x
53445401
ghostel-test-send-next-key-function-key
5402+
ghostel-test-send-string-routes-to-send-string
5403+
ghostel-test-send-key-obsolete-alias-still-works
5404+
ghostel-test-send-string-errors-outside-ghostel-buffer
5405+
ghostel-test-send-key-routes-to-send-encoded
5406+
ghostel-test-send-key-nil-mods-becomes-empty-string
5407+
ghostel-test-send-key-errors-outside-ghostel-buffer
53455408
ghostel-test-local-host-p
53465409
ghostel-test-update-directory-remote
53475410
ghostel-test-get-shell-local

0 commit comments

Comments
 (0)