X-Git-Url: https://thelambdalab.xyz/gitweb/index.cgi?a=blobdiff_plain;f=lirc.el;h=ad1a8faca0aaa5e5b2880803962d3b59dd2ab380;hb=ecc4dd4e33a620a6d365b1b433d92d66642ecf84;hp=131b7ccb42e44cd9c818c03ae643a7375287c1af;hpb=bdfc5855121daf26be78530df1dbb605aad428f9;p=lurk.git diff --git a/lirc.el b/lirc.el index 131b7cc..ad1a8fa 100644 --- a/lirc.el +++ b/lirc.el @@ -30,6 +30,7 @@ (provide 'lirc) + ;;; Customizations ;; @@ -43,67 +44,149 @@ "Default full name.") (defcustom lirc-user-name "plugd" "Default user name.") -(defcustom lirc-host "localhost" - "Default server.") -(defcustom lirc-port 6667 - "Default port.") -(defcustom lirc-prompt-string "> " - "Prompt.") +(defcustom lirc-networks + '(("libera" "irc.libera.chat" 6697) + ("freenode" "chat.freenode.net" 6697) + ("local" "localhost" 6697)) + "IRC networks.") + +(defcustom lirc-allow-ipv6 nil + "Set to non-nil to allow use of IPv6.") ;;; Faces ;; +(defface lirc-text + '((t :inherit font-lock-preprocessor-face)) + "Face used for Lirc text.") + +(defface lirc-your-nick + '((t :inherit font-lock-constant-face)) + "Face used for highlighting your nick.") + (defface lirc-prompt '((t :inherit org-level-2)) "Face used for the prompt.") -(defface lirc-channel +(defface lirc-context '((t :inherit org-list-dt)) - "Face used for the channel name in the prompt.") + "Face used for the context name in the prompt.") + +(defface lirc-faded + '((t :inherit font-lock-preprocessor-face)) + "Face used for faded Lirc text.") + +(defface lirc-bold + '((t :inherit font-lock-function-name-face)) + "Face used for bold Lirc text.") + +(defface lirc-error + '((t :inherit font-lock-regexp-grouping-construct)) + "Face used for Lirc error text.") ;;; Global variables ;; -(defvar lirc-current-channel nil) -(defvar lirc-channel-list nil) +(defvar lirc-version "Lirc v0.1") + +(defvar lirc-notice-prefix + (concat + (propertize + "-" 'face 'lirc-faded) + (propertize + "!" 'face 'lirc-bold) + (propertize + "-" 'face 'lirc-faded))) + +(defvar lirc-error-prefix + (propertize "!!!" 'face 'lirc-error)) -(defvar lirc-response "") + +(defvar lirc-prompt-string + (propertize "> " 'face 'lirc-prompt)) ;;; Network process ;; +(defvar lirc-response "") + (defun lirc-filter (proc string) (dolist (line (split-string (concat lirc-response string) "\n")) (if (string-suffix-p "\r" line) (lirc-eval-msg-string (string-trim line)) (setq lirc-response line)))) -(defun lirc-get-process () +(defun lirc-sentinel (proc string) + (unless (equal "open" (string-trim string)) + (lirc-display-error "Disconnected from server.") + (clrhash lirc-contexts) + (setq lirc-current-context nil) + (lirc-render-prompt) + (cancel-timer lirc-ping-timer))) + +(defun lirc-start-process (network) + (let* ((row (assoc network lirc-networks)) + (host (elt row 1)) + (port (elt row 2)) + (flags (seq-drop row 3))) + (make-network-process :name "lirc" + :host host + :service port + :family (if lirc-allow-ipv6 nil 'ipv4) + :filter #'lirc-filter + :sentinel #'lirc-sentinel + :nowait nil + :tls-parameters (if (memq :notls flags) + nil + (cons 'gnutls-x509pki + (gnutls-boot-parameters + :type 'gnutls-x509pki + :hostname host))) + :buffer "*lirc*"))) + +(defvar lirc-ping-timer nil) +(defvar lirc-ping-period 60) + +(defun lirc-ping-function () + (lirc-send-msg (lirc-msg nil nil "PING" (car (process-contact (get-process "lirc"))))) + (setq lirc-ping-timer (run-with-timer lirc-ping-period nil #'lirc-ping-function))) + +(defun lirc-connect (network) + (if (get-process "lirc") + (lirc-display-error "Already connected. Disconnect first.") + (if (not (assoc network lirc-networks)) + (lirc-display-error "Network '" network "' is unknown.") + (clrhash lirc-contexts) + (setq lirc-current-context nil) + (lirc-start-process network) + (lirc-send-msg (lirc-msg nil nil "USER" lirc-user-name 0 "*" lirc-full-name)) + (lirc-send-msg (lirc-msg nil nil "NICK" lirc-nick)) + (setq lirc-ping-timer (run-with-timer lirc-ping-period nil #'lirc-ping-function))))) + + +(defun lirc-send-msg (msg) (let ((proc (get-process "lirc"))) (if (and proc (eq (process-status proc) 'open)) - proc - (make-network-process :name "lirc" - :host lirc-host - :service lirc-port - :filter #'lirc-filter - :nowait t - :buffer "*lirc*")))) - -;;; Messages + (process-send-string proc (concat (lirc-msg->string msg) "\r\n")) + (lirc-display-error "No server connection established.") + (error "No server connection established")))) + + +;;; Server messages ;; -(defun lirc-as-string (obj) +(defun lirc--as-string (obj) (if obj (with-output-to-string (princ obj)) nil)) (defun lirc-msg (tags src cmd &rest params) - (list (lirc-as-string tags) - (lirc-as-string src) - (upcase (lirc-as-string cmd)) - (mapcar #'lirc-as-string + (list (lirc--as-string tags) + (lirc--as-string src) + (upcase (lirc--as-string cmd)) + (mapcar #'lirc--as-string (if (and params (listp (elt params 0))) (elt params 0) params)))) @@ -121,11 +204,15 @@ (rx (opt (: "@" (group (* (not (or "\n" "\r" ";" " "))))) (* whitespace)) - (opt (: ":" (group (* (not (or "\n" "\r" " "))))) + (opt (: ":" (: (group (* (not (any space "!" "@")))) + (* (not (any space))))) (* whitespace)) (group (: (* (not whitespace)))) (* whitespace) - (opt (group (+ not-newline))))) + (opt (group (+ not-newline)))) + "Regex used to parse IRC messages. +Note that this regex is incomplete. Noteably, we discard the non-nick +portion of the source component of the message, as LIRC doesn't use this.") (defun lirc-string->msg (string) (if (string-match lirc-msg-regex string) @@ -163,43 +250,127 @@ nil)))) +;;; Contexts and users +;; + +(defvar lirc-context-table + '((channel lirc-display-channel-message) + (nick lirc-display-private-message) + (host lirc-diaplay-server-message))) + +(defvar lirc-current-context nil) +(defvar lirc-contexts (make-hash-table :test #'equal)) + +(defun lirc-add-context (name) + (puthash name nil lirc-contexts)) + +(defun lirc-del-context (name) + (remhash name lirc-contexts)) + +(defun lirc-get-context-users (name) + (gethash name lirc-contexts)) + +(defun lirc-add-context-users (context users) + (puthash context + (append users + (gethash context lirc-contexts)) + lirc-contexts)) + +(defun lirc-del-context-user (context user) + (puthash context + (remove user (gethash context lirc-contexts)) + lirc-contexts)) + +(defun lirc-del-user (user) + (dolist (context (lirc-get-context-list)) + (lirc-del-context-user context user))) + +(defun lirc-get-context-type (name) + (cond + ((string-prefix-p "#" name) 'channel) + ((string-match-p (rx (or "." "localhost")) name) 'host) + (t 'nick))) + +(defun lirc-get-context-list () + (let ((res nil)) + (maphash (lambda (key val) + (cl-pushnew key res)) + lirc-contexts) + res)) + +(defun lirc-get-next-context (&optional prev) + (if lirc-current-context + (let* ((context-list (if prev + (reverse (lirc-get-context-list)) + (lirc-get-context-list))) + (context-list* (member lirc-current-context context-list))) + (if (> (length context-list*) 1) + (cadr context-list*) + (car context-list))) + nil)) + +(defun lirc-cycle-contexts (&optional rev) + (if lirc-current-context + (progn + (setq lirc-current-context (lirc-get-next-context rev)) + (lirc-render-prompt)) + (lirc-display-error "No channels joined."))) + +(defun lirc-cycle-contexts-forward () + (interactive) + (lirc-cycle-contexts)) + +(defun lirc-cycle-contexts-reverse () + (interactive) + (lirc-cycle-contexts t)) + ;;; Buffer ;; -(defun lirc-display-string (string) +(defun lirc-display-string (&rest strings) (with-current-buffer (get-buffer-create "*lirc*") (save-excursion (goto-char lirc-prompt-marker) - (let ((inhibit-read-only t)) - (insert-before-markers (propertize (concat string "\n") 'read-only t)))))) - -(defun lirc-connect () - (lirc-send-msg (lirc-msg nil nil "USER" lirc-user-name 0 "*" lirc-full-name)) - (lirc-send-msg (lirc-msg nil nil "NICK" lirc-nick))) - -(defun lirc-send-msg (msg) - (let ((proc (lirc-get-process))) - (process-send-string proc (concat (lirc-msg->string msg) "\r\n")))) + (let ((inhibit-read-only t) + (old-pos (marker-position lirc-prompt-marker)) + (adaptive-fill-regexp (rx (= 6 anychar)))) + (insert-before-markers + (propertize (concat (format-time-string "%H:%M") " ") + 'face 'lirc-text + 'read-only t) + (propertize (concat (apply #'concat strings) "\n") + 'read-only t)) + (fill-region old-pos lirc-prompt-marker))))) (defun lirc-render-prompt () (with-current-buffer "*lirc*" - (set-marker-insertion-type lirc-prompt-marker nil) - (set-marker-insertion-type lirc-input-marker t) - (save-excursion - (let ((inhibit-read-only t)) - (delete-region lirc-prompt-marker lirc-input-marker) - (goto-char lirc-prompt-marker) - (insert - (propertize (if lirc-current-channel - lirc-current-channel - "[no channel]") - 'face 'lirc-channel - 'read-only t) - (propertize lirc-prompt-string - 'face 'lirc-prompt - 'read-only t - 'rear-nonsticky t))))) - (set-marker-insertion-type lirc-input-marker nil)) + (let ((update-point (= lirc-input-marker (point))) + (update-window-points (mapcar (lambda (w) + (list (= (window-point w) lirc-input-marker) + w)) + (get-buffer-window-list nil nil t)))) + (save-excursion + (set-marker-insertion-type lirc-prompt-marker nil) + (set-marker-insertion-type lirc-input-marker t) + (let ((inhibit-read-only t)) + (delete-region lirc-prompt-marker lirc-input-marker) + (goto-char lirc-prompt-marker) + (insert + (propertize (if lirc-current-context + lirc-current-context + "") + 'face 'lirc-context + 'read-only t) + (propertize lirc-prompt-string + 'face 'lirc-prompt + 'read-only t + 'rear-nonsticky t))) + (set-marker-insertion-type lirc-input-marker nil)) + (if update-point + (goto-char lirc-input-marker)) + (dolist (v update-window-points) + (if (car v) + (set-window-point (cadr v) lirc-input-marker)))))) (defvar lirc-prompt-marker nil "Marker for prompt position in LIRC buffer.") @@ -215,39 +386,213 @@ (if (markerp lirc-input-marker) (set-marker lirc-input-marker (point-max)) (setq lirc-input-marker (point-max-marker))) - (lirc-render-prompt) (goto-char (point-max)) - (recenter -1))) + (lirc-render-prompt))) + + +;;; Output formatting +;; + +(defun lirc-display-message (from to text) + (let* ((to-type (lirc-get-context-type to)) + (display-fun (cadr (assoc to-type lirc-context-table)))) + (funcall display-fun from to text))) + +(defun lirc-display-channel-message (from to text) + (lirc-display-string + (propertize (concat to + " <" from "> " + text) + 'face 'lirc-text))) + + +(defun lirc-display-action (channel-name nick action) + (lirc-display-string (concat channel-name + " * " + (propertize (concat nick " " action) + 'face 'lirc-text)))) + +(defun lirc-display-private-message (from to text) + (lirc-display-string + (concat + (propertize + (concat "[" from " -> " to "] " + text) + 'face 'lirc-text)))) + + +(defun lirc-display-notice (&rest notices) + (lirc-display-string lirc-notice-prefix " " (apply #'concat notices))) + +(defun lirc-display-error (&rest messages) + (lirc-display-string lirc-error-prefix " " + (propertize (apply #'concat messages) + 'face 'lirc-error))) ;;; Message evaluation ;; (defun lirc-eval-msg-string (string) + ;; (lirc-display-string string) (let* ((msg (lirc-string->msg string))) (pcase (lirc-msg-cmd msg) - ("PING" - (lirc-send-msg - (lirc-msg nil nil "PONG" (lirc-msg-params msg)))) - ;; ((rx (= 3 digit)) - ;; (lirc-display-string (string-join (cdr (lirc-msg-params msg)) " "))) - ((and "JOIN" (let (rx (: (literal lirc-nick) "!" (* anychar))) (lirc-msg-src msg))) - (let ((channel (car (lirc-msg-params msg)))) - (setq lirc-current-channel channel) - (add-to-list 'lirc-channel-list channel) - (lirc-render-prompt))) - (_ - (lirc-display-string (lirc-msg->string msg)))))) + ("PING" + (lirc-send-msg + (lirc-msg nil nil "PONG" (lirc-msg-params msg))) + (lirc-display-notice "ping-pong")) + + ("353" ; NAMEREPLY + (let* ((params (lirc-msg-params msg)) + (channel (elt params 2)) + (names (split-string (elt params 3)))) + (lirc-add-context-users channel names))) + + ("366" ; ENDOFNAMES + (lirc-display-notice + (lirc--as-string (length (lirc-get-context-users lirc-current-context))) + " users in " lirc-current-context)) + + ((rx (= 3 (any digit))) + (lirc-display-notice (mapconcat 'identity (cdr (lirc-msg-params msg)) " "))) + + ((and "JOIN" + (guard (equal lirc-nick (lirc-msg-src msg)))) + (let ((channel-name (car (lirc-msg-params msg)))) + (lirc-add-context channel-name) + (setq lirc-current-context channel-name) + (lirc-display-notice "Joining channel " channel-name) + (lirc-render-prompt))) + + ("JOIN" + (let ((channel-name (car (lirc-msg-params msg))) + (nick (lirc-msg-src msg))) + (lirc-add-context-users channel-name (list nick)) + (lirc-display-notice nick " joined channel " channel-name))) + + ((and "PART" + (guard (equal lirc-nick (lirc-msg-src msg)))) + (let ((channel-name (car (lirc-msg-params msg)))) + (lirc-display-notice "Left channel " channel-name) + (lirc-del-context channel-name) + (if (equal channel-name lirc-current-context) + (setq lirc-current-context (lirc-get-next-context))) + (lirc-render-prompt))) + + ("PART" + (let ((channel-name (car (lirc-msg-params msg))) + (nick (lirc-msg-src msg))) + (lirc-del-context-user channel-name nick) + (lirc-display-notice nick " left channel " channel-name))) + + ("QUIT" + (let ((nick (lirc-msg-src msg)) + (reason (mapconcat 'identity (lirc-msg-params msg) " "))) + (lirc-del-user nick) + (lirc-display-notice nick " quit: " reason))) + + ((and "NICK" + (guard (equal lirc-nick (lirc-msg-src msg)))) + (setq lirc-nick (car (lirc-msg-params msg))) + (lirc-display-notice "Set nick to " lirc-nick)) + + ("NICK" + (let ((old-nick (lirc-msg-src msg)) + (new-nick (car (lirc-msg-params msg)))) + (lirc-display-notice nick " is now known as " new-nick) + (lirc-rename-user nick new-nick))) + + ("NOTICE" + (let ((nick (lirc-msg-src msg)) + (channel (car (lirc-msg-params msg))) + (text (cadr (lirc-msg-params msg)))) + (pcase text + ((rx (: "\01VERSION " + (let version (* (not "\01"))) + "\01")) + (lirc-display-notice "CTCP version reply from " nick ": " version)) + (_ + (lirc-display-notice text))))) + + ("PRIVMSG" + (let* ((from (lirc-msg-src msg)) + (params (lirc-msg-params msg)) + (to (car params)) + (text (cadr params))) + (pcase text + ("\01VERSION\01" + (let ((version-string (concat lirc-version " - running on GNU Emacs " emacs-version))) + (lirc-send-msg (lirc-msg nil nil "NOTICE" + (list from (concat "\01VERSION " + version-string + "\01"))))) + (lirc-display-notice "CTCP version request received from " from)) + + ((rx (let ping (: "\01PING " (* (not "\01")) "\01"))) + (lirc-send-msg (lirc-msg nil nil "NOTICE" (list from ping))) + (lirc-display-notice "CTCP ping received from " from)) + + ("\01USERINFO\01" + (lirc-display-notice "CTCP userinfo request from " from " (no response sent)")) + + (_ + (lirc-display-message from to text))))) + (_ + (lirc-display-string (lirc-msg->string msg)))))) ;;; Command entering ;; (defun lirc-enter-string (string) - (cond ((string-prefix-p "/" string) - (let ((cmd-str (substring string 1))) - (lirc-send-msg (lirc-msg nil nil cmd-str)))) - (t - (error "Unknown command" string)))) + (if (string-prefix-p "/" string) + (pcase (substring string 1) + ((rx (: "CONNECT " (let network (* not-newline)))) + (lirc-display-notice "Attempting to connect to " network "...") + (lirc-connect network)) + + ((rx (: "TOPIC " (let new-topic (* not-newline)))) + (lirc-send-msg (lirc-msg nil nil "TOPIC" lirc-current-context new-topic))) + + ((rx (: "ME " (let action (* not-newline)))) + (lirc-send-msg (lirc-msg nil nil "PRIVMSG" + (list lirc-current-context + (concat "\01ACTION " action "\01")))) + (lirc-display-action lirc-nick action)) + + ((rx (: "VERSION" " " (let nick (* (not whitespace))))) + (lirc-send-msg (lirc-msg nil nil "PRIVMSG" + (list nick "\01VERSION\01"))) + (lirc-display-notice "CTCP version request sent to " nick)) + + ((rx "PART" (opt (: " " (let channel (* not-newline))))) + (if (or lirc-current-context channel) + (lirc-send-msg (lirc-msg nil nil "PART" (if channel + channel + lirc-current-context))) + (lirc-display-error "No current channel to leave."))) + + ((rx "MSG " + (let to (* (not whitespace))) + " " + (let text (* not-newline))) + (lirc-send-msg (lirc-msg nil nil "PRIVMSG" to text)) + (lirc-display-message lirc-nick to text)) + + ((rx (: (let cmd-str (+ (not whitespace))) + (opt (: " " (let params-str (* not-newline)))))) + (lirc-send-msg (lirc-msg nil nil (upcase cmd-str) + (if params-str + (split-string params-str) + nil))))) + + (unless (string-empty-p string) + (if lirc-current-context + (progn + (lirc-send-msg (lirc-msg nil nil "PRIVMSG" + lirc-current-context + string)) + (lirc-display-message lirc-nick lirc-current-context string)) + (lirc-display-error "No current context."))))) (defun lirc-enter () "Enter current contents of line after prompt." @@ -259,13 +604,14 @@ (delete-region lirc-input-marker (point-max))))) - ;;; Mode ;; (defvar lirc-mode-map (let ((map (make-sparse-keymap))) (define-key map (kbd "RET") 'lirc-enter) + (define-key map (kbd "") 'lirc-cycle-contexts-forward) + (define-key map (kbd "") 'lirc-cycle-contexts-reverse) map)) (define-derived-mode lirc-mode text-mode "lirc" @@ -285,7 +631,6 @@ (switch-to-buffer "*lirc*")) (lirc-mode) (lirc-setup-buffer) - (lirc-connect) "Started LIRC.")