detached 4e3b6b3

fix(web): drop sequence aria-keyshortcuts and use fake timers in keyboard tests

commit
4e3b6b376a37926f4cf831499d2d1eff24b7b9ab
author
brice · · 2026-10-22 07:58 UTC
parent
a1f6bdf
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +87 -97
M internal/adapters/web/jstest/keyboard.test.mjs +38 -4
@@ -4,17 +4,25 @@ import { readFileSync } from 'node:fs';
44 import { test } from 'node:test';
55 import { runInNewContext } from 'node:vm';
66 const source = readFileSync(new URL('../static/js/ermite.js', import.meta.url), 'utf8');
7-function keyboard({ off = false, typing = false, dialog = false } = {}) {
7+function keyboard({ owner = true, off = false, typing = false, dialog = false } = {}) {
88 let handler;
99 const fired = [];
10+ const timers = new Map();
11+ let timerID = 0;
1012 runInNewContext(source.slice(source.indexOf(' const single ='), source.indexOf(' // 2. A [data-fire]')), {
1113 addEventListener: (_, fn) => { handler = fn; },
1214 window: { ermiteKeys: { blocks: () => off } },
1315 ermite: { fire: name => fired.push(name) },
14- $: selector => selector === '[data-inbox]' || (dialog && selector === 'dialog[open]'),
15- closeMenus: () => false, clearTimeout() {}, setTimeout() {},
16+ $: selector => (owner && selector === '[data-inbox]') || (dialog && selector === 'dialog[open]'),
17+ closeMenus: () => false,
18+ clearTimeout: id => timers.delete(id),
19+ setTimeout: fn => { timers.set(++timerID, fn); return timerID; },
1620 });
17- return { fired, press(key, shiftKey = false) {
21+ return { fired, expireTimers() {
22+ const callbacks = [...timers.values()];
23+ timers.clear();
24+ for (const callback of callbacks) callback();
25+ }, press(key, shiftKey = false) {
1826 handler({ key, shiftKey, target: { closest: selector => typing && selector.startsWith('input'), blur() {} }, preventDefault() {} });
1927 } };
2028 }
@@ -40,3 +48,29 @@ test('uppercase actions respect preference, fields and dialogs', () => {
4048 assert.deepEqual(k.fired, []);
4149 }
4250 });
51+
52+test('Shift+I opens inbox while issues need the g i sequence', () => {
53+ const k = keyboard();
54+ k.press('I', true); k.press('i');
55+ assert.deepEqual(k.fired, ['inbox']);
56+ k.press('g'); k.press('i');
57+ assert.deepEqual(k.fired, ['inbox', 'i']);
58+});
59+test('inbox shortcut respects preference, fields, dialogs, and owner target', () => {
60+ for (const options of [{ off: true }, { typing: true }, { dialog: true }, { owner: false }]) {
61+ const k = keyboard(options); k.press('I', true);
62+ assert.deepEqual(k.fired, [], JSON.stringify(options));
63+ }
64+});
65+test('shifted punctuation still opens help and the command palette', () => {
66+ const k = keyboard();
67+ k.press('?', true); k.press(':', true);
68+ assert.deepEqual(k.fired, ['help', 'cmd']);
69+});
70+test('g prefix expires without scheduling real timers', () => {
71+ const k = keyboard();
72+ k.press('g'); k.expireTimers(); k.press('s');
73+ assert.deepEqual(k.fired, []);
74+ k.press('g'); k.press('s');
75+ assert.deepEqual(k.fired, ['s']);
76+});
@@ -4,17 +4,25 @@ import { readFileSync } from 'node:fs';
4 import { test } from 'node:test';4 import { test } from 'node:test';
5 import { runInNewContext } from 'node:vm';5 import { runInNewContext } from 'node:vm';
6 const source = readFileSync(new URL('../static/js/ermite.js', import.meta.url), 'utf8');6 const source = readFileSync(new URL('../static/js/ermite.js', import.meta.url), 'utf8');
7-function keyboard({ off = false, typing = false, dialog = false } = {}) {7+function keyboard({ owner = true, off = false, typing = false, dialog = false } = {}) {
8 let handler;8 let handler;
9 const fired = [];9 const fired = [];
10+ const timers = new Map();
11+ let timerID = 0;
10 runInNewContext(source.slice(source.indexOf(' const single ='), source.indexOf(' // 2. A [data-fire]')), {12 runInNewContext(source.slice(source.indexOf(' const single ='), source.indexOf(' // 2. A [data-fire]')), {
11 addEventListener: (_, fn) => { handler = fn; },13 addEventListener: (_, fn) => { handler = fn; },
12 window: { ermiteKeys: { blocks: () => off } },14 window: { ermiteKeys: { blocks: () => off } },
13 ermite: { fire: name => fired.push(name) },15 ermite: { fire: name => fired.push(name) },
14- $: selector => selector === '[data-inbox]' || (dialog && selector === 'dialog[open]'),16+ $: selector => (owner && selector === '[data-inbox]') || (dialog && selector === 'dialog[open]'),
15- closeMenus: () => false, clearTimeout() {}, setTimeout() {},17+ closeMenus: () => false,
18+ clearTimeout: id => timers.delete(id),
19+ setTimeout: fn => { timers.set(++timerID, fn); return timerID; },
16 });20 });
17- return { fired, press(key, shiftKey = false) {21+ return { fired, expireTimers() {
22+ const callbacks = [...timers.values()];
23+ timers.clear();
24+ for (const callback of callbacks) callback();
25+ }, press(key, shiftKey = false) {
18 handler({ key, shiftKey, target: { closest: selector => typing && selector.startsWith('input'), blur() {} }, preventDefault() {} });26 handler({ key, shiftKey, target: { closest: selector => typing && selector.startsWith('input'), blur() {} }, preventDefault() {} });
19 } };27 } };
20 }28 }
@@ -40,3 +48,29 @@ test('uppercase actions respect preference, fields and dialogs', () => {
40 assert.deepEqual(k.fired, []);48 assert.deepEqual(k.fired, []);
41 }49 }
42 });50 });
51+
52+test('Shift+I opens inbox while issues need the g i sequence', () => {
53+ const k = keyboard();
54+ k.press('I', true); k.press('i');
55+ assert.deepEqual(k.fired, ['inbox']);
56+ k.press('g'); k.press('i');
57+ assert.deepEqual(k.fired, ['inbox', 'i']);
58+});
59+test('inbox shortcut respects preference, fields, dialogs, and owner target', () => {
60+ for (const options of [{ off: true }, { typing: true }, { dialog: true }, { owner: false }]) {
61+ const k = keyboard(options); k.press('I', true);
62+ assert.deepEqual(k.fired, [], JSON.stringify(options));
63+ }
64+});
65+test('shifted punctuation still opens help and the command palette', () => {
66+ const k = keyboard();
67+ k.press('?', true); k.press(':', true);
68+ assert.deepEqual(k.fired, ['help', 'cmd']);
69+});
70+test('g prefix expires without scheduling real timers', () => {
71+ const k = keyboard();
72+ k.press('g'); k.expireTimers(); k.press('s');
73+ assert.deepEqual(k.fired, []);
74+ k.press('g'); k.press('s');
75+ assert.deepEqual(k.fired, ['s']);
76+});
D internal/adapters/web/jstest/keys.test.mjs +0 -31
@@ -1,31 +0,0 @@
1-// Exercise the production keydown handler without loading the rest of the page.
2-import assert from 'node:assert/strict';
3-import { readFileSync } from 'node:fs';
4-import { test } from 'node:test';
5-const src = readFileSync(new URL('../static/js/ermite.js', import.meta.url), 'utf8');
6-const block = src.slice(src.indexOf(' // 1. Keys.'), src.indexOf(' // 2. A [data-fire]'));
7-
8-function page({ owner = true, off = false, typing = false, dialog = false } = {}) {
9- let handler;
10- const fired = [];
11- const window = { ermiteKeys: { blocks: () => off } };
12- const $ = (s) => s === 'dialog[open]' ? dialog : s === '[data-inbox]' ? owner : null;
13- new Function('window', '$', 'ermite', 'addEventListener', block)(window, $, { fire: (n) => fired.push(n) }, (_, f) => { handler = f; });
14- return (key) => {
15- const e = { key, shiftKey: key === 'I', preventDefault() {}, target: { closest: (s) => s.startsWith('input') && typing } };
16- handler(e);
17- return fired;
18- };
19-}
20-test('Shift+I opens inbox while issues need the g i sequence', () => {
21- const press = page();
22- assert.deepEqual(press('I'), ['inbox']);
23- assert.deepEqual(press('i'), ['inbox']);
24- press('g');
25- assert.deepEqual(press('i'), ['inbox', 'i']);
26-});
27-test('inbox shortcut respects preference, fields, dialogs, and owner target', () => {
28- for (const options of [{ off: true }, { typing: true }, { dialog: true }, { owner: false }]) {
29- assert.deepEqual(page(options)('I'), [], JSON.stringify(options));
30- }
31-});
@@ -1,31 +0,0 @@
1-// Exercise the production keydown handler without loading the rest of the page.
2-import assert from 'node:assert/strict';
3-import { readFileSync } from 'node:fs';
4-import { test } from 'node:test';
5-const src = readFileSync(new URL('../static/js/ermite.js', import.meta.url), 'utf8');
6-const block = src.slice(src.indexOf(' // 1. Keys.'), src.indexOf(' // 2. A [data-fire]'));
7-
8-function page({ owner = true, off = false, typing = false, dialog = false } = {}) {
9- let handler;
10- const fired = [];
11- const window = { ermiteKeys: { blocks: () => off } };
12- const $ = (s) => s === 'dialog[open]' ? dialog : s === '[data-inbox]' ? owner : null;
13- new Function('window', '$', 'ermite', 'addEventListener', block)(window, $, { fire: (n) => fired.push(n) }, (_, f) => { handler = f; });
14- return (key) => {
15- const e = { key, shiftKey: key === 'I', preventDefault() {}, target: { closest: (s) => s.startsWith('input') && typing } };
16- handler(e);
17- return fired;
18- };
19-}
20-test('Shift+I opens inbox while issues need the g i sequence', () => {
21- const press = page();
22- assert.deepEqual(press('I'), ['inbox']);
23- assert.deepEqual(press('i'), ['inbox']);
24- press('g');
25- assert.deepEqual(press('i'), ['inbox', 'i']);
26-});
27-test('inbox shortcut respects preference, fields, dialogs, and owner target', () => {
28- for (const options of [{ off: true }, { typing: true }, { dialog: true }, { owner: false }]) {
29- assert.deepEqual(page(options)('I'), [], JSON.stringify(options));
30- }
31-});
M internal/adapters/web/keys_test.go +1 -1
@@ -17,7 +17,7 @@ func TestSectionShortcutSequences(t *testing.T) {
1717 bindings := renderToString(t, Keys())
1818 for _, section := range sections {
1919 assert.Contains(t, header, `data-key="g `+section.Key+`"`)
20- assert.Contains(t, header, `aria-keyshortcuts="g `+section.Key+`"`)
20+ assert.NotContains(t, header, `aria-keyshortcuts="g `+section.Key+`"`)
2121 assert.Contains(t, header, `<kbd aria-hidden="true">g `+section.Key+`</kbd>`)
2222 assert.Contains(t, bindings, "ermite.tab('"+section.Key+"')")
2323 }
@@ -17,7 +17,7 @@ func TestSectionShortcutSequences(t *testing.T) {
17 bindings := renderToString(t, Keys())17 bindings := renderToString(t, Keys())
18 for _, section := range sections {18 for _, section := range sections {
19 assert.Contains(t, header, `data-key="g `+section.Key+`"`)19 assert.Contains(t, header, `data-key="g `+section.Key+`"`)
20- assert.Contains(t, header, `aria-keyshortcuts="g `+section.Key+`"`)20+ assert.NotContains(t, header, `aria-keyshortcuts="g `+section.Key+`"`)
21 assert.Contains(t, header, `<kbd aria-hidden="true">g `+section.Key+`</kbd>`)21 assert.Contains(t, header, `<kbd aria-hidden="true">g `+section.Key+`</kbd>`)
22 assert.Contains(t, bindings, "ermite.tab('"+section.Key+"')")22 assert.Contains(t, bindings, "ermite.tab('"+section.Key+"')")
23 }23 }
M internal/adapters/web/releases_test.go +2 -1
@@ -120,7 +120,8 @@ func TestReleasePages(t *testing.T) {
120120 assert.Contains(t, body, "2026-10-12")
121121 assert.Contains(t, body, `<td>1</td>`)
122122 assert.Contains(t, body, `?page=next%2B%2F%3D`)
123- assert.Contains(t, body, `aria-keyshortcuts="g r"`)
123+ assert.Contains(t, body, `data-key="g r"`)
124+ assert.NotContains(t, body, `aria-keyshortcuts="g r"`)
124125 assert.Contains(t, body, "view=releases")
125126 }
126127 owner := f.request("GET", "/proj/releases?page=cursor%2B%2F%3D", "tok", "")
@@ -120,7 +120,8 @@ func TestReleasePages(t *testing.T) {
120 assert.Contains(t, body, "2026-10-12")120 assert.Contains(t, body, "2026-10-12")
121 assert.Contains(t, body, `<td>1</td>`)121 assert.Contains(t, body, `<td>1</td>`)
122 assert.Contains(t, body, `?page=next%2B%2F%3D`)122 assert.Contains(t, body, `?page=next%2B%2F%3D`)
123- assert.Contains(t, body, `aria-keyshortcuts="g r"`)123+ assert.Contains(t, body, `data-key="g r"`)
124+ assert.NotContains(t, body, `aria-keyshortcuts="g r"`)
124 assert.Contains(t, body, "view=releases")125 assert.Contains(t, body, "view=releases")
125 }126 }
126 owner := f.request("GET", "/proj/releases?page=cursor%2B%2F%3D", "tok", "")127 owner := f.request("GET", "/proj/releases?page=cursor%2B%2F%3D", "tok", "")
M internal/adapters/web/shell.templ +0 -1
@@ -173,7 +173,6 @@ templ Header(l Layout) {
173173 <a
174174 href={ templ.SafeURL(s.Href) }
175175 data-key={ "g " + s.Key }
176- aria-keyshortcuts={ "g " + s.Key }
177176 if s.Current {
178177 aria-current="page"
179178 }
@@ -173,7 +173,6 @@ templ Header(l Layout) {
173 <a173 <a
174 href={ templ.SafeURL(s.Href) }174 href={ templ.SafeURL(s.Href) }
175 data-key={ "g " + s.Key }175 data-key={ "g " + s.Key }
176- aria-keyshortcuts={ "g " + s.Key }
177 if s.Current {176 if s.Current {
178 aria-current="page"177 aria-current="page"
179 }178 }
M internal/adapters/web/shell_templ.go +46 -59
@@ -751,102 +751,89 @@ func Header(l Layout) templ.Component {
751751 if templ_7745c5c3_Err != nil {
752752 return templ_7745c5c3_Err
753753 }
754- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "\" aria-keyshortcuts=\"")
755- if templ_7745c5c3_Err != nil {
756- return templ_7745c5c3_Err
757- }
758- var templ_7745c5c3_Var39 string
759- templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.ResolveAttributeValue("g " + s.Key)
760- if templ_7745c5c3_Err != nil {
761- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 176, Col: 40}
762- }
763- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var39)
764- if templ_7745c5c3_Err != nil {
765- return templ_7745c5c3_Err
766- }
767- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "\"")
754+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "\"")
768755 if templ_7745c5c3_Err != nil {
769756 return templ_7745c5c3_Err
770757 }
771758 if s.Current {
772- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, " aria-current=\"page\"")
759+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, " aria-current=\"page\"")
773760 if templ_7745c5c3_Err != nil {
774761 return templ_7745c5c3_Err
775762 }
776763 }
777- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, ">")
764+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, ">")
778765 if templ_7745c5c3_Err != nil {
779766 return templ_7745c5c3_Err
780767 }
781- var templ_7745c5c3_Var40 string
782- templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label)
768+ var templ_7745c5c3_Var39 string
769+ templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label)
783770 if templ_7745c5c3_Err != nil {
784- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 181, Col: 17}
771+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 180, Col: 17}
785772 }
786- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
773+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
787774 if templ_7745c5c3_Err != nil {
788775 return templ_7745c5c3_Err
789776 }
790- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, " ")
777+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, " ")
791778 if templ_7745c5c3_Err != nil {
792779 return templ_7745c5c3_Err
793780 }
794781 if s.Count != "" {
795- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "<span class=\"count\">")
782+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "<span class=\"count\">")
796783 if templ_7745c5c3_Err != nil {
797784 return templ_7745c5c3_Err
798785 }
799- var templ_7745c5c3_Var41 string
800- templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(s.Count)
786+ var templ_7745c5c3_Var40 string
787+ templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(s.Count)
801788 if templ_7745c5c3_Err != nil {
802- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 183, Col: 38}
789+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 182, Col: 38}
803790 }
804- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
791+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
805792 if templ_7745c5c3_Err != nil {
806793 return templ_7745c5c3_Err
807794 }
808- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "<span class=\"sr\">")
795+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "<span class=\"sr\">")
809796 if templ_7745c5c3_Err != nil {
810797 return templ_7745c5c3_Err
811798 }
812- var templ_7745c5c3_Var42 string
813- templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(" open")
799+ var templ_7745c5c3_Var41 string
800+ templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(" open")
814801 if templ_7745c5c3_Err != nil {
815- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 183, Col: 66}
802+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 182, Col: 66}
816803 }
817- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
804+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
818805 if templ_7745c5c3_Err != nil {
819806 return templ_7745c5c3_Err
820807 }
821- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</span></span> ")
808+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "</span></span> ")
822809 if templ_7745c5c3_Err != nil {
823810 return templ_7745c5c3_Err
824811 }
825812 }
826- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "<kbd aria-hidden=\"true\">")
813+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "<kbd aria-hidden=\"true\">")
827814 if templ_7745c5c3_Err != nil {
828815 return templ_7745c5c3_Err
829816 }
830- var templ_7745c5c3_Var43 string
831- templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs("g " + s.Key)
817+ var templ_7745c5c3_Var42 string
818+ templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs("g " + s.Key)
832819 if templ_7745c5c3_Err != nil {
833- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 185, Col: 46}
820+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 184, Col: 46}
834821 }
835- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
822+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
836823 if templ_7745c5c3_Err != nil {
837824 return templ_7745c5c3_Err
838825 }
839- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</kbd></a></li>")
826+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "</kbd></a></li>")
840827 if templ_7745c5c3_Err != nil {
841828 return templ_7745c5c3_Err
842829 }
843830 }
844- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "</ul></nav>")
831+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</ul></nav>")
845832 if templ_7745c5c3_Err != nil {
846833 return templ_7745c5c3_Err
847834 }
848835 }
849- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "</header>")
836+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "</header>")
850837 if templ_7745c5c3_Err != nil {
851838 return templ_7745c5c3_Err
852839 }
@@ -872,38 +859,38 @@ func Footer(l Layout) templ.Component {
872859 }()
873860 }
874861 ctx = templ.InitializeContext(ctx)
875- templ_7745c5c3_Var44 := templ.GetChildren(ctx)
876- if templ_7745c5c3_Var44 == nil {
877- templ_7745c5c3_Var44 = templ.NopComponent
862+ templ_7745c5c3_Var43 := templ.GetChildren(ctx)
863+ if templ_7745c5c3_Var43 == nil {
864+ templ_7745c5c3_Var43 = templ.NopComponent
878865 }
879866 ctx = templ.ClearChildren(ctx)
880- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "<footer class=\"foot\"><span>ermite ")
867+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "<footer class=\"foot\"><span>ermite ")
881868 if templ_7745c5c3_Err != nil {
882869 return templ_7745c5c3_Err
883870 }
884- var templ_7745c5c3_Var45 string
885- templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version)
871+ var templ_7745c5c3_Var44 string
872+ templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version)
886873 if templ_7745c5c3_Err != nil {
887- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 199, Col: 26}
874+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 198, Col: 26}
888875 }
889- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
876+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
890877 if templ_7745c5c3_Err != nil {
891878 return templ_7745c5c3_Err
892879 }
893- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "</span> <a href=\"")
880+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "</span> <a href=\"")
894881 if templ_7745c5c3_Err != nil {
895882 return templ_7745c5c3_Err
896883 }
897- var templ_7745c5c3_Var46 templ.SafeURL
898- templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL))
884+ var templ_7745c5c3_Var45 templ.SafeURL
885+ templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL))
899886 if templ_7745c5c3_Err != nil {
900- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 200, Col: 38}
887+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 199, Col: 38}
901888 }
902- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))
889+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
903890 if templ_7745c5c3_Err != nil {
904891 return templ_7745c5c3_Err
905892 }
906- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <button type=\"button\" class=\"foot-keys\" data-js data-fire=\"help\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\"><kbd aria-hidden=\"true\">?</kbd> keyboard shortcuts</button></footer>")
893+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <button type=\"button\" class=\"foot-keys\" data-js data-fire=\"help\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\"><kbd aria-hidden=\"true\">?</kbd> keyboard shortcuts</button></footer>")
907894 if templ_7745c5c3_Err != nil {
908895 return templ_7745c5c3_Err
909896 }
@@ -931,12 +918,12 @@ func Hint(l Layout) templ.Component {
931918 }()
932919 }
933920 ctx = templ.InitializeContext(ctx)
934- templ_7745c5c3_Var47 := templ.GetChildren(ctx)
935- if templ_7745c5c3_Var47 == nil {
936- templ_7745c5c3_Var47 = templ.NopComponent
921+ templ_7745c5c3_Var46 := templ.GetChildren(ctx)
922+ if templ_7745c5c3_Var46 == nil {
923+ templ_7745c5c3_Var46 = templ.NopComponent
937924 }
938925 ctx = templ.ClearChildren(ctx)
939- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "<div class=\"hint\" role=\"status\" aria-live=\"polite\"><span class=\"hint-nav\" :text=\"document.body.dataset.nav || ''\"></span> <span class=\"hint-sel\" :text=\"document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''\"></span> <span class=\"hint-flash\" :text=\"document.body.dataset.flash || ''\"></span>")
926+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "<div class=\"hint\" role=\"status\" aria-live=\"polite\"><span class=\"hint-nav\" :text=\"document.body.dataset.nav || ''\"></span> <span class=\"hint-sel\" :text=\"document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''\"></span> <span class=\"hint-flash\" :text=\"document.body.dataset.flash || ''\"></span>")
940927 if templ_7745c5c3_Err != nil {
941928 return templ_7745c5c3_Err
942929 }
@@ -944,7 +931,7 @@ func Hint(l Layout) templ.Component {
944931 if templ_7745c5c3_Err != nil {
945932 return templ_7745c5c3_Err
946933 }
947- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "</div>")
934+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "</div>")
948935 if templ_7745c5c3_Err != nil {
949936 return templ_7745c5c3_Err
950937 }
@@ -751,102 +751,89 @@ func Header(l Layout) templ.Component {
751 if templ_7745c5c3_Err != nil {751 if templ_7745c5c3_Err != nil {
752 return templ_7745c5c3_Err752 return templ_7745c5c3_Err
753 }753 }
754- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "\" aria-keyshortcuts=\"")754+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "\"")
755- if templ_7745c5c3_Err != nil {
756- return templ_7745c5c3_Err
757- }
758- var templ_7745c5c3_Var39 string
759- templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.ResolveAttributeValue("g " + s.Key)
760- if templ_7745c5c3_Err != nil {
761- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 176, Col: 40}
762- }
763- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var39)
764- if templ_7745c5c3_Err != nil {
765- return templ_7745c5c3_Err
766- }
767- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "\"")
768 if templ_7745c5c3_Err != nil {755 if templ_7745c5c3_Err != nil {
769 return templ_7745c5c3_Err756 return templ_7745c5c3_Err
770 }757 }
771 if s.Current {758 if s.Current {
772- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, " aria-current=\"page\"")759+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, " aria-current=\"page\"")
773 if templ_7745c5c3_Err != nil {760 if templ_7745c5c3_Err != nil {
774 return templ_7745c5c3_Err761 return templ_7745c5c3_Err
775 }762 }
776 }763 }
777- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, ">")764+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, ">")
778 if templ_7745c5c3_Err != nil {765 if templ_7745c5c3_Err != nil {
779 return templ_7745c5c3_Err766 return templ_7745c5c3_Err
780 }767 }
781- var templ_7745c5c3_Var40 string768+ var templ_7745c5c3_Var39 string
782- templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label)769+ templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(s.Label)
783 if templ_7745c5c3_Err != nil {770 if templ_7745c5c3_Err != nil {
784- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 181, Col: 17}771+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 180, Col: 17}
785 }772 }
786- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))773+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
787 if templ_7745c5c3_Err != nil {774 if templ_7745c5c3_Err != nil {
788 return templ_7745c5c3_Err775 return templ_7745c5c3_Err
789 }776 }
790- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, " ")777+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, " ")
791 if templ_7745c5c3_Err != nil {778 if templ_7745c5c3_Err != nil {
792 return templ_7745c5c3_Err779 return templ_7745c5c3_Err
793 }780 }
794 if s.Count != "" {781 if s.Count != "" {
795- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "<span class=\"count\">")782+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "<span class=\"count\">")
796 if templ_7745c5c3_Err != nil {783 if templ_7745c5c3_Err != nil {
797 return templ_7745c5c3_Err784 return templ_7745c5c3_Err
798 }785 }
799- var templ_7745c5c3_Var41 string786+ var templ_7745c5c3_Var40 string
800- templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(s.Count)787+ templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(s.Count)
801 if templ_7745c5c3_Err != nil {788 if templ_7745c5c3_Err != nil {
802- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 183, Col: 38}789+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 182, Col: 38}
803 }790 }
804- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))791+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
805 if templ_7745c5c3_Err != nil {792 if templ_7745c5c3_Err != nil {
806 return templ_7745c5c3_Err793 return templ_7745c5c3_Err
807 }794 }
808- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "<span class=\"sr\">")795+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "<span class=\"sr\">")
809 if templ_7745c5c3_Err != nil {796 if templ_7745c5c3_Err != nil {
810 return templ_7745c5c3_Err797 return templ_7745c5c3_Err
811 }798 }
812- var templ_7745c5c3_Var42 string799+ var templ_7745c5c3_Var41 string
813- templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(" open")800+ templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(" open")
814 if templ_7745c5c3_Err != nil {801 if templ_7745c5c3_Err != nil {
815- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 183, Col: 66}802+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 182, Col: 66}
816 }803 }
817- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))804+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
818 if templ_7745c5c3_Err != nil {805 if templ_7745c5c3_Err != nil {
819 return templ_7745c5c3_Err806 return templ_7745c5c3_Err
820 }807 }
821- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</span></span> ")808+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "</span></span> ")
822 if templ_7745c5c3_Err != nil {809 if templ_7745c5c3_Err != nil {
823 return templ_7745c5c3_Err810 return templ_7745c5c3_Err
824 }811 }
825 }812 }
826- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "<kbd aria-hidden=\"true\">")813+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "<kbd aria-hidden=\"true\">")
827 if templ_7745c5c3_Err != nil {814 if templ_7745c5c3_Err != nil {
828 return templ_7745c5c3_Err815 return templ_7745c5c3_Err
829 }816 }
830- var templ_7745c5c3_Var43 string817+ var templ_7745c5c3_Var42 string
831- templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs("g " + s.Key)818+ templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs("g " + s.Key)
832 if templ_7745c5c3_Err != nil {819 if templ_7745c5c3_Err != nil {
833- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 185, Col: 46}820+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 184, Col: 46}
834 }821 }
835- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))822+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
836 if templ_7745c5c3_Err != nil {823 if templ_7745c5c3_Err != nil {
837 return templ_7745c5c3_Err824 return templ_7745c5c3_Err
838 }825 }
839- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</kbd></a></li>")826+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "</kbd></a></li>")
840 if templ_7745c5c3_Err != nil {827 if templ_7745c5c3_Err != nil {
841 return templ_7745c5c3_Err828 return templ_7745c5c3_Err
842 }829 }
843 }830 }
844- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "</ul></nav>")831+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "</ul></nav>")
845 if templ_7745c5c3_Err != nil {832 if templ_7745c5c3_Err != nil {
846 return templ_7745c5c3_Err833 return templ_7745c5c3_Err
847 }834 }
848 }835 }
849- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "</header>")836+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "</header>")
850 if templ_7745c5c3_Err != nil {837 if templ_7745c5c3_Err != nil {
851 return templ_7745c5c3_Err838 return templ_7745c5c3_Err
852 }839 }
@@ -872,38 +859,38 @@ func Footer(l Layout) templ.Component {
872 }()859 }()
873 }860 }
874 ctx = templ.InitializeContext(ctx)861 ctx = templ.InitializeContext(ctx)
875- templ_7745c5c3_Var44 := templ.GetChildren(ctx)862+ templ_7745c5c3_Var43 := templ.GetChildren(ctx)
876- if templ_7745c5c3_Var44 == nil {863+ if templ_7745c5c3_Var43 == nil {
877- templ_7745c5c3_Var44 = templ.NopComponent864+ templ_7745c5c3_Var43 = templ.NopComponent
878 }865 }
879 ctx = templ.ClearChildren(ctx)866 ctx = templ.ClearChildren(ctx)
880- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "<footer class=\"foot\"><span>ermite ")867+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "<footer class=\"foot\"><span>ermite ")
881 if templ_7745c5c3_Err != nil {868 if templ_7745c5c3_Err != nil {
882 return templ_7745c5c3_Err869 return templ_7745c5c3_Err
883 }870 }
884- var templ_7745c5c3_Var45 string871+ var templ_7745c5c3_Var44 string
885- templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version)872+ templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(l.Version)
886 if templ_7745c5c3_Err != nil {873 if templ_7745c5c3_Err != nil {
887- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 199, Col: 26}874+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 198, Col: 26}
888 }875 }
889- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))876+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
890 if templ_7745c5c3_Err != nil {877 if templ_7745c5c3_Err != nil {
891 return templ_7745c5c3_Err878 return templ_7745c5c3_Err
892 }879 }
893- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "</span> <a href=\"")880+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "</span> <a href=\"")
894 if templ_7745c5c3_Err != nil {881 if templ_7745c5c3_Err != nil {
895 return templ_7745c5c3_Err882 return templ_7745c5c3_Err
896 }883 }
897- var templ_7745c5c3_Var46 templ.SafeURL884+ var templ_7745c5c3_Var45 templ.SafeURL
898- templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL))885+ templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(l.SourceURL))
899 if templ_7745c5c3_Err != nil {886 if templ_7745c5c3_Err != nil {
900- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 200, Col: 38}887+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/adapters/web/shell.templ`, Line: 199, Col: 38}
901 }888 }
902- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))889+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
903 if templ_7745c5c3_Err != nil {890 if templ_7745c5c3_Err != nil {
904 return templ_7745c5c3_Err891 return templ_7745c5c3_Err
905 }892 }
906- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <button type=\"button\" class=\"foot-keys\" data-js data-fire=\"help\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\"><kbd aria-hidden=\"true\">?</kbd> keyboard shortcuts</button></footer>")893+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "\" rel=\"noopener\" title=\"source of this version (AGPL-3.0-or-later)\">source</a> <button type=\"button\" class=\"foot-keys\" data-js data-fire=\"help\" aria-haspopup=\"dialog\" aria-controls=\"panel-help\" aria-keyshortcuts=\"?\"><kbd aria-hidden=\"true\">?</kbd> keyboard shortcuts</button></footer>")
907 if templ_7745c5c3_Err != nil {894 if templ_7745c5c3_Err != nil {
908 return templ_7745c5c3_Err895 return templ_7745c5c3_Err
909 }896 }
@@ -931,12 +918,12 @@ func Hint(l Layout) templ.Component {
931 }()918 }()
932 }919 }
933 ctx = templ.InitializeContext(ctx)920 ctx = templ.InitializeContext(ctx)
934- templ_7745c5c3_Var47 := templ.GetChildren(ctx)921+ templ_7745c5c3_Var46 := templ.GetChildren(ctx)
935- if templ_7745c5c3_Var47 == nil {922+ if templ_7745c5c3_Var46 == nil {
936- templ_7745c5c3_Var47 = templ.NopComponent923+ templ_7745c5c3_Var46 = templ.NopComponent
937 }924 }
938 ctx = templ.ClearChildren(ctx)925 ctx = templ.ClearChildren(ctx)
939- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "<div class=\"hint\" role=\"status\" aria-live=\"polite\"><span class=\"hint-nav\" :text=\"document.body.dataset.nav || ''\"></span> <span class=\"hint-sel\" :text=\"document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''\"></span> <span class=\"hint-flash\" :text=\"document.body.dataset.flash || ''\"></span>")926+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "<div class=\"hint\" role=\"status\" aria-live=\"polite\"><span class=\"hint-nav\" :text=\"document.body.dataset.nav || ''\"></span> <span class=\"hint-sel\" :text=\"document.body.dataset.sel ? document.body.dataset.sel + ' · y permalink' : ''\"></span> <span class=\"hint-flash\" :text=\"document.body.dataset.flash || ''\"></span>")
940 if templ_7745c5c3_Err != nil {927 if templ_7745c5c3_Err != nil {
941 return templ_7745c5c3_Err928 return templ_7745c5c3_Err
942 }929 }
@@ -944,7 +931,7 @@ func Hint(l Layout) templ.Component {
944 if templ_7745c5c3_Err != nil {931 if templ_7745c5c3_Err != nil {
945 return templ_7745c5c3_Err932 return templ_7745c5c3_Err
946 }933 }
947- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "</div>")934+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "</div>")
948 if templ_7745c5c3_Err != nil {935 if templ_7745c5c3_Err != nil {
949 return templ_7745c5c3_Err936 return templ_7745c5c3_Err
950 }937 }

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 4

Tags 45

↑↓ moveenter openesc close