Skip to content

Commit 8d2bcca

Browse files
committed
REPL, visual commands: allow "sudo" and "ENV" modifiers
a command like: ENV=env sudo htop will be recognized accordingly as visual. Thanks @Ambrevar ruricolist/cmd#10
1 parent db259c7 commit 8d2bcca

File tree

5 files changed

+153
-53
lines changed

5 files changed

+153
-53
lines changed

ciel.asd

+2
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
:cl-punch
8686

8787
:serapeum
88+
:shlex
8889

8990
;; tests
9091
:fiveam
@@ -111,6 +112,7 @@
111112
((:file "ciel")
112113
))
113114
(:file "repl")
115+
(:file "shell-utils")
114116
(:file "repl-utils"))
115117

116118
:build-operation "program-op"

docs/repl.md

+18-6
Original file line numberDiff line numberDiff line change
@@ -63,19 +63,31 @@ Use square brackets `[...]` to write a shell script, and use `$` inside it to es
6363

6464
The result is concatenated into a string and printed on stdout.
6565

66-
This feature is only available in CIEL's REPL, not on the CIEL-USER package.
66+
This feature is only available by default in CIEL's REPL, not on the
67+
CIEL-USER package. To enable it yourself, do:
6768

68-
Some programs are **visual** / interactive / ncurses-based, and need
69+
(ciel:enable-shell-passthrough)
70+
71+
But, some programs are **visual**, or interactive, because they have an ncurses or similar interface. They need
6972
to be run in their own terminal window. CIEL recognizes a few (`vim`,
70-
`htop`, `man`…) and runs them in the first terminal emulator found on
71-
the system of `terminator`, `xterm`, `gnome-terminal`). See the
72-
`*visual-commands*` variable.
73+
`htop`, `man`… see `*visual-commands*`) and runs them in the first terminal emulator found on
74+
the system: `terminator`, `xterm`, `gnome-terminal`, Emacs' `vterm` (with emacsclient) or your own.
75+
76+
So, you can run a command similar to this one:
77+
78+
ENV=env sudo htop
79+
80+
and it will open in a new terminal (hint: a visual command doesn't require the `!` prefix).
81+
82+
To use your terminal emulator of choice, do:
83+
84+
(push "myterminal" *visual-terminal-emulator-choices*)
7385

7486
> Note: this feature is experimental.
7587
7688
> Note: we encourage our users to use Emacs rather than a terminal!
7789
78-
We use the [Clesh](https://github.com/Neronus/clesh) library.
90+
We use the [Clesh](https://github.com/Neronus/clesh) library for the `!` shell passthrough.
7991

8092
See also [SHCL](https://github.com/bradleyjensen/shcl) for a more unholy union of posix-shell and Common Lisp.
8193

repl-utils.lisp

-46
Original file line numberDiff line numberDiff line change
@@ -47,49 +47,3 @@ bar:qux
4747
(format t "~c[1C" #\esc))
4848
(finish-output)))
4949

50-
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
51-
;;; Run visual / interactive / ncurses commands in their terminal window.
52-
;;;
53-
;;; How to guess a program is interactive?
54-
;;; We currently look from a hand-made list (à la Eshell).
55-
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
56-
57-
(defparameter *visual-commands*
58-
'(;; "emacs -nw" ;; in eshell, concept of visual-subcommands.
59-
"vim" "vi"
60-
"nano"
61-
"htop" "top"
62-
"man" "less" "more"
63-
"screen" "tmux"
64-
"lynx" "links" "mutt" "pine" "tin" "elm" "ncftp" "ncdu"
65-
"ranger"
66-
;; last but not least
67-
"ciel-repl")
68-
"List of visual/interactive/ncurses-based programs that will be run in their own terminal window.")
69-
70-
(defparameter *visual-terminal-emulator-choices*
71-
'("terminator" "x-terminal-emulator" "xterm" "gnome-terminal"))
72-
73-
(defparameter *visual-terminal-switches* '("-e")
74-
"Default options to the terminal. `-e' aka `--command'.")
75-
76-
(defun find-terminal ()
77-
"Return the first terminal emulator found on the system from the `*visual-terminal-emulator-choices*' list."
78-
(loop for program in *visual-terminal-emulator-choices*
79-
when (which:which program)
80-
return program))
81-
82-
(defun visual-command-p (text)
83-
"The command TEXT starts by a known visual command, listed in `*visual-commands*'."
84-
(let* ((cmd (string-left-trim "!" text)) ;; strip clesh syntax.
85-
(first-word (first (str:words cmd))))
86-
;; This will be smarter. https://github.com/ruricolist/cmd/issues/10
87-
(find first-word *visual-commands* :test #'equalp)))
88-
89-
(defun run-visual-command (text)
90-
"Run this text command into another terminal window."
91-
(let ((cmd (string-left-trim "!" text)))
92-
(uiop:launch-program `( ,(find-terminal)
93-
;; quick way to flatten the list of switches:
94-
,@*visual-terminal-switches*
95-
,cmd))))

shell-utils.lisp

+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
(in-package :sbcli)
2+
3+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
4+
;;; Run visual / interactive / ncurses commands in their terminal window.
5+
;;;
6+
;;; How to guess a program is interactive?
7+
;;; We currently look from a hand-made list (à la Eshell).
8+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
9+
10+
;; thanks @ambrevar: https://github.com/ruricolist/cmd/issues/10
11+
;; for handling command wrappers (sudo) and vterm.
12+
13+
(defparameter *visual-commands*
14+
'(;; "emacs -nw" ;; in eshell, see concept of visual-subcommands.
15+
"vim" "vi"
16+
"nano"
17+
"htop" "top"
18+
"man" "less" "more"
19+
"screen" "tmux"
20+
"lynx" "links" "mutt" "pine" "tin" "elm" "ncftp" "ncdu"
21+
"ranger"
22+
;; last but not least
23+
"ciel-repl")
24+
"List of visual/interactive/ncurses-based programs that will be run in their own terminal window.")
25+
26+
(defun vterm-terminal (cmd)
27+
"Build a command (string) to send to emacsclient to open CMD with Emacs' vterm."
28+
(list
29+
"emacsclient" "--eval"
30+
(let ((*print-case* :downcase))
31+
(write-to-string
32+
`(progn
33+
(vterm)
34+
(vterm-insert ,cmd)
35+
(vterm-send-return))))))
36+
37+
(defparameter *visual-terminal-emulator-choices*
38+
'("terminator" "x-terminal-emulator" "xterm" "gnome-terminal"
39+
#'vterm-terminal)
40+
"List of terminals, either a string or a function (that returns a more complete command, as a string).")
41+
42+
(defparameter *visual-terminal-switches* '("-e")
43+
"Default options to the terminal. `-e' aka `--command'.")
44+
45+
(defvar *command-wrappers* '("sudo" "env"))
46+
47+
(defun find-terminal ()
48+
"Return the first terminal emulator found on the system from the `*visual-terminal-emulator-choices*' list."
49+
(loop for program in *visual-terminal-emulator-choices*
50+
if (and (stringp program)
51+
(which:which program))
52+
return program
53+
else if (functionp program) return program))
54+
55+
(defun basename (arg)
56+
(when arg
57+
(namestring (pathname-name arg))))
58+
59+
(defun shell-command-wrapper-p (command)
60+
(find (basename command)
61+
*command-wrappers*
62+
:test #'string-equal))
63+
64+
(defun shell-flag-p (arg)
65+
(str:starts-with-p "-" arg))
66+
67+
(defun shell-variable-p (arg)
68+
(and (< 1 (length arg))
69+
(str:contains? "=" (subseq arg 1))))
70+
71+
(defun shell-first-positional-argument (command)
72+
"Recursively find the first command that's not a flag, not a variable setting and
73+
not in `*command-wrappers*'."
74+
(when command
75+
(if (or (shell-flag-p (first command))
76+
(shell-variable-p (first command))
77+
(shell-command-wrapper-p (first command)))
78+
(shell-first-positional-argument (rest command))
79+
(first command))))
80+
81+
(defun shell-ensure-clean-command-list (command)
82+
"Return a list of commands, stripped out of a potential \"!\" prefix from Clesh syntax."
83+
(unless (consp command)
84+
(setf command (shlex:split command)))
85+
;; remove optional ! clesh syntax.
86+
(setf (first command)
87+
(string-left-trim "!" (first command)))
88+
;; remove blank strings, in case we wrote "! command".
89+
(remove-if #'str:blankp command))
90+
91+
(defun visual-command-p (command)
92+
"Return true if COMMAND runs one of the programs in `*visual-commands*'.
93+
COMMAND is either a list of strings or a string.
94+
`*command-wrappers*' are supported, i.e. the following works:
95+
96+
env FOO=BAR sudo -i powertop"
97+
(setf command (shell-ensure-clean-command-list command))
98+
(let ((cmd (shell-first-positional-argument command)))
99+
(when cmd
100+
(find (basename cmd)
101+
*visual-commands*
102+
:test #'string=))))
103+
104+
(defun run-visual-command (text)
105+
"Run this command (string) in another terminal window."
106+
(let* ((cmd (string-left-trim "!" text))
107+
(terminal (find-terminal)))
108+
(if terminal
109+
(cond
110+
((stringp terminal)
111+
(uiop:launch-program `(,terminal
112+
;; flatten the list of switches
113+
,@*visual-terminal-switches*
114+
,cmd)))
115+
((functionp terminal)
116+
(uiop:launch-program (funcall terminal cmd)))
117+
(t
118+
(format *error-output* "We cannot use a terminal designator of type ~a. Please use a string (\"xterm\") or a function that returns a string." (type-of terminal))))
119+
;; else no terminal found.
120+
(format *error-output* "Could not find a terminal emulator amongst the list ~a: ~s"
121+
'*visual-terminal-emulator-choices*
122+
*visual-terminal-emulator-choices*))))
123+
124+
#+(or)
125+
(assert (string-equal "htop"
126+
(visual-command-p "env rst=ldv sudo htop")))

src/ciel.lisp

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
12
(in-package :cl-user)
23
(defpackage ciel
3-
(:use :cl))
4+
(:use :cl)
5+
(:export :enable-shell-passthrough))
46

57
(in-package :ciel)
68

@@ -332,3 +334,7 @@ We currently only try this with serapeum. See *deps/serapeum/sequences-hashtable
332334

333335
(when *pretty-print-hash-tables*
334336
(toggle-pretty-print-hash-table t))
337+
338+
(defun enable-shell-passthrough ()
339+
"Enable the shell passthrough with \"!\". Enable Clesh's readtable."
340+
(named-readtables:in-readtable clesh:syntax))

0 commit comments

Comments
 (0)