Reintroduce autocmd and augroups. Fix #356
authorYoann Blein <yoann.blein@free.fr>
Sun, 1 Oct 2017 13:39:09 +0000 (15:39 +0200)
committerYoann Blein <yoann.blein@free.fr>
Sun, 1 Oct 2017 13:39:09 +0000 (15:39 +0200)
CHANGELOG.md
doc/vimb.1
src/autocmd.c [new file with mode: 0644]
src/autocmd.h [new file with mode: 0644]
src/config.def.h
src/ex.c
src/main.c
src/main.h
src/util.c
src/util.h

index 7ccdceb..90a7cd2 100644 (file)
@@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
   bookmarks without loading the page first.
 * Refresh hints after scrolling the page or resizing the window which makes
   extended hint mode more comfortable.
+* Reintroduce the automatic commands from vimb2. An automatic command is
+  executed automatically in response to some event, such as a URI being opened.
 
 ### Changed
 
index 0599232..31a0ae7 100644 (file)
@@ -632,6 +632,126 @@ queue.
 .TP
 .B :qc[lear]
 Removes all entries from queue.
+.SS Automatic commands
+An autocommand is a command that is executed automatically in response to some
+event, such as a URI being opened.
+Autocommands are very powerful.
+Use them with care and they will help you avoid typing many commands.
+.PP
+Autocommands are built with following properties.
+.TP
+.I group
+When the [\fIgroup\fP] argument is not given, Vimb uses the current group as
+defined with ':augroup', otherwise, Vimb uses the group defined with
+[\fIgroup\fP].
+Groups are useful to remove multiple grouped autocommands.
+.TP
+.I event
+You can specify a comma separated list of event names.
+No white space can be used in this list.
+.RS
+.PP
+.PD 0
+Events:
+.TP
+.B LoadStarted
+Fired if a new page is going to be opened.
+No data has been received yet, the load may still fail for transport issues.
+.TP
+.B LoadCommited
+Fired if first data chunk has arrived, meaning that the necessary transport
+requirements are established, and the load is being performed.
+This is the right event to toggle content related setting
+like 'scripts', 'plugins' and such things.
+.TP
+.B LoadFinished
+Fires when everything that was required to display on the page has been loaded.
+.TP
+.B DownloadStarted
+Fired right after a download is started.
+.TP
+.B DownloadFinished
+Fired if a Vimb managed download is finished.
+For external download this event is not available.
+.TP
+.B DownloadFailed
+Fired if a Vimb managed download failed.
+For external download this event is not available.
+.PD
+.RE
+.TP
+.I pat
+Comma separated list of patterns, matches in order to check if a autocommand
+applies to the URI associated to an event.
+To use ',' within the single patterns this must be escaped as '\e,'.
+.RS
+.PP
+.PD 0
+Patterns:
+.IP "\fB*\fP"
+Matches any sequence of characters.
+This includes also '/' in contrast to shell patterns.
+.IP "\fB?\fP"
+Matches any single character except of '/'.
+.IP "\fB{one,two}\fP"
+Matches 'one' or 'two'.
+Any '{', ',' and '}' within this pattern must be escaped by a '\\'.
+\&'*' and '?' have no special meaning within the curly braces.
+.IP "\fB\e\fP"
+Use backslash to escape the special meaning of '?*{},' in the pattern or
+pattern list.
+.PD
+.RE
+.TP
+.I cmd
+Any `ex` command vimb understands.
+The leading ':' is not required.
+Multiple commands can be separated by '|'.
+.TP
+.BI ":au[tocmd] [" group "] {" event "} {" pat "} {" cmd "}"
+Add \fIcmd\fP to the list of commands that Vimb will execute automatically on
+\fIevent\fP for a URI matching \fIpat\fP autocmd-patterns.
+Vimb always adds the \fIcmd\fP after existing autocommands, so that the
+autocommands are executed in the order in which they were given.
+.TP
+.BI ":au[tocmd]! [" group "] {" event "} {" pat "} {" cmd "}"
+Remove all autocommands associated with \fIevent\fP and which pattern match
+\fIpat\fP, and add the command \fIcmd\fP.
+Note that the pattern is not matches literally to find autocommands
+to remove, like Vim does.
+Vimb matches the autocommand pattern with \fIpat\fP.
+If [\fIgroup\fP] is not given, deletes autocommands in current group,
+as noted above.
+.TP
+.BI ":au[tocmd]! [" group "] {" event "} {" pat "}"
+Remove all autocommands associated with \fIevent\fP and which pattern matches
+\fIpat\fP in given group (current group by default).
+.TP
+.BI ":au[tocmd]! [" group "] * {" pat "}"
+Remove all autocommands with patterns matching \fIpat\fP for all events
+in given group (current group by default).
+.TP
+.BI ":au[tocmd]! [" group "] {" event "}"
+Remove all autocommands for \fIevent\fP in given group (current group
+by default).
+.TP
+.BI ":au[tocmd]! [" group "]"
+Remove all autocommands in given group (current group by default).
+.TP
+.BI ":aug[roup] {" name "}"
+Define the autocmd group \fIname\fP for the following ":autocmd" commands.
+The name "end" selects the default group.
+.TP
+.BI ":aug[roup]! {" name "}"
+Delete the autocmd group \fIname\fP.
+.PP
+Example:
+.EX
+:aug github
+:  au LoadCommited * set scripts=off|set cookie-accept=never
+:  au LoadCommited http{s,}://github.com/* set scripts=on
+:aug end
+.EE
 .SS Misc
 .TP
 .B :cl[earcache]
