v0.6.0 d5cc8fe

keys.templ RawHistoryFolder

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(); ermiteFinder.load();
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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 45

↑↓ moveenter openesc close