From: Daniel Carl Date: Mon, 27 Feb 2017 23:09:19 +0000 (+0100) Subject: Allow to manage bookmarks and queue. X-Git-Url: https://git.owens.tech/assets/me.webp/assets/me.webp/git?a=commitdiff_plain;h=ca8dfe7dee5f4c7474f0723ec65cf9a3978f083e;p=vimb.git Allow to manage bookmarks and queue. --- diff --git a/README.md b/README.md index ca56071..2eee22e 100644 --- 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 index 0000000..110d72d --- /dev/null +++ b/src/bookmark.c @@ -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 + +#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 titletags */ + 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 index 0000000..dea4556 --- /dev/null +++ b/src/bookmark.h @@ -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 */ + diff --git a/src/command.c b/src/command.c index 700bd2c..7fbc878 100644 --- a/src/command.c +++ b/src/command.c @@ -21,11 +21,15 @@ * This file contains functions that are used by normal mode and command mode * together. */ -#include "command.h" +#include + #include "config.h" +#ifdef FEATURE_QUEUE +#include "bookmark.h" +#endif +#include "command.h" #include "history.h" #include "main.h" -#include 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 diff --git a/src/config.def.h b/src/config.def.h index 8b2d27b..37874b7 100644 --- a/src/config.def.h +++ b/src/config.def.h @@ -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 */ diff --git a/src/ex.c b/src/ex.c index c3acc0a..4ca9153 100644 --- a/src/ex.c +++ b/src/ex.c @@ -27,6 +27,7 @@ #include #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: diff --git a/src/normal.c b/src/normal.c index e92de75..faa5733 100644 --- a/src/normal.c +++ b/src/normal.c @@ -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; } diff --git a/src/util.c b/src/util.c index be48e71..29557f2 100644 --- a/src/util.c +++ b/src/util.c @@ -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. diff --git a/src/util.h b/src/util.h index 94a60ca..ca9ba68 100644 --- a/src/util.h +++ b/src/util.h @@ -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);