v0.31.0 bf0756b

input_focus.go RawHistoryFolder

1package cef
2
3import (
4	"context"
5
6	"github.com/bnema/dumber/internal/application/port"
7)
8
9var (
10	_ port.PageInputFocuser   = (*WebView)(nil)
11	_ port.PageFocusNavigator = (*WebView)(nil)
12)
13
14// FocusNextInput moves focus to the next visible, editable page input. When
15// no eligible input is active it focuses the first one; movement wraps at the
16// end so repeated gi commands continue to cycle through the page inputs.
17func (wv *WebView) FocusNextInput() {
18	if wv == nil || wv.destroyed.Load() {
19		return
20	}
21	wv.RunJavaScript(context.Background(), focusNextInputScript())
22}
23
24// NavigatePageFocus keeps reverse and forward focus traversal inside the live
25// page when GTK would otherwise move focus out of the WebView.
26func (wv *WebView) NavigatePageFocus(backward bool) {
27	if wv == nil || wv.destroyed.Load() {
28		return
29	}
30	wv.RunJavaScript(context.Background(), pageFocusNavigationScript(backward))
31}
32
33func focusNextInputScript() string {
34	return `(() => {
35  const candidates = Array.from(document.querySelectorAll("input,textarea,[contenteditable]"));
36  const hiddenInputTypes = new Set([
37    "button", "checkbox", "color", "file", "hidden", "image", "radio", "range", "reset", "submit"
38  ]);
39  const isVisible = (element) => {
40    if (!(element instanceof HTMLElement) || !element.isConnected) return false;
41    if (element.closest("[hidden],[inert],[aria-hidden=\"true\"]")) return false;
42    if (element.getClientRects().length === 0) return false;
43    const style = window.getComputedStyle(element);
44    return style.display !== "none" && style.visibility !== "hidden" && style.visibility !== "collapse";
45  };
46  const isEligible = (element) => {
47    if (!isVisible(element)) return false;
48    if (element instanceof HTMLInputElement) {
49      return !element.matches(":disabled") && !element.readOnly && !hiddenInputTypes.has(element.type.toLowerCase());
50    }
51    if (element instanceof HTMLTextAreaElement) {
52      return !element.matches(":disabled") && !element.readOnly;
53    }
54    return element.isContentEditable && element.getAttribute("contenteditable") !== "false";
55  };
56  const eligible = candidates.filter(isEligible);
57  if (eligible.length === 0) return;
58  const active = document.activeElement;
59  const activeIndex = eligible.findIndex((element) => element === active || element.contains(active));
60  const target = eligible[(activeIndex + 1 + eligible.length) % eligible.length];
61  target.focus();
62})();`
63}
64
65func pageFocusNavigationScript(backward bool) string {
66	direction := "1"
67	if backward {
68		direction = "-1"
69	}
70	return `(() => {
71  const direction = ` + direction + `;
72  const selector = [
73    "a[href]", "area[href]", "button:not(:disabled)",
74    "input:not(:disabled):not([type=\"hidden\"])", "select:not(:disabled)",
75    "textarea:not(:disabled)", "[contenteditable]:not([contenteditable=\"false\"])",
76    "[tabindex]:not([tabindex=\"-1\"])"
77  ].join(",");
78  const isVisible = (element) => {
79    if (!(element instanceof HTMLElement) || !element.isConnected) return false;
80    if (element.closest("[hidden],[inert],[aria-hidden=\"true\"]")) return false;
81    if (element.getClientRects().length === 0) return false;
82    const style = window.getComputedStyle(element);
83    return style.display !== "none" && style.visibility !== "hidden" && style.visibility !== "collapse";
84  };
85  const candidates = Array.from(document.querySelectorAll(selector)).filter((element) => {
86    return isVisible(element) && !element.matches(":disabled") && element.tabIndex >= 0;
87  });
88  if (candidates.length === 0) return;
89  const active = document.activeElement;
90  const activeIndex = candidates.findIndex((element) => element === active || element.contains(active));
91  const targetIndex = activeIndex < 0
92    ? (direction < 0 ? candidates.length - 1 : 0)
93    : activeIndex + direction;
94  if (targetIndex < 0 || targetIndex >= candidates.length) return;
95  candidates[targetIndex].focus();
96})();`
97}

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 8

Tags 42

↑↓ moveenter openesc close