diff --git a/src/autocmd.c b/src/autocmd.c
new file mode 100644 (file)
index 0000000..c021f33
--- /dev/null
@@ -0,0 +1,487 @@
+/**
+ * vimb - a webkit based vim like browser.
+ *
+ * Copyright (C) 2012-2017 Daniel Carl
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+
+#include <string.h>
+
+#include "config.h"
+#ifdef FEATURE_AUTOCMD
+#include "autocmd.h"
+#include "ascii.h"
+#include "ex.h"
+#include "util.h"
+#include "completion.h"
+
+typedef struct {
+    guint bits;     /* the bits identify the events the command applies to */
+    char *excmd;    /* ex command string to be run on matches event */
+    char *pattern;  /* list of patterns the uri is matched agains */
+} AutoCmd;
+
+struct AuGroup {
+    char    *name;
+    GSList  *cmds;
+};
+
+typedef struct AuGroup AuGroup;
+
+static struct {
+    const char *name;
+    guint      bits;
+} events[] = {
+    {"*",                0x00ff},
+    {"LoadStarted",      0x0001},
+    {"LoadCommitted",    0x0002},
+    /*{"LoadFirstLayout",  0x0004},*/
+    {"LoadFinished",     0x0008},
+    /*{"LoadFailed",       0x0010},*/
+    {"DownloadStarted",  0x0020},
+    {"DownloadFinished", 0x0040},
+    {"DownloadFailed",   0x0080},
+};
+
+static GSList *get_group(Client *c, const char *name);
+static guint get_event_bits(Client *c, const char *name);
+static void rebuild_used_bits(Client *c);
+static char *get_next_word(char **line);
+static AuGroup *new_group(const char *name);
+static void free_group(AuGroup *group);
+static AutoCmd *new_autocmd(const char *excmd, const char *pattern);
+static void free_autocmd(AutoCmd *cmd);
+
+
+void autocmd_init(Client *c)
+{
+    c->autocmd.curgroup = new_group("end");
+    c->autocmd.groups   = g_slist_prepend(c->autocmd.groups, c->autocmd.curgroup);
+    c->autocmd.usedbits = 0;
+}
+
+void autocmd_cleanup(Client *c)
+{
+    if (c->autocmd.groups) {
+        g_slist_free_full(c->autocmd.groups, (GDestroyNotify)free_group);
+    }
+}
+
+/**
+ * Handle the :augroup {group} ex command.
+ */
+gboolean autocmd_augroup(Client *c, char *name, gboolean delete)
+{
+    GSList *item;
+
+    if (!*name) {
+        return false;
+    }
+
+    /* check for group "end" that marks the default group */
+    if (!strcmp(name, "end")) {
+        c->autocmd.curgroup = (AuGroup*)c->autocmd.groups->data;
+        return true;
+    }
+
+    item = get_group(c, name);
+
+    /* check if the group is going to be removed */
+    if (delete) {
+        /* group does not exist - so do nothing */
+        if (!item) {
+            return true;
+        }
+        if (c->autocmd.curgroup == (AuGroup*)item->data) {
+            /* if the group to delete is the current - switch the the default
+             * group after removing it */
+            c->autocmd.curgroup = (AuGroup*)c->autocmd.groups->data;
+        }
+
+        /* now remove the group */
+        free_group((AuGroup*)item->data);
+        c->autocmd.groups = g_slist_delete_link(c->autocmd.groups, item);
+
+        /* there where autocmds remove - so recreate the usedbits */
+        rebuild_used_bits(c);
+
+        return true;
+    }
+
+    /* check if the group does already exists */
+    if (item) {
+        /* if the group is found in the known groups use it as current */
+        c->autocmd.curgroup = (AuGroup*)item->data;
+
+        return true;
+    }
+
+    /* create a new group and use it as current */
+    c->autocmd.curgroup = new_group(name);
+
+    /* append it to known groups */
+    c->autocmd.groups = g_slist_prepend(c->autocmd.groups, c->autocmd.curgroup);
+
+    return true;
+}
+
+/**
+ * Add or delete auto commands.
+ *
+ * :au[tocmd]! [group] {event} {pat} [nested] {cmd}
+ * group and nested flag are not supported at the moment.
+ */
+gboolean autocmd_add(Client *c, char *name, gboolean delete)
+{
+    guint bits;
+    char *parse, *word, *pattern, *excmd;
+    GSList *item;
+    AuGroup *grp = NULL;
+
+    parse = name;
+
+    /* parse group name if it's there */
+    word = get_next_word(&parse);
+    if (word) {
+        /* check if the word is a known group name */
+        item = get_group(c, word);
+        if (item) {
+            grp = (AuGroup*)item->data;
+
+            /* group is found - get the next word */
+            word = get_next_word(&parse);
+        }
+    }
+    if (!grp) {
+        /* no group found - use the current one */
+        grp = c->autocmd.curgroup;
+    }
+
+    /* parse event name - if none was matched run it for all events */
+    if (word) {
+        bits = get_event_bits(c, word);
+        if (!bits) {
+            return false;
+        }
+        word = get_next_word(&parse);
+    } else {
+        bits = events[AU_ALL].bits;
+    }
+
+    /* last word is the pattern - if not found use '*' */
+    pattern = word ? word : "*";
+
+    /* the rest of the line becoes the ex command to run */
+    if (parse && !*parse) {
+        parse = NULL;
+    }
+    excmd = parse;
+
+    /* delete the autocmd if bang was given */
+    if (delete) {
+        GSList *lc;
+        AutoCmd *cmd;
+        gboolean removed = false;
+
+        /* check if the group does already exists */
+        for (lc = grp->cmds; lc; lc = lc->next) {
+            cmd = (AutoCmd*)lc->data;
+            /* if not bits match - skip the command */
+            if (!(cmd->bits & bits)) {
+                continue;
+            }
+            /* skip if pattern does not match - we check the pattern against
+             * another pattern */
+            if (!util_wildmatch(pattern, cmd->pattern)) {
+                continue;
+            }
+            /* remove the bits from the item */
+            cmd->bits &= ~bits;
+
+            /* if the command has no matching events - remove it */
+            grp->cmds = g_slist_delete_link(grp->cmds, lc);
+            free_autocmd(cmd);
+
+            removed = true;
+        }
+
+        /* if ther was at least one command removed - rebuilt the used bits */
+        if (removed) {
+            rebuild_used_bits(c);
+        }
+
+        return true;
+    }
+
+    /* add the new autocmd */
+    if (excmd && grp) {
+        AutoCmd *cmd = new_autocmd(excmd, pattern);
+        cmd->bits    = bits;
+
+        /* add the new autocmd to the group */
+        grp->cmds = g_slist_append(grp->cmds, cmd);
+
+        /* merge the autocmd bits into the used bits */
+        c->autocmd.usedbits |= cmd->bits;
+    }
+
+    return true;
+}
+
+/**
+ * Run named auto command.
+ */
+gboolean autocmd_run(Client *c, AuEvent event, const char *uri, const char *group)
+{
+    GSList  *lg, *lc;
+    AuGroup *grp;
+    AutoCmd *cmd;
+    guint bits = events[event].bits;
+
+    /* if there is no autocmd for this event - skip here */
+    if (!(c->autocmd.usedbits & bits)) {
+        return true;
+    }
+
+    /* loop over the groups and find matching commands */
+    for (lg = c->autocmd.groups; lg; lg = lg->next) {
+        grp = lg->data;
+        /* if a group was given - skip all none matching groupes */
+        if (group && strcmp(group, grp->name)) {
+            continue;
+        }
+        /* test each command in group */
+        for (lc = grp->cmds; lc; lc = lc->next) {
+            cmd = lc->data;
+            /* skip if this dos not match the event bits */
+            if (!(bits & cmd->bits)) {
+                continue;
+            }
+            /* check pattern only if uri was given */
+            /* skip if pattern does not match */
+            if (uri && !util_wildmatch(cmd->pattern, uri)) {
+                continue;
+            }
+            /* run the command */
+            /* TODO shoult the result be tested for RESULT_COMPLETE? */
+            /* run command and make sure it's not writte to command history */
+            ex_run_string(c, cmd->excmd, false);
+        }
+    }
+
+    return true;
+}
+
+gboolean autocmd_fill_group_completion(Client *c, GtkListStore *store, const char *input)
+{
+    GSList *lg;
+    gboolean found = false;
+    GtkTreeIter iter;
+
+    if (!input || !*input) {
+        for (lg = c->autocmd.groups; lg; lg = lg->next) {
+            gtk_list_store_append(store, &iter);
+            gtk_list_store_set(store, &iter, COMPLETION_STORE_FIRST, ((AuGroup*)lg->data)->name, -1);
+            found = true;
+        }
+    } else {
+        for (lg = c->autocmd.groups; lg; lg = lg->next) {
+            char *value = ((AuGroup*)lg->data)->name;
+            if (g_str_has_prefix(value, input)) {
+                gtk_list_store_append(store, &iter);
+                gtk_list_store_set(store, &iter, COMPLETION_STORE_FIRST, value, -1);
+                found = true;
+            }
+        }
+    }
+
+    return found;
+}
+
+gboolean autocmd_fill_event_completion(Client *c, GtkListStore *store, const char *input)
+{
+    int i;
+    const char *value;
+    gboolean found = false;
+    GtkTreeIter iter;
+
+    if (!input || !*input) {
+        for (i = 0; i < LENGTH(events); i++) {
+            gtk_list_store_append(store, &iter);
+            gtk_list_store_set(store, &iter, COMPLETION_STORE_FIRST, events[i].name, -1);
+            found = true;
+        }
+    } else {
+        for (i = 0; i < LENGTH(events); i++) {
+            value = events[i].name;
+            if (g_str_has_prefix(value, input)) {
+                gtk_list_store_append(store, &iter);
+                gtk_list_store_set(store, &iter, COMPLETION_STORE_FIRST, value, -1);
+                found = true;
+            }
+        }
+    }
+
+    return found;
+}
+
+/**
+ * Get the augroup by it's name.
+ */
+static GSList *get_group(Client *c, const char *name)
+{
+    GSList  *lg;
+    AuGroup *grp;
+
+    for (lg = c->autocmd.groups; lg; lg = lg->next) {
+        grp = lg->data;
+        if (!strcmp(grp->name, name)) {
+            return lg;
+        }
+    }
+
+    return NULL;
+}
+
+static guint get_event_bits(Client *c, const char *name)
+{
+    int result = 0;
+
+    while (*name) {
+        int i, len;
+        for (i = 0; i < LENGTH(events); i++) {
+            /* we count the chars that given name has in common with the
+             * current processed event */
+            len = 0;
+            while (name[len] && events[i].name[len] && name[len] == events[i].name[len]) {
+                len++;
+            }
+
+            /* check if the matching chars built a full word match */
+            if (events[i].name[len] == '\0'
+                && (name[len] == '\0' || name[len] == ',')
+            ) {
+                /* add the bits to the result */
+                result |= events[i].bits;
+
+                /* move pointer after the match and skip possible ',' */
+                name += len;
+                if (*name == ',') {
+                    name++;
+                }
+                break;
+            }
+        }
+
+        /* check if the end was reached without a match */
+        if (i >= LENGTH(events)) {
+            /* is this the right place to write the error */
+            vb_echo(c, MSG_ERROR, TRUE, "Bad autocmd event name: %s", name);
+            return 0;
+        }
+    }
+
+    return result;
+}
+
+/**
+ * Rebuild the usedbits from scratch.
+ * Save all used autocmd event bits in the bitmap.
+ */
+static void rebuild_used_bits(Client *c)
+{
+    GSList *lc, *lg;
+    AuGroup *grp;
+
+    /* clear the current set bits */
+    c->autocmd.usedbits = 0;
+    /* loop over the groups */
+    for (lg = c->autocmd.groups; lg; lg = lg->next) {
+        grp = (AuGroup*)lg->data;
+
+        /* merge the used event bints into the bitmap */
+        for (lc = grp->cmds; lc; lc = lc->next) {
+            c->autocmd.usedbits |= ((AutoCmd*)lc->data)->bits;
+        }
+    }
+}
+
+/**
+ * Get the next word from given line.
+ * Given line pointer is set past the word and and a 0-byte is added there.
+ */
+static char *get_next_word(char **line)
+{
+    char *word;
+
+    if (!*line || !**line) {
+        return NULL;
+    }
+
+    /* remember where the word starts */
+    word = *line;
+
+    /* move pointer to the end of the word or of the line */
+    while (**line && !VB_IS_SPACE(**line)) {
+        (*line)++;
+    }
+
+    /* end the word */
+    if (**line) {
+        *(*line)++ = '\0';
+    }
+
+    /* skip trailing whitespace */
+    while (VB_IS_SPACE(**line)) {
+        (*line)++;
+    }
+
+    return word;
+}
+
+static AuGroup *new_group(const char *name)
+{
+    AuGroup *new = g_slice_new(AuGroup);
+    new->name    = g_strdup(name);
+    new->cmds    = NULL;
+
+    return new;
+}
+
+static void free_group(AuGroup *group)
+{
+    g_free(group->name);
+    if (group->cmds) {
+        g_slist_free_full(group->cmds, (GDestroyNotify)free_autocmd);
+    }
+    g_slice_free(AuGroup, group);
+}
+
+static AutoCmd *new_autocmd(const char *excmd, const char *pattern)
+{
+    AutoCmd *new = g_slice_new(AutoCmd);
+    new->excmd   = g_strdup(excmd);
+    new->pattern = g_strdup(pattern);
+    return new;
+}
+
+static void free_autocmd(AutoCmd *cmd)
+{
+    g_free(cmd->excmd);
+    g_free(cmd->pattern);
+    g_slice_free(AutoCmd, cmd);
+}
+
+#endif
diff --git a/src/autocmd.h b/src/autocmd.h
new file mode 100644 (file)
index 0000000..736bd88
--- /dev/null
@@ -0,0 +1,50 @@
+/**
+ * vimb - a webkit based vim like browser.
+ *
+ * Copyright (C) 2012-2017 Daniel Carl
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+
+#include "config.h"
+#ifdef FEATURE_AUTOCMD
+
+#ifndef _AUTOCMD_H
+#define _AUTOCMD_H
+
+#include "main.h"
+
+/* this values correspond to indices in events[] array in autocmd.c */
+typedef enum {
+    AU_ALL,
+    AU_LOAD_STARTED,
+    AU_LOAD_COMMITTED,
+    /*AU_LOAD_FIRST_LAYOUT,*/
+    AU_LOAD_FINISHED,
+    /*AU_LOAD_FAILED,*/
+    AU_DOWNLOAD_STARTED,
+    AU_DOWNLOAD_FINISHED,
+    AU_DOWNLOAD_FAILED,
+} AuEvent;
+
+void autocmd_init(Client *c);
+void autocmd_cleanup(Client *c);
+gboolean autocmd_augroup(Client *c, char *name, gboolean delete);
+gboolean autocmd_add(Client *c, char *name, gboolean delete);
+gboolean autocmd_run(Client *c, AuEvent event, const char *uri, const char *group);
+gboolean autocmd_fill_group_completion(Client *c, GtkListStore *store, const char *input);
+gboolean autocmd_fill_event_completion(Client *c, GtkListStore *store, const char *input);
+
+#endif /* end of include guard: _AUTOCMD_H */
+#endif
index 48f3392..12c22a0 100644 (file)
@@ -35,6 +35,8 @@
 #define PROGRESS_BAR_LEN            20
 #endif
 
