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-webkit/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}