Hacker News new | ask | show | jobs
by devit 589 days ago
I like the way it works, and I'm experimenting with customizing Firefox to behave like browser.horse.

Currently I did this, which seems to completely mimic what they show in the video:

1. Install Tree Style Tabs. This will give you hierarchical tabs in the sidebar

2. Install Simple Tab Groups. This will let you create multiple separate "tab groups" (aka workspaces) with different sets of tabs.

3. Go to Settings, turn on "Open previous tabs and windows" in General/Startup. This will make Firefox reload your tabs on startup

4. In about:config set browser.tabs.unloadOnLowMemory to true. This will make the browser auto-unload unused tabs so you can have unlimited tabs without running out of memory.

5. In about:config set browser.search.openintab to true. This will make the search bar open search results in a new tab

6. In Tree Style Tabs config set "Promote All Children to parent level always". This will make closing intermediate tabs in the tree work properly (remove the intermediate and reparent all children to the intermediate parent).

7. Install Tampermonkey and add the userscript at the bottom. This will remap click to open a new foreground tab (i.e. original ctrl+shift+click), shift+click to navigate in the current page (i.e. original click) and ctrl+shift+click or shift+middle click to open in a new window (i.e. original shift+click).

Note that the extensions require privileges to access data on all sites, so make sure you trust them or do this on a separate profile or VM.

Compared to browser.horse, this is free and customizable, but might be less optimized, perhaps less featureful and won't automatically get any new feature the browser.horse developers invent.

    // ==UserScript==
    // @name         Click opens in new foreground tab
    // @namespace    http://tampermonkey.net/
    // @version      2024-11-12
    // @description  Click opens in new foreground tab
    // @author       You
    // @match        *://*/\*
    // @grant        none
    // ==/UserScript==

    (function() {
    'use strict';

    function generateRandomId(length) {
        const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
        let result = '';
        for (let i = 0; i < length; i++) {
            result += characters.charAt(Math.floor(Math.random() * characters.length));
        }
        return result;
    }

    const eventProp = "customClick_" + generateRandomId(32);

    function customClickHandler(event) {
        if (event[eventProp]) {
            //console.log("customClick: recursion " + event);
            return;
        } else {
            //console.log("customClick: first " + event);
        }
        const ctrl = event.button === 1 || (event.ctrlKey && event.button === 0);
        const shift = event.shiftKey;

        let newCtrl = undefined;
        let newShift = undefined;

        //console.log("customClick: detected with " + ctrl + " " + shift);
        if (shift && !ctrl) {
            newCtrl = false;
            newShift = false;
        } else if (shift && ctrl) {
            newCtrl = false;
            newShift = true;
        } else if (!shift && !ctrl) {
            newCtrl = true;
            newShift = true;
        } else { // !shift && ctrl
            return;
        }
        //console.log("customClick: dispatching " + newCtrl + " " + newShift);
        const options = {};
        let source = event;
        while(source) {
            for (const prop of Object.getOwnPropertyNames(source)) {
                options[prop] = event[prop];
            }
            source = Object.getPrototypeOf(source);
        }
        options.button = 0;
        options.ctrlKey = newCtrl;
        options.shiftKey = newShift;

        let newEvent = new PointerEvent('click', options);
        newEvent[eventProp] = true;

        event.preventDefault();
        event.stopPropagation();
        event.target.dispatchEvent(newEvent);
    }
    document.addEventListener('click', customClickHandler, true);
    document.addEventListener('auxclick', customClickHandler, true);
    })();