v0.3.0 9fdf386

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 -> ((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 n = rows[Math.max(0, Math.min(rows.length - 1, i + d))];
18           q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' })
19         })(1);
20         k-k from:body -> ((d) => {
21           if (q('dialog[open]').count) return;
22           let rows = q('[data-rows] > :not([hidden])').arr();
23           if (!rows.length) return (scrollBy(0, d * 180), q('#main').arr()[0].scrollBy(0, d * 180));
24           let i = rows.findIndex(r => r.hasAttribute('data-cursor'));
25           let n = rows[Math.max(0, Math.min(rows.length - 1, i + d))];
26           q(n).take('data-cursor'); n.scrollIntoView({ block: 'nearest' })
27         })(-1);
28         k-top from:body -> q('[data-rows] > :not([hidden])').arr()[0]?.scrollIntoView({ block: 'nearest' }) ?? (scrollTo(0, 0), q('#main').arr()[0].scrollTo(0, 0));
29         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));
30         k-open from:body -> q('dialog[open]').count || document.querySelector('[data-cursor] a')?.click();
31         k-esc from:body -> q('dialog[open]').arr().forEach(d => d.close());
32         k-help from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-help').arr()[0].show();
33         k-refs from:body -> q('#panel-refs').count && (q('dialog[open]').arr().forEach(d => d.close()), q('#panel-refs').arr()[0].show(), q('#refs-filter').arr()[0].focus());
34         k-cmd from:body -> q('dialog[open]').arr().forEach(d => d.close()); q('#panel-cmd').arr()[0].show(); q('#cmd-q').arr()[0].focus();
35         k-filter from:body -> q('[data-filter]').arr()[0]?.focus();
36         k-gh from:body -> document.querySelector('.top a[data-g=h]')?.click();
37         k-gt from:body -> document.querySelector('.secs [data-key=t]')?.click();
38         k-gc from:body -> document.querySelector('.secs [data-key=c]')?.click();
39         k-gr from:body -> document.querySelector('.secs [data-key=r]')?.click();
40         k-s from:body -> document.querySelector('.secs [data-key=s]')?.click();
41         k-t from:body -> document.querySelector('.secs [data-key=t]')?.click();
42         k-c from:body -> document.querySelector('.secs [data-key=c]')?.click();
43         k-r from:body -> document.querySelector('.secs [data-key=r]')?.click();
44         k-m from:body -> document.querySelector('.secs [data-key=m]')?.click();
45         k-gm from:body -> document.querySelector('.secs [data-key=m]')?.click();
46         k-next from:body -> {
47           let ns = q('#diffs .cmt').arr(); if (!ns.length) return;
48           let i = ns.findIndex(n => n.getBoundingClientRect().top > innerHeight / 3);
49           let n = ns[i < 0 ? 0 : i]; n.closest('details') && (n.closest('details').open = true); n.scrollIntoView({ block: 'center' })
50         };
51         k-split from:body -> q('[data-diff]').attr['data-diff'] = 'split'; sessionStorage.setItem('diff', 'split');
52         k-unified from:body -> q('[data-diff]').attr['data-diff'] = 'unified'; sessionStorage.setItem('diff', 'unified');
53         k-collapse from:body -> q('[data-file]').arr().forEach(d => d.open = false);
54         k-permalink from:body -> {
55           let b = document.body.dataset;
56           if (!b.sha) { b.flash = 'no ref to pin on this page'; await timeout('2s'); b.flash = ''; return }
57           history.replaceState(null, '', location.pathname.replace('/' + b.ref + '/', '/' + b.sha + '/') + location.hash);
58           navigator.clipboard?.writeText(location.href);
59           b.flash = 'permalink ' + b.sha.slice(0, 7) + ' copied'; await timeout('2s'); b.flash = ''
60         }"
61	></div>
62}

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