diff --git a/ciel.asd b/ciel.asd index 7e68cb6..312557f 100644 --- a/ciel.asd +++ b/ciel.asd @@ -85,6 +85,7 @@ :cl-punch :serapeum + :shlex ;; tests :fiveam @@ -111,6 +112,7 @@ ((:file "ciel") )) (:file "repl") + (:file "shell-utils") (:file "repl-utils")) :build-operation "program-op" diff --git a/docs/repl.md b/docs/repl.md index 3299bca..3ee2f82 100644 --- a/docs/repl.md +++ b/docs/repl.md @@ -63,19 +63,31 @@ Use square brackets `[...]` to write a shell script, and use `$` inside it to es The result is concatenated into a string and printed on stdout. -This feature is only available in CIEL's REPL, not on the CIEL-USER package. +This feature is only available by default in CIEL's REPL, not on the +CIEL-USER package. To enable it yourself, do: -Some programs are **visual** / interactive / ncurses-based, and need + (ciel:enable-shell-passthrough) + +But, some programs are **visual**, or interactive, because they have an ncurses or similar interface. They need to be run in their own terminal window. CIEL recognizes a few (`vim`, -`htop`, `man`…) and runs them in the first terminal emulator found on -the system of `terminator`, `xterm`, `gnome-terminal`). See the -`*visual-commands*` variable. +`htop`, `man`… see `*visual-commands*`) and runs them in the first terminal emulator found on +the system: `terminator`, `xterm`, `gnome-terminal`, Emacs' `vterm` (with emacsclient) or your own. + +So, you can run a command similar to this one: + + ENV=env sudo htop + +and it will open in a new terminal (hint: a visual command doesn't require the `!` prefix). + +To use your terminal emulator of choice, do: + + (push "myterminal" *visual-terminal-emulator-choices*) > Note: this feature is experimental. > Note: we encourage our users to use Emacs rather than a terminal! -We use the [Clesh](https://github.com/Neronus/clesh) library. +We use the [Clesh](https://github.com/Neronus/clesh) library for the `!` shell passthrough. See also [SHCL](https://github.com/bradleyjensen/shcl) for a more unholy union of posix-shell and Common Lisp. diff --git a/repl-utils.lisp b/repl-utils.lisp index a6af89a..4d08698 100644 --- a/repl-utils.lisp +++ b/repl-utils.lisp @@ -47,49 +47,3 @@ bar:qux (format t "~c[1C" #\esc)) (finish-output))) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;;; Run visual / interactive / ncurses commands in their terminal window. -;;; -;;; How to guess a program is interactive? -;;; We currently look from a hand-made list (à la Eshell). -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(defparameter *visual-commands* - '(;; "emacs -nw" ;; in eshell, concept of visual-subcommands. - "vim" "vi" - "nano" - "htop" "top" - "man" "less" "more" - "screen" "tmux" - "lynx" "links" "mutt" "pine" "tin" "elm" "ncftp" "ncdu" - "ranger" - ;; last but not least - "ciel-repl") - "List of visual/interactive/ncurses-based programs that will be run in their own terminal window.") - -(defparameter *visual-terminal-emulator-choices* - '("terminator" "x-terminal-emulator" "xterm" "gnome-terminal")) - -(defparameter *visual-terminal-switches* '("-e") - "Default options to the terminal. `-e' aka `--command'.") - -(defun find-terminal () - "Return the first terminal emulator found on the system from the `*visual-terminal-emulator-choices*' list." - (loop for program in *visual-terminal-emulator-choices* - when (which:which program) - return program)) - -(defun visual-command-p (text) - "The command TEXT starts by a known visual command, listed in `*visual-commands*'." - (let* ((cmd (string-left-trim "!" text)) ;; strip clesh syntax. - (first-word (first (str:words cmd)))) - ;; This will be smarter. https://github.com/ruricolist/cmd/issues/10 - (find first-word *visual-commands* :test #'equalp))) - -(defun run-visual-command (text) - "Run this text command into another terminal window." - (let ((cmd (string-left-trim "!" text))) - (uiop:launch-program `( ,(find-terminal) - ;; quick way to flatten the list of switches: - ,@*visual-terminal-switches* - ,cmd)))) diff --git a/shell-utils.lisp b/shell-utils.lisp new file mode 100644 index 0000000..92028ec --- /dev/null +++ b/shell-utils.lisp @@ -0,0 +1,126 @@ +(in-package :sbcli) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; Run visual / interactive / ncurses commands in their terminal window. +;;; +;;; How to guess a program is interactive? +;;; We currently look from a hand-made list (à la Eshell). +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; thanks @ambrevar: https://github.com/ruricolist/cmd/issues/10 +;; for handling command wrappers (sudo) and vterm. + +(defparameter *visual-commands* + '(;; "emacs -nw" ;; in eshell, see concept of visual-subcommands. + "vim" "vi" + "nano" + "htop" "top" + "man" "less" "more" + "screen" "tmux" + "lynx" "links" "mutt" "pine" "tin" "elm" "ncftp" "ncdu" + "ranger" + ;; last but not least + "ciel-repl") + "List of visual/interactive/ncurses-based programs that will be run in their own terminal window.") + +(defun vterm-terminal (cmd) + "Build a command (string) to send to emacsclient to open CMD with Emacs' vterm." + (list + "emacsclient" "--eval" + (let ((*print-case* :downcase)) + (write-to-string + `(progn + (vterm) + (vterm-insert ,cmd) + (vterm-send-return)))))) + +(defparameter *visual-terminal-emulator-choices* + '("terminator" "x-terminal-emulator" "xterm" "gnome-terminal" + #'vterm-terminal) + "List of terminals, either a string or a function (that returns a more complete command, as a string).") + +(defparameter *visual-terminal-switches* '("-e") + "Default options to the terminal. `-e' aka `--command'.") + +(defvar *command-wrappers* '("sudo" "env")) + +(defun find-terminal () + "Return the first terminal emulator found on the system from the `*visual-terminal-emulator-choices*' list." + (loop for program in *visual-terminal-emulator-choices* + if (and (stringp program) + (which:which program)) + return program + else if (functionp program) return program)) + +(defun basename (arg) + (when arg + (namestring (pathname-name arg)))) + +(defun shell-command-wrapper-p (command) + (find (basename command) + *command-wrappers* + :test #'string-equal)) + +(defun shell-flag-p (arg) + (str:starts-with-p "-" arg)) + +(defun shell-variable-p (arg) + (and (< 1 (length arg)) + (str:contains? "=" (subseq arg 1)))) + +(defun shell-first-positional-argument (command) + "Recursively find the first command that's not a flag, not a variable setting and +not in `*command-wrappers*'." + (when command + (if (or (shell-flag-p (first command)) + (shell-variable-p (first command)) + (shell-command-wrapper-p (first command))) + (shell-first-positional-argument (rest command)) + (first command)))) + +(defun shell-ensure-clean-command-list (command) + "Return a list of commands, stripped out of a potential \"!\" prefix from Clesh syntax." + (unless (consp command) + (setf command (shlex:split command))) + ;; remove optional ! clesh syntax. + (setf (first command) + (string-left-trim "!" (first command))) + ;; remove blank strings, in case we wrote "! command". + (remove-if #'str:blankp command)) + +(defun visual-command-p (command) + "Return true if COMMAND runs one of the programs in `*visual-commands*'. + COMMAND is either a list of strings or a string. +`*command-wrappers*' are supported, i.e. the following works: + + env FOO=BAR sudo -i powertop" + (setf command (shell-ensure-clean-command-list command)) + (let ((cmd (shell-first-positional-argument command))) + (when cmd + (find (basename cmd) + *visual-commands* + :test #'string=)))) + +(defun run-visual-command (text) + "Run this command (string) in another terminal window." + (let* ((cmd (string-left-trim "!" text)) + (terminal (find-terminal))) + (if terminal + (cond + ((stringp terminal) + (uiop:launch-program `(,terminal + ;; flatten the list of switches + ,@*visual-terminal-switches* + ,cmd))) + ((functionp terminal) + (uiop:launch-program (funcall terminal cmd))) + (t + (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)))) + ;; else no terminal found. + (format *error-output* "Could not find a terminal emulator amongst the list ~a: ~s" + '*visual-terminal-emulator-choices* + *visual-terminal-emulator-choices*)))) + +#+(or) +(assert (string-equal "htop" + (visual-command-p "env rst=ldv sudo htop"))) diff --git a/src/ciel.lisp b/src/ciel.lisp index a703e86..b7efbd1 100644 --- a/src/ciel.lisp +++ b/src/ciel.lisp @@ -1,6 +1,8 @@ + (in-package :cl-user) (defpackage ciel - (:use :cl)) + (:use :cl) + (:export :enable-shell-passthrough)) (in-package :ciel) @@ -332,3 +334,7 @@ We currently only try this with serapeum. See *deps/serapeum/sequences-hashtable (when *pretty-print-hash-tables* (toggle-pretty-print-hash-table t)) + +(defun enable-shell-passthrough () + "Enable the shell passthrough with \"!\". Enable Clesh's readtable." + (named-readtables:in-readtable clesh:syntax))