// Copyright (c) 2006-2009 by Martin Stubenschrott // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. const DEFAULT_FAVICON = "chrome://mozapps/skin/places/defaultFavicon.png"; // Try to import older command line history, quick marks, etc. liberator.registerObserver("load", function () { let branch = "extensions." + config.name.toLowerCase(); if (!options.getPref(branch + ".commandline_cmd_history")) return; let store = storage["history-command"]; let pref = options.getPref(branch + ".commandline_cmd_history"); for (let [k, v] in Iterator(pref.split("\n"))) store.push(v); store = storage["quickmarks"]; pref = options.getPref(branch + ".quickmarks") .split("\n"); while (pref.length > 0) store.set(pref.shift(), pref.shift()); options.resetPref(branch + ".commandline_cmd_history"); options.resetPref(branch + ".commandline_search_history"); options.resetPref(branch + ".quickmarks"); }); // also includes methods for dealing with keywords and search engines function Bookmarks() //{{{ { //////////////////////////////////////////////////////////////////////////////// ////////////////////// PRIVATE SECTION ///////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ const historyService = PlacesUtils.history; const bookmarksService = PlacesUtils.bookmarks; const taggingService = PlacesUtils.tagging; const faviconService = services.get("favicon"); const livemarkService = services.get("livemark"); // XXX for strange Firefox bug :( // Error: [Exception... "Component returned failure code: 0x8000ffff (NS_ERROR_UNEXPECTED) [nsIObserverService.addObserver]" // nsresult: "0x8000ffff (NS_ERROR_UNEXPECTED)" // location: "JS frame :: file://~firefox/components/nsTaggingService.js :: anonymous :: line 89" // data: no] // Source file: file://~firefox/components/nsTaggingService.js taggingService.getTagsForURI(window.makeURI("http://mysterious.bug"), {}); const Bookmark = new Struct("url", "title", "icon", "keyword", "tags", "id"); const Keyword = new Struct("keyword", "title", "icon", "url"); Bookmark.defaultValue("icon", function () getFavicon(this.url)); Bookmark.prototype.__defineGetter__("extra", function () [ ["keyword", this.keyword, "Keyword"], ["tags", this.tags.join(", "), "Tag"] ].filter(function (item) item[1])); const storage = modules.storage; function Cache(name, store) { const rootFolders = [bookmarksService.toolbarFolder, bookmarksService.bookmarksMenuFolder, bookmarksService.unfiledBookmarksFolder]; const sleep = liberator.sleep; // Storage objects are global to all windows, 'liberator' isn't. let bookmarks = []; let self = this; this.__defineGetter__("name", function () name); this.__defineGetter__("store", function () store); this.__defineGetter__("bookmarks", function () this.load()); this.__defineGetter__("keywords", function () [new Keyword(k.keyword, k.title, k.icon, k.url) for ([, k] in Iterator(self.bookmarks)) if (k.keyword)]); this.__iterator__ = function () (val for ([, val] in Iterator(self.bookmarks))); function loadBookmark(node) { try { let uri = util.newURI(node.uri); let keyword = bookmarksService.getKeywordForBookmark(node.itemId); let tags = taggingService.getTagsForURI(uri, {}) || []; let bmark = new Bookmark(node.uri, node.title, node.icon && node.icon.spec, keyword, tags, node.itemId); bookmarks.push(bmark); return bmark; } catch (e) { liberator.dump("Failed to create bookmark for URI: " + node.uri); liberator.reportError(e); return null; } } function readBookmark(id) { return { itemId: id, uri: bookmarksService.getBookmarkURI(id).spec, title: bookmarksService.getItemTitle(id) }; } function deleteBookmark(id) { let length = bookmarks.length; bookmarks = bookmarks.filter(function (item) item.id != id); return bookmarks.length < length; } this.findRoot = function findRoot(id) { do { var root = id; id = bookmarksService.getFolderIdForItem(id); } while (id != bookmarksService.placesRoot && id != root); return root; } this.isBookmark = function (id) rootFolders.indexOf(self.findRoot(id)) >= 0; this.isRegularBookmark = function findRoot(id) { do { var root = id; if (livemarkService && livemarkService.isLivemark(id)) return false; id = bookmarksService.getFolderIdForItem(id); } while (id != bookmarksService.placesRoot && id != root); return rootFolders.indexOf(root) >= 0; } // since we don't use a threaded bookmark loading (by set preload) // anymore, is this loading synchronization still needed? --mst let loading = false; this.load = function load() { if (loading) { while (loading) sleep(10); return bookmarks; } // update our bookmark cache bookmarks = []; loading = true; let folders = rootFolders.slice(); let query = historyService.getNewQuery(); let options = historyService.getNewQueryOptions(); while (folders.length > 0) { query.setFolders(folders, 1); folders.shift(); let result = historyService.executeQuery(query, options); let folder = result.root; folder.containerOpen = true; // iterate over the immediate children of this folder for (let i = 0; i < folder.childCount; i++) { let node = folder.getChild(i); if (node.type == node.RESULT_TYPE_FOLDER) // folder folders.push(node.itemId); else if (node.type == node.RESULT_TYPE_URI) // bookmark loadBookmark(node); } // close a container after using it! folder.containerOpen = false; } this.__defineGetter__("bookmarks", function () bookmarks); loading = false; return bookmarks; }; var observer = { onBeginUpdateBatch: function onBeginUpdateBatch() {}, onEndUpdateBatch: function onEndUpdateBatch() {}, onItemVisited: function onItemVisited() {}, onItemMoved: function onItemMoved() {}, onItemAdded: function onItemAdded(itemId, folder, index) { // liberator.dump("onItemAdded(" + itemId + ", " + folder + ", " + index + ")\n"); if (bookmarksService.getItemType(itemId) == bookmarksService.TYPE_BOOKMARK) { if (self.isBookmark(itemId)) { let bmark = loadBookmark(readBookmark(itemId)); storage.fireEvent(name, "add", bmark); } } }, onItemRemoved: function onItemRemoved(itemId, folder, index) { // liberator.dump("onItemRemoved(" + itemId + ", " + folder + ", " + index + ")\n"); if (deleteBookmark(itemId)) storage.fireEvent(name, "remove", itemId); }, onItemChanged: function onItemChanged(itemId, property, isAnnotation, value) { if (isAnnotation) return; // liberator.dump("onItemChanged(" + itemId + ", " + property + ", " + value + ")\n"); let bookmark = bookmarks.filter(function (item) item.id == itemId)[0]; if (bookmark) { if (property == "tags") value = taggingService.getTagsForURI(util.newURI(bookmark.url), {}); if (property in bookmark) bookmark[property] = value; storage.fireEvent(name, "change", itemId); } }, QueryInterface: function QueryInterface(iid) { if (iid.equals(Ci.nsINavBookmarkObserver) || iid.equals(Ci.nsISupports)) return this; throw Cr.NS_ERROR_NO_INTERFACE; } }; bookmarksService.addObserver(observer, false); } function getFavicon(uri) { try { return faviconService.getFaviconImageForPage(util.newURI(uri)).spec; } catch (e) { return ""; } } let bookmarkObserver = function (key, event, arg) { if (event == "add") autocommands.trigger("BookmarkAdd", arg); statusline.updateUrl(); }; var cache = storage.newObject("bookmark-cache", Cache, { store: false }); storage.addObserver("bookmark-cache", bookmarkObserver, window); /////////////////////////////////////////////////////////////////////////////}}} ////////////////////// OPTIONS ///////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ options.add(["defsearch", "ds"], "Set the default search engine", "string", "google", { completer: function completer(context) { completion.search(context, true); context.completions = [["", "Don't perform searches by default"]].concat(context.completions); }, validator: Option.validateCompleter }); /////////////////////////////////////////////////////////////////////////////}}} ////////////////////// MAPPINGS //////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ var myModes = config.browserModes; mappings.add(myModes, ["a"], "Open a prompt to bookmark the current URL", function () { let options = {}; let bmarks = bookmarks.get(buffer.URL).filter(function (bmark) bmark.url == buffer.URL); if (bmarks.length == 1) { let bmark = bmarks[0]; options["-title"] = bmark.title; if (bmark.keyword) options["-keyword"] = bmark.keyword; if (bmark.tags.length > 0) options["-tags"] = bmark.tags.join(", "); } else { if (buffer.title != buffer.URL) options["-title"] = buffer.title; } commandline.open(":", commands.commandToString({ command: "bmark", options: options, arguments: [buffer.URL], bang: bmarks.length == 1 }), modes.EX); }); mappings.add(myModes, ["A"], "Toggle bookmarked state of current URL", function () { bookmarks.toggle(buffer.URL); }); /////////////////////////////////////////////////////////////////////////////}}} ////////////////////// COMMANDS //////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ commands.add(["ju[mps]"], "Show jumplist", function () { let sh = history.session; let list = template.jumps(sh.index, sh); commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE); }, { argCount: "0" }); // TODO: Clean this up. function tags(context, args) { let filter = context.filter; let have = filter.split(","); args.completeFilter = have.pop(); let prefix = filter.substr(0, filter.length - args.completeFilter.length); let tags = util.Array.uniq(util.Array.flatten([b.tags for ([k, b] in Iterator(cache.bookmarks))])); return [[prefix + tag, tag] for ([i, tag] in Iterator(tags)) if (have.indexOf(tag) < 0)]; } function title(context, args) { if (!args.bang) return [[content.document.title, "Current Page Title"]]; context.keys.text = "title"; context.keys.description = "url"; return bookmarks.get(args.join(" "), args["-tags"], null, { keyword: args["-keyword"], title: context.filter }); } function keyword(context, args) { if (!args.bang) return []; context.keys.text = "keyword"; return bookmarks.get(args.join(" "), args["-tags"], null, { keyword: context.filter, title: args["-title"] }); } commands.add(["bma[rk]"], "Add a bookmark", function (args) { let url = args.length == 0 ? buffer.URL : args[0]; let title = args["-title"] || (args.length == 0 ? buffer.title : null); let keyword = args["-keyword"] || null; let tags = args["-tags"] || []; if (bookmarks.add(false, title, url, keyword, tags, args.bang)) { let extra = (title == url) ? "" : " (" + title + ")"; liberator.echomsg("Added bookmark: " + url + extra, 1, commandline.FORCE_SINGLELINE); } else liberator.echoerr("Exxx: Could not add bookmark `" + title + "'", commandline.FORCE_SINGLELINE); }, { argCount: "?", bang: true, completer: function (context, args) { if (!args.bang) { context.completions = [[content.document.documentURI, "Current Location"]]; return; } completion.bookmark(context, args["-tags"], { keyword: args["-keyword"], title: args["-title"] }); }, options: [[["-title", "-t"], commands.OPTION_STRING, null, title], [["-tags", "-T"], commands.OPTION_LIST, null, tags], [["-keyword", "-k"], commands.OPTION_STRING, function (arg) /\w/.test(arg)]] }); commands.add(["bmarks"], "List or open multiple bookmarks", function (args) { bookmarks.list(args.join(" "), args["-tags"] || [], args.bang, args["-max"]); }, { bang: true, completer: function completer(context, args) { context.quote = null; context.filter = args.join(" "); completion.bookmark(context, args["-tags"]); }, options: [[["-tags", "-T"], commands.OPTION_LIST, null, tags], [["-max", "-m"], commands.OPTION_INT]] }); commands.add(["delbm[arks]"], "Delete a bookmark", function (args) { if (args.bang) { commandline.input("This will delete all bookmarks. Would you like to continue? (yes/[no]) ", function (resp) { if (resp && resp.match(/^y(es)?$/i)) { cache.bookmarks.forEach(function (bmark) { bookmarksService.removeItem(bmark.id); }); liberator.echomsg("All bookmarks deleted", 1, commandline.FORCE_SINGLELINE); } }); } else { let url = args.string || buffer.URL; let deletedCount = bookmarks.remove(url); liberator.echomsg(deletedCount + " bookmark(s) with url " + url.quote() + " deleted", 1, commandline.FORCE_SINGLELINE); } }, { argCount: "?", bang: true, completer: function completer(context) completion.bookmark(context), literal: 0 }); /////////////////////////////////////////////////////////////////////////////}}} ////////////////////// COMPLETIONS ///////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ completion.bookmark = function bookmark(context, tags, extra) { context.title = ["Bookmark", "Title"]; context.format = bookmarks.format; for (let val in Iterator(extra || [])) { let [k, v] = val; // Need block scope here for the closure if (v) context.filters.push(function (item) this._match(v, item[k])); } // Need to make a copy because set completions() checks instanceof Array, // and this may be an Array from another window. context.completions = Array.slice(storage["bookmark-cache"].bookmarks); completion.urls(context, tags); }; completion.search = function search(context, noSuggest) { let [, keyword, space, args] = context.filter.match(/^\s*(\S*)(\s*)(.*)$/); let keywords = bookmarks.getKeywords(); let engines = bookmarks.getSearchEngines(); context.title = ["Search Keywords"]; context.completions = keywords.concat(engines); context.keys = { text: 0, description: 1, icon: 2 }; if (!space || noSuggest) return; context.fork("suggest", keyword.length + space.length, this, "searchEngineSuggest", keyword, true); let item = keywords.filter(function (k) k.keyword == keyword)[0]; if (item && item.url.indexOf("%s") > -1) context.fork("keyword/" + keyword, keyword.length + space.length, null, function (context) { context.format = history.format; context.title = [keyword + " Quick Search"]; // context.background = true; context.compare = CompletionContext.Sort.unsorted; context.generate = function () { let [begin, end] = item.url.split("%s"); return history.get({ uri: window.makeURI(begin), uriIsPrefix: true }).map(function (item) { let rest = item.url.length - end.length; let query = item.url.substring(begin.length, rest); if (item.url.substr(rest) == end && query.indexOf("&") == -1) { item.url = decodeURIComponent(query.replace(/#.*/, "")); return item; } }).filter(util.identity); }; }); }; completion.searchEngineSuggest = function searchEngineSuggest(context, engineAliases, kludge) { if (!context.filter) return; let engineList = (engineAliases || options["suggestengines"] || "google").split(","); let completions = []; engineList.forEach(function (name) { let engine = services.get("browserSearch").getEngineByAlias(name); if (!engine) return; let [, word] = /^\s*(\S+)/.exec(context.filter) || []; if (!kludge && word == name) // FIXME: Check for matching keywords return; let ctxt = context.fork(name, 0); ctxt.title = [engine.description + " Suggestions"]; ctxt.compare = CompletionContext.Sort.unsorted; ctxt.incomplete = true; bookmarks.getSuggestions(name, ctxt.filter, function (compl) { ctxt.incomplete = false; ctxt.completions = compl; }); }); }; completion.addUrlCompleter("S", "Suggest engines", completion.searchEngineSuggest); completion.addUrlCompleter("b", "Bookmarks", completion.bookmark); completion.addUrlCompleter("s", "Search engines and keyword URLs", completion.search); /////////////////////////////////////////////////////////////////////////////}}} ////////////////////// PUBLIC SECTION ////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ return { get format() ({ anchored: false, title: ["URL", "Info"], keys: { text: "url", description: "title", icon: "icon", extra: "extra", tags: "tags" }, process: [template.icon, template.bookmarkDescription] }), // TODO: why is this a filter? --djk get: function get(filter, tags, maxItems, extra) { return completion.runCompleter("bookmark", filter, maxItems, tags, extra); }, // if starOnly = true it is saved in the unfiledBookmarksFolder, otherwise in the bookmarksMenuFolder add: function add(starOnly, title, url, keyword, tags, force) { try { let uri = util.createURI(url); if (!force) { for (let bmark in cache) { if (bmark[0] == uri.spec) { var id = bmark[5]; if (title) bookmarksService.setItemTitle(id, title); break; } } } if (id == undefined) id = bookmarksService.insertBookmark( bookmarksService[starOnly ? "unfiledBookmarksFolder" : "bookmarksMenuFolder"], uri, -1, title || url); if (!id) return false; if (keyword) bookmarksService.setKeywordForBookmark(id, keyword); if (tags) { taggingService.untagURI(uri, null); taggingService.tagURI(uri, tags); } } catch (e) { liberator.log(e, 0); return false; } return true; }, toggle: function toggle(url) { if (!url) return; let count = this.remove(url); if (count > 0) commandline.echo("Removed bookmark: " + url, commandline.HL_NORMAL, commandline.FORCE_SINGLELINE); else { let title = buffer.title || url; let extra = ""; if (title != url) extra = " (" + title + ")"; this.add(true, title, url); commandline.echo("Added bookmark: " + url + extra, commandline.HL_NORMAL, commandline.FORCE_SINGLELINE); } }, isBookmarked: function isBookmarked(url) { try { return bookmarksService.getBookmarkIdsForURI(makeURI(url), {}) .some(cache.isRegularBookmark); } catch (e) { return false; } }, // returns number of deleted bookmarks remove: function remove(url) { try { let uri = util.newURI(url); let bmarks = bookmarksService.getBookmarkIdsForURI(uri, {}) .filter(cache.isRegularBookmark); bmarks.forEach(bookmarksService.removeItem); return bmarks.length; } catch (e) { liberator.log(e, 0); return 0; } }, getFavicon: function (url) getFavicon(url), // TODO: add filtering // also ensures that each search engine has a Liberator-friendly alias getSearchEngines: function getSearchEngines() { let searchEngines = []; for (let [, engine] in Iterator(services.get("browserSearch").getVisibleEngines({}))) { let alias = engine.alias; if (!alias || !/^[a-z0-9_-]+$/.test(alias)) alias = engine.name.replace(/^\W*([a-zA-Z_-]+).*/, "$1").toLowerCase(); if (!alias) alias = "search"; // for search engines which we can't find a suitable alias // make sure we can use search engines which would have the same alias (add numbers at the end) let newAlias = alias; for (let j = 1; j <= 10; j++) // <=10 is intentional { if (!searchEngines.some(function (item) item[0] == newAlias)) break; newAlias = alias + j; } // only write when it changed, writes are really slow if (engine.alias != newAlias) engine.alias = newAlias; searchEngines.push([engine.alias, engine.description, engine.iconURI && engine.iconURI.spec]); } return searchEngines; }, getSuggestions: function getSuggestions(engineName, query, callback) { const responseType = "application/x-suggestions+json"; let engine = services.get("browserSearch").getEngineByAlias(engineName); if (engine && engine.supportsResponseType(responseType)) var queryURI = engine.getSubmission(query, responseType).uri.spec; if (!queryURI) return []; function process(resp) { let results = []; try { results = services.get("json").decode(resp.responseText)[1]; results = [[item, ""] for ([k, item] in Iterator(results)) if (typeof item == "string")]; } catch (e) {} if (!callback) return results; callback(results); } let resp = util.httpGet(queryURI, callback && process); if (!callback) return process(resp); }, // TODO: add filtering // format of returned array: // [keyword, helptext, url] getKeywords: function getKeywords() { return cache.keywords; }, // full search string including engine name as first word in @param text // if @param useDefSearch is true, it uses the default search engine // @returns the url for the search string // if the search also requires a postData, [url, postData] is returned getSearchURL: function getSearchURL(text, useDefsearch) { let searchString = (useDefsearch ? options["defsearch"] + " " : "") + text; // we need to make sure our custom alias have been set, even if the user // did not :open once before this.getSearchEngines(); // ripped from Firefox function getShortcutOrURI(url) { var shortcutURL = null; var keyword = url; var param = ""; var offset = url.indexOf(" "); if (offset > 0) { keyword = url.substr(0, offset); param = url.substr(offset + 1); } var engine = services.get("browserSearch").getEngineByAlias(keyword); if (engine) { var submission = engine.getSubmission(param, null); return [submission.uri.spec, submission.postData]; } [shortcutURL, postData] = PlacesUtils.getURLAndPostDataForKeyword(keyword); if (!shortcutURL) return [url, null]; let data = window.unescape(postData || ""); if (/%s/i.test(shortcutURL) || /%s/i.test(data)) { var charset = ""; var matches = shortcutURL.match(/^(.*)\&mozcharset=([a-zA-Z][_\-a-zA-Z0-9]+)\s*$/); if (matches) [, shortcutURL, charset] = matches; else { try { charset = PlacesUtils.history.getCharsetForURI(window.makeURI(shortcutURL)); } catch (e) {} } var encodedParam; if (charset) encodedParam = escape(window.convertFromUnicode(charset, param)); else encodedParam = encodeURIComponent(param); shortcutURL = shortcutURL.replace(/%s/g, encodedParam).replace(/%S/g, param); if (/%s/i.test(data)) postData = window.getPostDataStream(data, param, encodedParam, "application/x-www-form-urlencoded"); } else if (param) return [shortcutURL, null]; return [shortcutURL, postData]; } let [url, postData] = getShortcutOrURI(searchString); if (url == searchString) return null; if (postData) return [url, postData]; return url; // can be null }, // if openItems is true, open the matching bookmarks items in tabs rather than display list: function list(filter, tags, openItems, maxItems) { // FIXME: returning here doesn't make sense // Why the hell doesn't it make sense? --Kris // Because it unconditionally bypasses the final error message // block and does so only when listing items, not opening them. In // short it breaks the :bmarks command which doesn't make much // sense to me but I'm old-fashioned. --djk if (!openItems) return completion.listCompleter("bookmark", filter, maxItems, tags); let items = completion.runCompleter("bookmark", filter, maxItems, tags); if (items.length) return liberator.open(items.map(function (i) i.url), liberator.NEW_TAB); if (filter.length > 0 && tags.length > 0) liberator.echoerr("E283: No bookmarks matching tags: \"" + tags + "\" and string: \"" + filter + "\""); else if (filter.length > 0) liberator.echoerr("E283: No bookmarks matching string: \"" + filter + "\""); else if (tags.length > 0) liberator.echoerr("E283: No bookmarks matching tags: \"" + tags + "\""); else liberator.echoerr("No bookmarks set"); } }; //}}} } //}}} function History() //{{{ { //////////////////////////////////////////////////////////////////////////////// ////////////////////// PRIVATE SECTION ///////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ const historyService = PlacesUtils.history; /////////////////////////////////////////////////////////////////////////////}}} ////////////////////// MAPPINGS //////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ var myModes = config.browserModes; mappings.add(myModes, [""], "Go to an older position in the jump list", function (count) { history.stepTo(-Math.max(count, 1)); }, { count: true }); mappings.add(myModes, [""], "Go to a newer position in the jump list", function (count) { history.stepTo(Math.max(count, 1)); }, { count: true }); mappings.add(myModes, ["H", "", ""], "Go back in the browser history", function (count) { history.stepTo(-Math.max(count, 1)); }, { count: true }); mappings.add(myModes, ["L", "", ""], "Go forward in the browser history", function (count) { history.stepTo(Math.max(count, 1)); }, { count: true }); /////////////////////////////////////////////////////////////////////////////}}} ////////////////////// COMMANDS //////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ commands.add(["ba[ck]"], "Go back in the browser history", function (args) { let url = args.literalArg; if (args.bang) history.goToStart(); else { if (url) { let sh = history.session; if (/^\d+(:|$)/.test(url) && sh.index - parseInt(url) in sh) return void window.getWebNavigation().gotoIndex(sh.index - parseInt(url)); for (let [i, ent] in Iterator(sh.slice(0, sh.index).reverse())) if (ent.URI.spec == url) return void window.getWebNavigation().gotoIndex(i); liberator.echoerr("Exxx: URL not found in history"); } else history.stepTo(-Math.max(args.count, 1)); } }, { argCount: "?", bang: true, completer: function completer(context) { let sh = history.session; context.anchored = false; context.compare = CompletionContext.Sort.unsorted; context.filters = [CompletionContext.Filter.textDescription]; context.completions = sh.slice(0, sh.index).reverse(); context.keys = { text: function (item) (sh.index - item.index) + ": " + item.URI.spec, description: "title", icon: "icon" }; }, count: true, literal: 0 }); commands.add(["fo[rward]", "fw"], "Go forward in the browser history", function (args) { let url = args.literalArg; if (args.bang) history.goToEnd(); else { if (url) { let sh = history.session; if (/^\d+(:|$)/.test(url) && sh.index + parseInt(url) in sh) return void window.getWebNavigation().gotoIndex(sh.index + parseInt(url)); for (let [i, ent] in Iterator(sh.slice(sh.index + 1))) if (ent.URI.spec == url) return void window.getWebNavigation().gotoIndex(i); liberator.echoerr("Exxx: URL not found in history"); } else history.stepTo(Math.max(args.count, 1)); } }, { argCount: "?", bang: true, completer: function completer(context) { let sh = history.session; context.anchored = false; context.compare = CompletionContext.Sort.unsorted; context.filters = [CompletionContext.Filter.textDescription]; context.completions = sh.slice(sh.index + 1); context.keys = { text: function (item) (item.index - sh.index) + ": " + item.URI.spec, description: "title", icon: "icon" }; }, count: true, literal: 0 }); commands.add(["hist[ory]", "hs"], "Show recently visited URLs", function (args) { history.list(args.join(" "), args.bang, args["-max"] || 1000); }, { bang: true, completer: function (context) { context.quote = null; completion.history(context); }, // completer: function (filter) completion.history(filter) options: [[["-max", "-m"], commands.OPTION_INT]] }); /////////////////////////////////////////////////////////////////////////////}}} ////////////////////// COMPLETIONS ///////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ completion.history = function _history(context, maxItems) { context.format = history.format; context.title = ["History"]; context.compare = CompletionContext.Sort.unsorted; //context.background = true; if (context.maxItems == null) context.maxItems = 100; context.regenerate = true; context.generate = function () history.get(context.filter, this.maxItems); }; completion.addUrlCompleter("h", "History", completion.history); /////////////////////////////////////////////////////////////////////////////}}} ////////////////////// PUBLIC SECTION ////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ return { get format() bookmarks.format, get service() historyService, get: function get(filter, maxItems) { // no query parameters will get all history let query = historyService.getNewQuery(); let options = historyService.getNewQueryOptions(); if (typeof filter == "string") filter = { searchTerms: filter }; for (let [k, v] in Iterator(filter)) query[k] = v; options.sortingMode = options.SORT_BY_DATE_DESCENDING; options.resultType = options.RESULTS_AS_URI; if (maxItems > 0) options.maxResults = maxItems; // execute the query let root = historyService.executeQuery(query, options).root; root.containerOpen = true; let items = util.map(util.range(0, root.childCount), function (i) { let node = root.getChild(i); return { url: node.uri, title: node.title, icon: node.icon ? node.icon.spec : DEFAULT_FAVICON }; }); root.containerOpen = false; // close a container after using it! return items; }, get session() { let sh = window.getWebNavigation().sessionHistory; let obj = []; obj.index = sh.index; obj.__iterator__ = function () util.Array.iteritems(this); for (let i in util.range(0, sh.count)) { obj[i] = { index: i, __proto__: sh.getEntryAtIndex(i, false) }; util.memoize(obj[i], "icon", function (obj) services.get("favicon").getFaviconImageForPage(obj.URI).spec); } return obj; }, // TODO: better names stepTo: function stepTo(steps) { let start = 0; let end = window.getWebNavigation().sessionHistory.count - 1; let current = window.getWebNavigation().sessionHistory.index; if (current == start && steps < 0 || current == end && steps > 0) liberator.beep(); else { let index = util.Math.constrain(current + steps, start, end); window.getWebNavigation().gotoIndex(index); } }, goToStart: function goToStart() { let index = window.getWebNavigation().sessionHistory.index; if (index > 0) window.getWebNavigation().gotoIndex(0); else liberator.beep(); }, goToEnd: function goToEnd() { let sh = window.getWebNavigation().sessionHistory; let max = sh.count - 1; if (sh.index < max) window.getWebNavigation().gotoIndex(max); else liberator.beep(); }, // if openItems is true, open the matching history items in tabs rather than display list: function list(filter, openItems, maxItems) { // FIXME: returning here doesn't make sense // Why the hell doesn't it make sense? --Kris // See comment at bookmarks.list --djk if (!openItems) return completion.listCompleter("history", filter, maxItems); let items = completion.runCompleter("history", filter, maxItems); if (items.length) return liberator.open(items.map(function (i) i.url), liberator.NEW_TAB); if (filter.length > 0) liberator.echoerr("E283: No history matching \"" + filter + "\""); else liberator.echoerr("No history set"); } }; //}}} } //}}} /** @scope modules */ /** * @instance quickmarks */ function QuickMarks() //{{{ { //////////////////////////////////////////////////////////////////////////////// ////////////////////// PRIVATE SECTION ///////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ var qmarks = storage.newMap("quickmarks", true); /////////////////////////////////////////////////////////////////////////////}}} ////////////////////// MAPPINGS //////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ var myModes = config.browserModes; mappings.add(myModes, ["go"], "Jump to a QuickMark", function (arg) { quickmarks.jumpTo(arg, liberator.CURRENT_TAB); }, { arg: true }); mappings.add(myModes, ["gn"], "Jump to a QuickMark in a new tab", function (arg) { quickmarks.jumpTo(arg, /\bquickmark\b/.test(options["activate"]) ? liberator.NEW_TAB : liberator.NEW_BACKGROUND_TAB); }, { arg: true }); mappings.add(myModes, ["M"], "Add new QuickMark for current URL", function (arg) { if (/[^a-zA-Z0-9]/.test(arg)) return void liberator.beep(); quickmarks.add(arg, buffer.URL); }, { arg: true }); /////////////////////////////////////////////////////////////////////////////}}} ////////////////////// COMMANDS //////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ commands.add(["delqm[arks]"], "Delete the specified QuickMarks", function (args) { // TODO: finish arg parsing - we really need a proper way to do this. :) if (!args.bang && !args.string) return void liberator.echoerr("E471: Argument required"); if (args.bang && args.string) return void liberator.echoerr("E474: Invalid argument"); if (args.bang) quickmarks.removeAll(); else quickmarks.remove(args.string); }, { bang: true, completer: function (context) { context.title = ["QuickMark", "URL"]; context.completions = qmarks; } }); commands.add(["qma[rk]"], "Mark a URL with a letter for quick access", function (args) { let matches = args.string.match(/^([a-zA-Z0-9])(?:\s+(.+))?$/); if (!matches) liberator.echoerr("E488: Trailing characters"); else if (!matches[2]) quickmarks.add(matches[1], buffer.URL); else quickmarks.add(matches[1], matches[2]); }, { argCount: "+" }); commands.add(["qmarks"], "Show all QuickMarks", function (args) { args = args.string; // ignore invalid qmark characters unless there are no valid qmark chars if (args && !/[a-zA-Z0-9]/.test(args)) return void liberator.echoerr("E283: No QuickMarks matching \"" + args + "\""); let filter = args.replace(/[^a-zA-Z0-9]/g, ""); quickmarks.list(filter); }); /////////////////////////////////////////////////////////////////////////////}}} ////////////////////// PUBLIC SECTION ////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////{{{ return { /** * Adds a new quickmark with name qmark referencing * the URL location. Any existing quickmark with the same name * will be replaced. * * @param {string} qmark The name of the quickmark {A-Z}. * @param {string} location The URL accessed by this quickmark. */ add: function add(qmark, location) { qmarks.set(qmark, location); liberator.echomsg("Added Quick Mark '" + qmark + "': " + location, 1); }, /** * Deletes the specified quickmarks. The filter is a list of * quickmarks and ranges are supported. Eg. "ab c d e-k". * * @param {string} filter The list of quickmarks to delete. * */ remove: function remove(filter) { let pattern = RegExp("[" + filter.replace(/\s+/g, "") + "]"); for (let [qmark, ] in qmarks) { if (pattern.test(qmark)) qmarks.remove(qmark); } }, /** * Removes all quickmarks. */ removeAll: function removeAll() { qmarks.clear(); }, /** * Opens the URL referenced by the specified qmark. * * @param {string} qmark The quickmark to open. * @param {number} where A constant describing where to open the page. * See {@link Liberator#open}. */ jumpTo: function jumpTo(qmark, where) { let url = qmarks.get(qmark); if (url) liberator.open(url, where); else liberator.echoerr("E20: QuickMark not set"); }, /** * Lists all quickmarks matching filter in the message window. * * @param {string} filter The list of quickmarks to display. Eg. "abc" * Ranges are not supported. */ // FIXME: filter should match that of quickmarks.remove or vice versa list: function list(filter) { let marks = [k for ([k, v] in qmarks)]; let lowercaseMarks = marks.filter(function (x) /[a-z]/.test(x)).sort(); let uppercaseMarks = marks.filter(function (x) /[A-Z]/.test(x)).sort(); let numberMarks = marks.filter(function (x) /[0-9]/.test(x)).sort(); marks = Array.concat(lowercaseMarks, uppercaseMarks, numberMarks); if (marks.length == 0) return void liberator.echoerr("No QuickMarks set"); if (filter.length > 0) { marks = marks.filter(function (qmark) filter.indexOf(qmark) >= 0); if (marks.length == 0) return void liberator.echoerr("E283: No QuickMarks matching \"" + filter + "\""); } let items = [[mark, qmarks.get(mark)] for ([k, mark] in Iterator(marks))]; template.genericTable(items, { title: ["QuickMark", "URL"] }); } }; //}}} } //}}} // vim: set fdm=marker sw=4 ts=4 et: