v0.1.0 47aaa0f

controls_extra_test.go RawHistoryFolder

1package ui
2
3import (
4	"github.com/bnema/nefergui/internal/css"
5	"github.com/bnema/nefergui/internal/edit"
6	"github.com/bnema/nefergui/internal/keyboard"
7	"testing"
8)
9
10type clip struct{ data []byte }
11
12func (c *clip) ReadText(limit int) ([]byte, error) { return c.data, nil }
13func (c *clip) WriteText(v []byte) error           { c.data = append([]byte(nil), v...); return nil }
14func TestRadioSliderAndEditingIdentity(t *testing.T) {
15	r := newRuntime()
16	r.styles = css.Compile(css.UA(), css.Parse(`input,radio,slider {height:20px}`))
17	var choice string
18	v := 3.0
19	name := "e\u0301"
20	order := []string{"a", "b"}
21	view := func(f *Frame) {
22		root := f.Root()
23		for _, key := range order {
24			root.Input(key, &name, Key(key))
25		}
26		root.Radio("One", "one", &choice, Key("r1"))
27		root.Radio("Two", "two", &choice, Key("r2"))
28		root.Slider("level", &v, 0, 10, 1, Key("s"))
29	}
30	r.Build(view)
31	r.Queue(inputEvent{Target: r.Target(3), Kind: "activate"})
32	r.Build(view)
33	if choice != "two" {
34		t.Fatal(choice)
35	}
36	r.route(key("Tab"))
37	r.Build(view)
38	for !r.state.focus.same(r.Target(4)) {
39		r.route(key("Tab"))
40		r.Build(view)
41		if r.state.focus.same(r.Target(0)) {
42			t.Fatalf("slider skipped: %v", func() []string {
43				var ids []string
44				for _, e := range r.focusOrder() {
45					ids = append(ids, idKey(e.identity))
46				}
47				return ids
48			}())
49		}
50	}
51	if !r.state.focus.same(r.Target(4)) {
52		t.Fatalf("slider focus: focus=%s order=%v", idKey(r.state.focus), func() []string {
53			var ids []string
54			for _, e := range r.focusOrder() {
55				ids = append(ids, idKey(e.identity))
56			}
57			return ids
58		}())
59	}
60	r.route(key("Right"))
61	r.Build(view)
62	if v != 4 {
63		t.Fatal(v)
64	}
65	r.Queue(inputEvent{Target: r.Target(0), Kind: "edit-key", Text: "backspace"})
66	r.Build(view)
67	if name != "" {
68		t.Fatal("grapheme delete", name)
69	}
70	order = []string{"b", "a"}
71	r.Redraw()
72	r.Build(view)
73	if r.edits[idKey(r.Target(0))] == r.edits[idKey(r.Target(1))] {
74		t.Fatal("state aliased")
75	}
76}
77func TestDisabledEditorRejectsFocusAndText(t *testing.T) {
78	r := newRuntime()
79	r.styles = css.Compile(css.UA(), css.Parse(`input {height:20px}`))
80	value := "keep"
81	view := func(f *Frame) { f.Root().Input("name", &value, Key("name"), Disabled(true)) }
82	r.Build(view)
83	r.route(key("Tab"))
84	r.Build(view)
85	if r.state.focus != nil {
86		t.Fatalf("disabled input focused: %s", idKey(r.state.focus))
87	}
88	r.route(platformInput{Kind: "key", Key: keyboard.Key{Name: "q", Text: "x", Pressed: true}})
89	r.Build(view)
90	if value != "keep" {
91		t.Fatalf("disabled input edited: %q", value)
92	}
93}
94
95func TestEditFocusIMEAndClipboard(t *testing.T) {
96	r := newRuntime()
97	r.styles = css.Compile(css.UA(), css.Parse(`input {height:20px}`))
98	var value string
99	view := func(f *Frame) { f.Root().Input("secret", &value, Key("secret"), Password(true)) }
100	r.Build(view)
101	r.clipboard = &clip{data: []byte("paste")}
102	r.route(key("Tab"))
103	r.Build(view)
104	r.route(platformInput{Kind: "key", Key: keyboard.Key{Name: "q", Text: "x", Pressed: true}})
105	r.Build(view)
106	if value != "x" || r.committed.children[0].text != "•" {
107		t.Fatalf("value=%q display=%q focus=%s", value, r.committed.children[0].text, idKey(r.state.focus))
108	}
109	r.route(platformInput{Kind: "ime-done", IME: edit.IMEBatch{Preedit: "候補"}})
110	r.Build(view)
111	if value != "x" {
112		t.Fatal(value)
113	}
114	r.route(platformInput{Kind: "focus-out"})
115	r.Build(view)
116	if r.edits[idKey(r.Target(0))].Preedit != "" {
117		t.Fatal("preedit survives blur")
118	}
119}

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 1

Tags 12

↑↓ moveenter openesc close