(sh-font-lock-syntactic-keywords): Make $@ and $? into sexps.
[bpt/emacs.git] / lisp / net / rcirc.el
CommitLineData
bd43c990
RS
1;;; rcirc.el --- default, simple IRC client.
2
3;; Copyright (C) 2005 Free Software Foundation, Inc.
4
5;; Author: Ryan Yeske
bd43c990
RS
6;; URL: http://www.nongnu.org/rcirc
7;; Keywords: comm
8
9;; This file is not currently part of GNU Emacs.
10
11;; This file is free software; you can redistribute it and/or modify
12;; it under the terms of the GNU General Public License as published by
13;; the Free Software Foundation; either version 2, or (at your option)
14;; any later version.
15
16;; This file is distributed in the hope that it will be useful,
17;; but WITHOUT ANY WARRANTY; without even the implied warranty of
18;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19;; GNU General Public License for more details.
20
21;; You should have received a copy of the GNU General Public License
22;; along with GNU Emacs; see the file COPYING. If not, write to
23;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
24;; Boston, MA 02111-1307, USA.
25
26;;; Commentary:
27
28;; rcirc is an Internet Relay Chat (IRC) client for Emacs
29
30;; IRC is a form of instant communication over the Internet. It is
31;; mainly designed for group (many-to-many) communication in
32;; discussion forums called channels, but also allows one-to-one
33;; communication.
34
bd43c990
RS
35;; Open a new irc connection with:
36;; M-x irc RET
37
38;;; Code:
39
40(require 'ring)
41(require 'time-date)
42(eval-when-compile (require 'cl))
43
44(defvar rcirc-server "irc.freenode.net"
45 "The default server to connect to.")
46
47(defvar rcirc-port 6667
48 "The default port to connect to.")
49
50(defvar rcirc-nick (user-login-name)
51 "Your nick.")
52
53(defvar rcirc-user-name (user-login-name)
54 "Your user name sent to the server when connecting.")
55
56(defvar rcirc-user-full-name (if (string= (user-full-name) "")
57 rcirc-user-name
58 (user-full-name))
59 "The full name sent to the server when connecting.")
60
61(defvar rcirc-startup-channels-alist nil
62 "Alist of channels to join at startup.
63Each element looks like (REGEXP . CHANNEL-LIST).")
64
65(defvar rcirc-fill-flag t
66 "*Non-nil means fill messages printed in channel buffers.")
67
68(defvar rcirc-fill-column nil
69 "*If non-nil, fill to this column, otherwise use value of `fill-column'.")
70
71(defvar rcirc-fill-prefix nil
72 "*Text to insert before filled lines.
73If nil, calculate the prefix dynamically to line up text
74underneath each nick.")
75
76(defvar rcirc-ignore-channel-activity nil
77 "If non-nil, ignore activity in this channel.")
78(make-variable-buffer-local 'rcirc-ignore-channel-activity)
79
80(defvar rcirc-ignore-all-activity-flag nil
81 "*Non-nil means track activity, but do not display it in the modeline.")
82
83(defvar rcirc-time-format "%H:%M "
84 "*Describes how timestamps are printed.
85Used as the first arg to `format-time-string'.")
86
87(defvar rcirc-input-ring-size 1024
88 "*Size of input history ring.")
89
90(defvar rcirc-read-only-flag t
91 "*Non-nil means make text in irc buffers read-only.")
92
93(defvar rcirc-buffer-maximum-lines nil
94 "*The maximum size in lines for rcirc buffers.
95Channel buffers are truncated from the top to be no greater than this
96number. If zero or nil, no truncating is done.")
97
98(defvar rcirc-authinfo-file-name
99 "~/.rcirc-authinfo"
100 "File containing rcirc authentication passwords.
101The file consists of a single list, with each element itself a
102list with a SERVER-REGEXP string, a NICK-REGEXP string, a METHOD
103and the remaining method specific ARGUMENTS. The valid METHOD
104symbols are `nickserv', `chanserv' and `bitlbee'.
105
106The required ARGUMENTS for each METHOD symbol are:
107 `nickserv': PASSWORD
108 `chanserv': CHANNEL PASSWORD
109 `bitlbee': PASSWORD
110
111Example:
112 ((\"freenode\" \"bob\" nickserv \"p455w0rd\")
113 (\"freenode\" \"bob\" chanserv \"#bobland\" \"passwd99\")
114 (\"bitlbee\" \"robert\" bitlbee \"sekrit\"))")
115
116(defvar rcirc-auto-authenticate-flag (file-readable-p rcirc-authinfo-file-name)
117 "*Non-nil means automatically send authentication string to server.
118See also `rcirc-authinfo-file-name'.")
119
120(defvar rcirc-print-hooks nil
121 "Hook run after text is printed.
122Called with 5 arguments, PROCESS, SENDER, RESPONSE, TARGET and TEXT.")
123
124(defvar rcirc-prompt "%n> "
125 "Prompt string to use in irc buffers.
126
127The following replacements are made:
128%n is your nick.
129%s is the server.
130%t is the buffer target, a channel or a user.
131
132Setting this alone will not affect the prompt;
133use `rcirc-update-prompt' after changing this variable.")
134
135(defvar rcirc-prompt-start-marker nil)
136(defvar rcirc-prompt-end-marker nil)
137
138(defvar rcirc-nick-table nil)
139
140(defvar rcirc-activity nil
141 "List of channels with unviewed activity.")
142
143(defvar rcirc-activity-string ""
144 "String displayed in modeline representing `rcirc-activity'.")
145(put 'rcirc-activity-string 'risky-local-variable t)
146
147(defvar rcirc-process nil
148 "The server process associated with this buffer.")
149
150(defvar rcirc-target nil
151 "The channel or user associated with this buffer.")
152
153(defvar rcirc-channels nil
154 "Joined channels.")
155
156(defvar rcirc-private-chats nil
157 "Private chats open.")
158
159(defvar rcirc-urls nil
160 "List of urls seen in the current buffer.")
161
162(defvar rcirc-keepalive-seconds 60
163 "Number of seconds between keepalive pings.")
164
165\f
166(defun rcirc-version (&optional here)
167 "Return rcirc version string.
168If optional argument HERE is non-nil, insert string at point."
169 (interactive "P")
e636ae15 170 (let ((version "rcirc.el 0.9 $Revision: 1.3 $"))
bd43c990
RS
171 (if here
172 (insert version)
173 (if (interactive-p)
174 (message "%s" version)
175 version))))
176
177(defvar rcirc-startup-channels nil)
178;;;###autoload
179(defun rcirc (&optional server port nick channels)
180 "Connect to IRC.
181
182If any of the the optional SERVER, PORT, NICK or CHANNELS are not
183supplied, they are taken from the variables `rcirc-server',
184`rcirc-port', `rcirc-nick', and `rcirc-startup-channels',
185respectively."
186 (interactive (list (read-string "IRC Server: " rcirc-server)
187 (read-string "IRC Port: " (number-to-string rcirc-port))
188 (read-string "IRC Nick: " rcirc-nick)))
189 (or server (setq server rcirc-server))
190 (or port (setq port rcirc-port))
191 (or nick (setq nick rcirc-nick))
192 (or channels
193 (setq channels
194 (if (interactive-p)
195 (delete ""
196 (split-string
197 (read-string "Channels: "
198 (mapconcat 'identity
199 (rcirc-startup-channels server)
200 " "))
201 "[, ]+"))
202 (rcirc-startup-channels server))))
203 (or global-mode-string (setq global-mode-string '("")))
204 (and (not (memq 'rcirc-activity-string global-mode-string))
205 (setq global-mode-string
206 (append global-mode-string '(rcirc-activity-string))))
207 (add-hook 'window-configuration-change-hook 'rcirc-update-activity)
208 (rcirc-connect server port nick rcirc-user-name rcirc-user-full-name
209 channels))
210
211;;;###autoload
212(defalias 'irc 'rcirc)
213
214\f
215(defvar rcirc-process-output nil)
216(defvar rcirc-last-buffer nil)
217(defvar rcirc-topic nil)
218(defvar rcirc-keepalive-timer nil)
219(make-variable-buffer-local 'rcirc-topic)
220(defun rcirc-connect (server port nick user-name full-name startup-channels)
221 "Return a connection to SERVER on PORT.
222
223User will identify using the values of NICK, USER-NAME and
224FULL-NAME. The variable list of channel names in
225STARTUP-CHANNELS will automatically be joined on startup."
226 (save-excursion
227 (message "Connecting to %s..." server)
228 (let* ((inhibit-eol-conversion)
229 (port-number (if (stringp port)
230 (string-to-number port)
231 port))
232 (process (open-network-stream server nil server port-number)))
233 ;; set up process
234 (set-process-coding-system process 'raw-text 'raw-text)
235 (set-process-filter process 'rcirc-filter)
236 (switch-to-buffer (concat "*" (process-name process) "*"))
237 (set-process-buffer process (current-buffer))
238 (set-process-sentinel process 'rcirc-sentinel)
239 (rcirc-mode process nil)
240 (make-local-variable 'rcirc-nick-table)
241 (setq rcirc-nick-table (make-hash-table :test 'equal))
242 (make-local-variable 'rcirc-server)
243 (setq rcirc-server server)
244 (make-local-variable 'rcirc-nick)
245 (setq rcirc-nick nick)
246 (make-local-variable 'rcirc-process-output)
247 (setq rcirc-process-output nil)
248 (make-local-variable 'rcirc-last-buffer)
249 (setq rcirc-last-buffer (current-buffer))
250 (make-local-variable 'rcirc-channels)
251 (setq rcirc-channels nil)
252 (make-local-variable 'rcirc-private-chats)
253 (setq rcirc-private-chats nil)
254 (make-local-variable 'rcirc-startup-channels)
255 (setq rcirc-startup-channels startup-channels)
256
257 ;; identify
258 (rcirc-send-string process (concat "NICK " nick))
259 (rcirc-send-string process (concat "USER " user-name
260 " hostname servername :"
261 full-name))
262
263 ;; setup ping timer if necessary
264 (unless rcirc-keepalive-timer
265 (setq rcirc-keepalive-timer
266 (run-at-time 0 rcirc-keepalive-seconds 'rcirc-keepalive)))
267
268 (message "Connecting to %s...done" server)
269
270 ;; return process object
271 process)))
272
273(defun rcirc-keepalive ()
274 "Send keep alive pings to active rcirc processes."
275 (if (rcirc-process-list)
276 (mapc (lambda (process)
277 (with-current-buffer (process-buffer process)
278 (rcirc-send-string process (concat "PING " rcirc-server))))
279 (rcirc-process-list))
280 (cancel-timer rcirc-keepalive-timer)
281 (setq rcirc-keepalive-timer nil)))
282
283(defvar rcirc-log-buffer "*rcirc log*")
284(defvar rcirc-log-p nil
285 "If non-nil, write information to `rcirc-log-buffer'.")
286(defun rcirc-log (process text)
287 "Add an entry to the debug log including PROCESS and TEXT.
288Debug text is written to `rcirc-log-buffer' if `rcirc-log-p' is
289non-nil."
290 (when rcirc-log-p
291 (save-excursion
292 (save-window-excursion
293 (set-buffer (get-buffer-create rcirc-log-buffer))
294 (goto-char (point-max))
295 (insert (concat
296 "["
297 (format-time-string "%Y-%m-%dT%T ") (process-name process)
298 "] "
299 text))))))
300
301(defvar rcirc-sentinel-hooks nil
302 "Hook functions called when the process sentinel is called.
303Functions are called with PROCESS and SENTINEL arguments.")
304
305(defun rcirc-sentinel (process sentinel)
306 "Called when PROCESS receives SENTINEL."
307 (let ((sentinel (replace-regexp-in-string "\n" "" sentinel)))
308 (rcirc-log process (format "SENTINEL: %S %S\n" process sentinel))
309 (with-current-buffer (process-buffer process)
310 (dolist (target (append rcirc-channels
311 rcirc-private-chats
312 (list (current-buffer))))
313 (rcirc-print process "rcirc.el" "ERROR" target
314 (format "%s: %s (%S)"
315 (process-name process)
316 sentinel
317 (process-status process)) t)
318 ;; remove the prompt from buffers
319 (with-current-buffer (if (eq target (current-buffer))
320 (current-buffer)
321 (rcirc-get-buffer process target))
322 (let ((inhibit-read-only t))
323 (delete-region rcirc-prompt-start-marker
324 rcirc-prompt-end-marker)))))
325 (run-hook-with-args 'rcirc-sentinel-hooks process sentinel)))
326
327(defun rcirc-process-list ()
328 "Return a list of rcirc processes."
329 (let (ps)
330 (mapc (lambda (p)
331 (when (process-buffer p)
332 (with-current-buffer (process-buffer p)
333 (when (eq major-mode 'rcirc-mode)
334 (setq ps (cons p ps))))))
335 (process-list))
336 ps))
337
338(defvar rcirc-receive-message-hooks nil
339 "Hook functions run when a message is recieved from server.
340Function is called with PROCESS COMMAND SENDER ARGS and LINE.")
341(defun rcirc-filter (process output)
342 "Called when PROCESS receives OUTPUT."
343 (rcirc-log process output)
344 (with-current-buffer (process-buffer process)
345 (setq rcirc-process-output (concat rcirc-process-output output))
346 (when (= (aref rcirc-process-output
347 (1- (length rcirc-process-output))) ?\n)
348 (mapc (lambda (line)
349 (rcirc-process-server-response process line))
350 (delete "" (split-string rcirc-process-output "[\n\r]")))
351 (setq rcirc-process-output nil))))
352
353(defvar rcirc-trap-errors nil)
354(defun rcirc-process-server-response (process text)
355 (if rcirc-trap-errors
356 (condition-case err
357 (rcirc-process-server-response-1 process text)
358 (error
359 (rcirc-print process "RCIRC" "ERROR" nil
360 (format "rcirc: error processing: \"%s\" %s" text err))))
361 (rcirc-process-server-response-1 process text)))
362
363(defun rcirc-process-server-response-1 (process text)
364 (if (string-match "^\\(:\\([^ ]+\\) \\)?\\([^ ]+\\) \\(.+\\)$" text)
365 (let* ((sender (match-string 2 text))
366 (cmd (match-string 3 text))
367 (args (match-string 4 text))
368 (handler (intern-soft (concat "rcirc-handler-" cmd))))
369 (string-match "^\\([^:]*\\):?\\(.+\\)?$" args)
370 (let* ((args1 (match-string 1 args))
371 (args2 (match-string 2 args))
372 (args (append (delete "" (split-string args1 " "))
373 (list args2))))
374 (if (not (fboundp handler))
375 (rcirc-handler-generic process cmd sender args text)
376 (funcall handler process sender args text))
377 (run-hook-with-args 'rcirc-receive-message-hooks
378 process cmd sender args text)))
379 (message "UNHANDLED: %s" text)))
380
381(defun rcirc-handler-generic (process command sender args text)
382 "Generic server response handler."
383 (rcirc-print process sender command nil
384 (mapconcat 'identity (cdr args) " ")))
385
386(defun rcirc-send-string (process string)
387 "Send PROCESS a STRING plus a newline."
388 (let ((string (concat (encode-coding-string string
389 buffer-file-coding-system)
390 "\n")))
391 (rcirc-log process string)
392 (process-send-string process string)))
393
394(defun rcirc-server (process)
395 "Return PROCESS server, given by the 001 response."
396 (with-current-buffer (process-buffer process)
397 rcirc-server))
398
399(defun rcirc-nick (process)
400 "Return PROCESS nick."
401 (with-current-buffer (process-buffer process)
402 rcirc-nick))
403
404(defvar rcirc-max-message-length 450
405 "Messages longer than this value will be split.")
406
407(defun rcirc-send-message (process target message &optional noticep)
408 "Send TARGET associated with PROCESS a privmsg with text MESSAGE.
409If NOTICEP is non-nil, send a notice instead of privmsg."
410 ;; max message length is 512 including CRLF
411 (let* ((response (if noticep "NOTICE" "PRIVMSG"))
412 (oversize (> (length message) rcirc-max-message-length))
413 (text (if oversize
414 (substring message 0 rcirc-max-message-length)
415 message))
416 (text (if (string= text "")
417 " "
418 text))
419 (more (if oversize
420 (substring message rcirc-max-message-length))))
421 (rcirc-print process (rcirc-nick process) response target text)
422 (rcirc-send-string process (concat response " " target " :" text))
423 (if more
424 (rcirc-send-message process target more noticep))))
425
426(defvar rcirc-input-ring nil)
427(defvar rcirc-input-ring-index 0)
428(defun rcirc-prev-input-string (arg)
429 (ring-ref rcirc-input-ring (+ rcirc-input-ring-index arg)))
430
431(defun rcirc-insert-prev-input (arg)
432 (interactive "p")
433 (when (<= rcirc-prompt-end-marker (point))
434 (delete-region rcirc-prompt-end-marker (point-max))
435 (insert (rcirc-prev-input-string 0))
436 (setq rcirc-input-ring-index (1+ rcirc-input-ring-index))))
437
438(defun rcirc-insert-next-input (arg)
439 (interactive "p")
440 (when (<= rcirc-prompt-end-marker (point))
441 (delete-region rcirc-prompt-end-marker (point-max))
442 (setq rcirc-input-ring-index (1- rcirc-input-ring-index))
443 (insert (rcirc-prev-input-string -1))))
444
445(defvar rcirc-nick-completions nil)
446(defvar rcirc-nick-completion-start-offset nil)
447(defun rcirc-complete-nick ()
448 "Cycle through nick completions from list of nicks in channel."
449 (interactive)
450 (if (eq last-command 'rcirc-complete-nick)
451 (setq rcirc-nick-completions
452 (append (cdr rcirc-nick-completions)
453 (list (car rcirc-nick-completions))))
454 (setq rcirc-nick-completion-start-offset
455 (- (save-excursion
456 (if (re-search-backward " " rcirc-prompt-end-marker t)
457 (1+ (point))
458 rcirc-prompt-end-marker))
459 rcirc-prompt-end-marker))
460 (setq rcirc-nick-completions
461 (let ((completion-ignore-case t))
462 (all-completions
463 (buffer-substring
464 (+ rcirc-prompt-end-marker
465 rcirc-nick-completion-start-offset)
466 (point))
467 (mapcar (lambda (x) (cons x nil))
468 (rcirc-channel-nicks rcirc-process
469 (rcirc-buffer-target)))))))
470 (let ((completion (car rcirc-nick-completions)))
471 (when completion
472 (delete-region (+ rcirc-prompt-end-marker
473 rcirc-nick-completion-start-offset)
474 (point))
475 (insert (concat completion
476 (if (= (+ rcirc-prompt-end-marker
477 rcirc-nick-completion-start-offset)
478 rcirc-prompt-end-marker)
479 ": "))))))
480
481(defun rcirc-buffer-target (&optional buffer)
482 "Return the name of target for BUFFER.
483If buffer is nil, return the target of the current buffer."
484 (with-current-buffer (or buffer (current-buffer))
485 rcirc-target))
486
487(defvar rcirc-mode-map (make-sparse-keymap)
488 "Keymap for rcirc mode.")
489
490(define-key rcirc-mode-map (kbd "RET") 'rcirc-send-input)
491(define-key rcirc-mode-map (kbd "M-p") 'rcirc-insert-prev-input)
492(define-key rcirc-mode-map (kbd "M-n") 'rcirc-insert-next-input)
493(define-key rcirc-mode-map (kbd "TAB") 'rcirc-complete-nick)
494(define-key rcirc-mode-map (kbd "C-c C-b") 'rcirc-browse-url)
495(define-key rcirc-mode-map (kbd "C-c C-c") 'rcirc-edit-multiline)
496(define-key rcirc-mode-map (kbd "C-c C-j") 'rcirc-cmd-join)
497(define-key rcirc-mode-map (kbd "C-c C-k") 'rcirc-cmd-kick)
498(define-key rcirc-mode-map (kbd "C-c C-l") 'rcirc-cmd-list)
499(define-key rcirc-mode-map (kbd "C-c C-d") 'rcirc-cmd-mode)
500(define-key rcirc-mode-map (kbd "C-c C-m") 'rcirc-cmd-msg)
501(define-key rcirc-mode-map (kbd "C-c C-r") 'rcirc-cmd-nick) ; rename
502(define-key rcirc-mode-map (kbd "C-c C-o") 'rcirc-cmd-oper)
503(define-key rcirc-mode-map (kbd "C-c C-p") 'rcirc-cmd-part)
504(define-key rcirc-mode-map (kbd "C-c C-q") 'rcirc-cmd-query)
505(define-key rcirc-mode-map (kbd "C-c C-t") 'rcirc-cmd-topic)
506(define-key rcirc-mode-map (kbd "C-c C-n") 'rcirc-cmd-names)
507(define-key rcirc-mode-map (kbd "C-c C-w") 'rcirc-cmd-whois)
508(define-key rcirc-mode-map (kbd "C-c C-x") 'rcirc-cmd-quit)
509(define-key rcirc-mode-map (kbd "C-c TAB") ; C-i
510 'rcirc-toggle-ignore-channel-activity)
511(define-key rcirc-mode-map (kbd "C-c C-s") 'rcirc-switch-to-server-buffer)
512(define-key rcirc-mode-map (kbd "C-c C-a") 'rcirc-jump-to-first-unread-line)
513
514(define-key global-map (kbd "C-c `") 'rcirc-next-active-buffer)
515(define-key global-map (kbd "C-c C-@") 'rcirc-next-active-buffer)
516(define-key global-map (kbd "C-c C-SPC") 'rcirc-next-active-buffer)
517
518(defvar rcirc-mode-hook nil
519 "Hook run when setting up rcirc buffer.")
520
521(defun rcirc-mode (process target)
522 "Major mode for irc channel buffers.
523
524\\{rcirc-mode-map}"
525 (kill-all-local-variables)
526 (use-local-map rcirc-mode-map)
527 (setq mode-name "rcirc")
528 (setq major-mode 'rcirc-mode)
529
530 (make-local-variable 'rcirc-input-ring)
531 (setq rcirc-input-ring (make-ring rcirc-input-ring-size))
532 (make-local-variable 'rcirc-process)
533 (setq rcirc-process process)
534 (make-local-variable 'rcirc-target)
535 (setq rcirc-target target)
536 (make-local-variable 'rcirc-urls)
537 (setq rcirc-urls nil)
538 (setq use-hard-newlines t)
539 (when (rcirc-channel-p rcirc-target)
540 (setq header-line-format 'rcirc-topic))
541
542 ;; setup the prompt and markers
543 (make-local-variable 'rcirc-prompt-start-marker)
544 (setq rcirc-prompt-start-marker (make-marker))
545 (set-marker rcirc-prompt-start-marker (point-max))
546 (make-local-variable 'rcirc-prompt-end-marker)
547 (setq rcirc-prompt-end-marker (make-marker))
548 (set-marker rcirc-prompt-end-marker (point-max))
549 (rcirc-update-prompt)
550 (goto-char rcirc-prompt-end-marker)
551 (make-local-variable 'overlay-arrow-position)
552 (setq overlay-arrow-position (make-marker))
553 (set-marker overlay-arrow-position nil)
554
555 (run-hooks 'rcirc-mode-hook))
556
c18a54de
RF
557(defmacro with-rcirc-process-buffer (process &rest body)
558 (declare (indent 1) (debug t))
559 `(with-current-buffer (process-buffer ,process)
560 ,@body))
561
bd43c990
RS
562(defun rcirc-update-prompt ()
563 "Reset the prompt string in the current buffer."
564 (let ((inhibit-read-only t)
565 (prompt (or rcirc-prompt "")))
566 (mapc (lambda (rep)
567 (setq prompt
568 (replace-regexp-in-string (car rep) (cdr rep) prompt)))
569 (list (cons "%n" (with-rcirc-process-buffer rcirc-process
570 rcirc-nick))
571 (cons "%s" (with-rcirc-process-buffer rcirc-process
572 rcirc-server))
573 (cons "%t" (or rcirc-target ""))))
574 (save-excursion
575 (delete-region rcirc-prompt-start-marker rcirc-prompt-end-marker)
576 (goto-char rcirc-prompt-start-marker)
577 (let ((start (point)))
578 (insert-before-markers prompt)
579 (set-marker rcirc-prompt-start-marker start)
580 (when (not (zerop (- rcirc-prompt-end-marker
581 rcirc-prompt-start-marker)))
582 (add-text-properties rcirc-prompt-start-marker
583 rcirc-prompt-end-marker
584 (list 'face 'rcirc-prompt-face
585 'read-only t 'field t
586 'front-sticky t 'rear-nonsticky t)))))))
587
588(defun rcirc-channel-p (target)
589 "Return t if TARGET is a channel name."
590 (and target
591 (not (zerop (length target)))
592 (or (eq (aref target 0) ?#)
593 (eq (aref target 0) ?&))))
594
595(defun rcirc-kill-buffer-hook ()
596 "Part the channel when killing an rcirc buffer."
597 (when (eq major-mode 'rcirc-mode)
598 (rcirc-clear-activity (current-buffer))
599 (when (and rcirc-process
600 (eq (process-status rcirc-process) 'open))
601 (if (rcirc-channel-p rcirc-target)
602 (rcirc-cmd-part "" rcirc-process rcirc-target)
603 ;; remove target from privchat list
604 (when rcirc-target
605 (let ((target (downcase rcirc-target)))
606 (with-rcirc-process-buffer rcirc-process
607 (setq rcirc-private-chats
608 (delete target rcirc-private-chats)))))))))
609(add-hook 'kill-buffer-hook 'rcirc-kill-buffer-hook)
610
611(defun rcirc-get-buffer-name (process target)
612 "Return buffer name based on PROCESS and TARGET."
613 (concat (and target (downcase target)) "@" (process-name process)))
614
615(defun rcirc-get-buffer (process target &optional error)
616 "Return the buffer associated with the PROCESS and TARGET.
617If TARGET is nil and ERROR is nil, return the process buffer."
618 (let ((buffer (and target
619 (get-buffer (rcirc-get-buffer-name process target)))))
620 (if (and buffer (buffer-live-p buffer))
621 buffer
622 (if error
623 (error "Buffer associated with %s does not exist" target)
624 (process-buffer process)))))
625
626(defun rcirc-get-buffer-create (process target)
627 "Return the buffer named associated with the PROCESS and TARGET.
628Create the buffer if it doesn't exist. If TARGET is nil, return
629the process buffer."
630 (with-current-buffer (process-buffer process)
631 (if (not target)
632 (current-buffer)
633 (let ((target (downcase target)))
634 ;; add private chats to list. we dont add channels here, they
635 ;; are managed by the join/part/quit handlers
636 (when (and (not (rcirc-channel-p target))
637 (not (member target rcirc-private-chats)))
638 (with-rcirc-process-buffer process
639 (setq rcirc-private-chats (cons target rcirc-private-chats))))
640 ;; create and setup a buffer, or return the existing one
641 (let ((bufname (rcirc-get-buffer-name process target)))
642 (with-current-buffer (get-buffer-create bufname)
643 (if (or (not rcirc-process)
644 (not (equal (process-status rcirc-process) 'open)))
645 (rcirc-mode process target)
646 (setq rcirc-target target))
647 (current-buffer)))))))
648
649(defun rcirc-send-input ()
650 "Send input to target associated with the current buffer."
651 (interactive)
652 (if (not (eq (process-status rcirc-process) 'open))
653 (error "Network connection to %s is not open"
654 (process-name rcirc-process))
655 ;; update last buffer
656 (rcirc-set-last-buffer rcirc-process (current-buffer))
657 (if (< (point) rcirc-prompt-end-marker)
658 ;; copy the line down to the input area
659 (progn
660 (forward-line 0)
661 (let ((start (if (eq (point) (point-min))
662 (point)
663 (if (get-text-property (1- (point)) 'hard)
664 (point)
665 (previous-single-property-change (point) 'hard))))
666 (end (next-single-property-change (1+ (point)) 'hard)))
667 (goto-char (point-max))
668 (insert (replace-regexp-in-string
669 "\n\\s-+" " "
670 (buffer-substring-no-properties start end)))))
671 ;; assume text has been read
672 (when (marker-position overlay-arrow-position)
673 (set-marker overlay-arrow-position nil))
674 ;; process input
675 (goto-char (point-max))
676 (let ((target (rcirc-buffer-target))
677 (start rcirc-prompt-end-marker))
678 (when (not (equal 0 (- (point) start)))
679 ;; delete a trailing newline
680 (when (eq (point) (point-at-bol))
681 (delete-backward-char 1))
682 (let ((input (buffer-substring-no-properties
683 rcirc-prompt-end-marker (point))))
684 ;; process a /cmd
685 (if (string-match "^/\\([^ ]+\\) ?\\(.*\\)$" input)
686 (let* ((command (match-string 1 input))
687 (fun (intern-soft (concat "rcirc-cmd-" command)))
688 (args (match-string 2 input)))
689 (newline)
690 (with-current-buffer (current-buffer)
691 (delete-region rcirc-prompt-end-marker (point))
692 (if (string= command "me")
693 (rcirc-print rcirc-process (rcirc-nick rcirc-process)
694 "ACTION" target args)
695 (rcirc-print rcirc-process (rcirc-nick rcirc-process)
696 "COMMAND" target input))
697 (set-marker rcirc-prompt-end-marker (point))
698 (if (fboundp fun)
699 (funcall fun args rcirc-process target)
700 (rcirc-send-string rcirc-process
701 (concat command " " args)))))
702 ;; send message to server
703 (if (not rcirc-target)
704 (message "Not joined")
705 (delete-region rcirc-prompt-end-marker (point))
706 (mapc (lambda (message)
707 (rcirc-send-message rcirc-process target message))
708 (split-string input "\n"))))
709 ;; add to input-ring
710 (save-excursion
711 (ring-insert rcirc-input-ring input)
712 (setq rcirc-input-ring-index 0))))))))
713
714(defvar rcirc-parent-buffer nil)
715(defvar rcirc-window-configuration nil)
716(defun rcirc-edit-multiline ()
717 "Move current edit to a dedicated buffer."
718 (interactive)
719 (let ((pos (1+ (- (point) rcirc-prompt-end-marker))))
720 (goto-char (point-max))
721 (let ((text (buffer-substring rcirc-prompt-end-marker (point)))
722 (parent (buffer-name))
723 (process rcirc-process))
724 (delete-region rcirc-prompt-end-marker (point))
725 (setq rcirc-window-configuration (current-window-configuration))
726 (pop-to-buffer (concat "*multiline " parent "*"))
727 (rcirc-multiline-edit-mode)
728 (setq rcirc-parent-buffer parent)
729 (setq rcirc-process process)
730 (insert text)
731 (and (> pos 0) (goto-char pos)))))
732
733(define-derived-mode rcirc-multiline-edit-mode
734 text-mode "rcirc multi"
735 "Major mode for multiline edits
736\\{rcirc-multiline-edit-mode-map}"
737 (make-local-variable 'rcirc-parent-buffer)
738 (make-local-variable 'rcirc-process))
739
740(define-key rcirc-multiline-edit-mode-map
741 (kbd "C-c C-c") 'rcirc-multiline-edit-submit)
742(define-key rcirc-multiline-edit-mode-map
743 (kbd "C-x C-s") 'rcirc-multiline-edit-submit)
744(define-key rcirc-multiline-edit-mode-map
745 (kbd "C-c C-k") 'rcirc-multiline-edit-cancel)
746(define-key rcirc-multiline-edit-mode-map
747 (kbd "ESC ESC ESC") 'rcirc-multiline-edit-cancel)
748
749(defun rcirc-multiline-edit-submit ()
750 "Send the text in buffer back to parent buffer."
751 (interactive)
752 (assert (and (eq major-mode 'rcirc-multiline-edit-mode)))
753 (assert rcirc-parent-buffer)
754 (let ((text (buffer-substring (point-min) (point-max)))
755 (buffer (current-buffer))
756 (pos (point)))
757 (set-buffer rcirc-parent-buffer)
758 (goto-char (point-max))
759 (insert text)
760 (goto-char (+ rcirc-prompt-end-marker (1- pos)))
761 (kill-buffer buffer)
762 (set-window-configuration rcirc-window-configuration)))
763
764(defun rcirc-multiline-edit-cancel ()
765 "Cancel the multiline edit."
766 (interactive)
767 (assert (and (eq major-mode 'rcirc-multiline-edit-mode)))
768 (kill-buffer (current-buffer))
769 (set-window-configuration rcirc-window-configuration))
770
771(defun rcirc-last-buffer (process)
772 "Return the last working buffer for PROCESS.
773Used for displaying messages that don't have an explicit destination."
774 (with-current-buffer (process-buffer process)
775 (or (and rcirc-last-buffer
776 (buffer-live-p rcirc-last-buffer)
777 rcirc-last-buffer)
778 (current-buffer))))
779
780(defun rcirc-set-last-buffer (process buffer)
781 "Set the last working buffer for PROCESS to BUFFER."
782 (with-current-buffer (process-buffer process)
783 (setq rcirc-last-buffer buffer)))
784
bd43c990
RS
785(defun rcirc-format-response-string (process sender response target text)
786 (concat (when rcirc-time-format
787 (format-time-string rcirc-time-format (current-time)))
788 (cond ((or (string= response "PRIVMSG")
789 (string= response "NOTICE")
790 (string= response "ACTION"))
791 (let (first middle end)
792 (cond ((string= response "PRIVMSG")
793 (setq first "<" middle "> "))
794 ((string= response "NOTICE")
795 (setq first "-" middle "- "))
796 (t
797 (setq first "[" middle " " end "]")))
798 (concat first
799 (rcirc-facify (rcirc-user-nick sender)
800 (if (string= sender
801 (rcirc-nick process))
802 'rcirc-my-nick-face
803 'rcirc-other-nick-face))
804 middle
805 (rcirc-mangle-text process text)
806 end)))
807 ((string= response "COMMAND")
808 text)
809 ((string= response "ERROR")
810 (propertize text 'face 'font-lock-warning-face))
811 (t
812 (rcirc-mangle-text
813 process
814 (rcirc-facify
815 (concat "*** "
816 (when (not (string= sender (rcirc-server process)))
817 (concat (rcirc-user-nick sender) " "))
818 (when (zerop (string-to-number response))
819 (concat response " "))
820 (when (and target (not (string= target rcirc-target)))
821 (concat target " "))
822 text)
823 'rcirc-server-face))))))
824
825(defvar rcirc-activity-type nil)
826(make-variable-buffer-local 'rcirc-activity-type)
827(defun rcirc-print (process sender response target text &optional activity)
828 "Print TEXT in the buffer associated with TARGET.
829Format based on SENDER and RESPONSE. If ACTIVITY is non-nil,
830record activity."
831 (let* ((buffer (cond ((bufferp target)
832 target)
833 ((not target)
834 (rcirc-last-buffer process))
835 ((not (rcirc-channel-p target))
836 (rcirc-get-buffer-create process target))
837 ((rcirc-get-buffer process target))
838 (t (process-buffer process))))
839 (inhibit-read-only t))
840 (with-current-buffer buffer
841 (let ((moving (= (point) rcirc-prompt-end-marker))
842 (old-point (point-marker))
843 (fill-start (marker-position rcirc-prompt-start-marker)))
844
845 (unless (string= sender (rcirc-nick process))
846 ;; only decode text from other senders, not ours
847 (setq text (decode-coding-string text buffer-file-coding-system))
848 ;; mark the line with overlay arrow
849 (unless (or (marker-position overlay-arrow-position)
850 (get-buffer-window (current-buffer)))
851 (set-marker overlay-arrow-position
852 (marker-position rcirc-prompt-start-marker))))
853
854 ;; temporarily set the marker insertion-type because
855 ;; insert-before-markers results in hidden text in new buffers
856 (goto-char rcirc-prompt-start-marker)
857 (set-marker-insertion-type rcirc-prompt-start-marker t)
858 (set-marker-insertion-type rcirc-prompt-end-marker t)
859 (insert
860 (rcirc-format-response-string process sender response target text)
861 (propertize "\n" 'hard t))
862 (set-marker-insertion-type rcirc-prompt-start-marker nil)
863 (set-marker-insertion-type rcirc-prompt-end-marker nil)
864
865 ;; fill the text we just inserted, maybe
866 (when (and rcirc-fill-flag
867 (not (string= response "372"))) ;/motd
868 (let ((fill-prefix
869 (or rcirc-fill-prefix
870 (make-string
871 (+ (if rcirc-time-format
872 (length (format-time-string
873 rcirc-time-format))
874 0)
875 (cond ((or (string= response "PRIVMSG")
876 (string= response "NOTICE"))
877 (+ (length (rcirc-user-nick sender))
878 2)) ; <>
879 ((string= response "ACTION")
880 (+ (length (rcirc-user-nick sender))
881 1)) ; [
882 (t 3)) ; ***
883 1)
884 ? )))
885 (fill-column (or rcirc-fill-column fill-column)))
886 (fill-region fill-start rcirc-prompt-start-marker 'left t)))
887
888 ;; truncate buffer if it is very long
889 (save-excursion
890 (when (and rcirc-buffer-maximum-lines
891 (> rcirc-buffer-maximum-lines 0)
892 (= (forward-line (- rcirc-buffer-maximum-lines)) 0))
893 (delete-region (point-min) (point))))
894
895 ;; set inserted text to be read-only
896 (when rcirc-read-only-flag
897 (put-text-property rcirc-prompt-start-marker fill-start 'read-only t)
898 (let ((inhibit-read-only t))
899 (put-text-property rcirc-prompt-start-marker fill-start
900 'front-sticky t)
901 (put-text-property (1- (point)) (point) 'rear-nonsticky t)))
902
903 ;; set the window point for buffers show in windows
904 (walk-windows (lambda (w)
905 (unless (eq (selected-window) w)
906 (when (and (eq (current-buffer)
907 (window-buffer w))
908 (>= (window-point w)
909 rcirc-prompt-end-marker))
910 (set-window-point w (point-max)))))
911 nil t)
912
913 ;; restore the point
914 (goto-char (if moving rcirc-prompt-end-marker old-point))
915
916 ;; flush undo (can we do something smarter here?)
917 (buffer-disable-undo)
918 (buffer-enable-undo))
919
920 ;; record modeline activity
921 (when activity
922 (let ((nick-match
923 (string-match (concat "\\b"
924 (regexp-quote (rcirc-nick process))
925 "\\b")
926 text)))
927 (when (or (not rcirc-ignore-channel-activity)
928 ;; always notice when our nick is mentioned, even
929 ;; if ignoring channel activity
930 nick-match)
931 (rcirc-record-activity
932 (current-buffer)
933 (when (or nick-match (not (rcirc-channel-p rcirc-target)))
934 'nick)))))
935
936 (run-hook-with-args 'rcirc-print-hooks
937 process sender response target text))))
938
939(defun rcirc-startup-channels (server)
940 "Return the list of startup channels for server."
941 (let (channels)
942 (dolist (i rcirc-startup-channels-alist)
943 (if (string-match (car i) server)
944 (setq channels (append channels (cdr i)))))
945 channels))
946
947(defun rcirc-join-channels (process channels)
948 "Join CHANNELS."
949 (save-window-excursion
950 (mapc (lambda (channel)
951 (with-current-buffer (process-buffer process)
952 (let (rcirc-last-buffer) ; make sure /join text is
953 ; printed in server buffer
954 (rcirc-print process (rcirc-nick process) "COMMAND"
955 nil (concat "/join " channel)))
956 (rcirc-cmd-join channel process)))
957 channels)))
958\f
959;;; nick management
960(defun rcirc-user-nick (user)
961 "Return the nick from USER. Remove any non-nick junk."
962 (if (string-match "^[@%+]?\\([^! ]+\\)!?" (or user ""))
963 (match-string 1 user)
964 user))
965
966(defun rcirc-user-non-nick (user)
967 "Return the non-nick portion of USER."
968 (if (string-match "^[@+]?[^! ]+!?\\(.*\\)" (or user ""))
969 (match-string 1 user)
970 user))
971
972(defun rcirc-nick-channels (process nick)
973 "Return list of channels for NICK."
974 (let ((nick (rcirc-user-nick nick)))
975 (with-current-buffer (process-buffer process)
976 (mapcar (lambda (x) (car x))
977 (gethash nick rcirc-nick-table)))))
978
979(defun rcirc-put-nick-channel (process nick channel)
980 "Add CHANNEL to list associated with NICK."
981 (with-current-buffer (process-buffer process)
982 (let* ((nick (rcirc-user-nick nick))
983 (chans (gethash nick rcirc-nick-table))
984 (record (assoc channel chans)))
985 (if record
986 (setcdr record (current-time))
987 (puthash nick (cons (cons channel (current-time))
988 chans)
989 rcirc-nick-table)))))
990
991(defun rcirc-nick-remove (process nick)
992 "Remove NICK from table."
993 (with-current-buffer (process-buffer process)
994 (remhash nick rcirc-nick-table)))
995
996(defun rcirc-remove-nick-channel (process nick channel)
997 "Remove the CHANNEL from list associated with NICK."
998 (with-current-buffer (process-buffer process)
999 (let* ((nick (rcirc-user-nick nick))
1000 (chans (gethash nick rcirc-nick-table))
1001 (newchans (assq-delete-all channel chans)))
1002 (if newchans
1003 (puthash nick newchans rcirc-nick-table)
1004 (remhash nick rcirc-nick-table)))))
1005
1006(defun rcirc-channel-nicks (process channel)
1007 "Return the list of nicks in CHANNEL sorted by last activity."
1008 (with-current-buffer (process-buffer process)
1009 (let (nicks)
1010 (maphash
1011 (lambda (k v)
1012 (let ((record (assoc channel v)))
1013 (if record
1014 (setq nicks (cons (cons k (cdr record)) nicks)))))
1015 rcirc-nick-table)
1016 (mapcar (lambda (x) (car x))
1017 (sort nicks (lambda (x y) (time-less-p (cdr y) (cdr x))))))))
1018\f
1019;;; activity tracking
1020(or (assq 'rcirc-ignore-channel-activity minor-mode-alist)
1021 (setq minor-mode-alist
1022 (cons '(rcirc-ignore-channel-activity " Ignore") minor-mode-alist)))
1023
1024(defun rcirc-toggle-ignore-channel-activity (&optional all)
1025 "Toggle the value of `rcirc-ignore-channel-activity'.
1026If ALL is non-nil, instead toggle the value of
1027`rcirc-ignore-all-activity-flag'."
1028 (interactive "P")
1029 (if all
1030 (progn
1031 (setq rcirc-ignore-all-activity-flag
1032 (not rcirc-ignore-all-activity-flag))
1033 (message (concat "Global activity "
1034 (if rcirc-ignore-all-activity-flag
1035 "hidden"
1036 "displayed")))
1037 (rcirc-update-activity-string))
1038 (setq rcirc-ignore-channel-activity
1039 (not rcirc-ignore-channel-activity)))
1040 (force-mode-line-update))
1041
1042(defvar rcirc-switch-to-buffer-function 'switch-to-buffer
1043 "Function to use when switching buffers.
1044Possible values are `switch-to-buffer', `pop-to-buffer', and
1045`display-buffer'.")
1046
1047(defun rcirc-switch-to-server-buffer ()
1048 "Switch to the server buffer associated with current channel buffer."
1049 (interactive)
1050 (funcall rcirc-switch-to-buffer-function (process-buffer rcirc-process)))
1051
1052(defun rcirc-jump-to-first-unread-line ()
1053 "Move the point to the first unread line in this buffer."
1054 (interactive)
1055 (when (marker-position overlay-arrow-position)
1056 (goto-char overlay-arrow-position)))
1057
1058(defvar rcirc-last-non-irc-buffer nil
1059 "The buffer to switch to when there is no more activity.")
1060
1061(defun rcirc-next-active-buffer (arg)
1062 "Go to the ARGth rcirc buffer with activity.
1063The function given by `rcirc-switch-to-buffer-function' is used to
1064show the buffer."
1065 (interactive "p")
1066 (if rcirc-activity
1067 (progn
1068 (unless (eq major-mode 'rcirc-mode)
1069 (setq rcirc-last-non-irc-buffer (current-buffer)))
1070 (if (and (> arg 0)
1071 (<= arg (length rcirc-activity)))
1072 (funcall rcirc-switch-to-buffer-function
1073 (nth (1- arg) rcirc-activity))
1074 (message "Invalid arg: %d" arg)))
1075 (if (eq major-mode 'rcirc-mode)
1076 (if (not (and rcirc-last-non-irc-buffer
1077 (buffer-live-p rcirc-last-non-irc-buffer)))
1078 (message "No last buffer.")
1079 (funcall rcirc-switch-to-buffer-function rcirc-last-non-irc-buffer)
1080 (setq rcirc-last-non-irc-buffer nil))
1081 (message "No channel activity. Go start something."))))
1082
1083(defvar rcirc-activity-hooks nil
1084 "Hook to be run when there is channel activity.
1085
1086Functions are called with a single argument, the buffer with the
1087activity. Only run if the buffer is not visible and
1088`rcirc-ignore-channel-activity' is non-nil.")
1089
1090(defun rcirc-record-activity (buffer type)
1091 "Record BUFFER activity with TYPE."
1092 (with-current-buffer buffer
1093 (when (not (get-buffer-window (current-buffer) t))
1094 (add-to-list 'rcirc-activity (current-buffer) 'append)
1095 (if (not rcirc-activity-type)
1096 (setq rcirc-activity-type type))
1097 (rcirc-update-activity-string)))
1098 (run-hook-with-args 'rcirc-activity-hooks buffer))
1099
1100(defun rcirc-clear-activity (buffer)
1101 "Clear the BUFFER activity."
1102 (setq rcirc-activity (delete buffer rcirc-activity))
1103 (with-current-buffer buffer
1104 (setq rcirc-activity-type nil)))
1105
1106(defun rcirc-update-activity-string ()
1107 "Update mode-line string."
1108 (setq rcirc-activity-string
1109 (if (or rcirc-ignore-all-activity-flag
1110 (not rcirc-activity))
1111 ""
1112 (concat " [" (mapconcat
1113 (lambda (b)
1114 (let ((s (rcirc-short-buffer-name b)))
1115 (with-current-buffer b
1116 (if (not (eq rcirc-activity-type 'nick))
1117 s
1118 (rcirc-facify s
1119 'rcirc-mode-line-nick-face)))))
1120 rcirc-activity ",") "]"))))
1121
1122(defun rcirc-short-buffer-name (buffer)
1123 "Return a short name for BUFFER to use in the modeline indicator."
1124 (with-current-buffer buffer
1125 (or rcirc-target (process-name rcirc-process))))
1126
1127(defun rcirc-update-activity ()
1128 "Go through visible windows and remove buffers from activity list."
1129 (walk-windows (lambda (w) (rcirc-clear-activity (window-buffer w))))
1130 (rcirc-update-activity-string))
1131
1132\f
1133;;; /commands these are called with 3 args: PROCESS, TARGET, which is
1134;; the current buffer/channel/user, and ARGS, which is a string
1135;; containing the text following the /cmd.
1136
1137(defmacro defun-rcirc-command (command argument docstring interactive-form
1138 &rest body)
1139 "Define a command."
1140 `(defun ,(intern (concat "rcirc-cmd-" (symbol-name command)))
1141 (,@argument &optional process target)
1142 ,(concat docstring "\n\nNote: If PROCESS or TARGET are nil, the values of"
1143 "\nbuffer local variables `rcirc-process' and `rcirc-target',"
1144 "\nwill be used.")
1145 ,interactive-form
1146 (let ((process (or process rcirc-process))
1147 (target (or target rcirc-target)))
1148 ,@body)))
1149
1150(defun-rcirc-command msg (message)
1151 "Send private MESSAGE to TARGET."
1152 (interactive "i")
1153 (if (null message)
1154 (progn
1155 (setq target (completing-read "Message nick: "
1156 (with-current-buffer
1157 (process-buffer rcirc-process)
1158 rcirc-nick-table)))
1159 (when (> (length target) 0)
1160 (setq message (read-string (format "Message %s: " target)))
1161 (when (> (length message) 0)
1162 (rcirc-send-message process target message))))
1163 (if (not (string-match "\\([^ ]+\\) \\(.+\\)" message))
1164 (message "Not enough args, or something.")
1165 (setq target (match-string 1 message)
1166 message (match-string 2 message))
1167 (rcirc-send-message process target message))))
1168
1169(defun-rcirc-command query (nick)
1170 "Open a private chat buffer to NICK."
1171 (interactive (list (completing-read "Query nick: "
1172 (with-current-buffer
1173 (process-buffer rcirc-process)
1174 rcirc-nick-table))))
1175 (let ((new-buffer (eq (rcirc-get-buffer rcirc-process nick)
1176 (process-buffer rcirc-process))))
1177 (switch-to-buffer (rcirc-get-buffer-create process nick))
1178 (when new-buffer
1179 (rcirc-cmd-whois nick))))
1180
1181(defun-rcirc-command join (args)
1182 "Join CHANNEL."
1183 (interactive "sJoin channel: ")
1184 (let* ((channel (car (split-string args)))
1185 (buffer (rcirc-get-buffer-create process channel)))
1186 (when (not (eq (selected-window) (minibuffer-window)))
1187 (funcall rcirc-switch-to-buffer-function buffer))
1188 (rcirc-send-string process (concat "JOIN " args))
1189 (rcirc-set-last-buffer process buffer)))
1190
1191(defun-rcirc-command part (channel)
1192 "Part CHANNEL."
1193 (interactive "sPart channel: ")
1194 (let ((channel (if (> (length channel) 0) channel target)))
1195 (rcirc-send-string process (concat "PART " channel " :" (rcirc-version)))))
1196
1197(defun-rcirc-command quit (reason)
1198 "Send a quit message to server with REASON."
1199 (interactive "sQuit reason: ")
1200 (rcirc-send-string process (concat "QUIT :" reason)))
1201
1202(defun-rcirc-command nick (nick)
1203 "Change nick to NICK."
1204 (interactive "i")
1205 (when (null nick)
1206 (setq nick (read-string "New nick: " (rcirc-nick process))))
1207 (rcirc-send-string process (concat "NICK " nick)))
1208
1209(defun-rcirc-command names (channel)
1210 "Display list of names in CHANNEL or in current channel if CHANNEL is nil.
1211If called interactively, prompt for a channel when prefix arg is supplied."
1212 (interactive "P")
1213 (if (interactive-p)
1214 (if channel
1215 (setq channel (read-string "List names in channel: " target))))
1216 (let ((channel (if (> (length channel) 0)
1217 channel
1218 target)))
1219 (rcirc-send-string process (concat "NAMES " channel))))
1220
1221(defun-rcirc-command topic (topic)
1222 "List TOPIC for the TARGET channel.
1223With a prefix arg, prompt for new topic."
1224 (interactive "P")
1225 (if (and (interactive-p) topic)
1226 (setq topic (read-string "New Topic: " rcirc-topic)))
1227 (rcirc-send-string process (concat "TOPIC " target
1228 (when (> (length topic) 0)
1229 (concat " :" topic)))))
1230
1231(defun-rcirc-command whois (nick)
1232 "Request information from server about NICK."
1233 (interactive (list
1234 (completing-read "Whois: "
1235 (with-current-buffer
1236 (process-buffer rcirc-process)
1237 rcirc-nick-table))))
1238 (rcirc-set-last-buffer rcirc-process (current-buffer))
1239 (rcirc-send-string process (concat "WHOIS " nick)))
1240
1241(defun-rcirc-command mode (args)
1242 "Set mode with ARGS."
1243 (interactive (list (concat (read-string "Mode nick or channel: ")
1244 " " (read-string "Mode: "))))
1245 (rcirc-send-string process (concat "MODE " args)))
1246
1247(defun-rcirc-command list (channels)
1248 "Request information on CHANNELS from server."
1249 (interactive "sList Channels: ")
1250 (rcirc-send-string process (concat "LIST " channels)))
1251
1252(defun-rcirc-command oper (args)
1253 "Send operator command to server."
1254 (interactive "sOper args: ")
1255 (rcirc-send-string process (concat "OPER " args)))
1256
1257(defun-rcirc-command quote (message)
1258 "Send MESSAGE literally to server."
1259 (interactive "sServer message: ")
1260 (rcirc-send-string process message))
1261
1262(defun-rcirc-command kick (arg)
1263 "Kick NICK from current channel."
1264 (interactive (list
1265 (concat (completing-read "Kick nick: "
1266 (rcirc-channel-nicks rcirc-process
1267 rcirc-target))
1268 (read-from-minibuffer "Kick reason: "))))
1269 (let* ((arglist (split-string arg))
1270 (argstring (concat (car arglist) " :"
1271 (mapconcat 'identity (cdr arglist) " "))))
1272 (rcirc-send-string process (concat "KICK " target " " argstring))))
1273
1274(defun rcirc-cmd-ctcp (args &optional process target)
1275 (if (string-match "^\\([^ ]+\\)\\s-+\\(.+\\)$" args)
1276 (let ((target (match-string 1 args))
1277 (request (match-string 2 args)))
1278 (rcirc-send-message process target
1279 (concat "\C-a" (upcase request) "\C-a")))
1280 (rcirc-print process (rcirc-nick process) "ERROR" target
1281 "usage: /ctcp NICK REQUEST")))
1282
1283(defun rcirc-cmd-me (args &optional process target)
1284 (rcirc-send-string process (format "PRIVMSG %s :\C-aACTION %s\C-a"
1285 target args)))
1286\f
1287(defun rcirc-message-leader (sender face)
1288 "Return a string with SENDER propertized with FACE."
1289 (rcirc-facify (concat "<" (rcirc-user-nick sender) "> ") face))
1290
1291(defun rcirc-facify (string face)
1292 "Return a copy of STRING with FACE property added."
1293 (propertize (or string "") 'face face 'rear-nonsticky t))
1294
1295;; shy grouping must be used within this regexp
1296(defvar rcirc-url-regexp
1297 "\\b\\(?:\\(?:www\\.\\|\\(?:s?https?\\|ftp\\|file\\|gopher\\|news\\|telnet\
1298\\|wais\\|mailto\\):\\)\\(?://[-a-zA-Z0-9_.]+:[0-9]*\\)?\\(?:[-a-zA-Z0-9_=!?#$\
1299@~`%&*+|\\/:;.,]\\|\\w\\)+\\(?:[-a-zA-Z0-9_=#$@~`%&*+|\\/]\\|\\w\\)\\)"
1300 "Regexp matching URL's. Set to nil to disable URL features in rcirc.")
1301
1302(defun rcirc-browse-url (&optional arg)
1303 "Prompt for url to browse based on urls in buffer."
1304 (interactive)
1305 (let ((completions (mapcar (lambda (x) (cons x nil)) rcirc-urls))
1306 (initial-input (car rcirc-urls))
1307 (history (cdr rcirc-urls)))
1308 (browse-url (completing-read "rcirc browse-url: "
1309 completions nil nil initial-input 'history)
1310 arg)))
1311
1312(defun rcirc-map-regexp (function regexp string)
1313 "Return a copy of STRING after calling FUNCTION for each REGEXP match.
1314FUNCTION takes 3 arguments, MATCH-START, MATCH-END, and STRING."
1315 (let ((start 0))
1316 (while (string-match regexp string start)
1317 (setq start (match-end 0))
1318 (funcall function (match-beginning 0) (match-end 0) string)))
1319 string)
1320
1321(defvar rcirc-nick-syntax-table
1322 (let ((table (make-syntax-table text-mode-syntax-table)))
1323 (mapc (lambda (c) (modify-syntax-entry c "w" table))
1324 "[]\\`_^{|}-")
1325 (modify-syntax-entry ?' "_" table)
1326 table)
1327 "Syntax table which includes all nick characters as word constituents.")
1328
1329(defun rcirc-mangle-text (process text)
1330 "Return TEXT with properties added based on various patterns."
1331 ;; ^B
1332 (setq text
1333 (rcirc-map-regexp (lambda (start end string)
1334 (add-text-properties
1335 start end
1336 (list 'face 'bold 'rear-nonsticky t)
1337 string))
1338 "\ 2.*?\ 2"
1339 text))
1340 (while (string-match "\\(.*\\)[\ 2\ 1]\\(.*\\)" text) ; deal with \1f
1341 (setq text (concat (match-string 1 text)
1342 (match-string 2 text))))
1343 ;; my nick
1344 (setq text
1345 (with-syntax-table rcirc-nick-syntax-table
1346 (rcirc-map-regexp (lambda (start end string)
1347 (add-text-properties
1348 start end
1349 (list 'face 'rcirc-nick-in-message-face
1350 'rear-nonsticky t)
1351 string))
1352 (concat "\\b"
1353 (regexp-quote (rcirc-nick process))
1354 "\\b")
1355 text)))
1356 ;; urls
1357 (setq text
1358 (rcirc-map-regexp
1359 (lambda (start end string)
1360 (let ((orig-face (get-text-property start 'face string)))
1361 (add-text-properties start end
1362 (list 'face (list orig-face 'bold)
1363 'rear-nonsticky t)
1364 string))
1365 (push (substring string start end) rcirc-urls))
1366 rcirc-url-regexp
1367 text))
1368 text)
1369
1370\f
1371;;; handlers
1372;; these are called with the server PROCESS, the SENDER, which is a
1373;; server or a user, depending on the command, the ARGS, which is a
1374;; list of strings, and the TEXT, which is the original server text,
1375;; verbatim
1376(defun rcirc-handler-001 (process sender args text)
1377 (rcirc-handler-generic process "001" sender args text)
1378 ;; set the real server name
1379 (with-current-buffer (process-buffer process)
1380 (setq rcirc-server sender)
1381 (setq rcirc-nick (car args))
1382 (rcirc-update-prompt)
1383 (when rcirc-auto-authenticate-flag (rcirc-authenticate))
1384 (let (rcirc-last-buffer)
1385 (rcirc-join-channels process rcirc-startup-channels))))
1386
1387(defun rcirc-handler-PRIVMSG (process sender args text)
1388 (let ((target (if (rcirc-channel-p (car args))
1389 (car args)
1390 (rcirc-user-nick sender)))
1391 (message (or (cadr args) "")))
1392 (if (string-match "^\C-a\\(.*\\)\C-a$" message)
1393 (rcirc-handler-CTCP process target sender (match-string 1 message))
1394 (rcirc-print process sender "PRIVMSG" target message t))
1395 ;; update nick timestamp
1396 (if (member target (rcirc-nick-channels process sender))
1397 (rcirc-put-nick-channel process sender target))))
1398
1399(defun rcirc-handler-NOTICE (process sender args text)
1400 (let ((target (car args))
1401 (message (cadr args)))
1402 (rcirc-print process sender "NOTICE"
1403 (cond ((rcirc-channel-p target)
1404 target)
1405 ((string-match "^\\[\\(#[^ ]+\\)\\]" message)
1406 (match-string 1 message))
1407 (sender
1408 (if (string= sender (rcirc-server process))
1409 (process-buffer process)
1410 (rcirc-user-nick sender))))
1411 message t)
1412 (and sender (rcirc-put-nick-channel process sender target))))
1413
1414(defun rcirc-handler-WALLOPS (process sender args text)
1415 (let ((target (rcirc-user-nick sender)))
1416 (rcirc-print process sender "WALLOPS" target (car args) t)))
1417
1418(defun rcirc-handler-JOIN (process sender args text)
1419 (let ((channel (downcase (car args)))
1420 (nick (rcirc-user-nick sender)))
1421 (rcirc-get-buffer-create process channel)
1422 (rcirc-print process sender "JOIN" channel "")
1423
1424 ;; print in private chat buffer if it exists
1425 (if (not (eq (process-buffer rcirc-process)
1426 (rcirc-get-buffer rcirc-process nick)))
1427 (rcirc-print process sender "JOIN" nick channel))
1428
1429 (rcirc-put-nick-channel process sender channel)
1430 (if (string= nick (rcirc-nick process))
1431 (setq rcirc-channels (cons channel rcirc-channels)))))
1432
1433;; PART and KICK are handled the same way
1434(defun rcirc-handler-PART-or-KICK (process response channel sender nick args)
1435 (rcirc-print process sender response channel (concat channel " " args))
1436
1437 ;; print in private chat buffer if it exists
1438 (when (not (eq (process-buffer rcirc-process)
1439 (rcirc-get-buffer rcirc-process nick)))
1440 (rcirc-print process sender response nick (concat channel " " args)))
1441
1442 (if (not (string= nick (rcirc-nick process)))
1443 ;; this is someone else leaving
1444 (rcirc-remove-nick-channel process nick channel)
1445 ;; this is us leaving
1446 (mapc (lambda (n)
1447 (rcirc-remove-nick-channel process n channel))
1448 (rcirc-channel-nicks process channel))
1449 (setq rcirc-channels (delete channel rcirc-channels))
1450 (with-current-buffer (rcirc-get-buffer process channel)
1451 (setq rcirc-target nil))))
1452
1453(defun rcirc-handler-PART (process sender args text)
1454 (rcirc-handler-PART-or-KICK process "PART"
1455 (car args) sender (rcirc-user-nick sender)
1456 (cadr args)))
1457
1458(defun rcirc-handler-KICK (process sender args text)
1459 (rcirc-handler-PART-or-KICK process "KICK" (car args) sender (cadr args)
1460 (caddr args)))
1461
1462(defun rcirc-handler-QUIT (process sender args text)
1463 (let ((nick (rcirc-user-nick sender)))
1464 (mapc (lambda (channel)
1465 (rcirc-print process sender "QUIT" channel (apply 'concat args)))
1466 (rcirc-nick-channels process nick))
1467
1468 ;; print in private chat buffer if it exists
1469 (if (not (eq (process-buffer rcirc-process)
1470 (rcirc-get-buffer rcirc-process nick)))
1471 (rcirc-print process sender "QUIT" nick (apply 'concat args)))
1472
1473 (rcirc-nick-remove process nick)))
1474
1475(defun rcirc-handler-NICK (process sender args text)
1476 (let* ((old-nick (rcirc-user-nick sender))
1477 (new-nick (car args))
1478 (channels (rcirc-nick-channels process old-nick)))
1479 ;; print message to nick's channels
1480 (dolist (target channels)
1481 (rcirc-print process sender "NICK" target new-nick))
1482 ;; update private chat buffer, if it exists
1483 (with-current-buffer (rcirc-get-buffer process old-nick)
1484 (when (not (equal (process-buffer rcirc-process)
1485 (current-buffer)))
1486 (rcirc-print process sender "NICK" old-nick new-nick)
1487 (setq rcirc-target new-nick)
1488 (rename-buffer (rcirc-get-buffer-name process new-nick))))
1489 ;; remove old nick and add new one
1490 (with-current-buffer (process-buffer process)
1491 (let ((v (gethash old-nick rcirc-nick-table)))
1492 (remhash old-nick rcirc-nick-table)
1493 (puthash new-nick v rcirc-nick-table))
1494 ;; if this is our nick...
1495 (when (string= old-nick rcirc-nick)
1496 (setq rcirc-nick new-nick)
1497 ;; update prompts
1498 (mapc (lambda (target)
1499 (with-current-buffer (rcirc-get-buffer process target)
1500 (rcirc-update-prompt)))
1501 (append rcirc-channels rcirc-private-chats))
1502 ;; reauthenticate
1503 (when rcirc-auto-authenticate-flag (rcirc-authenticate))))))
1504
1505(defun rcirc-handler-PING (process sender args text)
1506 (rcirc-send-string process (concat "PONG " (car args))))
1507
1508(defun rcirc-handler-PONG (process sender args text)
1509 ;; do nothing
1510 )
1511
1512(defun rcirc-handler-TOPIC (process sender args text)
1513 (let ((topic (cadr args)))
1514 (rcirc-print process sender "TOPIC" (car args) topic)
1515 (with-current-buffer (rcirc-get-buffer process (car args))
1516 (setq rcirc-topic topic))))
1517
1518(defun rcirc-handler-332 (process sender args text)
1519 "RPL_TOPIC"
1520 (with-current-buffer (rcirc-get-buffer process (cadr args))
1521 (setq rcirc-topic (caddr args))))
1522
1523(defun rcirc-handler-333 (process sender args text)
1524 "Not in rfc1459.txt"
1525 (with-current-buffer (rcirc-get-buffer process (cadr args))
1526 (let ((setter (caddr args))
1527 (time (current-time-string
1528 (seconds-to-time
1529 (string-to-number (cadddr args))))))
1530 (rcirc-print process sender "TOPIC" (cadr args)
1531 (format "%s (%s on %s)" rcirc-topic setter time)))))
1532
1533(defun rcirc-handler-477 (process sender args text)
1534 "ERR_NOCHANMODES"
1535 (rcirc-print process sender "477" (cadr args) (caddr args)))
1536
1537(defun rcirc-handler-MODE (process sender args text)
1538 (let ((target (car args))
1539 (msg (mapconcat 'identity (cdr args) " ")))
1540 (rcirc-print process sender "MODE"
1541 (if (string= target (rcirc-nick process))
1542 nil
1543 target)
1544 msg)
1545
1546 ;; print in private chat buffers if they exist
1547 (mapc (lambda (nick)
1548 (when (not (eq (process-buffer rcirc-process)
1549 (rcirc-get-buffer rcirc-process nick)))
1550 (rcirc-print process sender "MODE" nick msg)))
1551 (cddr args))))
1552
1553(defun rcirc-get-temp-buffer-create (process channel)
1554 "Return a buffer based on PROCESS and CHANNEL."
1555 (let ((tmpnam (concat " " (downcase channel) "TMP" (process-name process))))
1556 (get-buffer-create tmpnam)))
1557
1558(defun rcirc-handler-353 (process sender args text)
1559 "RPL_NAMREPLY"
1560 (let ((channel (downcase (caddr args))))
1561 (mapc (lambda (nick)
1562 (rcirc-put-nick-channel process nick channel))
1563 (delete "" (split-string (cadddr args) " ")))
1564 (with-current-buffer (rcirc-get-temp-buffer-create process channel)
1565 (goto-char (point-max))
1566 (insert (car (last args)) " "))))
1567
1568(defun rcirc-handler-366 (process sender args text)
1569 "RPL_ENDOFNAMES"
1570 (let* ((channel (cadr args))
1571 (buffer (rcirc-get-temp-buffer-create process channel)))
1572 (with-current-buffer buffer
1573 (rcirc-print process sender "NAMES" channel
1574 (buffer-substring (point-min) (point-max))))
1575 (kill-buffer buffer)))
1576
1577(defun rcirc-handler-433 (process sender args text)
1578 "ERR_NICKNAMEINUSE"
1579 (rcirc-handler-generic process "433" sender args text)
1580 (let* ((new-nick (concat (cadr args) "`")))
1581 (with-current-buffer (process-buffer process)
1582 (rcirc-cmd-nick new-nick nil process))))
1583
1584(defun rcirc-authenticate ()
1585 "Send authentication to process associated with current buffer.
1586Passwords are read from `rcirc-authinfo-file-name' (which see)."
1587 (interactive)
1588 (let ((password-alist
1589 (with-temp-buffer
1590 (insert-file-contents-literally rcirc-authinfo-file-name)
1591 (goto-char (point-min))
1592 (read (current-buffer)))))
1593 (with-current-buffer (process-buffer rcirc-process)
1594 (dolist (i password-alist)
1595 (let ((server (car i))
1596 (nick (cadr i))
1597 (method (caddr i))
1598 (args (cdddr i)))
1599 (when (and (string-match server rcirc-server)
1600 (string-match nick rcirc-nick))
1601 (cond ((equal method 'nickserv)
1602 (rcirc-send-string
1603 rcirc-process
1604 (concat
1605 "PRIVMSG nickserv :identify "
1606 (car args))))
1607 ((equal method 'chanserv)
1608 (rcirc-send-string
1609 rcirc-process
1610 (concat
1611 "PRIVMSG chanserv :identify "
1612 (car args) " " (cadr args))))
1613 ((equal method 'bitlbee)
1614 (rcirc-send-string
1615 rcirc-process
1616 (concat "PRIVMSG #bitlbee :identify " (car args))))
1617 (t
1618 (message "No %S authentication method defined"
1619 method)))))))))
1620
1621(defun rcirc-handler-INVITE (process sender args text)
1622 (rcirc-print process sender "INVITE" nil (mapconcat 'identity args " ") t))
1623
1624(defun rcirc-handler-ERROR (process sender args text)
1625 (rcirc-print process sender "ERROR" nil (mapconcat 'identity args " ")))
1626
1627(defun rcirc-handler-CTCP (process target sender text)
1628 (if (string-match "^\\([^ ]+\\) *\\(.*\\)$" text)
1629 (let* ((request (upcase (match-string 1 text)))
1630 (args (match-string 2 text))
1631 (nick (rcirc-user-nick sender))
1632 (handler (intern-soft (concat "rcirc-handler-ctcp-" request))))
1633 (if (not (fboundp handler))
1634 (rcirc-print process sender "ERROR" target
1635 (format "unhandled ctcp: %s" text))
1636 (funcall handler process target sender args)
1637 (if (not (string= request "ACTION"))
1638 (rcirc-print process sender "CTCP" target
1639 (format "%s" text)))))))
1640
1641(defun rcirc-handler-ctcp-VERSION (process target sender args)
1642 (rcirc-send-string process
1643 (concat "NOTICE " (rcirc-user-nick sender)
1644 " :\C-aVERSION " (rcirc-version)
1645 " - http://www.nongnu.org/rcirc"
1646 "\C-a")))
1647
1648(defun rcirc-handler-ctcp-ACTION (process target sender args)
1649 (rcirc-print process sender "ACTION" target args t))
1650
1651(defun rcirc-handler-ctcp-TIME (process target sender args)
1652 (rcirc-send-string process
1653 (concat "NOTICE " (rcirc-user-nick sender)
1654 " :\C-aTIME " (current-time-string) "\C-a")))
1655\f
1656(defface rcirc-my-nick-face
1657 '((((type tty) (class color)) (:foreground "blue" :weight bold))
1658 (((class color) (background light)) (:foreground "Blue"))
1659 (((class color) (background dark)) (:foreground "LightSkyBlue"))
1660 (t (:inverse-video t :bold t)))
1661 "The rcirc face used to highlight my messages."
1662 :group 'rcirc)
1663
1664(defface rcirc-other-nick-face
1665 '((((type tty) (class color)) (:foreground "yellow" :weight light))
1666 (((class grayscale) (background light))
1667 (:foreground "Gray90" :bold t :italic t))
1668 (((class grayscale) (background dark))
1669 (:foreground "DimGray" :bold t :italic t))
1670 (((class color) (background light)) (:foreground "DarkGoldenrod"))
1671 (((class color) (background dark)) (:foreground "LightGoldenrod"))
1672 (t (:bold t :italic t)))
1673 "The rcirc face used to highlight other messages."
1674 :group 'rcirc)
1675
1676(defface rcirc-server-face
1677 '((((type tty pc) (class color) (background light)) (:foreground "red"))
1678 (((type tty pc) (class color) (background dark)) (:foreground "red1"))
1679 (((class grayscale) (background light))
1680 (:foreground "DimGray" :bold t :italic t))
1681 (((class grayscale) (background dark))
1682 (:foreground "LightGray" :bold t :italic t))
1683 (((class color) (background light)) (:foreground "gray40"))
1684 (((class color) (background dark)) (:foreground "chocolate1"))
1685 (t (:bold t :italic t)))
1686 "The rcirc face used to highlight server messages."
1687 :group 'rcirc)
1688
1689(defface rcirc-nick-in-message-face
1690 '((((type tty) (class color)) (:foreground "cyan" :weight bold))
1691 (((class grayscale) (background light)) (:foreground "LightGray" :bold t))
1692 (((class grayscale) (background dark)) (:foreground "DimGray" :bold t))
1693 (((class color) (background light)) (:foreground "Purple"))
1694 (((class color) (background dark)) (:foreground "Cyan"))
1695 (t (:bold t)))
1696 "The rcirc face used to highlight instances of nick within messages."
1697 :group 'rcirc)
1698
1699(defface rcirc-prompt-face
1700 '((((background dark)) (:foreground "cyan"))
1701 (t (:foreground "dark blue")))
1702 "The rcirc face to use to highlight prompts."
1703 :group 'rcirc)
1704
1705(defface rcirc-mode-line-nick-face
1706 '((t (:bold t)))
1707 "The rcirc face used indicate activity directed at you."
1708 :group 'rcirc)
1709\f
1710;; When using M-x flyspell-mode, only check words past the input marker
1711(put 'rcirc-mode 'flyspell-mode-predicate 'rcirc-looking-at-input)
1712(defun rcirc-looking-at-input ()
1713 "Returns true if point is past the input marker."
1714 (>= (point) rcirc-prompt-end-marker))
1715\f
1716
1717(provide 'rcirc)
e636ae15
MB
1718
1719;; arch-tag: b471b7e8-6b5a-4399-b2c6-a3c78dfc8ffb
bd43c990 1720;;; rcirc.el ends here