1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package web
4
5// Keys is the keyboard map. ermite.js turns keys into k-<name> events on
6// <body>; this element (hx-on + hx-live helpers) is the only place that
7// reacts to them.
8templ Keys() {
9 <div
10 id="keys"
11 hidden
12 hx-on="k-j from:body -> (async (d) => {
13 if (q('dialog[open]').count) return;
14 let rows = q('[data-rows] > :not([hidden])').arr();
15 if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180));
16 let i = rows.findIndex(r => r.hasAttribute('data-cursor'));
17 let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j];
18 q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' });
19 let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark;
20 await timeout('1500ms'); if (b.nav === mark) b.nav = ''
21 })(1);
22 k-k from:body -> (async (d) => {
23 if (q('dialog[open]').count) return;
24 let rows = q('[data-rows] > :not([hidden])').arr();
25 if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180));
26 let i = rows.findIndex(r => r.hasAttribute('data-cursor'));
27 let j = Math.max(0, Math.min(rows.length - 1, i + d)), n = rows[j];
28 q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' });
29 let b = document.body.dataset, mark = (j + 1) + '/' + rows.length; b.nav = mark;
30 await timeout('1500ms'); if (b.nav === mark) b.nav = ''
31 })(-1);
32 k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0));
33 k-bottom from:body -> q('[data-rows] > :not([hidden])').arr().at(-1)?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 1e9), q('#main').arr()[0].scrollTo(0, 1e9));
34 k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click();
35 k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close());
36 k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].showModal();
37 k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].showModal(), q('#refs-filter').arr()[0].focus());
38 k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].showModal(); q('#cmd-q').arr()[0].focus();
39 k-filter from:body -> q('[data-filter]').arr()[0]?.focus();
40 k-gh from:body -> document.querySelector('.brand')?.click();
41 k-gt from:body -> document.querySelector('.tabs [data-key=t]')?.click();
42 k-gc from:body -> document.querySelector('.tabs [data-key=c]')?.click();
43 k-gr from:body -> document.querySelector('.tabs [data-key=r]')?.click();
44 k-s from:body -> document.querySelector('.tabs [data-key=s]')?.click();
45 k-t from:body -> document.querySelector('.tabs [data-key=t]')?.click();
46 k-c from:body -> document.querySelector('.tabs [data-key=c]')?.click();
47 k-r from:body -> document.querySelector('.tabs [data-key=r]')?.click();
48 k-m from:body -> document.querySelector('.tabs [data-key=m]')?.click();
49 k-gm from:body -> document.querySelector('.tabs [data-key=m]')?.click();
50 k-next from:body -> {
51 let ns = q('#diffs .cmt').arr(); if (!ns.length) return;
52 let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3);
53 let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' })
54 };
55 k-split from:body -> {
56 let el = q('[data-diff]').arr()[0]; if (!el) return;
57 el.dataset.diff = 'split'; sessionStorage.setItem('diff', 'split');
58 q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true)
59 };
60 k-unified from:body -> {
61 let el = q('[data-diff]').arr()[0]; if (!el) return;
62 el.dataset.diff = 'unified'; sessionStorage.setItem('diff', 'unified');
63 q('[data-file]').arr().filter(d => d.querySelector('.dt')).forEach(d => d.open = true)
64 };
65 k-collapse from:body -> { let fs = q('[data-file]').arr(), open = !fs.some(d => d.open); fs.forEach(d => d.open = open) };
66 k-permalink from:body -> {
67 let b = document.body.dataset;
68 if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return }
69 history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash);
70 navigator.clipboard?.writeText(location.href);
71 b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = ''
72 }"
73 ></div>
74}