// -*- eval: (outline-minor-mode 1) -*- // * Imports const {classes: Cc, interfaces: Ci, utils: Cu} = Components const {Preferences} = ChromeUtils.importESModule('resource://gre/modules/Preferences.sys.mjs', {}) const {AddonManager} = ChromeUtils.importESModule("resource://gre/modules/AddonManager.sys.mjs") const {PlacesUtils} = ChromeUtils.importESModule("resource://gre/modules/PlacesUtils.sys.mjs") const {FileUtils} = ChromeUtils.importESModule("resource://gre/modules/FileUtils.sys.mjs") const nsIEnvironment = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment) Cu.importGlobalProperties(['PathUtils']); // * Helpers let vimFXDirectory = __dirname.replace(new RegExp("^file://"), "") vimfx.on("locationChange", ({vim}) => vimfx.send(vim, "locationChange")) // `pathSearch' and `exec' adapted from // https://github.com/azuwis/.vimfx/blob/master/config.js function pathSearch(bin) { if (PathUtils.isAbsolute(bin)) return bin let pathListSep = (Services.appinfo.OS == 'WINNT') ? ';' : ':' let dirs = nsIEnvironment.get("PATH").split(pathListSep) let file = Cc['@mozilla.org/file/local;1'].createInstance(Ci.nsIFile) for (let dir of dirs) { let path = PathUtils.join(dir, bin) file.initWithPath(path) if (file.exists() && file.isFile() && file.isExecutable()) return path } return null } function makeProcess(cmd) { let file = Cc['@mozilla.org/file/local;1'].createInstance(Ci.nsIFile) file.initWithPath(pathSearch(cmd)) let process = Cc['@mozilla.org/process/util;1'].createInstance(Ci.nsIProcess) process.init(file) return process } function exec(cmd, ...args) { return makeProcess(cmd).runAsync(args, args.length) } function execSync(cmd, ...args) { return makeProcess(cmd).run(true, args, args.length) } function curry(fn, ...args) { return function() { fn(...args, ...arguments) } } function rcurry(fn, ...args) { return function() { fn(...arguments, ...args) } } function hint(args, fn, command="follow") { vimfx.modes.normal.commands[command].run(Object.assign({}, args, { callbackOverride(args) { // I know it's shadowed fn(args) return args.timesLeft > 1 } })) } function execOnLink(args, ...execArgs) { hint(args, ({href}) => exec(...execArgs.map(arg => arg.replace("%u", href)))) } function execOnCurrent({vim}, ...execArgs) { exec(...execArgs.map(arg => arg.replace("%u", vim.browser.currentURI.spec))) } // * Commands function toggleImages(args) { let val = Preferences.get("permissions.default.image") == 1 ? 2 : 1 Preferences.set({"permissions.default.image": val}) args.vim.browser.reload() } // inspired by https://old.reddit.com/r/FirefoxCSS/comments/cia5n2/is_there_a_way_to_refresh_userchromecss_without/ function reloadUserChrome(callback) { callback ||= () => 0 try { const {io, stylesheet} = Services let userChrome = Services.dirsvc.get("UChrm", Ci.nsIFile) userChrome.append("userChrome.css") userChrome = io.newFileURI(FileUtils.File(userChrome.path)) if (stylesheet.sheetRegistered(userChrome, stylesheet.USER_SHEET)) stylesheet.unregisterSheet(userChrome, stylesheet.USER_SHEET) stylesheet.loadAndRegisterSheet(userChrome, stylesheet.USER_SHEET) callback("userChrome.css successfully reloaded") } catch (e) { callback("Error reloading userChrome.css; check the browser console") throw e } } function listTabs({vim}) { vim.window.gTabsPanel.showAllTabsPanel() } function addTabToGroup({vim, count}) { if (!count) return vim.notify("No count specified") let {window: {gBrowser: {tabGroups, selectedTabs}}} = vim return tabGroups[count-1]?.addTabs(selectedTabs) } function removeTabFromGroup({vim}) { let {window: {gBrowser}} = vim for (let tab of gBrowser.selectedTabs) gBrowser.ungroupTab(tab) } function collapseTabGroup({vim, count}) { let {window: {gBrowser: {tabGroups, selectedTab}}} = vim, group = (count ? tabGroups[count-1] : selectedTab.group) if (group) return group.collapsed = !group.collapsed } function addTabGroup({vim}) { let {window: {gBrowser}} = vim, {selectedTab, selectedTabs, tabs} = gBrowser return gBrowser.addTabGroup(selectedTabs, { insertBefore: tabs[Math.max(0, tabs.indexOf(selectedTab)-1)], isUserTriggered: true, }) } function ungroupTabGroup({vim, count}) { let {window: {gBrowser: {tabGroups, selectedTab}}} = vim return (count ? tabGroups[count-1] : selectedTab.group)?.ungroupTabs() } function viewSource({vim}) { let {BrowserCommands, gBrowser} = vim.window BrowserCommands.viewSourceOfDocument({URL: gBrowser.currentURI.spec}) } const sendFn = msg => ({vim}) => vimfx.send(vim, msg, null, m => vim.notify(m)) let commands = { emacsclient: rcurry(execOnLink, "emacsclient", "%u"), emacsclient_media: rcurry(execOnLink, "emacsclient", "--eval", "(empv-play \"%u\")"), emacsclient_eww: rcurry(execOnLink, "emacsclient", "--eval", "(eww \"%u\")"), toggle_images: toggleImages, reload_userchrome: ({vim}) => reloadUserChrome(a => vim.notify(a)), list_tabs: listTabs, browse_git: rcurry(execOnCurrent, "browse-git", "%u"), browse_git_follow: rcurry(execOnLink, "browse-git", "%u"), go_way_back: sendFn("goWayBack"), go_way_forward: sendFn("goWayForward"), search_tabs: ({vim: {window: {gTabsPanel}}}) => gTabsPanel.searchTabs(), add_tab_to_group: addTabToGroup, remove_tab_from_group: removeTabFromGroup, collapse_tab_group: collapseTabGroup, add_tab_group: addTabGroup, ungroup_tab_group: ungroupTabGroup, view_source: viewSource, } // ** Apply Object.entries(commands) .forEach(([name, fn]) => vimfx.addCommand({name, description: name}, fn)) // * Keys // g: navigation // s: elements // x: less-common commands // xt: tabs mappings = { normal: { location: [ ["o", "focus_location_bar"], ["", "focus_search_bar"], ["y ", "paste_and_go"], ["uy ", "paste_and_go_in_tab"], ["w ", "copy_current_url"], ["gu u", "go_up_path"], ["gU U", "go_to_root"], ["gm m", "go_home"], ["gv v", "my/view_source"], ["B", "history_back"], ["F", "history_forward"], ["gh h", "history_list"], ["r", "reload"], ["R", "reload_force"], ["xtr tr", "reload_all"], ["xtR tR", "reload_all_force"], ["xg g", "stop"], ["xts ts", "stop_all"], ["gg", "my/browse_git"], ["gwb", "my/go_way_back"], ["gwf", "my/go_way_forward"], ], scrolling: [ ["b", "scroll_left"], ["f", "scroll_right"], ["n", "scroll_down"], ["p", "scroll_up"], ["", "scroll_page_down"], [" ", "scroll_page_up"], ["} )", "scroll_half_page_down"], ["{ (", "scroll_half_page_up"], [" ", "scroll_to_top"], [" ", "scroll_to_bottom"], ["a ", "scroll_to_left"], ["e ", "scroll_to_right"], ["", "mark_scroll_position"], ["u ", "scroll_to_mark"], ["g", "scroll_to_previous_position"], ["g", "scroll_to_next_position"], ], tabs: [ ["O xtn tN", "tab_new"], ["uO O uxtN tN", "tab_new_after_current"], ["xtn tn", "tab_duplicate"], ["P", "tab_select_previous"], ["N", "tab_select_next"], ["xto to", "tab_select_most_recent"], ["xtl tl", "tab_select_oldest_unvisited"], ["xtM tM", "tab_move_backward"], ["xtm tm", "tab_move_forward"], ["xt4 t5", "tab_move_to_window"], ["xta ta", "tab_select_first"], ["", "tab_select_first_non_pinned"], ["xte te", "tab_select_last"], ["", "tab_toggle_pinned"], ["k xt0 t0 d", "tab_close"], ["/ xtu tu", "tab_restore"], ["u/ uxtu tu", "tab_restore_list"], ["", "tab_close_to_end"], ["", "tab_close_other"], ["t", "my/search_tabs"], ["T", "my/list_tabs"], ["uxtg tg", "my/add_tab_group"], ["uxtG tG", "my/ungroup_tab_group"], ["xtg tg", "my/add_tab_to_group"], ["xtG tG", "my/remove_tab_from_group"], ["xtc tc", "my/collapse_tab_group"], ], browsing: [ ["j ss s", "follow"], ["uj j uss", "follow_multiple"], ["J x4j 4j SS", "follow_in_tab"], ["uJ J ux4j 4j uSS", "follow_in_focused_tab"], ["x5j 5j", "follow_in_window"], ["ux5j 5j us5f 5f", "follow_in_private_window"], ["uw ", "follow_copy"], ["sj j", "follow_focus"], ["sc c", "open_context_menu"], ["sb b", "click_browser_element"], ["[", "follow_previous"], ["]", "follow_next"], ["si i", "focus_text_input"], ["v sv v", "element_text_caret"], ["uv usv v", "element_text_select"], ["sw w", "element_text_copy"], ["se e", "my/emacsclient"], ["use e", "my/emacsclient_eww"], ["sE E", "my/emacsclient_media"], ["sg g", "my/browse_git_follow"], ], find: [ ["", "find"], ["", "find_highlight_all"], ["", "find_links_only"], ["", "find_next"], ["", "find_previous"], ], misc: [ ["x55 55", "window_new"], ["ux55 55", "window_new_private"], ["i", "enter_mode_ignore"], ["I", "quote"], ["gr r", "enter_reader_view"], ["", "edit_blacklist"], ["xc c", "reload_config_file"], ["?", "help"], ["", "esc"], ["xi i", "my/toggle_images"], ["uxc c", "my/reload_user_chrome"], ], }, caret: { "": [ ["b", "move_left"], ["f", "move_right"], ["n", "move_down"], ["p", "move_up"], ["", "move_word_left"], ["", "move_word_right"], ["a ", "move_to_line_start"], ["e ", "move_to_line_end"], [" ", "toggle_selection"], ["o", "toggle_selection_direction"], ["w ", "copy_selection_and_exit"], ["", "exit"], ], }, hints: { "": [ ["", "exit"], [" ", "activate_highlighted"], ["", "rotate_markers_forward"], ["", "rotate_markers_backward"], ["", "delete_char"], ["", "toggle_complementary"], ["", "increase_count"], ], }, ignore: { "": [ ["", "exit"], ["", "unquote"], ], }, find: { "": [ [" ", "exit"], ], }, marks: { "": [ ["", "exit"], ], }, } // ** Apply Object.entries(mappings).forEach(([mode, data]) => { Object.entries(data).forEach(([cat, data]) => { data.forEach(([key, cmd]) => { let pre = "" if (cmd.startsWith("my/")) cmd = cmd.slice(3), pre = "custom." vimfx.set(`${pre}mode.${mode}.${cmd}`, key) }) }) }) // * VimFX settings vimfx.set("scroll.last_find_mark", "") vimfx.set("scroll.last_position_mark", "") // * about:config preferences const prefs = { // ** I know what I'm doing, thanks "browser.aboutConfig.showWarning": false, // ** Basic anti-tracking "privacy.donottrackheader.enabled": true, "privacy.trackingprotection.enabled": true, "privacy.trackingprotection.socialtracking.enabled": true, // ** Basic stuff "browser.uidensity": 1, "browser.search.suggest.enabled": false, "general.smoothScroll": false, "font.size.variable.x-western": 14, "toolkit.legacyUserProfileCustomizations.stylesheets": true, "browser.backspace_action": 1, "browser.download.useDownloadDir": false, "network.dns.disablePrefetch": true, "network.predictor.enabled": false, "network.security.ports.banned.override": "10080", "browser.ctrlTab.sortByRecentlyUsed": true, "browser.download.dir": "/home/simon/tmp/dl", "browser.startup.page": 3, // enables session restore "ui.key.menuAccessKeyFocuses": false, "browser.tabs.insertAfterCurrent": true, // ** Whee! Autoscrolling! "general.autoScroll": true, "middlemouse.paste": false, // ** I like my scrolling the old-fashioned way "general.smoothScroll": false, "general.smoothScroll.lines": false, "general.smoothScroll.other": false, "general.smoothScroll.pages": false, "general.smoothScroll.pixels": false, // ** Devtools "devtools.three-pane-enabled": false, // ** Printing setttings "print.print_headerleft": "", "print.print_headerright": "", "print.print_footerleft": "", "print.print_footerright": "", "print.print_margin_top": 0, "print.print_margin_bottom": 0, "print.print_margin_left": 0, "print.print_margin_right": 0, // ** Keep things in the current tab, please "browser.link.open_newwindow": 1, "browser.link.open_newwindow.restriction": 0, // use above setting "browser.link.open_newwindow.override.external": 3, // external links in new tab // ** Enable AI (for once) in tab groups "browser.tabs.groups.smart.enabled": true, // ** Prefer Japanese to Chinese and Korean "font.cjk_pref_fallback_order": "ja,zh-cn,zh-hk,zh-tw,ko", // ** Let me debug my own browser "devtools.chrome.enabled": true, "devtools.debugger.remote-enabled": true, // ** Make VimFx able to interact with iframes "fission.webContentIsolationStrategy": 0, // ** Fix VimFx sometimes being silently inactive when going back/forward "fission.bfcacheInParent": false, // ** We use PassFF now "signon.rememberSignons": false, "signon.autofillForms": false, "extensions.formautofill.addresses.enabled": false, "extensions.formautofill.creditCards.enabled": false, "signon.formlessCapture.enabled": false, // ** I know some languages other than English, y'know "browser.translations.neverTranslateLanguages": "it,grc,la,jp", // ** No thanks, I don't care "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons": false, "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features": false, "browser.newtabpage.activity-stream.default.sites": "", "browser.newtabpage.activity-stream.feeds.section.highlights": false, "browser.newtabpage.activity-stream.feeds.section.snippets": false, "browser.newtabpage.activity-stream.feeds.section.topsites": false, "browser.newtabpage.activity-stream.feeds.section.topstories": false, "browser.newtabpage.activity-stream.feeds.snippets": false, "browser.newtabpage.activity-stream.feeds.topsites": false, "browser.newtabpage.activity-stream.feeds.weatherfeed": false, "browser.newtabpage.activity-stream.showSponsored": false, "browser.newtabpage.activity-stream.showSponsoredTopSites": false, "browser.newtabpage.activity-stream.showWeather": false, "browser.preferences.moreFromMozilla": false, "browser.shopping.experience2023.active": false, "browser.shopping.experience2023.enabled": false, "browser.shopping.experience2023.optedIn": 0, "browser.topsites.useRemoteSetting": false, "browser.urlbar.suggest.quicksuggest.sponsored": false, "browser.urlbar.suggest.quicksuggest.nonsponsored": false, "extensions.getAddons.showPane": false, "extensions.htmlaboutaddons.recommendations.enabled": false, "identity.fxaccounts.enabled": false, "identity.fxaccounts.toolbar.pxiToolbarEnabled": false, "lightweightThemes.getMoreURL": "", "sidebar.main.tools": "history", "webchannel.allowObject.urlWhitelist": "", // ** Disable telemetry "toolkit.telemetry.unified": false, "toolkit.telemetry.enabled": false, "toolkit.telemetry.server": "data:,", "toolkit.telemetry.archive.enabled": false, "toolkit.telemetry.newProfilePing.enabled": false, "toolkit.telemetry.updatePing.enabled": false, "toolkit.telemetry.firstShutdownPing.enabled": false, "toolkit.telemetry.shutdownPingSender.enabled": false, "toolkit.telemetry.bhrPing.enabled": false, "toolkit.telemetry.cachedClientID": "", "toolkit.telemetry.previousBuildID": "", "toolkit.telemetry.server_owner": "", "toolkit.coverage.opt-out": true, "toolkit.telemetry.coverage.opt-out": true, "toolkit.coverage.enabled": false, "toolkit.coverage.endpoint.base": "", "toolkit.crashreporter.infoURL": "", "datareporting.healthreport.uploadEnabled": false, "datareporting.policy.dataSubmissionEnabled": false, "security.protectionspopup.recordEventTelemetry": false, "browser.ping-centre.telemetry": false, "app.normandy.enabled": false, "app.normandy.api_url": "", "app.shield.optoutstudies.enabled": false, "browser.discovery.enabled": false, "browser.tabs.crashReporting.sendReport": false, "breakpad.reportURL": "", "network.connectivity-service.enabled": false, "network.captive-portal-service.enabled": false, "captivedetect.canonicalURL": "", "dom.private-attribution.submission.enabled": false, "app.update.service.enabled": false, "app.update.background.scheduling.enabled": false, "default-browser-agent.enabled": false, "network.protocol-handler.external.ms-windows-store": false, "toolkit.winRegisterApplicationRestart": false, "app.update.auto": false, // ** Enable "experimental" features "dom.dialog_element.enabled": true, "layout.css.has-selector.enabled": true, } // ** Apply Preferences.set(prefs) // * Addons let addons = { "uBlock Origin": "https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/latest.xpi", "Music Score Downloader": "https://addons.mozilla.org/firefox/downloads/latest/music-score-downloader/latest.xpi", "PassFF": "https://addons.mozilla.org/firefox/downloads/latest/passff/latest.xpi", "Chrome Mask": "https://addons.mozilla.org/firefox/downloads/latest/chrome-mask/latest.xpi", } // ** Apply function installAddonURL(url) { AddonManager.getInstallForURL(url) .then(aI => aI.install()) } function installAddonFile(file) { let nsIFile = Cc['@mozilla.org/file/local;1'].createInstance(Ci.nsIFile) nsIFile.initWithPath(file) AddonManager.getInstallForFile(nsIFile) .then(aI => aI.install()) } function addonInstalledP(name) { return AddonManager.getAllAddons() .then(aa => (aa.map(a => a.name).includes(name))) } for (let [name, url] of Object.entries(addons)) { addonInstalledP(name) .then(installedp => { if (!installedp) installAddonURL(url) }) } // ** TabFS (async function() { if (Services.appinfo.OS != "Linux") return if (await addonInstalledP("TabFS")) return let tabFSDirectory = PathUtils.join(vimFXDirectory, "tabfs") const sh = c => execSync("sh", "-c", c) // compile native messenger sh(`cd ${tabFSDirectory} && make -C fs`) if (!FileUtils.File(PathUtils.join(tabFSDirectory, "fs", "tabfs")).exists()) throw new Error("TabFS compilation failed, investigate manually") // install native messenger sh(`cd ${tabFSDirectory} && ./install.sh firefox`) // install addon sh(`cd ${tabFSDirectory}/extension && zip TabFS.xpi manifest.json background.js captureURL.js -r vendor`) installAddonFile(PathUtils.join(tabFSDirectory, "extension", "TabFS.xpi")) })(); // * UserChrome & UserContent async function ensureUserFile(name) { let chromeDir = PathUtils.join(PathUtils.profileDir, "chrome") let userFileProfile = PathUtils.join(chromeDir, name) let userFileVimFX = PathUtils.join(vimFXDirectory, name) execSync("mkdir", chromeDir) // TODO: Maybe use XPCOM APIs? if (!FileUtils.File(userFileProfile).exists()) exec("ln", "-s", userFileVimFX, userFileProfile) } ensureUserFile("userChrome.css") ensureUserFile("userContent.css") // * Redirects let redirects = { // can't have regex literals in object, so we use new RegExp later "^(.+?)://(|www\\.)reddit.com(/(?!media).*)?$": "$1://old.reddit.com$3", "^(.+?)://(.+\\.)?youtube\\.com(/.*)?$/": "$1://inv.nadeko.net$3", "^(.+?)://(.+\\.)?youtu\\.be(/.*)?$": "$1://inv.nadeko.net$3", "^(.+?)://(twitter|x)\\.com(/.*)?$": "$1://nitter.poast.org$3", "^(.+?)://genius\\.com(/.*)?$": "$1://dm.vern.cc$2", "^(.+?)://(.+\\.)?medium\\.com(/.*)?$": "$1://$2scribe.rip$3", "^(.+?)://imdb.com(/.*)?$": "$1://librembd.lunar.icu$2", "^(.+?)://(bsky.app/.*)$": "$1://skyview.social?url=$2", "^(.+?)://(i\\.)?imgur\\.com(/.*)?$": "$1://rimgo.catsarch.com$3", // "^(.+?)://(.+)\\.fandom.com(/.*)?$": "$1://breezewiki.com/$2$3", } // ** Implementation function redirect(url) { for (let [pat, rep] of Object.entries(redirects)) { pat = new RegExp(pat) if (pat.test(url)) return url.replace(pat, rep) } } vimfx.on("locationChange", ({vim, location: oldURL}) => { let newURL = redirect(oldURL.toString()) if (newURL) vimfx.send(vim, "location.replace", newURL) }) // * Search engines let searchEngines = { "Anna's Archive": {key: "aa", url: "https://annas-archive.org/search?q=%s"}, "Arch Wiki": {key: "aw", url: "https://wiki.archlinux.org/index.php?search=%s"}, "Brave Search": {key: "b", url: "https://search.brave.com/search?q=%s"}, "DDG": {key: "d", url: "https://duckduckgo.com/?q=%s"}, "Debian Bugs": {key: "dbg", url: "https://bugs.debian.org/%s"}, "Debian Packages": {key: "p", url: "https://packages.debian.org/%s"}, "Debian Wiki": {key: "dw", url: "https://wiki.debian.org/FrontPage?action=fullsearch&titlesearch=0&value=%s"}, "Gentoo Packages": {key: "gp", url: "https://packages.gentoo.org/packages/search?q=%s"}, "Gentoo Portage Overlays": {key: "gpo", url: "https://gpo.zugaina.org/Search?search=%s"}, "Gentoo Wiki": {key: "gw", url: "https://wiki.gentoo.org/index.php?title=Special:Search&search=%s"}, "IMSLP": {key: "imslp", url: "https://search.brave.com/search?q=%s+site%3Aimslp.org"}, "Internet Archive": {key: "arch", url: "https://archive.org/search.php?query=%s"}, "Jisho": {key: "ji", url: "https://jisho.org/search/%s"}, "Kbin": {key: "m", url: "https://fedia.io/m/%s"}, "MDN": {key: "mdn", url: "https://developer.mozilla.org/en-US/search?q=%s"}, "MusicBrainz": {key: "mbz", url: "https://musicbrainz.org/search?type=release&method=indexed&query=%s"}, "Marginalia": {key: "mg", url: "https://search.marginalia.nu/search?query=%s"}, "Nix Packages": {key: "np", url: "https://search.nixos.org/packages?query=%s"}, "Wiby": {key: "wb", url: "https://wiby.me/?q=%s"}, "Wikipedia": {key: "w", url: "https://wikipedia.org/w/index.php?search=%s"}, "Wiktionary (Japanese)": {key: "wkj", url: "https://wiktionary.org/w/index.php?search=%s#Japanese"}, "Wiktionary (Latin)": {key: "wkl", url: "https://wiktionary.org/w/index.php?search=%s#Latin"}, "Wiktionary": {key: "wk", url: "https://wiktionary.org/w/index.php?search=%s"}, "Wolfram|Alpha": {key: "wa", url: "https://www.wolframalpha.com/input?i=%s"}, "X Files": {key: "x", url: "http://zoar.cx/~andrea/x/%s"}, } let defaultEngine = "Brave Search" // ** Set const getEngine = name => Services.search.getEngineByName(name) const removeEngine = name => Services.search.removeEngine(getEngine(name)) const addEngine = async (name, {key, url}) => Services.search.addUserEngine({name, url: url.replace("%s", "{searchTerms}"), alias: key}) async function resetEngine(name, opts) { if (getEngine(name)) await removeEngine(name) return addEngine(name, opts) } (async function() { for (let [name, opts] of Object.entries(searchEngines)) getEngine(name) || await addEngine(name, opts) let oldDef = Services.search.defaultEngine Services.search.defaultEngine = getEngine(defaultEngine) if (!searchEngines[oldDef.name]) Services.search.removeEngine(oldDef) for (let s of await Services.search.getEngines()) if (searchEngines[s._name]) await resetEngine(s._name, searchEngines[s._name]) else await removeEngine(s._name) })()