comparison mcabber/mcabber/hooks.c @ 1668:41c26b7d2890

Install mcabber headers * Change mcabber headers naming scheme * Move 'src/' -> 'mcabber/' * Add missing include <mcabber/config.h>'s * Create and install clean config.h version in 'include/' * Move "dirty" config.h version to 'mcabber/' * Add $(top_srcdir) to compiler include path * Update modules HOWTO
author Myhailo Danylenko <isbear@ukrpost.net>
date Mon, 18 Jan 2010 15:36:19 +0200
parents mcabber/src/hooks.c@c3d0cb4dc9d4
children b09f82f61745
comparison
equal deleted inserted replaced
1667:8af0e0ad20ad 1668:41c26b7d2890
1 /*
2 * hooks.c -- Hooks layer
3 *
4 * Copyright (C) 2005-2009 Mikael Berthe <mikael@lilotux.net>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or (at
9 * your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
19 * USA
20 */
21
22 #include <loudmouth/loudmouth.h>
23 #include <stdlib.h>
24 #include <string.h>
25 #include <sys/types.h>
26 #include <unistd.h>
27
28 #include "hooks.h"
29 #include "screen.h"
30 #include "roster.h"
31 #include "histolog.h"
32 #include "hbuf.h"
33 #include "settings.h"
34 #include "utils.h"
35 #include "utf8.h"
36 #include "commands.h"
37 #include "main.h"
38
39 #ifdef MODULES_ENABLE
40 #include <glib.h>
41
42 typedef struct {
43 hk_handler_t handler;
44 guint32 flags;
45 gpointer userdata;
46 } hook_list_data_t;
47
48 static GSList *hk_handler_queue = NULL;
49
50 void hk_add_handler (hk_handler_t handler, guint32 flags, gpointer userdata)
51 {
52 hook_list_data_t *h = g_new (hook_list_data_t, 1);
53 h->handler = handler;
54 h->flags = flags;
55 h->userdata = userdata;
56 hk_handler_queue = g_slist_append (hk_handler_queue, h);
57 }
58
59 static gint hk_queue_search_cb (hook_list_data_t *a, hook_list_data_t *b)
60 {
61 if (a->handler == b->handler && a->userdata == b->userdata)
62 return 0;
63 else
64 return 1;
65 }
66
67 void hk_del_handler (hk_handler_t handler, gpointer userdata)
68 {
69 hook_list_data_t h = { handler, 0, userdata };
70 GSList *el = g_slist_find_custom (hk_handler_queue, &h, (GCompareFunc) hk_queue_search_cb);
71 if (el) {
72 g_free (el->data);
73 hk_handler_queue = g_slist_delete_link (hk_handler_queue, el);
74 }
75 }
76 #endif
77
78 static char *extcmd;
79
80 static const char *COMMAND_ME = "/me ";
81
82 void hk_message_in(const char *bjid, const char *resname,
83 time_t timestamp, const char *msg, LmMessageSubType type,
84 guint encrypted)
85 {
86 int new_guy = FALSE;
87 int is_groupchat = FALSE; // groupchat message
88 int is_room = FALSE; // window is a room window
89 int log_muc_conf = FALSE;
90 int active_window = FALSE;
91 int message_flags = 0;
92 guint rtype = ROSTER_TYPE_USER;
93 char *wmsg = NULL, *bmsg = NULL, *mmsg = NULL;
94 GSList *roster_usr;
95 unsigned mucnicklen = 0;
96 const char *ename = NULL;
97
98 if (encrypted == ENCRYPTED_PGP)
99 message_flags |= HBB_PREFIX_PGPCRYPT;
100 else if (encrypted == ENCRYPTED_OTR)
101 message_flags |= HBB_PREFIX_OTRCRYPT;
102
103 if (type == LM_MESSAGE_SUB_TYPE_GROUPCHAT) {
104 rtype = ROSTER_TYPE_ROOM;
105 is_groupchat = TRUE;
106 log_muc_conf = settings_opt_get_int("log_muc_conf");
107 if (!resname) {
108 message_flags = HBB_PREFIX_INFO | HBB_PREFIX_NOFLAG;
109 resname = "";
110 wmsg = bmsg = g_strdup_printf("~ %s", msg);
111 } else {
112 wmsg = bmsg = g_strdup_printf("<%s> %s", resname, msg);
113 mucnicklen = strlen(resname) + 2;
114 if (!strncmp(msg, COMMAND_ME, strlen(COMMAND_ME)))
115 wmsg = mmsg = g_strdup_printf("*%s %s", resname, msg+4);
116 }
117 } else {
118 bmsg = g_strdup(msg);
119 if (!strncmp(msg, COMMAND_ME, strlen(COMMAND_ME))) {
120 gchar *shortid = g_strdup(bjid);
121 if (settings_opt_get_int("buddy_me_fulljid") == FALSE) {
122 gchar *p = strchr(shortid, '@'); // Truncate the jid
123 if (p)
124 *p = '\0';
125 }
126 wmsg = mmsg = g_strdup_printf("*%s %s", shortid, msg+4);
127 g_free(shortid);
128 } else
129 wmsg = (char*) msg;
130 }
131
132 // If this user isn't in the roster, we add it
133 roster_usr = roster_find(bjid, jidsearch, 0);
134 if (!roster_usr) {
135 new_guy = TRUE;
136 roster_usr = roster_add_user(bjid, NULL, NULL, rtype, sub_none, -1);
137 if (!roster_usr) { // Shouldn't happen...
138 scr_LogPrint(LPRINT_LOGNORM, "ERROR: unable to add buddy!");
139 g_free(bmsg);
140 g_free(mmsg);
141 return;
142 }
143 } else if (is_groupchat) {
144 // Make sure the type is ROOM
145 buddy_settype(roster_usr->data, ROSTER_TYPE_ROOM);
146 }
147
148 is_room = !!(buddy_gettype(roster_usr->data) & ROSTER_TYPE_ROOM);
149
150 if (is_room) {
151 if (!is_groupchat) {
152 // This is a private message from a room participant
153 g_free(bmsg);
154 if (!resname) {
155 resname = "";
156 wmsg = bmsg = g_strdup(msg);
157 } else {
158 wmsg = bmsg = g_strdup_printf("PRIV#<%s> %s", resname, msg);
159 if (!strncmp(msg, COMMAND_ME, strlen(COMMAND_ME))) {
160 g_free(mmsg);
161 wmsg = mmsg = g_strdup_printf("PRIV#*%s %s", resname, msg+4);
162 }
163 }
164 message_flags |= HBB_PREFIX_HLIGHT;
165 } else {
166 // This is a regular chatroom message.
167 const char *nick = buddy_getnickname(roster_usr->data);
168
169 if (nick) {
170 // Let's see if we are the message sender, in which case we'll
171 // highlight it.
172 if (resname && !strcmp(resname, nick)) {
173 message_flags |= HBB_PREFIX_HLIGHT_OUT;
174 } else if (!settings_opt_get_int("muc_disable_nick_hl")) {
175 // We're not the sender. Can we see our nick?
176 const char *msgptr = msg;
177 while ((msgptr = strcasestr(msgptr, nick)) != NULL) {
178 const char *leftb, *rightb;
179 // The message contains our nick. Let's check it's not
180 // in the middle of another word (i.e. preceded/followed
181 // immediately by an alphanumeric character or an underscore.
182 rightb = msgptr+strlen(nick);
183 if (msgptr == msg)
184 leftb = NULL;
185 else
186 leftb = prev_char((char*)msgptr, msg);
187 msgptr = next_char((char*)msgptr);
188 // Check left boundary
189 if (leftb && (iswalnum(get_char(leftb)) || get_char(leftb) == '_'))
190 continue;
191 // Check right boundary
192 if (!iswalnum(get_char(rightb)) && get_char(rightb) != '_')
193 message_flags |= HBB_PREFIX_HLIGHT;
194 }
195 }
196 }
197 }
198 }
199
200 if (type == LM_MESSAGE_SUB_TYPE_ERROR) {
201 message_flags = HBB_PREFIX_ERR | HBB_PREFIX_IN;
202 scr_LogPrint(LPRINT_LOGNORM, "Error message received from <%s>", bjid);
203 }
204
205 // Note: the hlog_write should not be called first, because in some
206 // cases scr_WriteIncomingMessage() will load the history and we'd
207 // have the message twice...
208 scr_WriteIncomingMessage(bjid, wmsg, timestamp, message_flags, mucnicklen);
209
210 // We don't log the modified message, but the original one
211 if (wmsg == mmsg)
212 wmsg = bmsg;
213
214 // - We don't log the message if it is an error message
215 // - We don't log the message if it is a private conf. message
216 // - We don't log the message if it is groupchat message and the log_muc_conf
217 // option is off (and it is not a history line)
218 if (!(message_flags & HBB_PREFIX_ERR) &&
219 (!is_room || (is_groupchat && log_muc_conf && !timestamp)))
220 hlog_write_message(bjid, timestamp, 0, wmsg);
221
222 if (settings_opt_get_int("events_ignore_active_window") &&
223 current_buddy && scr_get_chatmode()) {
224 gpointer bud = BUDDATA(current_buddy);
225 if (bud) {
226 const char *cjid = buddy_getjid(bud);
227 if (cjid && !strcasecmp(cjid, bjid))
228 active_window = TRUE;
229 }
230 }
231
232 if (settings_opt_get_int("eventcmd_use_nickname"))
233 ename = roster_getname(bjid);
234
235 #ifdef MODULES_ENABLE
236 {
237 GSList *h = hk_handler_queue;
238 if (h) {
239 #if 0
240 hk_arg_t *args = g_new (hk_arg_t, 5);
241 args[0].name = "hook";
242 args[0].value = "hook-message-in";
243 args[1].name = "jid";
244 args[1].value = bjid;
245 args[2].name = "message";
246 args[2].value = wmsg;
247 args[3].name = "groupchat";
248 args[3].value = is_groupchat ? "true" : "false";
249 args[4].name = NULL;
250 args[4].value = NULL;
251 #else
252 // We can use a const array for keys/static values, so modules
253 // can do fast known to them args check by just comparing pointers...
254 hk_arg_t args[] = {
255 { "hook", "hook-message-in" },
256 { "jid", bjid },
257 { "message", wmsg },
258 { "groupchat", is_groupchat ? "true" : "false" },
259 { NULL, NULL },
260 };
261 #endif
262 while (h) {
263 hook_list_data_t *data = h->data;
264 if (data->flags & HOOK_MESSAGE_IN)
265 (data->handler) (HOOK_MESSAGE_IN, args, data->userdata);
266 h = g_slist_next (h);
267 }
268 }
269 }
270 #endif
271
272 // External command
273 // - We do not call hk_ext_cmd() for history lines in MUC
274 // - We do call hk_ext_cmd() for private messages in a room
275 // - We do call hk_ext_cmd() for messages to the current window
276 if (!active_window && ((is_groupchat && !timestamp) || !is_groupchat))
277 hk_ext_cmd(ename ? ename : bjid, (is_groupchat ? 'G' : 'M'), 'R', wmsg);
278
279 // Display the sender in the log window
280 if ((!is_groupchat) && !(message_flags & HBB_PREFIX_ERR) &&
281 settings_opt_get_int("log_display_sender")) {
282 const char *name = roster_getname(bjid);
283 if (!name) name = "";
284 scr_LogPrint(LPRINT_NORMAL, "Message received from %s <%s/%s>",
285 name, bjid, (resname ? resname : ""));
286 }
287
288 // Beep, if enabled:
289 // - if it's a private message
290 // - if it's a public message and it's highlighted
291 if (settings_opt_get_int("beep_on_message")) {
292 if ((!is_groupchat && !(message_flags & HBB_PREFIX_ERR)) ||
293 (is_groupchat && (message_flags & HBB_PREFIX_HLIGHT)))
294 scr_Beep();
295 }
296
297 // We need to update the roster if the sender is unknown or
298 // if the sender is offline/invisible and a filter is set.
299 if (new_guy ||
300 (buddy_getstatus(roster_usr->data, NULL) == offline &&
301 buddylist_isset_filter()))
302 {
303 update_roster = TRUE;
304 }
305
306 g_free(bmsg);
307 g_free(mmsg);
308 }
309
310 // hk_message_out()
311 // nick should be set for private messages in a chat room, and null for
312 // normal messages.
313 void hk_message_out(const char *bjid, const char *nick,
314 time_t timestamp, const char *msg,
315 guint encrypted, gpointer xep184)
316 {
317 char *wmsg = NULL, *bmsg = NULL, *mmsg = NULL;
318 guint cryptflag = 0;
319
320 if (nick) {
321 wmsg = bmsg = g_strdup_printf("PRIV#<%s> %s", nick, msg);
322 if (!strncmp(msg, COMMAND_ME, strlen(COMMAND_ME))) {
323 const char *mynick = roster_getnickname(bjid);
324 wmsg = mmsg = g_strdup_printf("PRIV#<%s> *%s %s", nick,
325 (mynick ? mynick : "me"), msg+4);
326 }
327 } else {
328 wmsg = (char*)msg;
329 if (!strncmp(msg, COMMAND_ME, strlen(COMMAND_ME))) {
330 char *myid = jid_get_username(settings_opt_get("jid"));
331 if (myid) {
332 wmsg = mmsg = g_strdup_printf("*%s %s", myid, msg+4);
333 g_free(myid);
334 }
335 }
336 }
337
338 // Note: the hlog_write should not be called first, because in some
339 // cases scr_WriteOutgoingMessage() will load the history and we'd
340 // have the message twice...
341 if (encrypted == ENCRYPTED_PGP)
342 cryptflag = HBB_PREFIX_PGPCRYPT;
343 else if (encrypted == ENCRYPTED_OTR)
344 cryptflag = HBB_PREFIX_OTRCRYPT;
345 scr_WriteOutgoingMessage(bjid, wmsg, cryptflag, xep184);
346
347 // We don't log private messages
348 if (!nick)
349 hlog_write_message(bjid, timestamp, 1, msg);
350
351 #ifdef MODULES_ENABLE
352 {
353 GSList *h = hk_handler_queue;
354 if (h) {
355 hk_arg_t args[] = {
356 { "hook", "hook-message-out" },
357 { "jid", bjid },
358 { "message", wmsg },
359 { NULL, NULL },
360 };
361 while (h) {
362 hook_list_data_t *data = h->data;
363 if (data->flags & HOOK_MESSAGE_OUT)
364 (data->handler) (HOOK_MESSAGE_OUT, args, data->userdata);
365 h = g_slist_next (h);
366 }
367 }
368 }
369 #endif
370
371 // External command
372 hk_ext_cmd(bjid, 'M', 'S', NULL);
373
374 g_free(bmsg);
375 g_free(mmsg);
376 }
377
378 void hk_statuschange(const char *bjid, const char *resname, gchar prio,
379 time_t timestamp, enum imstatus status,
380 const char *status_msg)
381 {
382 int st_in_buf;
383 enum imstatus oldstat;
384 char *bn;
385 char *logsmsg;
386 const char *rn = (resname ? resname : "");
387 const char *ename = NULL;
388
389 if (settings_opt_get_int("eventcmd_use_nickname"))
390 ename = roster_getname(bjid);
391
392 oldstat = roster_getstatus(bjid, resname);
393
394 st_in_buf = settings_opt_get_int("show_status_in_buffer");
395
396 if (settings_opt_get_int("log_display_presence")) {
397 int buddy_format = settings_opt_get_int("buddy_format");
398 bn = NULL;
399 if (buddy_format) {
400 const char *name = roster_getname(bjid);
401 if (name && strcmp(name, bjid)) {
402 if (buddy_format == 1)
403 bn = g_strdup_printf("%s <%s/%s>", name, bjid, rn);
404 else if (buddy_format == 2)
405 bn = g_strdup_printf("%s/%s", name, rn);
406 else if (buddy_format == 3)
407 bn = g_strdup_printf("%s", name);
408 }
409 }
410
411 if (!bn)
412 bn = g_strdup_printf("<%s/%s>", bjid, rn);
413
414 logsmsg = g_strdup(status_msg ? status_msg : "");
415 replace_nl_with_dots(logsmsg);
416
417 scr_LogPrint(LPRINT_LOGNORM, "Buddy status has changed: [%c>%c] %s %s",
418 imstatus2char[oldstat], imstatus2char[status], bn, logsmsg);
419 g_free(logsmsg);
420 g_free(bn);
421 }
422
423 if (st_in_buf == 2 ||
424 (st_in_buf == 1 && (status == offline || oldstat == offline))) {
425 // Write the status change in the buddy's buffer, only if it already exists
426 if (scr_BuddyBufferExists(bjid)) {
427 bn = g_strdup_printf("Buddy status has changed: [%c>%c] %s",
428 imstatus2char[oldstat], imstatus2char[status],
429 ((status_msg) ? status_msg : ""));
430 scr_WriteIncomingMessage(bjid, bn, timestamp,
431 HBB_PREFIX_INFO|HBB_PREFIX_NOFLAG, 0);
432 g_free(bn);
433 }
434 }
435
436 roster_setstatus(bjid, rn, prio, status, status_msg, timestamp,
437 role_none, affil_none, NULL);
438 buddylist_build();
439 scr_DrawRoster();
440 hlog_write_status(bjid, timestamp, status, status_msg);
441
442 #ifdef MODULES_ENABLE
443 {
444 GSList *h = hk_handler_queue;
445 if (h) {
446 char os[2] = " \0";
447 char ns[2] = " \0";
448 hk_arg_t args[] = {
449 { "hook", "hook-status-change" },
450 { "jid", bjid },
451 { "resource", rn },
452 { "old_status", os },
453 { "new_status", ns },
454 { "message", status_msg ? status_msg : "" },
455 { NULL, NULL },
456 };
457 os[0] = imstatus2char[oldstat];
458 ns[0] = imstatus2char[status];
459 while (h) {
460 hook_list_data_t *data = h->data;
461 if (data->flags & HOOK_STATUS_CHANGE)
462 (data->handler) (HOOK_STATUS_CHANGE, args, data->userdata);
463 h = g_slist_next (h);
464 }
465 }
466 }
467 #endif
468
469 // External command
470 hk_ext_cmd(ename ? ename : bjid, 'S', imstatus2char[status], NULL);
471 }
472
473 void hk_mystatuschange(time_t timestamp, enum imstatus old_status,
474 enum imstatus new_status, const char *msg)
475 {
476 scr_LogPrint(LPRINT_LOGNORM, "Your status has been set: [%c>%c] %s",
477 imstatus2char[old_status], imstatus2char[new_status],
478 (msg ? msg : ""));
479
480 #ifdef MODULES_ENABLE
481 {
482 GSList *h = hk_handler_queue;
483 if (h) {
484 char ns[2] = " \0";
485 hk_arg_t args[] = {
486 { "hook", "hook-my-status-change" },
487 { "new_status", ns },
488 { "message", msg ? msg : "" },
489 { NULL, NULL },
490 };
491 ns[0] = imstatus2char[new_status];
492 while (h) {
493 hook_list_data_t *data = h->data;
494 if (data->flags & HOOK_MY_STATUS_CHANGE)
495 (data->handler) (HOOK_MY_STATUS_CHANGE, args, data->userdata);
496 h = g_slist_next (h);
497 }
498 }
499 }
500 #endif
501
502 //hlog_write_status(NULL, 0, status);
503 }
504
505
506 /* Internal commands */
507
508 void hook_execute_internal(const char *hookname)
509 {
510 const char *hook_command;
511 char *buf;
512 char *cmdline;
513
514 #ifdef MODULES_ENABLE
515 {
516 GSList *h = hk_handler_queue;
517 if (h) {
518 hk_arg_t args[] = {
519 { "hook", hookname },
520 { NULL, NULL },
521 };
522 while (h) {
523 hook_list_data_t *data = h->data;
524 if (data->flags & HOOK_INTERNAL)
525 (data->handler) (HOOK_INTERNAL, args, data->userdata);
526 h = g_slist_next (h);
527 }
528 }
529 }
530 #endif
531
532 hook_command = settings_opt_get(hookname);
533 if (!hook_command)
534 return;
535
536 buf = g_strdup_printf("Running %s...", hookname);
537 scr_LogPrint(LPRINT_LOGNORM, "%s", buf);
538
539 cmdline = from_utf8(hook_command);
540 if (process_command(cmdline, TRUE) == 255)
541 mcabber_set_terminate_ui();
542
543 g_free(cmdline);
544 g_free(buf);
545 }
546
547
548 /* External commands */
549
550 // hk_ext_cmd_init()
551 // Initialize external command variable.
552 // Can be called with parameter NULL to reset and free memory.
553 void hk_ext_cmd_init(const char *command)
554 {
555 if (extcmd) {
556 g_free(extcmd);
557 extcmd = NULL;
558 }
559 if (command)
560 extcmd = expand_filename(command);
561 }
562
563 // hk_ext_cmd()
564 // Launch an external command (process) for the given event.
565 // For now, data should be NULL.
566 void hk_ext_cmd(const char *bjid, guchar type, guchar info, const char *data)
567 {
568 pid_t pid;
569 char *arg_type = NULL;
570 char *arg_info = NULL;
571 char *arg_data = NULL;
572 char status_str[2];
573 char *datafname = NULL;
574 char unread_str[16];
575
576 if (!extcmd) return;
577
578 // Prepare arg_* (external command parameters)
579 switch (type) {
580 case 'M': /* Normal message */
581 arg_type = "MSG";
582 if (info == 'R')
583 arg_info = "IN";
584 else if (info == 'S')
585 arg_info = "OUT";
586 break;
587 case 'G': /* Groupchat message */
588 arg_type = "MSG";
589 arg_info = "MUC";
590 break;
591 case 'S': /* Status change */
592 arg_type = "STATUS";
593 if (strchr(imstatus2char, tolower(info))) {
594 status_str[0] = toupper(info);
595 status_str[1] = 0;
596 arg_info = status_str;
597 }
598 break;
599 case 'U': /* Unread buffer count */
600 arg_type = "UNREAD";
601 g_snprintf(unread_str, sizeof unread_str, "%d", info);
602 arg_info = unread_str; /* number of remaining unread bjids */
603 break;
604 default:
605 return;
606 }
607
608 if (!arg_type || !arg_info) return;
609
610 if (strchr("MG", type) && data && settings_opt_get_int("event_log_files")) {
611 int fd;
612 const char *prefix;
613 char *prefix_xp = NULL;
614 char *data_locale;
615
616 data_locale = from_utf8(data);
617 prefix = settings_opt_get("event_log_dir");
618 if (prefix)
619 prefix = prefix_xp = expand_filename(prefix);
620 else
621 prefix = ut_get_tmpdir();
622 datafname = g_strdup_printf("%s/mcabber-%d.XXXXXX", prefix, getpid());
623 g_free(prefix_xp);
624
625 // XXX Some old systems may require us to set umask first.
626 fd = mkstemp(datafname);
627 if (fd == -1) {
628 g_free(datafname);
629 datafname = NULL;
630 scr_LogPrint(LPRINT_LOGNORM,
631 "Unable to create temp file for external command.");
632 } else {
633 size_t data_locale_len = strlen(data_locale);
634 ssize_t a = write(fd, data_locale, data_locale_len);
635 ssize_t b = write(fd, "\n", 1);
636 if ((size_t)a != data_locale_len || b != 1) {
637 g_free(datafname);
638 datafname = NULL;
639 scr_LogPrint(LPRINT_LOGNORM,
640 "Unable to write to temp file for external command.");
641 }
642 close(fd);
643 arg_data = datafname;
644 }
645 g_free(data_locale);
646 }
647
648 if ((pid=fork()) == -1) {
649 scr_LogPrint(LPRINT_LOGNORM, "Fork error, cannot launch external command.");
650 g_free(datafname);
651 return;
652 }
653
654 if (pid == 0) { // child
655 // Close standard file descriptors
656 close(STDIN_FILENO);
657 close(STDOUT_FILENO);
658 close(STDERR_FILENO);
659 if (execl(extcmd, extcmd, arg_type, arg_info, bjid, arg_data,
660 (char *)NULL) == -1) {
661 // scr_LogPrint(LPRINT_LOGNORM, "Cannot execute external command.");
662 exit(1);
663 }
664 }
665 g_free(datafname);
666 }
667
668 /* vim: set expandtab cindent cinoptions=>2\:2(0: For Vim users... */