webview_target.go RawHistoryFolder

1package textinput
2
3import (
4	"context"
5	"fmt"
6
7	"github.com/bnema/dumber/internal/application/port"
8	"github.com/bnema/dumber/internal/logging"
9	"github.com/bnema/puregotk/v4/webkit"
10)
11
12// deleteBeforeCursorJS is the JavaScript template to delete n characters before cursor.
13// It handles both input/textarea elements and contenteditable elements.
14const deleteBeforeCursorJS = `
15(function() {
16	const n = %d;
17	const el = window.__dumber_lastEditableEl || document.activeElement;
18	if (!el) return;
19	
20	// Handle input and textarea elements
21	if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
22		const start = el.selectionStart;
23		const end = el.selectionEnd;
24		if (start === end && start >= n) {
25			el.value = el.value.slice(0, start - n) + el.value.slice(end);
26			el.selectionStart = el.selectionEnd = start - n;
27			// Trigger input event for frameworks like React
28			el.dispatchEvent(new Event('input', { bubbles: true }));
29		}
30		return;
31	}
32	
33	// Handle contenteditable elements
34	if (el.isContentEditable) {
35		const sel = window.getSelection();
36		if (!sel.rangeCount) return;
37		for (let i = 0; i < n; i++) {
38			sel.modify('extend', 'backward', 'character');
39		}
40		sel.deleteFromDocument();
41	}
42})();
43`
44
45// insertTextJS inserts text at the cursor position of the last focused editable element.
46// Uses the tracked element from the accent detection script so insertion still works
47// after GTK focus moves to the accent picker.
48const insertTextJS = `
49(function() {
50	const text = %q;
51	const el = window.__dumber_lastEditableEl || document.activeElement;
52	if (!el) return;
53
54	el.focus();
55
56	if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
57		const start = el.selectionStart;
58		const end = el.selectionEnd;
59		el.value = el.value.slice(0, start) + text + el.value.slice(end);
60		el.selectionStart = el.selectionEnd = start + text.length;
61		el.dispatchEvent(new Event('input', { bubbles: true }));
62		return;
63	}
64
65	// execCommand is deprecated but remains the most reliable way to insert
66	// text into contenteditable elements across WebKit/GTK. The modern
67	// InputEvent('insertText') alternative has inconsistent support.
68	if (el.isContentEditable) {
69		document.execCommand('insertText', false, text);
70	}
71})();
72`
73
74// WebViewTarget implements text input and focus restoration for WebView fields.
75type WebViewTarget struct {
76	webView *webkit.WebView
77}
78
79var _ port.TextInputTarget = (*WebViewTarget)(nil)
80var _ port.Focusable = (*WebViewTarget)(nil)
81
82// NewWebViewTarget creates a new WebView target.
83func NewWebViewTarget(webView *webkit.WebView) *WebViewTarget {
84	return &WebViewTarget{
85		webView: webView,
86	}
87}
88
89// InsertText inserts text at the current cursor position in the WebView.
90func (t *WebViewTarget) InsertText(ctx context.Context, text string) error {
91	log := logging.FromContext(ctx)
92
93	if t.webView == nil {
94		log.Warn().Msg("WebViewTarget: webView is nil")
95		return nil
96	}
97
98	script := fmt.Sprintf(insertTextJS, text)
99	t.webView.EvaluateJavascript(script, -1, nil, nil, nil, nil, 0)
100
101	log.Debug().
102		Int("len", len(text)).
103		Msg("inserted text into WebView via JS")
104
105	return nil
106}
107
108// DeleteBeforeCursor deletes n characters before the cursor in the WebView.
109// Uses JavaScript to manipulate the focused input element.
110func (t *WebViewTarget) DeleteBeforeCursor(ctx context.Context, n int) error {
111	log := logging.FromContext(ctx)
112
113	if t.webView == nil {
114		log.Warn().Msg("WebViewTarget: webView is nil")
115		return nil
116	}
117
118	if n <= 0 {
119		return nil
120	}
121
122	// Execute JavaScript to delete characters
123	script := fmt.Sprintf(deleteBeforeCursorJS, n)
124	t.webView.EvaluateJavascript(script, -1, nil, nil, nil, nil, 0)
125
126	log.Debug().Int("n", n).Msg("deleted characters before cursor in WebView")
127	return nil
128}
129
130// Focus restores GTK focus to the WebView widget and refocuses the last
131// editable element inside the page.
132func (t *WebViewTarget) Focus(_ context.Context) {
133	if t.webView == nil {
134		return
135	}
136
137	t.webView.GrabFocus()
138	t.webView.EvaluateJavascript(`
139		(function() {
140			var el = window.__dumber_lastEditableEl;
141			if (el) el.focus();
142		})();
143	`, -1, nil, nil, nil, nil, 0)
144}

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