Allow to manage bookmarks and queue.
authorDaniel Carl <danielcarl@gmx.de>
Mon, 27 Feb 2017 23:09:19 +0000 (00:09 +0100)
committerDaniel Carl <danielcarl@gmx.de>
Mon, 27 Feb 2017 23:10:33 +0000 (00:10 +0100)
README.md
src/bookmark.c [new file with mode: 0644]
src/bookmark.h [new file with mode: 0644]
src/command.c
src/config.def.h
src/ex.c
src/normal.c
src/util.c
src/util.h

index ca56071..2eee22e 100644 (file)
--- a/README.md
+++ b/README.md
@@ -104,13 +104,21 @@ project directory.
   - [ ] hinting
   - [x] searching and matching of search results
   - [x] navigation j, k, h, l, ...
-  - [ ] history and history lookup
+  - [x] history and history lookup
   - [ ] completion
+    - [ ] augroup
+    - [ ] autocmd
+    - [x] bookmarks
+    - [ ] file paths for :source and :save
+    - [x] search phrases
+    - [x] settings
+    - [ ] url handler
+    - [x] url history
   - [ ] HSTS
   - [ ] auto-response-header
   - [x] cookies support
   - [x] register support and `:register` command
-  - [ ] read it later queue
+  - [x] read it later queue
   - [ ] show scroll indicator in statusbar as top, x%, bottom or all
         how can we get this information from webview easily?
   - [x] find a way to disable the scrollbars on the main frame
