detached 208528f

refactor(core): move client event handling out of Run into clientEvent

commit
208528f6cb72c6b767be691421e49f8b1b61d317
author
brice · · 2026-10-08 05:52 UTC
parent
c76c9b5
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +174 -133
A internal/core/client.go +170 -0
@@ -0,0 +1,170 @@
1+package core
2+
3+import (
4+ "context"
5+
6+ "github.com/bnema/neferwl/internal/ports"
7+)
8+
9+// clientEvent applies one client event. publish asks for a new scene;
10+// stop ends Run (a command could not be delivered).
11+func (c *Core) clientEvent(ctx context.Context, ev ports.ClientEvent) (publish, stop bool) {
12+ switch v := ev.(type) {
13+ case ports.SessionLockChanged:
14+ if !c.applyLockChanged(v) {
15+ return false, false
16+ }
17+ case ports.CaptureSessionOpen:
18+ c.captureOpen(v)
19+ case ports.CaptureSessionClose:
20+ c.captureClose(v.ID)
21+ case ports.CaptureFrameTaken:
22+ // Nothing new to show (the target flashes already): no scene.
23+ if !c.captureFrame(v) {
24+ return false, false
25+ }
26+ case ports.CaptureExclusionBegin:
27+ c.captureExclusionBegin(v)
28+ case ports.CaptureExclusionLayer:
29+ c.captureExclusionLayer(v)
30+ case ports.CaptureExclusionEnd:
31+ c.captureExclusionEnd(v.Session)
32+ case ports.LayerChanged:
33+ c.layerChanged = true
34+ c.setLayers(v.Layers)
35+ case ports.InputRegionChanged:
36+ c.windows.setRegion(v)
37+ case ports.WindowMapped:
38+ c.mapWindow(v)
39+ case ports.WindowResized:
40+ c.resizeFloating(v)
41+ case ports.PopupRequest:
42+ if err := c.placePopup(ctx, v); err != nil {
43+ return false, true
44+ }
45+ case ports.PopupMapped:
46+ if p := c.popups[v.ID]; p != nil {
47+ p.mapped = true
48+ }
49+ case ports.ShortcutsInhibit:
50+ c.windows.setInhibitShortcuts(v.Window, v.Active)
51+ if err := c.updateInhibit(ctx); err != nil {
52+ return false, true
53+ }
54+ return false, false
55+ case ports.OutputPower:
56+ if i := c.screenIndex(v.Output); i >= 0 && c.screens[i].off == v.On {
57+ c.screens[i].off = !v.On
58+ }
59+ case ports.IdleInhibit:
60+ c.windows.setIdleInhibit(v.Window, v.Active)
61+ c.publishState()
62+ return false, false
63+ case ports.WindowAppID:
64+ c.windows.setAppID(v.ID, v.AppID)
65+ case ports.WindowParent:
66+ if _, w := c.screenOf(v.ID); w != nil {
67+ w.SetDialogParent(v.ID, v.Parent)
68+ }
69+ case ports.WindowUnmapped:
70+ if c.unmapClient(ctx, v) != nil {
71+ return false, true
72+ }
73+ case ports.PointerConstrained:
74+ c.constrained = v
75+ case ports.PointerWarp:
76+ if c.warpPointer(ctx, v) != nil {
77+ return false, true
78+ }
79+ case ports.WindowMoveRequest:
80+ if c.clientDrag(ctx, v) != nil {
81+ return false, true
82+ }
83+ case ports.WindowFullscreenRequest:
84+ if c.fullscreenRequest(ctx, v) != nil {
85+ return false, true
86+ }
87+ case ports.WorkspaceActivate:
88+ if c.activateWorkspaces(ctx, v) != nil {
89+ return false, true
90+ }
91+ case ports.WindowActivate:
92+ if c.activate(ctx, v.ID) != nil {
93+ return false, true
94+ }
95+ }
96+ return true, false
97+}
98+
99+// unmapClient drops a window or popup the client unmapped.
100+func (c *Core) unmapClient(ctx context.Context, v ports.WindowUnmapped) error {
101+ if c.popups[v.ID] != nil {
102+ if err := c.dropPopup(ctx, v.ID); err != nil {
103+ return err
104+ }
105+ }
106+ if err := c.closePopupsOf(ctx, v.ID); err != nil {
107+ return err
108+ }
109+ c.windows.drop(v.ID)
110+ if c.drag != nil && c.drag.id == v.ID {
111+ c.abortDrag()
112+ }
113+ c.unmapWindow(v)
114+ c.releaseSlots()
115+ if c.pointer == v.ID {
116+ c.pointer = 0
117+ if err := c.command(ctx, ports.PointerFocus{}); err != nil {
118+ return err
119+ }
120+ }
121+ return nil
122+}
123+
124+// fullscreenRequest answers xdg_toplevel set/unset_fullscreen, from the
125+// window itself or a taskbar (External).
126+func (c *Core) fullscreenRequest(ctx context.Context, v ports.WindowFullscreenRequest) error {
127+ c.configures.answer[v.ID] = true
128+ if v.Fullscreen && !v.External && c.now().Sub(c.windows.lookup(v.ID).mappedAt) < fullscreenGrace {
129+ return nil
130+ }
131+ // A taskbar request is a user action on that window, as an
132+ // activation: it comes on screen with the focus, leaving
133+ // another window's fullscreen.
134+ if v.External && v.Fullscreen {
135+ if err := c.activate(ctx, v.ID); err != nil {
136+ return err
137+ }
138+ }
139+ if s, _ := c.screenOf(v.ID); s != nil {
140+ s.mon.SetFullscreen(v.ID, v.Fullscreen)
141+ if w := s.mon.Current(); v.Fullscreen && w.fullscreen == v.ID {
142+ // A taskbar or late fullscreen request that took
143+ // effect: the window does not fade in, it keeps the
144+ // direct scanout path. One that was ignored (the
145+ // grace after the map) leaves the entrance running.
146+ delete(s.rects, v.ID)
147+ }
148+ }
149+ return nil
150+}
151+
152+// activateWorkspaces shows the workspaces of v by stable ID, in order.
153+func (c *Core) activateWorkspaces(ctx context.Context, v ports.WorkspaceActivate) error {
154+ before := c.cur().mon.Current()
155+ for _, id := range v.IDs {
156+ for i, sc := range c.screens {
157+ if sc.name() == "" {
158+ continue
159+ }
160+ for w := range sc.mon.all() {
161+ if w.ID == id {
162+ c.focusScreen = i
163+ sc.mon.show(w)
164+ break
165+ }
166+ }
167+ }
168+ }
169+ return c.workspaceVisible(ctx, c.cur().mon.Current() != before)
170+}
@@ -0,0 +1,170 @@
1+package core
2+
3+import (
4+ "context"
5+
6+ "github.com/bnema/neferwl/internal/ports"
7+)
8+
9+// clientEvent applies one client event. publish asks for a new scene;
10+// stop ends Run (a command could not be delivered).
11+func (c *Core) clientEvent(ctx context.Context, ev ports.ClientEvent) (publish, stop bool) {
12+ switch v := ev.(type) {
13+ case ports.SessionLockChanged:
14+ if !c.applyLockChanged(v) {
15+ return false, false
16+ }
17+ case ports.CaptureSessionOpen:
18+ c.captureOpen(v)
19+ case ports.CaptureSessionClose:
20+ c.captureClose(v.ID)
21+ case ports.CaptureFrameTaken:
22+ // Nothing new to show (the target flashes already): no scene.
23+ if !c.captureFrame(v) {
24+ return false, false
25+ }
26+ case ports.CaptureExclusionBegin:
27+ c.captureExclusionBegin(v)
28+ case ports.CaptureExclusionLayer:
29+ c.captureExclusionLayer(v)
30+ case ports.CaptureExclusionEnd:
31+ c.captureExclusionEnd(v.Session)
32+ case ports.LayerChanged:
33+ c.layerChanged = true
34+ c.setLayers(v.Layers)
35+ case ports.InputRegionChanged:
36+ c.windows.setRegion(v)
37+ case ports.WindowMapped:
38+ c.mapWindow(v)
39+ case ports.WindowResized:
40+ c.resizeFloating(v)
41+ case ports.PopupRequest:
42+ if err := c.placePopup(ctx, v); err != nil {
43+ return false, true
44+ }
45+ case ports.PopupMapped:
46+ if p := c.popups[v.ID]; p != nil {
47+ p.mapped = true
48+ }
49+ case ports.ShortcutsInhibit:
50+ c.windows.setInhibitShortcuts(v.Window, v.Active)
51+ if err := c.updateInhibit(ctx); err != nil {
52+ return false, true
53+ }
54+ return false, false
55+ case ports.OutputPower:
56+ if i := c.screenIndex(v.Output); i >= 0 && c.screens[i].off == v.On {
57+ c.screens[i].off = !v.On
58+ }
59+ case ports.IdleInhibit:
60+ c.windows.setIdleInhibit(v.Window, v.Active)
61+ c.publishState()
62+ return false, false
63+ case ports.WindowAppID:
64+ c.windows.setAppID(v.ID, v.AppID)
65+ case ports.WindowParent:
66+ if _, w := c.screenOf(v.ID); w != nil {
67+ w.SetDialogParent(v.ID, v.Parent)
68+ }
69+ case ports.WindowUnmapped:
70+ if c.unmapClient(ctx, v) != nil {
71+ return false, true
72+ }
73+ case ports.PointerConstrained:
74+ c.constrained = v
75+ case ports.PointerWarp:
76+ if c.warpPointer(ctx, v) != nil {
77+ return false, true
78+ }
79+ case ports.WindowMoveRequest:
80+ if c.clientDrag(ctx, v) != nil {
81+ return false, true
82+ }
83+ case ports.WindowFullscreenRequest:
84+ if c.fullscreenRequest(ctx, v) != nil {
85+ return false, true
86+ }
87+ case ports.WorkspaceActivate:
88+ if c.activateWorkspaces(ctx, v) != nil {
89+ return false, true
90+ }
91+ case ports.WindowActivate:
92+ if c.activate(ctx, v.ID) != nil {
93+ return false, true
94+ }
95+ }
96+ return true, false
97+}
98+
99+// unmapClient drops a window or popup the client unmapped.
100+func (c *Core) unmapClient(ctx context.Context, v ports.WindowUnmapped) error {
101+ if c.popups[v.ID] != nil {
102+ if err := c.dropPopup(ctx, v.ID); err != nil {
103+ return err
104+ }
105+ }
106+ if err := c.closePopupsOf(ctx, v.ID); err != nil {
107+ return err
108+ }
109+ c.windows.drop(v.ID)
110+ if c.drag != nil && c.drag.id == v.ID {
111+ c.abortDrag()
112+ }
113+ c.unmapWindow(v)
114+ c.releaseSlots()
115+ if c.pointer == v.ID {
116+ c.pointer = 0
117+ if err := c.command(ctx, ports.PointerFocus{}); err != nil {
118+ return err
119+ }
120+ }
121+ return nil
122+}
123+
124+// fullscreenRequest answers xdg_toplevel set/unset_fullscreen, from the
125+// window itself or a taskbar (External).
126+func (c *Core) fullscreenRequest(ctx context.Context, v ports.WindowFullscreenRequest) error {
127+ c.configures.answer[v.ID] = true
128+ if v.Fullscreen && !v.External && c.now().Sub(c.windows.lookup(v.ID).mappedAt) < fullscreenGrace {
129+ return nil
130+ }
131+ // A taskbar request is a user action on that window, as an
132+ // activation: it comes on screen with the focus, leaving
133+ // another window's fullscreen.
134+ if v.External && v.Fullscreen {
135+ if err := c.activate(ctx, v.ID); err != nil {
136+ return err
137+ }
138+ }
139+ if s, _ := c.screenOf(v.ID); s != nil {
140+ s.mon.SetFullscreen(v.ID, v.Fullscreen)
141+ if w := s.mon.Current(); v.Fullscreen && w.fullscreen == v.ID {
142+ // A taskbar or late fullscreen request that took
143+ // effect: the window does not fade in, it keeps the
144+ // direct scanout path. One that was ignored (the
145+ // grace after the map) leaves the entrance running.
146+ delete(s.rects, v.ID)
147+ }
148+ }
149+ return nil
150+}
151+
152+// activateWorkspaces shows the workspaces of v by stable ID, in order.
153+func (c *Core) activateWorkspaces(ctx context.Context, v ports.WorkspaceActivate) error {
154+ before := c.cur().mon.Current()
155+ for _, id := range v.IDs {
156+ for i, sc := range c.screens {
157+ if sc.name() == "" {
158+ continue
159+ }
160+ for w := range sc.mon.all() {
161+ if w.ID == id {
162+ c.focusScreen = i
163+ sc.mon.show(w)
164+ break
165+ }
166+ }
167+ }
168+ }
169+ return c.workspaceVisible(ctx, c.cur().mon.Current() != before)
170+}
M internal/core/core.go +4 -133
@@ -1117,140 +1117,11 @@ func (c *Core) Run(ctx context.Context) error {
11171117 if c.blockProtected(ev) {
11181118 continue
11191119 }
1120- switch v := ev.(type) {
1121- case ports.SessionLockChanged:
1122- if !c.applyLockChanged(v) {
1123- continue
1124- }
1125- case ports.CaptureSessionOpen:
1126- c.captureOpen(v)
1127- case ports.CaptureSessionClose:
1128- c.captureClose(v.ID)
1129- case ports.CaptureFrameTaken:
1130- // Nothing new to show (the target flashes already): no scene.
1131- if !c.captureFrame(v) {
1132- continue
1133- }
1134- case ports.CaptureExclusionBegin:
1135- c.captureExclusionBegin(v)
1136- case ports.CaptureExclusionLayer:
1137- c.captureExclusionLayer(v)
1138- case ports.CaptureExclusionEnd:
1139- c.captureExclusionEnd(v.Session)
1140- case ports.LayerChanged:
1141- c.layerChanged = true
1142- c.setLayers(v.Layers)
1143- case ports.InputRegionChanged:
1144- c.windows.setRegion(v)
1145- case ports.WindowMapped:
1146- c.mapWindow(v)
1147- case ports.WindowResized:
1148- c.resizeFloating(v)
1149- case ports.PopupRequest:
1150- if err := c.placePopup(ctx, v); err != nil {
1151- return nil
1152- }
1153- case ports.PopupMapped:
1154- if p := c.popups[v.ID]; p != nil {
1155- p.mapped = true
1156- }
1157- case ports.ShortcutsInhibit:
1158- c.windows.setInhibitShortcuts(v.Window, v.Active)
1159- if err := c.updateInhibit(ctx); err != nil {
1160- return nil
1161- }
1162- continue
1163- case ports.OutputPower:
1164- if i := c.screenIndex(v.Output); i >= 0 && c.screens[i].off == v.On {
1165- c.screens[i].off = !v.On
1166- }
1167- case ports.IdleInhibit:
1168- c.windows.setIdleInhibit(v.Window, v.Active)
1169- c.publishState()
1170- continue
1171- case ports.WindowAppID:
1172- c.windows.setAppID(v.ID, v.AppID)
1173- case ports.WindowParent:
1174- if _, w := c.screenOf(v.ID); w != nil {
1175- w.SetDialogParent(v.ID, v.Parent)
1176- }
1177- case ports.WindowUnmapped:
1178- if c.popups[v.ID] != nil {
1179- if err := c.dropPopup(ctx, v.ID); err != nil {
1180- return nil
1181- }
1182- }
1183- if err := c.closePopupsOf(ctx, v.ID); err != nil {
1184- return nil
1185- }
1186- c.windows.drop(v.ID)
1187- if c.drag != nil && c.drag.id == v.ID {
1188- c.abortDrag()
1189- }
1190- c.unmapWindow(v)
1191- c.releaseSlots()
1192- if c.pointer == v.ID {
1193- c.pointer = 0
1194- if err := c.command(ctx, ports.PointerFocus{}); err != nil {
1195- return nil
1196- }
1197- }
1198- case ports.PointerConstrained:
1199- c.constrained = v
1200- case ports.PointerWarp:
1201- if c.warpPointer(ctx, v) != nil {
1202- return nil
1203- }
1204- case ports.WindowMoveRequest:
1205- if c.clientDrag(ctx, v) != nil {
1206- return nil
1207- }
1208- case ports.WindowFullscreenRequest:
1209- c.configures.answer[v.ID] = true
1210- if v.Fullscreen && !v.External && c.now().Sub(c.windows.lookup(v.ID).mappedAt) < fullscreenGrace {
1211- break
1212- }
1213- // A taskbar request is a user action on that window, as an
1214- // activation: it comes on screen with the focus, leaving
1215- // another window's fullscreen.
1216- if v.External && v.Fullscreen && c.activate(ctx, v.ID) != nil {
1217- return nil
1218- }
1219- if s, _ := c.screenOf(v.ID); s != nil {
1220- s.mon.SetFullscreen(v.ID, v.Fullscreen)
1221- if w := s.mon.Current(); v.Fullscreen && w.fullscreen == v.ID {
1222- // A taskbar or late fullscreen request that took
1223- // effect: the window does not fade in, it keeps the
1224- // direct scanout path. One that was ignored (the
1225- // grace after the map) leaves the entrance running.
1226- delete(s.rects, v.ID)
1227- }
1228- }
1229- case ports.WorkspaceActivate:
1230- before := c.cur().mon.Current()
1231- for _, id := range v.IDs {
1232- for i, sc := range c.screens {
1233- if sc.name() == "" {
1234- continue
1235- }
1236- for w := range sc.mon.all() {
1237- if w.ID == id {
1238- c.focusScreen = i
1239- sc.mon.show(w)
1240- break
1241- }
1242- }
1243- }
1244- }
1245- if c.workspaceVisible(ctx, c.cur().mon.Current() != before) != nil {
1246- return nil
1247- }
1248- case ports.WindowActivate:
1249- if c.activate(ctx, v.ID) != nil {
1250- return nil
1251- }
1120+ publish, stop := c.clientEvent(ctx, ev)
1121+ if stop {
1122+ return nil
12521123 }
1253- if err := c.publish(ctx); err != nil {
1124+ if publish && c.publish(ctx) != nil {
12541125 return nil
12551126 }
12561127 case f, ok := <-c.ch.Frames:
@@ -1117,140 +1117,11 @@ func (c *Core) Run(ctx context.Context) error {
1117 if c.blockProtected(ev) {1117 if c.blockProtected(ev) {
1118 continue1118 continue
1119 }1119 }
1120- switch v := ev.(type) {1120+ publish, stop := c.clientEvent(ctx, ev)
1121- case ports.SessionLockChanged:1121+ if stop {
1122- if !c.applyLockChanged(v) {1122+ return nil
1123- continue
1124- }
1125- case ports.CaptureSessionOpen:
1126- c.captureOpen(v)
1127- case ports.CaptureSessionClose:
1128- c.captureClose(v.ID)
1129- case ports.CaptureFrameTaken:
1130- // Nothing new to show (the target flashes already): no scene.
1131- if !c.captureFrame(v) {
1132- continue
1133- }
1134- case ports.CaptureExclusionBegin:
1135- c.captureExclusionBegin(v)
1136- case ports.CaptureExclusionLayer:
1137- c.captureExclusionLayer(v)
1138- case ports.CaptureExclusionEnd:
1139- c.captureExclusionEnd(v.Session)
1140- case ports.LayerChanged:
1141- c.layerChanged = true
1142- c.setLayers(v.Layers)
1143- case ports.InputRegionChanged:
1144- c.windows.setRegion(v)
1145- case ports.WindowMapped:
1146- c.mapWindow(v)
1147- case ports.WindowResized:
1148- c.resizeFloating(v)
1149- case ports.PopupRequest:
1150- if err := c.placePopup(ctx, v); err != nil {
1151- return nil
1152- }
1153- case ports.PopupMapped:
1154- if p := c.popups[v.ID]; p != nil {
1155- p.mapped = true
1156- }
1157- case ports.ShortcutsInhibit:
1158- c.windows.setInhibitShortcuts(v.Window, v.Active)
1159- if err := c.updateInhibit(ctx); err != nil {
1160- return nil
1161- }
1162- continue
1163- case ports.OutputPower:
1164- if i := c.screenIndex(v.Output); i >= 0 && c.screens[i].off == v.On {
1165- c.screens[i].off = !v.On
1166- }
1167- case ports.IdleInhibit:
1168- c.windows.setIdleInhibit(v.Window, v.Active)
1169- c.publishState()
1170- continue
1171- case ports.WindowAppID:
1172- c.windows.setAppID(v.ID, v.AppID)
1173- case ports.WindowParent:
1174- if _, w := c.screenOf(v.ID); w != nil {
1175- w.SetDialogParent(v.ID, v.Parent)
1176- }
1177- case ports.WindowUnmapped:
1178- if c.popups[v.ID] != nil {
1179- if err := c.dropPopup(ctx, v.ID); err != nil {
1180- return nil
1181- }
1182- }
1183- if err := c.closePopupsOf(ctx, v.ID); err != nil {
1184- return nil
1185- }
1186- c.windows.drop(v.ID)
1187- if c.drag != nil && c.drag.id == v.ID {
1188- c.abortDrag()
1189- }
1190- c.unmapWindow(v)
1191- c.releaseSlots()
1192- if c.pointer == v.ID {
1193- c.pointer = 0
1194- if err := c.command(ctx, ports.PointerFocus{}); err != nil {
1195- return nil
1196- }
1197- }
1198- case ports.PointerConstrained:
1199- c.constrained = v
1200- case ports.PointerWarp:
1201- if c.warpPointer(ctx, v) != nil {
1202- return nil
1203- }
1204- case ports.WindowMoveRequest:
1205- if c.clientDrag(ctx, v) != nil {
1206- return nil
1207- }
1208- case ports.WindowFullscreenRequest:
1209- c.configures.answer[v.ID] = true
1210- if v.Fullscreen && !v.External && c.now().Sub(c.windows.lookup(v.ID).mappedAt) < fullscreenGrace {
1211- break
1212- }
1213- // A taskbar request is a user action on that window, as an
1214- // activation: it comes on screen with the focus, leaving
1215- // another window's fullscreen.
1216- if v.External && v.Fullscreen && c.activate(ctx, v.ID) != nil {
1217- return nil
1218- }
1219- if s, _ := c.screenOf(v.ID); s != nil {
1220- s.mon.SetFullscreen(v.ID, v.Fullscreen)
1221- if w := s.mon.Current(); v.Fullscreen && w.fullscreen == v.ID {
1222- // A taskbar or late fullscreen request that took
1223- // effect: the window does not fade in, it keeps the
1224- // direct scanout path. One that was ignored (the
1225- // grace after the map) leaves the entrance running.
1226- delete(s.rects, v.ID)
1227- }
1228- }
1229- case ports.WorkspaceActivate:
1230- before := c.cur().mon.Current()
1231- for _, id := range v.IDs {
1232- for i, sc := range c.screens {
1233- if sc.name() == "" {
1234- continue
1235- }
1236- for w := range sc.mon.all() {
1237- if w.ID == id {
1238- c.focusScreen = i
1239- sc.mon.show(w)
1240- break
1241- }
1242- }
1243- }
1244- }
1245- if c.workspaceVisible(ctx, c.cur().mon.Current() != before) != nil {
1246- return nil
1247- }
1248- case ports.WindowActivate:
1249- if c.activate(ctx, v.ID) != nil {
1250- return nil
1251- }
1252 }1123 }
1253- if err := c.publish(ctx); err != nil {1124+ if publish && c.publish(ctx) != nil {
1254 return nil1125 return nil
1255 }1126 }
1256 case f, ok := <-c.ch.Frames:1127 case f, ok := <-c.ch.Frames:

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