aboutsummaryrefslogtreecommitdiff
path: root/modules/firefox.home/chrome/globalKeybindings.uc.js
blob: dd1f0f7111539caf2835e2d04af3ac5a2f094572 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
// ==UserScript==
// @name                 Global Keybindings
// @version              1.0
// @description          Setup global keybindings on all windows
// @startup              UC.globalKeybindings.init(win);
// @shutdown             UC.globalKeybindings.destroy();
// ==/UserScript==


(() => {
  // Configure global keybindings
  const KEYBINDINGS = () => ({
    RELEASE: [
      [ctrl(shift(key('J'))), moveSelectedTabBy(+1)],
      [ctrl(shift(key('K'))), moveSelectedTabBy(-1)],
      [ctrl(alt(key('p'))), togglePassthrough(), { modes: ALL_MODES }],
    ],
    PRESS: [
      // Block default key bindings
      [ctrl(shift(key('J'))), preventDefault()],
      [ctrl(shift(key('K'))), preventDefault()],

      [ctrl(key('j')), preventDefault(nextTab())],
      [ctrl(key('k')), preventDefault(prevTab())],

      // Ctrl + number takes to the tab at position
      ...(Array.from({ length: 10 }, (_, idx) =>
        [ctrl(key(idx === 9 ? 0 : idx + 1)), tabIndex(idx)],
      )),
    ],
  });

  const nextTab = () => () => updateTabIndex((n, len) => (n + 1) % len);
  const prevTab = () => () => updateTabIndex((n, len) => n === 0 ? len - 1 : n - 1);
  const tabIndex = idx => () => updateTabIndex((_n, _len) => idx)
  const preventDefault = f => e => { e.preventDefault(); f?.(e); };
  const moveSelectedTabBy = incr => () => updateSelectedTabIndex(incr);
  const togglePassthrough = () => () => UC.globalKeybindings.updateMode(m => m !== MODE_PASSTHRU ? MODE_PASSTHRU : MODE_NORMAL)

  const MODE_PASSTHRU = 'passthru';
  const MODE_NORMAL = '';
  const ALL_MODES = [MODE_NORMAL, MODE_PASSTHRU];

  const ctrl = b => e => b(e) && e.ctrlKey;
  const alt = b => e => b(e) && e.altKey;
  const shift = b => e => b(e) && e.shiftKey;
  const key = key => e => `${e.key}` === `${key}`;

  const updateTabIndex = (f) => {
    const newIndex = f(gBrowser.tabContainer.selectedIndex, gBrowser.tabs.length)
    if (typeof newIndex !== 'number') return;
    if (newIndex >= gBrowser.tabContainer.allTabs.length) return;
    if (newIndex < 0) return;
    gBrowser.selectTabAtIndex(newIndex);
  };

  const updateSelectedTabIndex = (incr) => {
    if (typeof incr !== 'number') return;
    if (incr === +1) return gBrowser.moveTabForward();
    if (incr === -1) return gBrowser.moveTabBackward();
    const newIndex = gBrowser.tabContainer.selectedIndex + incr;
    if (newIndex >= gBrowser.tabContainer.allTabs.length) return;
    if (newIndex < 0) return;
    gBrowser.moveTabTo(gBrowser.selectedTab, newIndex)
  };

  const module = {
    mode: MODE_NORMAL,
    allModes: ALL_MODES,

    keybindings: KEYBINDINGS(),

    evaluateKeybindings: (bindings, e) => {
      for (const [isKey, action, options] of bindings) {
        // Only allow keys from the given mode
        if ((options?.modes ?? [MODE_NORMAL]).includes(module.mode)) {
          if (isKey(e)) {
            const shouldContinue = action(e);
            if (!shouldContinue) break;
          }
        }
      }
    },

    updateMode: f => {
      module.mode = f(module.mode);
      gBrowser.tabContainer.dataset.keyMode = module.mode
    },

    handleKeyUpEvent: e => module.evaluateKeybindings(module.keybindings.RELEASE, e),
    handleKeyDownEvent: e => module.evaluateKeybindings(module.keybindings.PRESS, e),

    init(win) {
      let observe = () => {
        Services.obs.removeObserver(observe, 'browser-window-before-show');
        win.addEventListener('keyup', module.handleKeyUpEvent, true);
        win.addEventListener('keydown', module.handleKeyDownEvent, true);
      }

      if (win.__SSi) {
        win.addEventListener('keyup', module.handleKeyUpEvent, true);
        win.addEventListener('keydown', module.handleKeyDownEvent, true);
      } else {
        Services.obs.addObserver(observe, 'browser-window-before-show');
      }
    },
    destroy() {
      _uc.windows((_doc, win) => {
        win.removeEventListener('keyup', module.handleKeyUpEvent, true);
        win.removeEventListener('keydown', module.handleKeyDownEvent, true);
      });
      delete UC.globalKeybindings;
    },
  };

  UC.globalKeybindings = module;
})();