From b3dafe96d3cff54a61d7d26c3a68aecef4e5608b Mon Sep 17 00:00:00 2001 From: Henrik Lissner Date: Mon, 12 Jun 2017 12:37:38 +0200 Subject: [PATCH] app/irc: general rewrite (#103) + Refactor initialization process. + Refactor for consistency. + Add +irc-defer-notifications (for ZNC users). + Rewrote =irc (opens separate workspace + auto-connects to registered networks). + Add +irc/connect (connect to specific network). + Add +irc/quit (kill whole circe session). + Add +irc/ivy-jump-to-channel command. + Rewrite README. + Silence QUIT/PART default messages; they're cute, but no thanks. + Truncate nicks non-destructively. + Jump to prompt when entering insert mode (with evil). + Activate solaire-mde in channel buffers to visually distinguish them from server buffers. --- modules/app/irc/README.org | 69 +++++++++++++-- modules/app/irc/autoload/irc.el | 72 ++++++++++++--- modules/app/irc/config.el | 150 +++++++++++++++++--------------- 3 files changed, 201 insertions(+), 90 deletions(-) diff --git a/modules/app/irc/README.org b/modules/app/irc/README.org index 13765a55e..703aec053 100644 --- a/modules/app/irc/README.org +++ b/modules/app/irc/README.org @@ -1,19 +1,74 @@ * :app irc -This module makes Emacs an irc client, using [[https://github.com/jorgenschaefer/circe][~circe~]]. +This module turns adds an IRC client to Emacs ([[https://github.com/jorgenschaefer/circe][~circe~)]] with native notifications ([[https://github.com/eqyiel/circe-notifications][circe-notifications]]). ** Dependencies +This module has no dependencies, besides =gnutls-cli= or =openssl= for secure connections. -I use ~pass~ to not have the passwords written in my dotfiles. It's available under ~:tools~ modules. -If you want to use TLS you also need =openssl= or =gnutls-cli=. +** Configure +Use the ~:irc~ setting to configure IRC servers. Its second argument (a plist) takes the same arguments as ~circe-network-options~. -Configure Emacs to use your favorite irc servers: #+BEGIN_SRC emacs-lisp :tangle no (set! :irc "chat.freenode.net" `(:tls t - :nick "benny" - :sasl-username ,(password-store-get "irc/freenode") - :sasl-password ,(password-store-get "irc/freenode") + :nick "doom" + :sasl-username "myusername" + :sasl-password "mypassword" + :channels ("#emacs"))) +#+END_SRC + +*It is a obviously a bad idea to store auth-details in plaintext,* so here are some ways to avoid that: + +*** Pass: the unix password manager +[[https://www.passwordstore.org/][Pass]] is my tool of choice. I use it to manage my passwords. If you activate the [[/modules/tools/password-store/README.org][:tools password-store]] module you get an elisp API through which to access your password store. + +~:irc~'s plist can use functions instead of strings. ~+pass-get-user~ and ~+pass-get-secret~ can help here: + +#+BEGIN_SRC emacs-lisp :tangle no +(set! :irc "chat.freenode.net" + `(:tls t + :nick "doom" + :sasl-username ,(+pass-get-user "irc/freenode.net") + :sasl-password ,(+pass-get-secret "irc/freenode.net") + :channels ("#emacs"))) +#+END_SRC + +But wait, there's more! This stores your password in a public variable which could be accessed or appear in backtraces. Not good! So we go a step further: + +#+BEGIN_SRC emacs-lisp :tangle no +(set! :irc "chat.freenode.net" + `(:tls t + :nick "doom" + :sasl-username ,(+pass-get-user "irc/freenode.net") + :sasl-password (lambda (&rest _) (+pass-get-secret "irc/freenode.net")) + :channels ("#emacs"))) +#+END_SRC + +And you're good to go! + +*** Emacs' auth-source API +~auth-source~ is built into Emacs. As suggested [[https://github.com/jorgenschaefer/circe/wiki/Configuration#safer-password-management][in the circe wiki]], you can store (and retrieve) encrypted passwords with it. + +#+BEGIN_SRC emacs-lisp :tangle no +(setq auth-sources '("~/.authinfo.gpg")) + +(defun my-fetch-password (&rest params) + (require 'auth-source) + (let ((match (car (apply #'auth-source-search params)))) + (if match + (let ((secret (plist-get match :secret))) + (if (functionp secret) + (funcall secret) + secret)) + (error "Password not found for %S" params)))) + +(defun my-nickserv-password (server) + (my-fetch-password :user "forcer" :host "irc.freenode.net")) + +(set! :irc "chat.freenode.net" + '(:tls t + :nick "doom" + :sasl-password my-nickserver-password :channels ("#emacs"))) #+END_SRC diff --git a/modules/app/irc/autoload/irc.el b/modules/app/irc/autoload/irc.el index 3430a93ec..677b5f699 100644 --- a/modules/app/irc/autoload/irc.el +++ b/modules/app/irc/autoload/irc.el @@ -1,17 +1,65 @@ ;;; app/irc/autoload/email.el -*- lexical-binding: t; -*- -;;;###autoload -(defun =irc () - "Connect to IRC." - (interactive) - (call-interactively #'circe)) +(defvar +irc--workspace-name "*IRC*") + +(defun +irc-setup-wconf (&optional inhibit-workspace) + (unless inhibit-workspace + (+workspace-switch +irc--workspace-name t)) + (let ((buffers (doom-buffers-in-mode 'circe-mode nil t))) + (if buffers + (ignore (switch-to-buffer (car buffers))) + (require 'circe) + (delete-other-windows) + (switch-to-buffer (doom-fallback-buffer)) + t))) ;;;###autoload -(defun +irc/connect-all () - "Connect to all `:irc' defined servers." - (interactive) - ;; force a library load for +irc--accounts - (circe--version) - (cl-loop for network in +irc--accounts - collect (circe (car network)))) +(defun =irc (&optional inhibit-workspace) + "Connect to IRC and auto-connect to all registered networks." + (interactive "P") + (and (+irc-setup-wconf inhibit-workspace) + (cl-loop for network in +irc-connections + collect (circe (car network))))) +;;;###autoload +(defun +irc/connect () + "Connect to a specific registered server." + (interactive) + (and (+irc-setup-wconf inhibit-workspace) + (call-interactively #'circe))) + +;;;###autoload +(defun +irc/quit () + "Kill current circe session and workgroup." + (interactive) + (if (y-or-n-p "Really kill IRC session?") + (let (circe-channel-killed-confirmation + circe-server-killed-confirmation) + (mapcar #'kill-buffer (doom-buffers-in-mode 'circe-mode (buffer-list) t)) + (when (equal (+workspace-current-name) +irc--workspace-name) + (+workspace/delete +irc--workspace-name))) + (message "Aborted"))) + +;;;###autoload +(defun +irc/ivy-jump-to-channel (&optional this-server) + "Jump to an open channel or server buffer with ivy. If THIS-SERVER (universal +argument) is non-nil only show channels in current server." + (interactive "P") + (if (not (circe-server-buffers)) + (message "No circe buffers available") + (when (and this-server (not circe-server-buffer)) + (setq this-server nil)) + (ivy-read (format "Jump to%s: " (if this-server (format " (%s)" (buffer-name circe-server-buffer)) "")) + (cl-loop with servers = (if this-server (list circe-server-buffer) (circe-server-buffers)) + with current-buffer = (current-buffer) + for server in servers + collect (buffer-name server) + nconc + (with-current-buffer server + (cl-loop for buf in (circe-server-chat-buffers) + unless (eq buf current-buffer) + collect (format " %s" (buffer-name buf))))) + :action #'ivy--switch-buffer-action + :preselect (buffer-name (current-buffer)) + :keymap ivy-switch-buffer-map + :caller '+irc/ivy-jump-to-channel))) diff --git a/modules/app/irc/config.el b/modules/app/irc/config.el index 0080cd3ae..226f8619b 100644 --- a/modules/app/irc/config.el +++ b/modules/app/irc/config.el @@ -1,9 +1,5 @@ ;;; app/irc/config.el -*- lexical-binding: t; -*- -;; -;; Config -;; - (defvar +irc-left-padding 13 "TODO") @@ -19,13 +15,18 @@ (defvar +irc-notifications-watch-strings nil "TODO") +(defvar +irc-connections nil + "A list of connections set with :irc. W") + +(defvar +irc-defer-notifications nil + "How long to defer enabling notifications, in seconds (e.g. 5min = 300). +Useful for ZNC users who want to avoid the deluge of notifications during buffer +playback.") + (def-setting! :irc (server letvars) "Registers an irc server for circe." - `(progn - (push ',(cons server letvars) +irc--accounts) - (push ',(cons server letvars) circe-network-options))) - -(defvar +irc--accounts nil) + `(cl-pushnew (cons ,server ,letvars) +irc-connections + :test #'equal :key #'car)) ;; @@ -33,93 +34,92 @@ ;; (def-package! circe - ;; need a low impact function just to force an eval - :commands (circe circe--version) - :init - (setq circe-network-defaults nil) + :commands (circe circe-server-buffers) + :init (setq circe-network-defaults nil) :config - (set! :evil-state 'circe-channel-mode 'emacs) + ;; change hands + (setq circe-network-options +irc-connections) + (defvaralias '+irc-connections 'circe-network-options) - (defun +irc|circe-format-padding (left right) - (format (format "%%%ds | %%s" +irc-left-padding) left right)) + (defsubst +irc--pad (left right) + (format (format "%%%ds | %%s" +irc-left-padding) + (concat "*** " left) right)) - (setq circe-use-cycle-completion t + (setq circe-default-quit-message nil + circe-default-part-message nil + circe-use-cycle-completion t circe-reduce-lurker-spam t circe-format-say (format "{nick:+%ss} │ {body}" +irc-left-padding) circe-format-self-say circe-format-say circe-format-action (format "{nick:+%ss} * {body}" +irc-left-padding) circe-format-self-action circe-format-action - circe-format-server-topic - (+irc|circe-format-padding "*** Topic" "{userhost}: {topic-diff}") - + (+irc--pad "Topic" "{userhost}: {topic-diff}") circe-format-server-join-in-channel - (+irc|circe-format-padding "*** Join" "{nick} ({userinfo}) joined {channel}") - + (+irc--pad "Join" "{nick} ({userinfo}) joined {channel}") circe-format-server-join - (+irc|circe-format-padding "*** Join" "{nick} ({userinfo})") - + (+irc--pad "Join" "{nick} ({userinfo})") circe-format-server-part - (+irc|circe-format-padding "*** Part" "{nick} ({userhost}) left {channel}: {reason}") - + (+irc--pad "Part" "{nick} ({userhost}) left {channel}: {reason}") circe-format-server-quit - (+irc|circe-format-padding "*** Quit" "{nick} ({userhost}) left IRC: {reason}]") - + (+irc--pad "Quit" "{nick} ({userhost}) left IRC: {reason}]") circe-format-server-quit-channel - (+irc|circe-format-padding "*** Quit" "{nick} ({userhost}) left {channel}: {reason}]") - + (+irc--pad "Quit" "{nick} ({userhost}) left {channel}: {reason}]") circe-format-server-rejoin - (+irc|circe-format-padding "*** Re-join" "{nick} ({userhost}), left {departuredelta} ago") - + (+irc--pad "Re-join" "{nick} ({userhost}), left {departuredelta} ago") circe-format-server-nick-change - (+irc|circe-format-padding "*** Nick" "{old-nick} ({userhost}) is now known as {new-nick}") - + (+irc--pad "Nick" "{old-nick} ({userhost}) is now known as {new-nick}") circe-format-server-nick-change-self - (+irc|circe-format-padding "*** Nick" "You are now known as {new-nick} ({old-nick})") - + (+irc--pad "Nick" "You are now known as {new-nick} ({old-nick})") circe-format-server-nick-change-self - (+irc|circe-format-padding "*** Nick" "{old-nick} ({userhost}) is now known as {new-nick}") - + (+irc--pad "Nick" "{old-nick} ({userhost}) is now known as {new-nick}") circe-format-server-mode-change - (+irc|circe-format-padding "*** Mode" "{change} on {target} by {setter} ({userhost})") - + (+irc--pad "Mode" "{change} on {target} by {setter} ({userhost})") circe-format-server-lurker-activity - (+irc|circe-format-padding "*** Lurk" "{nick} joined {joindelta} ago")) + (+irc--pad "Lurk" "{nick} joined {joindelta} ago")) - (defun +irc*circe-truncate-nicks (orig-func keywords) - "If nick is too long, truncate it. Uses `+irc-left-padding' -to determine length." - (when (plist-member keywords :nick) - (let* ((long-nick (plist-get keywords :nick)) - (short-nick (s-left (- +irc-left-padding 1) long-nick))) - ;; only change the nick if it's needed - (unless (or (= +irc-left-padding - (length long-nick)) - (equal long-nick short-nick)) - (plist-put keywords :nick (s-concat short-nick "▶"))))) - (funcall orig-func keywords)) - (advice-add 'circe--display-add-nick-property :around #'+irc*circe-truncate-nicks) + (enable-circe-new-day-notifier) (defun +irc*circe-disconnect-hook (&rest _) (run-hooks '+irc-disconnect-hook)) (advice-add 'circe--irc-conn-disconnected :after #'+irc*circe-disconnect-hook) + (defun +irc*circe-truncate-nicks () + "Truncate long nicknames in chat output (non-destructive)." + (when-let (beg (text-property-any (point-min) (point-max) 'lui-format-argument 'nick)) + (goto-char beg) + (let ((end (next-single-property-change beg 'lui-format-argument)) + (nick (plist-get (plist-get (text-properties-at beg) 'lui-keywords) + :nick))) + (when (> (length nick) +irc-left-padding) + (compose-region (+ beg +irc-left-padding -1) end + ?…))))) + (add-hook 'lui-pre-output-hook #'+irc*circe-truncate-nicks) + (add-hook! '+irc-disconnect-hook (run-at-time "5 minute" nil #'circe-reconnect-all)) (defun +irc|circe-message-option-bot (nick &rest ignored) (when (member nick +irc-bot-list) - '((text-properties . (face circe-fool-face - lui-do-not-track t))))) + '((text-properties . (face circe-fool-face lui-do-not-track t))))) (add-hook 'circe-message-option-functions #'+irc|circe-message-option-bot) - (enable-circe-new-day-notifier) - (add-hook! 'circe-server-connected-hook - (run-at-time "5 minutes" nil #'enable-circe-notifications)) - (add-hook! 'circe-channel-mode-hook - #'(enable-circe-color-nicks enable-lui-autopaste turn-on-visual-line-mode))) + #'(enable-circe-color-nicks enable-lui-autopaste turn-on-visual-line-mode)) + + (after! evil + ;; Let `+irc/quit' and `circe' handle buffer cleanup + (map! :map circe-mode-map [remap doom/kill-this-buffer] #'bury-buffer) + + ;; Ensure entering insert mode will put us at the prompt. + (add-hook! 'lui-mode-hook + (add-hook 'evil-insert-state-entry-hook #'end-of-buffer nil t))) + + (after! solaire-mode + ;; distinguish chat/channel buffers from server buffers. + (add-hook 'circe-chat-mode-hook #'solaire-mode))) + (def-package! circe-color-nicks @@ -131,22 +131,33 @@ to determine length." (def-package! circe-new-day-notifier :commands enable-circe-new-day-notifier - :config (setq circe-new-day-notifier-format-message - (+irc|circe-format-padding - "*** Day" - "Date changed [{day}]"))) + :config + (setq circe-new-day-notifier-format-message + (+irc--pad "Day" "Date changed [{day}]"))) (def-package! circe-notifications :commands enable-circe-notifications - :config (setq circe-notifications-watch-strings - +irc-notifications-watch-strings)) + :init + (if +irc-defer-notifications + (add-hook! 'circe-server-connected-hook + (run-at-time +irc-defer-notifications nil #'enable-circe-notifications)) + (add-hook 'circe-server-connected-hook #'enable-circe-notifications)) + :config + (setq circe-notifications-watch-strings +irc-notifications-watch-strings + circe-notifications-emacs-focused nil + circe-notifications-alert-style + (cond (IS-MAC 'osx-notifier) + (IS-LINUX 'libnotify)))) (def-package! lui :commands lui-mode :config (map! :map lui-mode-map "C-u" #'lui-kill-to-beginning-of-line) + (when (featurep! :feature spellcheck) + (setq lui-flyspell-p t + lui-fill-type nil)) (enable-lui-logging) (defun +irc|lui-setup-margin () @@ -157,10 +168,7 @@ to determine length." (setq fringes-outside-margins t word-wrap t wrap-prefix (s-repeat (+ +irc-left-padding 3) " "))) - (add-hook! 'lui-mode-hook '(+irc|lui-setup-margin +irc|lui-setup-wrap)) - (when (featurep! :feature spellcheck) - (setq lui-flyspell-p t - lui-fill-type nil))) + (add-hook! 'lui-mode-hook #'(+irc|lui-setup-margin +irc|lui-setup-wrap))) (def-package! lui-autopaste