statefile_test.go RawHistoryFolder

1package statefile
2
3import (
4	"context"
5	"encoding/json"
6	"errors"
7	"os"
8	"path/filepath"
9	"reflect"
10	"strings"
11	"testing"
12	"time"
13
14	"github.com/bnema/neferwl/internal/adapters/workspaceid"
15	"github.com/bnema/neferwl/internal/ports"
16	"github.com/bnema/zerowrap"
17)
18
19var sample = State{
20	Output:  "DP-2",
21	Outputs: []Output{{Name: "HDMI-A-1", Active: 1, Count: 1, WorkspaceID: "p-1"}, {Name: "DP-2", Active: 2, Count: 3, Workspace: "web", WorkspaceID: "name:web"}},
22	Windows: []Window{{ID: 1, AppID: "foot", PID: 100, Output: "HDMI-A-1", Workspace: 1, WorkspaceID: "p-1", Visible: true}, {ID: 2, AppID: "foot", PID: 200, Output: "DP-2", Workspace: 2, WorkspaceID: "name:web", Visible: true}},
23}
24
25// The file holds the latest state while the session runs; a stale file from
26// a crashed session goes at start, and the file is removed at exit.
27func TestRunWritesAndRemoves(t *testing.T) {
28	path, err := Path(t.TempDir(), "wayland-9")
29	if err != nil {
30		t.Fatal(err)
31	}
32	if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
33		t.Fatal(err)
34	}
35	if err := os.WriteFile(path, []byte(`{"output":"stale"}`), 0o600); err != nil {
36		t.Fatal(err)
37	}
38	states := make(chan ports.State, 1)
39	ctx, cancel := context.WithCancel(context.Background())
40	done := make(chan error, 1)
41	go func() { done <- Run(ctx, path, states, workspaceid.WithPrefix("0a1b2c3d"), zerowrap.Default()) }()
42	focused := ports.WindowState{ID: 2, AppID: "foot", PID: 200, Output: "DP-2", Workspace: 1, WorkspaceID: 9, Visible: true}
43	states <- ports.State{Output: "DP-2", Outputs: []ports.OutputState{{Name: "DP-2", Active: 1, Count: 1, WorkspaceID: 9}}, Window: &focused, Windows: []ports.WindowState{focused}}
44	want := State{Output: "DP-2", Outputs: []Output{{Name: "DP-2", Active: 1, Count: 1, WorkspaceID: "0a1b2c3d-9"}}, Window: &Window{ID: 2, AppID: "foot", PID: 200, Output: "DP-2", Workspace: 1, WorkspaceID: "0a1b2c3d-9", Visible: true}, Windows: []Window{{ID: 2, AppID: "foot", PID: 200, Output: "DP-2", Workspace: 1, WorkspaceID: "0a1b2c3d-9", Visible: true}}}
45	deadline := time.Now().Add(2 * time.Second)
46	for {
47		if got, err := Read(path); err == nil && got.Output != "stale" {
48			if !reflect.DeepEqual(got, want) {
49				t.Fatalf("%+v", got)
50			}
51			break
52		}
53		if time.Now().After(deadline) {
54			t.Fatal("no state file")
55		}
56		time.Sleep(5 * time.Millisecond)
57	}
58	cancel()
59	if err := <-done; err != nil {
60		t.Fatal(err)
61	}
62	if _, err := os.Stat(path); !os.IsNotExist(err) {
63		t.Fatal("state file left behind", err)
64	}
65}
66
67// Stash fields reach the file with their script-facing names.
68func TestWindowStashFields(t *testing.T) {
69	got := window(ports.WindowState{ID: 3, Floating: true, StashIndex: 2, StashCount: 4, Hidden: true}, workspaceid.WithPrefix("0a1b2c3d"))
70	if !got.Floating || got.StashIndex != 2 || got.StashCount != 4 || !got.Hidden {
71		t.Fatalf("%+v", got)
72	}
73	data, err := json.Marshal(got)
74	if err != nil {
75		t.Fatal(err)
76	}
77	for _, key := range []string{`"floating":true`, `"stash_index":2`, `"stash_count":4`, `"hidden":true`} {
78		if !strings.Contains(string(data), key) {
79			t.Fatalf("%s missing %s", data, key)
80		}
81	}
82}
83
84func TestPath(t *testing.T) {
85	if p, err := Path("/run/user/1", "/run/user/1/wayland-1"); err != nil || p != "/run/user/1/neferwl/wayland-1.json" {
86		t.Fatal(p, err)
87	}
88	for _, c := range [][2]string{{"", "wayland-1"}, {"/run/user/1", ""}} {
89		if _, err := Path(c[0], c[1]); err == nil {
90			t.Fatal(c)
91		}
92	}
93}
94
95// A shell finds its window through its ancestors.
96func TestOutputOfWalksParents(t *testing.T) {
97	tree := newMockprocTree(t)
98	// shell 300 → login 250 → terminal 200 (window on DP-2)
99	tree.EXPECT().Parent(300).Return(250, true)
100	tree.EXPECT().Parent(250).Return(200, true)
101	o, err := outputOf(sample, 300, tree)
102	if err != nil || o.Name != "DP-2" || o.Active != 2 || o.Count != 3 {
103		t.Fatal(o, err)
104	}
105	// The window's own process needs no walk.
106	if o, err := outputOf(sample, 100, newMockprocTree(t)); err != nil || o.Name != "HDMI-A-1" {
107		t.Fatal(o, err)
108	}
109	// No window up to init.
110	orphan := newMockprocTree(t)
111	orphan.EXPECT().Parent(400).Return(1, true)
112	orphan.EXPECT().Parent(1).Return(0, false)
113	if _, err := outputOf(sample, 400, orphan); !errors.Is(err, ErrNoWindow) {
114		t.Fatal(err)
115	}
116}
117
118// One process with windows on two outputs (a terminal server): the focused
119// window wins, else a visible one.
120func TestOutputOfPrefersFocusedWindow(t *testing.T) {
121	st := sample
122	st.Windows = []Window{
123		{ID: 1, PID: 100, Output: "HDMI-A-1", Visible: true},
124		{ID: 2, PID: 100, Output: "DP-2", Visible: false},
125		{ID: 3, PID: 100, Output: "DP-2", Visible: true},
126	}
127	if o, _ := outputOf(st, 100, newMockprocTree(t)); o.Name != "HDMI-A-1" {
128		t.Fatal(o)
129	}
130	st.Window = &st.Windows[2]
131	if o, _ := outputOf(st, 100, newMockprocTree(t)); o.Name != "DP-2" {
132		t.Fatal(o)
133	}
134}
135
136func TestLinuxProcTreeParent(t *testing.T) {
137	if ppid, ok := (linuxProcTree{}).Parent(os.Getpid()); !ok || ppid != os.Getppid() {
138		t.Fatal(ppid, ok)
139	}
140}
141
142// A configured workspace is named after its configured name, any other one
143// after the launch prefix and its core ID: the string of ext_workspace_handle_v1.
144func TestWorkspaceIDStrings(t *testing.T) {
145	ids := workspaceid.WithPrefix("0a1b2c3d")
146	st := fromPorts(ports.State{
147		Outputs: []ports.OutputState{{Name: "DP-2", Workspace: "web", WorkspaceID: 7}, {Name: "DP-3", WorkspaceID: 8}},
148		Windows: []ports.WindowState{{ID: 1, WorkspaceID: 7, WorkspaceName: "web"}, {ID: 2, WorkspaceID: 8}},
149	}, ids)
150	if st.Outputs[0].WorkspaceID != "name:web" || st.Outputs[1].WorkspaceID != "0a1b2c3d-8" {
151		t.Fatalf("outputs %+v", st.Outputs)
152	}
153	if st.Windows[0].WorkspaceID != "name:web" || st.Windows[1].WorkspaceID != "0a1b2c3d-8" {
154		t.Fatalf("windows %+v", st.Windows)
155	}
156	data, err := json.Marshal(st.Windows[1])
157	if err != nil || !strings.Contains(string(data), `"workspace_id":"0a1b2c3d-8"`) {
158		t.Fatalf("%s %v", data, err)
159	}
160}

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 25

Tags 10

↑↓ moveenter openesc close