diff options
61 files changed, 4205 insertions, 816 deletions
diff --git a/.alias.d/00-general b/.alias.d/00-general index c9a4668..3feaef4 100644 --- a/.alias.d/00-general +++ b/.alias.d/00-general @@ -17,4 +17,6 @@ alias e=$EDITOR alias mtr='mtr --curses' alias t='cd ~/src/' -alias by='bat -l yaml'
\ No newline at end of file +alias by='batcat -l yaml' + +alias fd=fdfind
\ No newline at end of file @@ -130,3 +130,7 @@ PROMPT_DIRTRIM=2 if [ -e ~/.bashrc.post ]; then . ~/.bashrc.post fi + +export NVM_DIR="$HOME/.nvm" +[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm +[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion diff --git a/.config/rofi/config b/.config/rofi/config deleted file mode 100644 index 0406648..0000000 --- a/.config/rofi/config +++ /dev/null @@ -1 +0,0 @@ -rofi.theme: /usr/share/rofi/themes/Arc-Dark.rasi diff --git a/.config/rofi/config.rasi b/.config/rofi/config.rasi new file mode 100644 index 0000000..3b108ba --- /dev/null +++ b/.config/rofi/config.rasi @@ -0,0 +1,7 @@ +configuration { + font: "Iosevka 20"; + modes: [ combi ]; + combi-modes: [ window, drun, run ]; +} + +@theme "Arc-Dark" diff --git a/.emacs.d/early-init.el b/.emacs.d/early-init.el index 495c277..9d4ce2c 100644 --- a/.emacs.d/early-init.el +++ b/.emacs.d/early-init.el @@ -1,20 +1,65 @@ +;; I don't use any of these (menu-bar-mode -1) (tool-bar-mode -1) (scroll-bar-mode -1) -;; Initialise installed packages -(setq package-enable-at-startup t) - -;; Do not report warning errors -(setq native-comp-async-report-warnings-errors 'silent) - -;; Truly maximize screen +;; Do not resize when font size changes (setq frame-resize-pixelwise t) -;; Start maximized +;; By default, start maximized, undecorated (add-to-list 'default-frame-alist '(fullscreen . maximized)) +(add-to-list 'default-frame-alist '(undecorated . t)) + +;; Extend this list from to add more startup frames. +(defvar rul-startup-frames + '(("main" . nil) + ("terminals" . multi-vterm)) + "Startup frame specifications. +Each entry has the form (NAME . SETUP). NAME is the frame name. +SETUP is an optional function or interactive command called in that frame.") + +(defun rul-run-startup-frame-setup (setup) + "Run startup frame SETUP." + (cond + ((null setup) nil) + ((commandp setup) (call-interactively setup)) + ((functionp setup) (funcall setup)) + (t (message "Ignoring invalid startup frame setup: %S" setup)))) + +(defun rul-apply-startup-frame-name (frame name) + "Set FRAME name and title to NAME." + (with-selected-frame frame + (set-frame-name name) + (modify-frame-parameters frame `((title . ,name))))) + +(defun rul-create-startup-frames () + "Create the configured startup frames." + (when (display-graphic-p) + (let ((initial-frame (selected-frame)) + (specs rul-startup-frames)) + (when specs + (pcase-let ((`(,name . ,setup) (car specs))) + (rul-apply-startup-frame-name initial-frame name) + (with-selected-frame initial-frame + (rul-run-startup-frame-setup setup))) + (dolist (spec (cdr specs)) + (pcase-let ((`(,name . ,setup) spec)) + (let ((frame (make-frame `((name . ,name) + (title . ,name))))) + (rul-apply-startup-frame-name frame name) + (with-selected-frame frame + (rul-run-startup-frame-setup setup))))) + (select-frame initial-frame))))) + +(add-hook 'emacs-startup-hook #'rul-create-startup-frames) + +;; Initialise installed packages, otherwise, basic functions are not +;; available during the initialization stage. +(setq package-enable-at-startup t) -;; No need for titlebar -(modify-frame-parameters nil '((undecorated . t))) +;; Do not report warnings. It's too noisy. +(setq native-comp-async-report-warnings-errors 'silent) -;;; early-init.el ends here +;; Keep things minimal +(setq inhibit-startup-screen t) +(setq inhibit-startup-echo-area-message user-login-name) diff --git a/.emacs.d/init.el b/.emacs.d/init.el index 248abcb..6954df3 100644 --- a/.emacs.d/init.el +++ b/.emacs.d/init.el @@ -1,110 +1,35 @@ +;; package.el +(require 'package) (add-to-list 'package-archives '("melpa-stable" . "https://stable.melpa.org/packages/") t) (add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t) +(let ((backup-dir "~/.backup")) + (unless (file-directory-p backup-dir) + (make-directory backup-dir t)) + (setq backup-directory-alist `(("." . ,backup-dir)))) + +(setq + backup-by-copying t ; Don't delink hardlinks + delete-old-versions t ; Clean up the backups + kept-new-versions 3 ; keep some new versions + kept-old-versions 2 ; and some old ones, too + version-control t) ; Use version numbers on backups + ;; Do not persist customizations (setq custom-file (make-temp-file "emacs-custom-")) -;; Place backups in ~/.backups/ directory, like a civilized program. -;; ------ -(if (file-directory-p "~/.backup") - (setq backup-directory-alist '(("." . "~/.backup"))) - (message "Directory does not exist: ~/.backup")) - -(filesets-init) - -(setq backup-by-copying t ; Don't delink hardlinks - delete-old-versions t ; Clean up the backups - version-control t ; Use version numbers on backups, - kept-new-versions 3 ; keep some new versions - kept-old-versions 2) ; and some old ones, too - -;; --------- -;; Generic keybindings -;; --------- -(global-set-key (kbd "C-c d") 'diff-buffer-with-file) -(global-set-key (kbd "C-c R") 'revert-buffer) -(global-set-key (kbd "C-c w") 'whitespace-cleanup) - -(global-set-key (kbd "C-c b s") 'bookmark-set) -(global-set-key (kbd "C-c b j") 'bookmark-jump) - -(global-set-key (kbd "<f12>") 'compile) - -(defun help/insert-em-dash () - "Inserts an EM-DASH (not a HYPEN, not an N-DASH)" - (interactive) - (insert "—")) - -(global-set-key (kbd "C--") #'help/insert-em-dash) - -;; ------ -;; General config -;; ------ - -(setq fill-column 79) - -;; Make sure that pressing middle mouse button pastes right at point, -;; not where the mouse cursor is. -(setq mouse-yank-at-point t) - -(setq column-number-mode 1) -(setq line-number-mode 1) -(setq-default indent-tabs-mode nil) -(setq-default tab-width 4) - -(setq-default c-basic-offset 4) - -;; ------ -;; Initialize environment -;; ------ - -(setenv "TMPDIR" (concat (getenv "HOME") "/tmp")) -(add-hook 'after-init-hook #'server-start) - -;; ------ -;; Helper for compilation. -;; ------ -;; Close the compilation window if there was no error at all. -(defun compilation-exit-autoclose (status code msg) - ;; If M-x compile exists with a 0 - (when (and (eq status 'exit) (zerop code)) - ;; then bury the *compilation* buffer, so that C-x b doesn't go there - (bury-buffer) - ;; and delete the *compilation* window - (delete-window (get-buffer-window (get-buffer "*compilation*")))) - ;; Always return the anticipated result of compilation-exit-message-function - (cons msg code)) - -;; Specify my function (maybe I should have done a lambda function) -(setq compilation-exit-message-function 'compilation-exit-autoclose) -(setq compilation-read-command nil) - -;; Themes -(add-to-list 'custom-theme-load-path "~/.emacs.d/themes/") - -;; Remove scratch message -(setq initial-scratch-message "") - -;; Ask y or n instead of yes or no -(defalias 'yes-or-no-p 'y-or-n-p) +(setq fill-column 79) ; Wrap lines +(setq mouse-yank-at-point t) ; Do not follow mouse curors when mouse-yanking -;; Fancier buffer selection -(global-set-key (kbd "C-x C-b") 'bs-show) +(setq-default indent-tabs-mode nil) ; No tabs when indenting +(setq-default tab-width 4) ; How many spaces a tab represents -;;; Stefan Monnier <foo at acm.org>. It is the opposite of fill-paragraph -(defun unfill-paragraph (&optional region) - "Takes a multi-line paragraph and makes it into a single line of text." - (interactive (progn (barf-if-buffer-read-only) '(t))) - (let ((fill-column (point-max)) - ;; This would override `fill-column' if it's an integer. - (emacs-lisp-docstring-fill-column t)) - (fill-paragraph nil region))) +(setq initial-scratch-message "") -;; Handy key definition -(define-key global-map "\M-Q" 'unfill-paragraph) +(defalias 'yes-or-no-p 'y-or-n-p) ;; Only flash the mode line (setq ring-bell-function @@ -119,87 +44,59 @@ (setq show-paren-delay 0) (show-paren-mode 1) -;; Save what you enter into minibuffer prompts -(setq history-length 25) -(savehist-mode 1) +(savehist-mode 1) ; Save histories, including minibuffer -;; Remember and restore cursor information -(save-place-mode 1) +(save-place-mode 1) ; Remember and restore cursor information -;; Desktop -(setq desktop-path `(,user-emacs-directory)) -(setq desktop-base-file-name "desktop") -(desktop-save-mode 1) +(setq auto-save-no-message t) ; Do not print a message when auto-saving -;; Set this to t if you don't understand what it means -(setq vc-follow-symlinks nil) +(pixel-scroll-precision-mode 1) ; Precision scrolling -;; dired -(use-package dired-preview - :ensure t - :config +(defun rul-auto-copy-region () + (when (use-region-p) + (kill-ring-save (region-beginning) (region-end)))) - (setq dired-preview-delay 0.3) - (setq dired-preview-max-size (expt 2 20)) - (setq dired-preview-ignored-extensions-regexp - (concat "\\." - "\\(mkv\\|webm\\|mp4\\|mp3\\|ogg\\|m4a" - "\\|gz\\|zst\\|tar\\|xz\\|rar\\|zip" - "\\|iso\\|epub\\|pdf\\)")) +(add-hook 'activate-mark-hook #'rul-auto-copy-region) - (dired-preview-global-mode 1)) -(add-hook 'dired-mode-hook #'dired-hide-details-mode) +;; Source: https://protesilaos.com/codelog/2024-12-11-emacs-diff-save-some-buffers/ +(add-to-list 'save-some-buffers-action-alist + (list "d" + (lambda (buffer) (diff-buffer-with-file (buffer-file-name buffer))) + "show diff between the buffer and its file")) -(setq dired-guess-shell-alist-user - '(("\\.\\(png\\|jpe?g\\|tiff\\)" "feh" "xdg-open") - ("\\.\\(mp[34]\\|m4a\\|ogg\\|flac\\|webm\\|mkv\\)" "mpv" "xdg-open") - (".*" "xdg-open"))) +;; Server +(require 'server) +(setq server-client-instructions nil) ; Keep it quiet when opening an ec -;;; Icons -(use-package nerd-icons :ensure t ) -(use-package nerd-icons-dired :ensure t - :config - (add-hook 'dired-mode-hook #'nerd-icons-dired-mode) - ) +(unless (server-running-p) + (server-start)) -(dolist (path '("~/.emacs.d/rul-lisp/config" "~/.emacs.d/rul-lisp/packages")) +(dolist (path '("~/.emacs.d/rul-lisp/packages")) (add-to-list 'load-path path)) -(pixel-scroll-precision-mode 1) +(when-let* ((file (locate-user-emacs-file "rul-pre-init.el")) + ((file-exists-p file))) + (load-file file)) +(require 'rul-themes) +(require 'rul-bindings) (require 'rul-completion) -(require 'rul-prog) -(require 'rul-elfeed) +(require 'rul-dashboard) +(require 'rul-fm) +(require 'rul-fonts) +(require 'rul-io) +(require 'rul-mail) (require 'rul-modeline) -(require 'rul-media) (require 'rul-org) +(require 'rul-prog) +(require 'rul-terminals) +(require 'rul-vc) (require 'rul-wm) (require 'rul-write) -(load-file "~/.emacs.d/rul-init.d/fonts.el") -(load-file "~/.emacs.d/rul-init.d/themes.el") - -;; Init parts (will be deprecated in favor of packages) -(load-file "~/.emacs.d/rul-init.d/auto-fill.el") -(load-file "~/.emacs.d/rul-init.d/flycheck.el") -(load-file "~/.emacs.d/rul-init.d/flyspell.el") -(load-file "~/.emacs.d/rul-init.d/go-lang.el") -(load-file "~/.emacs.d/rul-init.d/hydra.el") -(load-file "~/.emacs.d/rul-init.d/ibuffer.el") -(load-file "~/.emacs.d/rul-init.d/imenu.el") -(load-file "~/.emacs.d/rul-init.d/latex.el") -(load-file "~/.emacs.d/rul-init.d/logos.el") -(load-file "~/.emacs.d/rul-init.d/magit.el") -(load-file "~/.emacs.d/rul-init.d/mail-mode.el") -(load-file "~/.emacs.d/rul-init.d/markdown.el") -(load-file "~/.emacs.d/rul-init.d/notmuch.el") -(load-file "~/.emacs.d/rul-init.d/vterm.el") -(load-file "~/.emacs.d/rul-init.d/which-key.el") - (when-let* ((file (locate-user-emacs-file "rul-post-init.el")) ((file-exists-p file))) (load-file file)) ;; init.el ends here -(put 'dired-find-alternate-file 'disabled nil) diff --git a/.emacs.d/rul-emacs.org b/.emacs.d/rul-emacs.org new file mode 100644 index 0000000..b12e34b --- /dev/null +++ b/.emacs.d/rul-emacs.org @@ -0,0 +1,1998 @@ +This is (will be) my Emacs literate configuration file. A self +contained file with all my configuration is useful for documentation +purposes. It will be modeled using the technique described by +Protesilaos for his own Emacs config file: +<https://protesilaos.com/emacs/dotemacs>. + +This method consists in generating all files /a priori/, after modifying +this file, and *not* at load time, as that would be too slow. + +#+begin_src emacs-lisp :tangle no :results none +(org-babel-tangle) +#+end_src + +* Overview of files and directories + +- =early-init.el=: quoting the [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Early-Init-File.html][Emacs documentation]], this file is "loaded + before the package system and GUI is initialized, so in it you can + customize variables that affect the package initialization process" +- =init.el=: the skeleton of my configuration framework. It will load + the rest of the modules. +- =rul-emacs-modules/=: a directory with Emacs modules specific to my + configuration. Modules group code related to a topic or theme of + configuration. For example, =rul-prog.el= contains code related to + programming, and =rul-org.el= contains code related to org-mode. If a + module gets too big, I can create a smaller module under the same + topic; for example, =rul-org-agenda.el=. +- =rul-post-init.el=: this file will be loaded after =init.el=, and will + normally live in other git repository. Here I normally add overrides + needed in my work computer. +- =rul-emacs.org=: this file. It (will) generate the rest of the structure. + +* Early configuration file (=early-init.el=) +** Graphical aspects +Customization of graphical aspects of Emacs, such as size, panels, etc. + +#+begin_src emacs-lisp :tangle "early-init.el" +;; I don't use any of these +(menu-bar-mode -1) +(tool-bar-mode -1) +(scroll-bar-mode -1) +#+end_src + +** Frame configuration +I like to keep a few frames open all the time. A main frame, where I +open my org files, code, etc. A frame for communication and reading, +such as email and feeds, and a frame for terminals. + +Currently, the frames are all the same, but I will add configuration +to distinguish them so I can automate their placement in my desktop +environment. + +#+begin_src emacs-lisp :tangle "early-init.el" +;; Do not resize when font size changes +(setq frame-resize-pixelwise t) + +;; By default, start maximized, undecorated +(add-to-list 'default-frame-alist '(fullscreen . maximized)) +(add-to-list 'default-frame-alist '(undecorated . t)) + +;; Extend this list from to add more startup frames. +(defvar rul-startup-frames + '(("main" . nil) + ("terminals" . multi-vterm)) + "Startup frame specifications. +Each entry has the form (NAME . SETUP). NAME is the frame name. +SETUP is an optional function or interactive command called in that frame.") + +(defun rul-run-startup-frame-setup (setup) + "Run startup frame SETUP." + (cond + ((null setup) nil) + ((commandp setup) (call-interactively setup)) + ((functionp setup) (funcall setup)) + (t (message "Ignoring invalid startup frame setup: %S" setup)))) + +(defun rul-apply-startup-frame-name (frame name) + "Set FRAME name and title to NAME." + (with-selected-frame frame + (set-frame-name name) + (modify-frame-parameters frame `((title . ,name))))) + +(defun rul-create-startup-frames () + "Create the configured startup frames." + (when (display-graphic-p) + (let ((initial-frame (selected-frame)) + (specs rul-startup-frames)) + (when specs + (pcase-let ((`(,name . ,setup) (car specs))) + (rul-apply-startup-frame-name initial-frame name) + (with-selected-frame initial-frame + (rul-run-startup-frame-setup setup))) + (dolist (spec (cdr specs)) + (pcase-let ((`(,name . ,setup) spec)) + (let ((frame (make-frame `((name . ,name) + (title . ,name))))) + (rul-apply-startup-frame-name frame name) + (with-selected-frame frame + (rul-run-startup-frame-setup setup))))) + (select-frame initial-frame))))) + +(add-hook 'emacs-startup-hook #'rul-create-startup-frames) +#+end_src + +** Miscellany +#+begin_src emacs-lisp :tangle "early-init.el" +;; Initialise installed packages, otherwise, basic functions are not +;; available during the initialization stage. +(setq package-enable-at-startup t) + +;; Do not report warnings. It's too noisy. +(setq native-comp-async-report-warnings-errors 'silent) + +;; Keep things minimal +(setq inhibit-startup-screen t) +(setq inhibit-startup-echo-area-message user-login-name) +#+end_src + +* Main configuration file (=init.el=) +** Package matters + +I use package from both stable and bleeding-edge Melpa. + +#+begin_src emacs-lisp :tangle "init.el" +;; package.el +(require 'package) +(add-to-list 'package-archives + '("melpa-stable" . "https://stable.melpa.org/packages/") t) + +(add-to-list 'package-archives + '("melpa" . "https://melpa.org/packages/") t) +#+end_src +** Backups +Emacs tends to clutter the filesystem with backup files. A backup file is normally the filename with a =~= suffix. I rather have my filesystem clean, and centralize all backups in a single directory. + +#+begin_src emacs-lisp :tangle "init.el" +(let ((backup-dir "~/.backup")) + (unless (file-directory-p backup-dir) + (make-directory backup-dir t)) + (setq backup-directory-alist `(("." . ,backup-dir)))) + +(setq + backup-by-copying t ; Don't delink hardlinks + delete-old-versions t ; Clean up the backups + kept-new-versions 3 ; keep some new versions + kept-old-versions 2 ; and some old ones, too + version-control t) ; Use version numbers on backups +#+end_src +** Customizations +Customizations don't place nicely with version control, so I do them in a random file that won't get persisted. + +Configurations that need persisting will be added to =custom-set-variables= and =custom-set-faces=. + +#+begin_src emacs-lisp :tangle "init.el" +;; Do not persist customizations +(setq custom-file (make-temp-file "emacs-custom-")) +#+end_src + +** Editor interface +General configurations related to text editing across all modes. + +#+begin_src emacs-lisp :tangle "init.el" +(setq fill-column 79) ; Wrap lines +(setq mouse-yank-at-point t) ; Do not follow mouse curors when mouse-yanking + +(setq-default indent-tabs-mode nil) ; No tabs when indenting +(setq-default tab-width 4) ; How many spaces a tab represents + +(setq initial-scratch-message "") + +(defalias 'yes-or-no-p 'y-or-n-p) + +;; Only flash the mode line +(setq ring-bell-function + (lambda () + (let ((orig-fg (face-foreground 'mode-line))) + (set-face-foreground 'mode-line "#F2804F") + (run-with-idle-timer 0.1 nil + (lambda (fg) (set-face-foreground 'mode-line fg)) + orig-fg)))) + +;; Highlight parens +(setq show-paren-delay 0) +(show-paren-mode 1) + +(savehist-mode 1) ; Save histories, including minibuffer + +(save-place-mode 1) ; Remember and restore cursor information + +(setq auto-save-no-message t) ; Do not print a message when auto-saving + +(pixel-scroll-precision-mode 1) ; Precision scrolling + +(defun rul-auto-copy-region () + (when (use-region-p) + (kill-ring-save (region-beginning) (region-end)))) + +(add-hook 'activate-mark-hook #'rul-auto-copy-region) + +;; Source: https://protesilaos.com/codelog/2024-12-11-emacs-diff-save-some-buffers/ +(add-to-list 'save-some-buffers-action-alist + (list "d" + (lambda (buffer) (diff-buffer-with-file (buffer-file-name buffer))) + "show diff between the buffer and its file")) +#+end_src +** Emacs server +I used to run Emacs as a systemd daemon, but it was not too deterministic as sometimes it would break. + + https://rbenencia.name/blog/emacs-daemon-as-a-systemd-service/ + +Now, I simply start it from Emacs itself. This approach works well for me. + +#+begin_src emacs-lisp :tangle "init.el" +;; Server +(require 'server) +(setq server-client-instructions nil) ; Keep it quiet when opening an ec + +(unless (server-running-p) + (server-start)) +#+end_src +** Modules machinery +#+begin_src emacs-lisp :tangle "init.el" +(dolist (path '("~/.emacs.d/rul-lisp/packages")) + (add-to-list 'load-path path)) + +(when-let* ((file (locate-user-emacs-file "rul-pre-init.el")) + ((file-exists-p file))) + (load-file file)) + +(require 'rul-themes) +(require 'rul-bindings) +(require 'rul-completion) +(require 'rul-dashboard) +(require 'rul-fm) +(require 'rul-fonts) +(require 'rul-io) +(require 'rul-mail) +(require 'rul-modeline) +(require 'rul-org) +(require 'rul-prog) +(require 'rul-terminals) +(require 'rul-vc) +(require 'rul-wm) +(require 'rul-write) + +(when-let* ((file (locate-user-emacs-file "rul-post-init.el")) + ((file-exists-p file))) + (load-file file)) + +;; init.el ends here +#+end_src + +* Modules +I group my configuration in logical modules. In general, a module +contains configuration for more than one package. + +** The =themes= module +The =themes= module contains code pertaining to Emacs themes. + +#+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-themes.el" +(use-package ef-themes :ensure t) +(use-package modus-themes + :ensure t + :config + (setq + modus-themes-mode-line '(accented borderless padded) + modus-themes-region '(bg-only) + modus-themes-bold-constructs t + modus-themes-italic-constructs t + modus-themes-paren-match '(bold intense) + modus-themes-headings (quote ((1 . (rainbow variable-pitch 1.3)) + (2 . (rainbow 1.1)) + (t . (rainbow)))) + modus-themes-org-blocks 'tinted)) +#+end_src + + +Additionally, this module subscribes to =org.freedesktop.appearance color-theme= +to detect what color theme is preferred, and set our Emacs theme accordingly. + +#+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-themes.el" +(use-package dbus) +(defun mf/set-theme-from-dbus-value (value) + "Set the appropiate theme according to the color-scheme setting value." + (message "value is %s" value) + (if (equal value '1) + (progn (message "Switch to dark theme") + (modus-themes-select 'modus-vivendi)) + (progn (message "Switch to light theme") + (modus-themes-select 'modus-operandi)))) + +(defun mf/color-scheme-changed (path var value) + "DBus handler to detect when the color-scheme has changed." + (when (and (string-equal path "org.freedesktop.appearance") + (string-equal var "color-scheme")) + (mf/set-theme-from-dbus-value (car value)) + )) + +;; Register for future changes +(dbus-register-signal + :session "org.freedesktop.portal.Desktop" + "/org/freedesktop/portal/desktop" "org.freedesktop.portal.Settings" + "SettingChanged" + #'mf/color-scheme-changed) + +;; Request the current color-scheme +(dbus-call-method-asynchronously + :session "org.freedesktop.portal.Desktop" + "/org/freedesktop/portal/desktop" "org.freedesktop.portal.Settings" + "Read" + (lambda (value) (mf/set-theme-from-dbus-value (caar value))) + "org.freedesktop.appearance" + "color-scheme" + ) + + +(provide 'rul-themes) +#+end_src + +** The =bindings= module +This module contains code pertaining to keybindings. It starts by +defining a set global keys. + +#+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-bindings.el" +;; Global keybindings +(global-set-key (kbd "C-c R") 'revert-buffer) +(global-set-key (kbd "C-c w") 'whitespace-cleanup) + +(defun help/insert-em-dash () + "Inserts an EM-DASH (not a HYPEN, not an N-DASH)" + (interactive) + (insert "—")) + +(global-set-key (kbd "C--") #'help/insert-em-dash) +#+end_src + +Next, we define a few /hydras/. /Hydras/ are a way of grouping keybindings +together, offering a menu on the way. + +#+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-bindings.el" +(use-package hydra + :ensure t + :defer 1) + +;; tab-bar +(defhydra hydra-tab-bar (:color amaranth) + "Tab Bar Operations" + ("t" tab-new "Create a new tab" :column "Creation" :exit t) + ("d" dired-other-tab "Open Dired in another tab") + ("f" find-file-other-tab "Find file in another tab") + ("x" tab-close "Close current tab") + ("m" tab-move "Move current tab" :column "Management") + ("r" tab-rename "Rename Tab") + ("<return>" tab-bar-select-tab-by-name "Select tab by name" :column "Navigation") + ("l" tab-next "Next Tab") + ("j" tab-previous "Previous Tab") + ("q" nil "Exit" :exit t)) + +(global-set-key (kbd "C-x t") 'hydra-tab-bar/body) + +;; Zoom +(defhydra hydra-zoom () + "zoom" + ("g" text-scale-increase "in") + ("l" text-scale-decrease "out")) + +(global-set-key (kbd "C-c z") 'hydra-zoom/body) + +;; Go +(defhydra hydra-go () + "zoom" + ("=" gofmt :exit t) + ("c" go-coverage :exit t)) + +(global-set-key (kbd "C-c m") 'hydra-go/body) +#+end_src + +Finally, we make use of =which-key=, which will show a menu with all +keybinding options after a prefix is pressed. I think this package has +the potential to obsolete =hydra=, so I'll have to revisit that code. + +#+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-bindings.el" +(use-package which-key + :ensure t + :config + (which-key-mode)) + +(provide 'rul-bindings) +#+end_src + +** The =completions= module +This module contains code pertaining to completion and the minibuffer. + +#+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-completion.el" +(use-package orderless :ensure t) + +(setq completion-styles '(basic substring initials orderless)) +(setq completion-category-overrides + '( + (file (styles . (basic partial-completion orderless))) + (project-file (styles . (flex basic substring partial-completion orderless))) + )) + +(setq completion-ignore-case t) +#+end_src + +The =vertico= package provides a vertical completion UI based on the default completion +system. + +#+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-completion.el" +;; Enable vertico +(use-package vertico + :ensure t + :init + (vertico-mode) + + :config + (add-hook 'rfn-eshadow-update-overlay-hook #'vertico-directory-tidy)) +#+end_src + +The =marginalia= package annotates the completion candidates with useful contextual +information. + +#+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-completion.el" +;; Enable rich annotations using the Marginalia package +(use-package marginalia + :ensure t + :bind (:map minibuffer-local-map + ("M-A" . marginalia-cycle)) + :init + (marginalia-mode)) +#+end_src + +The =consult= package replaces most of Emacs core functions with +completion-friendly alternatives that integrates well with =vertico= and +=marginalia=. + +#+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-completion.el" +(use-package consult + :ensure t + :bind (;; C-c bindings in `mode-specific-map' + ("C-c M-x" . consult-mode-command) + ("C-c h" . consult-history) + ("C-c k" . consult-kmacro) + ("C-c m" . consult-man) + ("C-c i" . consult-info) + ([remap Info-search] . consult-info) + + ;; C-x bindings in `ctl-x-map' + ("C-x M-:" . consult-complex-command) ;; orig. repeat-complex-command + ("C-x b" . consult-buffer) ;; orig. switch-to-buffer + ("C-x 4 b" . consult-buffer-other-window) ;; orig. switch-to-buffer-other-window + ("C-x 5 b" . consult-buffer-other-frame) ;; orig. switch-to-buffer-other-frame + ("C-x r b" . consult-bookmark) ;; orig. bookmark-jump + ("C-x p b" . consult-project-buffer) ;; orig. project-switch-to-buffer + + ;; Custom M-# bindings for fast register access + ("M-#" . consult-register-load) + ("M-'" . consult-register-store) ;; orig. abbrev-prefix-mark (unrelated) + ("C-M-#" . consult-register) + + ;; Other custom bindings + ("M-y" . consult-yank-pop) ;; orig. yank-pop + + ;; M-g bindings in `goto-map' + ("M-g e" . consult-compile-error) + ("M-g f" . consult-flymake) ;; Alternative: consult-flycheck + ("M-g g" . consult-goto-line) ;; orig. goto-line + ("M-g M-g" . consult-goto-line) ;; orig. goto-line + ("M-g o" . consult-outline) ;; Alternative: consult-org-heading + ("M-g m" . consult-mark) + ("M-g k" . consult-global-mark) + ("M-g i" . consult-imenu) + ("M-g I" . consult-imenu-multi) + + ;; M-s bindings in `search-map' + ("M-s d" . consult-find) + ("M-s D" . consult-locate) + ("M-s g" . consult-grep) + ("M-s G" . consult-git-grep) + ("M-s r" . consult-ripgrep) + ("M-s l" . consult-line) + ("M-s L" . consult-line-multi) + ("M-s k" . consult-keep-lines) + ("M-s u" . consult-focus-lines) + + ;; Isearch integration + ("M-s e" . consult-isearch-history) + + :map isearch-mode-map + ("M-e" . consult-isearch-history) ;; orig. isearch-edit-string + ("M-s e" . consult-isearch-history) ;; orig. isearch-edit-string + ("M-s l" . consult-line) ;; needed by consult-line to detect isearch + ("M-s L" . consult-line-multi) ;; needed by consult-line to detect isearch + + ;; Minibuffer history + :map minibuffer-local-map + ("M-s" . consult-history) ;; orig. next-matching-history-element + ("M-r" . consult-history)) ;; orig. previous-matching-history-element + + :init + (setq xref-show-xrefs-function #'consult-xref) + (setq xref-show-definitions-function #'consult-xref) + (add-hook 'completion-list-mode-hook #'consult-preview-at-point-mode) + + :config + (setq consult-preview-key 'any) + (setq consult-narrow-key "<") +) +#+end_src + +The next piece of code corresponds to =embark=, a package that enables +context-specific actions in the minibuffer, or common buffers. + +#+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-completion.el" +(use-package embark + :ensure t + + :bind + (("C-." . embark-act) ;; pick some comfortable binding + ("M-." . embark-dwim) ;; good alternative: M-. + ("C-h B" . embark-bindings)) ;; alternative for `describe-bindings' + + :init + (setq prefix-help-command #'embark-prefix-help-command) + + :config + ;; Hide the mode line of the Embark live/completions buffers + (add-to-list 'display-buffer-alist + '("\\`\\*Embark Collect \\(Live\\|Completions\\)\\*" + nil + (window-parameters (mode-line-format . none))))) + +(use-package embark-consult + :ensure t + :hook + (embark-collect-mode . consult-preview-at-point-mode)) + +(provide 'rul-completion) +#+end_src + +** The =dashboard= module +#+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-dashboard.el" +(use-package page-break-lines :ensure t) + +(use-package dashboard + :ensure t + + :config + (dashboard-setup-startup-hook) + + :custom + (dashboard-center-content t) + (dashboard-startup-banner 3) + (dashboard-items '((recents . 5) + (bookmarks . 5) + (projects . 5) + (agenda . 5) + )) + (dashboard-icon-type 'nerd-icons) + (dashboard-set-heading-icons t) + (dashboard-set-file-icons t) +) + +(provide 'rul-dashboard) +#+end_src +** The =fm= module +The =fm= module contains code pertaining to file management. In +particular, it's the module that configures =dired= and adds a few extra +packages. + +#+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-fm.el" +;;; rul-fm.el --- File management + +;; dired +(add-hook 'dired-mode-hook #'dired-hide-details-mode) +(setq dired-guess-shell-alist-user + '(("\\.\\(png\\|jpe?g\\|tiff\\)" "feh" "xdg-open") + ("\\.\\(mp[34]\\|m4a\\|ogg\\|flac\\|webm\\|mkv\\)" "mpv" "xdg-open") + (".*" "xdg-open"))) + +(setq dired-kill-when-opening-new-dired-buffer t) +(put 'dired-find-alternate-file 'disabled nil) + +;;; Icons +(use-package nerd-icons :ensure t ) +(use-package nerd-icons-dired :ensure t + :config + (add-hook 'dired-mode-hook #'nerd-icons-dired-mode)) + +(provide 'rul-fm) +#+end_src + +** The =fonts= module +The =fonts= module contains code pertaining to fonts. In particular, it +installs =fontaine=, a software that allows defining font presets. + +#+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-fonts.el" +;;; rul-fonts.el --- Fonts configuration + +(use-package fontaine + :ensure t + :config + (setq fontaine-presets + '((tiny + :default-height 100) + (small + :default-height 120) + (medium + :default-height 150) + (wayland-medium + :default-height 320) + (large + :default-weight semilight + :default-height 180 + :bold-weight extrabold) + (presentation + :default-weight semilight + :default-height 200 + :bold-weight extrabold) + (jumbo + :default-weight semilight + :default-height 230 + :bold-weight extrabold) + (writing + :default-height 140 + :default-family "Lato" + :variable-pitch-family "Regular" + ) + (t + :default-family "Iosevka" + :default-weight regular + :default-height 150 + :variable-pitch-family "Iosevka Aile"))) + (fontaine-set-preset 'medium)) + +(provide 'rul-fonts) +#+end_src + +** The =io= module +The =io= module contains configurations for packages related to Internet +services and media. I don't have excessive costumizations in these +packages, so they're somewhat unrelated fragments of code grouped in +the same file. + +We install =elfeed= to browse RSS and Atom feeds. +#+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-io.el" +;;; rul-io.el --- Configuration for Internet and media packages + +(use-package elfeed :ensure t) +(provide 'rul-feeds) +#+end_src + +The =empv= package allow us to use the =mpv= player from within +Emacs. Here we're simply installing it and configuring it with some +Internet radio channels. It requires =mpv= to be installed. +#+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-io.el" +(use-package empv +:ensure t +:config + (bind-key "C-x m" empv-map) + (setq empv-radio-channels + '( + ("SomaFM - Groove Salad" . "http://www.somafm.com/groovesalad.pls") + ("SomaFM - DEFCON" . "https://somafm.com/defcon256.pls") + ("SomaFM - Metal" . "https://somafm.com/metal.pls") + ("SomaFM - Lush" . "https://somafm.com/lush130.pls") + ("KCSM Jazz 91" . "http://ice5.securenetsystems.net/KCSM") + ("KSUA 91.5 FM" . "https://stream.radio.co/se776fab22/listen") + )) + (setq empv-fd-binary "fdfind") + ) + +(provide 'rul-io) +#+end_src +** The =mail= module +Emacs can act as Mail User Agent. My preferred package for this is +=notmuch=. + +#+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-mail.el" +;;; rul-mail.el --- Email configuration + +;; mml-sec.el +;; Use sender to find GPG key. +(setq mml-secure-openpgp-sign-with-sender t) + +;; Keep HTML mail readable without sender-defined styling. +(setq shr-use-colors nil + shr-use-fonts nil) + +(with-eval-after-load 'shr + (set-face-attribute 'shr-link nil :inherit 'default :underline t)) + +(use-package notmuch + :ensure t + :config + ;; UI + (setq notmuch-show-logo nil + notmuch-column-control 1.0 + notmuch-hello-auto-refresh t + notmuch-hello-recent-searches-max 20 + notmuch-hello-thousands-separator "" + notmuch-show-all-tags-list t + notmuch-show-text/html-blocked-images nil + ) + + (setq notmuch-draft-folder "current/Drafts") + + (defvar-local rul/notmuch-show-refreshing-for-olivetti nil) + + (defun rul/notmuch-show-enable-olivetti () + "Enable Olivetti before notmuch renders message HTML." + (setq-local olivetti-body-width 100 + shr-width nil + shr-max-width olivetti-body-width) + (olivetti-mode 1)) + + (defun rul/notmuch-show-refresh-after-olivetti () + "Refresh an already-rendered notmuch buffer after Olivetti changes width." + (when (and (derived-mode-p 'notmuch-show-mode) + olivetti-mode + notmuch-show-thread-id + (not rul/notmuch-show-refreshing-for-olivetti)) + (setq rul/notmuch-show-refreshing-for-olivetti t) + (run-at-time + 0 nil + (lambda (buffer) + (when (buffer-live-p buffer) + (with-current-buffer buffer + (unwind-protect + (when (and (derived-mode-p 'notmuch-show-mode) + olivetti-mode + notmuch-show-thread-id) + (notmuch-refresh-this-buffer)) + (setq rul/notmuch-show-refreshing-for-olivetti nil))))) + (current-buffer)))) + + (add-hook 'notmuch-show-mode-hook #'rul/notmuch-show-enable-olivetti) + + (with-eval-after-load 'olivetti + (add-hook 'olivetti-mode-on-hook + #'rul/notmuch-show-refresh-after-olivetti + t)) + + (defun rul/notmuch-inline-override-images (types) + "Treat standalone image MIME parts as attachments in notmuch." + (if (member "image/.*" types) + types + (append types '("image/.*")))) + + (unless (advice-member-p #'rul/notmuch-inline-override-images + 'notmuch--inline-override-types) + (advice-add 'notmuch--inline-override-types :filter-return + #'rul/notmuch-inline-override-images)) + + ;; Keymaps + (defun rul/capture-mail() + "Capture mail to org mode." + (interactive) + (org-store-link nil) + (org-capture nil "m") + ) + + (bind-key "c" 'rul/capture-mail notmuch-show-mode-map) + + ;; Keep uppercase refresh cheap after tag operations; use M-g for a full poll. + (define-key notmuch-show-mode-map "G" 'notmuch-refresh-this-buffer) + (define-key notmuch-search-mode-map "G" 'notmuch-refresh-this-buffer) + (define-key notmuch-show-mode-map (kbd "M-g") 'notmuch-poll-and-refresh-this-buffer) + (define-key notmuch-search-mode-map (kbd "M-g") 'notmuch-poll-and-refresh-this-buffer) + + (define-key notmuch-show-mode-map "R" 'notmuch-show-reply) + (define-key notmuch-search-mode-map "R" 'notmuch-search-reply-to-thread) + + ;; Spam + (define-key notmuch-show-mode-map "S" + (lambda () + "mark message as spam" + (interactive) + (notmuch-show-tag (list "+spam" "-inbox" "-unread")))) + + (define-key notmuch-search-mode-map "S" + (lambda (&optional beg end) + "mark thread as spam" + (interactive (notmuch-search-interactive-region)) + (notmuch-search-tag (list "+spam" "-inbox" "-unread") beg end))) + + ;; Archive + ;; Stock `a` applies notmuch-archive-tags but doesn't re-run the search, so + ;; the thread stays visible in the tag:inbox buffer with stale results. Wrap + ;; both `a` and `A` to refresh the buffer so the thread drops out of view. + (setq notmuch-archive-tags (list "-inbox" "+archive")) + + (defun rul/notmuch-search-archive (&optional unarchive beg end) + "Archive threads and refresh so they drop out of the inbox view." + (interactive (cons current-prefix-arg (notmuch-search-interactive-region))) + (notmuch-search-archive-thread unarchive beg end) + (notmuch-refresh-this-buffer)) + + (defun rul/notmuch-show-archive () + "Archive the current message with +archive -inbox -unread and refresh." + (interactive) + (notmuch-show-tag (list "+archive" "-inbox" "-unread")) + (notmuch-refresh-this-buffer)) + + (define-key notmuch-search-mode-map "a" 'rul/notmuch-search-archive) + (define-key notmuch-search-mode-map "A" 'rul/notmuch-search-archive) + (define-key notmuch-show-mode-map "a" 'rul/notmuch-show-archive) + (define-key notmuch-show-mode-map "A" 'rul/notmuch-show-archive) + + ;; Mark as read + (define-key notmuch-search-mode-map "r" + (lambda (&optional beg end) + "mark thread as read" + (interactive (notmuch-search-interactive-region)) + (notmuch-search-tag (list "-unread") beg end) + (notmuch-search-next-thread))) + + (define-key notmuch-search-mode-map (kbd "RET") + (lambda () + "Show the selected thread with notmuch-tree if it has more +than one email. Use notmuch-show otherwise." + (interactive) + (if (= (plist-get (notmuch-search-get-result) :total) 1) + (notmuch-search-show-thread) + (notmuch-tree (notmuch-search-find-thread-id) + notmuch-search-query-string + nil + (notmuch-prettify-subject (notmuch-search-find-subject)))))) + + (defun color-inbox-if-unread () (interactive) + (save-excursion + (goto-char (point-min)) + (let ((cnt (car (process-lines "notmuch" "count" "tag:inbox and tag:unread")))) + (when (> (string-to-number cnt) 0) + (save-excursion + (when (search-forward "inbox" (point-max) t) + (let* ((overlays (overlays-in (match-beginning 0) (match-end 0))) + (overlay (car overlays))) + (when overlay + (overlay-put overlay 'face '((:inherit bold) (:foreground "green"))))))))))) + + (defvar notmuch-hello-refresh-count 0) + (defun notmuch-hello-refresh-status-message () + (let* ((new-count + (string-to-number + (car (process-lines notmuch-command "count")))) + (diff-count (- new-count notmuch-hello-refresh-count))) + (cond + ((= notmuch-hello-refresh-count 0) + (message "You have %s messages." + (notmuch-hello-nice-number new-count))) + ((> diff-count 0) + (message "You have %s more messages since last refresh." + (notmuch-hello-nice-number diff-count))) + ((< diff-count 0) + (message "You have %s fewer messages since last refresh." + (notmuch-hello-nice-number (- diff-count))))) + (setq notmuch-hello-refresh-count new-count))) + + (add-hook 'notmuch-hello-refresh-hook 'color-inbox-if-unread) + (add-hook 'notmuch-hello-refresh-hook 'notmuch-hello-refresh-status-message) + + (setq notmuch-hello-sections '(notmuch-hello-insert-saved-searches + notmuch-hello-insert-search + notmuch-hello-insert-recent-searches + notmuch-hello-insert-alltags + )) + + ;; https://git.sr.ht/~tslil/dotfiles/tree/4e51afbb/emacs/notmuch-config.el#L76-82 + (defmacro make-binds (mode-map binds argfunc &rest body) + "Create keybindings in `mode-map' using a list of (keystr . arg) +pairs in `binds' of the form ( ... (argfunc arg) body)." + `(progn ,@(mapcar (lambda (pair) + `(define-key ,mode-map (kbd ,(car pair)) + (lambda () (interactive) (,argfunc ,(cdr pair)) ,@body))) + (eval binds)))) + + (defvar notmuch-hello-tree-searches '(("u" . "tag:unread") + ("i" . "tag:inbox") + ("*" . "*")) + "List of (key . query) pairs to bind in notmuch-hello.") + + (make-binds notmuch-hello-mode-map + notmuch-hello-tree-searches + notmuch-search) +) ;; ends use-package notmuch + +(use-package notmuch-indicator :ensure t) + +(provide 'rul-mail) +#+end_src +** The =modeline= module +The =modeline= module contains code pertaining to Emacs modeline. + +#+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-modeline.el" +;;; rul-modeline.el --- Modeline configuration + +;; Most of the code in this file is based on: +;; https://git.sr.ht/~protesilaos/dotfiles/tree/cf26bc34/item/emacs/.emacs.d/prot-lisp/prot-modeline.el +;; +;; All Kudos to Prot. + +;;;; Faces +(defface rul-modeline-indicator-red + '((default :inherit bold) + (((class color) (min-colors 88) (background light)) + :foreground "#880000") + (((class color) (min-colors 88) (background dark)) + :foreground "#ff9f9f") + (t :foreground "red")) + "Face for modeline indicators.") + +;;;; Common helper functions +(defcustom rul-modeline-string-truncate-length 9 + "String length after which truncation should be done in small windows." + :type 'natnum) + +(defun rul-modeline--string-truncate-p (str) + "Return non-nil if STR should be truncated." + (and (< (window-total-width) split-width-threshold) + (> (length str) rul-modeline-string-truncate-length) + (not (one-window-p :no-minibuffer)))) + +(defun rul-modeline-string-truncate (str) + "Return truncated STR, if appropriate, else return STR. +Truncation is done up to `rul-modeline-string-truncate-length'." + (if (rul-modeline--string-truncate-p str) + (concat (substring str 0 rul-modeline-string-truncate-length) "...") + str)) + +;;;; Major mode +(defun rul-modeline-major-mode-indicator () + "Return appropriate propertized mode line indicator for the major mode." + (let ((indicator (cond + ((derived-mode-p 'text-mode) "§") + ((derived-mode-p 'prog-mode) "λ") + ((derived-mode-p 'comint-mode) ">_") + (t "◦")))) + (propertize indicator 'face 'shadow))) + +(defun rul-modeline-major-mode-name () + "Return capitalized `major-mode' without the -mode suffix." + (capitalize (string-replace "-mode" "" (symbol-name major-mode)))) + +(defun rul-modeline-major-mode-help-echo () + "Return `help-echo' value for `rul-modeline-major-mode'." + (if-let ((parent (get major-mode 'derived-mode-parent))) + (format "Symbol: `%s'. Derived from: `%s'" major-mode parent) + (format "Symbol: `%s'." major-mode))) + +(defvar-local rul-modeline-major-mode + (list + (propertize "%[" 'face 'rul-modeline-indicator-red) + '(:eval + (concat + (rul-modeline-major-mode-indicator) + " " + (propertize + (rul-modeline-string-truncate + (rul-modeline-major-mode-name)) + 'mouse-face 'mode-line-highlight + 'help-echo (rul-modeline-major-mode-help-echo)))) + (propertize "%]" 'face 'rul-modeline-indicator-red)) + "Mode line construct for displaying major modes.") + +(with-eval-after-load 'eglot + (setq mode-line-misc-info + (delete '(eglot--managed-mode (" [" eglot--mode-line-format "] ")) mode-line-misc-info))) + +(defvar-local prot-modeline-eglot + `(:eval + (when (and (featurep 'eglot) (mode-line-window-selected-p)) + '(eglot--managed-mode eglot--mode-line-format))) + "Mode line construct displaying Eglot information. +Specific to the current window's mode line.") + +;;;; Miscellaneous +(defvar-local rul-modeline-misc-info + '(:eval + (when (mode-line-window-selected-p) + mode-line-misc-info)) + "Mode line construct displaying `mode-line-misc-info'. +Specific to the current window's mode line.") + +;;;; Display current time +(setq display-time-format " %a %e %b, %H:%M ") +(setq display-time-default-load-average nil) +(setq display-time-mail-string "") + +;;;; Variables used in the modeline need to be in `risky-local-variable'. +(dolist (construct '( + rul-modeline-major-mode + rul-modeline-misc-info + )) + (put construct 'risky-local-variable t)) + +;;;; Finally, define the modeline format +(setq-default mode-line-format + '("%e" + mode-line-front-space + mode-line-buffer-identification + mode-line-front-space + mode-line-percent-position + mode-line-front-space + rul-modeline-major-mode + prot-modeline-eglot + mode-line-format-right-align + rul-modeline-misc-info + mode-line-front-space + mode-line-front-space + )) + +(provide 'rul-modeline) +#+end_src +** The =org= module + +My org mode configuration is quite big, so I split it across multiple files. + +#+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-org.el" +;;; rul-org.el --- Org configuration +(require 'org) +(require 'org-capture) +(require 'org-protocol) +(require 'org-habit) + +(require 'rul-org-agenda) + +(setq org-attach-use-inheritance t) +(setq org-cycle-separator-lines 0) +(setq org-hide-leading-stars nil) +(setq org-startup-indented t) +(setq org-edit-src-content-indentation 0) + +(use-package org-modern :ensure t) +(use-package org-pomodoro + :ensure t + :config + (defun rul/disable-notifications () + "Disable GNOME notifications." + (shell-command "gsettings set org.gnome.desktop.notifications show-banners false")) + + (defun rul/enable-notifications () + "Enable GNOME notifications." + (shell-command "gsettings set org.gnome.desktop.notifications show-banners true")) + + ;; Add hooks for Pomodoro start and finish + (add-hook 'org-pomodoro-started-hook #'rul/disable-notifications) + (add-hook 'org-pomodoro-finished-hook #'rul/enable-notifications) + (add-hook 'org-pomodoro-killed-hook #'rul/enable-notifications)) + +;; (add-hook 'org-mode-hook 'turn-off-auto-fill) +;; (add-hook 'auto-save-hook 'org-save-all-org-buffers) +(add-hook 'org-mode-hook 'visual-line-mode) + +(use-package org-download + :ensure t + :config + (add-hook 'dired-mode-hook 'org-download-enable)) + +(setq org-startup-indented t + org-pretty-entities nil + org-hide-emphasis-markers t + ;; show actually italicized text instead of /italicized text/ + org-fontify-whole-heading-line t + org-fontify-done-headline t + org-fontify-quote-and-verse-blocks t) + +;; ORG BINDINGS ;; +(global-set-key (kbd "C-c l") #'org-store-link) +(global-set-key (kbd "C-c c") #'org-capture) +(global-set-key (kbd "C-c s") #'org-schedule) + +(global-set-key (kbd "<f6>") 'org-clock-goto) +(global-set-key (kbd "<f9>") 'org-clock-in-last) +(global-set-key (kbd "<f10>") 'org-clock-out) +(global-set-key (kbd "<f12>") 'org-agenda) + +;; ORG STATES ;; +(setq org-todo-keywords + (quote ((sequence "TODO(t)" "MAYBE(m)" "NEXT(n)" "|" "DONE(d)") + (sequence "WAITING(w@/!)" "HOLD(h@/!)" "|" "CANCELLED(c@/!)" "MEETING")))) + +(setq org-use-fast-todo-selection t) + +(setq org-todo-state-tags-triggers + (quote (("CANCELLED" ("CANCELLED" . t)) + ("WAITING" ("WAITING" . t)) + ("HOLD" ("WAITING") ("HOLD" . t)) + (done ("WAITING") ("HOLD")) + ("TODO" ("WAITING") ("CANCELLED") ("HOLD")) + ("NEXT" ("WAITING") ("CANCELLED") ("HOLD")) + ("DONE" ("WAITING") ("CANCELLED") ("HOLD"))))) + +(setq org-enforce-todo-dependencies t) +(setq org-log-done (quote time)) +(setq org-log-redeadline (quote time)) +(setq org-log-reschedule (quote time)) +(setq org-log-into-drawer t) + +;; CAPTURE ;; +(setq org-capture-templates + (quote + ( + + ("w" "Todo" entry + (file+headline org-refile-path "Tasks") + "* TODO %?" + :empty-lines 1) + + ("m" + "Capture incoming email" + entry + (file+headline org-refile-path "Incoming") + "* TODO Re: %:description\n\n Source: %u, %a\n" + :empty-lines 1) + + ("e" "Elfeed entry" entry + (file+headline org-refile-path "Read later") + "* %? [[%:link][%:description]]\n %U\n %:description\n") + + ("L" "Web Link" entry + (file+headline org-refile-path "Read later") + "* %?[[%:link][%:description]]\n %:initial\n \nCaptured On: %U" + ) + + ("l" "Web Link with Selection" entry + (file+headline org-refile-path "Read later") + "* [[%:link][%:description]]\n %:initial\n \nCaptured On: %U") + ))) + +;; REFILE ;; + +; Targets include this file and any file contributing to the agenda - up to 3 levels deep +(setq org-refile-targets + '((nil :maxlevel . 3) + (org-agenda-files :maxlevel . 3))) + +; Targets complete directly with IDO +(setq org-outline-path-complete-in-steps nil) + +; Allow refile to create parent tasks with confirmation +(setq org-refile-allow-creating-parent-nodes (quote confirm)) + + + +;; ORG REPORTS ;; +; Set default column view headings: Task Effort Clock_Summary +(setq org-columns-default-format "%80ITEM(Task) %10Effort(Effort){:} %10CLOCKSUM") + +(defun my-org-clocktable-indent-string (level) + (if (= level 1) + "" + (let ((str "^")) + (while (> level 2) + (setq level (1- level) + str (concat str "--"))) + (concat str "-> ")))) + +(advice-add 'org-clocktable-indent-string :override #'my-org-clocktable-indent-string) + +(setq org-clock-clocktable-default-properties '(:maxlevel 4 :scope file :formula %)) + +; global Effort estimate values +; global STYLE property values for completion +(setq org-global-properties (quote (("Effort_ALL" . "0:15 0:30 0:45 1:00 2:00 3:00 4:00 5:00 6:00 0:00") + ("STYLE_ALL" . "habit")))) + +;; TAGS ;; +; Tags with fast selection keys +(setq org-tag-alist (quote ((:startgroup) + ("@errand" . ?e) + ("@office" . ?o) + ("@home" . ?H) + (:endgroup) + ("WAITING" . ?w) + ("HOLD" . ?h) + ("CANCELLED" . ?c) + ("FLAGGED" . ??)))) + +(setq org-stuck-projects + '("+LEVEL=2+PROJECT/-MAYBE-DONE" ("NEXT") ("@shop") + "\\<IGNORE\\>")) + +; Allow setting single tags without the menu +(setq org-fast-tag-selection-single-key (quote expert)) + +;; org-modern +(add-hook 'org-mode-hook 'org-modern-mode) +(add-hook 'org-agenda-finalize-hook #'org-modern-agenda) + +;; Honor ATTR_ORG attribute. Defaults to image's width if not set. +(setq org-image-actual-width nil) + +(setq org-clock-mode-line-total 'today) + +;; org-tempus +(unless (package-installed-p 'org-tempus) + (package-vc-install "https://github.com/rul/org-tempus.git")) + +(use-package org-tempus + :init + (org-tempus-mode 1)) + +(use-package org-remark-global-tracking + :ensure org-remark + ;; `org-remark-global-tracking' is shipped by the `org-remark' + ;; package, so ensure the package exists before loading this feature. + :hook (after-init . org-remark-global-tracking-mode) + :config + ;; Selectively keep or comment out the following if you want to use + ;; extensions for Info-mode, EWW, and NOV.el (EPUB) respectively. + (use-package org-remark-eww :after eww :config (org-remark-eww-mode +1)) + (use-package org-remark-nov :after nov :config (org-remark-nov-mode +1))) + +(use-package org-remark + :ensure t + :bind (;; :bind keyword also implicitly defers org-remark itself. + ;; Keybindings before :map is set for global-map. Adjust the keybinds + ;; as you see fit. + ("C-c n m" . org-remark-mark) + ("C-c n l" . org-remark-mark-line) + :map org-remark-mode-map + ("C-c n o" . org-remark-open) + ("C-c n ]" . org-remark-view-next) + ("C-c n [" . org-remark-view-prev) + ("C-c n r" . org-remark-remove) + ("C-c n d" . org-remark-delete))) + +(provide 'rul-org) +#+end_src + +#+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-org-agenda.el" +;;; rul-org-agenda.el --- Org agenda configuration +(require 'org) + +(global-set-key (kbd "<f12>") #'org-agenda) +(global-set-key (kbd "C-c a") #'org-agenda) + +(defun rul-org-agenda-save-after-todo (&rest _) + "Save Org buffers after changing a TODO state from agenda." + (org-save-all-org-buffers)) + +(with-eval-after-load 'org-agenda + (advice-add 'org-agenda-todo :after #'rul-org-agenda-save-after-todo)) + +(defun bh/is-project-p () + "Any task with a todo keyword subtask" + (save-restriction + (widen) + (let ((has-subtask) + (subtree-end (save-excursion (org-end-of-subtree t))) + (is-a-task (member (nth 2 (org-heading-components)) org-todo-keywords-1))) + (save-excursion + (forward-line 1) + (while (and (not has-subtask) + (< (point) subtree-end) + (re-search-forward "^\*+ " subtree-end t)) + (when (member (org-get-todo-state) org-todo-keywords-1) + (setq has-subtask t)))) + (and is-a-task has-subtask)))) + +(defun bh/is-project-subtree-p () + "Any task with a todo keyword that is in a project subtree. +Callers of this function already widen the buffer view." + (let ((task (save-excursion (org-back-to-heading 'invisible-ok) + (point)))) + (save-excursion + (bh/find-project-task) + (if (equal (point) task) + nil + t)))) + +(defun bh/is-task-p () + "Any task with a todo keyword and no subtask" + (save-restriction + (widen) + (let ((has-subtask) + (subtree-end (save-excursion (org-end-of-subtree t))) + (is-a-task (member (nth 2 (org-heading-components)) org-todo-keywords-1))) + (save-excursion + (forward-line 1) + (while (and (not has-subtask) + (< (point) subtree-end) + (re-search-forward "^\*+ " subtree-end t)) + (when (member (org-get-todo-state) org-todo-keywords-1) + (setq has-subtask t)))) + (and is-a-task (not has-subtask))))) + +(defun bh/is-subproject-p () + "Any task which is a subtask of another project" + (let ((is-subproject) + (is-a-task (member (nth 2 (org-heading-components)) org-todo-keywords-1))) + (save-excursion + (while (and (not is-subproject) (org-up-heading-safe)) + (when (member (nth 2 (org-heading-components)) org-todo-keywords-1) + (setq is-subproject t)))) + (and is-a-task is-subproject))) + +(defun bh/list-sublevels-for-projects-indented () + "Set org-tags-match-list-sublevels so when restricted to a subtree we list all subtasks. + This is normally used by skipping functions where this variable is already local to the agenda." + (if (marker-buffer org-agenda-restrict-begin) + (setq org-tags-match-list-sublevels 'indented) + (setq org-tags-match-list-sublevels nil)) + nil) + +(defun bh/list-sublevels-for-projects () + "Set org-tags-match-list-sublevels so when restricted to a subtree we list all subtasks. + This is normally used by skipping functions where this variable is already local to the agenda." + (if (marker-buffer org-agenda-restrict-begin) + (setq org-tags-match-list-sublevels t) + (setq org-tags-match-list-sublevels nil)) + nil) + +(defvar bh/hide-scheduled-and-waiting-next-tasks t) + +(defun bh/toggle-next-task-display () + (interactive) + (setq bh/hide-scheduled-and-waiting-next-tasks (not bh/hide-scheduled-and-waiting-next-tasks)) + (when (equal major-mode 'org-agenda-mode) + (org-agenda-redo)) + (message "%s WAITING and SCHEDULED NEXT Tasks" (if bh/hide-scheduled-and-waiting-next-tasks "Hide" "Show"))) + +(defun bh/skip-stuck-projects () + "Skip trees that are not stuck projects" + (save-restriction + (widen) + (let ((next-headline (save-excursion (or (outline-next-heading) (point-max))))) + (if (bh/is-project-p) + (let* ((subtree-end (save-excursion (org-end-of-subtree t))) + (has-next )) + (save-excursion + (forward-line 1) + (while (and (not has-next) (< (point) subtree-end) (re-search-forward "^\\*+ NEXT " subtree-end t)) + (unless (member "WAITING" (org-get-tags-at)) + (setq has-next t)))) + (if has-next + nil + next-headline)) ; a stuck project, has subtasks but no next task + nil)))) + +(defun bh/skip-non-stuck-projects () + "Skip trees that are not stuck projects" + ;; (bh/list-sublevels-for-projects-indented) + (save-restriction + (widen) + (let ((next-headline (save-excursion (or (outline-next-heading) (point-max))))) + (if (bh/is-project-p) + (let* ((subtree-end (save-excursion (org-end-of-subtree t))) + (has-next )) + (save-excursion + (forward-line 1) + (while (and (not has-next) (< (point) subtree-end) (re-search-forward "^\\*+ NEXT " subtree-end t)) + (unless (member "WAITING" (org-get-tags-at)) + (setq has-next t)))) + (if has-next + next-headline + nil)) ; a stuck project, has subtasks but no next task + next-headline)))) + +(defun bh/skip-non-projects () + "Skip trees that are not projects" + ;; (bh/list-sublevels-for-projects-indented) + (if (save-excursion (bh/skip-non-stuck-projects)) + (save-restriction + (widen) + (let ((subtree-end (save-excursion (org-end-of-subtree t)))) + (cond + ((bh/is-project-p) + nil) + ((and (bh/is-project-subtree-p) (not (bh/is-task-p))) + nil) + (t + subtree-end)))) + (save-excursion (org-end-of-subtree t)))) + +(defun bh/skip-non-tasks () + "Show non-project tasks. +Skip project and sub-project tasks, habits, and project related tasks." + (save-restriction + (widen) + (let ((next-headline (save-excursion (or (outline-next-heading) (point-max))))) + (cond + ((bh/is-task-p) + nil) + (t + next-headline))))) + +(defun bh/skip-project-trees-and-habits () + "Skip trees that are projects" + (save-restriction + (widen) + (let ((subtree-end (save-excursion (org-end-of-subtree t)))) + (cond + ((bh/is-project-p) + subtree-end) + ((org-is-habit-p) + subtree-end) + (t + nil))))) + +(defun bh/skip-projects-and-habits-and-single-tasks () + "Skip trees that are projects, tasks that are habits, single non-project tasks" + (save-restriction + (widen) + (let ((next-headline (save-excursion (or (outline-next-heading) (point-max))))) + (cond + ((org-is-habit-p) + next-headline) + ((and bh/hide-scheduled-and-waiting-next-tasks + (member "WAITING" (org-get-tags-at))) + next-headline) + ((bh/is-project-p) + next-headline) + ((and (bh/is-task-p) (not (bh/is-project-subtree-p))) + next-headline) + (t + nil))))) + +(defun bh/skip-project-tasks-maybe () + "Show tasks related to the current restriction. +When restricted to a project, skip project and sub project tasks, habits, NEXT tasks, and loose tasks. +When not restricted, skip project and sub-project tasks, habits, and project related tasks." + (save-restriction + (widen) + (let* ((subtree-end (save-excursion (org-end-of-subtree t))) + (next-headline (save-excursion (or (outline-next-heading) (point-max)))) + (limit-to-project (marker-buffer org-agenda-restrict-begin))) + (cond + ((bh/is-project-p) + next-headline) + ((org-is-habit-p) + subtree-end) + ((and (not limit-to-project) + (bh/is-project-subtree-p)) + subtree-end) + ((and limit-to-project + (bh/is-project-subtree-p) + (member (org-get-todo-state) (list "NEXT"))) + subtree-end) + (t + nil))))) + +(defun bh/skip-project-tasks () + "Show non-project tasks. +Skip project and sub-project tasks, habits, and project related tasks." + (save-restriction + (widen) + (let* ((subtree-end (save-excursion (org-end-of-subtree t)))) + (cond + ((bh/is-project-p) + subtree-end) + ((org-is-habit-p) + subtree-end) + ((bh/is-project-subtree-p) + subtree-end) + ((not (org-entry-is-todo-p)) + subtree-end) + (t + nil))))) + +(defun bh/skip-non-project-tasks () + "Show project tasks. +Skip project and sub-project tasks, habits, and loose non-project tasks." + (save-restriction + (widen) + (let* ((subtree-end (save-excursion (org-end-of-subtree t))) + (next-headline (save-excursion (or (outline-next-heading) (point-max))))) + (cond + ((bh/is-project-p) + next-headline) + ((org-is-habit-p) + subtree-end) + ((and (bh/is-project-subtree-p) + (member (org-get-todo-state) (list "NEXT"))) + subtree-end) + ((not (bh/is-project-subtree-p)) + subtree-end) + (t + nil))))) + +(defun bh/skip-projects-and-habits () + "Skip trees that are projects and tasks that are habits" + (save-restriction + (widen) + (let ((subtree-end (save-excursion (org-end-of-subtree t)))) + (cond + ((bh/is-project-p) + subtree-end) + ((org-is-habit-p) + subtree-end) + (t + nil))))) + +(defun bh/skip-non-subprojects () + "Skip trees that are not projects" + (let ((next-headline (save-excursion (outline-next-heading)))) + (if (bh/is-subproject-p) + nil + next-headline))) + +;; CLOCKING ;; +;; Resume clocking task when emacs is restarted +(org-clock-persistence-insinuate) +;; +;; Show lot of clocking history so it's easy to pick items off the C-F11 list +(setq org-clock-history-length 23) +;; Resume clocking task on clock-in if the clock is open +(setq org-clock-in-resume t) +;; Separate drawers for clocking and logs +(setq org-drawers (quote ("PROPERTIES" "LOGBOOK"))) +;; Save clock data and state changes and notes in the LOGBOOK drawer +(setq org-clock-into-drawer t) +;; Sometimes I change tasks I'm clocking quickly - this removes clocked tasks with 0:00 duration +(setq org-clock-out-remove-zero-time-clocks t) +;; Clock out when moving task to a done state +(setq org-clock-out-when-done t) +;; Save the running clock and all clock history when exiting Emacs, load it on startup +(setq org-clock-persist t) +;; Do not prompt to resume an active clock +(setq org-clock-persist-query-resume nil) +;; Enable auto clock resolution for finding open clocks +(setq org-clock-auto-clock-resolution (quote when-no-clock-is-running)) +;; Include current clocking task in clock reports +(setq org-clock-report-include-clocking-task t) + +(defun bh/find-project-task () + "Move point to the parent (project) task if any" + (save-restriction + (widen) + (let ((parent-task (save-excursion (org-back-to-heading 'invisible-ok) (point)))) + (while (org-up-heading-safe) + (when (member (nth 2 (org-heading-components)) org-todo-keywords-1) + (setq parent-task (point)))) + (goto-char parent-task) + parent-task))) + +;; https://stackoverflow.com/a/10091330 +(defun zin/org-agenda-skip-tag (tag &optional others) + "Skip all entries that correspond to TAG. + +If OTHERS is true, skip all entries that do not correspond to TAG." + (let ((next-headline (save-excursion (or (outline-next-heading) (point-max)))) + (current-headline (or (and (org-at-heading-p) + (point)) + (save-excursion (org-back-to-heading))))) + (if others + (if (not (member tag (org-get-tags-at current-headline))) + next-headline + nil) + (if (member tag (org-get-tags-at current-headline)) + next-headline + nil)))) + +;; AGENDA VIEW ;; + +;; Do not dim blocked tasks +(setq org-agenda-compact-blocks nil) +(setq org-agenda-dim-blocked-tasks nil) +(setq org-agenda-block-separator 61) + +;; Agenda log mode items to display (closed and state changes by default) +(setq org-agenda-log-mode-items (quote (closed state))) + +; For tag searches ignore tasks with scheduled and deadline dates +(setq org-agenda-tags-todo-honor-ignore-options t) + +(setq org-icalendar-include-body nil) +(setq org-icalendar-include-bbdb-anniversaries t) +(setq org-icalendar-include-todo t) +(setq org-icalendar-use-scheduled '(todo-start event-if-not-todo event-if-todo-not-done)) + +(provide 'rul-org-agenda) +#+end_src +** The =prog= module +This package contains code related to programming or markup languages +modes. As my configurations are generally small, I prefer to have them +on a single file. + +#+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-prog.el" +;;; rul-prog.el --- Configuration related to programming and markup +;;; languages +(use-package eglot :ensure t) + +;; Go +(use-package go-mode + :ensure t + :init + (progn + (bind-key [remap find-tag] #'godef-jump)) + :config + (add-hook 'go-mode-hook 'electric-pair-mode) + (add-hook 'before-save-hook 'gofmt-before-save)) + +(use-package go-eldoc + :ensure t + :init + (add-hook 'go-mode-hook 'go-eldoc-setup)) + +;; Latex +(add-hook 'latex-mode-hook 'flyspell-mode) +(setq TeX-PDF-mode t) + +(defun pdfevince () + (add-to-list 'TeX-output-view-style + '("^pdf$" "." "evince %o %(outpage)"))) + +(add-hook 'LaTeX-mode-hook 'pdfevince t) ; AUCTeX LaTeX mode + +;; Markdown +(use-package markdown-mode + :ensure t + :config + (setq auto-mode-alist + (cons '("\\.mdwn" . markdown-mode) auto-mode-alist))) + +;; Python +(use-package blacken :ensure t :defer t) + +;; Terraform +(use-package terraform-mode :ensure t :defer t) + +;; YAML +(use-package yaml-mode :ensure t :defer t) + +;; Rust +(use-package rust-mode + :defer t + :init + (setq rust-mode-treesitter-derive t) + :config + (add-hook 'rust-mode-hook 'eglot-ensure)) + +(provide 'rul-prog) +#+end_src +** The =terminals= module +TODO + +#+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-terminals.el" +(use-package vterm + :ensure t + :init + (setq vterm-always-compile-module t + vterm-max-scrollback 100000) + :hook + (vterm-mode . goto-address-mode) + :bind + (:map vterm-mode-map + ("C-c C-t" . vterm-copy-mode) + ("C-l" . vterm-clear)) + :config + (define-key vterm-mode-map (kbd "C-c C-c") + (lambda () + (interactive) + (vterm-send-string "\C-c"))) + + (defun rul/vterm-copy-and-exit (beg end) + "Copy region and exit `vterm-copy-mode'." + (interactive "r") + (kill-ring-save beg end) + (vterm-copy-mode -1)) + + (define-key vterm-copy-mode-map (kbd "w") #'rul/vterm-copy-and-exit) + (define-key vterm-copy-mode-map (kbd "M-w") #'rul/vterm-copy-and-exit)) + +(use-package multi-vterm + :ensure t + :after vterm + :bind (("C-c t" . multi-vterm)) + :config + (setq vterm-kill-buffer-on-exit t) + + (defvar-local rul/vterm-close-tab-on-kill nil + "When non-nil, close this buffer's tab when the vterm buffer is killed.") + + (defun rul/vterm-maybe-close-tab () + "Close the current tab if this vterm buffer was opened in its own tab." + (when rul/vterm-close-tab-on-kill + (tab-close))) + + (defun rul/vterm-new-tab () + "Create a new tab and open a new vterm." + (interactive) + (tab-new) + (multi-vterm) + (setq-local rul/vterm-close-tab-on-kill t) + (add-hook 'kill-buffer-hook #'rul/vterm-maybe-close-tab nil t)) + + ;; Inside vterm buffers, make C-c t spawn a new tab + vterm + (define-key vterm-mode-map (kbd "C-c t") #'rul/vterm-new-tab) + (define-key vterm-mode-map (kbd "C-S-t") #'rul/vterm-new-tab)) + +(provide 'rul-terminals) +#+end_src + +** The =vc= module +TODO + +#+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-vc.el" +;;; rul-vc.el --- Version control configuration -*- lexical-binding: t; -*- + +(setq vc-follow-symlinks nil) + +(use-package magit + :ensure t + :bind (("C-c g s" . magit-status) + ("C-c g F" . magit-pull-from-upstream) + ("C-c g b" . magit-blame)) + :hook (git-commit-setup . rul/git-commit-setup) + :config + (defun rul/git-commit-setup () + "Enable useful text modes for Git commit buffers." + (flyspell-mode 1) + (auto-fill-mode 1)) + + (defun rul/magit-status-save-window-config (&rest _) + "Save current window configuration before invoking `magit-status'." + (window-configuration-to-register :magit-fullscreen)) + + (defun rul/magit-status-single-window (&rest _) + "Display `magit-status' in a single window." + (delete-other-windows)) + + (advice-add 'magit-status :before #'rul/magit-status-save-window-config) + (advice-add 'magit-status :after #'rul/magit-status-single-window)) + +(with-eval-after-load 'project + (add-to-list 'project-switch-commands + '(magit-project-status "Magit" "m"))) + +(provide 'rul-vc) +;;; rul-vc.el ends here +#+end_src + +** The =wm= module +TODO + +#+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-wm.el" +;;;; window.el +;; Inspiration: https://christiantietze.de/posts/2022/12/updated-org-mode-agenda-display-buffer-alist/ +(defun rul/display-buffer-org-agenda-managed-p (buffer-name action) + "Determine whether BUFFER-NAME is an org-agenda managed buffer." + (with-current-buffer buffer-name + (or (derived-mode-p 'org-mode 'org-agenda-mode) + (member (buffer-file-name) (org-agenda-files))))) + +;; Side window for dictionary +(setq switch-to-buffer-obey-display-actions t) +(add-to-list 'display-buffer-alist + '("^\\*Dictionary\\*" display-buffer-in-side-window + (side . bottom) + (window-height . 12) + )) + +;;;; tab-bar.el +(let ((map global-map)) +(define-key map (kbd "C-<next>") 'tab-bar-switch-to-next-tab) +(define-key map (kbd "C-<prior>") 'tab-bar-switch-to-prev-tab) +(define-key map (kbd "<f8>") 'tab-bar-mode)) + +(setq tab-bar-format + '(tab-bar-format-tabs + ;; tab-bar-format-align-right + ;; tab-bar-format-global + )) + +(setq tab-bar-new-tab-to 'rightmost) +(setq tab-bar-close-button-show nil) +(set-face-attribute 'tab-bar nil :height 0.8) + +;; I've moved to a frame oriented workflow, so I no longer use tabs. +;; (tab-bar-mode 1) + +;; Pop-up buffers +;; https://protesilaos.com/codelog/2024-09-19-emacs-command-popup-frame-emacsclient/ +(defun prot-window-delete-popup-frame (&rest _) + "Kill selected selected frame if it has parameter `prot-window-popup-frame'. +Use this function via a hook." + (when (frame-parameter nil 'prot-window-popup-frame) + (delete-frame))) + +(defmacro prot-window-define-with-popup-frame (command) + "Define interactive function which calls COMMAND in a new frame. +Make the new frame have the `prot-window-popup-frame' parameter." + `(defun ,(intern (format "prot-window-popup-%s" command)) () + ,(format "Run `%s' in a popup frame with `prot-window-popup-frame' parameter. +Also see `prot-window-delete-popup-frame'." command) + (interactive) + (let ((frame (make-frame '((prot-window-popup-frame . t))))) + (select-frame frame) + ;; Placeholder for frame, otherwise it'll get autoclosed. + (switch-to-buffer " prot-window-hidden-buffer-for-popup-frame") + (condition-case nil + (call-interactively ',command) + ((quit error user-error) + (delete-frame frame)))))) + +(declare-function org-capture "org-capture" (&optional goto keys)) +(defvar org-capture-after-finalize-hook) + +;;;###autoload (autoload 'prot-window-popup-org-capture "prot-window") +(prot-window-define-with-popup-frame org-capture) + +(add-hook 'org-capture-after-finalize-hook #'prot-window-delete-popup-frame) + +(use-package olivetti + :ensure t + :defer t + :config + (setq olivetti-body-width 100)) + +(use-package logos +:ensure t +:config + +;; If you want to use outlines instead of page breaks (the ^L) +(setq logos-outlines-are-pages t) +(setq logos-outline-regexp-alist + `((emacs-lisp-mode . "^;;;+ ") + (org-mode . "^\\*+ +") + (markdown-mode . "^\\#+ +") + )) + +;; These apply when `logos-focus-mode' is enabled. Their value is +;; buffer-local. +(setq-default logos-hide-mode-line t + logos-hide-buffer-boundaries t + logos-hide-fringe t + logos-variable-pitch nil + logos-buffer-read-only nil + logos-scroll-lock nil + logos-olivetti t + olivetti-body-width 100 + ) + + +(let ((map global-map)) + (define-key map [remap narrow-to-region] #'logos-narrow-dwim) + (define-key map [remap forward-page] #'logos-forward-page-dwim) + (define-key map [remap backward-page] #'logos-backward-page-dwim) + (define-key map (kbd "<f7>") #'logos-focus-mode)) +) + +(use-package beframe + :ensure t + :hook (after-init . beframe-mode) + :config + (setq beframe-functions-in-frames '(project-prompt-project-dir)) + (setq beframe-global-buffers nil) + (define-key global-map (kbd "C-c b") beframe-prefix-map) + + ;;Integration with Consult + (defvar consult-buffer-sources) + (declare-function consult--buffer-state "consult") + + (with-eval-after-load 'consult + (defface beframe-buffer + '((t :inherit font-lock-string-face)) + "Face for `consult' framed buffers.") + + (defun my-beframe-buffer-names-sorted (&optional frame) + "Return the list of buffers from `beframe-buffer-names' sorted by visibility. +With optional argument FRAME, return the list of buffers of FRAME." + (beframe-buffer-names frame :sort #'beframe-buffer-sort-visibility)) + + (defvar beframe-consult-source + `( :name "Frame-specific buffers (current frame)" + :narrow ?F + :category buffer + :face beframe-buffer + :history beframe-history + :items ,#'my-beframe-buffer-names-sorted + :action ,#'switch-to-buffer + :state ,#'consult--buffer-state)) + + (add-to-list 'consult-buffer-sources 'beframe-consult-source))) + +(defun kill-project-buffers-and-close-frame () + (interactive) + (project-kill-buffers) + (delete-frame (selected-frame))) + +(define-key global-map (kbd "C-x p K") 'kill-project-buffers-and-close-frame) + +(add-hook 'text-mode-hook 'context-menu-mode) + +(defun my-context-menu (menu click) + "My context menu" + (define-key-after menu [dictionary-lookup] + '(menu-item "Dict" dictionary-search-word-at-mouse + :help "Look up in dictionary")) + menu) + +;; hook into context menu +(add-hook 'context-menu-functions #'my-context-menu) + +(provide 'rul-wm) +#+end_src + +** The =write= module +TODO + +#+begin_src emacs-lisp :tangle "rul-lisp/packages/rul-write.el" +;;;; `dictionary' +(setq dictionary-server "localhost" + dictionary-default-popup-strategy "lev" + dictionary-create-buttons nil + dictionary-use-single-buffer t) +(define-key global-map (kbd "C-c d") #'dictionary-lookup-definition) + +(use-package denote + :ensure t + :hook (dired-mode . denote-dired-mode) + :bind + (("C-c n n" . denote) + ("C-c n r" . denote-rename-file) + ("C-c n l" . denote-link) + ("C-c n b" . denote-backlinks)) + :config + (denote-rename-buffer-mode 1) + (setq denote-infer-keywords t) + (setq denote-sort-keywords t) + (setq denote-file-type 'org) + (setq denote-excluded-directories-regexp nil) + (setq denote-allow-multi-word-keywords nil) + (setq denote-link-fontify-backlinks t) + (setq denote-rename-no-confirm t) + + (let ((map global-map)) + (define-key map (kbd "C-c n j") #'denote-journal-new-or-existing-entry) + (define-key map (kbd "C-c n n") #'denote) + (define-key map (kbd "C-c n f") #'denote-open-or-create) + (define-key map (kbd "C-c n i") #'denote-link) + (define-key map (kbd "C-c n r") #'denote-rename-file) + ) +) + +(use-package electric + :init + (setq electric-quote-replace-double t) + :hook + (message-mode . electric-quote-local-mode)) + +(use-package message + :init + (setq mml-enable-flowed nil) + :hook + (message-mode . my/message-mode-setup)) + +(defun my/message-mode-setup () + (setq fill-column 72 + sentence-end-double-space nil) + (auto-fill-mode -1) + (visual-line-mode 1) + (when (require 'olivetti nil t) + (setq-local olivetti-body-width 100) + (olivetti-mode 1))) + +;; Flycheck +(use-package flycheck + :ensure t + :config + +(flycheck-define-checker proselint + "A linter for prose." + :command ("proselint" source-inplace) + :error-patterns + ((warning line-start (file-name) ":" line ":" column ": " + (id (one-or-more (not (any " ")))) + (message) line-end)) + :modes (text-mode markdown-mode gfm-mode org-mode)) + +(add-to-list 'flycheck-checkers 'proselint) + +;; TODO: docker run --rm -p 8010:8010 erikvl87/languagetool +(use-package flycheck-languagetool + :ensure t + :hook (message-mode . flycheck-languagetool-setup) + :init + (setq flycheck-languagetool-url "http://localhost:8010") +)) + +;; Flyspell +(defcustom flyspell-delayed-commands nil + "List of commands that are \"delayed\" for Flyspell mode. +After these commands, Flyspell checking is delayed for a short time, +whose length is specified by `flyspell-delay'." + :group 'flyspell + :type '(repeat (symbol))) + +(setq ispell-dictionary "en") +(setq flyspell-default-dictionary "en") + +(setq flyspell-issue-welcome-flag nil) +(setq-default ispell-list-command "list") + +(provide 'rul-write) +#+end_src diff --git a/.emacs.d/rul-init.d/auto-fill.el b/.emacs.d/rul-init.d/auto-fill.el deleted file mode 100644 index dad2831..0000000 --- a/.emacs.d/rul-init.d/auto-fill.el +++ /dev/null @@ -1,4 +0,0 @@ -;; auto-fill mode -(add-hook 'text-mode-hook 'turn-on-auto-fill) -(global-set-key (kbd "C-c q") 'auto-fill-mode) - diff --git a/.emacs.d/rul-init.d/flycheck.el b/.emacs.d/rul-init.d/flycheck.el deleted file mode 100644 index 006081e..0000000 --- a/.emacs.d/rul-init.d/flycheck.el +++ /dev/null @@ -1,22 +0,0 @@ -(use-package flycheck - :ensure t - :config - -(flycheck-define-checker proselint - "A linter for prose." - :command ("proselint" source-inplace) - :error-patterns - ((warning line-start (file-name) ":" line ":" column ": " - (id (one-or-more (not (any " ")))) - (message) line-end)) - :modes (text-mode markdown-mode gfm-mode org-mode)) - -(add-to-list 'flycheck-checkers 'proselint) - -;; TODO: docker run --rm -p 8010:8010 erikvl87/languagetool -(use-package flycheck-languagetool - :ensure t - :hook (message-mode . flycheck-languagetool-setup) - :init - (setq flycheck-languagetool-url "http://localhost:8010") -)) diff --git a/.emacs.d/rul-init.d/flyspell.el b/.emacs.d/rul-init.d/flyspell.el deleted file mode 100644 index 8cf27b8..0000000 --- a/.emacs.d/rul-init.d/flyspell.el +++ /dev/null @@ -1,12 +0,0 @@ -(defcustom flyspell-delayed-commands nil - "List of commands that are \"delayed\" for Flyspell mode. -After these commands, Flyspell checking is delayed for a short time, -whose length is specified by `flyspell-delay'." - :group 'flyspell - :type '(repeat (symbol))) - -(setq ispell-dictionary "en") -(setq flyspell-default-dictionary "en") - -(setq flyspell-issue-welcome-flag nil) -(setq-default ispell-list-command "list") diff --git a/.emacs.d/rul-init.d/go-lang.el b/.emacs.d/rul-init.d/go-lang.el deleted file mode 100644 index 8ec678b..0000000 --- a/.emacs.d/rul-init.d/go-lang.el +++ /dev/null @@ -1,23 +0,0 @@ -;; Debian packages: elpa-go-mode -;; Elpa packages: go-eldoc - -(use-package go-mode - :ensure t - :init - (progn - (bind-key [remap find-tag] #'godef-jump)) - :config - (add-hook 'go-mode-hook #'yas-minor-mode) - (add-hook 'go-mode-hook 'electric-pair-mode) - (add-hook 'go-mode-hook 'my-go-mode-hook) - (add-hook 'before-save-hook 'gofmt-before-save) -) - -(use-package go-eldoc - :ensure t - :init - (add-hook 'go-mode-hook 'go-eldoc-setup)) - -;; Define function to call when go-mode loads -(defun my-go-mode-hook () - (set 'compile-command "go build -v && go test -v && go vet")) diff --git a/.emacs.d/rul-init.d/ibuffer.el b/.emacs.d/rul-init.d/ibuffer.el deleted file mode 100644 index d5198d8..0000000 --- a/.emacs.d/rul-init.d/ibuffer.el +++ /dev/null @@ -1,35 +0,0 @@ -;; Debian packages: elpa-ibuffer-vc - -(use-package ibuffer ; Better buffer list - :bind (([remap list-buffers] . ibuffer)) - ;; Show VC Status in ibuffer - :config (setq ibuffer-formats - '((mark modified read-only vc-status-mini " " - (name 18 18 :left :elide) - " " - (size 9 -1 :right) - " " - (mode 16 16 :left :elide) - " " - (vc-status 16 16 :left) - " " - filename-and-process) - (mark modified read-only " " - (name 18 18 :left :elide) - " " - (size 9 -1 :right) - " " - (mode 16 16 :left :elide) - " " filename-and-process) - (mark " " - (name 16 -1) - " " filename)))) - -(use-package ibuffer-vc ; Group buffers by VC project and status - :ensure t - :defer t - :init (add-hook 'ibuffer-hook - (lambda () - (ibuffer-vc-set-filter-groups-by-vc-root) - (unless (eq ibuffer-sorting-mode 'alphabetic) - (ibuffer-do-sort-by-alphabetic))))) diff --git a/.emacs.d/rul-init.d/imenu.el b/.emacs.d/rul-init.d/imenu.el deleted file mode 100644 index 1a2b29b..0000000 --- a/.emacs.d/rul-init.d/imenu.el +++ /dev/null @@ -1,12 +0,0 @@ -;; Debian packages: elpa-imenu-list -(use-package imenu-list - :ensure t - :bind ("C-." . imenu-list-minor-mode) - :config - (setq imenu-list-focus-after-activation t) - (setq imenu-list-size 0.2) - (setq imenu-list-position 'left) - (add-hook 'go-mode-hook #'imenu-list-minor-mode)) - - - diff --git a/.emacs.d/rul-init.d/latex.el b/.emacs.d/rul-init.d/latex.el deleted file mode 100644 index de4de1f..0000000 --- a/.emacs.d/rul-init.d/latex.el +++ /dev/null @@ -1,9 +0,0 @@ -(add-hook 'latex-mode-hook 'flyspell-mode) -(setq TeX-PDF-mode t) - -(defun pdfevince () - (add-to-list 'TeX-output-view-style - '("^pdf$" "." "evince %o %(outpage)"))) - -(add-hook 'LaTeX-mode-hook 'pdfevince t) ; AUCTeX LaTeX mode - diff --git a/.emacs.d/rul-init.d/logos.el b/.emacs.d/rul-init.d/logos.el deleted file mode 100644 index 0ea1244..0000000 --- a/.emacs.d/rul-init.d/logos.el +++ /dev/null @@ -1,31 +0,0 @@ -(use-package logos -:ensure t -:config - -;; If you want to use outlines instead of page breaks (the ^L) -(setq logos-outlines-are-pages t) -(setq logos-outline-regexp-alist - `((emacs-lisp-mode . "^;;;+ ") - (org-mode . "^\\*+ +") - (markdown-mode . "^\\#+ +") - )) - -;; These apply when `logos-focus-mode' is enabled. Their value is -;; buffer-local. -(setq-default logos-hide-mode-line t - logos-hide-buffer-boundaries t - logos-hide-fringe t - logos-variable-pitch nil - logos-buffer-read-only nil - logos-scroll-lock nil - logos-olivetti t - olivetti-body-width 100 - ) - - -(let ((map global-map)) - (define-key map [remap narrow-to-region] #'logos-narrow-dwim) - (define-key map [remap forward-page] #'logos-forward-page-dwim) - (define-key map [remap backward-page] #'logos-backward-page-dwim) - (define-key map (kbd "<f9>") #'logos-focus-mode)) -) diff --git a/.emacs.d/rul-init.d/magit.el b/.emacs.d/rul-init.d/magit.el deleted file mode 100644 index 20ba99d..0000000 --- a/.emacs.d/rul-init.d/magit.el +++ /dev/null @@ -1,17 +0,0 @@ -;; Debian packages: elpa-magit - -(use-package magit - :ensure t - :defer t - :bind - (("C-c g s" . magit-status) - ("C-c g b" . magit-blame)) - :config - (progn - (defun inkel/magit-log-edit-mode-hook () - (flyspell-mode t) - (turn-on-auto-fill)) - (defadvice magit-status (around magit-fullscreen activate) - (window-configuration-to-register :magit-fullscreen) - ad-do-it - (delete-other-windows)))) diff --git a/.emacs.d/rul-init.d/mail-mode.el b/.emacs.d/rul-init.d/mail-mode.el deleted file mode 100644 index 09b04f9..0000000 --- a/.emacs.d/rul-init.d/mail-mode.el +++ /dev/null @@ -1,15 +0,0 @@ -(setq auto-mode-alist (append '((".*tmp/mutt.*" . message-mode)) auto-mode-alist)) -(setq auto-mode-alist (append '((".*tmp/neomutt.*" . message-mode)) auto-mode-alist)) -(add-to-list 'auto-mode-alist '("/mutt" . mail-mode)) - -(setq mml-secure-openpgp-sign-with-sender t) - -(add-hook 'mail-mode-hook - (lambda () - (font-lock-add-keywords nil - '(("^[ \t]*>[ \t]*>[ \t]*>.*$" - (0 'compilation-error)) - ("^[ \t]*>[ \t]*>.*$" - (0 'compilation-column-number)) - ("^[ \t]*>.*$" - (0 'comint-highlight-prompt)))))) diff --git a/.emacs.d/rul-init.d/markdown.el b/.emacs.d/rul-init.d/markdown.el deleted file mode 100644 index f035509..0000000 --- a/.emacs.d/rul-init.d/markdown.el +++ /dev/null @@ -1,5 +0,0 @@ -(autoload 'markdown-mode "markdown-mode.el" - "Major mode for editing Markdown files" t) - -(setq auto-mode-alist - (cons '("\\.mdwn" . markdown-mode) auto-mode-alist)) diff --git a/.emacs.d/rul-init.d/notmuch.el b/.emacs.d/rul-init.d/notmuch.el deleted file mode 100644 index e8e4d9d..0000000 --- a/.emacs.d/rul-init.d/notmuch.el +++ /dev/null @@ -1,136 +0,0 @@ -;; -------- -;; notmuch mode -;; -------- -(require 'rul-config-mail) -(use-package notmuch -:ensure t -:config -;;;; General UI -(setq notmuch-show-logo nil - notmuch-column-control 1.0 - notmuch-hello-auto-refresh t - notmuch-hello-recent-searches-max 20 - notmuch-hello-thousands-separator "" - notmuch-show-all-tags-list t) - -;; Keymaps -(defun rul/capture-mail() - "Capture mail to org mode." - (interactive) - (org-store-link nil) - (org-capture nil "m") - ) - -(bind-key "c" 'rul/capture-mail notmuch-show-mode-map) - -(define-key notmuch-show-mode-map "R" 'notmuch-show-reply) -(define-key notmuch-search-mode-map "R" 'notmuch-search-reply-to-thread) - -; Spam -(define-key notmuch-show-mode-map "S" - (lambda () - "mark message as spam" - (interactive) - (notmuch-show-tag (list "+spam" "-inbox" "-unread")))) - -(define-key notmuch-search-mode-map "S" - (lambda (&optional beg end) - "mark thread as spam" - (interactive (notmuch-search-interactive-region)) - (notmuch-search-tag (list "+spam" "-inbox" "-unread") beg end))) - -; Archive -(setq notmuch-archive-tags (list "-inbox" "+archive")) -(define-key notmuch-show-mode-map "A" - (lambda () - "archive" - (interactive) - (notmuch-show-tag (list "+archive" "-inbox" "-unread")) - (notmuch-refresh-this-buffer))) - -(define-key notmuch-search-mode-map "A" - (lambda (&optional beg end) - "archive thread" - (interactive (notmuch-search-interactive-region)) - (notmuch-search-tag (list "+archive" "-inbox" "-unread") beg end) - (notmuch-refresh-this-buffer))) - -; Mark as read -(define-key notmuch-search-mode-map "r" - (lambda (&optional beg end) - "mark thread as read" - (interactive (notmuch-search-interactive-region)) - (notmuch-search-tag (list "-unread") beg end) - (notmuch-search-next-thread))) - -(define-key notmuch-search-mode-map (kbd "RET") - (lambda () - "Show the selected thread with notmuch-tree if it has more -than one email. Use notmuch-show otherwise." - (interactive) - (if (= (plist-get (notmuch-search-get-result) :total) 1) - (notmuch-search-show-thread) - (notmuch-tree (notmuch-search-find-thread-id) - notmuch-search-query-string - nil - (notmuch-prettify-subject (notmuch-search-find-subject)))))) - -(defun color-inbox-if-unread () (interactive) - (save-excursion - (goto-char (point-min)) - (let ((cnt (car (process-lines "notmuch" "count" "tag:inbox and tag:unread")))) - (when (> (string-to-number cnt) 0) - (save-excursion - (when (search-forward "inbox" (point-max) t) - (let* ((overlays (overlays-in (match-beginning 0) (match-end 0))) - (overlay (car overlays))) - (when overlay - (overlay-put overlay 'face '((:inherit bold) (:foreground "green"))))))))))) - -(defvar notmuch-hello-refresh-count 0) -(defun notmuch-hello-refresh-status-message () - (let* ((new-count - (string-to-number - (car (process-lines notmuch-command "count")))) - (diff-count (- new-count notmuch-hello-refresh-count))) - (cond - ((= notmuch-hello-refresh-count 0) - (message "You have %s messages." - (notmuch-hello-nice-number new-count))) - ((> diff-count 0) - (message "You have %s more messages since last refresh." - (notmuch-hello-nice-number diff-count))) - ((< diff-count 0) - (message "You have %s fewer messages since last refresh." - (notmuch-hello-nice-number (- diff-count))))) - (setq notmuch-hello-refresh-count new-count))) - -(add-hook 'notmuch-hello-refresh-hook 'color-inbox-if-unread) -(add-hook 'notmuch-hello-refresh-hook 'notmuch-hello-refresh-status-message) - -(setq notmuch-hello-sections '(notmuch-hello-insert-saved-searches - notmuch-hello-insert-search - notmuch-hello-insert-recent-searches - notmuch-hello-insert-alltags - )) - -;; https://git.sr.ht/~tslil/dotfiles/tree/4e51afbb/emacs/notmuch-config.el#L76-82 -(defmacro make-binds (mode-map binds argfunc &rest body) - "Create keybindings in `mode-map' using a list of (keystr . arg) -pairs in `binds' of the form ( ... (argfunc arg) body)." - `(progn ,@(mapcar (lambda (pair) - `(define-key ,mode-map (kbd ,(car pair)) - (lambda () (interactive) (,argfunc ,(cdr pair)) ,@body))) - (eval binds)))) - -(defvar notmuch-hello-tree-searches '(("u" . "tag:unread") - ("i" . "tag:inbox") - ("*" . "*")) - "List of (key . query) pairs to bind in notmuch-hello.") - -(make-binds notmuch-hello-mode-map - notmuch-hello-tree-searches - notmuch-search) -) - -(use-package notmuch-indicator :ensure t) diff --git a/.emacs.d/rul-init.d/python.el b/.emacs.d/rul-init.d/python.el deleted file mode 100644 index 3186c72..0000000 --- a/.emacs.d/rul-init.d/python.el +++ /dev/null @@ -1 +0,0 @@ -(add-hook 'python-mode-hook 'py-autopep8-enable-on-save) diff --git a/.emacs.d/rul-init.d/vterm.el b/.emacs.d/rul-init.d/vterm.el deleted file mode 100644 index b3bed95..0000000 --- a/.emacs.d/rul-init.d/vterm.el +++ /dev/null @@ -1,35 +0,0 @@ -(use-package multi-vterm -:ensure t -:after (consult) -:config - -(setq vterm-source - `(:name "VTerm Buffer" - :category buffer - :narrow ?t - :face 'consult-buffer - :action ,#'consult--buffer-action - :items - (lambda () - (mapcar #'buffer-name - (seq-filter - (lambda (x) - (eq (buffer-local-value 'major-mode x) 'vterm-mode)) - (buffer-list)))))) - -(setq consult-vterm-buffer-sources - (list - `(:hidden nil :narrow ?t ,@vterm-source))) - -(setq vterm-max-scrollback 100000) - -(defun consult-vterm-buffer () - "Switch to a vterm buffer" - (interactive) - (consult-buffer consult-vterm-buffer-sources)) - -(bind-key "C-x b" 'consult-vterm-buffer vterm-mode-map) - -(add-to-list 'consult-buffer-sources 'vterm-source 'append) -(add-hook 'vterm-mode-hook 'goto-address-mode) -) diff --git a/.emacs.d/rul-init.d/which-key.el b/.emacs.d/rul-init.d/which-key.el deleted file mode 100644 index 1d8fd8d..0000000 --- a/.emacs.d/rul-init.d/which-key.el +++ /dev/null @@ -1,5 +0,0 @@ -(use-package which-key - :ensure t - :config - (which-key-mode) -) diff --git a/.emacs.d/rul-lisp/config/rul-config-elfeed.el b/.emacs.d/rul-lisp/config/rul-config-elfeed.el deleted file mode 100644 index 026c325..0000000 --- a/.emacs.d/rul-lisp/config/rul-config-elfeed.el +++ /dev/null @@ -1,2 +0,0 @@ -(setq elfeed-feeds '("https://planet.debian.org/rss10.xml")) -(provide 'rul-config-elfeed) diff --git a/.emacs.d/rul-lisp/config/rul-config-mail.el b/.emacs.d/rul-lisp/config/rul-config-mail.el deleted file mode 100644 index 240f8be..0000000 --- a/.emacs.d/rul-lisp/config/rul-config-mail.el +++ /dev/null @@ -1 +0,0 @@ -(provide 'rul-config-mail) diff --git a/.emacs.d/rul-lisp/config/rul-config-org.el b/.emacs.d/rul-lisp/config/rul-config-org.el deleted file mode 100644 index bf538dc..0000000 --- a/.emacs.d/rul-lisp/config/rul-config-org.el +++ /dev/null @@ -1,22 +0,0 @@ -(setq - org-agenda-files '("~/org/") - org-agenda-custom-commands - '(("x" agenda) - ("y" agenda*) - ("w" todo "WAITING") - ("W" todo-tree "WAITING") - ) - org-journal-file-type 'yearly - org-journal-dir "~/org/journal/" - org-journal-file-format "%Y.org" - org-journal-time-prefix "* " - org-journal-time-format "" - org-refile-path "~/refile.org" - org-roam-directory "~/org/roam/" - - org-agenda-private-local-path "/tmp/example.ics" - org-agenda-private-remote-path "/sshx:user@host:example.ics" - ) - -(provide 'rul-config-org) - diff --git a/.emacs.d/rul-lisp/packages/org-agenda-shell.el b/.emacs.d/rul-lisp/packages/org-agenda-shell.el new file mode 100644 index 0000000..4c4c493 --- /dev/null +++ b/.emacs.d/rul-lisp/packages/org-agenda-shell.el @@ -0,0 +1,214 @@ +;;; org-agenda-shell.el --- Export Org agenda state for shell integrations -*- lexical-binding: t; -*- + +(require 'cl-lib) +(require 'json) +(require 'org) +(require 'org-clock) +(require 'seq) + +(defgroup org-agenda-shell nil + "Export Org agenda data for desktop integrations." + :group 'org) + +(defcustom org-agenda-shell-snapshot-path "~/.cache/org-agenda-shell/today.json" + "Path to the JSON snapshot consumed by external shell integrations." + :type 'file) + +(defcustom org-agenda-shell-export-idle-delay 2 + "Idle delay, in seconds, before exporting the agenda snapshot after changes." + :type 'number) + +(defvar org-agenda-shell--export-idle-timer nil + "Pending idle timer for agenda snapshot exports.") + +(defun org-agenda-shell--time-epoch (time) + "Return TIME as an integer Unix epoch." + (truncate (float-time time))) + +(defun org-agenda-shell--json-bool (value) + "Return VALUE encoded as a JSON boolean." + (if value t :json-false)) + +(defun org-agenda-shell--today-days () + "Return today's date as an absolute day count." + (time-to-days (current-time))) + +(defun org-agenda-shell--open-todo-p () + "Return non-nil when the current heading is an open TODO item." + (let ((state (org-get-todo-state))) + (and state + (not (member state org-done-keywords))))) + +(defun org-agenda-shell--scheduled-clock-string (scheduled) + "Return the HH:MM component extracted from SCHEDULED, if present." + (when (and scheduled + (string-match "\\([0-9]\\{1,2\\}:[0-9]\\{2\\}\\)" scheduled)) + (match-string 1 scheduled))) + +(defun org-agenda-shell--task-record () + "Return the current heading as an export task alist, or nil." + (let* ((scheduled (org-entry-get nil "SCHEDULED")) + (scheduled-time (and scheduled (org-time-string-to-time scheduled))) + (scheduled-days (and scheduled-time (time-to-days scheduled-time))) + (today-days (org-agenda-shell--today-days))) + (when (and scheduled + scheduled-time + scheduled-days + (<= scheduled-days today-days) + (org-agenda-shell--open-todo-p)) + (let* ((file (buffer-file-name (buffer-base-buffer))) + (begin (point)) + (task-id (or (org-entry-get nil "ID") + (format "%s::%d" file begin))) + (scheduled-for (format-time-string "%F" scheduled-time)) + (clock-time (org-agenda-shell--scheduled-clock-string scheduled)) + (title (org-get-heading t t t t)) + (state (org-get-todo-state))) + `((id . ,task-id) + (title . ,title) + (time . ,clock-time) + (state . ,state) + (category . ,(org-get-category)) + (scheduled_for . ,scheduled-for) + (is_today . ,(org-agenda-shell--json-bool + (= scheduled-days today-days))) + (is_overdue . ,(org-agenda-shell--json-bool + (< scheduled-days today-days))) + (source_file . ,file) + (_sort_days . ,scheduled-days) + (_sort_time . ,(or clock-time ""))))))) + +(defun org-agenda-shell--task< (left right) + "Return non-nil when LEFT should sort before RIGHT." + (let ((left-days (alist-get '_sort_days left)) + (right-days (alist-get '_sort_days right)) + (left-time (alist-get '_sort_time left)) + (right-time (alist-get '_sort_time right)) + (left-title (alist-get 'title left)) + (right-title (alist-get 'title right))) + (or (< left-days right-days) + (and (= left-days right-days) + (or (string< left-time right-time) + (and (string= left-time right-time) + (string< left-title right-title))))))) + +(defun org-agenda-shell--public-task (task) + "Return TASK without exporter-only sort keys." + (seq-remove + (lambda (pair) + (memq (car pair) '(_sort_days _sort_time))) + task)) + +(defun org-agenda-shell--collect-tasks () + "Return agenda tasks scheduled for today and overdue scheduled items." + (let (tasks) + (dolist (file (org-agenda-files)) + (when (file-readable-p file) + (let ((create-lockfiles nil)) + (with-current-buffer (find-file-noselect file) + (org-with-wide-buffer + (org-map-entries + (lambda () + (let ((task (org-agenda-shell--task-record))) + (when task + (push task tasks)))) + nil + 'file)))))) + (sort tasks #'org-agenda-shell--task<))) + +(defun org-agenda-shell--clocked-in-record () + "Return the currently clocked-in Org task as an alist, or nil." + (when (and (org-clocking-p) + (marker-buffer org-clock-marker)) + (org-with-point-at org-clock-marker + (let* ((file (buffer-file-name (buffer-base-buffer))) + (begin (point)) + (started-at org-clock-start-time) + (task-id (or (org-entry-get nil "ID") + (format "%s::%d" file begin)))) + `((id . ,task-id) + (title . ,(or org-clock-current-task + (org-get-heading t t t t))) + (state . ,(org-get-todo-state)) + (category . ,(org-get-category)) + (source_file . ,file) + (started_at . ,(format-time-string "%FT%T%z" started-at)) + (started_epoch . ,(org-agenda-shell--time-epoch started-at))))))) + +;;;###autoload +(defun org-agenda-shell-export () + "Write the JSON snapshot consumed by shell integrations." + (interactive) + (let* ((now (current-time)) + (json-encoding-pretty-print nil) + (tasks (mapcar #'org-agenda-shell--public-task + (org-agenda-shell--collect-tasks))) + (clocked-in (org-agenda-shell--clocked-in-record)) + (today-count (cl-count-if (lambda (task) + (eq t (alist-get 'is_today task))) + tasks)) + (overdue-count (cl-count-if (lambda (task) + (eq t (alist-get 'is_overdue task))) + tasks)) + (payload `((generated_at . ,(format-time-string "%FT%T%z" now)) + (generated_epoch . ,(org-agenda-shell--time-epoch now)) + (date . ,(format-time-string "%F" now)) + (task_count . ,(length tasks)) + (today_count . ,today-count) + (overdue_count . ,overdue-count) + (clocked_in . ,clocked-in) + (today_tasks . ,(vconcat tasks)))) + (target (expand-file-name org-agenda-shell-snapshot-path)) + (target-dir (file-name-directory target))) + (make-directory target-dir t) + (with-temp-file target + (insert (json-encode payload)) + (insert "\n")))) + +(defun org-agenda-shell-safe-export () + "Export the agenda snapshot and log any errors." + (setq org-agenda-shell--export-idle-timer nil) + (condition-case err + (org-agenda-shell-export) + (error + (message "org-agenda-shell export failed: %s" + (error-message-string err))))) + +(defun org-agenda-shell-schedule-export () + "Schedule an idle export of the agenda snapshot." + (when org-agenda-shell--export-idle-timer + (cancel-timer org-agenda-shell--export-idle-timer)) + (setq org-agenda-shell--export-idle-timer + (run-with-idle-timer + org-agenda-shell-export-idle-delay + nil + #'org-agenda-shell-safe-export))) + +(defun org-agenda-shell--after-save-hook () + "Refresh the agenda snapshot when an agenda file is saved." + (when (and buffer-file-name + (member (file-truename buffer-file-name) + (mapcar #'file-truename (org-agenda-files)))) + (org-agenda-shell-schedule-export))) + +;;;###autoload +(define-minor-mode org-agenda-shell-mode + "Keep a JSON snapshot of the Org agenda up to date." + :global t + (if org-agenda-shell-mode + (progn + (add-hook 'after-save-hook #'org-agenda-shell--after-save-hook) + (add-hook 'org-clock-in-hook #'org-agenda-shell-schedule-export) + (add-hook 'org-clock-out-hook #'org-agenda-shell-schedule-export) + (add-hook 'org-clock-cancel-hook #'org-agenda-shell-schedule-export) + (org-agenda-shell-schedule-export)) + (remove-hook 'after-save-hook #'org-agenda-shell--after-save-hook) + (remove-hook 'org-clock-in-hook #'org-agenda-shell-schedule-export) + (remove-hook 'org-clock-out-hook #'org-agenda-shell-schedule-export) + (remove-hook 'org-clock-cancel-hook #'org-agenda-shell-schedule-export) + (when org-agenda-shell--export-idle-timer + (cancel-timer org-agenda-shell--export-idle-timer) + (setq org-agenda-shell--export-idle-timer nil)))) + +(provide 'org-agenda-shell) +;;; org-agenda-shell.el ends here diff --git a/.emacs.d/rul-init.d/hydra.el b/.emacs.d/rul-lisp/packages/rul-bindings.el index 525162e..3a608c0 100644 --- a/.emacs.d/rul-init.d/hydra.el +++ b/.emacs.d/rul-lisp/packages/rul-bindings.el @@ -1,3 +1,14 @@ +;; Global keybindings +(global-set-key (kbd "C-c R") 'revert-buffer) +(global-set-key (kbd "C-c w") 'whitespace-cleanup) + +(defun help/insert-em-dash () + "Inserts an EM-DASH (not a HYPEN, not an N-DASH)" + (interactive) + (insert "—")) + +(global-set-key (kbd "C--") #'help/insert-em-dash) + (use-package hydra :ensure t :defer 1) @@ -32,17 +43,11 @@ ("=" gofmt :exit t) ("c" go-coverage :exit t)) -;; vterm -(defhydra hydra-vterm () - "zoom" - ("t" multi-vterm "Open a terminal" :exit t) - ("d" multi-vterm-dedicated-open "Dedicated" :exit t) - ("p" multi-vterm-prev "Previous terminal") - ("n" multi-vterm-next "Next terminal") - ("r" multi-vterm-rename-buffer "Rename buffer" :exit t) - ) - -(global-set-key (kbd "C-c t") 'hydra-vterm/body) +(global-set-key (kbd "C-c m") 'hydra-go/body) +(use-package which-key + :ensure t + :config + (which-key-mode)) -(global-set-key (kbd "C-c m") 'hydra-go/body) +(provide 'rul-bindings) diff --git a/.emacs.d/rul-lisp/packages/rul-completion.el b/.emacs.d/rul-lisp/packages/rul-completion.el index 2b3afa1..3412354 100644 --- a/.emacs.d/rul-lisp/packages/rul-completion.el +++ b/.emacs.d/rul-lisp/packages/rul-completion.el @@ -1,5 +1,14 @@ (use-package orderless :ensure t) +(setq completion-styles '(basic substring initials orderless)) +(setq completion-category-overrides + '( + (file (styles . (basic partial-completion orderless))) + (project-file (styles . (flex basic substring partial-completion orderless))) + )) + +(setq completion-ignore-case t) + ;; Enable vertico (use-package vertico :ensure t @@ -7,50 +16,18 @@ (vertico-mode) :config - (add-hook 'rfn-eshadow-update-overlay-hook #'vertico-directory-tidy) - - ;; Different scroll margin - ;; (setq vertico-scroll-margin 0) - - ;; Show more candidates - ;; (setq vertico-count 20) - - ;; Grow and shrink the Vertico minibuffer - ;; (setq vertico-resize t) - - ;; Optionally enable cycling for `vertico-next' and `vertico-previous'. - ;; (setq vertico-cycle t) - ) + (add-hook 'rfn-eshadow-update-overlay-hook #'vertico-directory-tidy)) ;; Enable rich annotations using the Marginalia package (use-package marginalia :ensure t - ;; Bind `marginalia-cycle' locally in the minibuffer. To make the binding - ;; available in the *Completions* buffer, add it to the - ;; `completion-list-mode-map'. :bind (:map minibuffer-local-map ("M-A" . marginalia-cycle)) - - ;; The :init section is always executed. :init - - ;; Marginalia must be actived in the :init section of use-package such that - ;; the mode gets enabled right away. Note that this forces loading the - ;; package. (marginalia-mode)) -(setq completion-styles '(basic substring initials orderless)) -(setq completion-category-overrides - '( - (file (styles . (basic partial-completion orderless))) - (project-file (styles . (flex basic substring partial-completion orderless))) - )) - -(setq completion-ignore-case t) - (use-package consult :ensure t - ;; Replace bindings. Lazily loaded due by `use-package'. :bind (;; C-c bindings in `mode-specific-map' ("C-c M-x" . consult-mode-command) ("C-c h" . consult-history) @@ -126,7 +103,7 @@ :bind (("C-." . embark-act) ;; pick some comfortable binding - ("C-;" . embark-dwim) ;; good alternative: M-. + ("M-." . embark-dwim) ;; good alternative: M-. ("C-h B" . embark-bindings)) ;; alternative for `describe-bindings' :init @@ -139,9 +116,8 @@ nil (window-parameters (mode-line-format . none))))) -;; Consult users will also want the embark-consult package. (use-package embark-consult - :ensure t ; only need to install it, embark loads it after consult if found + :ensure t :hook (embark-collect-mode . consult-preview-at-point-mode)) diff --git a/.emacs.d/rul-lisp/packages/rul-dashboard.el b/.emacs.d/rul-lisp/packages/rul-dashboard.el new file mode 100644 index 0000000..67bd188 --- /dev/null +++ b/.emacs.d/rul-lisp/packages/rul-dashboard.el @@ -0,0 +1,22 @@ +(use-package page-break-lines :ensure t) + +(use-package dashboard + :ensure t + + :config + (dashboard-setup-startup-hook) + + :custom + (dashboard-center-content t) + (dashboard-startup-banner 3) + (dashboard-items '((recents . 5) + (bookmarks . 5) + (projects . 5) + (agenda . 5) + )) + (dashboard-icon-type 'nerd-icons) + (dashboard-set-heading-icons t) + (dashboard-set-file-icons t) +) + +(provide 'rul-dashboard) diff --git a/.emacs.d/rul-lisp/packages/rul-elfeed.el b/.emacs.d/rul-lisp/packages/rul-elfeed.el deleted file mode 100644 index 22aacdd..0000000 --- a/.emacs.d/rul-lisp/packages/rul-elfeed.el +++ /dev/null @@ -1,6 +0,0 @@ -(use-package elfeed -:ensure t -:config -(require 'rul-config-elfeed)) - -(provide 'rul-elfeed) diff --git a/.emacs.d/rul-lisp/packages/rul-fm.el b/.emacs.d/rul-lisp/packages/rul-fm.el new file mode 100644 index 0000000..83aaf37 --- /dev/null +++ b/.emacs.d/rul-lisp/packages/rul-fm.el @@ -0,0 +1,19 @@ +;;; rul-fm.el --- File management + +;; dired +(add-hook 'dired-mode-hook #'dired-hide-details-mode) +(setq dired-guess-shell-alist-user + '(("\\.\\(png\\|jpe?g\\|tiff\\)" "feh" "xdg-open") + ("\\.\\(mp[34]\\|m4a\\|ogg\\|flac\\|webm\\|mkv\\)" "mpv" "xdg-open") + (".*" "xdg-open"))) + +(setq dired-kill-when-opening-new-dired-buffer t) +(put 'dired-find-alternate-file 'disabled nil) + +;;; Icons +(use-package nerd-icons :ensure t ) +(use-package nerd-icons-dired :ensure t + :config + (add-hook 'dired-mode-hook #'nerd-icons-dired-mode)) + +(provide 'rul-fm) diff --git a/.emacs.d/rul-init.d/fonts.el b/.emacs.d/rul-lisp/packages/rul-fonts.el index a7cb255..c3b7202 100644 --- a/.emacs.d/rul-init.d/fonts.el +++ b/.emacs.d/rul-lisp/packages/rul-fonts.el @@ -1,3 +1,5 @@ +;;; rul-fonts.el --- Fonts configuration + (use-package fontaine :ensure t :config @@ -7,7 +9,9 @@ (small :default-height 120) (medium - :default-height 140) + :default-height 150) + (wayland-medium + :default-height 320) (large :default-weight semilight :default-height 180 @@ -20,11 +24,16 @@ :default-weight semilight :default-height 230 :bold-weight extrabold) + (writing + :default-height 140 + :default-family "Lato" + :variable-pitch-family "Regular" + ) (t :default-family "Iosevka" :default-weight regular - :default-height 140 + :default-height 150 :variable-pitch-family "Iosevka Aile"))) - - ;; Set desired style from `fontaine-presets' (fontaine-set-preset 'medium)) + +(provide 'rul-fonts) diff --git a/.emacs.d/rul-lisp/packages/rul-media.el b/.emacs.d/rul-lisp/packages/rul-io.el index b2f8db1..50e5a68 100644 --- a/.emacs.d/rul-lisp/packages/rul-media.el +++ b/.emacs.d/rul-lisp/packages/rul-io.el @@ -1,3 +1,8 @@ +;;; rul-io.el --- Configuration for Internet and media packages + +(use-package elfeed :ensure t) +(provide 'rul-feeds) + (use-package empv :ensure t :config @@ -9,7 +14,9 @@ ("SomaFM - Metal" . "https://somafm.com/metal.pls") ("SomaFM - Lush" . "https://somafm.com/lush130.pls") ("KCSM Jazz 91" . "http://ice5.securenetsystems.net/KCSM") + ("KSUA 91.5 FM" . "https://stream.radio.co/se776fab22/listen") )) + (setq empv-fd-binary "fdfind") ) -(provide 'rul-media) +(provide 'rul-io) diff --git a/.emacs.d/rul-lisp/packages/rul-mail.el b/.emacs.d/rul-lisp/packages/rul-mail.el new file mode 100644 index 0000000..12846a0 --- /dev/null +++ b/.emacs.d/rul-lisp/packages/rul-mail.el @@ -0,0 +1,211 @@ +;;; rul-mail.el --- Email configuration + +;; mml-sec.el +;; Use sender to find GPG key. +(setq mml-secure-openpgp-sign-with-sender t) + +;; Keep HTML mail readable without sender-defined styling. +(setq shr-use-colors nil + shr-use-fonts nil) + +(with-eval-after-load 'shr + (set-face-attribute 'shr-link nil :inherit 'default :underline t)) + +(use-package notmuch + :ensure t + :config + ;; UI + (setq notmuch-show-logo nil + notmuch-column-control 1.0 + notmuch-hello-auto-refresh t + notmuch-hello-recent-searches-max 20 + notmuch-hello-thousands-separator "" + notmuch-show-all-tags-list t + notmuch-show-text/html-blocked-images nil + ) + + (setq notmuch-draft-folder "current/Drafts") + + (defvar-local rul/notmuch-show-refreshing-for-olivetti nil) + + (defun rul/notmuch-show-enable-olivetti () + "Enable Olivetti before notmuch renders message HTML." + (setq-local olivetti-body-width 100 + shr-width nil + shr-max-width olivetti-body-width) + (olivetti-mode 1)) + + (defun rul/notmuch-show-refresh-after-olivetti () + "Refresh an already-rendered notmuch buffer after Olivetti changes width." + (when (and (derived-mode-p 'notmuch-show-mode) + olivetti-mode + notmuch-show-thread-id + (not rul/notmuch-show-refreshing-for-olivetti)) + (setq rul/notmuch-show-refreshing-for-olivetti t) + (run-at-time + 0 nil + (lambda (buffer) + (when (buffer-live-p buffer) + (with-current-buffer buffer + (unwind-protect + (when (and (derived-mode-p 'notmuch-show-mode) + olivetti-mode + notmuch-show-thread-id) + (notmuch-refresh-this-buffer)) + (setq rul/notmuch-show-refreshing-for-olivetti nil))))) + (current-buffer)))) + + (add-hook 'notmuch-show-mode-hook #'rul/notmuch-show-enable-olivetti) + + (with-eval-after-load 'olivetti + (add-hook 'olivetti-mode-on-hook + #'rul/notmuch-show-refresh-after-olivetti + t)) + + (defun rul/notmuch-inline-override-images (types) + "Treat standalone image MIME parts as attachments in notmuch." + (if (member "image/.*" types) + types + (append types '("image/.*")))) + + (unless (advice-member-p #'rul/notmuch-inline-override-images + 'notmuch--inline-override-types) + (advice-add 'notmuch--inline-override-types :filter-return + #'rul/notmuch-inline-override-images)) + + ;; Keymaps + (defun rul/capture-mail() + "Capture mail to org mode." + (interactive) + (org-store-link nil) + (org-capture nil "m") + ) + + (bind-key "c" 'rul/capture-mail notmuch-show-mode-map) + + ;; Keep uppercase refresh cheap after tag operations; use M-g for a full poll. + (define-key notmuch-show-mode-map "G" 'notmuch-refresh-this-buffer) + (define-key notmuch-search-mode-map "G" 'notmuch-refresh-this-buffer) + (define-key notmuch-show-mode-map (kbd "M-g") 'notmuch-poll-and-refresh-this-buffer) + (define-key notmuch-search-mode-map (kbd "M-g") 'notmuch-poll-and-refresh-this-buffer) + + (define-key notmuch-show-mode-map "R" 'notmuch-show-reply) + (define-key notmuch-search-mode-map "R" 'notmuch-search-reply-to-thread) + + ;; Spam + (define-key notmuch-show-mode-map "S" + (lambda () + "mark message as spam" + (interactive) + (notmuch-show-tag (list "+spam" "-inbox" "-unread")))) + + (define-key notmuch-search-mode-map "S" + (lambda (&optional beg end) + "mark thread as spam" + (interactive (notmuch-search-interactive-region)) + (notmuch-search-tag (list "+spam" "-inbox" "-unread") beg end))) + + ;; Archive + ;; Stock `a` applies notmuch-archive-tags but doesn't re-run the search, so + ;; the thread stays visible in the tag:inbox buffer with stale results. Wrap + ;; both `a` and `A` to refresh the buffer so the thread drops out of view. + (setq notmuch-archive-tags (list "-inbox" "+archive")) + + (defun rul/notmuch-search-archive (&optional unarchive beg end) + "Archive threads and refresh so they drop out of the inbox view." + (interactive (cons current-prefix-arg (notmuch-search-interactive-region))) + (notmuch-search-archive-thread unarchive beg end) + (notmuch-refresh-this-buffer)) + + (defun rul/notmuch-show-archive () + "Archive the current message with +archive -inbox -unread and refresh." + (interactive) + (notmuch-show-tag (list "+archive" "-inbox" "-unread")) + (notmuch-refresh-this-buffer)) + + (define-key notmuch-search-mode-map "a" 'rul/notmuch-search-archive) + (define-key notmuch-search-mode-map "A" 'rul/notmuch-search-archive) + (define-key notmuch-show-mode-map "a" 'rul/notmuch-show-archive) + (define-key notmuch-show-mode-map "A" 'rul/notmuch-show-archive) + + ;; Mark as read + (define-key notmuch-search-mode-map "r" + (lambda (&optional beg end) + "mark thread as read" + (interactive (notmuch-search-interactive-region)) + (notmuch-search-tag (list "-unread") beg end) + (notmuch-search-next-thread))) + + (define-key notmuch-search-mode-map (kbd "RET") + (lambda () + "Show the selected thread with notmuch-tree if it has more +than one email. Use notmuch-show otherwise." + (interactive) + (if (= (plist-get (notmuch-search-get-result) :total) 1) + (notmuch-search-show-thread) + (notmuch-tree (notmuch-search-find-thread-id) + notmuch-search-query-string + nil + (notmuch-prettify-subject (notmuch-search-find-subject)))))) + + (defun color-inbox-if-unread () (interactive) + (save-excursion + (goto-char (point-min)) + (let ((cnt (car (process-lines "notmuch" "count" "tag:inbox and tag:unread")))) + (when (> (string-to-number cnt) 0) + (save-excursion + (when (search-forward "inbox" (point-max) t) + (let* ((overlays (overlays-in (match-beginning 0) (match-end 0))) + (overlay (car overlays))) + (when overlay + (overlay-put overlay 'face '((:inherit bold) (:foreground "green"))))))))))) + + (defvar notmuch-hello-refresh-count 0) + (defun notmuch-hello-refresh-status-message () + (let* ((new-count + (string-to-number + (car (process-lines notmuch-command "count")))) + (diff-count (- new-count notmuch-hello-refresh-count))) + (cond + ((= notmuch-hello-refresh-count 0) + (message "You have %s messages." + (notmuch-hello-nice-number new-count))) + ((> diff-count 0) + (message "You have %s more messages since last refresh." + (notmuch-hello-nice-number diff-count))) + ((< diff-count 0) + (message "You have %s fewer messages since last refresh." + (notmuch-hello-nice-number (- diff-count))))) + (setq notmuch-hello-refresh-count new-count))) + + (add-hook 'notmuch-hello-refresh-hook 'color-inbox-if-unread) + (add-hook 'notmuch-hello-refresh-hook 'notmuch-hello-refresh-status-message) + + (setq notmuch-hello-sections '(notmuch-hello-insert-saved-searches + notmuch-hello-insert-search + notmuch-hello-insert-recent-searches + notmuch-hello-insert-alltags + )) + + ;; https://git.sr.ht/~tslil/dotfiles/tree/4e51afbb/emacs/notmuch-config.el#L76-82 + (defmacro make-binds (mode-map binds argfunc &rest body) + "Create keybindings in `mode-map' using a list of (keystr . arg) +pairs in `binds' of the form ( ... (argfunc arg) body)." + `(progn ,@(mapcar (lambda (pair) + `(define-key ,mode-map (kbd ,(car pair)) + (lambda () (interactive) (,argfunc ,(cdr pair)) ,@body))) + (eval binds)))) + + (defvar notmuch-hello-tree-searches '(("u" . "tag:unread") + ("i" . "tag:inbox") + ("*" . "*")) + "List of (key . query) pairs to bind in notmuch-hello.") + + (make-binds notmuch-hello-mode-map + notmuch-hello-tree-searches + notmuch-search) +) ;; ends use-package notmuch + +(use-package notmuch-indicator :ensure t) + +(provide 'rul-mail) diff --git a/.emacs.d/rul-lisp/packages/rul-modeline.el b/.emacs.d/rul-lisp/packages/rul-modeline.el index ae250c4..32c6048 100644 --- a/.emacs.d/rul-lisp/packages/rul-modeline.el +++ b/.emacs.d/rul-lisp/packages/rul-modeline.el @@ -1,3 +1,5 @@ +;;; rul-modeline.el --- Modeline configuration + ;; Most of the code in this file is based on: ;; https://git.sr.ht/~protesilaos/dotfiles/tree/cf26bc34/item/emacs/.emacs.d/prot-lisp/prot-modeline.el ;; @@ -103,10 +105,14 @@ Specific to the current window's mode line.") mode-line-front-space mode-line-buffer-identification mode-line-front-space + mode-line-percent-position + mode-line-front-space rul-modeline-major-mode prot-modeline-eglot mode-line-format-right-align rul-modeline-misc-info + mode-line-front-space + mode-line-front-space )) (provide 'rul-modeline) diff --git a/.emacs.d/rul-lisp/packages/rul-org-agenda.el b/.emacs.d/rul-lisp/packages/rul-org-agenda.el index bfdc8d3..9d249f9 100644 --- a/.emacs.d/rul-lisp/packages/rul-org-agenda.el +++ b/.emacs.d/rul-lisp/packages/rul-org-agenda.el @@ -1,8 +1,16 @@ +;;; rul-org-agenda.el --- Org agenda configuration (require 'org) (global-set-key (kbd "<f12>") #'org-agenda) (global-set-key (kbd "C-c a") #'org-agenda) +(defun rul-org-agenda-save-after-todo (&rest _) + "Save Org buffers after changing a TODO state from agenda." + (org-save-all-org-buffers)) + +(with-eval-after-load 'org-agenda + (advice-add 'org-agenda-todo :after #'rul-org-agenda-save-after-todo)) + (defun bh/is-project-p () "Any task with a todo keyword subtask" (save-restriction @@ -267,8 +275,6 @@ Skip project and sub-project tasks, habits, and loose non-project tasks." (setq org-clock-history-length 23) ;; Resume clocking task on clock-in if the clock is open (setq org-clock-in-resume t) -;; Change tasks to NEXT when clocking in -(setq org-clock-in-switch-to-state 'bh/clock-in-to-next) ;; Separate drawers for clocking and logs (setq org-drawers (quote ("PROPERTIES" "LOGBOOK"))) ;; Save clock data and state changes and notes in the LOGBOOK drawer @@ -286,22 +292,6 @@ Skip project and sub-project tasks, habits, and loose non-project tasks." ;; Include current clocking task in clock reports (setq org-clock-report-include-clocking-task t) - -(setq bh/keep-clock-running nil) - -(defun bh/clock-in-to-next (kw) - "Switch a task from TODO to NEXT when clocking in. -Skips capture tasks, projects, and subprojects. -Switch projects and subprojects from NEXT back to TODO" - (when (not (and (boundp 'org-capture-mode) org-capture-mode)) - (cond - ((and (member (org-get-todo-state) (list "TODO")) - (bh/is-task-p)) - "NEXT") - ((and (member (org-get-todo-state) (list "NEXT")) - (bh/is-project-p)) - "TODO")))) - (defun bh/find-project-task () "Move point to the parent (project) task if any" (save-restriction @@ -313,60 +303,6 @@ Switch projects and subprojects from NEXT back to TODO" (goto-char parent-task) parent-task))) -(defun bh/punch-in (arg) - "Start continuous clocking and set the default task to the -selected task. If no task is selected set the Organization task -as the default task." - (interactive "p") - (setq bh/keep-clock-running t) - (if (equal major-mode 'org-agenda-mode) - ;; - ;; We're in the agenda - ;; - (let* ((marker (org-get-at-bol 'org-hd-marker)) - (tags (org-with-point-at marker (org-get-tags-at)))) - (if (and (eq arg 4) tags) - (org-agenda-clock-in '(16)) - (bh/clock-in-organization-task-as-default))) - ;; - ;; We are not in the agenda - ;; - (save-restriction - (widen) - ; Find the tags on the current task - (if (and (equal major-mode 'org-mode) (not (org-before-first-heading-p)) (eq arg 4)) - (org-clock-in '(16)) - (bh/clock-in-organization-task-as-default))))) - -(defun bh/punch-out () - (interactive) - (setq bh/keep-clock-running nil) - (when (org-clock-is-active) - (org-clock-out)) - (org-agenda-remove-restriction-lock)) - -(defun bh/clock-in-default-task () - (save-excursion - (org-with-point-at org-clock-default-task - (org-clock-in)))) - -(defun bh/clock-in-parent-task () - "Move point to the parent (project) task if any and clock in" - (let ((parent-task)) - (save-excursion - (save-restriction - (widen) - (while (and (not parent-task) (org-up-heading-safe)) - (when (member (nth 2 (org-heading-components)) org-todo-keywords-1) - (setq parent-task (point)))) - (if parent-task - (org-with-point-at parent-task - (org-clock-in)) - (when bh/keep-clock-running - (bh/clock-in-default-task))))))) - -(defvar bh/organization-task-id "eb155a82-92b2-4f25-a3c6-0304591af2f9") - ;; https://stackoverflow.com/a/10091330 (defun zin/org-agenda-skip-tag (tag &optional others) "Skip all entries that correspond to TAG. @@ -384,20 +320,6 @@ If OTHERS is true, skip all entries that do not correspond to TAG." next-headline nil)))) -(defun bh/clock-in-organization-task-as-default () - (interactive) - (org-with-point-at (org-id-find bh/organization-task-id 'marker) - (org-clock-in '(16)))) - -(defun bh/clock-out-maybe () - (when (and bh/keep-clock-running - (not org-clock-clocking-in) - (marker-buffer org-clock-default-task) - (not org-clock-resolving-clocks-due-to-idleness)) - (bh/clock-in-parent-task))) - -(add-hook 'org-clock-out-hook 'bh/clock-out-maybe 'append) - ;; AGENDA VIEW ;; ;; Do not dim blocked tasks diff --git a/.emacs.d/rul-lisp/packages/rul-org-journal.el b/.emacs.d/rul-lisp/packages/rul-org-journal.el deleted file mode 100644 index 46fcebf..0000000 --- a/.emacs.d/rul-lisp/packages/rul-org-journal.el +++ /dev/null @@ -1,19 +0,0 @@ -(use-package org-journal - :ensure t - :init - ;; Change default prefix key; needs to be set before loading org-journal - (setq org-journal-prefix-key "C-c j ") - :config - (require 'rul-config-org) - (setq org-journal-date-format "%A, %d %B %Y") - - (global-set-key (kbd "C-c j o") 'org-journal-open-current-journal-file) - (global-set-key (kbd "C-c j j") 'org-journal-new-entry) - (global-set-key (kbd "C-c j J") 'org-journal-new-scheduled-entry) - - (define-key org-journal-mode-map (kbd "C-c j n") 'org-journal-next-entry) - (define-key org-journal-mode-map (kbd "C-c j p") 'org-journal-previous-entry) - (define-key org-journal-mode-map (kbd "C-c j r") 'org-journal-reschedule-scheduled-entry) - ) - -(provide 'rul-org-journal) diff --git a/.emacs.d/rul-lisp/packages/rul-org-notify.el b/.emacs.d/rul-lisp/packages/rul-org-notify.el deleted file mode 100644 index 50c35a0..0000000 --- a/.emacs.d/rul-lisp/packages/rul-org-notify.el +++ /dev/null @@ -1,9 +0,0 @@ -(use-package org-notify - :ensure t - :config - (org-notify-start) - (org-notify-add 'default - '(:time "-1s" :period "20s" :duration 10 - :actions (-message -ding)) - '(:time "1d" :actions -notify/window - :duration 60))) diff --git a/.emacs.d/rul-lisp/packages/rul-org.el b/.emacs.d/rul-lisp/packages/rul-org.el index d076948..f488ab0 100644 --- a/.emacs.d/rul-lisp/packages/rul-org.el +++ b/.emacs.d/rul-lisp/packages/rul-org.el @@ -1,24 +1,38 @@ -;; Debian packages: elpa-org -;; Elpa packages: org-modern +;;; rul-org.el --- Org configuration (require 'org) (require 'org-capture) (require 'org-protocol) (require 'org-habit) -(require 'rul-config-org) - (require 'rul-org-agenda) -(require 'rul-org-journal) +(require 'org-agenda-shell) +(org-agenda-shell-mode 1) +(setq org-attach-use-inheritance t) (setq org-cycle-separator-lines 0) -(setq org-startup-indented t) (setq org-hide-leading-stars nil) +(setq org-startup-indented t) +(setq org-edit-src-content-indentation 0) (use-package org-modern :ensure t) -(use-package org-pomodoro :ensure t) +(use-package org-pomodoro + :ensure t + :config + (defun rul/disable-notifications () + "Disable GNOME notifications." + (shell-command "gsettings set org.gnome.desktop.notifications show-banners false")) + + (defun rul/enable-notifications () + "Enable GNOME notifications." + (shell-command "gsettings set org.gnome.desktop.notifications show-banners true")) -(add-hook 'org-mode-hook 'turn-off-auto-fill) -(add-hook 'auto-save-hook 'org-save-all-org-buffers) + ;; Add hooks for Pomodoro start and finish + (add-hook 'org-pomodoro-started-hook #'rul/disable-notifications) + (add-hook 'org-pomodoro-finished-hook #'rul/enable-notifications) + (add-hook 'org-pomodoro-killed-hook #'rul/enable-notifications)) + +;; (add-hook 'org-mode-hook 'turn-off-auto-fill) +;; (add-hook 'auto-save-hook 'org-save-all-org-buffers) (add-hook 'org-mode-hook 'visual-line-mode) (use-package org-download @@ -39,6 +53,11 @@ (global-set-key (kbd "C-c c") #'org-capture) (global-set-key (kbd "C-c s") #'org-schedule) +(global-set-key (kbd "<f6>") 'org-clock-goto) +(global-set-key (kbd "<f9>") 'org-clock-in-last) +(global-set-key (kbd "<f10>") 'org-clock-out) +(global-set-key (kbd "<f12>") 'org-agenda) + ;; ORG STATES ;; (setq org-todo-keywords (quote ((sequence "TODO(t)" "MAYBE(m)" "NEXT(n)" "|" "DONE(d)") @@ -59,16 +78,16 @@ (setq org-log-done (quote time)) (setq org-log-redeadline (quote time)) (setq org-log-reschedule (quote time)) +(setq org-log-into-drawer t) ;; CAPTURE ;; -(setq org-default-notes-file org-refile-path) (setq org-capture-templates (quote ( ("w" "Todo" entry (file+headline org-refile-path "Tasks") - "* TODO " + "* TODO %?" :empty-lines 1) ("m" @@ -84,39 +103,14 @@ ("L" "Web Link" entry (file+headline org-refile-path "Read later") - "* %?[[%:link][%:description]] %(progn (setq rul/delete-frame-after-capture 1) \"\")\n %:initial\n \nCaptured On: %U" + "* %?[[%:link][%:description]]\n %:initial\n \nCaptured On: %U" ) ("l" "Web Link with Selection" entry (file+headline org-refile-path "Read later") - "* [[%:link][%:description]] %(progn (setq rul/delete-frame-after-capture 1) \"\")\n %:initial\n \nCaptured On: %U") - + "* [[%:link][%:description]]\n %:initial\n \nCaptured On: %U") ))) -(defvar rul/delete-frame-after-capture 0 "Whether to delete the last frame after the current capture") - -(defun rul/delete-frame-if-necessary () - "Delete the last frame if necessary." - (cond - ((= rul/delete-frame-after-capture 0) nil) - ((> rul/delete-frame-after-capture 1) - (setq rul/delete-frame-after-capture (- rul/delete-frame-after-capture 1))) - (t - (setq rul/delete-frame-after-capture 0) - (delete-frame)))) - -(defun rul/org-capture-before () - "Function to run before org capture." - (setq rul/delete-frame-after-capture (1+ rul/delete-frame-after-capture))) - -(defun rul/org-capture-after () - "Function to run after org capture." - (rul/delete-frame-if-necessary)) - -(advice-add 'org-capture-finalize :after 'rul/delete-frame-if-necessary) -(advice-add 'org-capture-kill :after 'rul/delete-frame-if-necessary) -(advice-add 'org-capture-refile :after 'rul/delete-frame-if-necessary) - ;; REFILE ;; ; Targets include this file and any file contributing to the agenda - up to 3 levels deep @@ -180,4 +174,39 @@ ;; Honor ATTR_ORG attribute. Defaults to image's width if not set. (setq org-image-actual-width nil) +(setq org-clock-mode-line-total 'today) + +;; org-tempus +(unless (package-installed-p 'org-tempus) + (package-vc-install "https://github.com/rul/org-tempus.git")) + +(use-package org-tempus + :init + (org-tempus-mode 1)) + +(use-package org-remark-global-tracking + :ensure org-remark + ;; `org-remark-global-tracking' is shipped by the `org-remark' + ;; package, so ensure the package exists before loading this feature. + :hook (after-init . org-remark-global-tracking-mode) + :config + ;; Selectively keep or comment out the following if you want to use + ;; extensions for Info-mode, EWW, and NOV.el (EPUB) respectively. + (use-package org-remark-eww :after eww :config (org-remark-eww-mode +1)) + (use-package org-remark-nov :after nov :config (org-remark-nov-mode +1))) + +(use-package org-remark + :ensure t + :bind (;; :bind keyword also implicitly defers org-remark itself. + ;; Keybindings before :map is set for global-map. Adjust the keybinds + ;; as you see fit. + ("C-c n m" . org-remark-mark) + ("C-c n l" . org-remark-mark-line) + :map org-remark-mode-map + ("C-c n o" . org-remark-open) + ("C-c n ]" . org-remark-view-next) + ("C-c n [" . org-remark-view-prev) + ("C-c n r" . org-remark-remove) + ("C-c n d" . org-remark-delete))) + (provide 'rul-org) diff --git a/.emacs.d/rul-lisp/packages/rul-prog.el b/.emacs.d/rul-lisp/packages/rul-prog.el index eb9c676..72625cb 100644 --- a/.emacs.d/rul-lisp/packages/rul-prog.el +++ b/.emacs.d/rul-lisp/packages/rul-prog.el @@ -1,14 +1,41 @@ +;;; rul-prog.el --- Configuration related to programming and markup +;;; languages (use-package eglot :ensure t) -;; Dart -(setq dart-server-format-on-save t) -(add-hook 'dart-mode-hook 'lsp) +;; Go +(use-package go-mode + :ensure t + :init + (progn + (bind-key [remap find-tag] #'godef-jump)) + :config + (add-hook 'go-mode-hook 'electric-pair-mode) + (add-hook 'before-save-hook 'gofmt-before-save)) -(setq gc-cons-threshold (* 100 1024 1024) - read-process-output-max (* 1024 1024)) +(use-package go-eldoc + :ensure t + :init + (add-hook 'go-mode-hook 'go-eldoc-setup)) + +;; Latex +(add-hook 'latex-mode-hook 'flyspell-mode) +(setq TeX-PDF-mode t) + +(defun pdfevince () + (add-to-list 'TeX-output-view-style + '("^pdf$" "." "evince %o %(outpage)"))) + +(add-hook 'LaTeX-mode-hook 'pdfevince t) ; AUCTeX LaTeX mode + +;; Markdown +(use-package markdown-mode + :ensure t + :config + (setq auto-mode-alist + (cons '("\\.mdwn" . markdown-mode) auto-mode-alist))) ;; Python -(use-package blacken :ensure t) +(use-package blacken :ensure t :defer t) ;; Terraform (use-package terraform-mode :ensure t :defer t) @@ -16,4 +43,12 @@ ;; YAML (use-package yaml-mode :ensure t :defer t) +;; Rust +(use-package rust-mode + :defer t + :init + (setq rust-mode-treesitter-derive t) + :config + (add-hook 'rust-mode-hook 'eglot-ensure)) + (provide 'rul-prog) diff --git a/.emacs.d/rul-lisp/packages/rul-terminals.el b/.emacs.d/rul-lisp/packages/rul-terminals.el new file mode 100644 index 0000000..f6a8913 --- /dev/null +++ b/.emacs.d/rul-lisp/packages/rul-terminals.el @@ -0,0 +1,54 @@ +(use-package vterm + :ensure t + :init + (setq vterm-always-compile-module t + vterm-max-scrollback 100000) + :hook + (vterm-mode . goto-address-mode) + :bind + (:map vterm-mode-map + ("C-c C-t" . vterm-copy-mode) + ("C-l" . vterm-clear)) + :config + (define-key vterm-mode-map (kbd "C-c C-c") + (lambda () + (interactive) + (vterm-send-string "\C-c"))) + + (defun rul/vterm-copy-and-exit (beg end) + "Copy region and exit `vterm-copy-mode'." + (interactive "r") + (kill-ring-save beg end) + (vterm-copy-mode -1)) + + (define-key vterm-copy-mode-map (kbd "w") #'rul/vterm-copy-and-exit) + (define-key vterm-copy-mode-map (kbd "M-w") #'rul/vterm-copy-and-exit)) + +(use-package multi-vterm + :ensure t + :after vterm + :bind (("C-c t" . multi-vterm)) + :config + (setq vterm-kill-buffer-on-exit t) + + (defvar-local rul/vterm-close-tab-on-kill nil + "When non-nil, close this buffer's tab when the vterm buffer is killed.") + + (defun rul/vterm-maybe-close-tab () + "Close the current tab if this vterm buffer was opened in its own tab." + (when rul/vterm-close-tab-on-kill + (tab-close))) + + (defun rul/vterm-new-tab () + "Create a new tab and open a new vterm." + (interactive) + (tab-new) + (multi-vterm) + (setq-local rul/vterm-close-tab-on-kill t) + (add-hook 'kill-buffer-hook #'rul/vterm-maybe-close-tab nil t)) + + ;; Inside vterm buffers, make C-c t spawn a new tab + vterm + (define-key vterm-mode-map (kbd "C-c t") #'rul/vterm-new-tab) + (define-key vterm-mode-map (kbd "C-S-t") #'rul/vterm-new-tab)) + +(provide 'rul-terminals) diff --git a/.emacs.d/rul-init.d/themes.el b/.emacs.d/rul-lisp/packages/rul-themes.el index d5c591a..2c696ec 100644 --- a/.emacs.d/rul-init.d/themes.el +++ b/.emacs.d/rul-lisp/packages/rul-themes.el @@ -1,17 +1,17 @@ -(use-package modus-themes :ensure t) (use-package ef-themes :ensure t) - -(setq - modus-themes-mode-line '(accented borderless padded) - modus-themes-region '(bg-only) - modus-themes-bold-constructs t - modus-themes-italic-constructs t - modus-themes-paren-match '(bold intense) - modus-themes-headings (quote ((1 . (rainbow variable-pitch 1.3)) - (2 . (rainbow 1.1)) - (t . (rainbow)))) - modus-themes-org-blocks 'tinted - ) +(use-package modus-themes + :ensure t + :config + (setq + modus-themes-mode-line '(accented borderless padded) + modus-themes-region '(bg-only) + modus-themes-bold-constructs t + modus-themes-italic-constructs t + modus-themes-paren-match '(bold intense) + modus-themes-headings (quote ((1 . (rainbow variable-pitch 1.3)) + (2 . (rainbow 1.1)) + (t . (rainbow)))) + modus-themes-org-blocks 'tinted)) (use-package dbus) (defun mf/set-theme-from-dbus-value (value) @@ -19,9 +19,9 @@ (message "value is %s" value) (if (equal value '1) (progn (message "Switch to dark theme") - (modus-themes-select 'modus-vivendi-tinted)) + (modus-themes-select 'modus-vivendi)) (progn (message "Switch to light theme") - (modus-themes-select 'modus-operandi-tinted)))) + (modus-themes-select 'modus-operandi)))) (defun mf/color-scheme-changed (path var value) "DBus handler to detect when the color-scheme has changed." @@ -46,3 +46,6 @@ "org.freedesktop.appearance" "color-scheme" ) + + +(provide 'rul-themes) diff --git a/.emacs.d/rul-lisp/packages/rul-vc.el b/.emacs.d/rul-lisp/packages/rul-vc.el new file mode 100644 index 0000000..8551647 --- /dev/null +++ b/.emacs.d/rul-lisp/packages/rul-vc.el @@ -0,0 +1,33 @@ +;;; rul-vc.el --- Version control configuration -*- lexical-binding: t; -*- + +(setq vc-follow-symlinks nil) + +(use-package magit + :ensure t + :bind (("C-c g s" . magit-status) + ("C-c g F" . magit-pull-from-upstream) + ("C-c g b" . magit-blame)) + :hook (git-commit-setup . rul/git-commit-setup) + :config + (defun rul/git-commit-setup () + "Enable useful text modes for Git commit buffers." + (flyspell-mode 1) + (auto-fill-mode 1)) + + (defun rul/magit-status-save-window-config (&rest _) + "Save current window configuration before invoking `magit-status'." + (window-configuration-to-register :magit-fullscreen)) + + (defun rul/magit-status-single-window (&rest _) + "Display `magit-status' in a single window." + (delete-other-windows)) + + (advice-add 'magit-status :before #'rul/magit-status-save-window-config) + (advice-add 'magit-status :after #'rul/magit-status-single-window)) + +(with-eval-after-load 'project + (add-to-list 'project-switch-commands + '(magit-project-status "Magit" "m"))) + +(provide 'rul-vc) +;;; rul-vc.el ends here diff --git a/.emacs.d/rul-lisp/packages/rul-wm.el b/.emacs.d/rul-lisp/packages/rul-wm.el index cea3a76..90bd031 100644 --- a/.emacs.d/rul-lisp/packages/rul-wm.el +++ b/.emacs.d/rul-lisp/packages/rul-wm.el @@ -6,6 +6,14 @@ (or (derived-mode-p 'org-mode 'org-agenda-mode) (member (buffer-file-name) (org-agenda-files))))) +;; Side window for dictionary +(setq switch-to-buffer-obey-display-actions t) +(add-to-list 'display-buffer-alist + '("^\\*Dictionary\\*" display-buffer-in-side-window + (side . bottom) + (window-height . 12) + )) + ;;;; tab-bar.el (let ((map global-map)) (define-key map (kbd "C-<next>") 'tab-bar-switch-to-next-tab) @@ -21,14 +29,131 @@ (setq tab-bar-new-tab-to 'rightmost) (setq tab-bar-close-button-show nil) (set-face-attribute 'tab-bar nil :height 0.8) -(tab-bar-mode 1) + +;; I've moved to a frame oriented workflow, so I no longer use tabs. +;; (tab-bar-mode 1) + +;; Pop-up buffers +;; https://protesilaos.com/codelog/2024-09-19-emacs-command-popup-frame-emacsclient/ +(defun prot-window-delete-popup-frame (&rest _) + "Kill selected selected frame if it has parameter `prot-window-popup-frame'. +Use this function via a hook." + (when (frame-parameter nil 'prot-window-popup-frame) + (delete-frame))) + +(defmacro prot-window-define-with-popup-frame (command) + "Define interactive function which calls COMMAND in a new frame. +Make the new frame have the `prot-window-popup-frame' parameter." + `(defun ,(intern (format "prot-window-popup-%s" command)) () + ,(format "Run `%s' in a popup frame with `prot-window-popup-frame' parameter. +Also see `prot-window-delete-popup-frame'." command) + (interactive) + (let ((frame (make-frame '((prot-window-popup-frame . t))))) + (select-frame frame) + ;; Placeholder for frame, otherwise it'll get autoclosed. + (switch-to-buffer " prot-window-hidden-buffer-for-popup-frame") + (condition-case nil + (call-interactively ',command) + ((quit error user-error) + (delete-frame frame)))))) + +(declare-function org-capture "org-capture" (&optional goto keys)) +(defvar org-capture-after-finalize-hook) + +;;;###autoload (autoload 'prot-window-popup-org-capture "prot-window") +(prot-window-define-with-popup-frame org-capture) + +(add-hook 'org-capture-after-finalize-hook #'prot-window-delete-popup-frame) (use-package olivetti :ensure t :defer t :config - (setq - olivetti-body-width 100 - )) + (setq olivetti-body-width 100)) + +(use-package logos +:ensure t +:config + +;; If you want to use outlines instead of page breaks (the ^L) +(setq logos-outlines-are-pages t) +(setq logos-outline-regexp-alist + `((emacs-lisp-mode . "^;;;+ ") + (org-mode . "^\\*+ +") + (markdown-mode . "^\\#+ +") + )) + +;; These apply when `logos-focus-mode' is enabled. Their value is +;; buffer-local. +(setq-default logos-hide-mode-line t + logos-hide-buffer-boundaries t + logos-hide-fringe t + logos-variable-pitch nil + logos-buffer-read-only nil + logos-scroll-lock nil + logos-olivetti t + olivetti-body-width 100 + ) + + +(let ((map global-map)) + (define-key map [remap narrow-to-region] #'logos-narrow-dwim) + (define-key map [remap forward-page] #'logos-forward-page-dwim) + (define-key map [remap backward-page] #'logos-backward-page-dwim) + (define-key map (kbd "<f7>") #'logos-focus-mode)) +) + +(use-package beframe + :ensure t + :hook (after-init . beframe-mode) + :config + (setq beframe-functions-in-frames '(project-prompt-project-dir)) + (setq beframe-global-buffers nil) + (define-key global-map (kbd "C-c b") beframe-prefix-map) + + ;;Integration with Consult + (defvar consult-buffer-sources) + (declare-function consult--buffer-state "consult") + + (with-eval-after-load 'consult + (defface beframe-buffer + '((t :inherit font-lock-string-face)) + "Face for `consult' framed buffers.") + + (defun my-beframe-buffer-names-sorted (&optional frame) + "Return the list of buffers from `beframe-buffer-names' sorted by visibility. +With optional argument FRAME, return the list of buffers of FRAME." + (beframe-buffer-names frame :sort #'beframe-buffer-sort-visibility)) + + (defvar beframe-consult-source + `( :name "Frame-specific buffers (current frame)" + :narrow ?F + :category buffer + :face beframe-buffer + :history beframe-history + :items ,#'my-beframe-buffer-names-sorted + :action ,#'switch-to-buffer + :state ,#'consult--buffer-state)) + + (add-to-list 'consult-buffer-sources 'beframe-consult-source))) + +(defun kill-project-buffers-and-close-frame () + (interactive) + (project-kill-buffers) + (delete-frame (selected-frame))) + +(define-key global-map (kbd "C-x p K") 'kill-project-buffers-and-close-frame) + +(add-hook 'text-mode-hook 'context-menu-mode) + +(defun my-context-menu (menu click) + "My context menu" + (define-key-after menu [dictionary-lookup] + '(menu-item "Dict" dictionary-search-word-at-mouse + :help "Look up in dictionary")) + menu) + +;; hook into context menu +(add-hook 'context-menu-functions #'my-context-menu) (provide 'rul-wm) diff --git a/.emacs.d/rul-lisp/packages/rul-write.el b/.emacs.d/rul-lisp/packages/rul-write.el index 16eae84..7d37e95 100644 --- a/.emacs.d/rul-lisp/packages/rul-write.el +++ b/.emacs.d/rul-lisp/packages/rul-write.el @@ -1,5 +1,5 @@ ;;;; `dictionary' -(setq dictionary-server "dict.org" +(setq dictionary-server "localhost" dictionary-default-popup-strategy "lev" dictionary-create-buttons nil dictionary-use-single-buffer t) @@ -7,7 +7,14 @@ (use-package denote :ensure t + :hook (dired-mode . denote-dired-mode) + :bind + (("C-c n n" . denote) + ("C-c n r" . denote-rename-file) + ("C-c n l" . denote-link) + ("C-c n b" . denote-backlinks)) :config + (denote-rename-buffer-mode 1) (setq denote-infer-keywords t) (setq denote-sort-keywords t) (setq denote-file-type 'org) @@ -16,34 +23,72 @@ (setq denote-link-fontify-backlinks t) (setq denote-rename-no-confirm t) - (add-hook 'find-file-hook #'denote-link-buttonize-buffer) - (add-hook 'dired-mode-hook #'denote-dired-mode-in-directories) - (let ((map global-map)) - (define-key map (kbd "C-c n j") #'rul/denote-journal) + (define-key map (kbd "C-c n j") #'denote-journal-new-or-existing-entry) (define-key map (kbd "C-c n n") #'denote) (define-key map (kbd "C-c n f") #'denote-open-or-create) (define-key map (kbd "C-c n i") #'denote-link) (define-key map (kbd "C-c n r") #'denote-rename-file) - )) - -(defun rul/denote-journal () - "Create an entry tagged 'journal' with the date as its title. -If a journal for the current day exists, visit it. If multiple -entries exist, prompt with completion for a choice between them. -Else create a new file." - (interactive) - (let* ((today (format-time-string "%A %e %B %Y")) - (string (denote-sluggify today "title")) - (files (denote-directory-files-matching-regexp string))) - (cond - ((> (length files) 1) - (find-file (completing-read "Select file: " files nil :require-match))) - (files - (find-file (car files))) - (t - (denote - today - '("journal")))))) + ) +) + +(use-package electric + :init + (setq electric-quote-replace-double t) + :hook + (message-mode . electric-quote-local-mode)) + +(use-package message + :init + (setq mml-enable-flowed nil) + :hook + (message-mode . my/message-mode-setup)) + +(defun my/message-mode-setup () + (setq fill-column 72 + sentence-end-double-space nil) + (auto-fill-mode -1) + (visual-line-mode 1) + (when (require 'olivetti nil t) + (setq-local olivetti-body-width 100) + (olivetti-mode 1))) + +;; Flycheck +(use-package flycheck + :ensure t + :config + +(flycheck-define-checker proselint + "A linter for prose." + :command ("proselint" source-inplace) + :error-patterns + ((warning line-start (file-name) ":" line ":" column ": " + (id (one-or-more (not (any " ")))) + (message) line-end)) + :modes (text-mode markdown-mode gfm-mode org-mode)) + +(add-to-list 'flycheck-checkers 'proselint) + +;; TODO: docker run --rm -p 8010:8010 erikvl87/languagetool +(use-package flycheck-languagetool + :ensure t + :hook (message-mode . flycheck-languagetool-setup) + :init + (setq flycheck-languagetool-url "http://localhost:8010") +)) + +;; Flyspell +(defcustom flyspell-delayed-commands nil + "List of commands that are \"delayed\" for Flyspell mode. +After these commands, Flyspell checking is delayed for a short time, +whose length is specified by `flyspell-delay'." + :group 'flyspell + :type '(repeat (symbol))) + +(setq ispell-dictionary "en") +(setq flyspell-default-dictionary "en") + +(setq flyspell-issue-welcome-flag nil) +(setq-default ispell-list-command "list") (provide 'rul-write) diff --git a/.environment.d/00-languages b/.environment.d/00-languages index e3ad179..3bba1f4 100644 --- a/.environment.d/00-languages +++ b/.environment.d/00-languages @@ -12,6 +12,10 @@ if [ -d "$HOME/.cargo/bin" ]; then PATH="$HOME/.cargo/bin:$PATH" fi +if [ -d "$HOME/.npm-global/bin" ]; then + PATH="$HOME/.npm-global/bin:$PATH" +fi + # Local Go installation if [ -d "/usr/local/go/bin" ]; then PATH="/usr/local/go/bin:$PATH" diff --git a/.inputrc b/.inputrc new file mode 100644 index 0000000..a8e424b --- /dev/null +++ b/.inputrc @@ -0,0 +1,2 @@ +"\M-\C-f": shell-forward-word +"\M-\C-b": shell-backward-word
\ No newline at end of file diff --git a/.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/extension.js b/.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/extension.js new file mode 100644 index 0000000..b92048c --- /dev/null +++ b/.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/extension.js @@ -0,0 +1,548 @@ +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; +import St from 'gi://St'; +import Clutter from 'gi://Clutter'; +import Pango from 'gi://Pango'; + +import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js'; +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js'; +import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js'; + +const SNAPSHOT_RELATIVE_PATH = '.cache/org-agenda-shell/today.json'; +const REFRESH_INTERVAL_SECONDS = 60; +const RELOAD_DELAY_MS = 200; +const PANEL_TITLE_MAX = 20; + +const InfoMenuItem = GObject.registerClass( +class InfoMenuItem extends PopupMenu.PopupBaseMenuItem { + _init(primaryText, secondaryText = '', {overdue = false} = {}) { + super._init({reactive: false, can_focus: false}); + + const box = new St.BoxLayout({ + vertical: true, + x_expand: true, + style_class: 'org-agenda-indicator-row', + }); + + this._primaryLabel = new St.Label({ + text: primaryText, + x_expand: true, + style_class: 'org-agenda-indicator-row-title', + }); + this._primaryLabel.clutter_text.ellipsize = Pango.EllipsizeMode.END; + + if (overdue) + this._primaryLabel.add_style_class_name('org-agenda-indicator-overdue'); + + box.add_child(this._primaryLabel); + + if (secondaryText) { + this._secondaryLabel = new St.Label({ + text: secondaryText, + x_expand: true, + style_class: 'org-agenda-indicator-row-meta', + }); + this._secondaryLabel.clutter_text.ellipsize = Pango.EllipsizeMode.END; + box.add_child(this._secondaryLabel); + } + + this.add_child(box); + } +}); + +const SectionHeaderItem = GObject.registerClass( +class SectionHeaderItem extends PopupMenu.PopupBaseMenuItem { + _init(text) { + super._init({reactive: false, can_focus: false}); + + const label = new St.Label({ + text, + x_expand: true, + style_class: 'org-agenda-indicator-section-label', + }); + const box = new St.BoxLayout({ + x_expand: true, + style_class: 'org-agenda-indicator-section', + }); + + box.add_child(label); + this.add_child(box); + } +}); + +const Indicator = GObject.registerClass( +class Indicator extends PanelMenu.Button { + _init(settings) { + super._init(0.0, 'Org Agenda Indicator'); + + this._settings = settings; + this._snapshotPath = GLib.build_filenamev([ + GLib.get_home_dir(), + SNAPSHOT_RELATIVE_PATH, + ]); + this._snapshotDir = GLib.path_get_dirname(this._snapshotPath); + this._snapshotParentDir = GLib.path_get_dirname(this._snapshotDir); + this._snapshotDirBaseName = GLib.path_get_basename(this._snapshotDir); + this._snapshotBaseName = GLib.path_get_basename(this._snapshotPath); + this._directoryMonitor = null; + this._monitoredDirectory = null; + this._refreshSourceId = 0; + this._reloadSourceId = 0; + this._state = { + status: 'loading', + taskCount: 0, + todayCount: 0, + overdueCount: 0, + date: null, + generatedAt: null, + generatedEpoch: null, + tasks: [], + clockedIn: null, + error: null, + }; + + this._label = new St.Label({ + text: '...', + y_align: Clutter.ActorAlign.CENTER, + name: 'org-agenda-indicator-label', + }); + this._label.clutter_text.ellipsize = Pango.EllipsizeMode.END; + this.add_child(this._label); + + this._showClockedInChangedId = this._settings.connect( + 'changed::show-clocked-in-task', + () => { + this._syncLabel(); + this._rebuildMenu(); + }); + + this._refreshSourceId = GLib.timeout_add_seconds( + GLib.PRIORITY_DEFAULT, + REFRESH_INTERVAL_SECONDS, + () => { + this._loadSnapshot(); + return GLib.SOURCE_CONTINUE; + }); + + this._startMonitoring(); + this._loadSnapshot(); + } + + destroy() { + if (this._reloadSourceId) { + GLib.source_remove(this._reloadSourceId); + this._reloadSourceId = 0; + } + + if (this._refreshSourceId) { + GLib.source_remove(this._refreshSourceId); + this._refreshSourceId = 0; + } + + this._directoryMonitor?.cancel(); + this._directoryMonitor = null; + if (this._showClockedInChangedId) { + this._settings.disconnect(this._showClockedInChangedId); + this._showClockedInChangedId = 0; + } + + super.destroy(); + } + + _startMonitoring() { + this._directoryMonitor?.cancel(); + this._directoryMonitor = null; + + const targetDirectory = GLib.file_test(this._snapshotDir, GLib.FileTest.IS_DIR) + ? this._snapshotDir + : this._snapshotParentDir; + const directory = Gio.File.new_for_path(targetDirectory); + + try { + this._directoryMonitor = directory.monitor_directory( + Gio.FileMonitorFlags.WATCH_MOVES, + null); + this._monitoredDirectory = targetDirectory; + this._directoryMonitor.connect('changed', (_monitor, file, otherFile, eventType) => { + if (!this._eventTouchesSnapshot(file, otherFile, eventType)) + return; + + this._scheduleReload(); + }); + } catch (error) { + this._monitoredDirectory = null; + this._setState({ + status: 'error', + taskCount: 0, + todayCount: 0, + overdueCount: 0, + date: null, + generatedAt: null, + generatedEpoch: null, + tasks: [], + clockedIn: null, + error: `Unable to monitor ${this._snapshotDir}: ${error.message}`, + }); + } + } + + _eventTouchesSnapshot(file, otherFile, eventType) { + const relevantEvents = new Set([ + Gio.FileMonitorEvent.CHANGED, + Gio.FileMonitorEvent.CHANGES_DONE_HINT, + Gio.FileMonitorEvent.CREATED, + Gio.FileMonitorEvent.MOVED_IN, + Gio.FileMonitorEvent.MOVED_OUT, + Gio.FileMonitorEvent.DELETED, + Gio.FileMonitorEvent.ATTRIBUTE_CHANGED, + ]); + + if (!relevantEvents.has(eventType)) + return false; + + const names = [ + file?.get_basename(), + otherFile?.get_basename(), + ]; + + if (this._monitoredDirectory === this._snapshotParentDir) + return names.includes(this._snapshotDirBaseName); + + return names.includes(this._snapshotBaseName); + } + + _scheduleReload() { + if (this._reloadSourceId) + return; + + this._reloadSourceId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + RELOAD_DELAY_MS, + () => { + this._reloadSourceId = 0; + this._loadSnapshot(); + return GLib.SOURCE_REMOVE; + }); + } + + _loadSnapshot() { + const snapshotFile = Gio.File.new_for_path(this._snapshotPath); + + if ((this._monitoredDirectory === this._snapshotParentDir && + GLib.file_test(this._snapshotDir, GLib.FileTest.IS_DIR)) || + (this._monitoredDirectory === this._snapshotDir && + !GLib.file_test(this._snapshotDir, GLib.FileTest.IS_DIR))) + this._startMonitoring(); + + try { + if (!snapshotFile.query_exists(null)) { + this._setState({ + status: 'missing', + taskCount: 0, + todayCount: 0, + overdueCount: 0, + date: null, + generatedAt: null, + generatedEpoch: null, + tasks: [], + clockedIn: null, + error: null, + }); + return; + } + + const [, contents] = snapshotFile.load_contents(null); + const payload = JSON.parse(new TextDecoder().decode(contents)); + const currentDate = GLib.DateTime.new_now_local().format('%F'); + + if (payload.date !== currentDate) { + this._setState({ + status: 'stale', + taskCount: 0, + todayCount: 0, + overdueCount: 0, + date: payload.date ?? null, + generatedAt: payload.generated_at ?? null, + generatedEpoch: payload.generated_epoch ?? null, + tasks: [], + clockedIn: payload.clocked_in ?? null, + error: null, + }); + return; + } + + const tasks = Array.isArray(payload.today_tasks) ? payload.today_tasks : []; + const overdueCount = Number(payload.overdue_count ?? tasks.filter(task => task.is_overdue).length); + const todayCount = Number(payload.today_count ?? tasks.length - overdueCount); + + this._setState({ + status: 'ready', + taskCount: Number(payload.task_count ?? tasks.length), + todayCount, + overdueCount, + date: payload.date ?? null, + generatedAt: payload.generated_at ?? null, + generatedEpoch: payload.generated_epoch ?? null, + tasks, + clockedIn: payload.clocked_in ?? null, + error: null, + }); + } catch (error) { + this._setState({ + status: 'error', + taskCount: 0, + todayCount: 0, + overdueCount: 0, + date: null, + generatedAt: null, + generatedEpoch: null, + tasks: [], + clockedIn: null, + error: error.message, + }); + } + } + + _setState(nextState) { + this._state = nextState; + this._syncLabel(); + this._rebuildMenu(); + } + + _syncLabel() { + switch (this._state.status) { + case 'ready': + this._label.set_text(this._panelText()); + break; + case 'loading': + this._label.set_text('...'); + break; + case 'missing': + this._label.set_text('-'); + break; + default: + this._label.set_text('?'); + break; + } + } + + _rebuildMenu() { + this.menu.removeAll(); + + this.menu.addMenuItem(new InfoMenuItem(this._summaryText(), this._summaryMetaText())); + + if (this._state.error) { + this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + this.menu.addMenuItem(new InfoMenuItem('Unable to read agenda snapshot', this._state.error)); + return; + } + + if (this._state.status !== 'ready') + return; + + const todayTasks = this._state.tasks.filter(task => !task.is_overdue); + const overdueTasks = this._state.tasks.filter(task => task.is_overdue); + + if (this._showClockedInTask() && this._state.clockedIn) { + this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + this.menu.addMenuItem(new SectionHeaderItem('Clocked In')); + this.menu.addMenuItem(this._clockedInItem(this._state.clockedIn)); + } + + if (todayTasks.length > 0 || overdueTasks.length > 0) { + this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + } + + if (todayTasks.length > 0) { + this.menu.addMenuItem(new SectionHeaderItem('Today')); + for (const task of todayTasks) + this.menu.addMenuItem(this._taskItem(task)); + } + + if (overdueTasks.length > 0) { + if (todayTasks.length > 0) + this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + + this.menu.addMenuItem(new SectionHeaderItem('Overdue')); + for (const task of overdueTasks) + this.menu.addMenuItem(this._taskItem(task)); + } + } + + _panelText() { + const compactCount = this._compactCountText(); + + if (this._showClockedInTask() && this._state.clockedIn?.title) { + const title = this._truncate(this._state.clockedIn.title, PANEL_TITLE_MAX); + return compactCount ? `● ${title} · ${compactCount}` : `● ${title}`; + } + + return compactCount; + } + + _compactCountText() { + const {todayCount, overdueCount} = this._state; + + if (overdueCount > 0 && todayCount > 0) + return `${todayCount}+${overdueCount}!`; + + if (overdueCount > 0) + return `${overdueCount}!`; + + return String(todayCount); + } + + _summaryText() { + switch (this._state.status) { + case 'ready': + if (this._state.taskCount === 0) + return this._state.clockedIn ? 'Clock is running' : 'No scheduled tasks'; + + return this._countPhrase(this._state.todayCount, 'today') + + (this._state.overdueCount > 0 ? `, ${this._countPhrase(this._state.overdueCount, 'overdue')}` : ''); + case 'missing': + return 'Agenda snapshot not found'; + case 'stale': + return `Agenda snapshot is stale (${this._state.date ?? 'unknown date'})`; + case 'loading': + return 'Loading org agenda snapshot'; + default: + return 'Unable to read agenda snapshot'; + } + } + + _summaryMetaText() { + const parts = []; + + if (this._showClockedInTask() && this._state.clockedIn) + parts.push(this._clockSummaryText(this._state.clockedIn)); + + if (this._state.generatedEpoch) + parts.push(`Updated ${this._timeOfDay(this._state.generatedEpoch)}`); + else if (this._state.generatedAt) + parts.push(`Updated ${this._state.generatedAt}`); + + return parts.join(' '); + } + + _showClockedInTask() { + return this._settings.get_boolean('show-clocked-in-task'); + } + + _countPhrase(count, label) { + if (count === 1) + return `1 ${label}`; + + return `${count} ${label}`; + } + + _clockedInItem(task) { + return new InfoMenuItem( + task.title ?? 'Clock running', + this._clockTaskMeta(task)); + } + + _taskItem(task) { + return new InfoMenuItem( + this._taskPrimaryText(task), + this._taskMetaText(task), + {overdue: task.is_overdue}); + } + + _taskPrimaryText(task) { + const pieces = []; + + if (task.time) + pieces.push(task.time); + + pieces.push(task.title ?? 'Untitled task'); + + return pieces.join(' '); + } + + _taskMetaText(task) { + const pieces = []; + + if (task.state) + pieces.push(task.state); + + if (task.category) + pieces.push(task.category); + + if (task.is_overdue && task.scheduled_for) + pieces.push(`scheduled ${this._friendlyDate(task.scheduled_for)}`); + + return pieces.join(' · '); + } + + _clockTaskMeta(task) { + const pieces = []; + + if (task.started_epoch) + pieces.push(`started ${this._timeOfDay(task.started_epoch)}`); + + const elapsed = this._elapsedClockText(task.started_epoch); + if (elapsed) + pieces.push(elapsed); + + if (task.state) + pieces.push(task.state); + + if (task.category) + pieces.push(task.category); + + return pieces.join(' · '); + } + + _clockSummaryText(task) { + const elapsed = this._elapsedClockText(task.started_epoch); + return elapsed ? `Clock ${elapsed}` : 'Clock running'; + } + + _elapsedClockText(startedEpoch) { + if (!startedEpoch) + return ''; + + const roundedMinutes = Math.max(0, Math.floor((Date.now() / 1000 - startedEpoch) / 60)); + const hours = Math.floor(roundedMinutes / 60); + const remainder = roundedMinutes % 60; + + if (hours > 0) + return `${hours}h ${remainder}m`; + + return `${roundedMinutes}m`; + } + + _friendlyDate(dateText) { + const date = GLib.DateTime.new_from_iso8601( + `${dateText}T00:00:00`, + GLib.TimeZone.new_local()); + return date ? date.format('%b %-d') : dateText; + } + + _timeOfDay(epoch) { + const date = GLib.DateTime.new_from_unix_local(epoch); + return date ? date.format('%H:%M') : ''; + } + + _truncate(text, maxLength) { + if (!text || text.length <= maxLength) + return text ?? ''; + + return `${text.slice(0, maxLength - 1)}…`; + } +}); + +export default class OrgAgendaIndicatorExtension extends Extension { + enable() { + this._indicator = new Indicator(this.getSettings()); + Main.panel.addToStatusArea(this.uuid, this._indicator); + } + + disable() { + this._indicator?.destroy(); + this._indicator = null; + } +} diff --git a/.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/metadata.json b/.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/metadata.json new file mode 100644 index 0000000..d24f67c --- /dev/null +++ b/.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/metadata.json @@ -0,0 +1,11 @@ +{ + "uuid": "org-agenda-indicator@rbenencia.name", + "extension-id": "org-agenda-indicator", + "name": "Org Agenda Indicator", + "description": "Show today's org-agenda tasks in the top bar.", + "settings-schema": "org.gnome.shell.extensions.org-agenda-indicator", + "shell-version": ["46", "47", "48", "49"], + "session-modes": ["user"], + "url": "https://rbenencia.name", + "version": 1 +} diff --git a/.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/prefs.js b/.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/prefs.js new file mode 100644 index 0000000..08db40d --- /dev/null +++ b/.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/prefs.js @@ -0,0 +1,33 @@ +import Adw from 'gi://Adw'; +import Gtk from 'gi://Gtk'; + +import {ExtensionPreferences} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; + +export default class OrgAgendaIndicatorPreferences extends ExtensionPreferences { + fillPreferencesWindow(window) { + const settings = this.getSettings('org.gnome.shell.extensions.org-agenda-indicator'); + const page = new Adw.PreferencesPage(); + const group = new Adw.PreferencesGroup({ + title: 'Display', + description: 'Configure what the indicator shows.', + }); + const row = new Adw.ActionRow({ + title: 'Show clocked-in task', + subtitle: 'Display the active Org clock in the panel and menu when one is running.', + }); + const toggle = new Gtk.Switch({ + active: settings.get_boolean('show-clocked-in-task'), + valign: Gtk.Align.CENTER, + }); + + toggle.connect('notify::active', widget => { + settings.set_boolean('show-clocked-in-task', widget.get_active()); + }); + + row.add_suffix(toggle); + row.activatable_widget = toggle; + group.add(row); + page.add(group); + window.add(page); + } +} diff --git a/.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/schemas/org.gnome.shell.extensions.org-agenda-indicator.gschema.xml b/.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/schemas/org.gnome.shell.extensions.org-agenda-indicator.gschema.xml new file mode 100644 index 0000000..317b9a1 --- /dev/null +++ b/.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/schemas/org.gnome.shell.extensions.org-agenda-indicator.gschema.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<schemalist> + <schema id="org.gnome.shell.extensions.org-agenda-indicator" + path="/org/gnome/shell/extensions/org-agenda-indicator/"> + <key name="show-clocked-in-task" type="b"> + <default>true</default> + <summary>Show clocked-in task</summary> + <description> + When enabled, the indicator displays the active Org clock in the panel + and in a dedicated menu section. + </description> + </key> + </schema> +</schemalist> diff --git a/.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/stylesheet.css b/.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/stylesheet.css new file mode 100644 index 0000000..ed690bb --- /dev/null +++ b/.local/share/gnome-shell/extensions/org-agenda-indicator@rbenencia.name/stylesheet.css @@ -0,0 +1,38 @@ +#org-agenda-indicator-label { + margin: 0 6px; + max-width: 24em; +} + +.org-agenda-indicator-overdue { + font-weight: 600; +} + +.org-agenda-indicator-section { + padding: 6px 12px 2px; +} + +.org-agenda-indicator-section-label { + font-size: 0.82em; + font-weight: 700; + letter-spacing: 0.06em; + opacity: 0.7; + text-transform: uppercase; +} + +.org-agenda-indicator-row { + min-width: 22em; + padding: 3px 0; +} + +.org-agenda-indicator-row-title { + font-weight: 600; +} + +.org-agenda-indicator-row-meta { + font-size: 0.9em; + opacity: 0.75; +} + +.org-agenda-indicator-row-title.org-agenda-indicator-overdue { + color: #d97706; +} diff --git a/.local/share/gnome-shell/extensions/workspace-router@rbenencia.name/extension.js b/.local/share/gnome-shell/extensions/workspace-router@rbenencia.name/extension.js new file mode 100644 index 0000000..74f6cdf --- /dev/null +++ b/.local/share/gnome-shell/extensions/workspace-router@rbenencia.name/extension.js @@ -0,0 +1,285 @@ +import GLib from 'gi://GLib'; +import Meta from 'gi://Meta'; +import Shell from 'gi://Shell'; + +import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js'; +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; + +const EVALUATE_DELAY_MS = 150; +const ROUTABLE_WINDOW_TYPES = new Set([ + Meta.WindowType.NORMAL, + Meta.WindowType.DIALOG, + Meta.WindowType.MODAL_DIALOG, +]); +const WINDOW_RULES = [ + { + name: 'main-frame', + workspace: 0, + monitor: 'primary', + matchAll: [ + {field: 'appId', pattern: /emacs/i}, + {field: 'title', pattern: /^main$/i}, + ], + }, + { + name: 'browsers', + workspace: 1, + monitor: 'primary', + matchAny: [ + {field: 'appId', pattern: /(firefox|chromium|chrome)/i}, + {field: 'wmClass', pattern: /(firefox|chromium|chrome)/i}, + {field: 'wmClassInstance', pattern: /(firefox|chromium|chrome)/i}, + ], + }, + { + name: 'communications-frame', + workspace: 2, + monitor: 'primary', + matchAll: [ + {field: 'appId', pattern: /emacs/i}, + {field: 'title', pattern: /^communications$/i}, + ], + }, + { + name: 'communications-apps', + workspace: 2, + monitor: 'primary', + matchAny: [ + {field: 'title', pattern: /(ebex|lack|communications|notmuch|outlook|elfeed|thunderbird)/i}, + {field: 'appId', pattern: /(slack|outlook|thunderbird)/i}, + {field: 'wmClass', pattern: /(slack|outlook|thunderbird)/i}, + {field: 'wmClassInstance', pattern: /(slack|outlook|thunderbird)/i}, + ], + }, + { + name: 'terminals-frame', + workspace: 3, + monitor: 'primary', + matchAll: [ + {field: 'appId', pattern: /emacs/i}, + {field: 'title', pattern: /^terminals$/i}, + ], + }, + { + name: 'teleport', + workspace: 5, + monitor: 'primary', + matchAny: [ + {field: 'title', pattern: /teleport/i}, + {field: 'appId', pattern: /teleport/i}, + {field: 'wmClass', pattern: /teleport/i}, + {field: 'wmClassInstance', pattern: /teleport/i}, + ], + }, + { + name: 'terminals-apps', + workspace: 3, + monitor: 'primary', + matchAny: [ + {field: 'title', pattern: /(alacritty|kitty|terminal)/i}, + {field: 'appId', pattern: /(alacritty|kitty|console|terminal)/i}, + {field: 'wmClass', pattern: /(alacritty|kitty|terminal)/i}, + {field: 'wmClassInstance', pattern: /(alacritty|kitty|terminal)/i}, + ], + }, + { + name: 'misc', + workspace: 4, + monitor: 'primary', + matchAny: [ + {field: 'title', pattern: /(isco|eepa)/i}, + ], + }, + { + name: 'media', + workspace: 8, + monitor: 'primary', + matchAny: [ + {field: 'title', pattern: /(youtube|spotify)/i}, + {field: 'appId', pattern: /spotify/i}, + {field: 'wmClass', pattern: /spotify/i}, + {field: 'wmClassInstance', pattern: /spotify/i}, + ], + }, +]; + +function getWindowInfo(window, windowTracker) { + const app = windowTracker.get_window_app(window); + + return { + title: window.get_title() ?? '', + wmClass: window.get_wm_class() ?? '', + wmClassInstance: window.get_wm_class_instance() ?? '', + appId: app?.get_id() ?? '', + appName: app?.get_name() ?? '', + role: window.get_role() ?? '', + }; +} + +function matchCondition(info, condition) { + const value = info[condition.field] ?? ''; + return condition.pattern.test(value); +} + +function ruleMatches(info, rule) { + const matchAll = rule.matchAll ?? []; + const matchAny = rule.matchAny ?? []; + + if (!matchAll.every(condition => matchCondition(info, condition))) + return false; + + if (matchAny.length === 0) + return true; + + return matchAny.some(condition => matchCondition(info, condition)); +} + +function resolveMonitor(monitor) { + if (monitor === 'primary') + return Main.layoutManager.primaryIndex; + + if (Number.isInteger(monitor)) + return monitor; + + return null; +} + +export default class WorkspaceRouterExtension extends Extension { + enable() { + this._windowTracker = Shell.WindowTracker.get_default(); + this._trackedWindows = new Set(); + this._routedWindows = new Set(); + this._pendingEvaluations = new Map(); + + global.display.connectObject( + 'window-created', (_display, window) => this._trackWindow(window), + this); + + this._windowTracker.connectObject( + 'tracked-windows-changed', () => this._queueUnmatchedWindows(), + this); + + for (const actor of global.get_window_actors()) + this._trackWindow(actor.meta_window); + } + + disable() { + global.display.disconnectObject(this); + this._windowTracker?.disconnectObject(this); + + for (const [window, sourceId] of this._pendingEvaluations) { + GLib.source_remove(sourceId); + window.disconnectObject(this); + } + + for (const window of this._trackedWindows) + window.disconnectObject(this); + + this._pendingEvaluations.clear(); + this._trackedWindows.clear(); + this._routedWindows.clear(); + this._windowTracker = null; + } + + _trackWindow(window) { + if (!window || this._trackedWindows.has(window)) + return; + + if (!this._isRoutableWindow(window)) + return; + + this._trackedWindows.add(window); + + window.connectObject( + 'notify::title', () => this._queueWindow(window), + 'notify::wm-class', () => this._queueWindow(window), + 'unmanaged', () => this._cleanupWindow(window), + this); + + this._queueWindow(window); + } + + _queueUnmatchedWindows() { + for (const window of this._trackedWindows) + this._queueWindow(window); + } + + _queueWindow(window) { + if (!this._trackedWindows.has(window) || this._routedWindows.has(window)) + return; + + this._clearPendingEvaluation(window); + + const sourceId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + EVALUATE_DELAY_MS, + () => { + this._pendingEvaluations.delete(window); + this._routeWindow(window); + return GLib.SOURCE_REMOVE; + }); + + this._pendingEvaluations.set(window, sourceId); + } + + _routeWindow(window) { + if (!this._trackedWindows.has(window) || this._routedWindows.has(window)) + return; + + if (!this._isRoutableWindow(window)) + return; + + const info = getWindowInfo(window, this._windowTracker); + const rule = WINDOW_RULES.find(candidate => ruleMatches(info, candidate)); + + if (!rule) + return; + + this._moveWindow(window, rule); + this._routedWindows.add(window); + } + + _moveWindow(window, rule) { + this._ensureWorkspace(rule.workspace); + + const monitorIndex = resolveMonitor(rule.monitor); + if (monitorIndex !== null && monitorIndex !== window.get_monitor()) + window.move_to_monitor(monitorIndex); + + const workspace = window.get_workspace(); + if (!workspace || workspace.index() !== rule.workspace) + window.change_workspace_by_index(rule.workspace, false); + } + + _ensureWorkspace(index) { + const workspaceManager = global.workspace_manager; + + while (workspaceManager.n_workspaces <= index) + workspaceManager.append_new_workspace(false, global.get_current_time()); + } + + _isRoutableWindow(window) { + if (window.skip_taskbar || window.is_override_redirect()) + return false; + + if (window.is_on_all_workspaces()) + return false; + + return ROUTABLE_WINDOW_TYPES.has(window.get_window_type()); + } + + _clearPendingEvaluation(window) { + const sourceId = this._pendingEvaluations.get(window); + if (sourceId) { + GLib.source_remove(sourceId); + this._pendingEvaluations.delete(window); + } + } + + _cleanupWindow(window) { + this._clearPendingEvaluation(window); + this._trackedWindows.delete(window); + this._routedWindows.delete(window); + window.disconnectObject(this); + } +} diff --git a/.local/share/gnome-shell/extensions/workspace-router@rbenencia.name/metadata.json b/.local/share/gnome-shell/extensions/workspace-router@rbenencia.name/metadata.json new file mode 100644 index 0000000..ecff8cc --- /dev/null +++ b/.local/share/gnome-shell/extensions/workspace-router@rbenencia.name/metadata.json @@ -0,0 +1,10 @@ +{ + "uuid": "workspace-router@rbenencia.name", + "extension-id": "workspace-router", + "name": "Workspace Router", + "description": "Route newly created windows to fixed workspaces based on title, app ID, or WM class.", + "shell-version": ["46", "47", "48", "49"], + "url": "https://rbenencia.name", + "session-modes": ["user"], + "version": 1 +} diff --git a/.xprofile b/.xprofile new file mode 100644 index 0000000..d1606ec --- /dev/null +++ b/.xprofile @@ -0,0 +1 @@ +export PATH="$HOME/bin:$PATH"
\ No newline at end of file @@ -8,3 +8,6 @@ bin_file_name := 10-$(cur_dir) all: $(bin_dir)/pycombine -e '(Makefile|README.md|debian)' "$(HOME)" "$(repo_dir)" xdg-settings set default-url-scheme-handler org-protocol org-protocol.desktop + +fonts: + $(bin_dir)/setup-install-fonts diff --git a/bin/gnome-move-windows b/bin/gnome-move-windows new file mode 100755 index 0000000..f3a5e34 --- /dev/null +++ b/bin/gnome-move-windows @@ -0,0 +1,46 @@ +#!/bin/sh +# Move windows according to my workflow. Check bin/gnome-set-config to +# see its key-binding. Needs wmctrl. + +# Move all windows to the primary display. If they're on the secondary +# display, and we try to move them to a workspace, it won't work. +for window_id in $(wmctrl -l | awk '{print $1}'); do + wmctrl -i -r $window_id -e 0,0,0,-1,-1 +done + +# Assign windows to predetermined workplaces +misc=$(wmctrl -l | awk '/isco|eepa/ {print $1}') +main="$(wmctrl -xl | awk '/ emacs/ {print $1}')" +communications="$(wmctrl -xl | awk 'tolower($0) ~ /(ebex|lack|communications|notmuch|outlook|elfeed|thunderbird)/ {print $1}')" +media="$(wmctrl -l | awk '/YouTube|Spotify/ {print $1}')" +terminals="$(wmctrl -l | awk '/Alacritty|kitty|terminal/ {print $1}')" +teleport="$(wmctrl -xl | awk 'tolower($0) ~ /teleport/ {print $1}')" +browsers="$(wmctrl -xl | awk '/irefox|hrom/ {print $1}')" + +for window_id in $misc; do + wmctrl -i -r $window_id -t 4 +done + +for window_id in $main; do + wmctrl -i -r $window_id -t 0 +done + +for window_id in $browsers; do + wmctrl -i -r $window_id -t 1 +done + +for window_id in $communications; do + wmctrl -i -r $window_id -t 2 +done + +for window_id in $terminals; do + wmctrl -i -r $window_id -t 3 +done + +for window_id in $teleport; do + wmctrl -i -r $window_id -t 5 +done + +for window_id in $media; do + wmctrl -i -r $window_id -t 8 +done diff --git a/bin/gnome-set-config b/bin/gnome-set-config index 5bfb476..509d542 100755 --- a/bin/gnome-set-config +++ b/bin/gnome-set-config @@ -1,12 +1,99 @@ #!/bin/sh +# Sets my preferred Gnome config. +# Find existing bindings with: +# for e in $(gsettings list-schemas | grep bind); do gsettings list-recursively $e; done NUM_WORKSPACES=9 +WORKSPACE_ROUTER_UUID=workspace-router@rbenencia.name +ORG_AGENDA_INDICATOR_UUID=org-agenda-indicator@rbenencia.name + +ensure_extension_enabled() { + uuid="$1" + extension_dir="" + + for candidate in \ + "$HOME/.local/share/gnome-shell/extensions/$uuid" \ + "$HOME/.local/gnome-shell/extensions/$uuid"; do + if [ -d "$candidate" ]; then + extension_dir="$candidate" + break + fi + done + + if [ -z "$extension_dir" ]; then + return 0 + fi + + if [ -d "$extension_dir/schemas" ] && command -v glib-compile-schemas >/dev/null 2>&1; then + glib-compile-schemas "$extension_dir/schemas" >/dev/null 2>&1 || true + fi + + enabled_extensions=$( + python3 - "$uuid" "$(gsettings get org.gnome.shell enabled-extensions)" <<'PY' +import ast +import sys + +uuid = sys.argv[1] +enabled_extensions = ast.literal_eval(sys.argv[2]) + +if uuid not in enabled_extensions: + enabled_extensions.append(uuid) + +print(enabled_extensions) +PY + ) + gsettings set org.gnome.shell enabled-extensions "$enabled_extensions" + + if command -v gnome-extensions >/dev/null 2>&1; then + gnome-extensions enable "$uuid" >/dev/null 2>&1 || true + fi +} gsettings set org.gnome.mutter dynamic-workspaces false gsettings set org.gnome.desktop.wm.preferences num-workspaces $NUM_WORKSPACES +ensure_extension_enabled "$WORKSPACE_ROUTER_UUID" +ensure_extension_enabled "$ORG_AGENDA_INDICATOR_UUID" + +ROFI_CMD="$HOME/bin/rofi" + +# Disable the default <Super>p. I don't use it, and it's disruptive when I accidentally trigger it. +gsettings set org.gnome.mutter.keybindings switch-monitor '[]' + for i in $(seq 1 $NUM_WORKSPACES); do gsettings set org.gnome.shell.keybindings switch-to-application-$i '[]' gsettings set org.gnome.desktop.wm.keybindings switch-to-workspace-$i "['<Super>$i']" gsettings set org.gnome.desktop.wm.keybindings move-to-workspace-$i "['<Shift><Super>$i']" done + +# This configuration is not present in gsettings; we need to fall back to dconf +bindings="emacs org-mode move-windows rofi rofi-run" +keybindings_key="/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings" +keybindings=$(echo $bindings | awk -v key="$keybindings_key" '{for(i=1;i<=NF;i++) printf("'\''" key "/" $i "/'\''%s", (i==NF ? "" : ","))}') +keybindings="[$keybindings]" + +dconf write "$keybindings_key" "$keybindings" + +dconf write /org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/emacs/binding "'<Shift><Super>e'" +dconf write /org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/emacs/command "'emacsclient -c'" +dconf write /org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/emacs/name "'Emacs'" + +dconf write /org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/org-mode/binding "'<Shift><Super>o'" +dconf write /org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/org-mode/command "'emacsclient -e \"(prot-window-popup-org-capture)\"'" +dconf write /org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/org-mode/name "'org-capture'" + +dconf write /org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/move-windows/binding "'<Shift><Super>m'" +dconf write /org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/move-windows/command "'gnome-move-windows'" +dconf write /org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/move-windows/name "'move-windows'" + +# Disable default for <Super>space. +gsettings set org.gnome.desktop.wm.keybindings switch-input-source "[]" +gsettings set org.gnome.desktop.wm.keybindings switch-input-source-backward '[]' + +dconf write /org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/rofi/binding "'<Super>space'" +dconf write /org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/rofi/command "'$ROFI_CMD -show window'" +dconf write /org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/rofi/name "'rofi'" + +dconf write /org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/rofi-run/binding "'<Super>f2'" +dconf write /org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/rofi-run/command "'$ROFI_CMD -show run'" +dconf write /org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/rofi-run/name "'rofi-run'" diff --git a/bin/rofi b/bin/rofi new file mode 100755 index 0000000..cfa5a8f --- /dev/null +++ b/bin/rofi @@ -0,0 +1,30 @@ +#!/bin/sh + +# GNOME on Wayland does not support the layer-shell protocol that rofi's +# native Wayland backend requires, so fall back to Xwayland there. + +self_path=$(readlink -f "$0" 2>/dev/null || printf '%s\n' "$0") +rofi_bin="" + +for candidate in /usr/bin/rofi /bin/rofi; do + candidate_path=$(readlink -f "$candidate" 2>/dev/null || printf '%s\n' "$candidate") + if [ -x "$candidate" ] && [ "$candidate_path" != "$self_path" ]; then + rofi_bin="$candidate" + break + fi +done + +if [ -z "$rofi_bin" ]; then + printf '%s\n' "rofi: unable to find the system rofi binary" >&2 + exit 127 +fi + +if [ "${XDG_SESSION_TYPE:-}" = "wayland" ] && [ -n "${WAYLAND_DISPLAY:-}" ]; then + case "${XDG_CURRENT_DESKTOP:-}:${DESKTOP_SESSION:-}" in + *GNOME*:*|*:gnome|GNOME:*|gnome:*) + set -- -x11 "$@" + ;; + esac +fi + +exec "$rofi_bin" "$@" diff --git a/bin/setup-install-fonts b/bin/setup-install-fonts new file mode 100755 index 0000000..ea96873 --- /dev/null +++ b/bin/setup-install-fonts @@ -0,0 +1,26 @@ +#!/bin/bash + +set -e + +FONT_DIR="$HOME/.local/share/fonts" +TEMP_DIR=$(mktemp -d) + +mkdir -p "$FONT_DIR" + +pushd $TEMP_DIR +curl -s 'https://api.github.com/repos/be5invis/Iosevka/releases/latest' | \ + jq -r ".assets[] | .browser_download_url" | \ + grep PkgTTC-Iosevka | \ + xargs -n 1 curl -L -O --fail --silent --show-error + +for f in *.zip; do + unzip "$f" +done + +mv *.ttc "$FONT_DIR/" +fc-cache -f -v "$FONT_DIR" + +popd +rm -rf "$TEMP_DIR" + +echo "Iosevka fonts have been installed." diff --git a/debian/control b/debian/control index 9ff4f63..33513e5 100644 --- a/debian/control +++ b/debian/control @@ -6,8 +6,13 @@ Build-Depends: fzf, make, cmake, + jq, + libtool-bin, mpv, python3-proselint, + ripgrep, + rofi, + vim, Standards-Version: 4.5.1 Rules-Requires-Root: no |
