diff --git a/searx/plugins/__init__.py b/searx/plugins/__init__.py index 87cc0138..efb9b068 100644 --- a/searx/plugins/__init__.py +++ b/searx/plugins/__init__.py @@ -23,7 +23,8 @@ from searx.plugins import (https_rewrite, open_results_on_new_tab, self_info, search_on_category_select, - tracker_url_remover) + tracker_url_remover, + vim_hotkeys) required_attrs = (('name', str), ('description', str), @@ -77,3 +78,4 @@ plugins.register(open_results_on_new_tab) plugins.register(self_info) plugins.register(search_on_category_select) plugins.register(tracker_url_remover) +plugins.register(vim_hotkeys) diff --git a/searx/plugins/vim_hotkeys.py b/searx/plugins/vim_hotkeys.py new file mode 100644 index 00000000..e537a3ac --- /dev/null +++ b/searx/plugins/vim_hotkeys.py @@ -0,0 +1,10 @@ +from flask.ext.babel import gettext + +name = gettext('Vim-like hotkeys') +description = gettext('Navigate search results with Vim-like hotkeys ' + '(JavaScript required). ' + 'Press "h" key on main or result page to get help.') +default_on = False + +js_dependencies = ('plugins/js/vim_hotkeys.js',) +css_dependencies = ('plugins/css/vim_hotkeys.css',) diff --git a/searx/static/plugins/css/vim_hotkeys.css b/searx/static/plugins/css/vim_hotkeys.css new file mode 100644 index 00000000..2ccfdc1a --- /dev/null +++ b/searx/static/plugins/css/vim_hotkeys.css @@ -0,0 +1,26 @@ +.vim-hotkeys-help { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 9999999; + overflow-y: auto; + max-height: 80%; + box-shadow: 0 0 1em; +} + +.dflex { + display: -webkit-box; /* OLD - iOS 6-, Safari 3.1-6 */ + display: -moz-box; /* OLD - Firefox 19- (buggy but mostly works) */ + display: -ms-flexbox; /* TWEENER - IE 10 */ + display: -webkit-flex; /* NEW - Chrome */ + display: flex; /* NEW, Spec - Opera 12.1, Firefox 20+ */ +} + +.iflex { + -webkit-box-flex: 1; /* OLD - iOS 6-, Safari 3.1-6 */ + -moz-box-flex: 1; /* OLD - Firefox 19- */ + -webkit-flex: 1; /* Chrome */ + -ms-flex: 1; /* IE 10 */ + flex: 1; /* NEW, Spec - Opera 12.1, Firefox 20+ */ +} diff --git a/searx/static/plugins/js/vim_hotkeys.js b/searx/static/plugins/js/vim_hotkeys.js new file mode 100644 index 00000000..61500d8f --- /dev/null +++ b/searx/static/plugins/js/vim_hotkeys.js @@ -0,0 +1,336 @@ +$(document).ready(function() { + highlightResult('top')(); + + $('.result').on('click', function() { + highlightResult($(this))(); + }); + + var vimKeys = { + 27: { + key: 'Escape', + fun: removeFocus, + des: 'remove focus from the focused input', + cat: 'Control' + }, + 73: { + key: 'i', + fun: searchInputFocus, + des: 'focus on the search input', + cat: 'Control' + }, + 66: { + key: 'b', + fun: scrollPage(-window.innerHeight), + des: 'scroll one page up', + cat: 'Navigation' + }, + 70: { + key: 'f', + fun: scrollPage(window.innerHeight), + des: 'scroll one page down', + cat: 'Navigation' + }, + 85: { + key: 'u', + fun: scrollPage(-window.innerHeight / 2), + des: 'scroll half a page up', + cat: 'Navigation' + }, + 68: { + key: 'd', + fun: scrollPage(window.innerHeight / 2), + des: 'scroll half a page down', + cat: 'Navigation' + }, + 71: { + key: 'g', + fun: scrollPageTo(-document.body.scrollHeight, 'top'), + des: 'scroll to the top of the page', + cat: 'Navigation' + }, + 86: { + key: 'v', + fun: scrollPageTo(document.body.scrollHeight, 'bottom'), + des: 'scroll to the bottom of the page', + cat: 'Navigation' + }, + 75: { + key: 'k', + fun: highlightResult('up'), + des: 'select previous search result', + cat: 'Results' + }, + 74: { + key: 'j', + fun: highlightResult('down'), + des: 'select next search result', + cat: 'Results' + }, + 80: { + key: 'p', + fun: pageButtonClick(0), + des: 'go to previous page', + cat: 'Results' + }, + 78: { + key: 'n', + fun: pageButtonClick(1), + des: 'go to next page', + cat: 'Results' + }, + 79: { + key: 'o', + fun: openResult(false), + des: 'open search result', + cat: 'Results' + }, + 84: { + key: 't', + fun: openResult(true), + des: 'open the result in a new tab', + cat: 'Results' + }, + 82: { + key: 'r', + fun: reloadPage, + des: 'reload page from the server', + cat: 'Control' + }, + 72: { + key: 'h', + fun: toggleHelp, + des: 'toggle help window', + cat: 'Other' + } + }; + + $(document).keyup(function(e) { + // check for modifiers so we don't break browser's hotkeys + if (vimKeys.hasOwnProperty(e.keyCode) + && !e.ctrlKey + && !e.altKey + && !e.shiftKey + && !e.metaKey) + { + if (e.keyCode === 27) { + if (e.target.tagName.toLowerCase() === 'input') { + vimKeys[e.keyCode].fun(); + } + } else { + if (e.target === document.body) { + vimKeys[e.keyCode].fun(); + } + } + } + }); + + function highlightResult(which) { + return function() { + var current = $('.result[data-vim-selected]'); + if (current.length === 0) { + current = $('.result:first'); + if (current.length === 0) { + return; + } + } + + var next; + + if (typeof which !== 'string') { + next = which; + } else { + switch (which) { + case 'visible': + var top = $(window).scrollTop(); + var bot = top + $(window).height(); + var results = $('.result'); + + for (var i = 0; i < results.length; i++) { + next = $(results[i]); + var etop = next.offset().top; + var ebot = etop + next.height(); + + if ((ebot <= bot) && (etop > top)) { + break; + } + } + break; + case 'down': + next = current.next('.result'); + if (next.length === 0) { + next = $('.result:first'); + } + break; + case 'up': + next = current.prev('.result'); + if (next.length === 0) { + next = $('.result:last'); + } + break; + case 'bottom': + next = $('.result:last'); + break; + case 'top': + default: + next = $('.result:first'); + } + } + + if (next) { + current.removeAttr('data-vim-selected').removeClass('well well-sm'); + next.attr('data-vim-selected', 'true').addClass('well well-sm'); + scrollPageToSelected(); + } + } + } + + function reloadPage() { + document.location.reload(false); + } + + function removeFocus() { + if (document.activeElement) { + document.activeElement.blur(); + } + } + + function pageButtonClick(num) { + return function() { + var buttons = $('div#pagination button[type="submit"]'); + if (buttons.length !== 2) { + console.log('page navigation with this theme is not supported'); + return; + } + if (num >= 0 && num < buttons.length) { + buttons[num].click(); + } else { + console.log('pageButtonClick(): invalid argument'); + } + } + } + + function scrollPageToSelected() { + var sel = $('.result[data-vim-selected]'); + if (sel.length !== 1) { + return; + } + + var wnd = $(window); + + var wtop = wnd.scrollTop(); + var etop = sel.offset().top; + + var offset = 30; + + if (wtop > etop) { + wnd.scrollTop(etop - offset); + } else { + var ebot = etop + sel.height(); + var wbot = wtop + wnd.height(); + + if (wbot < ebot) { + wnd.scrollTop(ebot - wnd.height() + offset); + } + } + } + + function scrollPage(amount) { + return function() { + window.scrollBy(0, amount); + highlightResult('visible')(); + } + } + + function scrollPageTo(position, nav) { + return function() { + window.scrollTo(0, position); + highlightResult(nav)(); + } + } + + function searchInputFocus() { + $('input#q').focus(); + } + + function openResult(newTab) { + return function() { + var link = $('.result[data-vim-selected] .result_header a'); + if (link.length) { + var url = link.attr('href'); + if (newTab) { + window.open(url); + } else { + window.location.href = url; + } + } + }; + } + + function toggleHelp() { + var helpPanel = $('#vim-hotkeys-help'); + if (helpPanel.length) { + helpPanel.toggleClass('hidden'); + return; + } + + var categories = {}; + + for (var k in vimKeys) { + var key = vimKeys[k]; + categories[key.cat] = categories[key.cat] || []; + categories[key.cat].push(key); + } + + var sorted = Object.keys(categories).sort(function(a, b) { + return categories[b].length - categories[a].length; + }); + + if (sorted.length === 0) { + return; + } + + var html = '