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
|
;;; system-commands.el --- System power and session management -*- lexical-binding: t; coding: utf-8; -*-
;; author: Craig Jennings <c@cjennings.net>
;;
;;; Commentary:
;;
;; System commands for logout, lock, suspend, shutdown, reboot, and Emacs
;; exit/restart. Provides both a keymap (C-; !) and a completing-read menu.
;;
;; Commands include:
;; - Logout (terminate user session)
;; - Lock screen (slock)
;; - Suspend (systemctl suspend)
;; - Shutdown (systemctl poweroff)
;; - Reboot (systemctl reboot)
;; - Exit Emacs (kill-emacs)
;; - Restart Emacs (via systemctl --user restart emacs.service)
;;
;; Dangerous commands (logout, suspend, shutdown, reboot) require confirmation.
;;
;;; Code:
(eval-when-compile (require 'keybindings))
(eval-when-compile (require 'subr-x))
(require 'rx)
;; ------------------------------ System Commands ------------------------------
(defun cj/system-cmd--resolve (cmd)
"Return (values symbol-or-nil command-string label) for CMD."
(cond
((symbolp cmd)
(let ((val (and (boundp cmd) (symbol-value cmd))))
(unless (and (stringp val) (not (string-empty-p val)))
(user-error "Variable %s is not a non-empty string" cmd))
(list cmd val (symbol-name cmd))))
((stringp cmd)
(let ((s (string-trim cmd)))
(when (string-empty-p s) (user-error "Command string is empty"))
(list nil s "command")))
(t (user-error "Error: cj/system-cmd expects a string or a symbol"))))
(defun cj/system-cmd (cmd)
"Run CMD (string or symbol naming a string) detached via the shell.
Shell expansions like $(...) are supported. Output is silenced.
If CMD is deemed dangerous, ask for confirmation."
(interactive (list (read-shell-command "System command: ")))
(pcase-let ((`(,sym ,cmdstr ,label) (cj/system-cmd--resolve cmd)))
(when (and sym (get sym 'cj/system-confirm)
(memq (read-char-choice
(format "Run %s now (%s)? (Y/n) " label cmdstr)
'(?y ?Y ?n ?N ?\r ?\n ?\s))
'(?n ?N)))
(user-error "Aborted"))
(let ((proc (start-process-shell-command "cj/system-cmd" nil
(format "nohup %s >/dev/null 2>&1 &" cmdstr))))
(set-process-query-on-exit-flag proc nil)
(set-process-sentinel proc #'ignore)
(message "Running %s..." label))))
(defmacro cj/defsystem-command (name var cmdstr &optional confirm)
"Define VAR with CMDSTR and interactive command NAME to run it.
If CONFIRM is non-nil, mark VAR to always require confirmation."
(declare (indent defun))
`(progn
(defvar ,var ,cmdstr)
,(when confirm `(put ',var 'cj/system-confirm t))
(defun ,name ()
,(format "Run %s via `cj/system-cmd'." var)
(interactive)
(cj/system-cmd ',var))))
;; Define system commands
(cj/defsystem-command cj/system-cmd-logout logout-cmd "loginctl terminate-user $(whoami)" t)
(cj/defsystem-command cj/system-cmd-lock lockscreen-cmd "slock")
(cj/defsystem-command cj/system-cmd-suspend suspend-cmd "systemctl suspend" t)
(cj/defsystem-command cj/system-cmd-shutdown shutdown-cmd "systemctl poweroff" t)
(cj/defsystem-command cj/system-cmd-reboot reboot-cmd "systemctl reboot" t)
(defun cj/system-cmd-exit-emacs ()
"Exit Emacs server and all clients."
(interactive)
(when (memq (read-char-choice
"Exit Emacs? (Y/n) "
'(?y ?Y ?n ?N ?\r ?\n ?\s))
'(?n ?N))
(user-error "Aborted"))
(kill-emacs))
(defun cj/system-cmd-restart-emacs ()
"Restart Emacs server after saving buffers."
(interactive)
(when (memq (read-char-choice
"Restart Emacs? (Y/n) "
'(?y ?Y ?n ?N ?\r ?\n ?\s))
'(?n ?N))
(user-error "Aborted"))
(save-some-buffers)
;; Start the restart process before killing Emacs
(run-at-time 0.5 nil
(lambda ()
(call-process-shell-command
"systemctl --user restart emacs.service && emacsclient -c"
nil 0)))
(run-at-time 1 nil #'kill-emacs)
(message "Restarting Emacs..."))
(defvar-keymap cj/system-command-map
:doc "Keymap for system commands."
"L" #'cj/system-cmd-logout
"r" #'cj/system-cmd-reboot
"s" #'cj/system-cmd-shutdown
"S" #'cj/system-cmd-suspend
"l" #'cj/system-cmd-lock
"E" #'cj/system-cmd-exit-emacs
"e" #'cj/system-cmd-restart-emacs)
(keymap-set cj/custom-keymap "!" cj/system-command-map)
(defun cj/system-command-menu ()
"Present system commands via \='completing-read\='."
(interactive)
(let* ((commands '(("Logout System" . cj/system-cmd-logout)
("Lock Screen" . cj/system-cmd-lock)
("Suspend System" . cj/system-cmd-suspend)
("Shutdown System" . cj/system-cmd-shutdown)
("Reboot System" . cj/system-cmd-reboot)
("Exit Emacs" . cj/system-cmd-exit-emacs)
("Restart Emacs" . cj/system-cmd-restart-emacs)))
(choice (completing-read "System command: " commands nil t)))
(when-let ((cmd (alist-get choice commands nil nil #'equal)))
(call-interactively cmd))))
(keymap-set cj/custom-keymap "!" #'cj/system-command-menu)
(with-eval-after-load 'which-key
(which-key-add-key-based-replacements "C-; !" "system commands"))
(provide 'system-commands)
;;; system-commands.el ends here
|