-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathterm-cmd.el
235 lines (197 loc) · 8.98 KB
/
term-cmd.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
;;; term-cmd.el --- Send commands from programs running in term.el. -*- lexical-binding: t -*-
;; Copyright (C) 2014-2023 Callie Cameron
;; Author: Callie Cameron <[email protected]>
;; Version: 1.2
;; Url: https://github.com/calliecameron/term-cmd
;; Keywords: processes
;; Package-Requires: ((emacs "27.2") (dash "2.12.0") (f "0.18.2"))
;; This file is not part of GNU Emacs.
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; Send commands to Emacs from programs running in term.el.
;;
;; In vanilla Emacs, programs running in term.el can send commands
;; back to Emacs by printing a 'magic escape sequence' which the
;; terminal emulator parses -- this is how directory tracking
;; works. But the list of commands is hard-coded, and you can't add
;; new ones.
;;
;; This package lets you add new commands. It uses a different magic
;; escape sequence to avoid interfering with the built-in commands,
;; but the principle is the same. When a program prints a command, it
;; won't show up on the screen, but will instead be interpreted by
;; Emacs.
;;
;; This is a library, and doesn't make any user-visible changes. For
;; an example of something that uses it, see the 'term-alert' package
;; (https://github.com/calliecameron/term-alert).
;;
;;
;; Usage
;;
;; To register a command:
;;
;; (add-to-list
;; 'term-cmd-commands-alist
;; '("command" . some-callback-function))
;;
;; where "command" is the name of the command, and
;; some-callback-function is the function you want to be called when
;; the command is run. The function should take two arguments -- the
;; first is the command name itself, and the second is the command's
;; argument.
;;
;; To send a command, use the emacs-term-cmd script:
;;
;; emacs-term-cmd command arg
;;
;; If called outside Emacs, this does nothing (i.e. it won't mess
;; things up).
;;
;; Because the commands are based on terminal output, they work just
;; as well through nested shells, multiple SSH sessions, or tmux (not
;; 100% reliable -- see comments in emacs-term-cmd).
;;
;;
;; Installation
;;
;; Install the term-cmd package from MELPA.
;;
;; The emacs-term-cmd command will always be on the PATH of any shell
;; launched from Emacs. However, for full functionality you should add
;; ~/.emacs.d/term-cmd (or wherever your user-emacs-directory is) to
;; the PATH in your environment or shell's startup files, too
;; (e.g. ~/.profile, ~/.bashrc, ~/.zshrc, etc.), on any machine you
;; often SSH into; this will allow shells inside tmux or on other
;; machines to send commands back to Emacs on your local machine.
;;; Code:
(require 'term)
(require 'dash)
(require 'f)
;;;###autoload
(defvar term-cmd-commands-alist '()
"Commands to run based on process output.
Elements should be of the form (<string> . <func>) where string is the
command to match on, and func takes two args, the command and the
command's argument. To run a command from the terminal, output a line
of the form '\\eAnSiTTeRmCmD <command> <arg>\\n', where arg is an arbitrary
string; the function with key <command> will be called with command
and arg. Arg can also be omitted if it is not required.")
;; These variables allow incomplete commands at the end of input to be
;; stored and handled when more input arrives.
(defvar term-cmd--partial-cmd nil)
(make-local-variable 'term-cmd--partial-cmd)
;; These variables do the same thing, but for Emacs' built-in
;; directory-tracking messages; see
;; term-cmd--ansi-partial-beginning-check and
;; term-cmd--ansi-partial-end-check, below.
(defvar term-cmd--partial-ansi-terminal-message nil)
(make-local-variable 'term-cmd--partial-ansi-terminal-message)
;;;###autoload
(defun term-cmd--do-command (message)
"Scan MESSAGE for any commands, execute them, and return the remaining message."
;; Handle stored partial command
(when term-cmd--partial-cmd
(setq message (concat term-cmd--partial-cmd message))
(setq term-cmd--partial-cmd nil))
;; Process the commands
(while (string-match "\eAnSiTTeRmCmD +\\(.+\\)\n" message)
(let* ((matched (match-string 1 message))
command
arg)
;; Remove the command from the message that will eventually be printed
(setq message (replace-match "" t t message))
;; At least Bash inserts '\r\r' instead of just '\r' before '\n'
;; in the magic sequence received by Emacs.
(when (string-match "\r+$" matched)
(setq matched (replace-match "" t t matched)))
(if (string-match " " matched)
(progn
(setq command (substring matched 0 (match-beginning 0)))
(setq arg (substring matched (match-end 0))))
(setq command matched)
(setq arg ""))
(let ((func (assoc command term-cmd-commands-alist)))
(if func
(funcall (cdr func) command arg)
(message "Unknown term-cmd command '%s'" command)))))
;; If there is a partial message at the end of the string, store it
;; for future use.
(when (string-match "\eAnSiTTeRmCmD.+$" message)
(setq term-cmd--partial-cmd (match-string 0 message))
(setq message (replace-match "" t t message)))
message)
;; These functions handle partial messages at the end of the input,
;; but for Emacs' built-in directory-tracking commands rather than our
;; ones (the built-in commands use a different magic escape sequence,
;; only allow single-char commands, and are hardcoded in term.el -- so
;; not accessible to the user). This is essentially a fix for a
;; term.el bug (see
;; http://debbugs.gnu.org/cgi/bugreport.cgi?bug=17231), but that patch
;; never got in, so I might as well do it here, right?
;;;###autoload
(defun term-cmd--ansi-partial-beginning-check (message)
"Handle stored partial commands for built-in commands in MESSAGE."
(when term-cmd--partial-ansi-terminal-message
(setq message (concat term-cmd--partial-ansi-terminal-message message))
(setq term-cmd--partial-ansi-terminal-message nil))
message)
;;;###autoload
(defun term-cmd--ansi-partial-end-check (message)
"Handle partial built-in commands at the end of MESSAGE."
(when (string-match "\eAnSiT[^T].+$" message)
(setq term-cmd--partial-ansi-terminal-message (match-string 0 message))
(setq message (replace-match "" t t message)))
message)
;; The main advice that makes everything work.
;;;###autoload
(defun term-cmd--advice (orig-func &rest args)
; checkdoc-params: (orig-func args)
"Process any term-cmd commands before passing the remaining input on to term.el."
(let ((msg (car args)))
(setq msg (term-cmd--do-command msg))
(setq msg (term-cmd--ansi-partial-beginning-check msg))
(setq msg (apply orig-func (list msg)))
(term-cmd--ansi-partial-end-check msg)))
(defconst term-cmd--bin-dir (f-expand (f-join user-emacs-directory "term-cmd")))
(defconst term-cmd--executable-name "emacs-term-cmd")
(defconst term-cmd--executable-abs (f-join term-cmd--bin-dir term-cmd--executable-name))
;;;###autoload
(defun term-cmd--init ()
"Internal term-cmd initialisation function."
;; Make sure emacs-term-cmd is on the path for any shells launched
;; directly through term.el. However, it should also be on the path
;; in other shells (e.g. tmux, ssh sessions) so that it still works
;; if one of them happens to end up running under term.el; for this
;; the user must add it in their shell startup scripts. The script's
;; location changes when the package is updated through package.el,
;; so we copy it to a standard location to save the user having to
;; tweak their scripts whenever the package is updated.
(f-mkdir user-emacs-directory)
(f-mkdir term-cmd--bin-dir)
(when (f-exists? term-cmd--executable-abs)
(f-delete term-cmd--executable-abs))
(f-copy
(f-join (f-parent load-file-name) "bin" term-cmd--executable-name)
term-cmd--executable-abs)
(when
(not (string=
(executable-find term-cmd--executable-name)
term-cmd--executable-abs))
(message "term-cmd: please add '%s' to the PATH in your environment or shell's startup file (e.g. ~/.profile, ~/.bashrc, ~/.zshrc, etc.). Term-cmd will work in shells launched directly from Emacs even if you don't, but it will only work in tmux and ssh sessions if you do." term-cmd--bin-dir)
(add-to-list 'exec-path term-cmd--bin-dir)
(setenv "PATH" (concat term-cmd--bin-dir path-separator (getenv "PATH"))))
(advice-add 'term-handle-ansi-terminal-messages :around 'term-cmd--advice))
;;;###autoload
(term-cmd--init)
(provide 'term-cmd)
;;; term-cmd.el ends here