+#define FEATURE_AUTOCMD
+
 /* time in seconds after that message will be removed from inputbox if the
  * message where only temporary */
 #define MESSAGE_TIMEOUT             5
index d6a49de..8c41026 100644 (file)
--- a/src/ex.c
+++ b/src/ex.c
 #include "shortcut.h"
 #include "util.h"
 #include "ext-proxy.h"
+#include "autocmd.h"
 
 typedef enum {
-    /* TODO add feature autocmd */
+#ifdef FEATURE_AUTOCMD
+    EX_AUTOCMD,
+    EX_AUGROUP,
+#endif
     EX_BMA,
     EX_BMR,
     EX_EVAL,
@@ -127,6 +131,10 @@ static void skip_whitespace(const char **input);
 static void free_cmdarg(ExArg *arg);
 static VbCmdResult execute(Client *c, const ExArg *arg);
 
+#ifdef FEATURE_AUTOCMD
+static VbCmdResult ex_augroup(Client *c, const ExArg *arg);
+static VbCmdResult ex_autocmd(Client *c, const ExArg *arg);
+#endif
 static VbCmdResult ex_bookmark(Client *c, const ExArg *arg);
 static VbCmdResult ex_eval(Client *c, const ExArg *arg);
 static void on_eval_script_finished(GDBusProxy *proxy, GAsyncResult *result, Client *c);
@@ -161,6 +169,10 @@ static void history_rewind(void);
  * match. */
 static ExInfo commands[] = {
     /* command           code            func           flags */
+#ifdef FEATURE_AUTOCMD
+    {"autocmd",          EX_AUTOCMD,     ex_autocmd,    EX_FLAG_CMD|EX_FLAG_BANG},
+    {"augroup",          EX_AUGROUP,     ex_augroup,    EX_FLAG_LHS|EX_FLAG_BANG},
+#endif
     {"bma",              EX_BMA,         ex_bookmark,   EX_FLAG_RHS},
     {"bmr",              EX_BMR,         ex_bookmark,   EX_FLAG_RHS},
     {"cmap",             EX_CMAP,        ex_map,        EX_FLAG_LHS|EX_FLAG_CMD},
@@ -778,6 +790,18 @@ static void free_cmdarg(ExArg *arg)
     g_slice_free(ExArg, arg);
 }
 
+#ifdef FEATURE_AUTOCMD
+static VbCmdResult ex_augroup(Client *c, const ExArg *arg)
+{
+    return autocmd_augroup(c, arg->lhs->str, arg->bang) ? CMD_SUCCESS : CMD_ERROR;
+}
+
+static VbCmdResult ex_autocmd(Client *c, const ExArg *arg)
+{
+    return autocmd_add(c, arg->rhs->str, arg->bang) ? CMD_SUCCESS : CMD_ERROR;
+}
+#endif
+
 static VbCmdResult ex_bookmark(Client *c, const ExArg *arg)
 {
     if (arg->code == EX_BMR) {
@@ -1224,6 +1248,16 @@ static gboolean complete(Client *c, short direction)
                     found = util_filename_fill_completion(c, store, token);
                     break;
 
+#ifdef FEATURE_AUTOCMD
+                case EX_AUTOCMD:
+                    found = autocmd_fill_event_completion(c, store, token);
+                    break;
+
+                case EX_AUGROUP:
+                    found = autocmd_fill_group_completion(c, store, token);
+                    break;
+#endif
+
                 default:
                     break;
             }
index 131c149..c440902 100644 (file)
@@ -43,6 +43,7 @@
 #include "setting.h"
 #include "shortcut.h"
 #include "util.h"
+#include "autocmd.h"
 
 static void client_destroy(Client *c);
 static Client *client_new(WebKitWebView *webview);
@@ -647,6 +648,9 @@ static void client_destroy(Client *c)
     register_cleanup(c);
     setting_cleanup(c);
     handler_cleanup(c);
+#ifdef FEATURE_AUTOCMD
+    autocmd_cleanup(c);
+#endif
 
     g_slice_free(Client, c);
 
@@ -677,6 +681,9 @@ static Client *client_new(WebKitWebView *webview)
     completion_init(c);
     map_init(c);
     handler_init(c);
+#ifdef FEATURE_AUTOCMD
+    autocmd_init(c);
+#endif
 
     /* webview */
     c->webview   = webview_new(c, webview);
@@ -964,6 +971,11 @@ static void spawn_new_instance(const char *uri)
 static void on_webctx_download_started(WebKitWebContext *webctx,
         WebKitDownload *download, Client *c)
 {
+#ifdef FEATURE_AUTOCMD
+    const char *uri = webkit_uri_request_get_uri(webkit_download_get_request(download));
+    autocmd_run(c, AU_DOWNLOAD_STARTED, uri, NULL);
+#endif
+
     g_signal_connect(download, "decide-destination", G_CALLBACK(on_webdownload_decide_destination), c);
     g_signal_connect(download, "failed", G_CALLBACK(on_webdownload_failed), c);
     g_signal_connect(download, "finished", G_CALLBACK(on_webdownload_finished), c);
@@ -1019,6 +1031,11 @@ static void on_webdownload_failed(WebKitDownload *download,
     g_assert(error);
     g_assert(c);
 
+#ifdef FEATURE_AUTOCMD
+    const char *uri = webkit_uri_request_get_uri(webkit_download_get_request(download));
+    autocmd_run(c, AU_DOWNLOAD_FAILED, uri, NULL);
+#endif
+
     /* get the failed download's destination uri */
     g_object_get(download, "destination", &destination, NULL);
     g_assert(destination);
@@ -1054,6 +1071,11 @@ static void on_webdownload_finished(WebKitDownload *download, Client *c)
     g_assert(download);
     g_assert(c);
 
+#ifdef FEATURE_AUTOCMD
+    const char *uri = webkit_uri_request_get_uri(webkit_download_get_request(download));
+    autocmd_run(c, AU_DOWNLOAD_FINISHED, uri, NULL);
+#endif
+
     c->state.downloads = g_list_remove(c->state.downloads, download);
 
     /* to reflect the correct download count */
@@ -1216,10 +1238,14 @@ static void on_webview_load_changed(WebKitWebView *webview,
 
     switch (event) {
         case WEBKIT_LOAD_STARTED:
+            uri = webkit_web_view_get_uri(webview);
+#ifdef FEATURE_AUTOCMD
+            autocmd_run(c, AU_LOAD_STARTED, uri, NULL);
+#endif
             /* update load progress in statusbar */
             c->state.progress = 0;
             vb_statusbar_update(c);
-            set_title(c, webkit_web_view_get_uri(webview));
+            set_title(c, uri);
             /* Make sure hinting is cleared before the new page is loaded.
              * Without that vimb would still be in hinting mode after hinting
              * was started and some links was clicked my mouse. Even if there
@@ -1234,6 +1260,9 @@ static void on_webview_load_changed(WebKitWebView *webview,
 
         case WEBKIT_LOAD_COMMITTED:
             uri = webkit_web_view_get_uri(webview);
+#ifdef FEATURE_AUTOCMD
+            autocmd_run(c, AU_LOAD_COMMITTED, uri, NULL);
+#endif
             /* save the current URI in register % */
             vb_register_add(c, '%', uri);
             /* check if tls is on and the page is trusted */
@@ -1258,6 +1287,9 @@ static void on_webview_load_changed(WebKitWebView *webview,
 
         case WEBKIT_LOAD_FINISHED:
             uri = webkit_web_view_get_uri(webview);
+#ifdef FEATURE_AUTOCMD
+            autocmd_run(c, AU_LOAD_FINISHED, uri, NULL);
+#endif
             c->state.progress = 100;
             if (strncmp(uri, "about:", 6)) {
                 history_add(c, HISTORY_URL, uri, webkit_web_view_get_title(webview));
index 7eb1141..de66fc5 100644 (file)
@@ -207,6 +207,8 @@ struct Statusbar {
     GtkWidget *mode, *left, *right, *cmd;
 };
 
+struct AuGroup;
+
 struct Client {
     struct Client       *next;
     struct State        state;
@@ -250,6 +252,11 @@ struct Client {
     struct {
         GHashTable *table;                      /* holds the protocol handlers */
     } handlers;
+    struct {
+        struct AuGroup *curgroup;
+        GSList         *groups;
+        guint          usedbits;                /* holds all used event bits */
+    } autocmd;
 };
 
 struct Vimb {
index 8008533..fac1157 100644 (file)
@@ -38,6 +38,8 @@ static struct {
 extern struct Vimb vb;
 
 static void create_dir_if_not_exists(const char *dirpath);
+static gboolean match(const char *pattern, int patlen, const char *subject);
+static gboolean match_list(const char *pattern, int patlen, const char *subject);
 
 /**
  * Build the absolute file path of given path and possible given directory.
@@ -776,3 +778,204 @@ static void create_dir_if_not_exists(const char *dirpath)
         g_mkdir_with_parents(dirpath, 0755);
     }
 }
+
+/**
+ * Compares given string against also given list of patterns.
+ *
+ * *         Matches any sequence of characters.
+ * ?         Matches any single character except of '/'.
+ * {foo,bar} Matches foo or bar - '{', ',' and '}' within this pattern must be
+ *           escaped by '\'. '*' and '?' have no special meaning within the
+ *           curly braces.
+ * *?{}      these chars must always be escaped by '\' to match them literally
+ */
+gboolean util_wildmatch(const char *pattern, const char *subject)
+{
+    const char *end;
+    int braces, patlen, count;
+
+    /* loop through all pattens */
+    for (count = 0; *pattern; pattern = (*end == ',' ? end + 1 : end), count++) {
+        /* find end of the pattern - but be careful with comma in curly braces */
+        braces = 0;
+        for (end = pattern; *end && (*end != ',' || braces || *(end - 1) == '\\'); ++end) {
+            if (*end == '{') {
+                braces++;
+            } else if (*end == '}') {
+                braces--;
+            }
+        }
+        /* ignore single comma */
+        if (*pattern == *end) {
+            continue;
+        }
+        /* calculate the length of the pattern */
+        patlen = end - pattern;
+
+        /* if this pattern matches - return */
+        if (match(pattern, patlen, subject)) {
+            return true;
+        }
+    }
+
+    if (!count) {
+        /* empty pattern matches only on empty subject */
+        return !*subject;
+    }
+    /* there where one or more patterns but none of them matched */
+    return false;
+}
+
+
+/**
+ * Compares given subject string against the given pattern.
+ * The pattern needs not to bee NUL terminated.
+ */
+static gboolean match(const char *pattern, int patlen, const char *subject)
+{
+    int i;
+    char sl, pl;
+
+    while (patlen > 0) {
+        switch (*pattern) {
+            case '?':
+                /* '?' matches a single char except of / and subject end */
+                if (*subject == '/' || !*subject) {
+                    return false;
+                }
+                break;
+
+            case '*':
+                /* easiest case - the '*' ist the last char in pattern - this
+                 * will always match */
+                if (patlen == 1) {
+                    return true;
+                }
+                /* Try to match as much as possible. Try to match the complete
+                 * uri, if that fails move forward in uri and check for a
+                 * match. */
+                i = strlen(subject);
+                while (i >= 0 && !match(pattern + 1, patlen - 1, subject + i)) {
+                    i--;
+                }
+                return i >= 0;
+
+            case '}':
+                /* spurious '}' in pattern */
+                return false;
+
+            case '{':
+                /* possible {foo,bar} pattern */
+                return match_list(pattern, patlen, subject);
+
+            case '\\':
+                /* '\' escapes next special char */
+                if (strchr("*?{}", pattern[1])) {
+                    pattern++;
+                    patlen--;
+                    if (*pattern != *subject) {
+                        return false;
+                    }
+                }
+                break;
+
+            default:
+                /* compare case insensitive */
+                sl = *subject;
+                if (VB_IS_UPPER(sl)) {
+                    sl += 'a' - 'A';
+                }
+                pl = *pattern;
+                if (VB_IS_UPPER(pl)) {
+                    pl += 'a' - 'A';
+                }
+                if (sl != pl) {
+                    return false;
+                }
+                break;
+        }
+        /* do another loop run with next pattern and subject char */
+        pattern++;
+        patlen--;
+        subject++;
+    }
+
+    /* on end of pattern only a also ended subject is a match */
+    return !*subject;
+}
+
+/**
+ * Matches pattern starting with '{'.
+ * This function can process also on none null terminated pattern.
+ */
+static gboolean match_list(const char *pattern, int patlen, const char *subject)
+{
+    int endlen;
+    const char *end, *s;
+
+    /* finde the next none escaped '}' */
+    for (end = pattern, endlen = patlen; endlen > 0 && *end != '}'; end++, endlen--) {
+        /* if escape char - move pointer one additional step */
+        if (*end == '\\') {
+            end++;
+            endlen--;
+        }
+    }
+
+    if (!*end) {
+        /* unterminated '{' in pattern */
+        return false;
+    }
+
+    s = subject;
+    end++;      /* skip over } */
+    endlen--;
+    pattern++;  /* skip over { */
+    patlen--;
+    while (true) {
+        switch (*pattern) {
+            case ',':
+                if (match(end, endlen, s)) {
+                    return true;
+                }
+                s = subject;
+                pattern++;
+                patlen--;
+                break;
+
+            case '}':
+                return match(end, endlen, s);
+
+            case '\\':
+                if (pattern[1] == ',' || pattern[1] == '}' || pattern[1] == '{') {
+                    pattern++;
+                    patlen--;
+                }
+                /* fall through */
+
+            default:
+                if (*pattern == *s) {
+                    pattern++;
+                    patlen--;
+                    s++;
+                } else {
+                    /* this item of the list does not match - move forward to
+                     * the next none escaped ',' or '}' */
+                    s = subject;
+                    for (s = subject; *pattern != ',' && *pattern != '}'; pattern++, patlen--) {
+                        /* if escape char is found - skip next char */
+                        if (*pattern == '\\') {
+                            pattern++;
+                            patlen--;
+                        }
+                    }
+                    /* found ',' skip over it to check the next list item */
+                    if (*pattern == ',') {
+                        pattern++;
+                        patlen--;
+                    }
+                }
+        }
+    }
+}
+
index 7e802e4..b0b73b4 100644 (file)
@@ -55,5 +55,6 @@ gboolean util_parse_expansion(Client *c, const char **input, GString *str,
 char *util_sanitize_filename(char *filename);
 char *util_strcasestr(const char *haystack, const char *needle);
 char *util_str_replace(const char* search, const char* replace, const char* string);
+gboolean util_wildmatch(const char *pattern, const char *subject);
 
 #endif /* end of include guard: _UTIL_H */