1 ;;; MURK --- Multiserver Unibuffer iRc Klient -*- lexical-binding:t -*-
3 ;; Copyright (C) 2024 plugd
5 ;; Author: plugd <plugd@thelambdalab.xyz>
6 ;; Created: 11 May 2024
9 ;; Homepage: http://thelambdalab.xyz/murk
10 ;; Package-Requires: ((emacs "26"))
12 ;; This file is not part of GNU Emacs.
14 ;; This program is free software: you can redistribute it and/or modify
15 ;; it under the terms of the GNU General Public License as published by
16 ;; the Free Software Foundation, either version 3 of the License, or
17 ;; (at your option) any later version.
19 ;; This program is distributed in the hope that it will be useful,
20 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
21 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 ;; GNU General Public License for more details.
24 ;; You should have received a copy of the GNU General Public License
25 ;; along with this file. If not, see <http://www.gnu.org/licenses/>.
37 "Multiserver Unibuffer iRc Klient"
40 (defcustom murk-default-nick "plugd"
43 (defcustom murk-default-quit-msg "Bye"
44 "Default quit message when none supplied.")
46 (defcustom murk-networks
47 '(("debug" "localhost" 6667 :notls)
48 ("libera" "irc.libera.chat" 6697)
49 ("tilde" "tilde.chat" 6697))
52 (defcustom murk-display-header t
53 "If non-nil, use buffer header to display information on current host and channel.")
60 '((t :inherit default))
61 "Face used for murk text.")
64 '((t :inherit font-lock-keyword-face))
65 "Face used for the prompt.")
68 '((t :inherit murk-context))
69 "Face used for the context name in the prompt.")
72 '((t :inherit shadow))
73 "Face used for faded murk text.")
75 (defface murk-timestamp
76 '((t :inherit shadow))
77 "Face used for timestamps.")
81 "Face used for murk error text.")
84 '((t :inherit warning))
85 "Face used for murk notice text.")
91 (defvar murk-version "Murk v0.0"
92 "Value of this string is used in response to CTCP version queries.")
94 (defvar murk-notice-prefix "-!-")
95 (defvar murk-error-prefix "!!!")
96 (defvar murk-prompt-string ">")
98 (defvar murk-debug nil
99 "If non-nil, enable debug mode.")
102 ;;; Utility procedures
105 (defun murk--filtered-join (&rest args)
106 (string-join (seq-filter (lambda (el) el) args) " "))
108 (defun murk--as-string (obj)
110 (with-output-to-string (princ obj))
114 ;;; Network processes
117 (defvar murk-connection-table nil
118 "An alist associating servers to connection information.
119 This includes the process and the response string.")
121 (defun murk-connection-process (server)
122 (elt (assoc server murk-connection-table) 1))
124 (defun murk-connection-nick (server)
125 (elt (assoc server murk-connection-table) 2))
127 (defun murk-set-connection-nick (server nick)
128 (setf (elt (assoc server murk-connection-table) 2) nick))
130 (defun murk-connection-response (server)
131 (elt (assoc server murk-connection-table) 3))
133 (defun murk-set-connection-response (server string)
134 (setf (elt (assoc server murk-connection-table) 3) string))
136 (defun murk-connection-new (server process nick)
137 (add-to-list 'murk-connection-table
138 (list server process nick "")))
140 (defun murk-connection-remove (server)
141 (setq murk-connection-table (assoc-delete-all server murk-connection-table)))
143 (defun murk-make-server-filter (server)
144 (lambda (proc string)
145 (dolist (line (split-string (concat (murk-connection-response server) string)
147 (if (string-suffix-p "\r" line)
148 (murk-eval-msg-string server (string-trim line))
149 (murk-set-connection-response server line)))))
151 (defun murk-make-server-sentinel (server)
152 (lambda (proc string)
153 (unless (equal "open" (string-trim string))
154 (murk-display-error "Disconnected from server.")
155 (murk-connection-remove server)
156 (murk-remove-server-contexts server)
157 (murk-render-prompt))))
159 (defun murk-start-process (server)
160 (let* ((row (assoc server murk-networks))
163 (flags (seq-drop row 3)))
164 (make-network-process :name (concat "murk-" server)
168 :filter (murk-make-server-filter server)
169 :sentinel (murk-make-server-sentinel server)
171 :tls-parameters (if (memq :notls flags)
173 (cons 'gnutls-x509pki
174 (gnutls-boot-parameters
175 :type 'gnutls-x509pki
179 (defvar murk-ping-period 60)
181 ;; IDEA: Have a single ping timer which pings all connected hosts
183 (defun murk-connect (server)
184 (if (assoc server murk-connection-table)
185 (murk-display-error "Already connected to this network.")
186 (if (not (assoc server murk-networks))
187 (murk-display-error "Network '" server "' is unknown.")
188 (let ((proc (murk-start-process server)))
189 (murk-connection-new server proc murk-default-nick))
190 (murk-send-msg server (murk-msg nil nil "USER" murk-default-nick 0 "*" murk-default-nick))
191 (murk-send-msg server (murk-msg nil nil "NICK" murk-default-nick))
192 (murk-add-context (list server))
193 (murk-render-prompt))))
195 (defun murk-send-msg (server msg)
197 (murk-display-string nil nil (murk-msg->string msg)))
198 (let ((proc (murk-connection-process server)))
199 (if (and proc (eq (process-status proc) 'open))
200 (process-send-string proc (concat (murk-msg->string msg) "\r\n"))
201 (murk-display-error "No server connection established."))))
207 (defun murk-msg (tags src cmd &rest params)
208 (list (murk--as-string tags)
209 (murk--as-string src)
210 (upcase (murk--as-string cmd))
211 (mapcar #'murk--as-string
212 (if (and params (listp (elt params 0)))
216 (defun murk-msg-tags (msg) (elt msg 0))
217 (defun murk-msg-src (msg) (elt msg 1))
218 (defun murk-msg-cmd (msg) (elt msg 2))
219 (defun murk-msg-params (msg) (elt msg 3))
220 (defun murk-msg-trail (msg)
221 (let ((params (murk-msg-params msg)))
223 (elt params (- (length params) 1)))))
225 (defvar murk-msg-regex
227 (opt (: "@" (group (* (not (or "\n" "\r" ";" " ")))))
229 (opt (: ":" (: (group (* (not (any space "!" "@"))))
230 (* (not (any space)))))
232 (group (: (* (not whitespace))))
234 (opt (group (+ not-newline))))
235 "Regex used to parse IRC messages.
236 Note that this regex is incomplete. Noteably, we discard the non-nick
237 portion of the source component of the message, as mURK doesn't use this.")
239 (defun murk-string->msg (string)
240 (if (string-match murk-msg-regex string)
241 (let* ((tags (match-string 1 string))
242 (src (match-string 2 string))
243 (cmd (upcase (match-string 3 string)))
244 (params-str (match-string 4 string))
247 (let* ((idx (cl-search ":" params-str))
248 (l (split-string (string-trim (substring params-str 0 idx))))
249 (r (if idx (list (substring params-str (+ 1 idx))) nil)))
252 (apply #'murk-msg (append (list tags src cmd) params)))
253 (error "Failed to parse string " string)))
255 (defun murk-msg->string (msg)
256 (let ((tags (murk-msg-tags msg))
257 (src (murk-msg-src msg))
258 (cmd (murk-msg-cmd msg))
259 (params (murk-msg-params msg)))
261 (if tags (concat "@" tags) nil)
262 (if src (concat ":" src) nil)
264 (if (> (length params) 1)
265 (string-join (seq-take params (- (length params) 1)) " ")
267 (if (> (length params) 0)
268 (concat ":" (elt params (- (length params) 1)))
272 ;;; Contexts and Servers
275 ;; A context is a list (server name ...) where name is a string
276 ;; representing either a channel name or nick, and server is a symbol
277 ;; identifying the server.
279 ;; Each server has a special context (server nil) used for messages
280 ;; to/from the server itself.
282 (defvar murk-contexts nil
283 "List of currently-available contexts.
284 The head of this list is always the current context.")
286 (defun murk-current-context ()
287 "Returns the current context."
292 (defun murk-contexts-equal (c1 c2)
293 (if (murk-server-context-p c1)
294 (and (murk-server-context-p c2)
295 (equal (murk-context-server c1)
296 (murk-context-server c2)))
297 (and (not (murk-server-context-p c2))
298 (equal (seq-take c1 2)
301 (defun murk-context-server (ctx) (elt ctx 0))
302 (defun murk-context-name (ctx) (elt ctx 1))
303 (defun murk-context-users (ctx) (seq-drop ctx 2))
304 (defun murk-server-context-p (ctx) (not (cdr ctx)))
306 (defun murk-add-context (ctx)
307 (add-to-list 'murk-contexts ctx))
309 (defun murk-remove-context (ctx)
313 (murk-contexts-equal this-ctx ctx))
316 (defun murk-remove-server-contexts (server)
318 (assoc-delete-all server murk-contexts)))
320 (defun murk-context->string (ctx)
321 (if (murk-server-context-p ctx)
322 (concat "[" (murk-context-server ctx) "]")
323 (concat (murk-context-name ctx) "@"
324 (murk-context-server ctx))))
326 (defun murk-get-context (server &optional name)
328 (assoc server murk-contexts)
329 (let ((test-ctx (list server name)))
330 (seq-find (lambda (ctx)
331 (equal (seq-take ctx 2) test-ctx))
337 (defun murk-render-prompt ()
338 (with-current-buffer "*murk*"
339 (let ((update-point (= murk-input-marker (point)))
340 (update-window-points (mapcar (lambda (w)
341 (list (= (window-point w) murk-input-marker)
343 (get-buffer-window-list nil nil t))))
345 (set-marker-insertion-type murk-prompt-marker nil)
346 (set-marker-insertion-type murk-input-marker t)
347 (let ((inhibit-read-only t))
348 (delete-region murk-prompt-marker murk-input-marker)
349 (goto-char murk-prompt-marker)
351 (propertize (let ((ctx (murk-current-context)))
353 (murk-context->string ctx)
357 (propertize murk-prompt-string
360 (propertize " " ; Need this to be separate to mark it as rear-nonsticky
363 (set-marker-insertion-type murk-input-marker nil))
365 (goto-char murk-input-marker))
366 (dolist (v update-window-points)
368 (set-window-point (cadr v) murk-input-marker))))))
370 (defvar murk-prompt-marker nil
371 "Marker for prompt position in murk buffer.")
373 (defvar murk-input-marker nil
374 "Marker for prompt position in murk buffer.")
376 (defun murk-setup-header ()
377 (with-current-buffer "*murk*"
378 (setq-local header-line-format
380 (let* ((ctx (murk-current-context)))
382 (let ((server (murk-context-server ctx)))
384 "Network: " server ", "
385 (if (murk-server-context-p ctx)
389 (murk-context-name ctx)
392 (length (murk-context-users ctx)))
394 "No connection")))))))
396 (defun murk-setup-buffer ()
397 (with-current-buffer (get-buffer-create "*murk*")
398 (setq-local scroll-conservatively 1)
399 (setq-local buffer-invisibility-spec nil)
400 (if (markerp murk-prompt-marker)
401 (set-marker murk-prompt-marker (point-max))
402 (setq murk-prompt-marker (point-max-marker)))
403 (if (markerp murk-input-marker)
404 (set-marker murk-input-marker (point-max))
405 (setq murk-input-marker (point-max-marker)))
406 (goto-char (point-max))
408 (if murk-display-header
409 (murk-setup-header))))
411 (defun murk-clear-buffer ()
412 "Completely erase all non-prompt and non-input text from murk buffer."
413 (with-current-buffer "*murk*"
414 (let ((inhibit-read-only t))
415 (delete-region (point-min) murk-prompt-marker))))
418 ;;; Output formatting and highlighting
421 (defun murk--fill-strings (col indent &rest strings)
423 (setq buffer-invisibility-spec nil)
424 (let ((fill-column col)
425 (adaptive-fill-regexp (rx-to-string `(= ,indent anychar))))
426 (apply #'insert strings)
427 (fill-region (point-min) (point-max) nil t)
430 (defun murk-display-string (context prefix &rest strings)
431 (with-current-buffer "*murk*"
433 (goto-char murk-prompt-marker)
434 (let* ((inhibit-read-only t)
435 (old-pos (marker-position murk-prompt-marker))
436 (padded-timestamp (concat (format-time-string "%H:%M ")))
437 (padded-prefix (if prefix (concat prefix " ") ""))
438 (context-atom (if context (intern (murk-context->string context)) nil)))
439 (insert-before-markers
442 (+ (length padded-timestamp)
443 (length padded-prefix))
444 (propertize padded-timestamp
445 'face 'murk-timestamp
448 'invisible context-atom)
449 (propertize padded-prefix
452 'invisible context-atom)
454 (propertize (concat (apply #'murk-buttonify-urls strings) "\n")
457 'invisible context-atom)))))))
458 (murk-scroll-windows-to-last-line))
460 (defun murk-display-message (server from to text)
461 (let ((context (if (string-prefix-p "#" to)
462 (murk-get-context server to)
463 (murk-get-context server))))
466 (if (murk-server-context-p context)
467 (concat "[" from " -> " to "]")
468 (concat (murk-context->string context) " <" from ">"))
471 (defun murk-display-notice (context &rest notices)
474 (propertize murk-notice-prefix 'face 'murk-notice)
475 (apply #'concat notices)))
477 (defun murk-display-error (&rest messages)
480 (propertize murk-error-prefix 'face 'murk-error)
481 (apply #'concat messages)))
483 (defun murk--start-of-final-line ()
484 (with-current-buffer "*murk*"
486 (goto-char (point-max))
487 (line-beginning-position))))
489 (defun murk-scroll-windows-to-last-line ()
490 (with-current-buffer "*murk*"
491 (dolist (window (get-buffer-window-list))
492 (if (>= (window-point window) (murk--start-of-final-line))
493 (with-selected-window window
498 (defconst murk-url-regex
502 (group (or (+ (any alnum "." "-"))
503 (+ (any alnum ":"))))
504 (opt (group (: ":" (+ digit))))
507 (* (any alnum "-/.,#:%=&_?~@+"))
508 (any alnum "-/#:%=&_~@+")))))))
509 "Imperfect regex used to find URLs in plain text.")
511 (defun murk-click-url (button)
512 (browse-url (button-get button 'url)))
514 (defun murk-buttonify-urls (&rest strings)
515 "Turn substrings which look like urls in STRING into clickable buttons."
517 (apply #'insert strings)
518 (goto-char (point-min))
519 (while (re-search-forward murk-url-regex nil t)
520 (let ((url (match-string 0)))
521 (make-text-button (match-beginning 0)
523 'action #'murk-click-url
527 'help-echo "Open URL in browser.")))
530 (defun murk-add-formatting (string)
533 (goto-char (point-min))
538 (prev-point (point)))
539 (while (re-search-forward (rx (or (any "\x02\x1D\x1F\x1E\x0F")
540 (: "\x03" (+ digit) (opt "," (* digit)))))
542 (let ((beg (+ (match-beginning 0) 1)))
544 (add-face-text-property prev-point beg '(:weight bold)))
546 (add-face-text-property prev-point beg '(:slant italic)))
548 (add-face-text-property prev-point beg '(:underline t)))
550 (add-face-text-property prev-point beg '(:strike-through t)))
551 (pcase (match-string 0)
552 ("\x02" (setq bold (not bold)))
553 ("\x1D" (setq italics (not italics)))
554 ("\x1F" (setq underline (not underline)))
555 ("\x1E" (setq strikethrough (not strikethrough)))
560 (setq strikethrough nil))
562 (delete-region (match-beginning 0) (match-end 0))
563 (setq prev-point (point)))))
567 ;;; Message evaluation
570 (defun murk-eval-msg-string (server string)
572 (murk-display-string nil nil string))
573 (let* ((msg (murk-string->msg string)))
574 (pcase (murk-msg-cmd msg)
576 (murk-send-msg server
577 (murk-msg nil nil "PONG" (murk-msg-params msg))))
582 (let* ((params (murk-msg-params msg))
583 (nick (elt params 0))
584 (text (string-join (seq-drop params 1) " ")))
585 (murk-set-connection-nick server nick)
586 (murk-display-notice nil text)))
588 ((rx (= 3 (any digit)))
589 (murk-display-notice nil (mapconcat 'identity (cdr (murk-msg-params msg)) " ")))
592 (guard (equal (murk-connection-nick server)
593 (murk-msg-src msg))))
594 (let ((channel (car (murk-msg-params msg))))
595 (murk-add-context (list server channel))
596 (murk-display-notice (murk-current-context)
597 "Joining channel " channel " on " server)
598 (murk-render-prompt)))
601 (guard (equal (murk-connection-nick server)
602 (murk-msg-src msg))))
603 (let ((channel (car (murk-msg-params msg))))
604 (murk-display-notice (murk-current-context) "Left channel " channel)
605 (murk-remove-context (list server channel))
606 (murk-render-prompt)))
609 (let ((nick (murk-msg-src msg))
610 (reason (mapconcat 'identity (murk-msg-params msg) " ")))
613 (murk-display-notice nil nick " quit: " reason))))
616 (let* ((from (murk-msg-src msg))
617 (params (murk-msg-params msg))
619 (text (cadr params)))
622 (let ((version-string (concat murk-version " - running on GNU Emacs " emacs-version)))
623 (murk-send-msg server
624 (murk-msg nil nil "NOTICE"
625 (list from (concat "\01VERSION "
628 (murk-display-notice nil "CTCP version request received from "
631 ((rx (let ping (: "\01PING " (* (not "\01")) "\01")))
632 (murk-send-msg server (lurk-msg nil nil "NOTICE" (list from ping)))
633 (murk-display-notice nil "CTCP ping received from " from " on " server))
636 (murk-display-notice nil "CTCP userinfo request from " from
637 " on " server " (no response sent)"))
640 (murk-display-notice nil "CTCP clientinfo request from " from
641 " on " server " (no response sent)"))
643 ((rx (: "\01ACTION " (let action-text (* (not "\01"))) "\01"))
644 (murk-display-action from to action-text))
647 (murk-display-message server from to text)))))
650 (murk-display-notice nil (murk-msg->string msg))))))
655 (defvar murk-command-table
656 '(("DEBUG" "Toggle debug mode on/off." murk-command-debug murk-boolean-completions)
657 ("HEADER" "Toggle display of header." murk-command-header murk-boolean-completions)
658 ("NETWORKS" "List known IRC networks." murk-command-networks)
659 ("CONNECT" "Connect to an IRC network." murk-command-connect murk-network-completions)
660 ("QUIT" "Disconnect from current network." murk-command-quit)
661 ("JOIN" "Join one or more channels." murk-command-join)
662 ("PART" "Leave channel." murk-command-part murk-context-completions)
663 ("NICK" "Change nick." murk-command-nick)
664 ("MSG" "Send private message to user." murk-command-msg murk-nick-completions)
665 ("CLEAR" "Clear buffer text." murk-command-clear murk-context-completions)
666 ("HELP" "Display help on client commands." murk-command-help murk-help-completions))
667 "Table of commands explicitly supported by murk.")
669 (defun murk-boolean-completions ()
672 (defun murk-network-completions ()
673 (mapcar (lambda (row) (car row)) murk-networks))
675 (defun murk-command-help (params)
677 (let* ((cmd-str (upcase (car params)))
678 (row (assoc cmd-str murk-command-table #'equal)))
681 (murk-display-notice nil "Help for \x02" cmd-str "\x02:")
682 (murk-display-notice nil " " (elt row 1)))
683 (murk-display-notice nil "No such (client-interpreted) command.")))
684 (murk-display-notice nil "Client-interpreted commands:")
685 (dolist (row murk-command-table)
686 (murk-display-notice nil " \x02" (elt row 0) "\x02: " (elt row 1)))
687 (murk-display-notice nil "Use /HELP COMMAND to display information about a specific command.")))
689 (defun murk-command-debug (params)
692 (if (equal (upcase (car params)) "ON")
696 (murk-display-notice nil "Debug mode now " (if murk-debug "on" "off") "."))
698 (defun murk-command-header (params)
701 (equal (upcase (car params)) "ON")
702 (not header-line-format))
705 (murk-display-notice nil "Header enabled."))
706 (setq-local header-line-format nil)
707 (murk-display-notice nil "Header disabled.")))
709 (defun murk-command-clear (params)
712 (dolist (context params)
713 (murk-clear-context context))))
715 (defun murk-command-connect (params)
717 (let ((network (car params)))
718 (murk-display-notice nil "Attempting to connect to " network "...")
719 (murk-connect network))
720 (murk-display-notice nil "Usage: /connect <network>")))
722 (defun murk-command-networks (params)
723 (murk-display-notice nil "Currently-known networks:")
724 (dolist (row murk-networks)
725 (seq-let (network server port &rest others) row
726 (murk-display-notice nil "\t" network
728 " " (number-to-string port) "]")))
729 (murk-display-notice nil "(Modify the `murk-networks' variable to add more.)"))
731 (defun murk-command-quit (params)
732 (let ((ctx (murk-current-context)))
734 (murk-display-error "No current context.")
735 (let ((quit-msg (if params (string-join params " ") murk-default-quit-msg)))
737 (murk-context-server ctx)
738 (murk-msg nil nil "QUIT" quit-msg))))))
740 (defun murk-command-join (params)
742 (let ((server (murk-context-server (murk-current-context))))
743 (dolist (channel params)
744 (murk-send-msg server (murk-msg nil nil "JOIN" channel))))
745 (murk-display-notice nil "Usage: /join channel [channel2 ...]")))
747 (defun murk-command-part (params)
748 (let* ((server (murk-context-server (murk-current-context)))
751 (murk-context-name (murk-current-context)))))
753 (murk-send-msg server (murk-msg nil nil "PART" channel))
754 (murk-display-error "No current channel to leave."))))
756 (defun murk-command-msg (params)
757 (let ((server (murk-context-server (murk-current-context))))
758 (if (and params (>= (length params) 2))
759 (let ((to (car params))
760 (text (string-join (cdr params) " ")))
761 (murk-send-msg server (lurk-msg nil nil "PRIVMSG" to text))
762 (murk-display-message server
763 (murk-connection-nick server)
765 (murk-display-notice nil "Usage: /msg <nick> <message>"))))
770 (defun murk-enter-string (string)
771 (if (string-prefix-p "/" string)
773 ((rx (: "/" (let cmd-str (+ (not whitespace)))
775 (let params-str (+ anychar))
777 (let ((command-row (assoc (upcase cmd-str) murk-command-table #'equal))
778 (params (if params-str
779 (split-string params-str nil t)
781 (if (and command-row (elt command-row 2))
782 (funcall (elt command-row 2) params)
784 (murk-context-server (murk-current-context))
785 (murk-msg nil nil (upcase cmd-str) params)))))
787 (murk-display-error "Badly formed command.")))
788 (unless (string-empty-p string)
789 (if (murk-current-context)
790 (let ((server (murk-context-server (murk-current-context))))
791 (murk-send-msg server
792 (murk-msg nil nil "PRIVMSG"
793 (murk-context-name (murk-current-context))
795 (murk-display-message server
797 (murk-context->string (murk-current-context))
799 (murk-display-error "No current context.")))))
805 (defvar murk-history nil
806 "Commands and messages sent in current session.")
808 (defvar murk-history-index nil)
810 (defun murk-history-cycle (delta)
812 (with-current-buffer "*murk*"
813 (if murk-history-index
814 (setq murk-history-index
816 (min (- (length murk-history) 1)
817 (+ delta murk-history-index))))
818 (setq murk-history-index 0))
819 (delete-region murk-input-marker (point-max))
820 (insert (elt murk-history murk-history-index)))))
823 ;;; Interactive commands
827 "Enter current contents of line after prompt."
829 (with-current-buffer "*murk*"
830 (let ((line (buffer-substring murk-input-marker (point-max))))
831 (push line murk-history)
832 (setq murk-history-index nil)
833 (let ((inhibit-read-only t))
834 (delete-region murk-input-marker (point-max)))
835 (murk-enter-string line))))
837 (defun murk-history-next ()
839 (murk-history-cycle -1))
841 (defun murk-history-prev ()
843 (murk-history-cycle +1))
845 (defun murk-complete-input ()
847 (let ((completion-ignore-case t))
848 (when (>= (point) murk-input-marker)
849 (pcase (buffer-substring murk-input-marker (point))
850 ((rx (: "/" (let cmd-str (+ (not whitespace))) (+ " ") (* (not whitespace)) string-end))
851 (let ((space-idx (save-excursion
852 (re-search-backward " " murk-input-marker t)))
853 (table-row (assoc (upcase cmd-str) murk-command-table #'equal)))
854 (if (and table-row (elt table-row 3))
855 (let* ((completions-nospace (funcall (elt table-row 3)))
856 (completions (mapcar (lambda (el) (concat el " ")) completions-nospace)))
857 (completion-in-region (+ 1 space-idx) (point) completions)))))
858 ((rx (: "/" (* (not whitespace)) string-end))
859 (message (buffer-substring murk-input-marker (point)))
860 (completion-in-region murk-input-marker (point)
861 (mapcar (lambda (row) (concat "/" (car row) " "))
862 murk-command-table)))
864 (let* ((end (max murk-input-marker (point)))
865 (space-idx (save-excursion
866 (re-search-backward " " murk-input-marker t)))
867 (start (if space-idx (+ 1 space-idx) murk-input-marker)))
868 (unless (string-prefix-p "/" (buffer-substring start end))
869 (let* ((users (murk-get-context-users murk-current-context))
871 (lambda (u) (car (split-string u "@" t)))
873 (completion-in-region start end users-no@)))))))))
878 (defvar murk-mode-map
879 (let ((map (make-sparse-keymap)))
880 (define-key map (kbd "RET") 'murk-enter)
881 (define-key map (kbd "TAB") 'murk-complete-input)
882 (define-key map (kbd "<C-up>") 'murk-history-prev)
883 (define-key map (kbd "<C-down>") 'murk-history-next)
884 (when (fboundp 'evil-define-key*)
885 (evil-define-key* 'motion map
886 (kbd "TAB") 'murk-complete-input))
889 (define-derived-mode murk-mode text-mode "murk"
890 "Major mode for murk.")
892 (when (fboundp 'evil-set-initial-state)
893 (evil-set-initial-state 'murk-mode 'insert))
895 ;;; Main start procedure
899 "Start murk or just switch to the murk buffer if one already exists."
901 (if (get-buffer "*murk*")
902 (switch-to-buffer "*murk*")
903 (switch-to-buffer "*murk*")
909 ;;; murk.el ends here