v0.1.0 d9218d6

statefile_test.go RawHistoryFolder

1package statefile
2
3import (
4	"context"
5	"errors"
6	"os"
7	"path/filepath"
8	"reflect"
9	"testing"
10	"time"
11
12	"github.com/bnema/neferwl/internal/ports"
13	"github.com/bnema/zerowrap"
14)
15
16var sample = State{
17	Output:  "DP-2",
18	Outputs: []Output{{Name: "HDMI-A-1", Active: 1, Count: 1}, {Name: "DP-2", Active: 2, Count: 3, Workspace: "web"}},
19	Windows: []Window{{ID: 1, AppID: "foot", PID: 100, Output: "HDMI-A-1", Workspace: 1, Visible: true}, {ID: 2, AppID: "foot", PID: 200, Output: "DP-2", Workspace: 2, Visible: true}},
20}
21
22// The file holds the latest state while the session runs; a stale file from
23// a crashed session goes at start, and the file is removed at exit.
24func TestRunWritesAndRemoves(t *testing.T) {
25	path, err := Path(t.TempDir(), "wayland-9")
26	if err != nil {
27		t.Fatal(err)
28	}
29	if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
30		t.Fatal(err)
31	}
32	if err := os.WriteFile(path, []byte(`{"output":"stale"}`), 0o600); err != nil {
33		t.Fatal(err)
34	}
35	states := make(chan ports.State, 1)
36	ctx, cancel := context.WithCancel(context.Background())
37	done := make(chan error, 1)
38	go func() { done <- Run(ctx, path, states, zerowrap.Default()) }()
39	focused := ports.WindowState{ID: 2, AppID: "foot", PID: 200, Output: "DP-2", Workspace: 1, Visible: true}
40	states <- ports.State{Output: "DP-2", Outputs: []ports.OutputState{{Name: "DP-2", Active: 1, Count: 1}}, Window: &focused, Windows: []ports.WindowState{focused}}
41	want := State{Output: "DP-2", Outputs: []Output{{Name: "DP-2", Active: 1, Count: 1}}, Window: &Window{ID: 2, AppID: "foot", PID: 200, Output: "DP-2", Workspace: 1, Visible: true}, Windows: []Window{{ID: 2, AppID: "foot", PID: 200, Output: "DP-2", Workspace: 1, Visible: true}}}
42	deadline := time.Now().Add(2 * time.Second)
43	for {
44		if got, err := Read(path); err == nil && got.Output != "stale" {
45			if !reflect.DeepEqual(got, want) {
46				t.Fatalf("%+v", got)
47			}
48			break
49		}
50		if time.Now().After(deadline) {
51			t.Fatal("no state file")
52		}
53		time.Sleep(5 * time.Millisecond)
54	}
55	cancel()
56	if err := <-done; err != nil {
57		t.Fatal(err)
58	}
59	if _, err := os.Stat(path); !os.IsNotExist(err) {
60		t.Fatal("state file left behind", err)
61	}
62}
63
64func TestPath(t *testing.T) {
65	if p, err := Path("/run/user/1", "/run/user/1/wayland-1"); err != nil || p != "/run/user/1/neferwl/wayland-1.json" {
66		t.Fatal(p, err)
67	}
68	for _, c := range [][2]string{{"", "wayland-1"}, {"/run/user/1", ""}} {
69		if _, err := Path(c[0], c[1]); err == nil {
70			t.Fatal(c)
71		}
72	}
73}
74
75// A shell finds its window through its ancestors.
76func TestOutputOfWalksParents(t *testing.T) {
77	tree := newMockprocTree(t)
78	// shell 300 → login 250 → terminal 200 (window on DP-2)
79	tree.EXPECT().Parent(300).Return(250, true)
80	tree.EXPECT().Parent(250).Return(200, true)
81	o, err := outputOf(sample, 300, tree)
82	if err != nil || o.Name != "DP-2" || o.Active != 2 || o.Count != 3 {
83		t.Fatal(o, err)
84	}
85	// The window's own process needs no walk.
86	if o, err := outputOf(sample, 100, newMockprocTree(t)); err != nil || o.Name != "HDMI-A-1" {
87		t.Fatal(o, err)
88	}
89	// No window up to init.
90	orphan := newMockprocTree(t)
91	orphan.EXPECT().Parent(400).Return(1, true)
92	orphan.EXPECT().Parent(1).Return(0, false)
93	if _, err := outputOf(sample, 400, orphan); !errors.Is(err, ErrNoWindow) {
94		t.Fatal(err)
95	}
96}
97
98// One process with windows on two outputs (a terminal server): the focused
99// window wins, else a visible one.
100func TestOutputOfPrefersFocusedWindow(t *testing.T) {
101	st := sample
102	st.Windows = []Window{
103		{ID: 1, PID: 100, Output: "HDMI-A-1", Visible: true},
104		{ID: 2, PID: 100, Output: "DP-2", Visible: false},
105		{ID: 3, PID: 100, Output: "DP-2", Visible: true},
106	}
107	if o, _ := outputOf(st, 100, newMockprocTree(t)); o.Name != "HDMI-A-1" {
108		t.Fatal(o)
109	}
110	st.Window = &st.Windows[2]
111	if o, _ := outputOf(st, 100, newMockprocTree(t)); o.Name != "DP-2" {
112		t.Fatal(o)
113	}
114}
115
116func TestLinuxProcTreeParent(t *testing.T) {
117	if ppid, ok := (linuxProcTree{}).Parent(os.Getpid()); !ok || ppid != os.Getppid() {
118		t.Fatal(ppid, ok)
119	}
120}

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 26

Tags 10

↑↓ moveenter openesc close