utils.lisp 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  1. ;;; utils.lisp --- Additional variables, functions and commands
  2. ;; Copyright © 2013–2019 Alex Kost <alezost@gmail.com>
  3. ;; This program is free software; you can redistribute it and/or modify
  4. ;; it under the terms of the GNU General Public License as published by
  5. ;; the Free Software Foundation, either version 3 of the License, or
  6. ;; (at your option) any later version.
  7. ;;
  8. ;; This program is distributed in the hope that it will be useful,
  9. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. ;; GNU General Public License for more details.
  12. ;;
  13. ;; You should have received a copy of the GNU General Public License
  14. ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
  15. ;;; Code:
  16. (in-package :stumpwm)
  17. ;; These symbols are used in "mode-line-*.lisp" files.
  18. (export
  19. '(al/read-sys-file
  20. al/file-readable?))
  21. (defun al/executable-exists? (name)
  22. "Return t, if NAME executable exists in PATH."
  23. (zerop
  24. (nth-value 2
  25. (uiop:run-program (concat "command -v " name)
  26. :force-shell t
  27. :ignore-error-status t))))
  28. (defun al/read-sys-file (file-name &optional to-number)
  29. "Return a line (string) from FILE-NAME sysfs file.
  30. If TO-NUMBER is non-nil, convert this string into a number.
  31. Return nil in case of any error."
  32. (let ((file-name (probe-file file-name)))
  33. (and file-name
  34. (let ((param (with-open-file (file file-name)
  35. (read-line-from-sysfs file))))
  36. (if to-number
  37. (parse-integer param :junk-allowed t)
  38. param)))))
  39. (defun al/file-readable? (file)
  40. "Return t, if FILE is available for reading."
  41. (handler-case
  42. (with-open-file (f file)
  43. (and (read-line f) t))
  44. (stream-error () nil)))
  45. ;;; Floating windows
  46. (defun al/float-window-focus-forward
  47. (window-list &optional (window (group-current-window
  48. (current-group))))
  49. "Focus the next window in WINDOW-LIST after WINDOW."
  50. (let* ((wins (cdr (member window window-list)))
  51. (nw (if wins
  52. (car wins)
  53. ;; If the last window in the list is focused, then
  54. ;; focus the first one.
  55. (car window-list))))
  56. (and nw (focus-window nw))))
  57. (defcommand (al/float-window-other float-group) () ()
  58. "Focus previously focused floating window."
  59. (focus-window (cadr (group-windows (current-group)))))
  60. (defcommand (al/float-window-next float-group) () ()
  61. "Focus next floating window."
  62. (al/float-window-focus-forward
  63. (sort-windows (current-group))))
  64. (defcommand (al/float-window-previous float-group) () ()
  65. "Focus previous floating window."
  66. (al/float-window-focus-forward
  67. (nreverse (sort-windows (current-group)))))
  68. (defcommand (al/move-float-window float-group)
  69. (x y) ((:number "+ X: ") (:number "+ Y: "))
  70. "Move current floating window by X and Y pixels."
  71. (float-window-move-resize
  72. (current-window)
  73. :x (+ (window-x (current-window)) x)
  74. :y (+ (window-y (current-window)) y)))
  75. (defcommand (al/resize-float-window float-group)
  76. (width height) ((:number "+ Width: ") (:number "+ Height: "))
  77. "Resize current floating window by WIDTH and HEIGHT pixels."
  78. (float-window-move-resize
  79. (current-window)
  80. :width (+ (window-width (current-window)) width)
  81. :height (+ (window-height (current-window)) height)))
  82. (defcommand (al/float-window-gravity float-group)
  83. (gravity) ((:gravity "Gravity: "))
  84. "Move current floating window to a particular place of the screen.
  85. GRAVITY controls where the window will appear. Possible values are:
  86. :center, :top, :right, :bottom, :left, :top, :top-left, :bottom-right,
  87. :bottom-left."
  88. (let* ((screen-width (screen-width (current-screen)))
  89. (screen-height (screen-height (current-screen)))
  90. (window-width (+ (window-width (current-window))
  91. (* 2 *float-window-border*)))
  92. (window-height (+ (window-height (current-window))
  93. *float-window-border*
  94. *float-window-title-height*))
  95. (x-right (- screen-width window-width))
  96. (x-center (round (/ x-right 2)))
  97. (y-bottom (- screen-height window-height))
  98. (y-center (round (/ y-bottom 2)))
  99. (coords (ccase gravity
  100. (:center (cons x-center y-center))
  101. (:top-left (cons 0 0))
  102. (:top (cons nil 0))
  103. (:top-right (cons x-right 0))
  104. (:right (cons x-right nil))
  105. (:bottom-right (cons x-right y-bottom))
  106. (:bottom (cons nil y-bottom))
  107. (:bottom-left (cons 0 y-bottom))
  108. (:left (cons 0 nil)))))
  109. (float-window-move-resize
  110. (current-window)
  111. :x (car coords) :y (cdr coords))))
  112. ;;; Windows, frames and groups
  113. (defun al/class-window-p (class &optional (win (current-window)))
  114. "Return T if a window WIN is of class CLASS."
  115. (and win (string= class (window-class win))))
  116. (defcommand al/focus-window-by-class (class) ((:string "Window class: "))
  117. "Focus window class CLASS.
  118. Return the window or nil if there is no such."
  119. (if (al/class-window-p class)
  120. (current-window)
  121. (let ((win (car (or ;; priority to the window from the current group
  122. (find-matching-windows (list :class class) nil nil)
  123. (find-matching-windows (list :class class) t t)))))
  124. (if win
  125. (focus-all win)
  126. (message "No ~a window." class))
  127. win)))
  128. (defcommand al/gmove-to-other-group () ()
  129. "Move the current window to the other group and go to that group."
  130. (let ((group (car (remove-if (lambda (g) (eq g (current-group)))
  131. (screen-groups (current-screen))))))
  132. (if group
  133. (progn (gmove group)
  134. (switch-to-group group))
  135. (echo "There is only one group."))))
  136. (defcommand (al/fother tile-group) () ()
  137. "Jump to the previously selected frame.
  138. This is a substitution for `fother': the problem with `fother' is that
  139. it does nothing if the last frame does not exist anymore. This command
  140. simply moves the focus to the next existing frame."
  141. (let* ((group (current-group))
  142. (frames (group-frames group))
  143. (last-frame (tile-group-last-frame group)))
  144. (if (and last-frame
  145. (find last-frame frames))
  146. (focus-frame group last-frame)
  147. (focus-frame-after group frames))))
  148. (defun al/set-frames (frames &optional (populatep t))
  149. "Display FRAMES in the current group.
  150. The first frame will become the current one and will contain the current
  151. window. If POPULATEP is nil, do not populate the rest frames with
  152. windows."
  153. (let* ((screen (current-screen))
  154. (group (screen-current-group screen))
  155. (head (current-head group))
  156. (cur-window (group-current-window group))
  157. (cur-frame (first frames)))
  158. (mapc (lambda (w)
  159. (setf (window-frame w) cur-frame))
  160. (group-windows group))
  161. (mapc (lambda (f)
  162. (setf (frame-window f) nil))
  163. (rest frames))
  164. (setf (frame-window cur-frame) cur-window
  165. (tile-group-frame-head group head) frames)
  166. (when populatep
  167. (populate-frames group))
  168. (focus-frame group cur-frame)
  169. (update-decoration cur-window)
  170. (sync-frame-windows group cur-frame)))
  171. ;;; Showing and toggling the root window
  172. (defvar *al/window-configuration* nil
  173. "Last saved window configuration.")
  174. (defcommand al/show-root () ()
  175. "Show root window."
  176. (when (cdr (group-frames (current-group)))
  177. ;; Make one frame if necessary.
  178. (only))
  179. (fclear))
  180. (defcommand al/toggle-root () ()
  181. "Toggle between root window and last window configuration."
  182. (if (current-window)
  183. (progn
  184. (setf *al/window-configuration* (dump-group (current-group)))
  185. (al/show-root))
  186. ;; Current window is root.
  187. (if *al/window-configuration*
  188. (restore-group (current-group) *al/window-configuration*)
  189. (echo "There is no saved window configuration yet."))))
  190. ;;; Sending keys to windows
  191. (defcommand al/send-key (key &optional (win (current-window))) (:key)
  192. "Send key press and key release events for KEY to window WIN."
  193. (let ((xwin (window-xwin win)))
  194. (multiple-value-bind (code state) (key-to-keycode+state key)
  195. (flet ((send (event)
  196. (xlib:send-event xwin event (xlib:make-event-mask event)
  197. :display *display*
  198. :root (screen-root (window-screen win))
  199. :x 0 :y 0 :root-x 0 :root-y 0
  200. :window xwin :event-window xwin
  201. :code code
  202. :state state)))
  203. (send :key-press)
  204. (send :key-release)
  205. (xlib:display-finish-output *display*)))))
  206. (defun al/send-keys (keys &key (win (current-window))
  207. (sleep 0) loop loop-quit-var)
  208. "Send keys to the window WIN.
  209. KEYS is a string for `kbd', a list of such strings or functions or a
  210. function returning a string or a list.
  211. SLEEP is a time between sending keys or a function for defining
  212. this time.
  213. If LOOP is t, send keys in a loop (the whole combination of strings,
  214. lists and functions in KEYS will be repeated). It will be broken when
  215. a variable which name is passed to LOOP-QUIT-VAR returns t. Be aware,
  216. infinite loop is not a joke."
  217. (labels ((send-key (key)
  218. (al/send-key (kbd key) win)
  219. ;; (print key)
  220. (sleep (if (numberp sleep)
  221. sleep
  222. (funcall sleep))))
  223. (send-keys (key-def &optional loop)
  224. (loop
  225. do (cond
  226. ((stringp key-def)
  227. (send-key key-def))
  228. ((listp key-def)
  229. (dolist (key key-def) (send-keys key)))
  230. ((functionp key-def)
  231. (send-keys (funcall key-def)))
  232. (t (error "Keys should be a string, a list or a function")))
  233. while (and loop
  234. (null (and loop-quit-var (eval loop-quit-var)))))))
  235. (send-keys keys loop)
  236. (echo "Quitting sending keys.")))
  237. ;;; Interacting with Shepherd user services
  238. ;; The following makes sense only for my shepherd user services, which
  239. ;; can be started in different X instances/displays/VTs:
  240. ;; <https://github.com/alezost/shepherd-config>
  241. (defun al/herd-command (service &optional (action "restart")
  242. (display (getenv "DISPLAY")))
  243. "Return 'herd ACTION SERVICE:DISPLAY' command.
  244. DISPLAY is a display number (can be a number or string optionally
  245. beginning with ':') where a service is started."
  246. (format nil "herd ~a ~a:~a"
  247. action service
  248. (if (numberp display)
  249. display
  250. (string-left-trim ":" display))))
  251. (defun al/shepherd-service-started-p
  252. (service &optional (display (getenv "DISPLAY")))
  253. "Return non-nil, if Shepherd SERVICE is running."
  254. (let ((output (run-shell-command
  255. (al/herd-command service "status" display)
  256. t)))
  257. (search "started" output)))
  258. (defcommand al/toggle-shepherd-service
  259. (service &optional (display (getenv "DISPLAY")))
  260. ((:string "toggle Shepherd service: "))
  261. "Start/stop Shepherd SERVICE on DISPLAY."
  262. (let* ((startedp (al/shepherd-service-started-p service display)))
  263. (run-shell-command (al/herd-command service
  264. (if startedp "stop" "start")
  265. display))
  266. (message (concat "^5*~a~a^7* has been "
  267. (if startedp
  268. "^B^1*stopped"
  269. "^2*started")
  270. "^b^7*.")
  271. service display)))
  272. ;;; Interacting with emacs
  273. (defun al/emacs-window-p (&optional (window (current-window)))
  274. "Return non-nil, if WINDOW is Emacs window in the current frame."
  275. (and (al/class-window-p "Emacs" window)
  276. (or (not (eq (type-of (current-group)) 'tile-group))
  277. (eq (frame-window (tile-group-current-frame (current-group)))
  278. window))))
  279. (defcommand al/send-key-to-emacs (key) ((:key "Key: "))
  280. "Focus emacs window and send KEY to it."
  281. (let ((win (al/focus-window-by-class "Emacs")))
  282. (and win (al/send-key key win))))
  283. (defcommand al/emacs () ()
  284. "Start emacs unless it is already running, in which case focus it."
  285. (run-or-raise (al/herd-command "emacs")
  286. '(:class "Emacs")))
  287. (defcommand al/emacs-eval (arg &optional server-name) ((:shell "emacs-eval: "))
  288. "Evaluate ARG with emacsclient."
  289. (let ((args (list "--eval" arg)))
  290. (when server-name
  291. (setq args (append (list "--socket-name" server-name) args)))
  292. (run-prog "emacsclient" :args args :wait nil :search t)))
  293. (defcommand al/emacs-eval-show (arg) ((:shell "emacs-eval: "))
  294. "Evaluate ARG with emacsclient and raise emacs."
  295. (al/emacs-eval arg)
  296. (or (al/emacs-window-p) (al/emacs)))
  297. (defcommand al/emms-eval (arg &optional (server-name "server-emms"))
  298. ((:shell "emms-eval: "))
  299. "Evaluate ARG with emacsclient."
  300. (al/emacs-eval arg server-name))
  301. (defcommand al/emms-eval-show (arg) ((:shell "emms-eval: "))
  302. "Evaluate ARG with emacsclient and raise emacs."
  303. (al/emms-eval arg)
  304. (or (al/emacs-window-p) (al/emacs)))
  305. ;;; Interacting with browser
  306. (defvar *al/browsers*
  307. '(("icecat" . "IceCat")
  308. ("firefox" . "Firefox"))
  309. "Alist of browsers.
  310. Each assoc should have a form of `*al/current-browser*'.")
  311. (defvar *al/current-browser* nil
  312. "Browser used by `al/browser' command.
  313. The value should be a cons of program name and window class of this
  314. program.")
  315. (defun al/current-browser ()
  316. "Return the currently used browser."
  317. (or *al/current-browser*
  318. (setf *al/current-browser*
  319. (or (find-if (lambda (assoc)
  320. (al/executable-exists? (car assoc)))
  321. *al/browsers*)
  322. (progn
  323. (echo "No working browsers found among `*al/browsers*'")
  324. (car *al/browsers*))))))
  325. (defcommand al/browser (&optional args) (:rest)
  326. "Start browser unless it is already running, in which case focus it."
  327. (let ((browser (al/current-browser)))
  328. (if args
  329. (progn
  330. (run-shell-command (concat (car browser) " " args))
  331. (al/browser))
  332. (run-or-raise (car browser) `(:class ,(cdr browser))))))
  333. (defcommand al/browse (url) ((:shell "Browse URL: "))
  334. "Browse URL with `*al/current-browser*'."
  335. (run-prog (car (al/current-browser))
  336. :args (list url) :wait nil :search t))
  337. (defcommand al/browse-show (url) ((:shell "Browse URL: "))
  338. "Browse URL with `*al/current-browser*' and raise it."
  339. (al/browse url)
  340. (al/browser))
  341. ;;; Interacting with other progs
  342. (defcommand al/xterm () ()
  343. "Start xterm unless it is already running, in which case focus it."
  344. (run-or-raise (al/herd-command "xterm")
  345. '(:class "XTerm")))
  346. (defcommand al/toggle-unclutter () ()
  347. "Start/stop 'unclutter' on the current display."
  348. (al/toggle-shepherd-service "unclutter"))
  349. ;;; Mode line
  350. (defun al/mode-line-pos (pos)
  351. "Put the mode line at a position POS (can be :TOP or :BOTTOM)."
  352. (let ((screen (current-screen))
  353. (head (current-head)))
  354. (enable-mode-line screen head nil)
  355. (setf *mode-line-position* pos)
  356. (enable-mode-line screen head t)))
  357. (defcommand al/mode-line-on () ()
  358. "Turn the mode line on unconditionally."
  359. (enable-mode-line (current-screen) (current-head) t))
  360. (defcommand al/mode-line-bottom () ()
  361. "Put the mode line on the bottom of the screen."
  362. (al/mode-line-pos :bottom))
  363. (defcommand al/mode-line-top () ()
  364. "Put the mode line on the top of the screen."
  365. (al/mode-line-pos :top))
  366. ;;; Misc
  367. (defun al/random-float (bot top &optional (state *random-state*))
  368. "Return a random float between BOT and TOP bounds."
  369. (+ bot (random (- top bot) state)))
  370. (defun al/get-random-obj (objs)
  371. "Return a random object from alist OBJS.
  372. Each association is a pair of object and its probability (from 0 to
  373. 1). If the total probability is lower than 1, there is a chance to
  374. get nil."
  375. (let ((rnd (random 1.0))
  376. (prob 0))
  377. (loop
  378. for elm in objs
  379. do (setq prob (+ prob (cdr elm)))
  380. if (< rnd prob)
  381. return (car elm))))
  382. (defcommand al/banish-pointer () ()
  383. "Move mouse pointer to the top/center of the current screen."
  384. (let* ((screen (current-screen))
  385. (width (screen-width screen)))
  386. (xlib:warp-pointer (screen-root screen) (/ width 2) 0)))
  387. (defcommand al/yank-primary () ()
  388. "Insert X primary selection into the current window."
  389. (window-send-string (get-x-selection)))
  390. (defvar *al/ignore-emacs* nil
  391. "If non-nil, do not treat Emacs specially by `al/switch-frame-or-window'.")
  392. (defun al/switch-frame-or-window (switch-tile switch-float &optional key)
  393. "Select frame or window or emacs window.
  394. If current window is emacs and `*al/ignore-emacs*' is nil, send key
  395. sequence KEY to it.
  396. If current group is tiling, call SWITCH-TILE procedure.
  397. If current group is floating, call SWITCH-FLOAT procedure."
  398. (if (and key
  399. (al/emacs-window-p)
  400. (null *al/ignore-emacs*)
  401. ;; Ignore emacs anyway, if it has a single window.
  402. ;; The following code checks WINDOWS_NUM window property.
  403. ;; You can "teach" emacs to update this property by adding
  404. ;; this to your .emacs:
  405. ;; (add-hook 'window-configuration-change-hook
  406. ;; (lambda () (when (display-graphic-p)
  407. ;; (x-change-window-property
  408. ;; "WINDOWS_NUM"
  409. ;; (string (length (window-list)))
  410. ;; nil nil nil t))))
  411. (let ((windows-num (car (window-property (current-window)
  412. :WINDOWS_NUM))))
  413. (or (null windows-num)
  414. (/= 1 windows-num))))
  415. (al/send-key-to-emacs key)
  416. (if (eq (type-of (current-group)) 'tile-group)
  417. (funcall switch-tile)
  418. (funcall switch-float))))
  419. (defcommand al/other (&optional key) (:key)
  420. "Select previously selected frame or window or emacs window.
  421. If current window is emacs and `*al/ignore-emacs*' is nil, send key
  422. sequence KEY to it.
  423. If current group is tiling, select previously selected frame.
  424. If current group is floating, select previously selected window."
  425. (al/switch-frame-or-window #'al/fother #'al/float-window-other key))
  426. (defcommand al/next (&optional key) (:key)
  427. "Select next frame or window or emacs window.
  428. If current window is emacs and `*al/ignore-emacs*' is nil, send key
  429. sequence KEY to it.
  430. If current group is tiling, select next frame.
  431. If current group is floating, select next window."
  432. (al/switch-frame-or-window #'fnext #'al/float-window-next key))
  433. (defcommand al/toggle-ignore-emacs () ()
  434. "Toggle `*al/ignore-emacs*'."
  435. (setf *al/ignore-emacs* (not *al/ignore-emacs*))
  436. (message "^b^7*Switching between emacs windows ~a^b^7*."
  437. (if *al/ignore-emacs* "^B^1*disabled" "^2*enabled")))
  438. (defmacro al/defun-with-delay (seconds name args &rest body)
  439. "Define NAME function with ARGS and BODY.
  440. It is like a usual `defun', except when the function is called, it is
  441. evaluated only if the number of SECONDS has already been passed since
  442. the last call. If this time has not been passed yet, the previous value
  443. of the function is returned without evaluation.
  444. For example, the following `delayed-time' function will return a new
  445. time string only every 10 seconds:
  446. (al/defun-with-delay
  447. 10 delayed-time ()
  448. (time-format \"%H:%M:%S\"))
  449. "
  450. (let ((next-time-var (make-symbol "next-time"))
  451. (last-value-var (make-symbol "last-value")))
  452. `(let ((,next-time-var 0)
  453. ,last-value-var)
  454. (defun ,name ,args
  455. (let ((now (get-universal-time)))
  456. (if (< now ,next-time-var)
  457. ,last-value-var
  458. (setf ,next-time-var (+ now ,seconds)
  459. ,last-value-var (progn ,@body))))))))
  460. ;;; utils.lisp ends here