diff --git a/src/bookmark.c b/src/bookmark.c
new file mode 100644 (file)
index 0000000..110d72d
--- /dev/null
@@ -0,0 +1,329 @@
+/**
+ * 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"
+#include "main.h"
+#include "bookmark.h"
+#include "util.h"
+#include "completion.h"
+
+typedef struct {
+    char *uri;
+    char *title;
+    char *tags;
+} Bookmark;
+
+extern struct Vimb vb;
+
+static GList *load(const char *file);
+static gboolean bookmark_contains_all_tags(Bookmark *bm, char **query,
+    unsigned int qlen);
+static Bookmark *line_to_bookmark(const char *uri, const char *data);
+static void free_bookmark(Bookmark *bm);
+
+/**
+ * Write a new bookmark entry to the end of bookmark file.
+ */
+gboolean bookmark_add(const char *uri, const char *title, const char *tags)
+{
+    const char *file = vb.files[FILES_BOOKMARK];
+    if (tags) {
+        return util_file_append(file, "%s\t%s\t%s\n", uri, title ? title : "", tags);
+    }
+    if (title) {
+        return util_file_append(file, "%s\t%s\n", uri, title);
+    }
+    return util_file_append(file, "%s\n", uri);
+}
+
+gboolean bookmark_remove(const char *uri)
+{
+    char **lines, *line, *p;
+    int len, i;
+    GString *new;
+    gboolean removed = false;
+
+    if (!uri) {
+        return false;
+    }
+
+    lines = util_get_lines(vb.files[FILES_BOOKMARK]);
+    if (lines) {
+        new = g_string_new(NULL);
+        len = g_strv_length(lines) - 1;
+        for (i = 0; i < len; i++) {
+            line = lines[i];
+            g_strstrip(line);
+            /* ignore the title or bookmark tags and test only the uri */
+            if ((p = strchr(line, '\t'))) {
+                *p = '\0';
+                if (!strcmp(uri, line)) {
+                    removed = true;
+                    continue;
+                } else {
+                    /* reappend the tags */
+                    *p = '\t';
+                }
+            }
+            if (!strcmp(uri, line)) {
+                removed = true;
+                continue;
+            }
+            g_string_append_printf(new, "%s\n", line);
+        }
+        g_strfreev(lines);
+        g_file_set_contents(vb.files[FILES_BOOKMARK], new->str, -1, NULL);
+        g_string_free(new, true);
+    }
+
+    return removed;
+}
+
+gboolean bookmark_fill_completion(GtkListStore *store, const char *input)
+{
+    gboolean found = false;
+    char **parts;
+    unsigned int len;
+    GtkTreeIter iter;
+    GList *src = NULL;
+    Bookmark *bm;
+
+    src = load(vb.files[FILES_BOOKMARK]);
+    src = g_list_reverse(src);
+    if (!input || !*input) {
+        /* without any tags return all bookmarked items */
+        for (GList *l = src; l; l = l->next) {
+            bm = (Bookmark*)l->data;
+            gtk_list_store_append(store, &iter);
+            gtk_list_store_set(
+                store, &iter,
+                COMPLETION_STORE_FIRST, bm->uri,
+#ifdef FEATURE_TITLE_IN_COMPLETION
+                COMPLETION_STORE_SECOND, bm->title,
+#endif
+                -1
+            );
+            found = true;
+        }
+    } else {
+        parts = g_strsplit(input, " ", 0);
+        len   = g_strv_length(parts);
+
+        for (GList *l = src; l; l = l->next) {
+            bm = (Bookmark*)l->data;
+            if (bookmark_contains_all_tags(bm, parts, len)) {
+                gtk_list_store_append(store, &iter);
+                gtk_list_store_set(
+                    store, &iter,
+                    COMPLETION_STORE_FIRST, bm->uri,
+#ifdef FEATURE_TITLE_IN_COMPLETION
+                    COMPLETION_STORE_SECOND, bm->title,
+#endif
+                    -1
+                );
+                found = true;
+            }
+        }
+        g_strfreev(parts);
+    }
+
+    g_list_free_full(src, (GDestroyNotify)free_bookmark);
+
+    return found;
+}
+
+gboolean bookmark_fill_tag_completion(GtkListStore *store, const char *input)
+{
+    gboolean found;
+    unsigned int len, i;
+    char **tags, *tag;
+    GList *src = NULL, *taglist = NULL;
+    Bookmark *bm;
+
+    /* get all distinct tags from bookmark file */
+    src = load(vb.files[FILES_BOOKMARK]);
+    for (GList *l = src; l; l = l->next) {
+        bm = (Bookmark*)l->data;
+        /* if bookmark contains no tags we can go to the next bookmark */
+        if (!bm->tags) {
+            continue;
+        }
+
+        tags = g_strsplit(bm->tags, " ", -1);
+        len  = g_strv_length(tags);
+        for (i = 0; i < len; i++) {
+            tag = tags[i];
+            /* add tag only if it isn't already in the list */
+            if (!g_list_find_custom(taglist, tag, (GCompareFunc)strcmp)) {
+                taglist = g_list_prepend(taglist, g_strdup(tag));
+            }
+        }
+        g_strfreev(tags);
+    }
+
+    /* generate the completion with the found tags */
+    found = util_fill_completion(store, input, taglist);
+
+    g_list_free_full(src, (GDestroyNotify)free_bookmark);
+    g_list_free_full(taglist, (GDestroyNotify)g_free);
+
+    return found;
+}
+
+#ifdef FEATURE_QUEUE
+/**
+ * Push a uri to the end of the queue.
+ *
+ * @uri: URI to put into the queue
+ */
+gboolean bookmark_queue_push(const char *uri)
+{
+    return util_file_append(vb.files[FILES_QUEUE], "%s\n", uri);
+}
+
+/**
+ * Push a uri to the beginning of the queue.
+ *
+ * @uri: URI to put into the queue
+ */
+gboolean bookmark_queue_unshift(const char *uri)
+{
+    return util_file_prepend(vb.files[FILES_QUEUE], "%s\n", uri);
+}
+
+/**
+ * Retrieves the oldest entry from queue.
+ *
+ * @item_count: will be filled with the number of remaining items in queue.
+ * Returned uri must be freed with g_free.
+ */
+char *bookmark_queue_pop(int *item_count)
+{
+    return util_file_pop_line(vb.files[FILES_QUEUE], item_count);
+}
+
+/**
+ * Removes all contents from the queue file.
+ */
+gboolean bookmark_queue_clear(void)
+{
+    FILE *f;
+    if ((f = fopen(vb.files[FILES_QUEUE], "w"))) {
+        fclose(f);
+        return true;
+    }
+    return false;
+}
+#endif /* FEATURE_QUEUE */
+
+static GList *load(const char *file)
+{
+    return util_file_to_unique_list(file, (Util_Content_Func)line_to_bookmark, 0);
+}
+
+/**
+ * Checks if the given bookmark matches all given query strings as prefix. If
+ * the bookmark has no tags, the matching is done on the '/' splited URL.
+ *
+ * @bm:    bookmark to test if it matches
+ * @query: char array with tags to search for
+ * @qlen:  length of given query char array
+ *
+ * Return: true if the bookmark contained all tags
+ */
+static gboolean bookmark_contains_all_tags(Bookmark *bm, char **query,
+    unsigned int qlen)
+{
+    const char *separators;
+    char *cursor;
+    unsigned int i;
+    gboolean found;
+
+    if (!qlen) {
+        return true;
+    }
+
+    if (bm->tags) {
+        /* If there are tags - use them for matching. */
+        separators = " ";
+        cursor     = bm->tags;
+    } else {
+        /* No tags available - matching is based on the path parts of the URL. */
+        separators = "./";
+        cursor     = bm->uri;
+    }
+
+    /* iterate over all query parts */
+    for (i = 0; i < qlen; i++) {
+        found = false;
+
+        /* we want to do a prefix match on all bookmark tags - so we check for
+         * a match on string begin - if this fails we move the cursor to the
+         * next space and do the test again */
+        while (cursor && *cursor) {
+            /* match was not found at current cursor position */
+            if (g_str_has_prefix(cursor, query[i])) {
+                found = true;
+                break;
+            }
+            /* If match was not found at the cursor position - move cursor
+             * behind the next separator char. */
+            if ((cursor = strpbrk(cursor, separators))) {
+                cursor++;
+            }
+        }
+
+        if (!found) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+static Bookmark *line_to_bookmark(const char *uri, const char *data)
+{
+    char *p;
+    Bookmark *bm;
+
+    /* data part may consist of title or title<tab>tags */
+    bm      = g_slice_new(Bookmark);
+    bm->uri = g_strdup(uri);
+    if (data && (p = strchr(data, '\t'))) {
+        *p        = '\0';
+        bm->title = g_strdup(data);
+        bm->tags  = g_strdup(p + 1);
+    } else {
+        bm->title = g_strdup(data);
+        bm->tags  = NULL;
+    }
+
+    return bm;
+}
+
+static void free_bookmark(Bookmark *bm)
+{
+    g_free(bm->uri);
+    g_free(bm->title);
+    g_free(bm->tags);
+    g_slice_free(Bookmark, bm);
+}
+
diff --git a/src/bookmark.h b/src/bookmark.h
new file mode 100644 (file)
index 0000000..dea4556
--- /dev/null
@@ -0,0 +1,37 @@
+/**
+ * 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/.
+ */
+
+#ifndef _BOOKMARK_H
+#define _BOOKMARK_H
+
+#include "main.h"
+
+gboolean bookmark_add(const char *uri, const char *title, const char *tags);
+gboolean bookmark_remove(const char *uri);
+gboolean bookmark_fill_completion(GtkListStore *store, const char *input);
+gboolean bookmark_fill_tag_completion(GtkListStore *store, const char *input);
+#ifdef FEATURE_QUEUE
+gboolean bookmark_queue_push(const char *uri);
+gboolean bookmark_queue_unshift(const char *uri);
+char *bookmark_queue_pop(int *item_count);
+gboolean bookmark_queue_clear(void);
+#endif
+
+#endif /* end of include guard: _BOOKMARK_H */
+
index 700bd2c..7fbc878 100644 (file)
  * This file contains functions that are used by normal mode and command mode
  * together.
  */
-#include "command.h"
+#include <stdlib.h>
+
 #include "config.h"
+#ifdef FEATURE_QUEUE
+#include "bookmark.h"
+#endif
+#include "command.h"
 #include "history.h"
 #include "main.h"
-#include <stdlib.h>
 
 extern struct Vimb vb;
 
@@ -164,7 +168,40 @@ gboolean command_save(Client *c, const Arg *arg)
 #ifdef FEATURE_QUEUE
 gboolean command_queue(Client *c, const Arg *arg)
 {
-    /* TODO no implemented yet */
-    return TRUE;
+    gboolean res = FALSE;
+    int count    = 0;
+    char *uri;
+
+    switch (arg->i) {
+        case COMMAND_QUEUE_POP:
+            if ((uri = bookmark_queue_pop(&count))) {
+                res = vb_load_uri(c, &(Arg){TARGET_CURRENT, uri});
+                g_free(uri);
+            }
+            vb_echo(c, MSG_NORMAL, FALSE, "Queue length %d", count);
+            break;
+
+        case COMMAND_QUEUE_PUSH:
+            res = bookmark_queue_push(arg->s ? arg->s : c->state.uri);
+            if (res) {
+                vb_echo(c, MSG_NORMAL, FALSE, "Pushed to queue");
+            }
+            break;
+
+        case COMMAND_QUEUE_UNSHIFT:
+            res = bookmark_queue_unshift(arg->s ? arg->s : c->state.uri);
+            if (res) {
+                vb_echo(c, MSG_NORMAL, FALSE, "Pushed to queue");
+            }
+            break;
+
+        case COMMAND_QUEUE_CLEAR:
+            if (bookmark_queue_clear()) {
+                vb_echo(c, MSG_NORMAL, FALSE, "Queue cleared");
+            }
+            break;
+    }
+
+    return res;
 }
 #endif
index 8b2d27b..37874b7 100644 (file)
@@ -28,6 +28,8 @@
 #define FEATURE_TITLE_IN_COMPLETION
 /* support gui style settings compatible with vimb2 */
 #define FEATURE_GUI_STYLE_VIMB2_COMPAT
+/* enable the read it later queue */
+#define FEATURE_QUEUE
 
 #ifdef FEATURE_WGET_PROGRESS_BAR
 /* chars to use for the progressbar */
index c3acc0a..4ca9153 100644 (file)
--- a/src/ex.c
+++ b/src/ex.c
@@ -27,6 +27,7 @@
 #include <sys/wait.h>
 
 #include "ascii.h"
+#include "bookmark.h"
 #include "command.h"
 #include "completion.h"
 #include "config.h"
@@ -745,8 +746,19 @@ static void free_cmdarg(ExArg *arg)
 
 static VbCmdResult ex_bookmark(Client *c, const ExArg *arg)
 {
-    /* TODO no implemented yet */
-    return CMD_SUCCESS;
+    if (arg->code == EX_BMR) {
+        if (bookmark_remove(*arg->rhs->str ? arg->rhs->str : c->state.uri)) {
+            vb_echo_force(c, MSG_NORMAL, TRUE, "  Bookmark removed");
+
+            return CMD_SUCCESS | CMD_KEEPINPUT;
+        }
+    } else if (bookmark_add(c->state.uri, c->state.title, arg->rhs->str)) {
+        vb_echo_force(c, MSG_NORMAL, TRUE, "  Bookmark added");
+
+        return CMD_SUCCESS | CMD_KEEPINPUT;
+    }
+
+    return CMD_ERROR;
 }
 
 static VbCmdResult ex_eval(Client *c, const ExArg *arg)
@@ -855,8 +867,38 @@ static VbCmdResult ex_open(Client *c, const ExArg *arg)
 #ifdef FEATURE_QUEUE
 static VbCmdResult ex_queue(Client *c, const ExArg *arg)
 {
-    /* TODO no implemented yet */
-    return CMD_SUCCESS;
+    Arg a = {0};
+
+    switch (arg->code) {
+        case EX_QPUSH:
+            a.i = COMMAND_QUEUE_PUSH;
+            break;
+
+        case EX_QUNSHIFT:
+            a.i = COMMAND_QUEUE_UNSHIFT;
+            break;
+
+        case EX_QPOP:
+            a.i = COMMAND_QUEUE_POP;
+            break;
+
+        case EX_QCLEAR:
+            a.i = COMMAND_QUEUE_CLEAR;
+            break;
+
+        default:
+            return CMD_ERROR;
+    }
+
+    /* if no argument is found in rhs, keep the uri in arg null to force
+     * command_queue() to use current URI */
+    if (arg->rhs->len) {
+        a.s = arg->rhs->str;
+    }
+
+    return command_queue(c, &a)
+        ? CMD_SUCCESS | CMD_KEEPINPUT
+        : CMD_ERROR | CMD_KEEPINPUT;
 }
 #endif
 
@@ -1092,9 +1134,12 @@ static gboolean complete(Client *c, short direction)
             switch (arg->code) {
                 case EX_OPEN:
                 case EX_TABOPEN:
-                    /* TODO add bookmark completion if *token == '!' */
                     sort  = FALSE;
-                    found = history_fill_completion(store, HISTORY_URL, token);
+                    if (*token == '!') {
+                        found = bookmark_fill_completion(store, token + 1);
+                    } else {
+                        found = history_fill_completion(store, HISTORY_URL, token);
+                    }
                     break;
 
                 case EX_SET:
@@ -1102,7 +1147,7 @@ static gboolean complete(Client *c, short direction)
                     break;
 
                 case EX_BMA:
-                    /* TODO fill bookmark completion */
+                    found = bookmark_fill_tag_completion(store, token);
                     break;
 
                 case EX_SCR:
index e92de75..faa5733 100644 (file)
@@ -633,7 +633,9 @@ static VbResult normal_prevnext(Client *c, const NormalCmdInfo *info)
 
 static VbResult normal_queue(Client *c, const NormalCmdInfo *info)
 {
-    /* TODO run next uri from queu and remove it from */
+#ifdef FEATURE_QUEUE
+    command_queue(c, &((Arg){COMMAND_QUEUE_POP}));
+#endif
     return RESULT_COMPLETE;
 }
 
index be48e71..29557f2 100644 (file)
@@ -104,6 +104,79 @@ gboolean util_file_append(const char *file, const char *format, ...)
     return FALSE;
 }
 
+/**
+ * Prepend new data to file.
+ *
+ * @file:   File to prepend the data
+ * @format: Format string used to process va_list
+ */
+gboolean util_file_prepend(const char *file, const char *format, ...)
+{
+    gboolean res = false;
+    va_list args;
+    char *content;
+    FILE *f;
+
+    content = util_get_file_contents(file, NULL);
+    if ((f = fopen(file, "w"))) {
+        flock(fileno(f), LOCK_EX);
+
+        va_start(args, format);
+        /* write new content to the file */
+        vfprintf(f, format, args);
+        va_end(args);
+
+        /* append previous file content */
+        fputs(content, f);
+
+        flock(fileno(f), LOCK_UN);
+        fclose(f);
+
+        res = true;
+    }
+    g_free(content);
+
+    return res;
+}
+
+
+/**
+ * Retrieves the first line from file and delete it from file.
+ *
+ * @file:       file to read from
+ * @item_count: will be filled with the number of remaining lines in file if it
+ *              is not NULL.
+ *
+ * Returned string must be freed with g_free.
+ */
+char *util_file_pop_line(const char *file, int *item_count)
+{
+    char **lines = util_get_lines(file);
+    char *new,
+         *line = NULL;
+    int len,
+        count = 0;
+
+    if (lines) {
+        len = g_strv_length(lines);
+        if (len) {
+            line = g_strdup(lines[0]);
+            /* minus one for last empty item and one for popped item */
+            count = len - 2;
+            new   = g_strjoinv("\n", lines + 1);
+            g_file_set_contents(file, new, -1, NULL);
+            g_free(new);
+        }
+        g_strfreev(lines);
+    }
+
+    if (item_count) {
+        *item_count = count;
+    }
+
+    return line;
+}
+
 /**
  * Retrieves the config directory path.
  * Returnes string must be freed.
@@ -259,6 +332,34 @@ GList *util_file_to_unique_list(const char *filename, Util_Content_Func func,
     return gl;
 }
 
+/**
+ * Fills the given list store by matching data of also given src list.
+ */
+gboolean util_fill_completion(GtkListStore *store, const char *input, GList *src)
+{
+    gboolean found = false;
+    GtkTreeIter iter;
+
+    if (!input || !*input) {
+        for (GList *l = src; l; l = l->next) {
+            gtk_list_store_append(store, &iter);
+            gtk_list_store_set(store, &iter, COMPLETION_STORE_FIRST, l->data, -1);
+            found = true;
+        }
+    } else {
+        for (GList *l = src; l; l = l->next) {
+            char *value = (char*)l->data;
+            if (g_str_has_prefix(value, input)) {
+                gtk_list_store_append(store, &iter);
+                gtk_list_store_set(store, &iter, COMPLETION_STORE_FIRST, l->data, -1);
+                found = true;
+            }
+        }
+    }
+
+    return found;
+}
+
 /**
  * Fills file path completion entries into given list store for also given
  * input.
index 94a60ca..ca9ba68 100644 (file)
@@ -33,12 +33,15 @@ typedef void *(*Util_Content_Func)(const char*, const char*);
 void util_cleanup(void);
 char *util_expand(Client *c, const char *src, int expflags);
 gboolean util_file_append(const char *file, const char *format, ...);
+gboolean util_file_prepend(const char *file, const char *format, ...);
+char *util_file_pop_line(const char *file, int *item_count);
 char *util_get_config_dir(void);
 char *util_get_file_contents(const char *filename, gsize *length);
 char *util_get_filepath(const char *dir, const char *filename, gboolean create);
 char **util_get_lines(const char *filename);
 GList *util_file_to_unique_list(const char *filename, Util_Content_Func func,
         guint max_items);
+gboolean util_fill_completion(GtkListStore *store, const char *input, GList *src);
 gboolean util_filename_fill_completion(Client *c, GtkListStore *store, const char *input);
 gboolean util_parse_expansion(Client *c, const char **input, GString *str,
         int flags, const char *quoteable);