refactor(app): make session dependencies explicit
- commit
- 3d282fb99110359b9f981711523d20630d66380b
- author
- brice · · 2026-10-06 07:06 UTC
- parent
- c52e379
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
54 files · +653 -487
M .hexcheck.yaml +1 -1
| @@ -28,4 +28,4 @@ ruleSettings: | |||
| 28 | 28 | excludePaths: [internal/ports/client_events.go, internal/ports/client_commands.go, internal/ports/input_events.go, internal/ports/output_events.go, internal/ports/lease.go, internal/ports/session_lock_messages.go, internal/ports/session_backend.go] | |
| 29 | 29 | suspicious-business-logic-in-adapter: | |
| 30 | 30 | # app is the composition root; starting core and wiring channels is its job. | |
| 31 | - | excludePaths: [internal/app/app.go] | |
| 31 | + | excludePaths: [internal/app/app.go, internal/app/session.go] | |
| @@ -28,4 +28,4 @@ ruleSettings: | |||||
| 28 | excludePaths: [internal/ports/client_events.go, internal/ports/client_commands.go, internal/ports/input_events.go, internal/ports/output_events.go, internal/ports/lease.go, internal/ports/session_lock_messages.go, internal/ports/session_backend.go] | 28 | excludePaths: [internal/ports/client_events.go, internal/ports/client_commands.go, internal/ports/input_events.go, internal/ports/output_events.go, internal/ports/lease.go, internal/ports/session_lock_messages.go, internal/ports/session_backend.go] | ||
| 29 | suspicious-business-logic-in-adapter: | 29 | suspicious-business-logic-in-adapter: | ||
| 30 | # app is the composition root; starting core and wiring channels is its job. | 30 | # app is the composition root; starting core and wiring channels is its job. | ||
| 31 | - | excludePaths: [internal/app/app.go] | 31 | + | excludePaths: [internal/app/app.go, internal/app/session.go] |
M internal/adapters/launcher/launcher.go +6 -6
| @@ -16,14 +16,14 @@ import ( | |||
| 16 | 16 | ) | |
| 17 | 17 | ||
| 18 | 18 | type Launcher struct { | |
| 19 | - | // Security is the optional defensive admission gate. Set before Run. | |
| 20 | - | Security ports.SessionSecurity | |
| 19 | + | // security is the optional defensive admission gate, supplied at construction. | |
| 20 | + | security ports.SessionSecurity | |
| 21 | 21 | env []string | |
| 22 | 22 | log zerowrap.Logger | |
| 23 | 23 | } | |
| 24 | 24 | ||
| 25 | - | func New(env []string, log zerowrap.Logger) *Launcher { | |
| 26 | - | return &Launcher{env: append([]string(nil), env...), log: log} | |
| 25 | + | func New(env []string, security ports.SessionSecurity, log zerowrap.Logger) *Launcher { | |
| 26 | + | return &Launcher{env: append([]string(nil), env...), security: security, log: log} | |
| 27 | 27 | } | |
| 28 | 28 | ||
| 29 | 29 | func ChildEnv(base []string, waylandDisplay, runtimeDir string, cursorSize int) []string { | |
| @@ -101,10 +101,10 @@ func (l *Launcher) Run(ctx context.Context, reqs <-chan ports.SpawnRequest) erro | |||
| 101 | 101 | } | |
| 102 | 102 | ||
| 103 | 103 | func (l *Launcher) admitted(req ports.SpawnRequest) bool { | |
| 104 | - | if l.Security == nil { | |
| 104 | + | if l.security == nil { | |
| 105 | 105 | return true | |
| 106 | 106 | } | |
| 107 | - | state := l.Security.Snapshot() | |
| 107 | + | state := l.security.Snapshot() | |
| 108 | 108 | return !state.Protected && req.Security == state | |
| 109 | 109 | } | |
| 110 | 110 | ||
| @@ -16,14 +16,14 @@ import ( | |||||
| 16 | ) | 16 | ) | ||
| 17 | 17 | ||||
| 18 | type Launcher struct { | 18 | type Launcher struct { | ||
| 19 | - | // Security is the optional defensive admission gate. Set before Run. | 19 | + | // security is the optional defensive admission gate, supplied at construction. |
| 20 | - | Security ports.SessionSecurity | 20 | + | security ports.SessionSecurity |
| 21 | env []string | 21 | env []string | ||
| 22 | log zerowrap.Logger | 22 | log zerowrap.Logger | ||
| 23 | } | 23 | } | ||
| 24 | 24 | ||||
| 25 | - | func New(env []string, log zerowrap.Logger) *Launcher { | 25 | + | func New(env []string, security ports.SessionSecurity, log zerowrap.Logger) *Launcher { |
| 26 | - | return &Launcher{env: append([]string(nil), env...), log: log} | 26 | + | return &Launcher{env: append([]string(nil), env...), security: security, log: log} |
| 27 | } | 27 | } | ||
| 28 | 28 | ||||
| 29 | func ChildEnv(base []string, waylandDisplay, runtimeDir string, cursorSize int) []string { | 29 | func ChildEnv(base []string, waylandDisplay, runtimeDir string, cursorSize int) []string { | ||
| @@ -101,10 +101,10 @@ func (l *Launcher) Run(ctx context.Context, reqs <-chan ports.SpawnRequest) erro | |||||
| 101 | } | 101 | } | ||
| 102 | 102 | ||||
| 103 | func (l *Launcher) admitted(req ports.SpawnRequest) bool { | 103 | func (l *Launcher) admitted(req ports.SpawnRequest) bool { | ||
| 104 | - | if l.Security == nil { | 104 | + | if l.security == nil { |
| 105 | return true | 105 | return true | ||
| 106 | } | 106 | } | ||
| 107 | - | state := l.Security.Snapshot() | 107 | + | state := l.security.Snapshot() |
| 108 | return !state.Protected && req.Security == state | 108 | return !state.Protected && req.Security == state | ||
| 109 | } | 109 | } | ||
| 110 | 110 | ||||
M internal/adapters/launcher/launcher_test.go +2 -2
| @@ -35,7 +35,7 @@ func TestRun(t *testing.T) { | |||
| 35 | 35 | done := make(chan error, 1) | |
| 36 | 36 | reqs := make(chan ports.SpawnRequest, 3) | |
| 37 | 37 | go func() { | |
| 38 | - | done <- New([]string{"PATH=/bin", "FILE=" + file}, logging.For(ctx, "launcher")).Run(ctx, reqs) | |
| 38 | + | done <- New([]string{"PATH=/bin", "FILE=" + file}, nil, logging.For(ctx, "launcher")).Run(ctx, reqs) | |
| 39 | 39 | }() | |
| 40 | 40 | reqs <- ports.SpawnRequest{} | |
| 41 | 41 | reqs <- ports.SpawnRequest{Argv: []string{"nonexistent-neferwl-binary"}} | |
| @@ -78,7 +78,7 @@ func TestChildPath(t *testing.T) { | |||
| 78 | 78 | defer cancel() | |
| 79 | 79 | reqs := make(chan ports.SpawnRequest, 1) | |
| 80 | 80 | done := make(chan error, 1) | |
| 81 | - | go func() { done <- New([]string{"PATH=" + dir}, logging.For(ctx, "launcher")).Run(ctx, reqs) }() | |
| 81 | + | go func() { done <- New([]string{"PATH=" + dir}, nil, logging.For(ctx, "launcher")).Run(ctx, reqs) }() | |
| 82 | 82 | reqs <- ports.SpawnRequest{Argv: []string{name, result}} | |
| 83 | 83 | deadline := time.After(3 * time.Second) | |
| 84 | 84 | for { | |
| @@ -35,7 +35,7 @@ func TestRun(t *testing.T) { | |||||
| 35 | done := make(chan error, 1) | 35 | done := make(chan error, 1) | ||
| 36 | reqs := make(chan ports.SpawnRequest, 3) | 36 | reqs := make(chan ports.SpawnRequest, 3) | ||
| 37 | go func() { | 37 | go func() { | ||
| 38 | - | done <- New([]string{"PATH=/bin", "FILE=" + file}, logging.For(ctx, "launcher")).Run(ctx, reqs) | 38 | + | done <- New([]string{"PATH=/bin", "FILE=" + file}, nil, logging.For(ctx, "launcher")).Run(ctx, reqs) |
| 39 | }() | 39 | }() | ||
| 40 | reqs <- ports.SpawnRequest{} | 40 | reqs <- ports.SpawnRequest{} | ||
| 41 | reqs <- ports.SpawnRequest{Argv: []string{"nonexistent-neferwl-binary"}} | 41 | reqs <- ports.SpawnRequest{Argv: []string{"nonexistent-neferwl-binary"}} | ||
| @@ -78,7 +78,7 @@ func TestChildPath(t *testing.T) { | |||||
| 78 | defer cancel() | 78 | defer cancel() | ||
| 79 | reqs := make(chan ports.SpawnRequest, 1) | 79 | reqs := make(chan ports.SpawnRequest, 1) | ||
| 80 | done := make(chan error, 1) | 80 | done := make(chan error, 1) | ||
| 81 | - | go func() { done <- New([]string{"PATH=" + dir}, logging.For(ctx, "launcher")).Run(ctx, reqs) }() | 81 | + | go func() { done <- New([]string{"PATH=" + dir}, nil, logging.For(ctx, "launcher")).Run(ctx, reqs) }() |
| 82 | reqs <- ports.SpawnRequest{Argv: []string{name, result}} | 82 | reqs <- ports.SpawnRequest{Argv: []string{name, result}} | ||
| 83 | deadline := time.After(3 * time.Second) | 83 | deadline := time.After(3 * time.Second) | ||
| 84 | for { | 84 | for { | ||
M internal/adapters/launcher/security_test.go +2 -4
| @@ -14,8 +14,7 @@ import ( | |||
| 14 | 14 | ||
| 15 | 15 | func TestSpawnAdmissionRequiresCurrentUnlockedEpoch(t *testing.T) { | |
| 16 | 16 | var gate sessionsecurity.Gate | |
| 17 | - | l := New(nil, logging.For(context.Background(), "launcher")) | |
| 18 | - | l.Security = &gate | |
| 17 | + | l := New(nil, &gate, logging.For(context.Background(), "launcher")) | |
| 19 | 18 | if !l.admitted(ports.SpawnRequest{}) { | |
| 20 | 19 | t.Fatal("initial unlocked request refused") | |
| 21 | 20 | } | |
| @@ -43,8 +42,7 @@ func TestSpawnRechecksBeforeProcessStart(t *testing.T) { | |||
| 43 | 42 | security := portsmocks.NewMockSessionSecurity(t) | |
| 44 | 43 | security.EXPECT().Snapshot().Return(ports.SecurityState{}).Once() | |
| 45 | 44 | security.EXPECT().Snapshot().Return(ports.SecurityState{Generation: 1, Protected: true}).Once() | |
| 46 | - | l := New([]string{"PATH=/bin"}, logging.For(ctx, "launcher")) | |
| 47 | - | l.Security = security | |
| 45 | + | l := New([]string{"PATH=/bin"}, security, logging.For(ctx, "launcher")) | |
| 48 | 46 | result := filepath.Join(t.TempDir(), "must-not-exist") | |
| 49 | 47 | requests := make(chan ports.SpawnRequest, 1) | |
| 50 | 48 | requests <- ports.SpawnRequest{Argv: []string{"touch", result}} | |
| @@ -14,8 +14,7 @@ import ( | |||||
| 14 | 14 | ||||
| 15 | func TestSpawnAdmissionRequiresCurrentUnlockedEpoch(t *testing.T) { | 15 | func TestSpawnAdmissionRequiresCurrentUnlockedEpoch(t *testing.T) { | ||
| 16 | var gate sessionsecurity.Gate | 16 | var gate sessionsecurity.Gate | ||
| 17 | - | l := New(nil, logging.For(context.Background(), "launcher")) | 17 | + | l := New(nil, &gate, logging.For(context.Background(), "launcher")) |
| 18 | - | l.Security = &gate | |||
| 19 | if !l.admitted(ports.SpawnRequest{}) { | 18 | if !l.admitted(ports.SpawnRequest{}) { | ||
| 20 | t.Fatal("initial unlocked request refused") | 19 | t.Fatal("initial unlocked request refused") | ||
| 21 | } | 20 | } | ||
| @@ -43,8 +42,7 @@ func TestSpawnRechecksBeforeProcessStart(t *testing.T) { | |||||
| 43 | security := portsmocks.NewMockSessionSecurity(t) | 42 | security := portsmocks.NewMockSessionSecurity(t) | ||
| 44 | security.EXPECT().Snapshot().Return(ports.SecurityState{}).Once() | 43 | security.EXPECT().Snapshot().Return(ports.SecurityState{}).Once() | ||
| 45 | security.EXPECT().Snapshot().Return(ports.SecurityState{Generation: 1, Protected: true}).Once() | 44 | security.EXPECT().Snapshot().Return(ports.SecurityState{Generation: 1, Protected: true}).Once() | ||
| 46 | - | l := New([]string{"PATH=/bin"}, logging.For(ctx, "launcher")) | 45 | + | l := New([]string{"PATH=/bin"}, security, logging.For(ctx, "launcher")) |
| 47 | - | l.Security = security | |||
| 48 | result := filepath.Join(t.TempDir(), "must-not-exist") | 46 | result := filepath.Join(t.TempDir(), "must-not-exist") | ||
| 49 | requests := make(chan ports.SpawnRequest, 1) | 47 | requests := make(chan ports.SpawnRequest, 1) | ||
| 50 | requests <- ports.SpawnRequest{Argv: []string{"touch", result}} | 48 | requests <- ports.SpawnRequest{Argv: []string{"touch", result}} | ||
M internal/app/app.go +2 -293
| @@ -2,34 +2,14 @@ | |||
| 2 | 2 | package app | |
| 3 | 3 | ||
| 4 | 4 | import ( | |
| 5 | - | "bufio" | |
| 6 | 5 | "context" | |
| 7 | - | "errors" | |
| 8 | 6 | "fmt" | |
| 9 | 7 | "io" | |
| 10 | - | "os" | |
| 11 | - | "runtime" | |
| 12 | - | "sync" | |
| 13 | 8 | "time" | |
| 14 | 9 | ||
| 15 | - | "github.com/bnema/neferwl/internal/adapters/captureallow" | |
| 16 | - | "github.com/bnema/neferwl/internal/adapters/clock" | |
| 17 | - | "github.com/bnema/neferwl/internal/adapters/config" | |
| 18 | - | "github.com/bnema/neferwl/internal/adapters/drm" | |
| 19 | - | "github.com/bnema/neferwl/internal/adapters/headlessinput" | |
| 20 | - | "github.com/bnema/neferwl/internal/adapters/launcher" | |
| 21 | - | "github.com/bnema/neferwl/internal/adapters/libinput" | |
| 22 | 10 | "github.com/bnema/neferwl/internal/adapters/logging" | |
| 23 | - | "github.com/bnema/neferwl/internal/adapters/sched" | |
| 24 | - | "github.com/bnema/neferwl/internal/adapters/screensaver" | |
| 25 | - | "github.com/bnema/neferwl/internal/adapters/sessionsecurity" | |
| 26 | - | "github.com/bnema/neferwl/internal/adapters/statefile" | |
| 27 | - | "github.com/bnema/neferwl/internal/adapters/vulkan" | |
| 28 | - | "github.com/bnema/neferwl/internal/adapters/wayland" | |
| 29 | - | "github.com/bnema/neferwl/internal/adapters/workspaceid" | |
| 30 | 11 | "github.com/bnema/neferwl/internal/adapters/xkb" | |
| 31 | 12 | "github.com/bnema/neferwl/internal/adapters/xwayland" | |
| 32 | - | "github.com/bnema/neferwl/internal/core" | |
| 33 | 13 | "github.com/bnema/neferwl/internal/ports" | |
| 34 | 14 | "github.com/bnema/zerowrap" | |
| 35 | 15 | "golang.org/x/sys/unix" | |
| @@ -78,282 +58,11 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||
| 78 | 58 | ctx, cancel = context.WithCancel(ctx) | |
| 79 | 59 | } | |
| 80 | 60 | defer cancel() | |
| 81 | - | sizes := opts.Sizes | |
| 82 | - | if len(sizes) == 0 { | |
| 83 | - | sizes = [][2]int{{1920, 1080}} | |
| 84 | - | } | |
| 85 | - | var hw *drmBackend | |
| 86 | - | if opts.Backend == "drm" { | |
| 87 | - | var err error | |
| 88 | - | if hw, err = openDRM(ctx, opts.Config); err != nil { | |
| 89 | - | return err | |
| 90 | - | } | |
| 91 | - | defer hw.close() | |
| 92 | - | } | |
| 93 | - | security := &sessionsecurity.Gate{} | |
| 94 | - | p := newPipes(hw != nil) | |
| 95 | - | ch := p.core() | |
| 96 | - | ch.Security = security | |
| 97 | - | ch.Terminal = !opts.NoTerminal | |
| 98 | - | ch.Clock = clock.System{} | |
| 99 | - | c, err := core.New(opts.Config, ch) | |
| 100 | - | if err != nil { | |
| 101 | - | return err | |
| 102 | - | } | |
| 103 | - | km, err := xkb.New(xkb.RMLVO{Layout: opts.Config.Keyboard.Layout, Variant: opts.Config.Keyboard.Variant, Options: opts.Config.Keyboard.Options}) | |
| 104 | - | if err != nil { | |
| 105 | - | return err | |
| 106 | - | } | |
| 107 | - | keymap := km.String() | |
| 108 | - | runtimeDir := os.Getenv("XDG_RUNTIME_DIR") | |
| 109 | - | // Every output renders on the same GPU: its formats are the clients'. | |
| 110 | - | dmabuf := vulkan.Probe() | |
| 111 | - | log.Info().Int("formats", len(dmabuf.Formats)).Msg("dmabuf") | |
| 112 | - | // One launch prefix for ext-workspace ids and the state file. | |
| 113 | - | wsIDs := workspaceid.New() | |
| 114 | - | // The path is fixed: a config key would let a program of the same user | |
| 115 | - | // widen who may capture. | |
| 116 | - | // Production: the fixed path, a root-owned file. Only tests set a path, | |
| 117 | - | // and an owner for it. | |
| 118 | - | captureAllowLog := logging.For(ctx, "captureallow") | |
| 119 | - | var captureAllow *captureallow.Store | |
| 120 | - | if opts.captureAllowPath != "" { | |
| 121 | - | captureAllow = captureallow.NewStoreOwnedBy(opts.captureAllowPath, opts.captureAllowOwner, captureAllowLog) | |
| 122 | - | } else { | |
| 123 | - | captureAllow = captureallow.NewStore(captureallow.DefaultPath, captureAllowLog) | |
| 124 | - | } | |
| 125 | - | server, err := wayland.New(wayland.Options{ | |
| 126 | - | CaptureAllow: captureAllow, | |
| 127 | - | WorkspaceIDs: wsIDs, | |
| 128 | - | Security: security, | |
| 129 | - | RuntimeDir: runtimeDir, | |
| 130 | - | DMABuf: dmabuf, | |
| 131 | - | SyncobjNode: renderNode(dmabuf.Device), | |
| 132 | - | Keymap: keymap, | |
| 133 | - | RepeatRate: opts.Config.Keyboard.RepeatRate, | |
| 134 | - | RepeatDelay: opts.Config.Keyboard.RepeatDelay, | |
| 135 | - | }, p.wayland(), logging.For(ctx, "wayland")) | |
| 61 | + | s, err := assembleSession(ctx, opts) | |
| 136 | 62 | if err != nil { | |
| 137 | - | km.Close() | |
| 138 | 63 | return err | |
| 139 | 64 | } | |
| 140 | - | log.Info().Str("WAYLAND_DISPLAY", server.SocketName()).Msg("listening") | |
| 141 | - | childEnv := launcher.ChildEnv(os.Environ(), server.SocketName(), runtimeDir, cursorSize()) | |
| 142 | - | statePath, err := statefile.Path(runtimeDir, server.SocketName()) | |
| 143 | - | if err != nil { | |
| 144 | - | log.Warn().Err(err).Msg("state file disabled") | |
| 145 | - | } else { | |
| 146 | - | childEnv = append(childEnv, statefile.Env+"="+statePath) | |
| 147 | - | } | |
| 148 | - | xdisplay := openXwayland(ctx, opts, childEnv, logging.For(ctx, "xwayland")) | |
| 149 | - | if xdisplay != nil { | |
| 150 | - | defer xdisplay.Close() | |
| 151 | - | childEnv = append(childEnv, "DISPLAY="+xdisplay.Name()) | |
| 152 | - | } | |
| 153 | - | if opts.Session { | |
| 154 | - | launcher.ExportSession(ctx, childEnv, logging.For(ctx, "launcher")) | |
| 155 | - | launcher.NotifyReady(os.Getenv("NOTIFY_SOCKET"), logging.For(ctx, "launcher")) | |
| 156 | - | defer launcher.UnexportSession(childEnv, logging.For(ctx, "launcher")) | |
| 157 | - | } | |
| 158 | - | child := launcher.New(childEnv, logging.For(ctx, "launcher")) | |
| 159 | - | child.Security = security | |
| 160 | - | if inject != nil { | |
| 161 | - | inject(p.input) | |
| 162 | - | } | |
| 163 | - | var workers sync.WaitGroup | |
| 164 | - | workers.Add(8) | |
| 165 | - | done := make(chan error, 8) | |
| 166 | - | path := opts.ConfigPath | |
| 167 | - | if path == "" { | |
| 168 | - | path = config.DefaultPath() | |
| 169 | - | } | |
| 170 | - | go func() { | |
| 171 | - | defer workers.Done() | |
| 172 | - | done <- config.Watch(ctx, path, p.watched, logging.For(ctx, "config")) | |
| 173 | - | }() | |
| 174 | - | if p.scales != nil { | |
| 175 | - | workers.Add(1) | |
| 176 | - | go func() { | |
| 177 | - | defer workers.Done() | |
| 178 | - | core.PersistScales(ctx, p.scales, config.ScaleStore{Path: path}, launcher.NewNotifier(ctx, childEnv, logging.For(ctx, "launcher")), clock.System{}, time.Second) | |
| 179 | - | }() | |
| 180 | - | } | |
| 181 | - | curs := newCursors(clock.System{}, opts.Config.Cursor.HideAfter) | |
| 182 | - | defer curs.stop() | |
| 183 | - | workers.Add(1) | |
| 184 | - | go func() { | |
| 185 | - | defer workers.Done() | |
| 186 | - | relayConfig(ctx, opts.Config, p.watched, p.filtered, p.keymaps, p.deviceConfigs, curs, p.commands, logging.For(ctx, "config")) | |
| 187 | - | }() | |
| 188 | - | script := make(chan string) | |
| 189 | - | go func() { | |
| 190 | - | defer workers.Done() | |
| 191 | - | if hw != nil { | |
| 192 | - | // Core sends the first layout once DRM reports the outputs. | |
| 193 | - | var layout ports.Layout | |
| 194 | - | select { | |
| 195 | - | case layout = <-p.layouts: | |
| 196 | - | case <-ctx.Done(): | |
| 197 | - | km.Close() | |
| 198 | - | return | |
| 199 | - | } | |
| 200 | - | done <- safe("input", func() error { | |
| 201 | - | if opts.Config.Performance.Realtime { | |
| 202 | - | runtime.LockOSThread() | |
| 203 | - | // Let this dedicated thread exit with its scheduling policy. | |
| 204 | - | schedLog := logging.For(ctx, "sched") | |
| 205 | - | if err := sched.Realtime(schedLog); err != nil { | |
| 206 | - | schedLog.Warn().Err(err).Msg("input scheduling") | |
| 207 | - | } | |
| 208 | - | } | |
| 209 | - | return libinput.Run(ctx, libinput.Options{ | |
| 210 | - | Security: security, | |
| 211 | - | Seat: hw.seat, | |
| 212 | - | SeatName: hw.seat.Name(), | |
| 213 | - | Keymap: km, | |
| 214 | - | Keymaps: p.keymaps, | |
| 215 | - | Layout: layout, | |
| 216 | - | Layouts: p.layouts, | |
| 217 | - | Constraints: p.constraints, | |
| 218 | - | DeviceConfig: opts.Config.InputDevicesConfig, | |
| 219 | - | DeviceConfigs: p.deviceConfigs, | |
| 220 | - | Active: hw.seat.Subscribe(), | |
| 221 | - | MoveCursor: curs.move, | |
| 222 | - | Log: logging.For(ctx, "input"), | |
| 223 | - | LogMotion: logging.Enabled(ctx, "input-motion"), | |
| 224 | - | LogKeys: logging.Enabled(ctx, "input-keys"), | |
| 225 | - | }, p.input) | |
| 226 | - | }) | |
| 227 | - | return | |
| 228 | - | } | |
| 229 | - | if err := headlessinput.RunSecure(ctx, km, p.keymaps, script, p.input, p.layouts, func(o string, x, y float64) { curs.move(o, x, y, true) }, logging.For(ctx, "input"), security); err != nil && !errors.Is(err, context.Canceled) { | |
| 230 | - | select { | |
| 231 | - | case done <- err: | |
| 232 | - | case <-ctx.Done(): | |
| 233 | - | } | |
| 234 | - | } | |
| 235 | - | }() | |
| 236 | - | if opts.Script != nil { | |
| 237 | - | go func() { <-ctx.Done(); _ = opts.Script.Close() }() | |
| 238 | - | go func() { | |
| 239 | - | defer close(script) | |
| 240 | - | scanner := bufio.NewScanner(opts.Script) | |
| 241 | - | for scanner.Scan() { | |
| 242 | - | select { | |
| 243 | - | case <-ctx.Done(): | |
| 244 | - | return | |
| 245 | - | case script <- scanner.Text(): | |
| 246 | - | } | |
| 247 | - | } | |
| 248 | - | if err := scanner.Err(); err != nil { | |
| 249 | - | log.Warn().Err(err).Msg("reading input script") | |
| 250 | - | } | |
| 251 | - | }() | |
| 252 | - | } else { | |
| 253 | - | close(script) | |
| 254 | - | } | |
| 255 | - | go func() { defer workers.Done(); done <- server.Run(ctx) }() | |
| 256 | - | go func() { | |
| 257 | - | defer workers.Done() | |
| 258 | - | // Without a watch the allowlist stays as loaded: restart to apply. | |
| 259 | - | if err := captureAllow.Run(ctx); err != nil { | |
| 260 | - | log.Warn().Err(err).Msg("capture allowlist not watched, restart to apply changes") | |
| 261 | - | } | |
| 262 | - | }() | |
| 263 | - | go func() { defer workers.Done(); done <- child.Run(ctx, p.spawn) }() | |
| 264 | - | go func() { defer workers.Done(); done <- c.Run(ctx) }() | |
| 265 | - | if xdisplay != nil { | |
| 266 | - | workers.Add(1) | |
| 267 | - | // X11 failing leaves the Wayland session running; Run logs why. | |
| 268 | - | go func() { defer workers.Done(); _ = xdisplay.Run(ctx) }() | |
| 269 | - | } | |
| 270 | - | if statePath != "" { | |
| 271 | - | workers.Add(1) | |
| 272 | - | go func() { | |
| 273 | - | defer workers.Done() | |
| 274 | - | if err := statefile.Run(ctx, statePath, p.states, wsIDs, logging.For(ctx, "statefile")); err != nil { | |
| 275 | - | // Scripts lose their state; the session goes on. | |
| 276 | - | log.Warn().Err(err).Msg("state file disabled") | |
| 277 | - | } | |
| 278 | - | }() | |
| 279 | - | } | |
| 280 | - | go func() { | |
| 281 | - | defer workers.Done() | |
| 282 | - | consumeScenes(ctx, p.scenes, p.configErrors, opts.testScenes, p.renderScenes) | |
| 283 | - | }() | |
| 284 | - | workers.Add(1) | |
| 285 | - | go func() { defer workers.Done(); relayPresented(ctx, p.flips, p.presented, p.frames) }() | |
| 286 | - | ||
| 287 | - | go func() { | |
| 288 | - | defer workers.Done() | |
| 289 | - | outputIO := p.outputs(security) | |
| 290 | - | apply := newOutputApply(newOutputOverrides(opts.Config, hw == nil), logging.For(ctx, "app")) | |
| 291 | - | renderLog := logging.For(ctx, "render") | |
| 292 | - | newRenderer := func(w, h int) (ports.Renderer, error) { | |
| 293 | - | r, err := vulkan.New(w, h) | |
| 294 | - | if err != nil { | |
| 295 | - | return nil, err | |
| 296 | - | } | |
| 297 | - | r.SetLogger(renderLog) | |
| 298 | - | renderLog.Info().Str("queue_priority", r.QueuePriority()).Msg("vulkan queue") | |
| 299 | - | return r, nil | |
| 300 | - | } | |
| 301 | - | if hw != nil { | |
| 302 | - | done <- safe("output", func() error { | |
| 303 | - | want := func(cfg ports.Config) drm.Want { | |
| 304 | - | w := wantFromConfig(cfg) | |
| 305 | - | w.Sampled = dmabuf.Formats | |
| 306 | - | w.TraceFlips = logging.Enabled(ctx, "drm-flip") | |
| 307 | - | return w | |
| 308 | - | } | |
| 309 | - | return hw.runOutputs(ctx, want, opts.Config, apply, outputIO, curs, newRenderer, logging.For(ctx, "drm")) | |
| 310 | - | }) | |
| 311 | - | return | |
| 312 | - | } | |
| 313 | - | done <- runHeadless(ctx, headlessOptions{sizes: sizes, shots: opts.ScreenshotDir, hdr: opts.HeadlessHDR, raw: opts.ScreenshotRaw}, apply, outputIO, curs, newRenderer, logging.For(ctx, "render")) | |
| 314 | - | }() | |
| 315 | - | ||
| 316 | - | if hw != nil { | |
| 317 | - | workers.Add(1) | |
| 318 | - | go func() { defer workers.Done(); done <- safe("seat", func() error { return hw.seat.Run(ctx) }) }() | |
| 319 | - | // Only a real session serves the session bus: headless runs would take | |
| 320 | - | // the name from the desktop they run in. | |
| 321 | - | workers.Add(1) | |
| 322 | - | go func() { | |
| 323 | - | defer workers.Done() | |
| 324 | - | reports := screensaver.Reports{Held: p.idleInhibited, Activity: p.idleActivity} | |
| 325 | - | screensaver.Serve(ctx, "", reports, screensaverRetry, logging.For(ctx, "screensaver")) | |
| 326 | - | }() | |
| 327 | - | } | |
| 328 | - | var result error | |
| 329 | - | select { | |
| 330 | - | case <-ctx.Done(): | |
| 331 | - | if errors.Is(context.Cause(ctx), errTimeout) { | |
| 332 | - | log.Info().Str("reason", "timeout").Dur("after", opts.Timeout).Msg("stopping") | |
| 333 | - | } | |
| 334 | - | case result = <-done: | |
| 335 | - | log.Info().AnErr("cause", result).Msg("stopping") | |
| 336 | - | } | |
| 337 | - | cancel() | |
| 338 | - | workers.Wait() | |
| 339 | - | // The wayland server has stopped: no request can be queued any more. | |
| 340 | - | drainCaptures(ctx, p.captures, nil) | |
| 341 | - | // A keymap the input goroutine never took is still ours to free. | |
| 342 | - | select { | |
| 343 | - | case km := <-p.keymaps: | |
| 344 | - | km.Close() | |
| 345 | - | default: | |
| 346 | - | } | |
| 347 | - | for len(done) > 0 { | |
| 348 | - | err := <-done | |
| 349 | - | if result == nil && err != nil { | |
| 350 | - | result = err | |
| 351 | - | } | |
| 352 | - | } | |
| 353 | - | if errors.Is(result, core.ErrQuit) || errors.Is(result, libinput.ErrEmergencyQuit) { | |
| 354 | - | return nil | |
| 355 | - | } | |
| 356 | - | return result | |
| 65 | + | return s.run(ctx, inject) | |
| 357 | 66 | } | |
| 358 | 67 | ||
| 359 | 68 | // screensaverRetry is the first delay before the screensaver service tries | |
| @@ -2,34 +2,14 @@ | |||||
| 2 | package app | 2 | package app | ||
| 3 | 3 | ||||
| 4 | import ( | 4 | import ( | ||
| 5 | - | "bufio" | |||
| 6 | "context" | 5 | "context" | ||
| 7 | - | "errors" | |||
| 8 | "fmt" | 6 | "fmt" | ||
| 9 | "io" | 7 | "io" | ||
| 10 | - | "os" | |||
| 11 | - | "runtime" | |||
| 12 | - | "sync" | |||
| 13 | "time" | 8 | "time" | ||
| 14 | 9 | ||||
| 15 | - | "github.com/bnema/neferwl/internal/adapters/captureallow" | |||
| 16 | - | "github.com/bnema/neferwl/internal/adapters/clock" | |||
| 17 | - | "github.com/bnema/neferwl/internal/adapters/config" | |||
| 18 | - | "github.com/bnema/neferwl/internal/adapters/drm" | |||
| 19 | - | "github.com/bnema/neferwl/internal/adapters/headlessinput" | |||
| 20 | - | "github.com/bnema/neferwl/internal/adapters/launcher" | |||
| 21 | - | "github.com/bnema/neferwl/internal/adapters/libinput" | |||
| 22 | "github.com/bnema/neferwl/internal/adapters/logging" | 10 | "github.com/bnema/neferwl/internal/adapters/logging" | ||
| 23 | - | "github.com/bnema/neferwl/internal/adapters/sched" | |||
| 24 | - | "github.com/bnema/neferwl/internal/adapters/screensaver" | |||
| 25 | - | "github.com/bnema/neferwl/internal/adapters/sessionsecurity" | |||
| 26 | - | "github.com/bnema/neferwl/internal/adapters/statefile" | |||
| 27 | - | "github.com/bnema/neferwl/internal/adapters/vulkan" | |||
| 28 | - | "github.com/bnema/neferwl/internal/adapters/wayland" | |||
| 29 | - | "github.com/bnema/neferwl/internal/adapters/workspaceid" | |||
| 30 | "github.com/bnema/neferwl/internal/adapters/xkb" | 11 | "github.com/bnema/neferwl/internal/adapters/xkb" | ||
| 31 | "github.com/bnema/neferwl/internal/adapters/xwayland" | 12 | "github.com/bnema/neferwl/internal/adapters/xwayland" | ||
| 32 | - | "github.com/bnema/neferwl/internal/core" | |||
| 33 | "github.com/bnema/neferwl/internal/ports" | 13 | "github.com/bnema/neferwl/internal/ports" | ||
| 34 | "github.com/bnema/zerowrap" | 14 | "github.com/bnema/zerowrap" | ||
| 35 | "golang.org/x/sys/unix" | 15 | "golang.org/x/sys/unix" | ||
| @@ -78,282 +58,11 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent) | |||||
| 78 | ctx, cancel = context.WithCancel(ctx) | 58 | ctx, cancel = context.WithCancel(ctx) | ||
| 79 | } | 59 | } | ||
| 80 | defer cancel() | 60 | defer cancel() | ||
| 81 | - | sizes := opts.Sizes | 61 | + | s, err := assembleSession(ctx, opts) |
| 82 | - | if len(sizes) == 0 { | |||
| 83 | - | sizes = [][2]int{{1920, 1080}} | |||
| 84 | - | } | |||
| 85 | - | var hw *drmBackend | |||
| 86 | - | if opts.Backend == "drm" { | |||
| 87 | - | var err error | |||
| 88 | - | if hw, err = openDRM(ctx, opts.Config); err != nil { | |||
| 89 | - | return err | |||
| 90 | - | } | |||
| 91 | - | defer hw.close() | |||
| 92 | - | } | |||
| 93 | - | security := &sessionsecurity.Gate{} | |||
| 94 | - | p := newPipes(hw != nil) | |||
| 95 | - | ch := p.core() | |||
| 96 | - | ch.Security = security | |||
| 97 | - | ch.Terminal = !opts.NoTerminal | |||
| 98 | - | ch.Clock = clock.System{} | |||
| 99 | - | c, err := core.New(opts.Config, ch) | |||
| 100 | - | if err != nil { | |||
| 101 | - | return err | |||
| 102 | - | } | |||
| 103 | - | km, err := xkb.New(xkb.RMLVO{Layout: opts.Config.Keyboard.Layout, Variant: opts.Config.Keyboard.Variant, Options: opts.Config.Keyboard.Options}) | |||
| 104 | - | if err != nil { | |||
| 105 | - | return err | |||
| 106 | - | } | |||
| 107 | - | keymap := km.String() | |||
| 108 | - | runtimeDir := os.Getenv("XDG_RUNTIME_DIR") | |||
| 109 | - | // Every output renders on the same GPU: its formats are the clients'. | |||
| 110 | - | dmabuf := vulkan.Probe() | |||
| 111 | - | log.Info().Int("formats", len(dmabuf.Formats)).Msg("dmabuf") | |||
| 112 | - | // One launch prefix for ext-workspace ids and the state file. | |||
| 113 | - | wsIDs := workspaceid.New() | |||
| 114 | - | // The path is fixed: a config key would let a program of the same user | |||
| 115 | - | // widen who may capture. | |||
| 116 | - | // Production: the fixed path, a root-owned file. Only tests set a path, | |||
| 117 | - | // and an owner for it. | |||
| 118 | - | captureAllowLog := logging.For(ctx, "captureallow") | |||
| 119 | - | var captureAllow *captureallow.Store | |||
| 120 | - | if opts.captureAllowPath != "" { | |||
| 121 | - | captureAllow = captureallow.NewStoreOwnedBy(opts.captureAllowPath, opts.captureAllowOwner, captureAllowLog) | |||
| 122 | - | } else { | |||
| 123 | - | captureAllow = captureallow.NewStore(captureallow.DefaultPath, captureAllowLog) | |||
| 124 | - | } | |||
| 125 | - | server, err := wayland.New(wayland.Options{ | |||
| 126 | - | CaptureAllow: captureAllow, | |||
| 127 | - | WorkspaceIDs: wsIDs, | |||
| 128 | - | Security: security, | |||
| 129 | - | RuntimeDir: runtimeDir, | |||
| 130 | - | DMABuf: dmabuf, | |||
| 131 | - | SyncobjNode: renderNode(dmabuf.Device), | |||
| 132 | - | Keymap: keymap, | |||
| 133 | - | RepeatRate: opts.Config.Keyboard.RepeatRate, | |||
| 134 | - | RepeatDelay: opts.Config.Keyboard.RepeatDelay, | |||
| 135 | - | }, p.wayland(), logging.For(ctx, "wayland")) | |||
| 136 | if err != nil { | 62 | if err != nil { | ||
| 137 | - | km.Close() | |||
| 138 | return err | 63 | return err | ||
| 139 | } | 64 | } | ||
| 140 | - | log.Info().Str("WAYLAND_DISPLAY", server.SocketName()).Msg("listening") | 65 | + | return s.run(ctx, inject) |
| 141 | - | childEnv := launcher.ChildEnv(os.Environ(), server.SocketName(), runtimeDir, cursorSize()) | |||
| 142 | - | statePath, err := statefile.Path(runtimeDir, server.SocketName()) | |||
| 143 | - | if err != nil { | |||
| 144 | - | log.Warn().Err(err).Msg("state file disabled") | |||
| 145 | - | } else { | |||
| 146 | - | childEnv = append(childEnv, statefile.Env+"="+statePath) | |||
| 147 | - | } | |||
| 148 | - | xdisplay := openXwayland(ctx, opts, childEnv, logging.For(ctx, "xwayland")) | |||
| 149 | - | if xdisplay != nil { | |||
| 150 | - | defer xdisplay.Close() | |||
| 151 | - | childEnv = append(childEnv, "DISPLAY="+xdisplay.Name()) | |||
| 152 | - | } | |||
| 153 | - | if opts.Session { | |||
| 154 | - | launcher.ExportSession(ctx, childEnv, logging.For(ctx, "launcher")) | |||
| 155 | - | launcher.NotifyReady(os.Getenv("NOTIFY_SOCKET"), logging.For(ctx, "launcher")) | |||
| 156 | - | defer launcher.UnexportSession(childEnv, logging.For(ctx, "launcher")) | |||
| 157 | - | } | |||
| 158 | - | child := launcher.New(childEnv, logging.For(ctx, "launcher")) | |||
| 159 | - | child.Security = security | |||
| 160 | - | if inject != nil { | |||
| 161 | - | inject(p.input) | |||
| 162 | - | } | |||
| 163 | - | var workers sync.WaitGroup | |||
| 164 | - | workers.Add(8) | |||
| 165 | - | done := make(chan error, 8) | |||
| 166 | - | path := opts.ConfigPath | |||
| 167 | - | if path == "" { | |||
| 168 | - | path = config.DefaultPath() | |||
| 169 | - | } | |||
| 170 | - | go func() { | |||
| 171 | - | defer workers.Done() | |||
| 172 | - | done <- config.Watch(ctx, path, p.watched, logging.For(ctx, "config")) | |||
| 173 | - | }() | |||
| 174 | - | if p.scales != nil { | |||
| 175 | - | workers.Add(1) | |||
| 176 | - | go func() { | |||
| 177 | - | defer workers.Done() | |||
| 178 | - | core.PersistScales(ctx, p.scales, config.ScaleStore{Path: path}, launcher.NewNotifier(ctx, childEnv, logging.For(ctx, "launcher")), clock.System{}, time.Second) | |||
| 179 | - | }() | |||
| 180 | - | } | |||
| 181 | - | curs := newCursors(clock.System{}, opts.Config.Cursor.HideAfter) | |||
| 182 | - | defer curs.stop() | |||
| 183 | - | workers.Add(1) | |||
| 184 | - | go func() { | |||
| 185 | - | defer workers.Done() | |||
| 186 | - | relayConfig(ctx, opts.Config, p.watched, p.filtered, p.keymaps, p.deviceConfigs, curs, p.commands, logging.For(ctx, "config")) | |||
| 187 | - | }() | |||
| 188 | - | script := make(chan string) | |||
| 189 | - | go func() { | |||
| 190 | - | defer workers.Done() | |||
| 191 | - | if hw != nil { | |||
| 192 | - | // Core sends the first layout once DRM reports the outputs. | |||
| 193 | - | var layout ports.Layout | |||
| 194 | - | select { | |||
| 195 | - | case layout = <-p.layouts: | |||
| 196 | - | case <-ctx.Done(): | |||
| 197 | - | km.Close() | |||
| 198 | - | return | |||
| 199 | - | } | |||
| 200 | - | done <- safe("input", func() error { | |||
| 201 | - | if opts.Config.Performance.Realtime { | |||
| 202 | - | runtime.LockOSThread() | |||
| 203 | - | // Let this dedicated thread exit with its scheduling policy. | |||
| 204 | - | schedLog := logging.For(ctx, "sched") | |||
| 205 | - | if err := sched.Realtime(schedLog); err != nil { | |||
| 206 | - | schedLog.Warn().Err(err).Msg("input scheduling") | |||
| 207 | - | } | |||
| 208 | - | } | |||
| 209 | - | return libinput.Run(ctx, libinput.Options{ | |||
| 210 | - | Security: security, | |||
| 211 | - | Seat: hw.seat, | |||
| 212 | - | SeatName: hw.seat.Name(), | |||
| 213 | - | Keymap: km, | |||
| 214 | - | Keymaps: p.keymaps, | |||
| 215 | - | Layout: layout, | |||
| 216 | - | Layouts: p.layouts, | |||
| 217 | - | Constraints: p.constraints, | |||
| 218 | - | DeviceConfig: opts.Config.InputDevicesConfig, | |||
| 219 | - | DeviceConfigs: p.deviceConfigs, | |||
| 220 | - | Active: hw.seat.Subscribe(), | |||
| 221 | - | MoveCursor: curs.move, | |||
| 222 | - | Log: logging.For(ctx, "input"), | |||
| 223 | - | LogMotion: logging.Enabled(ctx, "input-motion"), | |||
| 224 | - | LogKeys: logging.Enabled(ctx, "input-keys"), | |||
| 225 | - | }, p.input) | |||
| 226 | - | }) | |||
| 227 | - | return | |||
| 228 | - | } | |||
| 229 | - | if err := headlessinput.RunSecure(ctx, km, p.keymaps, script, p.input, p.layouts, func(o string, x, y float64) { curs.move(o, x, y, true) }, logging.For(ctx, "input"), security); err != nil && !errors.Is(err, context.Canceled) { | |||
| 230 | - | select { | |||
| 231 | - | case done <- err: | |||
| 232 | - | case <-ctx.Done(): | |||
| 233 | - | } | |||
| 234 | - | } | |||
| 235 | - | }() | |||
| 236 | - | if opts.Script != nil { | |||
| 237 | - | go func() { <-ctx.Done(); _ = opts.Script.Close() }() | |||
| 238 | - | go func() { | |||
| 239 | - | defer close(script) | |||
| 240 | - | scanner := bufio.NewScanner(opts.Script) | |||
| 241 | - | for scanner.Scan() { | |||
| 242 | - | select { | |||
| 243 | - | case <-ctx.Done(): | |||
| 244 | - | return | |||
| 245 | - | case script <- scanner.Text(): | |||
| 246 | - | } | |||
| 247 | - | } | |||
| 248 | - | if err := scanner.Err(); err != nil { | |||
| 249 | - | log.Warn().Err(err).Msg("reading input script") | |||
| 250 | - | } | |||
| 251 | - | }() | |||
| 252 | - | } else { | |||
| 253 | - | close(script) | |||
| 254 | - | } | |||
| 255 | - | go func() { defer workers.Done(); done <- server.Run(ctx) }() | |||
| 256 | - | go func() { | |||
| 257 | - | defer workers.Done() | |||
| 258 | - | // Without a watch the allowlist stays as loaded: restart to apply. | |||
| 259 | - | if err := captureAllow.Run(ctx); err != nil { | |||
| 260 | - | log.Warn().Err(err).Msg("capture allowlist not watched, restart to apply changes") | |||
| 261 | - | } | |||
| 262 | - | }() | |||
| 263 | - | go func() { defer workers.Done(); done <- child.Run(ctx, p.spawn) }() | |||
| 264 | - | go func() { defer workers.Done(); done <- c.Run(ctx) }() | |||
| 265 | - | if xdisplay != nil { | |||
| 266 | - | workers.Add(1) | |||
| 267 | - | // X11 failing leaves the Wayland session running; Run logs why. | |||
| 268 | - | go func() { defer workers.Done(); _ = xdisplay.Run(ctx) }() | |||
| 269 | - | } | |||
| 270 | - | if statePath != "" { | |||
| 271 | - | workers.Add(1) | |||
| 272 | - | go func() { | |||
| 273 | - | defer workers.Done() | |||
| 274 | - | if err := statefile.Run(ctx, statePath, p.states, wsIDs, logging.For(ctx, "statefile")); err != nil { | |||
| 275 | - | // Scripts lose their state; the session goes on. | |||
| 276 | - | log.Warn().Err(err).Msg("state file disabled") | |||
| 277 | - | } | |||
| 278 | - | }() | |||
| 279 | - | } | |||
| 280 | - | go func() { | |||
| 281 | - | defer workers.Done() | |||
| 282 | - | consumeScenes(ctx, p.scenes, p.configErrors, opts.testScenes, p.renderScenes) | |||
| 283 | - | }() | |||
| 284 | - | workers.Add(1) | |||
| 285 | - | go func() { defer workers.Done(); relayPresented(ctx, p.flips, p.presented, p.frames) }() | |||
| 286 | - | ||||
| 287 | - | go func() { | |||
| 288 | - | defer workers.Done() | |||
| 289 | - | outputIO := p.outputs(security) | |||
| 290 | - | apply := newOutputApply(newOutputOverrides(opts.Config, hw == nil), logging.For(ctx, "app")) | |||
| 291 | - | renderLog := logging.For(ctx, "render") | |||
| 292 | - | newRenderer := func(w, h int) (ports.Renderer, error) { | |||
| 293 | - | r, err := vulkan.New(w, h) | |||
| 294 | - | if err != nil { | |||
| 295 | - | return nil, err | |||
| 296 | - | } | |||
| 297 | - | r.SetLogger(renderLog) | |||
| 298 | - | renderLog.Info().Str("queue_priority", r.QueuePriority()).Msg("vulkan queue") | |||
| 299 | - | return r, nil | |||
| 300 | - | } | |||
| 301 | - | if hw != nil { | |||
| 302 | - | done <- safe("output", func() error { | |||
| 303 | - | want := func(cfg ports.Config) drm.Want { | |||
| 304 | - | w := wantFromConfig(cfg) | |||
| 305 | - | w.Sampled = dmabuf.Formats | |||
| 306 | - | w.TraceFlips = logging.Enabled(ctx, "drm-flip") | |||
| 307 | - | return w | |||
| 308 | - | } | |||
| 309 | - | return hw.runOutputs(ctx, want, opts.Config, apply, outputIO, curs, newRenderer, logging.For(ctx, "drm")) | |||
| 310 | - | }) | |||
| 311 | - | return | |||
| 312 | - | } | |||
| 313 | - | done <- runHeadless(ctx, headlessOptions{sizes: sizes, shots: opts.ScreenshotDir, hdr: opts.HeadlessHDR, raw: opts.ScreenshotRaw}, apply, outputIO, curs, newRenderer, logging.For(ctx, "render")) | |||
| 314 | - | }() | |||
| 315 | - | ||||
| 316 | - | if hw != nil { | |||
| 317 | - | workers.Add(1) | |||
| 318 | - | go func() { defer workers.Done(); done <- safe("seat", func() error { return hw.seat.Run(ctx) }) }() | |||
| 319 | - | // Only a real session serves the session bus: headless runs would take | |||
| 320 | - | // the name from the desktop they run in. | |||
| 321 | - | workers.Add(1) | |||
| 322 | - | go func() { | |||
| 323 | - | defer workers.Done() | |||
| 324 | - | reports := screensaver.Reports{Held: p.idleInhibited, Activity: p.idleActivity} | |||
| 325 | - | screensaver.Serve(ctx, "", reports, screensaverRetry, logging.For(ctx, "screensaver")) | |||
| 326 | - | }() | |||
| 327 | - | } | |||
| 328 | - | var result error | |||
| 329 | - | select { | |||
| 330 | - | case <-ctx.Done(): | |||
| 331 | - | if errors.Is(context.Cause(ctx), errTimeout) { | |||
| 332 | - | log.Info().Str("reason", "timeout").Dur("after", opts.Timeout).Msg("stopping") | |||
| 333 | - | } | |||
| 334 | - | case result = <-done: | |||
| 335 | - | log.Info().AnErr("cause", result).Msg("stopping") | |||
| 336 | - | } | |||
| 337 | - | cancel() | |||
| 338 | - | workers.Wait() | |||
| 339 | - | // The wayland server has stopped: no request can be queued any more. | |||
| 340 | - | drainCaptures(ctx, p.captures, nil) | |||
| 341 | - | // A keymap the input goroutine never took is still ours to free. | |||
| 342 | - | select { | |||
| 343 | - | case km := <-p.keymaps: | |||
| 344 | - | km.Close() | |||
| 345 | - | default: | |||
| 346 | - | } | |||
| 347 | - | for len(done) > 0 { | |||
| 348 | - | err := <-done | |||
| 349 | - | if result == nil && err != nil { | |||
| 350 | - | result = err | |||
| 351 | - | } | |||
| 352 | - | } | |||
| 353 | - | if errors.Is(result, core.ErrQuit) || errors.Is(result, libinput.ErrEmergencyQuit) { | |||
| 354 | - | return nil | |||
| 355 | - | } | |||
| 356 | - | return result | |||
| 357 | } | 66 | } | ||
| 358 | 67 | ||||
| 359 | // screensaverRetry is the first delay before the screensaver service tries | 68 | // screensaverRetry is the first delay before the screensaver service tries | ||
M internal/app/headless.go +1 -26
| @@ -23,14 +23,8 @@ type headlessOptions struct { | |||
| 23 | 23 | // With several outputs, screenshots go to a subdirectory per output. Modes | |
| 24 | 24 | // are fixed, so apply answers every configuration without backend work. | |
| 25 | 25 | // | |
| 26 | - | // The raw PQ dump (o.raw) needs readable HDR targets, set on these | |
| 27 | - | // renderers only: the DRM path never reads them back. | |
| 28 | - | // | |
| 29 | - | // Virtual outputs have no display to list modifiers, so each renderer is | |
| 30 | - | // told it drives one (vulkan.Renderer.SetVirtualOutput); without it a real | |
| 31 | - | // GPU refuses the HDR targets and the output falls back to SDR. | |
| 26 | + | // newRenderer is configured for virtual outputs by newVulkanRenderer. | |
| 32 | 27 | func runHeadless(ctx context.Context, o headlessOptions, apply *outputApply, ch outputChannels, curs *cursors, newRenderer func(w, h int) (ports.Renderer, error), log zerowrap.Logger) error { | |
| 33 | - | newRenderer = virtualRenderer(newRenderer, o.raw) | |
| 34 | 28 | set := newOutputSet(ctx, ch.captured) | |
| 35 | 29 | set.wireSecurity(ch) | |
| 36 | 30 | inventory := ports.OutputHeads{} | |
| @@ -116,25 +110,6 @@ func runHeadless(ctx context.Context, o headlessOptions, apply *outputApply, ch | |||
| 116 | 110 | } | |
| 117 | 111 | } | |
| 118 | 112 | ||
| 119 | - | // virtualRenderer wraps a renderer factory for headless outputs. Vulkan is | |
| 120 | - | // the only implementation of these settings; they are not on the port. | |
| 121 | - | // readback makes the HDR targets readable (HDRPixels). | |
| 122 | - | func virtualRenderer(newRenderer func(w, h int) (ports.Renderer, error), readback bool) func(w, h int) (ports.Renderer, error) { | |
| 123 | - | return func(w, h int) (ports.Renderer, error) { | |
| 124 | - | r, err := newRenderer(w, h) | |
| 125 | - | if err != nil { | |
| 126 | - | return nil, err | |
| 127 | - | } | |
| 128 | - | if v, ok := r.(interface{ SetVirtualOutput(bool) }); ok { | |
| 129 | - | v.SetVirtualOutput(true) | |
| 130 | - | } | |
| 131 | - | if v, ok := r.(interface{ SetHDRReadback(bool) }); ok && readback { | |
| 132 | - | v.SetHDRReadback(true) | |
| 133 | - | } | |
| 134 | - | return r, nil | |
| 135 | - | } | |
| 136 | - | } | |
| 137 | - | ||
| 138 | 113 | func joinErr(a, b error) error { | |
| 139 | 114 | if a != nil { | |
| 140 | 115 | return a | |
| @@ -23,14 +23,8 @@ type headlessOptions struct { | |||||
| 23 | // With several outputs, screenshots go to a subdirectory per output. Modes | 23 | // With several outputs, screenshots go to a subdirectory per output. Modes | ||
| 24 | // are fixed, so apply answers every configuration without backend work. | 24 | // are fixed, so apply answers every configuration without backend work. | ||
| 25 | // | 25 | // | ||
| 26 | - | // The raw PQ dump (o.raw) needs readable HDR targets, set on these | 26 | + | // newRenderer is configured for virtual outputs by newVulkanRenderer. |
| 27 | - | // renderers only: the DRM path never reads them back. | |||
| 28 | - | // | |||
| 29 | - | // Virtual outputs have no display to list modifiers, so each renderer is | |||
| 30 | - | // told it drives one (vulkan.Renderer.SetVirtualOutput); without it a real | |||
| 31 | - | // GPU refuses the HDR targets and the output falls back to SDR. | |||
| 32 | func runHeadless(ctx context.Context, o headlessOptions, apply *outputApply, ch outputChannels, curs *cursors, newRenderer func(w, h int) (ports.Renderer, error), log zerowrap.Logger) error { | 27 | func runHeadless(ctx context.Context, o headlessOptions, apply *outputApply, ch outputChannels, curs *cursors, newRenderer func(w, h int) (ports.Renderer, error), log zerowrap.Logger) error { | ||
| 33 | - | newRenderer = virtualRenderer(newRenderer, o.raw) | |||
| 34 | set := newOutputSet(ctx, ch.captured) | 28 | set := newOutputSet(ctx, ch.captured) | ||
| 35 | set.wireSecurity(ch) | 29 | set.wireSecurity(ch) | ||
| 36 | inventory := ports.OutputHeads{} | 30 | inventory := ports.OutputHeads{} | ||
| @@ -116,25 +110,6 @@ func runHeadless(ctx context.Context, o headlessOptions, apply *outputApply, ch | |||||
| 116 | } | 110 | } | ||
| 117 | } | 111 | } | ||
| 118 | 112 | ||||
| 119 | - | // virtualRenderer wraps a renderer factory for headless outputs. Vulkan is | |||
| 120 | - | // the only implementation of these settings; they are not on the port. | |||
| 121 | - | // readback makes the HDR targets readable (HDRPixels). | |||
| 122 | - | func virtualRenderer(newRenderer func(w, h int) (ports.Renderer, error), readback bool) func(w, h int) (ports.Renderer, error) { | |||
| 123 | - | return func(w, h int) (ports.Renderer, error) { | |||
| 124 | - | r, err := newRenderer(w, h) | |||
| 125 | - | if err != nil { | |||
| 126 | - | return nil, err | |||
| 127 | - | } | |||
| 128 | - | if v, ok := r.(interface{ SetVirtualOutput(bool) }); ok { | |||
| 129 | - | v.SetVirtualOutput(true) | |||
| 130 | - | } | |||
| 131 | - | if v, ok := r.(interface{ SetHDRReadback(bool) }); ok && readback { | |||
| 132 | - | v.SetHDRReadback(true) | |||
| 133 | - | } | |||
| 134 | - | return r, nil | |||
| 135 | - | } | |||
| 136 | - | } | |||
| 137 | - | ||||
| 138 | func joinErr(a, b error) error { | 113 | func joinErr(a, b error) error { | ||
| 139 | if a != nil { | 114 | if a != nil { | ||
| 140 | return a | 115 | return a | ||
M internal/app/headless_test.go +15 -9
| @@ -3,7 +3,7 @@ package app | |||
| 3 | 3 | import ( | |
| 4 | 4 | "testing" | |
| 5 | 5 | ||
| 6 | - | "github.com/bnema/neferwl/internal/adapters/vulkan" | |
| 6 | + | "github.com/bnema/neferwl/internal/adapters/logging" | |
| 7 | 7 | "github.com/bnema/neferwl/internal/ports" | |
| 8 | 8 | ) | |
| 9 | 9 | ||
| @@ -12,14 +12,7 @@ import ( | |||
| 12 | 12 | // and a headless output falls back to SDR. Skips without Vulkan or without a | |
| 13 | 13 | // device that exports dmabufs (lavapipe in CI). | |
| 14 | 14 | func TestVirtualRendererExportsHDRTargets(t *testing.T) { | |
| 15 | - | newRenderer := func(w, h int) (ports.Renderer, error) { | |
| 16 | - | r, err := vulkan.New(w, h) | |
| 17 | - | if err != nil { | |
| 18 | - | return nil, err | |
| 19 | - | } | |
| 20 | - | return r, nil | |
| 21 | - | } | |
| 22 | - | r, err := virtualRenderer(newRenderer, false)(64, 16) | |
| 15 | + | r, err := newVulkanRenderer(logging.For(t.Context(), "render"), true, false)(64, 16) | |
| 23 | 16 | if err != nil { | |
| 24 | 17 | t.Skipf("Vulkan unavailable: %v", err) | |
| 25 | 18 | } | |
| @@ -45,3 +38,16 @@ func TestVirtualRendererExportsHDRTargets(t *testing.T) { | |||
| 45 | 38 | } | |
| 46 | 39 | } | |
| 47 | 40 | } | |
| 41 | + | ||
| 42 | + | // A failed assembly returns an error and releases what it acquired: without | |
| 43 | + | // a runtime directory wayland.New, the last fallible step, fails. | |
| 44 | + | func TestAssembleSessionFailure(t *testing.T) { | |
| 45 | + | t.Setenv("XDG_RUNTIME_DIR", "") | |
| 46 | + | s, err := assembleSession(t.Context(), Options{Backend: "headless", Config: ports.Config{}}) | |
| 47 | + | if err == nil { | |
| 48 | + | t.Fatal("assembleSession succeeded without XDG_RUNTIME_DIR") | |
| 49 | + | } | |
| 50 | + | if s != nil { | |
| 51 | + | t.Fatal("failed assembly returned a session") | |
| 52 | + | } | |
| 53 | + | } | |
| @@ -3,7 +3,7 @@ package app | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "testing" | 4 | "testing" | ||
| 5 | 5 | ||||
| 6 | - | "github.com/bnema/neferwl/internal/adapters/vulkan" | 6 | + | "github.com/bnema/neferwl/internal/adapters/logging" |
| 7 | "github.com/bnema/neferwl/internal/ports" | 7 | "github.com/bnema/neferwl/internal/ports" | ||
| 8 | ) | 8 | ) | ||
| 9 | 9 | ||||
| @@ -12,14 +12,7 @@ import ( | |||||
| 12 | // and a headless output falls back to SDR. Skips without Vulkan or without a | 12 | // and a headless output falls back to SDR. Skips without Vulkan or without a | ||
| 13 | // device that exports dmabufs (lavapipe in CI). | 13 | // device that exports dmabufs (lavapipe in CI). | ||
| 14 | func TestVirtualRendererExportsHDRTargets(t *testing.T) { | 14 | func TestVirtualRendererExportsHDRTargets(t *testing.T) { | ||
| 15 | - | newRenderer := func(w, h int) (ports.Renderer, error) { | 15 | + | r, err := newVulkanRenderer(logging.For(t.Context(), "render"), true, false)(64, 16) |
| 16 | - | r, err := vulkan.New(w, h) | |||
| 17 | - | if err != nil { | |||
| 18 | - | return nil, err | |||
| 19 | - | } | |||
| 20 | - | return r, nil | |||
| 21 | - | } | |||
| 22 | - | r, err := virtualRenderer(newRenderer, false)(64, 16) | |||
| 23 | if err != nil { | 16 | if err != nil { | ||
| 24 | t.Skipf("Vulkan unavailable: %v", err) | 17 | t.Skipf("Vulkan unavailable: %v", err) | ||
| 25 | } | 18 | } | ||
| @@ -45,3 +38,16 @@ func TestVirtualRendererExportsHDRTargets(t *testing.T) { | |||||
| 45 | } | 38 | } | ||
| 46 | } | 39 | } | ||
| 47 | } | 40 | } | ||
| 41 | + | ||||
| 42 | + | // A failed assembly returns an error and releases what it acquired: without | |||
| 43 | + | // a runtime directory wayland.New, the last fallible step, fails. | |||
| 44 | + | func TestAssembleSessionFailure(t *testing.T) { | |||
| 45 | + | t.Setenv("XDG_RUNTIME_DIR", "") | |||
| 46 | + | s, err := assembleSession(t.Context(), Options{Backend: "headless", Config: ports.Config{}}) | |||
| 47 | + | if err == nil { | |||
| 48 | + | t.Fatal("assembleSession succeeded without XDG_RUNTIME_DIR") | |||
| 49 | + | } | |||
| 50 | + | if s != nil { | |||
| 51 | + | t.Fatal("failed assembly returned a session") | |||
| 52 | + | } | |||
| 53 | + | } | |||
A internal/app/session.go +395 -0
| @@ -0,0 +1,395 @@ | |||
| 1 | + | package app | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "bufio" | |
| 5 | + | "context" | |
| 6 | + | "errors" | |
| 7 | + | "os" | |
| 8 | + | "runtime" | |
| 9 | + | "sync" | |
| 10 | + | "time" | |
| 11 | + | ||
| 12 | + | "github.com/bnema/neferwl/internal/adapters/captureallow" | |
| 13 | + | "github.com/bnema/neferwl/internal/adapters/clock" | |
| 14 | + | "github.com/bnema/neferwl/internal/adapters/config" | |
| 15 | + | "github.com/bnema/neferwl/internal/adapters/drm" | |
| 16 | + | "github.com/bnema/neferwl/internal/adapters/headlessinput" | |
| 17 | + | "github.com/bnema/neferwl/internal/adapters/launcher" | |
| 18 | + | "github.com/bnema/neferwl/internal/adapters/libinput" | |
| 19 | + | "github.com/bnema/neferwl/internal/adapters/logging" | |
| 20 | + | "github.com/bnema/neferwl/internal/adapters/sched" | |
| 21 | + | "github.com/bnema/neferwl/internal/adapters/screensaver" | |
| 22 | + | "github.com/bnema/neferwl/internal/adapters/sessionsecurity" | |
| 23 | + | "github.com/bnema/neferwl/internal/adapters/statefile" | |
| 24 | + | "github.com/bnema/neferwl/internal/adapters/vulkan" | |
| 25 | + | "github.com/bnema/neferwl/internal/adapters/wayland" | |
| 26 | + | "github.com/bnema/neferwl/internal/adapters/workspaceid" | |
| 27 | + | "github.com/bnema/neferwl/internal/adapters/xkb" | |
| 28 | + | "github.com/bnema/neferwl/internal/adapters/xwayland" | |
| 29 | + | "github.com/bnema/neferwl/internal/core" | |
| 30 | + | "github.com/bnema/neferwl/internal/ports" | |
| 31 | + | "github.com/bnema/zerowrap" | |
| 32 | + | ) | |
| 33 | + | ||
| 34 | + | // session holds the dependencies assembleSession acquired and wired; run | |
| 35 | + | // starts them. The session owns hw, curs and xdisplay (released by close) | |
| 36 | + | // until run ends; the keymap and the wayland server own themselves. | |
| 37 | + | type session struct { | |
| 38 | + | opts Options | |
| 39 | + | log zerowrap.Logger | |
| 40 | + | ||
| 41 | + | clock ports.Clock | |
| 42 | + | security *sessionsecurity.Gate | |
| 43 | + | p *pipes | |
| 44 | + | sizes [][2]int | |
| 45 | + | path string | |
| 46 | + | ||
| 47 | + | hw *drmBackend | |
| 48 | + | km *xkb.Keymap | |
| 49 | + | core *core.Core | |
| 50 | + | dmabuf ports.DMABufSupport | |
| 51 | + | wsIDs *workspaceid.IDs | |
| 52 | + | captureAllow *captureallow.Store | |
| 53 | + | server *wayland.Server | |
| 54 | + | childEnv []string | |
| 55 | + | statePath string | |
| 56 | + | xdisplay *xwayland.Display | |
| 57 | + | child *launcher.Launcher | |
| 58 | + | curs *cursors | |
| 59 | + | ||
| 60 | + | newRenderer func(w, h int) (ports.Renderer, error) | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | // assembleSession acquires and wires every dependency in startup order and | |
| 64 | + | // starts no long-lived worker. | |
| 65 | + | // | |
| 66 | + | // Invariant: wayland.New is the last fallible step. *wayland.Server has no | |
| 67 | + | // Close: its sockets are released only by Server.Run. Every later step | |
| 68 | + | // (statefile.Path, openXwayland, launcher.New, newCursors) degrades with a | |
| 69 | + | // warning or nil instead of failing. A failure therefore releases exactly | |
| 70 | + | // the DRM backend and the keymap (abort); nothing else is owned yet. | |
| 71 | + | func assembleSession(ctx context.Context, opts Options) (s *session, err error) { | |
| 72 | + | s = &session{opts: opts, log: logging.For(ctx, "app"), clock: clock.System{}, security: &sessionsecurity.Gate{}} | |
| 73 | + | defer func() { | |
| 74 | + | if err != nil { | |
| 75 | + | s.abort() | |
| 76 | + | s = nil | |
| 77 | + | } | |
| 78 | + | }() | |
| 79 | + | log := s.log | |
| 80 | + | s.sizes = opts.Sizes | |
| 81 | + | if len(s.sizes) == 0 { | |
| 82 | + | s.sizes = [][2]int{{1920, 1080}} | |
| 83 | + | } | |
| 84 | + | if opts.Backend == "drm" { | |
| 85 | + | if s.hw, err = openDRM(ctx, opts.Config); err != nil { | |
| 86 | + | return s, err | |
| 87 | + | } | |
| 88 | + | } | |
| 89 | + | s.p = newPipes(s.hw != nil) | |
| 90 | + | // core() carries channels only; the gate, clock and terminal flag are | |
| 91 | + | // explicit core.Options. | |
| 92 | + | if s.core, err = core.New(opts.Config, s.p.core(), core.Options{Security: s.security, Clock: s.clock, Terminal: !opts.NoTerminal}); err != nil { | |
| 93 | + | return s, err | |
| 94 | + | } | |
| 95 | + | if s.km, err = xkb.New(xkb.RMLVO{Layout: opts.Config.Keyboard.Layout, Variant: opts.Config.Keyboard.Variant, Options: opts.Config.Keyboard.Options}); err != nil { | |
| 96 | + | return s, err | |
| 97 | + | } | |
| 98 | + | runtimeDir := os.Getenv("XDG_RUNTIME_DIR") | |
| 99 | + | // Every output renders on the same GPU: its formats are the clients'. | |
| 100 | + | s.dmabuf = vulkan.Probe() | |
| 101 | + | log.Info().Int("formats", len(s.dmabuf.Formats)).Msg("dmabuf") | |
| 102 | + | // One launch prefix for ext-workspace ids and the state file. | |
| 103 | + | s.wsIDs = workspaceid.New() | |
| 104 | + | // The path is fixed: a config key would let a program of the same user | |
| 105 | + | // widen who may capture. | |
| 106 | + | // Production: the fixed path, a root-owned file. Only tests set a path, | |
| 107 | + | // and an owner for it. | |
| 108 | + | captureAllowLog := logging.For(ctx, "captureallow") | |
| 109 | + | if opts.captureAllowPath != "" { | |
| 110 | + | s.captureAllow = captureallow.NewStoreOwnedBy(opts.captureAllowPath, opts.captureAllowOwner, captureAllowLog) | |
| 111 | + | } else { | |
| 112 | + | s.captureAllow = captureallow.NewStore(captureallow.DefaultPath, captureAllowLog) | |
| 113 | + | } | |
| 114 | + | if s.server, err = wayland.New(wayland.Options{ | |
| 115 | + | CaptureAllow: s.captureAllow, | |
| 116 | + | WorkspaceIDs: s.wsIDs, | |
| 117 | + | Security: s.security, | |
| 118 | + | RuntimeDir: runtimeDir, | |
| 119 | + | DMABuf: s.dmabuf, | |
| 120 | + | SyncobjNode: renderNode(s.dmabuf.Device), | |
| 121 | + | Keymap: s.km.String(), | |
| 122 | + | RepeatRate: opts.Config.Keyboard.RepeatRate, | |
| 123 | + | RepeatDelay: opts.Config.Keyboard.RepeatDelay, | |
| 124 | + | }, s.p.wayland(), logging.For(ctx, "wayland")); err != nil { | |
| 125 | + | return s, err | |
| 126 | + | } | |
| 127 | + | log.Info().Str("WAYLAND_DISPLAY", s.server.SocketName()).Msg("listening") | |
| 128 | + | s.childEnv = launcher.ChildEnv(os.Environ(), s.server.SocketName(), runtimeDir, cursorSize()) | |
| 129 | + | statePath, serr := statefile.Path(runtimeDir, s.server.SocketName()) | |
| 130 | + | if serr != nil { | |
| 131 | + | log.Warn().Err(serr).Msg("state file disabled") | |
| 132 | + | } else { | |
| 133 | + | s.statePath = statePath | |
| 134 | + | s.childEnv = append(s.childEnv, statefile.Env+"="+statePath) | |
| 135 | + | } | |
| 136 | + | if s.xdisplay = openXwayland(ctx, opts, s.childEnv, logging.For(ctx, "xwayland")); s.xdisplay != nil { | |
| 137 | + | s.childEnv = append(s.childEnv, "DISPLAY="+s.xdisplay.Name()) | |
| 138 | + | } | |
| 139 | + | s.child = launcher.New(s.childEnv, s.security, logging.For(ctx, "launcher")) | |
| 140 | + | s.curs = newCursors(s.clock, opts.Config.Cursor.HideAfter) | |
| 141 | + | s.path = opts.ConfigPath | |
| 142 | + | if s.path == "" { | |
| 143 | + | s.path = config.DefaultPath() | |
| 144 | + | } | |
| 145 | + | renderLog := logging.For(ctx, "render") | |
| 146 | + | if s.hw != nil { | |
| 147 | + | s.newRenderer = newVulkanRenderer(renderLog, false, false) | |
| 148 | + | } else { | |
| 149 | + | s.newRenderer = newVulkanRenderer(renderLog, true, opts.ScreenshotRaw) | |
| 150 | + | } | |
| 151 | + | return s, nil | |
| 152 | + | } | |
| 153 | + | ||
| 154 | + | // abort releases what assembly acquired before ownership passed on: the DRM | |
| 155 | + | // backend and the keymap. It runs only when assembly fails, before | |
| 156 | + | // anything else is owned. | |
| 157 | + | func (s *session) abort() { | |
| 158 | + | if s.hw != nil { | |
| 159 | + | s.hw.close() | |
| 160 | + | } | |
| 161 | + | if s.km != nil { | |
| 162 | + | s.km.Close() | |
| 163 | + | } | |
| 164 | + | } | |
| 165 | + | ||
| 166 | + | // close releases what the session owns after its workers have joined. The | |
| 167 | + | // keymap and the wayland server are not here: the input goroutine and | |
| 168 | + | // Server.Run own them. | |
| 169 | + | func (s *session) close() { | |
| 170 | + | s.curs.stop() | |
| 171 | + | if s.xdisplay != nil { | |
| 172 | + | s.xdisplay.Close() | |
| 173 | + | } | |
| 174 | + | if s.hw != nil { | |
| 175 | + | s.hw.close() | |
| 176 | + | } | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | // newVulkanRenderer returns the concrete Vulkan renderer factory. Virtual | |
| 180 | + | // outputs have no display to list modifiers, so a headless renderer is told | |
| 181 | + | // it drives one: without it a real GPU refuses the HDR targets and the | |
| 182 | + | // output falls back to SDR. readback makes the HDR targets readable | |
| 183 | + | // (HDRPixels). These settings are Vulkan-only and not on ports.Renderer. | |
| 184 | + | func newVulkanRenderer(log zerowrap.Logger, virtual, readback bool) func(w, h int) (ports.Renderer, error) { | |
| 185 | + | return func(w, h int) (ports.Renderer, error) { | |
| 186 | + | r, err := vulkan.New(w, h) | |
| 187 | + | if err != nil { | |
| 188 | + | return nil, err | |
| 189 | + | } | |
| 190 | + | r.SetLogger(log) | |
| 191 | + | log.Info().Str("queue_priority", r.QueuePriority()).Msg("vulkan queue") | |
| 192 | + | r.SetVirtualOutput(virtual) | |
| 193 | + | if readback { | |
| 194 | + | r.SetHDRReadback(true) | |
| 195 | + | } | |
| 196 | + | return r, nil | |
| 197 | + | } | |
| 198 | + | } | |
| 199 | + | ||
| 200 | + | // run starts the workers and waits for the session to end; it releases the | |
| 201 | + | // session's resources after every worker has joined. | |
| 202 | + | func (s *session) run(ctx context.Context, inject func(chan<- ports.InputEvent)) error { | |
| 203 | + | defer s.close() | |
| 204 | + | ctx, cancel := context.WithCancel(ctx) | |
| 205 | + | defer cancel() | |
| 206 | + | opts, log, p, hw, km, security := s.opts, s.log, s.p, s.hw, s.km, s.security | |
| 207 | + | childEnv, curs, xdisplay, statePath := s.childEnv, s.curs, s.xdisplay, s.statePath | |
| 208 | + | server, captureAllow, c, child, wsIDs, dmabuf, sizes, path := s.server, s.captureAllow, s.core, s.child, s.wsIDs, s.dmabuf, s.sizes, s.path | |
| 209 | + | if opts.Session { | |
| 210 | + | launcher.ExportSession(ctx, childEnv, logging.For(ctx, "launcher")) | |
| 211 | + | launcher.NotifyReady(os.Getenv("NOTIFY_SOCKET"), logging.For(ctx, "launcher")) | |
| 212 | + | defer launcher.UnexportSession(childEnv, logging.For(ctx, "launcher")) | |
| 213 | + | } | |
| 214 | + | if inject != nil { | |
| 215 | + | inject(p.input) | |
| 216 | + | } | |
| 217 | + | var workers sync.WaitGroup | |
| 218 | + | workers.Add(8) | |
| 219 | + | done := make(chan error, 8) | |
| 220 | + | go func() { | |
| 221 | + | defer workers.Done() | |
| 222 | + | done <- config.Watch(ctx, path, p.watched, logging.For(ctx, "config")) | |
| 223 | + | }() | |
| 224 | + | if p.scales != nil { | |
| 225 | + | workers.Add(1) | |
| 226 | + | go func() { | |
| 227 | + | defer workers.Done() | |
| 228 | + | core.PersistScales(ctx, p.scales, config.ScaleStore{Path: path}, launcher.NewNotifier(ctx, childEnv, logging.For(ctx, "launcher")), s.clock, time.Second) | |
| 229 | + | }() | |
| 230 | + | } | |
| 231 | + | workers.Add(1) | |
| 232 | + | go func() { | |
| 233 | + | defer workers.Done() | |
| 234 | + | relayConfig(ctx, opts.Config, p.watched, p.filtered, p.keymaps, p.deviceConfigs, curs, p.commands, logging.For(ctx, "config")) | |
| 235 | + | }() | |
| 236 | + | script := make(chan string) | |
| 237 | + | go func() { | |
| 238 | + | defer workers.Done() | |
| 239 | + | if hw != nil { | |
| 240 | + | // Core sends the first layout once DRM reports the outputs. | |
| 241 | + | var layout ports.Layout | |
| 242 | + | select { | |
| 243 | + | case layout = <-p.layouts: | |
| 244 | + | case <-ctx.Done(): | |
| 245 | + | km.Close() | |
| 246 | + | return | |
| 247 | + | } | |
| 248 | + | done <- safe("input", func() error { | |
| 249 | + | if opts.Config.Performance.Realtime { | |
| 250 | + | runtime.LockOSThread() | |
| 251 | + | // Let this dedicated thread exit with its scheduling policy. | |
| 252 | + | schedLog := logging.For(ctx, "sched") | |
| 253 | + | if err := sched.Realtime(schedLog); err != nil { | |
| 254 | + | schedLog.Warn().Err(err).Msg("input scheduling") | |
| 255 | + | } | |
| 256 | + | } | |
| 257 | + | return libinput.Run(ctx, libinput.Options{ | |
| 258 | + | Security: security, | |
| 259 | + | Seat: hw.seat, | |
| 260 | + | SeatName: hw.seat.Name(), | |
| 261 | + | Keymap: km, | |
| 262 | + | Keymaps: p.keymaps, | |
| 263 | + | Layout: layout, | |
| 264 | + | Layouts: p.layouts, | |
| 265 | + | Constraints: p.constraints, | |
| 266 | + | DeviceConfig: opts.Config.InputDevicesConfig, | |
| 267 | + | DeviceConfigs: p.deviceConfigs, | |
| 268 | + | Active: hw.seat.Subscribe(), | |
| 269 | + | MoveCursor: curs.move, | |
| 270 | + | Log: logging.For(ctx, "input"), | |
| 271 | + | LogMotion: logging.Enabled(ctx, "input-motion"), | |
| 272 | + | LogKeys: logging.Enabled(ctx, "input-keys"), | |
| 273 | + | }, p.input) | |
| 274 | + | }) | |
| 275 | + | return | |
| 276 | + | } | |
| 277 | + | if err := headlessinput.RunSecure(ctx, km, p.keymaps, script, p.input, p.layouts, func(o string, x, y float64) { curs.move(o, x, y, true) }, logging.For(ctx, "input"), security); err != nil && !errors.Is(err, context.Canceled) { | |
| 278 | + | select { | |
| 279 | + | case done <- err: | |
| 280 | + | case <-ctx.Done(): | |
| 281 | + | } | |
| 282 | + | } | |
| 283 | + | }() | |
| 284 | + | if opts.Script != nil { | |
| 285 | + | go func() { <-ctx.Done(); _ = opts.Script.Close() }() | |
| 286 | + | go func() { | |
| 287 | + | defer close(script) | |
| 288 | + | scanner := bufio.NewScanner(opts.Script) | |
| 289 | + | for scanner.Scan() { | |
| 290 | + | select { | |
| 291 | + | case <-ctx.Done(): | |
| 292 | + | return | |
| 293 | + | case script <- scanner.Text(): | |
| 294 | + | } | |
| 295 | + | } | |
| 296 | + | if err := scanner.Err(); err != nil { | |
| 297 | + | log.Warn().Err(err).Msg("reading input script") | |
| 298 | + | } | |
| 299 | + | }() | |
| 300 | + | } else { | |
| 301 | + | close(script) | |
| 302 | + | } | |
| 303 | + | go func() { defer workers.Done(); done <- server.Run(ctx) }() | |
| 304 | + | go func() { | |
| 305 | + | defer workers.Done() | |
| 306 | + | // Without a watch the allowlist stays as loaded: restart to apply. | |
| 307 | + | if err := captureAllow.Run(ctx); err != nil { | |
| 308 | + | log.Warn().Err(err).Msg("capture allowlist not watched, restart to apply changes") | |
| 309 | + | } | |
| 310 | + | }() | |
| 311 | + | go func() { defer workers.Done(); done <- child.Run(ctx, p.spawn) }() | |
| 312 | + | go func() { defer workers.Done(); done <- c.Run(ctx) }() | |
| 313 | + | if xdisplay != nil { | |
| 314 | + | workers.Add(1) | |
| 315 | + | // X11 failing leaves the Wayland session running; Run logs why. | |
| 316 | + | go func() { defer workers.Done(); _ = xdisplay.Run(ctx) }() | |
| 317 | + | } | |
| 318 | + | if statePath != "" { | |
| 319 | + | workers.Add(1) | |
| 320 | + | go func() { | |
| 321 | + | defer workers.Done() | |
| 322 | + | if err := statefile.Run(ctx, statePath, p.states, wsIDs, logging.For(ctx, "statefile")); err != nil { | |
| 323 | + | // Scripts lose their state; the session goes on. | |
| 324 | + | log.Warn().Err(err).Msg("state file disabled") | |
| 325 | + | } | |
| 326 | + | }() | |
| 327 | + | } | |
| 328 | + | go func() { | |
| 329 | + | defer workers.Done() | |
| 330 | + | consumeScenes(ctx, p.scenes, p.configErrors, opts.testScenes, p.renderScenes) | |
| 331 | + | }() | |
| 332 | + | workers.Add(1) | |
| 333 | + | go func() { defer workers.Done(); relayPresented(ctx, p.flips, p.presented, p.frames) }() | |
| 334 | + | ||
| 335 | + | go func() { | |
| 336 | + | defer workers.Done() | |
| 337 | + | outputIO := p.outputs(security) | |
| 338 | + | apply := newOutputApply(newOutputOverrides(opts.Config, hw == nil), logging.For(ctx, "app")) | |
| 339 | + | if hw != nil { | |
| 340 | + | done <- safe("output", func() error { | |
| 341 | + | want := func(cfg ports.Config) drm.Want { | |
| 342 | + | w := wantFromConfig(cfg) | |
| 343 | + | w.Sampled = dmabuf.Formats | |
| 344 | + | w.TraceFlips = logging.Enabled(ctx, "drm-flip") | |
| 345 | + | return w | |
| 346 | + | } | |
| 347 | + | return hw.runOutputs(ctx, want, opts.Config, apply, outputIO, curs, s.newRenderer, logging.For(ctx, "drm")) | |
| 348 | + | }) | |
| 349 | + | return | |
| 350 | + | } | |
| 351 | + | done <- runHeadless(ctx, headlessOptions{sizes: sizes, shots: opts.ScreenshotDir, hdr: opts.HeadlessHDR, raw: opts.ScreenshotRaw}, apply, outputIO, curs, s.newRenderer, logging.For(ctx, "render")) | |
| 352 | + | }() | |
| 353 | + | ||
| 354 | + | if hw != nil { | |
| 355 | + | workers.Add(1) | |
| 356 | + | go func() { defer workers.Done(); done <- safe("seat", func() error { return hw.seat.Run(ctx) }) }() | |
| 357 | + | // Only a real session serves the session bus: headless runs would take | |
| 358 | + | // the name from the desktop they run in. | |
| 359 | + | workers.Add(1) | |
| 360 | + | go func() { | |
| 361 | + | defer workers.Done() | |
| 362 | + | reports := screensaver.Reports{Held: p.idleInhibited, Activity: p.idleActivity} | |
| 363 | + | screensaver.Serve(ctx, "", reports, screensaverRetry, logging.For(ctx, "screensaver")) | |
| 364 | + | }() | |
| 365 | + | } | |
| 366 | + | var result error | |
| 367 | + | select { | |
| 368 | + | case <-ctx.Done(): | |
| 369 | + | if errors.Is(context.Cause(ctx), errTimeout) { | |
| 370 | + | log.Info().Str("reason", "timeout").Dur("after", opts.Timeout).Msg("stopping") | |
| 371 | + | } | |
| 372 | + | case result = <-done: | |
| 373 | + | log.Info().AnErr("cause", result).Msg("stopping") | |
| 374 | + | } | |
| 375 | + | cancel() | |
| 376 | + | workers.Wait() | |
| 377 | + | // The wayland server has stopped: no request can be queued any more. | |
| 378 | + | drainCaptures(ctx, p.captures, nil) | |
| 379 | + | // A keymap the input goroutine never took is still ours to free. | |
| 380 | + | select { | |
| 381 | + | case km := <-p.keymaps: | |
| 382 | + | km.Close() | |
| 383 | + | default: | |
| 384 | + | } | |
| 385 | + | for len(done) > 0 { | |
| 386 | + | err := <-done | |
| 387 | + | if result == nil && err != nil { | |
| 388 | + | result = err | |
| 389 | + | } | |
| 390 | + | } | |
| 391 | + | if errors.Is(result, core.ErrQuit) || errors.Is(result, libinput.ErrEmergencyQuit) { | |
| 392 | + | return nil | |
| 393 | + | } | |
| 394 | + | return result | |
| 395 | + | } | |
| @@ -0,0 +1,395 @@ | |||||
| 1 | + | package app | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "bufio" | |||
| 5 | + | "context" | |||
| 6 | + | "errors" | |||
| 7 | + | "os" | |||
| 8 | + | "runtime" | |||
| 9 | + | "sync" | |||
| 10 | + | "time" | |||
| 11 | + | ||||
| 12 | + | "github.com/bnema/neferwl/internal/adapters/captureallow" | |||
| 13 | + | "github.com/bnema/neferwl/internal/adapters/clock" | |||
| 14 | + | "github.com/bnema/neferwl/internal/adapters/config" | |||
| 15 | + | "github.com/bnema/neferwl/internal/adapters/drm" | |||
| 16 | + | "github.com/bnema/neferwl/internal/adapters/headlessinput" | |||
| 17 | + | "github.com/bnema/neferwl/internal/adapters/launcher" | |||
| 18 | + | "github.com/bnema/neferwl/internal/adapters/libinput" | |||
| 19 | + | "github.com/bnema/neferwl/internal/adapters/logging" | |||
| 20 | + | "github.com/bnema/neferwl/internal/adapters/sched" | |||
| 21 | + | "github.com/bnema/neferwl/internal/adapters/screensaver" | |||
| 22 | + | "github.com/bnema/neferwl/internal/adapters/sessionsecurity" | |||
| 23 | + | "github.com/bnema/neferwl/internal/adapters/statefile" | |||
| 24 | + | "github.com/bnema/neferwl/internal/adapters/vulkan" | |||
| 25 | + | "github.com/bnema/neferwl/internal/adapters/wayland" | |||
| 26 | + | "github.com/bnema/neferwl/internal/adapters/workspaceid" | |||
| 27 | + | "github.com/bnema/neferwl/internal/adapters/xkb" | |||
| 28 | + | "github.com/bnema/neferwl/internal/adapters/xwayland" | |||
| 29 | + | "github.com/bnema/neferwl/internal/core" | |||
| 30 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 31 | + | "github.com/bnema/zerowrap" | |||
| 32 | + | ) | |||
| 33 | + | ||||
| 34 | + | // session holds the dependencies assembleSession acquired and wired; run | |||
| 35 | + | // starts them. The session owns hw, curs and xdisplay (released by close) | |||
| 36 | + | // until run ends; the keymap and the wayland server own themselves. | |||
| 37 | + | type session struct { | |||
| 38 | + | opts Options | |||
| 39 | + | log zerowrap.Logger | |||
| 40 | + | ||||
| 41 | + | clock ports.Clock | |||
| 42 | + | security *sessionsecurity.Gate | |||
| 43 | + | p *pipes | |||
| 44 | + | sizes [][2]int | |||
| 45 | + | path string | |||
| 46 | + | ||||
| 47 | + | hw *drmBackend | |||
| 48 | + | km *xkb.Keymap | |||
| 49 | + | core *core.Core | |||
| 50 | + | dmabuf ports.DMABufSupport | |||
| 51 | + | wsIDs *workspaceid.IDs | |||
| 52 | + | captureAllow *captureallow.Store | |||
| 53 | + | server *wayland.Server | |||
| 54 | + | childEnv []string | |||
| 55 | + | statePath string | |||
| 56 | + | xdisplay *xwayland.Display | |||
| 57 | + | child *launcher.Launcher | |||
| 58 | + | curs *cursors | |||
| 59 | + | ||||
| 60 | + | newRenderer func(w, h int) (ports.Renderer, error) | |||
| 61 | + | } | |||
| 62 | + | ||||
| 63 | + | // assembleSession acquires and wires every dependency in startup order and | |||
| 64 | + | // starts no long-lived worker. | |||
| 65 | + | // | |||
| 66 | + | // Invariant: wayland.New is the last fallible step. *wayland.Server has no | |||
| 67 | + | // Close: its sockets are released only by Server.Run. Every later step | |||
| 68 | + | // (statefile.Path, openXwayland, launcher.New, newCursors) degrades with a | |||
| 69 | + | // warning or nil instead of failing. A failure therefore releases exactly | |||
| 70 | + | // the DRM backend and the keymap (abort); nothing else is owned yet. | |||
| 71 | + | func assembleSession(ctx context.Context, opts Options) (s *session, err error) { | |||
| 72 | + | s = &session{opts: opts, log: logging.For(ctx, "app"), clock: clock.System{}, security: &sessionsecurity.Gate{}} | |||
| 73 | + | defer func() { | |||
| 74 | + | if err != nil { | |||
| 75 | + | s.abort() | |||
| 76 | + | s = nil | |||
| 77 | + | } | |||
| 78 | + | }() | |||
| 79 | + | log := s.log | |||
| 80 | + | s.sizes = opts.Sizes | |||
| 81 | + | if len(s.sizes) == 0 { | |||
| 82 | + | s.sizes = [][2]int{{1920, 1080}} | |||
| 83 | + | } | |||
| 84 | + | if opts.Backend == "drm" { | |||
| 85 | + | if s.hw, err = openDRM(ctx, opts.Config); err != nil { | |||
| 86 | + | return s, err | |||
| 87 | + | } | |||
| 88 | + | } | |||
| 89 | + | s.p = newPipes(s.hw != nil) | |||
| 90 | + | // core() carries channels only; the gate, clock and terminal flag are | |||
| 91 | + | // explicit core.Options. | |||
| 92 | + | if s.core, err = core.New(opts.Config, s.p.core(), core.Options{Security: s.security, Clock: s.clock, Terminal: !opts.NoTerminal}); err != nil { | |||
| 93 | + | return s, err | |||
| 94 | + | } | |||
| 95 | + | if s.km, err = xkb.New(xkb.RMLVO{Layout: opts.Config.Keyboard.Layout, Variant: opts.Config.Keyboard.Variant, Options: opts.Config.Keyboard.Options}); err != nil { | |||
| 96 | + | return s, err | |||
| 97 | + | } | |||
| 98 | + | runtimeDir := os.Getenv("XDG_RUNTIME_DIR") | |||
| 99 | + | // Every output renders on the same GPU: its formats are the clients'. | |||
| 100 | + | s.dmabuf = vulkan.Probe() | |||
| 101 | + | log.Info().Int("formats", len(s.dmabuf.Formats)).Msg("dmabuf") | |||
| 102 | + | // One launch prefix for ext-workspace ids and the state file. | |||
| 103 | + | s.wsIDs = workspaceid.New() | |||
| 104 | + | // The path is fixed: a config key would let a program of the same user | |||
| 105 | + | // widen who may capture. | |||
| 106 | + | // Production: the fixed path, a root-owned file. Only tests set a path, | |||
| 107 | + | // and an owner for it. | |||
| 108 | + | captureAllowLog := logging.For(ctx, "captureallow") | |||
| 109 | + | if opts.captureAllowPath != "" { | |||
| 110 | + | s.captureAllow = captureallow.NewStoreOwnedBy(opts.captureAllowPath, opts.captureAllowOwner, captureAllowLog) | |||
| 111 | + | } else { | |||
| 112 | + | s.captureAllow = captureallow.NewStore(captureallow.DefaultPath, captureAllowLog) | |||
| 113 | + | } | |||
| 114 | + | if s.server, err = wayland.New(wayland.Options{ | |||
| 115 | + | CaptureAllow: s.captureAllow, | |||
| 116 | + | WorkspaceIDs: s.wsIDs, | |||
| 117 | + | Security: s.security, | |||
| 118 | + | RuntimeDir: runtimeDir, | |||
| 119 | + | DMABuf: s.dmabuf, | |||
| 120 | + | SyncobjNode: renderNode(s.dmabuf.Device), | |||
| 121 | + | Keymap: s.km.String(), | |||
| 122 | + | RepeatRate: opts.Config.Keyboard.RepeatRate, | |||
| 123 | + | RepeatDelay: opts.Config.Keyboard.RepeatDelay, | |||
| 124 | + | }, s.p.wayland(), logging.For(ctx, "wayland")); err != nil { | |||
| 125 | + | return s, err | |||
| 126 | + | } | |||
| 127 | + | log.Info().Str("WAYLAND_DISPLAY", s.server.SocketName()).Msg("listening") | |||
| 128 | + | s.childEnv = launcher.ChildEnv(os.Environ(), s.server.SocketName(), runtimeDir, cursorSize()) | |||
| 129 | + | statePath, serr := statefile.Path(runtimeDir, s.server.SocketName()) | |||
| 130 | + | if serr != nil { | |||
| 131 | + | log.Warn().Err(serr).Msg("state file disabled") | |||
| 132 | + | } else { | |||
| 133 | + | s.statePath = statePath | |||
| 134 | + | s.childEnv = append(s.childEnv, statefile.Env+"="+statePath) | |||
| 135 | + | } | |||
| 136 | + | if s.xdisplay = openXwayland(ctx, opts, s.childEnv, logging.For(ctx, "xwayland")); s.xdisplay != nil { | |||
| 137 | + | s.childEnv = append(s.childEnv, "DISPLAY="+s.xdisplay.Name()) | |||
| 138 | + | } | |||
| 139 | + | s.child = launcher.New(s.childEnv, s.security, logging.For(ctx, "launcher")) | |||
| 140 | + | s.curs = newCursors(s.clock, opts.Config.Cursor.HideAfter) | |||
| 141 | + | s.path = opts.ConfigPath | |||
| 142 | + | if s.path == "" { | |||
| 143 | + | s.path = config.DefaultPath() | |||
| 144 | + | } | |||
| 145 | + | renderLog := logging.For(ctx, "render") | |||
| 146 | + | if s.hw != nil { | |||
| 147 | + | s.newRenderer = newVulkanRenderer(renderLog, false, false) | |||
| 148 | + | } else { | |||
| 149 | + | s.newRenderer = newVulkanRenderer(renderLog, true, opts.ScreenshotRaw) | |||
| 150 | + | } | |||
| 151 | + | return s, nil | |||
| 152 | + | } | |||
| 153 | + | ||||
| 154 | + | // abort releases what assembly acquired before ownership passed on: the DRM | |||
| 155 | + | // backend and the keymap. It runs only when assembly fails, before | |||
| 156 | + | // anything else is owned. | |||
| 157 | + | func (s *session) abort() { | |||
| 158 | + | if s.hw != nil { | |||
| 159 | + | s.hw.close() | |||
| 160 | + | } | |||
| 161 | + | if s.km != nil { | |||
| 162 | + | s.km.Close() | |||
| 163 | + | } | |||
| 164 | + | } | |||
| 165 | + | ||||
| 166 | + | // close releases what the session owns after its workers have joined. The | |||
| 167 | + | // keymap and the wayland server are not here: the input goroutine and | |||
| 168 | + | // Server.Run own them. | |||
| 169 | + | func (s *session) close() { | |||
| 170 | + | s.curs.stop() | |||
| 171 | + | if s.xdisplay != nil { | |||
| 172 | + | s.xdisplay.Close() | |||
| 173 | + | } | |||
| 174 | + | if s.hw != nil { | |||
| 175 | + | s.hw.close() | |||
| 176 | + | } | |||
| 177 | + | } | |||
| 178 | + | ||||
| 179 | + | // newVulkanRenderer returns the concrete Vulkan renderer factory. Virtual | |||
| 180 | + | // outputs have no display to list modifiers, so a headless renderer is told | |||
| 181 | + | // it drives one: without it a real GPU refuses the HDR targets and the | |||
| 182 | + | // output falls back to SDR. readback makes the HDR targets readable | |||
| 183 | + | // (HDRPixels). These settings are Vulkan-only and not on ports.Renderer. | |||
| 184 | + | func newVulkanRenderer(log zerowrap.Logger, virtual, readback bool) func(w, h int) (ports.Renderer, error) { | |||
| 185 | + | return func(w, h int) (ports.Renderer, error) { | |||
| 186 | + | r, err := vulkan.New(w, h) | |||
| 187 | + | if err != nil { | |||
| 188 | + | return nil, err | |||
| 189 | + | } | |||
| 190 | + | r.SetLogger(log) | |||
| 191 | + | log.Info().Str("queue_priority", r.QueuePriority()).Msg("vulkan queue") | |||
| 192 | + | r.SetVirtualOutput(virtual) | |||
| 193 | + | if readback { | |||
| 194 | + | r.SetHDRReadback(true) | |||
| 195 | + | } | |||
| 196 | + | return r, nil | |||
| 197 | + | } | |||
| 198 | + | } | |||
| 199 | + | ||||
| 200 | + | // run starts the workers and waits for the session to end; it releases the | |||
| 201 | + | // session's resources after every worker has joined. | |||
| 202 | + | func (s *session) run(ctx context.Context, inject func(chan<- ports.InputEvent)) error { | |||
| 203 | + | defer s.close() | |||
| 204 | + | ctx, cancel := context.WithCancel(ctx) | |||
| 205 | + | defer cancel() | |||
| 206 | + | opts, log, p, hw, km, security := s.opts, s.log, s.p, s.hw, s.km, s.security | |||
| 207 | + | childEnv, curs, xdisplay, statePath := s.childEnv, s.curs, s.xdisplay, s.statePath | |||
| 208 | + | server, captureAllow, c, child, wsIDs, dmabuf, sizes, path := s.server, s.captureAllow, s.core, s.child, s.wsIDs, s.dmabuf, s.sizes, s.path | |||
| 209 | + | if opts.Session { | |||
| 210 | + | launcher.ExportSession(ctx, childEnv, logging.For(ctx, "launcher")) | |||
| 211 | + | launcher.NotifyReady(os.Getenv("NOTIFY_SOCKET"), logging.For(ctx, "launcher")) | |||
| 212 | + | defer launcher.UnexportSession(childEnv, logging.For(ctx, "launcher")) | |||
| 213 | + | } | |||
| 214 | + | if inject != nil { | |||
| 215 | + | inject(p.input) | |||
| 216 | + | } | |||
| 217 | + | var workers sync.WaitGroup | |||
| 218 | + | workers.Add(8) | |||
| 219 | + | done := make(chan error, 8) | |||
| 220 | + | go func() { | |||
| 221 | + | defer workers.Done() | |||
| 222 | + | done <- config.Watch(ctx, path, p.watched, logging.For(ctx, "config")) | |||
| 223 | + | }() | |||
| 224 | + | if p.scales != nil { | |||
| 225 | + | workers.Add(1) | |||
| 226 | + | go func() { | |||
| 227 | + | defer workers.Done() | |||
| 228 | + | core.PersistScales(ctx, p.scales, config.ScaleStore{Path: path}, launcher.NewNotifier(ctx, childEnv, logging.For(ctx, "launcher")), s.clock, time.Second) | |||
| 229 | + | }() | |||
| 230 | + | } | |||
| 231 | + | workers.Add(1) | |||
| 232 | + | go func() { | |||
| 233 | + | defer workers.Done() | |||
| 234 | + | relayConfig(ctx, opts.Config, p.watched, p.filtered, p.keymaps, p.deviceConfigs, curs, p.commands, logging.For(ctx, "config")) | |||
| 235 | + | }() | |||
| 236 | + | script := make(chan string) | |||
| 237 | + | go func() { | |||
| 238 | + | defer workers.Done() | |||
| 239 | + | if hw != nil { | |||
| 240 | + | // Core sends the first layout once DRM reports the outputs. | |||
| 241 | + | var layout ports.Layout | |||
| 242 | + | select { | |||
| 243 | + | case layout = <-p.layouts: | |||
| 244 | + | case <-ctx.Done(): | |||
| 245 | + | km.Close() | |||
| 246 | + | return | |||
| 247 | + | } | |||
| 248 | + | done <- safe("input", func() error { | |||
| 249 | + | if opts.Config.Performance.Realtime { | |||
| 250 | + | runtime.LockOSThread() | |||
| 251 | + | // Let this dedicated thread exit with its scheduling policy. | |||
| 252 | + | schedLog := logging.For(ctx, "sched") | |||
| 253 | + | if err := sched.Realtime(schedLog); err != nil { | |||
| 254 | + | schedLog.Warn().Err(err).Msg("input scheduling") | |||
| 255 | + | } | |||
| 256 | + | } | |||
| 257 | + | return libinput.Run(ctx, libinput.Options{ | |||
| 258 | + | Security: security, | |||
| 259 | + | Seat: hw.seat, | |||
| 260 | + | SeatName: hw.seat.Name(), | |||
| 261 | + | Keymap: km, | |||
| 262 | + | Keymaps: p.keymaps, | |||
| 263 | + | Layout: layout, | |||
| 264 | + | Layouts: p.layouts, | |||
| 265 | + | Constraints: p.constraints, | |||
| 266 | + | DeviceConfig: opts.Config.InputDevicesConfig, | |||
| 267 | + | DeviceConfigs: p.deviceConfigs, | |||
| 268 | + | Active: hw.seat.Subscribe(), | |||
| 269 | + | MoveCursor: curs.move, | |||
| 270 | + | Log: logging.For(ctx, "input"), | |||
| 271 | + | LogMotion: logging.Enabled(ctx, "input-motion"), | |||
| 272 | + | LogKeys: logging.Enabled(ctx, "input-keys"), | |||
| 273 | + | }, p.input) | |||
| 274 | + | }) | |||
| 275 | + | return | |||
| 276 | + | } | |||
| 277 | + | if err := headlessinput.RunSecure(ctx, km, p.keymaps, script, p.input, p.layouts, func(o string, x, y float64) { curs.move(o, x, y, true) }, logging.For(ctx, "input"), security); err != nil && !errors.Is(err, context.Canceled) { | |||
| 278 | + | select { | |||
| 279 | + | case done <- err: | |||
| 280 | + | case <-ctx.Done(): | |||
| 281 | + | } | |||
| 282 | + | } | |||
| 283 | + | }() | |||
| 284 | + | if opts.Script != nil { | |||
| 285 | + | go func() { <-ctx.Done(); _ = opts.Script.Close() }() | |||
| 286 | + | go func() { | |||
| 287 | + | defer close(script) | |||
| 288 | + | scanner := bufio.NewScanner(opts.Script) | |||
| 289 | + | for scanner.Scan() { | |||
| 290 | + | select { | |||
| 291 | + | case <-ctx.Done(): | |||
| 292 | + | return | |||
| 293 | + | case script <- scanner.Text(): | |||
| 294 | + | } | |||
| 295 | + | } | |||
| 296 | + | if err := scanner.Err(); err != nil { | |||
| 297 | + | log.Warn().Err(err).Msg("reading input script") | |||
| 298 | + | } | |||
| 299 | + | }() | |||
| 300 | + | } else { | |||
| 301 | + | close(script) | |||
| 302 | + | } | |||
| 303 | + | go func() { defer workers.Done(); done <- server.Run(ctx) }() | |||
| 304 | + | go func() { | |||
| 305 | + | defer workers.Done() | |||
| 306 | + | // Without a watch the allowlist stays as loaded: restart to apply. | |||
| 307 | + | if err := captureAllow.Run(ctx); err != nil { | |||
| 308 | + | log.Warn().Err(err).Msg("capture allowlist not watched, restart to apply changes") | |||
| 309 | + | } | |||
| 310 | + | }() | |||
| 311 | + | go func() { defer workers.Done(); done <- child.Run(ctx, p.spawn) }() | |||
| 312 | + | go func() { defer workers.Done(); done <- c.Run(ctx) }() | |||
| 313 | + | if xdisplay != nil { | |||
| 314 | + | workers.Add(1) | |||
| 315 | + | // X11 failing leaves the Wayland session running; Run logs why. | |||
| 316 | + | go func() { defer workers.Done(); _ = xdisplay.Run(ctx) }() | |||
| 317 | + | } | |||
| 318 | + | if statePath != "" { | |||
| 319 | + | workers.Add(1) | |||
| 320 | + | go func() { | |||
| 321 | + | defer workers.Done() | |||
| 322 | + | if err := statefile.Run(ctx, statePath, p.states, wsIDs, logging.For(ctx, "statefile")); err != nil { | |||
| 323 | + | // Scripts lose their state; the session goes on. | |||
| 324 | + | log.Warn().Err(err).Msg("state file disabled") | |||
| 325 | + | } | |||
| 326 | + | }() | |||
| 327 | + | } | |||
| 328 | + | go func() { | |||
| 329 | + | defer workers.Done() | |||
| 330 | + | consumeScenes(ctx, p.scenes, p.configErrors, opts.testScenes, p.renderScenes) | |||
| 331 | + | }() | |||
| 332 | + | workers.Add(1) | |||
| 333 | + | go func() { defer workers.Done(); relayPresented(ctx, p.flips, p.presented, p.frames) }() | |||
| 334 | + | ||||
| 335 | + | go func() { | |||
| 336 | + | defer workers.Done() | |||
| 337 | + | outputIO := p.outputs(security) | |||
| 338 | + | apply := newOutputApply(newOutputOverrides(opts.Config, hw == nil), logging.For(ctx, "app")) | |||
| 339 | + | if hw != nil { | |||
| 340 | + | done <- safe("output", func() error { | |||
| 341 | + | want := func(cfg ports.Config) drm.Want { | |||
| 342 | + | w := wantFromConfig(cfg) | |||
| 343 | + | w.Sampled = dmabuf.Formats | |||
| 344 | + | w.TraceFlips = logging.Enabled(ctx, "drm-flip") | |||
| 345 | + | return w | |||
| 346 | + | } | |||
| 347 | + | return hw.runOutputs(ctx, want, opts.Config, apply, outputIO, curs, s.newRenderer, logging.For(ctx, "drm")) | |||
| 348 | + | }) | |||
| 349 | + | return | |||
| 350 | + | } | |||
| 351 | + | done <- runHeadless(ctx, headlessOptions{sizes: sizes, shots: opts.ScreenshotDir, hdr: opts.HeadlessHDR, raw: opts.ScreenshotRaw}, apply, outputIO, curs, s.newRenderer, logging.For(ctx, "render")) | |||
| 352 | + | }() | |||
| 353 | + | ||||
| 354 | + | if hw != nil { | |||
| 355 | + | workers.Add(1) | |||
| 356 | + | go func() { defer workers.Done(); done <- safe("seat", func() error { return hw.seat.Run(ctx) }) }() | |||
| 357 | + | // Only a real session serves the session bus: headless runs would take | |||
| 358 | + | // the name from the desktop they run in. | |||
| 359 | + | workers.Add(1) | |||
| 360 | + | go func() { | |||
| 361 | + | defer workers.Done() | |||
| 362 | + | reports := screensaver.Reports{Held: p.idleInhibited, Activity: p.idleActivity} | |||
| 363 | + | screensaver.Serve(ctx, "", reports, screensaverRetry, logging.For(ctx, "screensaver")) | |||
| 364 | + | }() | |||
| 365 | + | } | |||
| 366 | + | var result error | |||
| 367 | + | select { | |||
| 368 | + | case <-ctx.Done(): | |||
| 369 | + | if errors.Is(context.Cause(ctx), errTimeout) { | |||
| 370 | + | log.Info().Str("reason", "timeout").Dur("after", opts.Timeout).Msg("stopping") | |||
| 371 | + | } | |||
| 372 | + | case result = <-done: | |||
| 373 | + | log.Info().AnErr("cause", result).Msg("stopping") | |||
| 374 | + | } | |||
| 375 | + | cancel() | |||
| 376 | + | workers.Wait() | |||
| 377 | + | // The wayland server has stopped: no request can be queued any more. | |||
| 378 | + | drainCaptures(ctx, p.captures, nil) | |||
| 379 | + | // A keymap the input goroutine never took is still ours to free. | |||
| 380 | + | select { | |||
| 381 | + | case km := <-p.keymaps: | |||
| 382 | + | km.Close() | |||
| 383 | + | default: | |||
| 384 | + | } | |||
| 385 | + | for len(done) > 0 { | |||
| 386 | + | err := <-done | |||
| 387 | + | if result == nil && err != nil { | |||
| 388 | + | result = err | |||
| 389 | + | } | |||
| 390 | + | } | |||
| 391 | + | if errors.Is(result, core.ErrQuit) || errors.Is(result, libinput.ErrEmergencyQuit) { | |||
| 392 | + | return nil | |||
| 393 | + | } | |||
| 394 | + | return result | |||
| 395 | + | } | |||
M internal/app/wiring.go +2 -2
| @@ -118,8 +118,8 @@ func newPipes(realSession bool) *pipes { | |||
| 118 | 118 | return p | |
| 119 | 119 | } | |
| 120 | 120 | ||
| 121 | - | // core is core's side. Security, Terminal and Clock are not channels: the | |
| 122 | - | // caller sets them. | |
| 121 | + | // core is core's side: channels only. The security gate, clock and terminal | |
| 122 | + | // flag are not channels; assembleSession passes them as core.Options. | |
| 123 | 123 | func (p *pipes) core() core.Channels { | |
| 124 | 124 | return core.Channels{ | |
| 125 | 125 | Client: p.client, | |
| @@ -118,8 +118,8 @@ func newPipes(realSession bool) *pipes { | |||||
| 118 | return p | 118 | return p | ||
| 119 | } | 119 | } | ||
| 120 | 120 | ||||
| 121 | - | // core is core's side. Security, Terminal and Clock are not channels: the | 121 | + | // core is core's side: channels only. The security gate, clock and terminal |
| 122 | - | // caller sets them. | 122 | + | // flag are not channels; assembleSession passes them as core.Options. |
| 123 | func (p *pipes) core() core.Channels { | 123 | func (p *pipes) core() core.Channels { | ||
| 124 | return core.Channels{ | 124 | return core.Channels{ | ||
| 125 | Client: p.client, | 125 | Client: p.client, | ||
M internal/core/action_test.go +1 -1
| @@ -31,7 +31,7 @@ func TestDefaultBindsAccepted(t *testing.T) { | |||
| 31 | 31 | if cmd == "alt" { | |
| 32 | 32 | cfg = altCmdDefaults() | |
| 33 | 33 | } | |
| 34 | - | if _, err := core.New(cfg, core.Channels{Scenes: make(chan []ports.Scene, 1)}); err != nil { | |
| 34 | + | if _, err := core.New(cfg, core.Channels{Scenes: make(chan []ports.Scene, 1)}, core.Options{}); err != nil { | |
| 35 | 35 | t.Fatalf("cmd=%s: %v", cmd, err) | |
| 36 | 36 | } | |
| 37 | 37 | } | |
| @@ -31,7 +31,7 @@ func TestDefaultBindsAccepted(t *testing.T) { | |||||
| 31 | if cmd == "alt" { | 31 | if cmd == "alt" { | ||
| 32 | cfg = altCmdDefaults() | 32 | cfg = altCmdDefaults() | ||
| 33 | } | 33 | } | ||
| 34 | - | if _, err := core.New(cfg, core.Channels{Scenes: make(chan []ports.Scene, 1)}); err != nil { | 34 | + | if _, err := core.New(cfg, core.Channels{Scenes: make(chan []ports.Scene, 1)}, core.Options{}); err != nil { |
| 35 | t.Fatalf("cmd=%s: %v", cmd, err) | 35 | t.Fatalf("cmd=%s: %v", cmd, err) | ||
| 36 | } | 36 | } | ||
| 37 | } | 37 | } | ||
M internal/core/animations_interrupt_internal_test.go +1 -1
| @@ -43,7 +43,7 @@ func TestSessionLockMidRectMotion(t *testing.T) { | |||
| 43 | 43 | state := ports.SecurityState{} | |
| 44 | 44 | gate := portsmocks.NewMockSessionSecurity(t) | |
| 45 | 45 | gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return state }) | |
| 46 | - | c.ch.Security = gate | |
| 46 | + | c.opts.Security = gate | |
| 47 | 47 | sc := c.cur() | |
| 48 | 48 | movingRects(t, c, ic, sc) | |
| 49 | 49 | if !c.animating() || c.frameC == nil || sc.last.Seq == 0 { | |
| @@ -43,7 +43,7 @@ func TestSessionLockMidRectMotion(t *testing.T) { | |||||
| 43 | state := ports.SecurityState{} | 43 | state := ports.SecurityState{} | ||
| 44 | gate := portsmocks.NewMockSessionSecurity(t) | 44 | gate := portsmocks.NewMockSessionSecurity(t) | ||
| 45 | gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return state }) | 45 | gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return state }) | ||
| 46 | - | c.ch.Security = gate | 46 | + | c.opts.Security = gate |
| 47 | sc := c.cur() | 47 | sc := c.cur() | ||
| 48 | movingRects(t, c, ic, sc) | 48 | movingRects(t, c, ic, sc) | ||
| 49 | if !c.animating() || c.frameC == nil || sc.last.Seq == 0 { | 49 | if !c.animating() || c.frameC == nil || sc.last.Seq == 0 { | ||
M internal/core/capindicator.go +3 -3
| @@ -51,7 +51,7 @@ func (c *Core) captureFrame(v ports.CaptureFrameTaken) bool { | |||
| 51 | 51 | // Nothing of the target is left on screen to mark. | |
| 52 | 52 | return changed | |
| 53 | 53 | } | |
| 54 | - | return c.capt.flash(t, c.now(), c.ch.Clock) || changed | |
| 54 | + | return c.capt.flash(t, c.now(), c.opts.Clock) || changed | |
| 55 | 55 | } | |
| 56 | 56 | ||
| 57 | 57 | // flash marks t until ports.CaptureFlash after now and arms the timer. It | |
| @@ -153,7 +153,7 @@ func (c *Core) captureFlashTick() bool { | |||
| 153 | 153 | } | |
| 154 | 154 | now := c.now() | |
| 155 | 155 | c.capt.expire(now) | |
| 156 | - | c.capt.armTimer(now, c.ch.Clock) | |
| 156 | + | c.capt.armTimer(now, c.opts.Clock) | |
| 157 | 157 | return len(c.capt.flashes) != n | |
| 158 | 158 | } | |
| 159 | 159 | ||
| @@ -163,7 +163,7 @@ func (c *Core) armCaptureTimer() { | |||
| 163 | 163 | return | |
| 164 | 164 | } | |
| 165 | 165 | if c.capt.timerStop == nil { | |
| 166 | - | c.capt.armTimer(c.now(), c.ch.Clock) | |
| 166 | + | c.capt.armTimer(c.now(), c.opts.Clock) | |
| 167 | 167 | } | |
| 168 | 168 | } | |
| 169 | 169 | ||
| @@ -51,7 +51,7 @@ func (c *Core) captureFrame(v ports.CaptureFrameTaken) bool { | |||||
| 51 | // Nothing of the target is left on screen to mark. | 51 | // Nothing of the target is left on screen to mark. | ||
| 52 | return changed | 52 | return changed | ||
| 53 | } | 53 | } | ||
| 54 | - | return c.capt.flash(t, c.now(), c.ch.Clock) || changed | 54 | + | return c.capt.flash(t, c.now(), c.opts.Clock) || changed |
| 55 | } | 55 | } | ||
| 56 | 56 | ||||
| 57 | // flash marks t until ports.CaptureFlash after now and arms the timer. It | 57 | // flash marks t until ports.CaptureFlash after now and arms the timer. It | ||
| @@ -153,7 +153,7 @@ func (c *Core) captureFlashTick() bool { | |||||
| 153 | } | 153 | } | ||
| 154 | now := c.now() | 154 | now := c.now() | ||
| 155 | c.capt.expire(now) | 155 | c.capt.expire(now) | ||
| 156 | - | c.capt.armTimer(now, c.ch.Clock) | 156 | + | c.capt.armTimer(now, c.opts.Clock) |
| 157 | return len(c.capt.flashes) != n | 157 | return len(c.capt.flashes) != n | ||
| 158 | } | 158 | } | ||
| 159 | 159 | ||||
| @@ -163,7 +163,7 @@ func (c *Core) armCaptureTimer() { | |||||
| 163 | return | 163 | return | ||
| 164 | } | 164 | } | ||
| 165 | if c.capt.timerStop == nil { | 165 | if c.capt.timerStop == nil { | ||
| 166 | - | c.capt.armTimer(c.now(), c.ch.Clock) | 166 | + | c.capt.armTimer(c.now(), c.opts.Clock) |
| 167 | } | 167 | } | ||
| 168 | } | 168 | } | ||
| 169 | 169 | ||||
M internal/core/capindicator_test.go +5 -5
| @@ -39,7 +39,7 @@ func indicatorCore(t *testing.T) (*Core, *indicatorClock) { | |||
| 39 | 39 | cfg.Keyboard.CmdKey = "super" | |
| 40 | 40 | cfg.Layout.MaxColumns = 2 | |
| 41 | 41 | cfg.Border.Width = 0 | |
| 42 | - | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Commands: make(chan ports.ClientCommand, 64), Clock: clock}) | |
| 42 | + | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Commands: make(chan ports.ClientCommand, 64)}, Options{Clock: clock}) | |
| 43 | 43 | if err != nil { | |
| 44 | 44 | t.Fatal(err) | |
| 45 | 45 | } | |
| @@ -109,7 +109,7 @@ func TestCaptureIndicatorFlashClearedByProtectionAndWorksAfterUnlock(t *testing. | |||
| 109 | 109 | state := ports.SecurityState{Generation: 1} | |
| 110 | 110 | gate := portsmocks.NewMockSessionSecurity(t) | |
| 111 | 111 | gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return state }).Maybe() | |
| 112 | - | c.ch.Security = gate | |
| 112 | + | c.opts.Security = gate | |
| 113 | 113 | if !c.captureFrame(ports.CaptureFrameTaken{Output: "A"}) { | |
| 114 | 114 | t.Fatal("first frame shows nothing new") | |
| 115 | 115 | } | |
| @@ -200,7 +200,7 @@ func TestCaptureIndicatorHiddenWorkspacePill(t *testing.T) { | |||
| 200 | 200 | timer.EXPECT().Stop().Return(true).Maybe() | |
| 201 | 201 | timer.EXPECT().Reset(mock.Anything).Return(false).Maybe() | |
| 202 | 202 | clock.EXPECT().NewTimer(mock.Anything).Return(timer).Maybe() | |
| 203 | - | c.ch.Clock = clock | |
| 203 | + | c.opts.Clock = clock | |
| 204 | 204 | c.configures.cw.reset() | |
| 205 | 205 | c.captureOpen(ports.CaptureSessionOpen{ID: 1, Workspace: ws.ID}) | |
| 206 | 206 | c.captureFrame(ports.CaptureFrameTaken{Session: 1, Workspace: ws.ID}) | |
| @@ -238,7 +238,7 @@ func TestCaptureIndicatorNoAllocationWhenIdle(t *testing.T) { | |||
| 238 | 238 | } | |
| 239 | 239 | // A live flash, steady state, allocates nothing either (measured on the | |
| 240 | 240 | // system clock: the generated clock mock allocates on every call). | |
| 241 | - | c.ch.Clock = nil | |
| 241 | + | c.opts.Clock = nil | |
| 242 | 242 | c.captureFrame(ports.CaptureFrameTaken{Output: "A"}) | |
| 243 | 243 | if n := testing.AllocsPerRun(100, func() { c.captureExpire(); c.armCaptureTimer() }); n != 0 { | |
| 244 | 244 | t.Fatalf("%v allocations keeping a flash", n) | |
| @@ -301,7 +301,7 @@ func TestCaptureIndicatorInflatesThinTargets(t *testing.T) { | |||
| 301 | 301 | // scratch or shared while unchanged. | |
| 302 | 302 | func TestCaptureRecordingPublishAllocations(t *testing.T) { | |
| 303 | 303 | c, _ := indicatorCore(t) | |
| 304 | - | c.ch.Clock = nil | |
| 304 | + | c.opts.Clock = nil | |
| 305 | 305 | cmds := make(chan ports.ClientCommand, 64) | |
| 306 | 306 | c.ch.Commands = cmds | |
| 307 | 307 | c.captureOpen(ports.CaptureSessionOpen{ID: 1, Output: "A", Region: Rect{X: 10, Y: 10, W: 50, H: 40}}) | |
| @@ -39,7 +39,7 @@ func indicatorCore(t *testing.T) (*Core, *indicatorClock) { | |||||
| 39 | cfg.Keyboard.CmdKey = "super" | 39 | cfg.Keyboard.CmdKey = "super" | ||
| 40 | cfg.Layout.MaxColumns = 2 | 40 | cfg.Layout.MaxColumns = 2 | ||
| 41 | cfg.Border.Width = 0 | 41 | cfg.Border.Width = 0 | ||
| 42 | - | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Commands: make(chan ports.ClientCommand, 64), Clock: clock}) | 42 | + | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Commands: make(chan ports.ClientCommand, 64)}, Options{Clock: clock}) |
| 43 | if err != nil { | 43 | if err != nil { | ||
| 44 | t.Fatal(err) | 44 | t.Fatal(err) | ||
| 45 | } | 45 | } | ||
| @@ -109,7 +109,7 @@ func TestCaptureIndicatorFlashClearedByProtectionAndWorksAfterUnlock(t *testing. | |||||
| 109 | state := ports.SecurityState{Generation: 1} | 109 | state := ports.SecurityState{Generation: 1} | ||
| 110 | gate := portsmocks.NewMockSessionSecurity(t) | 110 | gate := portsmocks.NewMockSessionSecurity(t) | ||
| 111 | gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return state }).Maybe() | 111 | gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return state }).Maybe() | ||
| 112 | - | c.ch.Security = gate | 112 | + | c.opts.Security = gate |
| 113 | if !c.captureFrame(ports.CaptureFrameTaken{Output: "A"}) { | 113 | if !c.captureFrame(ports.CaptureFrameTaken{Output: "A"}) { | ||
| 114 | t.Fatal("first frame shows nothing new") | 114 | t.Fatal("first frame shows nothing new") | ||
| 115 | } | 115 | } | ||
| @@ -200,7 +200,7 @@ func TestCaptureIndicatorHiddenWorkspacePill(t *testing.T) { | |||||
| 200 | timer.EXPECT().Stop().Return(true).Maybe() | 200 | timer.EXPECT().Stop().Return(true).Maybe() | ||
| 201 | timer.EXPECT().Reset(mock.Anything).Return(false).Maybe() | 201 | timer.EXPECT().Reset(mock.Anything).Return(false).Maybe() | ||
| 202 | clock.EXPECT().NewTimer(mock.Anything).Return(timer).Maybe() | 202 | clock.EXPECT().NewTimer(mock.Anything).Return(timer).Maybe() | ||
| 203 | - | c.ch.Clock = clock | 203 | + | c.opts.Clock = clock |
| 204 | c.configures.cw.reset() | 204 | c.configures.cw.reset() | ||
| 205 | c.captureOpen(ports.CaptureSessionOpen{ID: 1, Workspace: ws.ID}) | 205 | c.captureOpen(ports.CaptureSessionOpen{ID: 1, Workspace: ws.ID}) | ||
| 206 | c.captureFrame(ports.CaptureFrameTaken{Session: 1, Workspace: ws.ID}) | 206 | c.captureFrame(ports.CaptureFrameTaken{Session: 1, Workspace: ws.ID}) | ||
| @@ -238,7 +238,7 @@ func TestCaptureIndicatorNoAllocationWhenIdle(t *testing.T) { | |||||
| 238 | } | 238 | } | ||
| 239 | // A live flash, steady state, allocates nothing either (measured on the | 239 | // A live flash, steady state, allocates nothing either (measured on the | ||
| 240 | // system clock: the generated clock mock allocates on every call). | 240 | // system clock: the generated clock mock allocates on every call). | ||
| 241 | - | c.ch.Clock = nil | 241 | + | c.opts.Clock = nil |
| 242 | c.captureFrame(ports.CaptureFrameTaken{Output: "A"}) | 242 | c.captureFrame(ports.CaptureFrameTaken{Output: "A"}) | ||
| 243 | if n := testing.AllocsPerRun(100, func() { c.captureExpire(); c.armCaptureTimer() }); n != 0 { | 243 | if n := testing.AllocsPerRun(100, func() { c.captureExpire(); c.armCaptureTimer() }); n != 0 { | ||
| 244 | t.Fatalf("%v allocations keeping a flash", n) | 244 | t.Fatalf("%v allocations keeping a flash", n) | ||
| @@ -301,7 +301,7 @@ func TestCaptureIndicatorInflatesThinTargets(t *testing.T) { | |||||
| 301 | // scratch or shared while unchanged. | 301 | // scratch or shared while unchanged. | ||
| 302 | func TestCaptureRecordingPublishAllocations(t *testing.T) { | 302 | func TestCaptureRecordingPublishAllocations(t *testing.T) { | ||
| 303 | c, _ := indicatorCore(t) | 303 | c, _ := indicatorCore(t) | ||
| 304 | - | c.ch.Clock = nil | 304 | + | c.opts.Clock = nil |
| 305 | cmds := make(chan ports.ClientCommand, 64) | 305 | cmds := make(chan ports.ClientCommand, 64) | ||
| 306 | c.ch.Commands = cmds | 306 | c.ch.Commands = cmds | ||
| 307 | c.captureOpen(ports.CaptureSessionOpen{ID: 1, Output: "A", Region: Rect{X: 10, Y: 10, W: 50, H: 40}}) | 307 | c.captureOpen(ports.CaptureSessionOpen{ID: 1, Output: "A", Region: Rect{X: 10, Y: 10, W: 50, H: 40}}) | ||
M internal/core/capture_test.go +2 -2
| @@ -33,7 +33,7 @@ func captureCore(t *testing.T) *captureRig { | |||
| 33 | 33 | scenes: make(chan []ports.Scene, 1), | |
| 34 | 34 | workspaces: make(chan ports.Workspaces, 1), | |
| 35 | 35 | } | |
| 36 | - | c, err := core.New(cfg, core.Channels{Client: r.client, Output: r.output, Commands: r.commands, Scenes: r.scenes, Workspaces: r.workspaces}) | |
| 36 | + | c, err := core.New(cfg, core.Channels{Client: r.client, Output: r.output, Commands: r.commands, Scenes: r.scenes, Workspaces: r.workspaces}, core.Options{}) | |
| 37 | 37 | if err != nil { | |
| 38 | 38 | t.Fatal(err) | |
| 39 | 39 | } | |
| @@ -514,7 +514,7 @@ func TestCaptureFrameFlashRunsOnTheCoreClock(t *testing.T) { | |||
| 514 | 514 | client := make(chan ports.ClientEvent, 4) | |
| 515 | 515 | output := make(chan ports.OutputEvent, 4) | |
| 516 | 516 | scenes := make(chan []ports.Scene, 1) | |
| 517 | - | c, err := core.New(cfg, core.Channels{Client: client, Output: output, Commands: make(chan ports.ClientCommand, 64), Scenes: scenes, Clock: clock}) | |
| 517 | + | c, err := core.New(cfg, core.Channels{Client: client, Output: output, Commands: make(chan ports.ClientCommand, 64), Scenes: scenes}, core.Options{Clock: clock}) | |
| 518 | 518 | if err != nil { | |
| 519 | 519 | t.Fatal(err) | |
| 520 | 520 | } | |
| @@ -33,7 +33,7 @@ func captureCore(t *testing.T) *captureRig { | |||||
| 33 | scenes: make(chan []ports.Scene, 1), | 33 | scenes: make(chan []ports.Scene, 1), | ||
| 34 | workspaces: make(chan ports.Workspaces, 1), | 34 | workspaces: make(chan ports.Workspaces, 1), | ||
| 35 | } | 35 | } | ||
| 36 | - | c, err := core.New(cfg, core.Channels{Client: r.client, Output: r.output, Commands: r.commands, Scenes: r.scenes, Workspaces: r.workspaces}) | 36 | + | c, err := core.New(cfg, core.Channels{Client: r.client, Output: r.output, Commands: r.commands, Scenes: r.scenes, Workspaces: r.workspaces}, core.Options{}) |
| 37 | if err != nil { | 37 | if err != nil { | ||
| 38 | t.Fatal(err) | 38 | t.Fatal(err) | ||
| 39 | } | 39 | } | ||
| @@ -514,7 +514,7 @@ func TestCaptureFrameFlashRunsOnTheCoreClock(t *testing.T) { | |||||
| 514 | client := make(chan ports.ClientEvent, 4) | 514 | client := make(chan ports.ClientEvent, 4) | ||
| 515 | output := make(chan ports.OutputEvent, 4) | 515 | output := make(chan ports.OutputEvent, 4) | ||
| 516 | scenes := make(chan []ports.Scene, 1) | 516 | scenes := make(chan []ports.Scene, 1) | ||
| 517 | - | c, err := core.New(cfg, core.Channels{Client: client, Output: output, Commands: make(chan ports.ClientCommand, 64), Scenes: scenes, Clock: clock}) | 517 | + | c, err := core.New(cfg, core.Channels{Client: client, Output: output, Commands: make(chan ports.ClientCommand, 64), Scenes: scenes}, core.Options{Clock: clock}) |
| 518 | if err != nil { | 518 | if err != nil { | ||
| 519 | t.Fatal(err) | 519 | t.Fatal(err) | ||
| 520 | } | 520 | } | ||
M internal/core/capture_workspace_test.go +1 -3
| @@ -25,9 +25,7 @@ func hiddenCaptureCommands(t *testing.T) (*Core, *Workspace, chan ports.ClientCo | |||
| 25 | 25 | cfg.Layout.MaxColumns = 2 | |
| 26 | 26 | cfg.Border.Width = 2 | |
| 27 | 27 | cfg.Background.Color = "#102030" | |
| 28 | - | c, err := New(cfg, Channels{ | |
| 29 | - | Scenes: make(chan []ports.Scene, 1), Commands: commands, | |
| 30 | - | }) | |
| 28 | + | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Commands: commands}, Options{}) | |
| 31 | 29 | if err != nil { | |
| 32 | 30 | t.Fatal(err) | |
| 33 | 31 | } | |
| @@ -25,9 +25,7 @@ func hiddenCaptureCommands(t *testing.T) (*Core, *Workspace, chan ports.ClientCo | |||||
| 25 | cfg.Layout.MaxColumns = 2 | 25 | cfg.Layout.MaxColumns = 2 | ||
| 26 | cfg.Border.Width = 2 | 26 | cfg.Border.Width = 2 | ||
| 27 | cfg.Background.Color = "#102030" | 27 | cfg.Background.Color = "#102030" | ||
| 28 | - | c, err := New(cfg, Channels{ | 28 | + | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Commands: commands}, Options{}) |
| 29 | - | Scenes: make(chan []ports.Scene, 1), Commands: commands, | |||
| 30 | - | }) | |||
| 31 | if err != nil { | 29 | if err != nil { | ||
| 32 | t.Fatal(err) | 30 | t.Fatal(err) | ||
| 33 | } | 31 | } | ||
M internal/core/constraint_frame_test.go +1 -4
| @@ -15,10 +15,7 @@ func partlyShown(t *testing.T) *Core { | |||
| 15 | 15 | var cfg ports.Config | |
| 16 | 16 | cfg.Keyboard.CmdKey = "super" | |
| 17 | 17 | cfg.Layout.MaxColumns = 2 | |
| 18 | - | c, err := New(cfg, Channels{ | |
| 19 | - | Scenes: make(chan []ports.Scene, 1), Layouts: make(chan ports.Layout, 1), Constraints: make(chan ports.PointerConstraint, 1), | |
| 20 | - | State: make(chan ports.State, 1), Workspaces: make(chan ports.Workspaces, 1), Commands: make(chan ports.ClientCommand, 16), | |
| 21 | - | }) | |
| 18 | + | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Layouts: make(chan ports.Layout, 1), Constraints: make(chan ports.PointerConstraint, 1), State: make(chan ports.State, 1), Workspaces: make(chan ports.Workspaces, 1), Commands: make(chan ports.ClientCommand, 16)}, Options{}) | |
| 22 | 19 | if err != nil { | |
| 23 | 20 | t.Fatal(err) | |
| 24 | 21 | } | |
| @@ -15,10 +15,7 @@ func partlyShown(t *testing.T) *Core { | |||||
| 15 | var cfg ports.Config | 15 | var cfg ports.Config | ||
| 16 | cfg.Keyboard.CmdKey = "super" | 16 | cfg.Keyboard.CmdKey = "super" | ||
| 17 | cfg.Layout.MaxColumns = 2 | 17 | cfg.Layout.MaxColumns = 2 | ||
| 18 | - | c, err := New(cfg, Channels{ | 18 | + | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Layouts: make(chan ports.Layout, 1), Constraints: make(chan ports.PointerConstraint, 1), State: make(chan ports.State, 1), Workspaces: make(chan ports.Workspaces, 1), Commands: make(chan ports.ClientCommand, 16)}, Options{}) |
| 19 | - | Scenes: make(chan []ports.Scene, 1), Layouts: make(chan ports.Layout, 1), Constraints: make(chan ports.PointerConstraint, 1), | |||
| 20 | - | State: make(chan ports.State, 1), Workspaces: make(chan ports.Workspaces, 1), Commands: make(chan ports.ClientCommand, 16), | |||
| 21 | - | }) | |||
| 22 | if err != nil { | 19 | if err != nil { | ||
| 23 | t.Fatal(err) | 20 | t.Fatal(err) | ||
| 24 | } | 21 | } | ||
M internal/core/core.go +17 -12
| @@ -21,8 +21,6 @@ var errSecurityChanged = errors.New("security epoch changed during input") | |||
| 21 | 21 | // its only sender and may drain a stale set; consumers only receive. Each | |
| 22 | 22 | // send holds one scene per output. | |
| 23 | 23 | type Channels struct { | |
| 24 | - | // Security is the defensive session gate; nil preserves standalone operation. | |
| 25 | - | Security ports.SessionSecurity | |
| 26 | 24 | Client <-chan ports.ClientEvent | |
| 27 | 25 | Input <-chan ports.InputEvent | |
| 28 | 26 | Output <-chan ports.OutputEvent | |
| @@ -45,16 +43,22 @@ type Channels struct { | |||
| 45 | 43 | // Scales, when set, receives each scale a scale bind sets, to persist it. | |
| 46 | 44 | // Core does not wait: a full channel drops the change. | |
| 47 | 45 | Scales chan<- ports.ScaleChanged | |
| 48 | - | // Terminal enables automatic terminal opening according to terminal.auto-open. | |
| 49 | - | Terminal bool | |
| 50 | - | // Clock tells the time for fullscreenGrace and slides; nil is the | |
| 51 | - | // system clock. | |
| 52 | - | Clock ports.Clock | |
| 53 | 46 | // Frames, when set, reports outputs' page flips: a running slide moves | |
| 54 | 47 | // one step per flip. Without flips it moves on a timer. | |
| 55 | 48 | Frames <-chan ports.OutputFrame | |
| 56 | 49 | } | |
| 57 | 50 | ||
| 51 | + | // Options are the services and policy core is constructed with. | |
| 52 | + | type Options struct { | |
| 53 | + | // Security is the defensive session gate; nil preserves standalone operation. | |
| 54 | + | Security ports.SessionSecurity | |
| 55 | + | // Clock tells the time for fullscreenGrace and slides; nil is the | |
| 56 | + | // system clock. | |
| 57 | + | Clock ports.Clock | |
| 58 | + | // Terminal enables automatic terminal opening according to terminal.auto-open. | |
| 59 | + | Terminal bool | |
| 60 | + | } | |
| 61 | + | ||
| 58 | 62 | // fullscreenGrace is how long after mapping a window's fullscreen request | |
| 59 | 63 | // is ignored. Wine asks for fullscreen whenever a window has the monitor's | |
| 60 | 64 | // size, so a launcher that remembered a fullscreen size would open | |
| @@ -81,6 +85,7 @@ type Core struct { | |||
| 81 | 85 | startup [][]string | |
| 82 | 86 | nextWorkspaceID uint64 | |
| 83 | 87 | ch Channels | |
| 88 | + | opts Options | |
| 84 | 89 | cfg ports.Config | |
| 85 | 90 | screens []*screen | |
| 86 | 91 | focusScreen int | |
| @@ -350,12 +355,12 @@ func (c *Core) spawnSlots(ctx context.Context, shown bool) error { | |||
| 350 | 355 | } | |
| 351 | 356 | return nil | |
| 352 | 357 | } | |
| 353 | - | func New(cfg ports.Config, ch Channels) (*Core, error) { | |
| 358 | + | func New(cfg ports.Config, ch Channels, opts Options) (*Core, error) { | |
| 354 | 359 | if cap(ch.Scenes) != 1 || (ch.Layouts != nil && cap(ch.Layouts) != 1) || (ch.Constraints != nil && cap(ch.Constraints) != 1) || (ch.State != nil && cap(ch.State) != 1) || (ch.Workspaces != nil && cap(ch.Workspaces) != 1) { | |
| 355 | 360 | return nil, fmt.Errorf("scenes, layouts, constraints, state and workspaces must have capacity 1") | |
| 356 | 361 | } | |
| 357 | 362 | // A placeholder screen holds windows until the first output arrives. | |
| 358 | - | c := &Core{inputKeys: map[string]bool{}, slots: map[slotKey]*slotState{}, placement: newSpawnPlacement(), windows: newWindowRegistry(), popups: map[WindowID]*popupState{}, ch: ch, pressed: map[string]bool{}, buttons: map[uint32]bool{}, swallow: map[uint32]bool{}, configures: newConfigures()} | |
| 363 | + | c := &Core{inputKeys: map[string]bool{}, slots: map[slotKey]*slotState{}, placement: newSpawnPlacement(), windows: newWindowRegistry(), popups: map[WindowID]*popupState{}, ch: ch, opts: opts, pressed: map[string]bool{}, buttons: map[uint32]bool{}, swallow: map[uint32]bool{}, configures: newConfigures()} | |
| 359 | 364 | c.screens = []*screen{{mon: newMonitorWithIDs("", "", &c.nextWorkspaceID), scale: 1, cfgScale: 1}} | |
| 360 | 365 | if err := c.apply(cfg); err != nil { | |
| 361 | 366 | return nil, err | |
| @@ -386,8 +391,8 @@ func (t sysTimer) Stop() bool { return t.t.Stop() } | |||
| 386 | 391 | func (t sysTimer) Reset(d time.Duration) bool { return t.t.Reset(d) } | |
| 387 | 392 | ||
| 388 | 393 | func (c *Core) now() time.Time { | |
| 389 | - | if c.ch.Clock != nil { | |
| 390 | - | return c.ch.Clock.Now() | |
| 394 | + | if c.opts.Clock != nil { | |
| 395 | + | return c.opts.Clock.Now() | |
| 391 | 396 | } | |
| 392 | 397 | return time.Now() | |
| 393 | 398 | } | |
| @@ -396,7 +401,7 @@ func (c *Core) command(ctx context.Context, v ports.ClientCommand) error { | |||
| 396 | 401 | if c.inputEpochChanged() { | |
| 397 | 402 | return errSecurityChanged | |
| 398 | 403 | } | |
| 399 | - | if c.ch.Security != nil { | |
| 404 | + | if c.opts.Security != nil { | |
| 400 | 405 | v = ports.SecurityCommand{State: c.security, Command: v} | |
| 401 | 406 | } | |
| 402 | 407 | select { | |
| @@ -21,8 +21,6 @@ var errSecurityChanged = errors.New("security epoch changed during input") | |||||
| 21 | // its only sender and may drain a stale set; consumers only receive. Each | 21 | // its only sender and may drain a stale set; consumers only receive. Each | ||
| 22 | // send holds one scene per output. | 22 | // send holds one scene per output. | ||
| 23 | type Channels struct { | 23 | type Channels struct { | ||
| 24 | - | // Security is the defensive session gate; nil preserves standalone operation. | |||
| 25 | - | Security ports.SessionSecurity | |||
| 26 | Client <-chan ports.ClientEvent | 24 | Client <-chan ports.ClientEvent | ||
| 27 | Input <-chan ports.InputEvent | 25 | Input <-chan ports.InputEvent | ||
| 28 | Output <-chan ports.OutputEvent | 26 | Output <-chan ports.OutputEvent | ||
| @@ -45,16 +43,22 @@ type Channels struct { | |||||
| 45 | // Scales, when set, receives each scale a scale bind sets, to persist it. | 43 | // Scales, when set, receives each scale a scale bind sets, to persist it. | ||
| 46 | // Core does not wait: a full channel drops the change. | 44 | // Core does not wait: a full channel drops the change. | ||
| 47 | Scales chan<- ports.ScaleChanged | 45 | Scales chan<- ports.ScaleChanged | ||
| 48 | - | // Terminal enables automatic terminal opening according to terminal.auto-open. | |||
| 49 | - | Terminal bool | |||
| 50 | - | // Clock tells the time for fullscreenGrace and slides; nil is the | |||
| 51 | - | // system clock. | |||
| 52 | - | Clock ports.Clock | |||
| 53 | // Frames, when set, reports outputs' page flips: a running slide moves | 46 | // Frames, when set, reports outputs' page flips: a running slide moves | ||
| 54 | // one step per flip. Without flips it moves on a timer. | 47 | // one step per flip. Without flips it moves on a timer. | ||
| 55 | Frames <-chan ports.OutputFrame | 48 | Frames <-chan ports.OutputFrame | ||
| 56 | } | 49 | } | ||
| 57 | 50 | ||||
| 51 | + | // Options are the services and policy core is constructed with. | |||
| 52 | + | type Options struct { | |||
| 53 | + | // Security is the defensive session gate; nil preserves standalone operation. | |||
| 54 | + | Security ports.SessionSecurity | |||
| 55 | + | // Clock tells the time for fullscreenGrace and slides; nil is the | |||
| 56 | + | // system clock. | |||
| 57 | + | Clock ports.Clock | |||
| 58 | + | // Terminal enables automatic terminal opening according to terminal.auto-open. | |||
| 59 | + | Terminal bool | |||
| 60 | + | } | |||
| 61 | + | ||||
| 58 | // fullscreenGrace is how long after mapping a window's fullscreen request | 62 | // fullscreenGrace is how long after mapping a window's fullscreen request | ||
| 59 | // is ignored. Wine asks for fullscreen whenever a window has the monitor's | 63 | // is ignored. Wine asks for fullscreen whenever a window has the monitor's | ||
| 60 | // size, so a launcher that remembered a fullscreen size would open | 64 | // size, so a launcher that remembered a fullscreen size would open | ||
| @@ -81,6 +85,7 @@ type Core struct { | |||||
| 81 | startup [][]string | 85 | startup [][]string | ||
| 82 | nextWorkspaceID uint64 | 86 | nextWorkspaceID uint64 | ||
| 83 | ch Channels | 87 | ch Channels | ||
| 88 | + | opts Options | |||
| 84 | cfg ports.Config | 89 | cfg ports.Config | ||
| 85 | screens []*screen | 90 | screens []*screen | ||
| 86 | focusScreen int | 91 | focusScreen int | ||
| @@ -350,12 +355,12 @@ func (c *Core) spawnSlots(ctx context.Context, shown bool) error { | |||||
| 350 | } | 355 | } | ||
| 351 | return nil | 356 | return nil | ||
| 352 | } | 357 | } | ||
| 353 | - | func New(cfg ports.Config, ch Channels) (*Core, error) { | 358 | + | func New(cfg ports.Config, ch Channels, opts Options) (*Core, error) { |
| 354 | if cap(ch.Scenes) != 1 || (ch.Layouts != nil && cap(ch.Layouts) != 1) || (ch.Constraints != nil && cap(ch.Constraints) != 1) || (ch.State != nil && cap(ch.State) != 1) || (ch.Workspaces != nil && cap(ch.Workspaces) != 1) { | 359 | if cap(ch.Scenes) != 1 || (ch.Layouts != nil && cap(ch.Layouts) != 1) || (ch.Constraints != nil && cap(ch.Constraints) != 1) || (ch.State != nil && cap(ch.State) != 1) || (ch.Workspaces != nil && cap(ch.Workspaces) != 1) { | ||
| 355 | return nil, fmt.Errorf("scenes, layouts, constraints, state and workspaces must have capacity 1") | 360 | return nil, fmt.Errorf("scenes, layouts, constraints, state and workspaces must have capacity 1") | ||
| 356 | } | 361 | } | ||
| 357 | // A placeholder screen holds windows until the first output arrives. | 362 | // A placeholder screen holds windows until the first output arrives. | ||
| 358 | - | c := &Core{inputKeys: map[string]bool{}, slots: map[slotKey]*slotState{}, placement: newSpawnPlacement(), windows: newWindowRegistry(), popups: map[WindowID]*popupState{}, ch: ch, pressed: map[string]bool{}, buttons: map[uint32]bool{}, swallow: map[uint32]bool{}, configures: newConfigures()} | 363 | + | c := &Core{inputKeys: map[string]bool{}, slots: map[slotKey]*slotState{}, placement: newSpawnPlacement(), windows: newWindowRegistry(), popups: map[WindowID]*popupState{}, ch: ch, opts: opts, pressed: map[string]bool{}, buttons: map[uint32]bool{}, swallow: map[uint32]bool{}, configures: newConfigures()} |
| 359 | c.screens = []*screen{{mon: newMonitorWithIDs("", "", &c.nextWorkspaceID), scale: 1, cfgScale: 1}} | 364 | c.screens = []*screen{{mon: newMonitorWithIDs("", "", &c.nextWorkspaceID), scale: 1, cfgScale: 1}} | ||
| 360 | if err := c.apply(cfg); err != nil { | 365 | if err := c.apply(cfg); err != nil { | ||
| 361 | return nil, err | 366 | return nil, err | ||
| @@ -386,8 +391,8 @@ func (t sysTimer) Stop() bool { return t.t.Stop() } | |||||
| 386 | func (t sysTimer) Reset(d time.Duration) bool { return t.t.Reset(d) } | 391 | func (t sysTimer) Reset(d time.Duration) bool { return t.t.Reset(d) } | ||
| 387 | 392 | ||||
| 388 | func (c *Core) now() time.Time { | 393 | func (c *Core) now() time.Time { | ||
| 389 | - | if c.ch.Clock != nil { | 394 | + | if c.opts.Clock != nil { |
| 390 | - | return c.ch.Clock.Now() | 395 | + | return c.opts.Clock.Now() |
| 391 | } | 396 | } | ||
| 392 | return time.Now() | 397 | return time.Now() | ||
| 393 | } | 398 | } | ||
| @@ -396,7 +401,7 @@ func (c *Core) command(ctx context.Context, v ports.ClientCommand) error { | |||||
| 396 | if c.inputEpochChanged() { | 401 | if c.inputEpochChanged() { | ||
| 397 | return errSecurityChanged | 402 | return errSecurityChanged | ||
| 398 | } | 403 | } | ||
| 399 | - | if c.ch.Security != nil { | 404 | + | if c.opts.Security != nil { |
| 400 | v = ports.SecurityCommand{State: c.security, Command: v} | 405 | v = ports.SecurityCommand{State: c.security, Command: v} | ||
| 401 | } | 406 | } | ||
| 402 | select { | 407 | select { | ||
M internal/core/core_test.go +41 -33
| @@ -58,14 +58,14 @@ type modeLanding struct { | |||
| 58 | 58 | lands int | |
| 59 | 59 | } | |
| 60 | 60 | ||
| 61 | - | // newLanding sets cfg's animations and, when on, the clock and flips of ch. | |
| 62 | - | func newLanding(t *testing.T, animated bool, cfg *ports.Config, ch *core.Channels, outputs ...string) *modeLanding { | |
| 61 | + | // newLanding sets cfg's animations and, when on, the clock of opts and flips of ch. | |
| 62 | + | func newLanding(t *testing.T, animated bool, cfg *ports.Config, ch *core.Channels, opts *core.Options, outputs ...string) *modeLanding { | |
| 63 | 63 | t.Helper() | |
| 64 | 64 | cfg.Animations.On = animated | |
| 65 | 65 | l := &modeLanding{t: t, scenes: ch.Scenes, outputs: outputs} | |
| 66 | 66 | if animated { | |
| 67 | 67 | l.clk, l.frames = newStepClock(t), make(chan ports.OutputFrame) | |
| 68 | - | ch.Clock, ch.Frames = l.clk.clock, l.frames | |
| 68 | + | opts.Clock, ch.Frames = l.clk.clock, l.frames | |
| 69 | 69 | t.Cleanup(func() { | |
| 70 | 70 | if l.lands == 0 { | |
| 71 | 71 | t.Error("no spring ever ran: the animations-on variant checked nothing") | |
| @@ -106,8 +106,9 @@ func owner(t *testing.T, animated bool) { | |||
| 106 | 106 | scenes := make(chan []ports.Scene, 1) | |
| 107 | 107 | errs := make(chan error, 8) | |
| 108 | 108 | ch := core.Channels{Client: client, Input: input, Output: output, Config: reload, Commands: commands, Spawn: spawn, Scenes: scenes, ConfigErrors: errs} | |
| 109 | - | l := newLanding(t, animated, &cfg, &ch, "OUT-1") | |
| 110 | - | c, err := core.New(cfg, ch) | |
| 109 | + | opts := core.Options{} | |
| 110 | + | l := newLanding(t, animated, &cfg, &ch, &opts, "OUT-1") | |
| 111 | + | c, err := core.New(cfg, ch, opts) | |
| 111 | 112 | if err != nil { | |
| 112 | 113 | t.Fatal(err) | |
| 113 | 114 | } | |
| @@ -186,11 +187,11 @@ func owner(t *testing.T, animated bool) { | |||
| 186 | 187 | } | |
| 187 | 188 | func TestCancelAndSceneCapacity(t *testing.T) { | |
| 188 | 189 | cfg := config.Defaults() | |
| 189 | - | if _, err := core.New(cfg, core.Channels{Scenes: make(chan []ports.Scene, 2)}); err == nil { | |
| 190 | + | if _, err := core.New(cfg, core.Channels{Scenes: make(chan []ports.Scene, 2)}, core.Options{}); err == nil { | |
| 190 | 191 | t.Fatal("capacity") | |
| 191 | 192 | } | |
| 192 | 193 | ctx, cancel := context.WithCancel(context.Background()) | |
| 193 | - | c, err := core.New(cfg, core.Channels{Scenes: make(chan []ports.Scene, 1)}) | |
| 194 | + | c, err := core.New(cfg, core.Channels{Scenes: make(chan []ports.Scene, 1)}, core.Options{}) | |
| 194 | 195 | if err != nil { | |
| 195 | 196 | t.Fatal(err) | |
| 196 | 197 | } | |
| @@ -208,7 +209,7 @@ func TestKeyPressState(t *testing.T) { | |||
| 208 | 209 | input := make(chan ports.InputEvent, 8) | |
| 209 | 210 | commands := make(chan ports.ClientCommand, 16) | |
| 210 | 211 | scenes := make(chan []ports.Scene, 1) | |
| 211 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Commands: commands, Scenes: scenes}) | |
| 212 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Commands: commands, Scenes: scenes}, core.Options{}) | |
| 212 | 213 | if err != nil { | |
| 213 | 214 | t.Fatal(err) | |
| 214 | 215 | } | |
| @@ -248,7 +249,7 @@ func TestBoundReleaseSwallowed(t *testing.T) { | |||
| 248 | 249 | client := make(chan ports.ClientEvent, 2) | |
| 249 | 250 | commands := make(chan ports.ClientCommand, 8) | |
| 250 | 251 | scenes := make(chan []ports.Scene, 1) | |
| 251 | - | c, err := core.New(cfg, core.Channels{Input: input, Client: client, Commands: commands, Scenes: scenes}) | |
| 252 | + | c, err := core.New(cfg, core.Channels{Input: input, Client: client, Commands: commands, Scenes: scenes}, core.Options{}) | |
| 252 | 253 | if err != nil { | |
| 253 | 254 | t.Fatal(err) | |
| 254 | 255 | } | |
| @@ -278,7 +279,7 @@ func TestPointerFocusAndGrab(t *testing.T) { | |||
| 278 | 279 | output := make(chan ports.OutputEvent, 8) | |
| 279 | 280 | commands := make(chan ports.ClientCommand, 64) | |
| 280 | 281 | scenes := make(chan []ports.Scene, 1) | |
| 281 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | |
| 282 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{}) | |
| 282 | 283 | if err != nil { | |
| 283 | 284 | t.Fatal(err) | |
| 284 | 285 | } | |
| @@ -343,8 +344,9 @@ func borderInset(t *testing.T, animated bool) { | |||
| 343 | 344 | commands := make(chan ports.ClientCommand, 64) | |
| 344 | 345 | scenes := make(chan []ports.Scene, 1) | |
| 345 | 346 | ch := core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes} | |
| 346 | - | l := newLanding(t, animated, &cfg, &ch, "OUT-1") | |
| 347 | - | c, err := core.New(cfg, ch) | |
| 347 | + | opts := core.Options{} | |
| 348 | + | l := newLanding(t, animated, &cfg, &ch, &opts, "OUT-1") | |
| 349 | + | c, err := core.New(cfg, ch, opts) | |
| 348 | 350 | if err != nil { | |
| 349 | 351 | t.Fatal(err) | |
| 350 | 352 | } | |
| @@ -390,7 +392,7 @@ func TestLayerKeyboardFocus(t *testing.T) { | |||
| 390 | 392 | output := make(chan ports.OutputEvent, 8) | |
| 391 | 393 | commands := make(chan ports.ClientCommand, 64) | |
| 392 | 394 | scenes := make(chan []ports.Scene, 1) | |
| 393 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | |
| 395 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{}) | |
| 394 | 396 | if err != nil { | |
| 395 | 397 | t.Fatal(err) | |
| 396 | 398 | } | |
| @@ -469,13 +471,14 @@ func workspaceSwitch(t *testing.T, animated bool) { | |||
| 469 | 471 | commands := make(chan ports.ClientCommand, 64) | |
| 470 | 472 | scenes := make(chan []ports.Scene, 1) | |
| 471 | 473 | ch := core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes} | |
| 474 | + | opts := core.Options{} | |
| 472 | 475 | var sc *stepClock | |
| 473 | 476 | frames := make(chan ports.OutputFrame) | |
| 474 | 477 | if animated { | |
| 475 | 478 | sc = newStepClock(t) | |
| 476 | - | ch.Clock, ch.Frames = sc.clock, frames | |
| 479 | + | opts.Clock, ch.Frames = sc.clock, frames | |
| 477 | 480 | } | |
| 478 | - | c, err := core.New(cfg, ch) | |
| 481 | + | c, err := core.New(cfg, ch, opts) | |
| 479 | 482 | if err != nil { | |
| 480 | 483 | t.Fatal(err) | |
| 481 | 484 | } | |
| @@ -529,13 +532,14 @@ func clickAfterWorkspaceSwitch(t *testing.T, animated bool) { | |||
| 529 | 532 | commands := make(chan ports.ClientCommand, 64) | |
| 530 | 533 | scenes := make(chan []ports.Scene, 1) | |
| 531 | 534 | ch := core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes} | |
| 535 | + | opts := core.Options{} | |
| 532 | 536 | var sc *stepClock | |
| 533 | 537 | frames := make(chan ports.OutputFrame) | |
| 534 | 538 | if animated { | |
| 535 | 539 | sc = newStepClock(t) | |
| 536 | - | ch.Clock, ch.Frames = sc.clock, frames | |
| 540 | + | opts.Clock, ch.Frames = sc.clock, frames | |
| 537 | 541 | } | |
| 538 | - | c, err := core.New(cfg, ch) | |
| 542 | + | c, err := core.New(cfg, ch, opts) | |
| 539 | 543 | if err != nil { | |
| 540 | 544 | t.Fatal(err) | |
| 541 | 545 | } | |
| @@ -586,7 +590,7 @@ func TestShiftReleasedFirst(t *testing.T) { | |||
| 586 | 590 | client := make(chan ports.ClientEvent, 2) | |
| 587 | 591 | commands := make(chan ports.ClientCommand, 16) | |
| 588 | 592 | scenes := make(chan []ports.Scene, 1) | |
| 589 | - | c, err := core.New(cfg, core.Channels{Input: input, Client: client, Commands: commands, Scenes: scenes}) | |
| 593 | + | c, err := core.New(cfg, core.Channels{Input: input, Client: client, Commands: commands, Scenes: scenes}, core.Options{}) | |
| 590 | 594 | if err != nil { | |
| 591 | 595 | t.Fatal(err) | |
| 592 | 596 | } | |
| @@ -624,8 +628,9 @@ func outputScale(t *testing.T, animated bool) { | |||
| 624 | 628 | commands := make(chan ports.ClientCommand, 64) | |
| 625 | 629 | scenes := make(chan []ports.Scene, 1) | |
| 626 | 630 | ch := core.Channels{Client: client, Input: input, Output: output, Config: reload, Commands: commands, Scenes: scenes} | |
| 627 | - | l := newLanding(t, animated, &cfg, &ch, "DP-2") | |
| 628 | - | c, err := core.New(cfg, ch) | |
| 631 | + | opts := core.Options{} | |
| 632 | + | l := newLanding(t, animated, &cfg, &ch, &opts, "DP-2") | |
| 633 | + | c, err := core.New(cfg, ch, opts) | |
| 629 | 634 | if err != nil { | |
| 630 | 635 | t.Fatal(err) | |
| 631 | 636 | } | |
| @@ -702,7 +707,7 @@ func TestPointerWarp(t *testing.T) { | |||
| 702 | 707 | commands := make(chan ports.ClientCommand, 64) | |
| 703 | 708 | scenes := make(chan []ports.Scene, 1) | |
| 704 | 709 | constraints := make(chan ports.PointerConstraint, 1) | |
| 705 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes, Constraints: constraints}) | |
| 710 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes, Constraints: constraints}, core.Options{}) | |
| 706 | 711 | if err != nil { | |
| 707 | 712 | t.Fatal(err) | |
| 708 | 713 | } | |
| @@ -755,8 +760,9 @@ func pointerConstraint(t *testing.T, animated bool) { | |||
| 755 | 760 | scenes := make(chan []ports.Scene, 1) | |
| 756 | 761 | constraints := make(chan ports.PointerConstraint, 1) | |
| 757 | 762 | ch := core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes, Constraints: constraints} | |
| 758 | - | l := newLanding(t, animated, &cfg, &ch, "A", "B") | |
| 759 | - | c, err := core.New(cfg, ch) | |
| 763 | + | opts := core.Options{} | |
| 764 | + | l := newLanding(t, animated, &cfg, &ch, &opts, "A", "B") | |
| 765 | + | c, err := core.New(cfg, ch, opts) | |
| 760 | 766 | if err != nil { | |
| 761 | 767 | t.Fatal(err) | |
| 762 | 768 | } | |
| @@ -826,7 +832,7 @@ func TestPopupChainDismissOrder(t *testing.T) { | |||
| 826 | 832 | output := make(chan ports.OutputEvent, 8) | |
| 827 | 833 | commands := make(chan ports.ClientCommand, 64) | |
| 828 | 834 | scenes := make(chan []ports.Scene, 1) | |
| 829 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | |
| 835 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{}) | |
| 830 | 836 | if err != nil { | |
| 831 | 837 | t.Fatal(err) | |
| 832 | 838 | } | |
| @@ -875,7 +881,7 @@ func TestFloatOverFullscreenHit(t *testing.T) { | |||
| 875 | 881 | output := make(chan ports.OutputEvent, 8) | |
| 876 | 882 | commands := make(chan ports.ClientCommand, 64) | |
| 877 | 883 | scenes := make(chan []ports.Scene, 1) | |
| 878 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes, Clock: steppingClock(t)}) | |
| 884 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{Clock: steppingClock(t)}) | |
| 879 | 885 | if err != nil { | |
| 880 | 886 | t.Fatal(err) | |
| 881 | 887 | } | |
| @@ -918,7 +924,7 @@ func TestDialogOverFullscreenHit(t *testing.T) { | |||
| 918 | 924 | output := make(chan ports.OutputEvent, 8) | |
| 919 | 925 | commands := make(chan ports.ClientCommand, 64) | |
| 920 | 926 | scenes := make(chan []ports.Scene, 1) | |
| 921 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes, Clock: steppingClock(t)}) | |
| 927 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{Clock: steppingClock(t)}) | |
| 922 | 928 | if err != nil { | |
| 923 | 929 | t.Fatal(err) | |
| 924 | 930 | } | |
| @@ -966,13 +972,14 @@ func fullscreenAtMapIgnored(t *testing.T, animated bool) { | |||
| 966 | 972 | scenes := make(chan []ports.Scene, 1) | |
| 967 | 973 | // Core reads the time on its goroutine: the clock is the test's. | |
| 968 | 974 | ch := core.Channels{Client: client, Output: output, Commands: commands, Scenes: scenes} | |
| 969 | - | l := newLanding(t, animated, &cfg, &ch, "OUT-1") | |
| 975 | + | opts := core.Options{} | |
| 976 | + | l := newLanding(t, animated, &cfg, &ch, &opts, "OUT-1") | |
| 970 | 977 | clk := l.clk | |
| 971 | 978 | if clk == nil { | |
| 972 | 979 | clk = newStepClock(t) | |
| 973 | - | ch.Clock = clk.clock | |
| 980 | + | opts.Clock = clk.clock | |
| 974 | 981 | } | |
| 975 | - | c, err := core.New(cfg, ch) | |
| 982 | + | c, err := core.New(cfg, ch, opts) | |
| 976 | 983 | if err != nil { | |
| 977 | 984 | t.Fatal(err) | |
| 978 | 985 | } | |
| @@ -1054,11 +1061,12 @@ func externalFullscreenAtMapApplies(t *testing.T, animated bool) { | |||
| 1054 | 1061 | commands := make(chan ports.ClientCommand, 64) | |
| 1055 | 1062 | scenes := make(chan []ports.Scene, 1) | |
| 1056 | 1063 | ch := core.Channels{Client: client, Output: output, Commands: commands, Scenes: scenes} | |
| 1057 | - | l := newLanding(t, animated, &cfg, &ch, "OUT-1") | |
| 1064 | + | opts := core.Options{} | |
| 1065 | + | l := newLanding(t, animated, &cfg, &ch, &opts, "OUT-1") | |
| 1058 | 1066 | if l.clk == nil { | |
| 1059 | - | ch.Clock = newStepClock(t).clock | |
| 1067 | + | opts.Clock = newStepClock(t).clock | |
| 1060 | 1068 | } | |
| 1061 | - | c, err := core.New(cfg, ch) | |
| 1069 | + | c, err := core.New(cfg, ch, opts) | |
| 1062 | 1070 | if err != nil { | |
| 1063 | 1071 | t.Fatal(err) | |
| 1064 | 1072 | } | |
| @@ -1193,7 +1201,7 @@ func TestWindowActivateShowsAndFocuses(t *testing.T) { | |||
| 1193 | 1201 | output := make(chan ports.OutputEvent, 8) | |
| 1194 | 1202 | commands := make(chan ports.ClientCommand, 64) | |
| 1195 | 1203 | scenes := make(chan []ports.Scene, 1) | |
| 1196 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | |
| 1204 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{}) | |
| 1197 | 1205 | if err != nil { | |
| 1198 | 1206 | t.Fatal(err) | |
| 1199 | 1207 | } | |
| @@ -58,14 +58,14 @@ type modeLanding struct { | |||||
| 58 | lands int | 58 | lands int | ||
| 59 | } | 59 | } | ||
| 60 | 60 | ||||
| 61 | - | // newLanding sets cfg's animations and, when on, the clock and flips of ch. | 61 | + | // newLanding sets cfg's animations and, when on, the clock of opts and flips of ch. |
| 62 | - | func newLanding(t *testing.T, animated bool, cfg *ports.Config, ch *core.Channels, outputs ...string) *modeLanding { | 62 | + | func newLanding(t *testing.T, animated bool, cfg *ports.Config, ch *core.Channels, opts *core.Options, outputs ...string) *modeLanding { |
| 63 | t.Helper() | 63 | t.Helper() | ||
| 64 | cfg.Animations.On = animated | 64 | cfg.Animations.On = animated | ||
| 65 | l := &modeLanding{t: t, scenes: ch.Scenes, outputs: outputs} | 65 | l := &modeLanding{t: t, scenes: ch.Scenes, outputs: outputs} | ||
| 66 | if animated { | 66 | if animated { | ||
| 67 | l.clk, l.frames = newStepClock(t), make(chan ports.OutputFrame) | 67 | l.clk, l.frames = newStepClock(t), make(chan ports.OutputFrame) | ||
| 68 | - | ch.Clock, ch.Frames = l.clk.clock, l.frames | 68 | + | opts.Clock, ch.Frames = l.clk.clock, l.frames |
| 69 | t.Cleanup(func() { | 69 | t.Cleanup(func() { | ||
| 70 | if l.lands == 0 { | 70 | if l.lands == 0 { | ||
| 71 | t.Error("no spring ever ran: the animations-on variant checked nothing") | 71 | t.Error("no spring ever ran: the animations-on variant checked nothing") | ||
| @@ -106,8 +106,9 @@ func owner(t *testing.T, animated bool) { | |||||
| 106 | scenes := make(chan []ports.Scene, 1) | 106 | scenes := make(chan []ports.Scene, 1) | ||
| 107 | errs := make(chan error, 8) | 107 | errs := make(chan error, 8) | ||
| 108 | ch := core.Channels{Client: client, Input: input, Output: output, Config: reload, Commands: commands, Spawn: spawn, Scenes: scenes, ConfigErrors: errs} | 108 | ch := core.Channels{Client: client, Input: input, Output: output, Config: reload, Commands: commands, Spawn: spawn, Scenes: scenes, ConfigErrors: errs} | ||
| 109 | - | l := newLanding(t, animated, &cfg, &ch, "OUT-1") | 109 | + | opts := core.Options{} |
| 110 | - | c, err := core.New(cfg, ch) | 110 | + | l := newLanding(t, animated, &cfg, &ch, &opts, "OUT-1") |
| 111 | + | c, err := core.New(cfg, ch, opts) | |||
| 111 | if err != nil { | 112 | if err != nil { | ||
| 112 | t.Fatal(err) | 113 | t.Fatal(err) | ||
| 113 | } | 114 | } | ||
| @@ -186,11 +187,11 @@ func owner(t *testing.T, animated bool) { | |||||
| 186 | } | 187 | } | ||
| 187 | func TestCancelAndSceneCapacity(t *testing.T) { | 188 | func TestCancelAndSceneCapacity(t *testing.T) { | ||
| 188 | cfg := config.Defaults() | 189 | cfg := config.Defaults() | ||
| 189 | - | if _, err := core.New(cfg, core.Channels{Scenes: make(chan []ports.Scene, 2)}); err == nil { | 190 | + | if _, err := core.New(cfg, core.Channels{Scenes: make(chan []ports.Scene, 2)}, core.Options{}); err == nil { |
| 190 | t.Fatal("capacity") | 191 | t.Fatal("capacity") | ||
| 191 | } | 192 | } | ||
| 192 | ctx, cancel := context.WithCancel(context.Background()) | 193 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 193 | - | c, err := core.New(cfg, core.Channels{Scenes: make(chan []ports.Scene, 1)}) | 194 | + | c, err := core.New(cfg, core.Channels{Scenes: make(chan []ports.Scene, 1)}, core.Options{}) |
| 194 | if err != nil { | 195 | if err != nil { | ||
| 195 | t.Fatal(err) | 196 | t.Fatal(err) | ||
| 196 | } | 197 | } | ||
| @@ -208,7 +209,7 @@ func TestKeyPressState(t *testing.T) { | |||||
| 208 | input := make(chan ports.InputEvent, 8) | 209 | input := make(chan ports.InputEvent, 8) | ||
| 209 | commands := make(chan ports.ClientCommand, 16) | 210 | commands := make(chan ports.ClientCommand, 16) | ||
| 210 | scenes := make(chan []ports.Scene, 1) | 211 | scenes := make(chan []ports.Scene, 1) | ||
| 211 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Commands: commands, Scenes: scenes}) | 212 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Commands: commands, Scenes: scenes}, core.Options{}) |
| 212 | if err != nil { | 213 | if err != nil { | ||
| 213 | t.Fatal(err) | 214 | t.Fatal(err) | ||
| 214 | } | 215 | } | ||
| @@ -248,7 +249,7 @@ func TestBoundReleaseSwallowed(t *testing.T) { | |||||
| 248 | client := make(chan ports.ClientEvent, 2) | 249 | client := make(chan ports.ClientEvent, 2) | ||
| 249 | commands := make(chan ports.ClientCommand, 8) | 250 | commands := make(chan ports.ClientCommand, 8) | ||
| 250 | scenes := make(chan []ports.Scene, 1) | 251 | scenes := make(chan []ports.Scene, 1) | ||
| 251 | - | c, err := core.New(cfg, core.Channels{Input: input, Client: client, Commands: commands, Scenes: scenes}) | 252 | + | c, err := core.New(cfg, core.Channels{Input: input, Client: client, Commands: commands, Scenes: scenes}, core.Options{}) |
| 252 | if err != nil { | 253 | if err != nil { | ||
| 253 | t.Fatal(err) | 254 | t.Fatal(err) | ||
| 254 | } | 255 | } | ||
| @@ -278,7 +279,7 @@ func TestPointerFocusAndGrab(t *testing.T) { | |||||
| 278 | output := make(chan ports.OutputEvent, 8) | 279 | output := make(chan ports.OutputEvent, 8) | ||
| 279 | commands := make(chan ports.ClientCommand, 64) | 280 | commands := make(chan ports.ClientCommand, 64) | ||
| 280 | scenes := make(chan []ports.Scene, 1) | 281 | scenes := make(chan []ports.Scene, 1) | ||
| 281 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | 282 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{}) |
| 282 | if err != nil { | 283 | if err != nil { | ||
| 283 | t.Fatal(err) | 284 | t.Fatal(err) | ||
| 284 | } | 285 | } | ||
| @@ -343,8 +344,9 @@ func borderInset(t *testing.T, animated bool) { | |||||
| 343 | commands := make(chan ports.ClientCommand, 64) | 344 | commands := make(chan ports.ClientCommand, 64) | ||
| 344 | scenes := make(chan []ports.Scene, 1) | 345 | scenes := make(chan []ports.Scene, 1) | ||
| 345 | ch := core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes} | 346 | ch := core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes} | ||
| 346 | - | l := newLanding(t, animated, &cfg, &ch, "OUT-1") | 347 | + | opts := core.Options{} |
| 347 | - | c, err := core.New(cfg, ch) | 348 | + | l := newLanding(t, animated, &cfg, &ch, &opts, "OUT-1") |
| 349 | + | c, err := core.New(cfg, ch, opts) | |||
| 348 | if err != nil { | 350 | if err != nil { | ||
| 349 | t.Fatal(err) | 351 | t.Fatal(err) | ||
| 350 | } | 352 | } | ||
| @@ -390,7 +392,7 @@ func TestLayerKeyboardFocus(t *testing.T) { | |||||
| 390 | output := make(chan ports.OutputEvent, 8) | 392 | output := make(chan ports.OutputEvent, 8) | ||
| 391 | commands := make(chan ports.ClientCommand, 64) | 393 | commands := make(chan ports.ClientCommand, 64) | ||
| 392 | scenes := make(chan []ports.Scene, 1) | 394 | scenes := make(chan []ports.Scene, 1) | ||
| 393 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | 395 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{}) |
| 394 | if err != nil { | 396 | if err != nil { | ||
| 395 | t.Fatal(err) | 397 | t.Fatal(err) | ||
| 396 | } | 398 | } | ||
| @@ -469,13 +471,14 @@ func workspaceSwitch(t *testing.T, animated bool) { | |||||
| 469 | commands := make(chan ports.ClientCommand, 64) | 471 | commands := make(chan ports.ClientCommand, 64) | ||
| 470 | scenes := make(chan []ports.Scene, 1) | 472 | scenes := make(chan []ports.Scene, 1) | ||
| 471 | ch := core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes} | 473 | ch := core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes} | ||
| 474 | + | opts := core.Options{} | |||
| 472 | var sc *stepClock | 475 | var sc *stepClock | ||
| 473 | frames := make(chan ports.OutputFrame) | 476 | frames := make(chan ports.OutputFrame) | ||
| 474 | if animated { | 477 | if animated { | ||
| 475 | sc = newStepClock(t) | 478 | sc = newStepClock(t) | ||
| 476 | - | ch.Clock, ch.Frames = sc.clock, frames | 479 | + | opts.Clock, ch.Frames = sc.clock, frames |
| 477 | } | 480 | } | ||
| 478 | - | c, err := core.New(cfg, ch) | 481 | + | c, err := core.New(cfg, ch, opts) |
| 479 | if err != nil { | 482 | if err != nil { | ||
| 480 | t.Fatal(err) | 483 | t.Fatal(err) | ||
| 481 | } | 484 | } | ||
| @@ -529,13 +532,14 @@ func clickAfterWorkspaceSwitch(t *testing.T, animated bool) { | |||||
| 529 | commands := make(chan ports.ClientCommand, 64) | 532 | commands := make(chan ports.ClientCommand, 64) | ||
| 530 | scenes := make(chan []ports.Scene, 1) | 533 | scenes := make(chan []ports.Scene, 1) | ||
| 531 | ch := core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes} | 534 | ch := core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes} | ||
| 535 | + | opts := core.Options{} | |||
| 532 | var sc *stepClock | 536 | var sc *stepClock | ||
| 533 | frames := make(chan ports.OutputFrame) | 537 | frames := make(chan ports.OutputFrame) | ||
| 534 | if animated { | 538 | if animated { | ||
| 535 | sc = newStepClock(t) | 539 | sc = newStepClock(t) | ||
| 536 | - | ch.Clock, ch.Frames = sc.clock, frames | 540 | + | opts.Clock, ch.Frames = sc.clock, frames |
| 537 | } | 541 | } | ||
| 538 | - | c, err := core.New(cfg, ch) | 542 | + | c, err := core.New(cfg, ch, opts) |
| 539 | if err != nil { | 543 | if err != nil { | ||
| 540 | t.Fatal(err) | 544 | t.Fatal(err) | ||
| 541 | } | 545 | } | ||
| @@ -586,7 +590,7 @@ func TestShiftReleasedFirst(t *testing.T) { | |||||
| 586 | client := make(chan ports.ClientEvent, 2) | 590 | client := make(chan ports.ClientEvent, 2) | ||
| 587 | commands := make(chan ports.ClientCommand, 16) | 591 | commands := make(chan ports.ClientCommand, 16) | ||
| 588 | scenes := make(chan []ports.Scene, 1) | 592 | scenes := make(chan []ports.Scene, 1) | ||
| 589 | - | c, err := core.New(cfg, core.Channels{Input: input, Client: client, Commands: commands, Scenes: scenes}) | 593 | + | c, err := core.New(cfg, core.Channels{Input: input, Client: client, Commands: commands, Scenes: scenes}, core.Options{}) |
| 590 | if err != nil { | 594 | if err != nil { | ||
| 591 | t.Fatal(err) | 595 | t.Fatal(err) | ||
| 592 | } | 596 | } | ||
| @@ -624,8 +628,9 @@ func outputScale(t *testing.T, animated bool) { | |||||
| 624 | commands := make(chan ports.ClientCommand, 64) | 628 | commands := make(chan ports.ClientCommand, 64) | ||
| 625 | scenes := make(chan []ports.Scene, 1) | 629 | scenes := make(chan []ports.Scene, 1) | ||
| 626 | ch := core.Channels{Client: client, Input: input, Output: output, Config: reload, Commands: commands, Scenes: scenes} | 630 | ch := core.Channels{Client: client, Input: input, Output: output, Config: reload, Commands: commands, Scenes: scenes} | ||
| 627 | - | l := newLanding(t, animated, &cfg, &ch, "DP-2") | 631 | + | opts := core.Options{} |
| 628 | - | c, err := core.New(cfg, ch) | 632 | + | l := newLanding(t, animated, &cfg, &ch, &opts, "DP-2") |
| 633 | + | c, err := core.New(cfg, ch, opts) | |||
| 629 | if err != nil { | 634 | if err != nil { | ||
| 630 | t.Fatal(err) | 635 | t.Fatal(err) | ||
| 631 | } | 636 | } | ||
| @@ -702,7 +707,7 @@ func TestPointerWarp(t *testing.T) { | |||||
| 702 | commands := make(chan ports.ClientCommand, 64) | 707 | commands := make(chan ports.ClientCommand, 64) | ||
| 703 | scenes := make(chan []ports.Scene, 1) | 708 | scenes := make(chan []ports.Scene, 1) | ||
| 704 | constraints := make(chan ports.PointerConstraint, 1) | 709 | constraints := make(chan ports.PointerConstraint, 1) | ||
| 705 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes, Constraints: constraints}) | 710 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes, Constraints: constraints}, core.Options{}) |
| 706 | if err != nil { | 711 | if err != nil { | ||
| 707 | t.Fatal(err) | 712 | t.Fatal(err) | ||
| 708 | } | 713 | } | ||
| @@ -755,8 +760,9 @@ func pointerConstraint(t *testing.T, animated bool) { | |||||
| 755 | scenes := make(chan []ports.Scene, 1) | 760 | scenes := make(chan []ports.Scene, 1) | ||
| 756 | constraints := make(chan ports.PointerConstraint, 1) | 761 | constraints := make(chan ports.PointerConstraint, 1) | ||
| 757 | ch := core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes, Constraints: constraints} | 762 | ch := core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes, Constraints: constraints} | ||
| 758 | - | l := newLanding(t, animated, &cfg, &ch, "A", "B") | 763 | + | opts := core.Options{} |
| 759 | - | c, err := core.New(cfg, ch) | 764 | + | l := newLanding(t, animated, &cfg, &ch, &opts, "A", "B") |
| 765 | + | c, err := core.New(cfg, ch, opts) | |||
| 760 | if err != nil { | 766 | if err != nil { | ||
| 761 | t.Fatal(err) | 767 | t.Fatal(err) | ||
| 762 | } | 768 | } | ||
| @@ -826,7 +832,7 @@ func TestPopupChainDismissOrder(t *testing.T) { | |||||
| 826 | output := make(chan ports.OutputEvent, 8) | 832 | output := make(chan ports.OutputEvent, 8) | ||
| 827 | commands := make(chan ports.ClientCommand, 64) | 833 | commands := make(chan ports.ClientCommand, 64) | ||
| 828 | scenes := make(chan []ports.Scene, 1) | 834 | scenes := make(chan []ports.Scene, 1) | ||
| 829 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | 835 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{}) |
| 830 | if err != nil { | 836 | if err != nil { | ||
| 831 | t.Fatal(err) | 837 | t.Fatal(err) | ||
| 832 | } | 838 | } | ||
| @@ -875,7 +881,7 @@ func TestFloatOverFullscreenHit(t *testing.T) { | |||||
| 875 | output := make(chan ports.OutputEvent, 8) | 881 | output := make(chan ports.OutputEvent, 8) | ||
| 876 | commands := make(chan ports.ClientCommand, 64) | 882 | commands := make(chan ports.ClientCommand, 64) | ||
| 877 | scenes := make(chan []ports.Scene, 1) | 883 | scenes := make(chan []ports.Scene, 1) | ||
| 878 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes, Clock: steppingClock(t)}) | 884 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{Clock: steppingClock(t)}) |
| 879 | if err != nil { | 885 | if err != nil { | ||
| 880 | t.Fatal(err) | 886 | t.Fatal(err) | ||
| 881 | } | 887 | } | ||
| @@ -918,7 +924,7 @@ func TestDialogOverFullscreenHit(t *testing.T) { | |||||
| 918 | output := make(chan ports.OutputEvent, 8) | 924 | output := make(chan ports.OutputEvent, 8) | ||
| 919 | commands := make(chan ports.ClientCommand, 64) | 925 | commands := make(chan ports.ClientCommand, 64) | ||
| 920 | scenes := make(chan []ports.Scene, 1) | 926 | scenes := make(chan []ports.Scene, 1) | ||
| 921 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes, Clock: steppingClock(t)}) | 927 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{Clock: steppingClock(t)}) |
| 922 | if err != nil { | 928 | if err != nil { | ||
| 923 | t.Fatal(err) | 929 | t.Fatal(err) | ||
| 924 | } | 930 | } | ||
| @@ -966,13 +972,14 @@ func fullscreenAtMapIgnored(t *testing.T, animated bool) { | |||||
| 966 | scenes := make(chan []ports.Scene, 1) | 972 | scenes := make(chan []ports.Scene, 1) | ||
| 967 | // Core reads the time on its goroutine: the clock is the test's. | 973 | // Core reads the time on its goroutine: the clock is the test's. | ||
| 968 | ch := core.Channels{Client: client, Output: output, Commands: commands, Scenes: scenes} | 974 | ch := core.Channels{Client: client, Output: output, Commands: commands, Scenes: scenes} | ||
| 969 | - | l := newLanding(t, animated, &cfg, &ch, "OUT-1") | 975 | + | opts := core.Options{} |
| 976 | + | l := newLanding(t, animated, &cfg, &ch, &opts, "OUT-1") | |||
| 970 | clk := l.clk | 977 | clk := l.clk | ||
| 971 | if clk == nil { | 978 | if clk == nil { | ||
| 972 | clk = newStepClock(t) | 979 | clk = newStepClock(t) | ||
| 973 | - | ch.Clock = clk.clock | 980 | + | opts.Clock = clk.clock |
| 974 | } | 981 | } | ||
| 975 | - | c, err := core.New(cfg, ch) | 982 | + | c, err := core.New(cfg, ch, opts) |
| 976 | if err != nil { | 983 | if err != nil { | ||
| 977 | t.Fatal(err) | 984 | t.Fatal(err) | ||
| 978 | } | 985 | } | ||
| @@ -1054,11 +1061,12 @@ func externalFullscreenAtMapApplies(t *testing.T, animated bool) { | |||||
| 1054 | commands := make(chan ports.ClientCommand, 64) | 1061 | commands := make(chan ports.ClientCommand, 64) | ||
| 1055 | scenes := make(chan []ports.Scene, 1) | 1062 | scenes := make(chan []ports.Scene, 1) | ||
| 1056 | ch := core.Channels{Client: client, Output: output, Commands: commands, Scenes: scenes} | 1063 | ch := core.Channels{Client: client, Output: output, Commands: commands, Scenes: scenes} | ||
| 1057 | - | l := newLanding(t, animated, &cfg, &ch, "OUT-1") | 1064 | + | opts := core.Options{} |
| 1065 | + | l := newLanding(t, animated, &cfg, &ch, &opts, "OUT-1") | |||
| 1058 | if l.clk == nil { | 1066 | if l.clk == nil { | ||
| 1059 | - | ch.Clock = newStepClock(t).clock | 1067 | + | opts.Clock = newStepClock(t).clock |
| 1060 | } | 1068 | } | ||
| 1061 | - | c, err := core.New(cfg, ch) | 1069 | + | c, err := core.New(cfg, ch, opts) |
| 1062 | if err != nil { | 1070 | if err != nil { | ||
| 1063 | t.Fatal(err) | 1071 | t.Fatal(err) | ||
| 1064 | } | 1072 | } | ||
| @@ -1193,7 +1201,7 @@ func TestWindowActivateShowsAndFocuses(t *testing.T) { | |||||
| 1193 | output := make(chan ports.OutputEvent, 8) | 1201 | output := make(chan ports.OutputEvent, 8) | ||
| 1194 | commands := make(chan ports.ClientCommand, 64) | 1202 | commands := make(chan ports.ClientCommand, 64) | ||
| 1195 | scenes := make(chan []ports.Scene, 1) | 1203 | scenes := make(chan []ports.Scene, 1) | ||
| 1196 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | 1204 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{}) |
| 1197 | if err != nil { | 1205 | if err != nil { | ||
| 1198 | t.Fatal(err) | 1206 | t.Fatal(err) | ||
| 1199 | } | 1207 | } | ||
M internal/core/drag_internal_test.go +1 -1
| @@ -96,7 +96,7 @@ func dragCore(t *testing.T) *Core { | |||
| 96 | 96 | cfg := ports.Config{} | |
| 97 | 97 | cfg.Keyboard.CmdKey = "super" | |
| 98 | 98 | cfg.Layout.MaxColumns = 3 | |
| 99 | - | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Commands: make(chan ports.ClientCommand, 1024)}) | |
| 99 | + | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Commands: make(chan ports.ClientCommand, 1024)}, Options{}) | |
| 100 | 100 | if err != nil { | |
| 101 | 101 | t.Fatal(err) | |
| 102 | 102 | } | |
| @@ -96,7 +96,7 @@ func dragCore(t *testing.T) *Core { | |||||
| 96 | cfg := ports.Config{} | 96 | cfg := ports.Config{} | ||
| 97 | cfg.Keyboard.CmdKey = "super" | 97 | cfg.Keyboard.CmdKey = "super" | ||
| 98 | cfg.Layout.MaxColumns = 3 | 98 | cfg.Layout.MaxColumns = 3 | ||
| 99 | - | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Commands: make(chan ports.ClientCommand, 1024)}) | 99 | + | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Commands: make(chan ports.ClientCommand, 1024)}, Options{}) |
| 100 | if err != nil { | 100 | if err != nil { | ||
| 101 | t.Fatal(err) | 101 | t.Fatal(err) | ||
| 102 | } | 102 | } | ||
M internal/core/drag_test.go +4 -3
| @@ -71,10 +71,11 @@ func newDragRig(t *testing.T, animated bool, edit func(*ports.Config), outs ...p | |||
| 71 | 71 | output: make(chan ports.OutputEvent), commands: make(chan ports.ClientCommand, 4096), | |
| 72 | 72 | scenes: make(chan []ports.Scene, 1), epoch: epoch, | |
| 73 | 73 | } | |
| 74 | - | ch := core.Channels{Security: gate, Client: r.client, Input: r.input, Output: r.output, Commands: r.commands, Scenes: r.scenes} | |
| 74 | + | ch := core.Channels{Client: r.client, Input: r.input, Output: r.output, Commands: r.commands, Scenes: r.scenes} | |
| 75 | + | opts := core.Options{Security: gate} | |
| 75 | 76 | if animated { | |
| 76 | 77 | r.frames, r.clock = make(chan ports.OutputFrame), newStepClock(t) | |
| 77 | - | ch.Frames, ch.Clock = r.frames, r.clock.clock | |
| 78 | + | ch.Frames, opts.Clock = r.frames, r.clock.clock | |
| 78 | 79 | // An on variant that never saw a spring would be the off variant again. | |
| 79 | 80 | t.Cleanup(func() { | |
| 80 | 81 | if r.lands == 0 { | |
| @@ -82,7 +83,7 @@ func newDragRig(t *testing.T, animated bool, edit func(*ports.Config), outs ...p | |||
| 82 | 83 | } | |
| 83 | 84 | }) | |
| 84 | 85 | } | |
| 85 | - | c, err := core.New(cfg, ch) | |
| 86 | + | c, err := core.New(cfg, ch, opts) | |
| 86 | 87 | if err != nil { | |
| 87 | 88 | t.Fatal(err) | |
| 88 | 89 | } | |
| @@ -71,10 +71,11 @@ func newDragRig(t *testing.T, animated bool, edit func(*ports.Config), outs ...p | |||||
| 71 | output: make(chan ports.OutputEvent), commands: make(chan ports.ClientCommand, 4096), | 71 | output: make(chan ports.OutputEvent), commands: make(chan ports.ClientCommand, 4096), | ||
| 72 | scenes: make(chan []ports.Scene, 1), epoch: epoch, | 72 | scenes: make(chan []ports.Scene, 1), epoch: epoch, | ||
| 73 | } | 73 | } | ||
| 74 | - | ch := core.Channels{Security: gate, Client: r.client, Input: r.input, Output: r.output, Commands: r.commands, Scenes: r.scenes} | 74 | + | ch := core.Channels{Client: r.client, Input: r.input, Output: r.output, Commands: r.commands, Scenes: r.scenes} |
| 75 | + | opts := core.Options{Security: gate} | |||
| 75 | if animated { | 76 | if animated { | ||
| 76 | r.frames, r.clock = make(chan ports.OutputFrame), newStepClock(t) | 77 | r.frames, r.clock = make(chan ports.OutputFrame), newStepClock(t) | ||
| 77 | - | ch.Frames, ch.Clock = r.frames, r.clock.clock | 78 | + | ch.Frames, opts.Clock = r.frames, r.clock.clock |
| 78 | // An on variant that never saw a spring would be the off variant again. | 79 | // An on variant that never saw a spring would be the off variant again. | ||
| 79 | t.Cleanup(func() { | 80 | t.Cleanup(func() { | ||
| 80 | if r.lands == 0 { | 81 | if r.lands == 0 { | ||
| @@ -82,7 +83,7 @@ func newDragRig(t *testing.T, animated bool, edit func(*ports.Config), outs ...p | |||||
| 82 | } | 83 | } | ||
| 83 | }) | 84 | }) | ||
| 84 | } | 85 | } | ||
| 85 | - | c, err := core.New(cfg, ch) | 86 | + | c, err := core.New(cfg, ch, opts) |
| 86 | if err != nil { | 87 | if err != nil { | ||
| 87 | t.Fatal(err) | 88 | t.Fatal(err) | ||
| 88 | } | 89 | } | ||
M internal/core/float_core_test.go +2 -2
| @@ -15,7 +15,7 @@ func TestCoreCoveringFloatSceneAndHit(t *testing.T) { | |||
| 15 | 15 | cfg.Floating.Dim = 0.3 | |
| 16 | 16 | scenes := make(chan []ports.Scene, 1) | |
| 17 | 17 | commands := make(chan ports.ClientCommand, 128) | |
| 18 | - | c, err := New(cfg, Channels{Scenes: scenes, Commands: commands}) | |
| 18 | + | c, err := New(cfg, Channels{Scenes: scenes, Commands: commands}, Options{}) | |
| 19 | 19 | if err != nil { | |
| 20 | 20 | t.Fatal(err) | |
| 21 | 21 | } | |
| @@ -76,7 +76,7 @@ func TestCoreOverviewDimsBehind(t *testing.T) { | |||
| 76 | 76 | cfg.Layout.MaxColumns = 2 | |
| 77 | 77 | cfg.Floating.Dim = 0.3 | |
| 78 | 78 | scenes := make(chan []ports.Scene, 1) | |
| 79 | - | c, err := New(cfg, Channels{Scenes: scenes, Commands: make(chan ports.ClientCommand, 128)}) | |
| 79 | + | c, err := New(cfg, Channels{Scenes: scenes, Commands: make(chan ports.ClientCommand, 128)}, Options{}) | |
| 80 | 80 | if err != nil { | |
| 81 | 81 | t.Fatal(err) | |
| 82 | 82 | } | |
| @@ -15,7 +15,7 @@ func TestCoreCoveringFloatSceneAndHit(t *testing.T) { | |||||
| 15 | cfg.Floating.Dim = 0.3 | 15 | cfg.Floating.Dim = 0.3 | ||
| 16 | scenes := make(chan []ports.Scene, 1) | 16 | scenes := make(chan []ports.Scene, 1) | ||
| 17 | commands := make(chan ports.ClientCommand, 128) | 17 | commands := make(chan ports.ClientCommand, 128) | ||
| 18 | - | c, err := New(cfg, Channels{Scenes: scenes, Commands: commands}) | 18 | + | c, err := New(cfg, Channels{Scenes: scenes, Commands: commands}, Options{}) |
| 19 | if err != nil { | 19 | if err != nil { | ||
| 20 | t.Fatal(err) | 20 | t.Fatal(err) | ||
| 21 | } | 21 | } | ||
| @@ -76,7 +76,7 @@ func TestCoreOverviewDimsBehind(t *testing.T) { | |||||
| 76 | cfg.Layout.MaxColumns = 2 | 76 | cfg.Layout.MaxColumns = 2 | ||
| 77 | cfg.Floating.Dim = 0.3 | 77 | cfg.Floating.Dim = 0.3 | ||
| 78 | scenes := make(chan []ports.Scene, 1) | 78 | scenes := make(chan []ports.Scene, 1) | ||
| 79 | - | c, err := New(cfg, Channels{Scenes: scenes, Commands: make(chan ports.ClientCommand, 128)}) | 79 | + | c, err := New(cfg, Channels{Scenes: scenes, Commands: make(chan ports.ClientCommand, 128)}, Options{}) |
| 80 | if err != nil { | 80 | if err != nil { | ||
| 81 | t.Fatal(err) | 81 | t.Fatal(err) | ||
| 82 | } | 82 | } | ||
M internal/core/floating_test.go +1 -1
| @@ -415,7 +415,7 @@ func TestFloatingSceneDimAndConfigure(t *testing.T) { | |||
| 415 | 415 | cfg.Border.Width = 2 | |
| 416 | 416 | commands := make(chan ports.ClientCommand, 128) | |
| 417 | 417 | scenes := make(chan []ports.Scene, 1) | |
| 418 | - | c, err := New(cfg, Channels{Commands: commands, Scenes: scenes}) | |
| 418 | + | c, err := New(cfg, Channels{Commands: commands, Scenes: scenes}, Options{}) | |
| 419 | 419 | if err != nil { | |
| 420 | 420 | t.Fatal(err) | |
| 421 | 421 | } | |
| @@ -415,7 +415,7 @@ func TestFloatingSceneDimAndConfigure(t *testing.T) { | |||||
| 415 | cfg.Border.Width = 2 | 415 | cfg.Border.Width = 2 | ||
| 416 | commands := make(chan ports.ClientCommand, 128) | 416 | commands := make(chan ports.ClientCommand, 128) | ||
| 417 | scenes := make(chan []ports.Scene, 1) | 417 | scenes := make(chan []ports.Scene, 1) | ||
| 418 | - | c, err := New(cfg, Channels{Commands: commands, Scenes: scenes}) | 418 | + | c, err := New(cfg, Channels{Commands: commands, Scenes: scenes}, Options{}) |
| 419 | if err != nil { | 419 | if err != nil { | ||
| 420 | t.Fatal(err) | 420 | t.Fatal(err) | ||
| 421 | } | 421 | } | ||
M internal/core/fullscreen_left_core_test.go +1 -1
| @@ -19,7 +19,7 @@ func TestCoreLeftFullscreenLatch(t *testing.T) { | |||
| 19 | 19 | cfg := ports.Config{} | |
| 20 | 20 | cfg.Keyboard.CmdKey = "super" | |
| 21 | 21 | cfg.Layout.MaxColumns = 2 | |
| 22 | - | c, err := New(cfg, Channels{Client: client, Output: output, Commands: commands, Scenes: scenes, Clock: laterClock(t)}) | |
| 22 | + | c, err := New(cfg, Channels{Client: client, Output: output, Commands: commands, Scenes: scenes}, Options{Clock: laterClock(t)}) | |
| 23 | 23 | if err != nil { | |
| 24 | 24 | t.Fatal(err) | |
| 25 | 25 | } | |
| @@ -19,7 +19,7 @@ func TestCoreLeftFullscreenLatch(t *testing.T) { | |||||
| 19 | cfg := ports.Config{} | 19 | cfg := ports.Config{} | ||
| 20 | cfg.Keyboard.CmdKey = "super" | 20 | cfg.Keyboard.CmdKey = "super" | ||
| 21 | cfg.Layout.MaxColumns = 2 | 21 | cfg.Layout.MaxColumns = 2 | ||
| 22 | - | c, err := New(cfg, Channels{Client: client, Output: output, Commands: commands, Scenes: scenes, Clock: laterClock(t)}) | 22 | + | c, err := New(cfg, Channels{Client: client, Output: output, Commands: commands, Scenes: scenes}, Options{Clock: laterClock(t)}) |
| 23 | if err != nil { | 23 | if err != nil { | ||
| 24 | t.Fatal(err) | 24 | t.Fatal(err) | ||
| 25 | } | 25 | } | ||
M internal/core/gesture.go +1 -1
| @@ -576,7 +576,7 @@ func (c *Core) frameFallback() time.Duration { | |||
| 576 | 576 | func (c *Core) armFrame() { | |
| 577 | 577 | d := c.frameFallback() | |
| 578 | 578 | if c.frameTimer == nil { | |
| 579 | - | c.frameTimer = newPortTimer(c.ch.Clock, d) | |
| 579 | + | c.frameTimer = newPortTimer(c.opts.Clock, d) | |
| 580 | 580 | } else { | |
| 581 | 581 | c.frameTimer.Stop() | |
| 582 | 582 | c.frameTimer.Reset(d) | |
| @@ -576,7 +576,7 @@ func (c *Core) frameFallback() time.Duration { | |||||
| 576 | func (c *Core) armFrame() { | 576 | func (c *Core) armFrame() { | ||
| 577 | d := c.frameFallback() | 577 | d := c.frameFallback() | ||
| 578 | if c.frameTimer == nil { | 578 | if c.frameTimer == nil { | ||
| 579 | - | c.frameTimer = newPortTimer(c.ch.Clock, d) | 579 | + | c.frameTimer = newPortTimer(c.opts.Clock, d) |
| 580 | } else { | 580 | } else { | ||
| 581 | c.frameTimer.Stop() | 581 | c.frameTimer.Stop() | ||
| 582 | c.frameTimer.Reset(d) | 582 | c.frameTimer.Reset(d) | ||
M internal/core/inhibit_test.go +2 -2
| @@ -39,7 +39,7 @@ func TestShortcutsInhibit(t *testing.T) { | |||
| 39 | 39 | spawn := make(chan ports.SpawnRequest, 4) | |
| 40 | 40 | state := make(chan ports.State, 1) | |
| 41 | 41 | output := make(chan ports.OutputEvent, 1) | |
| 42 | - | c, err := core.New(cfg, core.Channels{Input: input, Client: client, Output: output, Commands: commands, Scenes: scenes, Spawn: spawn, State: state}) | |
| 42 | + | c, err := core.New(cfg, core.Channels{Input: input, Client: client, Output: output, Commands: commands, Scenes: scenes, Spawn: spawn, State: state}, core.Options{}) | |
| 43 | 43 | if err != nil { | |
| 44 | 44 | t.Fatal(err) | |
| 45 | 45 | } | |
| @@ -99,7 +99,7 @@ func TestShortcutsInhibitDropsBoundRelease(t *testing.T) { | |||
| 99 | 99 | scenes := make(chan []ports.Scene, 1) | |
| 100 | 100 | spawn := make(chan ports.SpawnRequest, 4) | |
| 101 | 101 | output := make(chan ports.OutputEvent, 1) | |
| 102 | - | c, err := core.New(cfg, core.Channels{Input: input, Client: client, Output: output, Commands: commands, Scenes: scenes, Spawn: spawn}) | |
| 102 | + | c, err := core.New(cfg, core.Channels{Input: input, Client: client, Output: output, Commands: commands, Scenes: scenes, Spawn: spawn}, core.Options{}) | |
| 103 | 103 | if err != nil { | |
| 104 | 104 | t.Fatal(err) | |
| 105 | 105 | } | |
| @@ -39,7 +39,7 @@ func TestShortcutsInhibit(t *testing.T) { | |||||
| 39 | spawn := make(chan ports.SpawnRequest, 4) | 39 | spawn := make(chan ports.SpawnRequest, 4) | ||
| 40 | state := make(chan ports.State, 1) | 40 | state := make(chan ports.State, 1) | ||
| 41 | output := make(chan ports.OutputEvent, 1) | 41 | output := make(chan ports.OutputEvent, 1) | ||
| 42 | - | c, err := core.New(cfg, core.Channels{Input: input, Client: client, Output: output, Commands: commands, Scenes: scenes, Spawn: spawn, State: state}) | 42 | + | c, err := core.New(cfg, core.Channels{Input: input, Client: client, Output: output, Commands: commands, Scenes: scenes, Spawn: spawn, State: state}, core.Options{}) |
| 43 | if err != nil { | 43 | if err != nil { | ||
| 44 | t.Fatal(err) | 44 | t.Fatal(err) | ||
| 45 | } | 45 | } | ||
| @@ -99,7 +99,7 @@ func TestShortcutsInhibitDropsBoundRelease(t *testing.T) { | |||||
| 99 | scenes := make(chan []ports.Scene, 1) | 99 | scenes := make(chan []ports.Scene, 1) | ||
| 100 | spawn := make(chan ports.SpawnRequest, 4) | 100 | spawn := make(chan ports.SpawnRequest, 4) | ||
| 101 | output := make(chan ports.OutputEvent, 1) | 101 | output := make(chan ports.OutputEvent, 1) | ||
| 102 | - | c, err := core.New(cfg, core.Channels{Input: input, Client: client, Output: output, Commands: commands, Scenes: scenes, Spawn: spawn}) | 102 | + | c, err := core.New(cfg, core.Channels{Input: input, Client: client, Output: output, Commands: commands, Scenes: scenes, Spawn: spawn}, core.Options{}) |
| 103 | if err != nil { | 103 | if err != nil { | ||
| 104 | t.Fatal(err) | 104 | t.Fatal(err) | ||
| 105 | } | 105 | } | ||
M internal/core/layer_input_test.go +8 -8
| @@ -30,7 +30,7 @@ func TestLayerPointerPopupAndFocus(t *testing.T) { | |||
| 30 | 30 | output := make(chan ports.OutputEvent, 8) | |
| 31 | 31 | commands := make(chan ports.ClientCommand, 256) | |
| 32 | 32 | scenes := make(chan []ports.Scene, 1) | |
| 33 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | |
| 33 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{}) | |
| 34 | 34 | if err != nil { | |
| 35 | 35 | t.Fatal(err) | |
| 36 | 36 | } | |
| @@ -149,7 +149,7 @@ func TestWindowPopupUnderOverlay(t *testing.T) { | |||
| 149 | 149 | output := make(chan ports.OutputEvent, 8) | |
| 150 | 150 | commands := make(chan ports.ClientCommand, 256) | |
| 151 | 151 | scenes := make(chan []ports.Scene, 1) | |
| 152 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | |
| 152 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{}) | |
| 153 | 153 | if err != nil { | |
| 154 | 154 | t.Fatal(err) | |
| 155 | 155 | } | |
| @@ -186,7 +186,7 @@ func TestLayerHitOrder(t *testing.T) { | |||
| 186 | 186 | output := make(chan ports.OutputEvent, 8) | |
| 187 | 187 | commands := make(chan ports.ClientCommand, 256) | |
| 188 | 188 | scenes := make(chan []ports.Scene, 1) | |
| 189 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes, Clock: steppingClock(t)}) | |
| 189 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{Clock: steppingClock(t)}) | |
| 190 | 190 | if err != nil { | |
| 191 | 191 | t.Fatal(err) | |
| 192 | 192 | } | |
| @@ -232,7 +232,7 @@ func TestLayerFocusAcrossOutputs(t *testing.T) { | |||
| 232 | 232 | output := make(chan ports.OutputEvent, 8) | |
| 233 | 233 | commands := make(chan ports.ClientCommand, 256) | |
| 234 | 234 | scenes := make(chan []ports.Scene, 1) | |
| 235 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | |
| 235 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{}) | |
| 236 | 236 | if err != nil { | |
| 237 | 237 | t.Fatal(err) | |
| 238 | 238 | } | |
| @@ -280,7 +280,7 @@ func TestLayerInputRegionPassThrough(t *testing.T) { | |||
| 280 | 280 | output := make(chan ports.OutputEvent, 16) | |
| 281 | 281 | commands := make(chan ports.ClientCommand, 256) | |
| 282 | 282 | scenes := make(chan []ports.Scene, 1) | |
| 283 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | |
| 283 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{}) | |
| 284 | 284 | if err != nil { | |
| 285 | 285 | t.Fatal(err) | |
| 286 | 286 | } | |
| @@ -325,7 +325,7 @@ func TestRemovedLayerInputRegionCleared(t *testing.T) { | |||
| 325 | 325 | output := make(chan ports.OutputEvent, 16) | |
| 326 | 326 | commands := make(chan ports.ClientCommand, 256) | |
| 327 | 327 | scenes := make(chan []ports.Scene, 1) | |
| 328 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | |
| 328 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{}) | |
| 329 | 329 | if err != nil { | |
| 330 | 330 | t.Fatal(err) | |
| 331 | 331 | } | |
| @@ -367,7 +367,7 @@ func TestPopupInputRegionPassesThroughToWindow(t *testing.T) { | |||
| 367 | 367 | output := make(chan ports.OutputEvent, 16) | |
| 368 | 368 | commands := make(chan ports.ClientCommand, 256) | |
| 369 | 369 | scenes := make(chan []ports.Scene, 1) | |
| 370 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | |
| 370 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{}) | |
| 371 | 371 | if err != nil { | |
| 372 | 372 | t.Fatal(err) | |
| 373 | 373 | } | |
| @@ -404,7 +404,7 @@ func TestHiddenGrabPopupLosesKeyboard(t *testing.T) { | |||
| 404 | 404 | output := make(chan ports.OutputEvent, 8) | |
| 405 | 405 | commands := make(chan ports.ClientCommand, 64) | |
| 406 | 406 | scenes := make(chan []ports.Scene, 1) | |
| 407 | - | c, err := core.New(cfg, core.Channels{Client: client, Output: output, Commands: commands, Scenes: scenes, Clock: steppingClock(t)}) | |
| 407 | + | c, err := core.New(cfg, core.Channels{Client: client, Output: output, Commands: commands, Scenes: scenes}, core.Options{Clock: steppingClock(t)}) | |
| 408 | 408 | if err != nil { | |
| 409 | 409 | t.Fatal(err) | |
| 410 | 410 | } | |
| @@ -30,7 +30,7 @@ func TestLayerPointerPopupAndFocus(t *testing.T) { | |||||
| 30 | output := make(chan ports.OutputEvent, 8) | 30 | output := make(chan ports.OutputEvent, 8) | ||
| 31 | commands := make(chan ports.ClientCommand, 256) | 31 | commands := make(chan ports.ClientCommand, 256) | ||
| 32 | scenes := make(chan []ports.Scene, 1) | 32 | scenes := make(chan []ports.Scene, 1) | ||
| 33 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | 33 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{}) |
| 34 | if err != nil { | 34 | if err != nil { | ||
| 35 | t.Fatal(err) | 35 | t.Fatal(err) | ||
| 36 | } | 36 | } | ||
| @@ -149,7 +149,7 @@ func TestWindowPopupUnderOverlay(t *testing.T) { | |||||
| 149 | output := make(chan ports.OutputEvent, 8) | 149 | output := make(chan ports.OutputEvent, 8) | ||
| 150 | commands := make(chan ports.ClientCommand, 256) | 150 | commands := make(chan ports.ClientCommand, 256) | ||
| 151 | scenes := make(chan []ports.Scene, 1) | 151 | scenes := make(chan []ports.Scene, 1) | ||
| 152 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | 152 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{}) |
| 153 | if err != nil { | 153 | if err != nil { | ||
| 154 | t.Fatal(err) | 154 | t.Fatal(err) | ||
| 155 | } | 155 | } | ||
| @@ -186,7 +186,7 @@ func TestLayerHitOrder(t *testing.T) { | |||||
| 186 | output := make(chan ports.OutputEvent, 8) | 186 | output := make(chan ports.OutputEvent, 8) | ||
| 187 | commands := make(chan ports.ClientCommand, 256) | 187 | commands := make(chan ports.ClientCommand, 256) | ||
| 188 | scenes := make(chan []ports.Scene, 1) | 188 | scenes := make(chan []ports.Scene, 1) | ||
| 189 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes, Clock: steppingClock(t)}) | 189 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{Clock: steppingClock(t)}) |
| 190 | if err != nil { | 190 | if err != nil { | ||
| 191 | t.Fatal(err) | 191 | t.Fatal(err) | ||
| 192 | } | 192 | } | ||
| @@ -232,7 +232,7 @@ func TestLayerFocusAcrossOutputs(t *testing.T) { | |||||
| 232 | output := make(chan ports.OutputEvent, 8) | 232 | output := make(chan ports.OutputEvent, 8) | ||
| 233 | commands := make(chan ports.ClientCommand, 256) | 233 | commands := make(chan ports.ClientCommand, 256) | ||
| 234 | scenes := make(chan []ports.Scene, 1) | 234 | scenes := make(chan []ports.Scene, 1) | ||
| 235 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | 235 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{}) |
| 236 | if err != nil { | 236 | if err != nil { | ||
| 237 | t.Fatal(err) | 237 | t.Fatal(err) | ||
| 238 | } | 238 | } | ||
| @@ -280,7 +280,7 @@ func TestLayerInputRegionPassThrough(t *testing.T) { | |||||
| 280 | output := make(chan ports.OutputEvent, 16) | 280 | output := make(chan ports.OutputEvent, 16) | ||
| 281 | commands := make(chan ports.ClientCommand, 256) | 281 | commands := make(chan ports.ClientCommand, 256) | ||
| 282 | scenes := make(chan []ports.Scene, 1) | 282 | scenes := make(chan []ports.Scene, 1) | ||
| 283 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | 283 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{}) |
| 284 | if err != nil { | 284 | if err != nil { | ||
| 285 | t.Fatal(err) | 285 | t.Fatal(err) | ||
| 286 | } | 286 | } | ||
| @@ -325,7 +325,7 @@ func TestRemovedLayerInputRegionCleared(t *testing.T) { | |||||
| 325 | output := make(chan ports.OutputEvent, 16) | 325 | output := make(chan ports.OutputEvent, 16) | ||
| 326 | commands := make(chan ports.ClientCommand, 256) | 326 | commands := make(chan ports.ClientCommand, 256) | ||
| 327 | scenes := make(chan []ports.Scene, 1) | 327 | scenes := make(chan []ports.Scene, 1) | ||
| 328 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | 328 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{}) |
| 329 | if err != nil { | 329 | if err != nil { | ||
| 330 | t.Fatal(err) | 330 | t.Fatal(err) | ||
| 331 | } | 331 | } | ||
| @@ -367,7 +367,7 @@ func TestPopupInputRegionPassesThroughToWindow(t *testing.T) { | |||||
| 367 | output := make(chan ports.OutputEvent, 16) | 367 | output := make(chan ports.OutputEvent, 16) | ||
| 368 | commands := make(chan ports.ClientCommand, 256) | 368 | commands := make(chan ports.ClientCommand, 256) | ||
| 369 | scenes := make(chan []ports.Scene, 1) | 369 | scenes := make(chan []ports.Scene, 1) | ||
| 370 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | 370 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{}) |
| 371 | if err != nil { | 371 | if err != nil { | ||
| 372 | t.Fatal(err) | 372 | t.Fatal(err) | ||
| 373 | } | 373 | } | ||
| @@ -404,7 +404,7 @@ func TestHiddenGrabPopupLosesKeyboard(t *testing.T) { | |||||
| 404 | output := make(chan ports.OutputEvent, 8) | 404 | output := make(chan ports.OutputEvent, 8) | ||
| 405 | commands := make(chan ports.ClientCommand, 64) | 405 | commands := make(chan ports.ClientCommand, 64) | ||
| 406 | scenes := make(chan []ports.Scene, 1) | 406 | scenes := make(chan []ports.Scene, 1) | ||
| 407 | - | c, err := core.New(cfg, core.Channels{Client: client, Output: output, Commands: commands, Scenes: scenes, Clock: steppingClock(t)}) | 407 | + | c, err := core.New(cfg, core.Channels{Client: client, Output: output, Commands: commands, Scenes: scenes}, core.Options{Clock: steppingClock(t)}) |
| 408 | if err != nil { | 408 | if err != nil { | ||
| 409 | t.Fatal(err) | 409 | t.Fatal(err) | ||
| 410 | } | 410 | } | ||
M internal/core/layers_test.go +2 -2
| @@ -71,7 +71,7 @@ func TestLayerChangedChannels(t *testing.T) { | |||
| 71 | 71 | cfg := ports.Config{} | |
| 72 | 72 | cfg.Keyboard.CmdKey = "super" | |
| 73 | 73 | cfg.Layout.MaxColumns = 2 | |
| 74 | - | c, err := New(cfg, Channels{Client: client, Output: output, Commands: commands, Scenes: scenes}) | |
| 74 | + | c, err := New(cfg, Channels{Client: client, Output: output, Commands: commands, Scenes: scenes}, Options{}) | |
| 75 | 75 | if err != nil { | |
| 76 | 76 | t.Fatal(err) | |
| 77 | 77 | } | |
| @@ -171,7 +171,7 @@ func TestFullscreenSceneLayers(t *testing.T) { | |||
| 171 | 171 | cfg := ports.Config{} | |
| 172 | 172 | cfg.Keyboard.CmdKey = "super" | |
| 173 | 173 | cfg.Layout.MaxColumns = 2 | |
| 174 | - | c, err := New(cfg, Channels{Client: client, Output: output, Commands: commands, Scenes: scenes, Clock: laterClock(t)}) | |
| 174 | + | c, err := New(cfg, Channels{Client: client, Output: output, Commands: commands, Scenes: scenes}, Options{Clock: laterClock(t)}) | |
| 175 | 175 | if err != nil { | |
| 176 | 176 | t.Fatal(err) | |
| 177 | 177 | } | |
| @@ -71,7 +71,7 @@ func TestLayerChangedChannels(t *testing.T) { | |||||
| 71 | cfg := ports.Config{} | 71 | cfg := ports.Config{} | ||
| 72 | cfg.Keyboard.CmdKey = "super" | 72 | cfg.Keyboard.CmdKey = "super" | ||
| 73 | cfg.Layout.MaxColumns = 2 | 73 | cfg.Layout.MaxColumns = 2 | ||
| 74 | - | c, err := New(cfg, Channels{Client: client, Output: output, Commands: commands, Scenes: scenes}) | 74 | + | c, err := New(cfg, Channels{Client: client, Output: output, Commands: commands, Scenes: scenes}, Options{}) |
| 75 | if err != nil { | 75 | if err != nil { | ||
| 76 | t.Fatal(err) | 76 | t.Fatal(err) | ||
| 77 | } | 77 | } | ||
| @@ -171,7 +171,7 @@ func TestFullscreenSceneLayers(t *testing.T) { | |||||
| 171 | cfg := ports.Config{} | 171 | cfg := ports.Config{} | ||
| 172 | cfg.Keyboard.CmdKey = "super" | 172 | cfg.Keyboard.CmdKey = "super" | ||
| 173 | cfg.Layout.MaxColumns = 2 | 173 | cfg.Layout.MaxColumns = 2 | ||
| 174 | - | c, err := New(cfg, Channels{Client: client, Output: output, Commands: commands, Scenes: scenes, Clock: laterClock(t)}) | 174 | + | c, err := New(cfg, Channels{Client: client, Output: output, Commands: commands, Scenes: scenes}, Options{Clock: laterClock(t)}) |
| 175 | if err != nil { | 175 | if err != nil { | ||
| 176 | t.Fatal(err) | 176 | t.Fatal(err) | ||
| 177 | } | 177 | } | ||
M internal/core/overview_configure_test.go +2 -2
| @@ -15,7 +15,7 @@ func TestOverviewNeighborPreviewKeepsHiddenConfigureSize(t *testing.T) { | |||
| 15 | 15 | cfg.Layout.MaxColumns = 3 | |
| 16 | 16 | commands := make(chan ports.ClientCommand, 256) | |
| 17 | 17 | scenes := make(chan []ports.Scene, 1) | |
| 18 | - | c, err := New(cfg, Channels{Commands: commands, Scenes: scenes}) | |
| 18 | + | c, err := New(cfg, Channels{Commands: commands, Scenes: scenes}, Options{}) | |
| 19 | 19 | if err != nil { | |
| 20 | 20 | t.Fatal(err) | |
| 21 | 21 | } | |
| @@ -72,7 +72,7 @@ func TestOverviewConfigureUsesRealLayoutAfterMaximize(t *testing.T) { | |||
| 72 | 72 | cfg.Layout.MaxColumns = 3 | |
| 73 | 73 | commands := make(chan ports.ClientCommand, 256) | |
| 74 | 74 | scenes := make(chan []ports.Scene, 1) | |
| 75 | - | c, err := New(cfg, Channels{Commands: commands, Scenes: scenes}) | |
| 75 | + | c, err := New(cfg, Channels{Commands: commands, Scenes: scenes}, Options{}) | |
| 76 | 76 | if err != nil { | |
| 77 | 77 | t.Fatal(err) | |
| 78 | 78 | } | |
| @@ -15,7 +15,7 @@ func TestOverviewNeighborPreviewKeepsHiddenConfigureSize(t *testing.T) { | |||||
| 15 | cfg.Layout.MaxColumns = 3 | 15 | cfg.Layout.MaxColumns = 3 | ||
| 16 | commands := make(chan ports.ClientCommand, 256) | 16 | commands := make(chan ports.ClientCommand, 256) | ||
| 17 | scenes := make(chan []ports.Scene, 1) | 17 | scenes := make(chan []ports.Scene, 1) | ||
| 18 | - | c, err := New(cfg, Channels{Commands: commands, Scenes: scenes}) | 18 | + | c, err := New(cfg, Channels{Commands: commands, Scenes: scenes}, Options{}) |
| 19 | if err != nil { | 19 | if err != nil { | ||
| 20 | t.Fatal(err) | 20 | t.Fatal(err) | ||
| 21 | } | 21 | } | ||
| @@ -72,7 +72,7 @@ func TestOverviewConfigureUsesRealLayoutAfterMaximize(t *testing.T) { | |||||
| 72 | cfg.Layout.MaxColumns = 3 | 72 | cfg.Layout.MaxColumns = 3 | ||
| 73 | commands := make(chan ports.ClientCommand, 256) | 73 | commands := make(chan ports.ClientCommand, 256) | ||
| 74 | scenes := make(chan []ports.Scene, 1) | 74 | scenes := make(chan []ports.Scene, 1) | ||
| 75 | - | c, err := New(cfg, Channels{Commands: commands, Scenes: scenes}) | 75 | + | c, err := New(cfg, Channels{Commands: commands, Scenes: scenes}, Options{}) |
| 76 | if err != nil { | 76 | if err != nil { | ||
| 77 | t.Fatal(err) | 77 | t.Fatal(err) | ||
| 78 | } | 78 | } | ||
M internal/core/overview_input_test.go +4 -4
| @@ -18,7 +18,7 @@ func TestOverviewLauncherKeepsKeys(t *testing.T) { | |||
| 18 | 18 | output := make(chan ports.OutputEvent, 8) | |
| 19 | 19 | commands := make(chan ports.ClientCommand, 256) | |
| 20 | 20 | scenes := make(chan []ports.Scene, 1) | |
| 21 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | |
| 21 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{}) | |
| 22 | 22 | if err != nil { | |
| 23 | 23 | t.Fatal(err) | |
| 24 | 24 | } | |
| @@ -71,7 +71,7 @@ func TestOverviewFocusBindsNavigate(t *testing.T) { | |||
| 71 | 71 | output := make(chan ports.OutputEvent, 8) | |
| 72 | 72 | commands := make(chan ports.ClientCommand, 256) | |
| 73 | 73 | scenes := make(chan []ports.Scene, 1) | |
| 74 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | |
| 74 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{}) | |
| 75 | 75 | if err != nil { | |
| 76 | 76 | t.Fatal(err) | |
| 77 | 77 | } | |
| @@ -122,7 +122,7 @@ func TestOverviewCloseBindTargetsPreview(t *testing.T) { | |||
| 122 | 122 | output := make(chan ports.OutputEvent, 8) | |
| 123 | 123 | commands := make(chan ports.ClientCommand, 256) | |
| 124 | 124 | scenes := make(chan []ports.Scene, 1) | |
| 125 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | |
| 125 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{}) | |
| 126 | 126 | if err != nil { | |
| 127 | 127 | t.Fatal(err) | |
| 128 | 128 | } | |
| @@ -194,7 +194,7 @@ func TestOverviewThreeFingerNavigation(t *testing.T) { | |||
| 194 | 194 | output := make(chan ports.OutputEvent, 8) | |
| 195 | 195 | commands := make(chan ports.ClientCommand, 256) | |
| 196 | 196 | scenes := make(chan []ports.Scene, 1) | |
| 197 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | |
| 197 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{}) | |
| 198 | 198 | if err != nil { | |
| 199 | 199 | t.Fatal(err) | |
| 200 | 200 | } | |
| @@ -18,7 +18,7 @@ func TestOverviewLauncherKeepsKeys(t *testing.T) { | |||||
| 18 | output := make(chan ports.OutputEvent, 8) | 18 | output := make(chan ports.OutputEvent, 8) | ||
| 19 | commands := make(chan ports.ClientCommand, 256) | 19 | commands := make(chan ports.ClientCommand, 256) | ||
| 20 | scenes := make(chan []ports.Scene, 1) | 20 | scenes := make(chan []ports.Scene, 1) | ||
| 21 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | 21 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{}) |
| 22 | if err != nil { | 22 | if err != nil { | ||
| 23 | t.Fatal(err) | 23 | t.Fatal(err) | ||
| 24 | } | 24 | } | ||
| @@ -71,7 +71,7 @@ func TestOverviewFocusBindsNavigate(t *testing.T) { | |||||
| 71 | output := make(chan ports.OutputEvent, 8) | 71 | output := make(chan ports.OutputEvent, 8) | ||
| 72 | commands := make(chan ports.ClientCommand, 256) | 72 | commands := make(chan ports.ClientCommand, 256) | ||
| 73 | scenes := make(chan []ports.Scene, 1) | 73 | scenes := make(chan []ports.Scene, 1) | ||
| 74 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | 74 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{}) |
| 75 | if err != nil { | 75 | if err != nil { | ||
| 76 | t.Fatal(err) | 76 | t.Fatal(err) | ||
| 77 | } | 77 | } | ||
| @@ -122,7 +122,7 @@ func TestOverviewCloseBindTargetsPreview(t *testing.T) { | |||||
| 122 | output := make(chan ports.OutputEvent, 8) | 122 | output := make(chan ports.OutputEvent, 8) | ||
| 123 | commands := make(chan ports.ClientCommand, 256) | 123 | commands := make(chan ports.ClientCommand, 256) | ||
| 124 | scenes := make(chan []ports.Scene, 1) | 124 | scenes := make(chan []ports.Scene, 1) | ||
| 125 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | 125 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{}) |
| 126 | if err != nil { | 126 | if err != nil { | ||
| 127 | t.Fatal(err) | 127 | t.Fatal(err) | ||
| 128 | } | 128 | } | ||
| @@ -194,7 +194,7 @@ func TestOverviewThreeFingerNavigation(t *testing.T) { | |||||
| 194 | output := make(chan ports.OutputEvent, 8) | 194 | output := make(chan ports.OutputEvent, 8) | ||
| 195 | commands := make(chan ports.ClientCommand, 256) | 195 | commands := make(chan ports.ClientCommand, 256) | ||
| 196 | scenes := make(chan []ports.Scene, 1) | 196 | scenes := make(chan []ports.Scene, 1) | ||
| 197 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | 197 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}, core.Options{}) |
| 198 | if err != nil { | 198 | if err != nil { | ||
| 199 | t.Fatal(err) | 199 | t.Fatal(err) | ||
| 200 | } | 200 | } | ||
M internal/core/overview_named_test.go +2 -2
| @@ -124,7 +124,7 @@ func TestOverviewNamedAnchorSurvivesReplug(t *testing.T) { | |||
| 124 | 124 | cfg.Layout.MaxColumns = 3 | |
| 125 | 125 | cfg.Keyboard.CmdKey = "super" | |
| 126 | 126 | cfg.Workspaces = []ports.WorkspaceConfig{{Name: "dev", Monitor: "OUT-1"}} | |
| 127 | - | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1)}) | |
| 127 | + | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1)}, Options{}) | |
| 128 | 128 | if err != nil { | |
| 129 | 129 | t.Fatal(err) | |
| 130 | 130 | } | |
| @@ -394,7 +394,7 @@ func TestOverviewNamedDividerPublished(t *testing.T) { | |||
| 394 | 394 | cfg := ports.Config{} | |
| 395 | 395 | cfg.Keyboard.CmdKey = "super" | |
| 396 | 396 | cfg.Layout.MaxColumns = 3 | |
| 397 | - | c, err := New(cfg, Channels{Commands: make(chan ports.ClientCommand, 256), Scenes: scenes}) | |
| 397 | + | c, err := New(cfg, Channels{Commands: make(chan ports.ClientCommand, 256), Scenes: scenes}, Options{}) | |
| 398 | 398 | if err != nil { | |
| 399 | 399 | t.Fatal(err) | |
| 400 | 400 | } | |
| @@ -124,7 +124,7 @@ func TestOverviewNamedAnchorSurvivesReplug(t *testing.T) { | |||||
| 124 | cfg.Layout.MaxColumns = 3 | 124 | cfg.Layout.MaxColumns = 3 | ||
| 125 | cfg.Keyboard.CmdKey = "super" | 125 | cfg.Keyboard.CmdKey = "super" | ||
| 126 | cfg.Workspaces = []ports.WorkspaceConfig{{Name: "dev", Monitor: "OUT-1"}} | 126 | cfg.Workspaces = []ports.WorkspaceConfig{{Name: "dev", Monitor: "OUT-1"}} | ||
| 127 | - | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1)}) | 127 | + | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1)}, Options{}) |
| 128 | if err != nil { | 128 | if err != nil { | ||
| 129 | t.Fatal(err) | 129 | t.Fatal(err) | ||
| 130 | } | 130 | } | ||
| @@ -394,7 +394,7 @@ func TestOverviewNamedDividerPublished(t *testing.T) { | |||||
| 394 | cfg := ports.Config{} | 394 | cfg := ports.Config{} | ||
| 395 | cfg.Keyboard.CmdKey = "super" | 395 | cfg.Keyboard.CmdKey = "super" | ||
| 396 | cfg.Layout.MaxColumns = 3 | 396 | cfg.Layout.MaxColumns = 3 | ||
| 397 | - | c, err := New(cfg, Channels{Commands: make(chan ports.ClientCommand, 256), Scenes: scenes}) | 397 | + | c, err := New(cfg, Channels{Commands: make(chan ports.ClientCommand, 256), Scenes: scenes}, Options{}) |
| 398 | if err != nil { | 398 | if err != nil { | ||
| 399 | t.Fatal(err) | 399 | t.Fatal(err) | ||
| 400 | } | 400 | } | ||
M internal/core/persist_test.go +1 -1
| @@ -96,7 +96,7 @@ func TestScaleBindReportsScale(t *testing.T) { | |||
| 96 | 96 | output := make(chan ports.OutputEvent, 8) | |
| 97 | 97 | scales := make(chan ports.ScaleChanged, 8) | |
| 98 | 98 | scenes := make(chan []ports.Scene, 1) | |
| 99 | - | c, err := core.New(cfg, core.Channels{Input: input, Output: output, Commands: make(chan ports.ClientCommand, 64), Scenes: scenes, Scales: scales}) | |
| 99 | + | c, err := core.New(cfg, core.Channels{Input: input, Output: output, Commands: make(chan ports.ClientCommand, 64), Scenes: scenes, Scales: scales}, core.Options{}) | |
| 100 | 100 | if err != nil { | |
| 101 | 101 | t.Fatal(err) | |
| 102 | 102 | } | |
| @@ -96,7 +96,7 @@ func TestScaleBindReportsScale(t *testing.T) { | |||||
| 96 | output := make(chan ports.OutputEvent, 8) | 96 | output := make(chan ports.OutputEvent, 8) | ||
| 97 | scales := make(chan ports.ScaleChanged, 8) | 97 | scales := make(chan ports.ScaleChanged, 8) | ||
| 98 | scenes := make(chan []ports.Scene, 1) | 98 | scenes := make(chan []ports.Scene, 1) | ||
| 99 | - | c, err := core.New(cfg, core.Channels{Input: input, Output: output, Commands: make(chan ports.ClientCommand, 64), Scenes: scenes, Scales: scales}) | 99 | + | c, err := core.New(cfg, core.Channels{Input: input, Output: output, Commands: make(chan ports.ClientCommand, 64), Scenes: scenes, Scales: scales}, core.Options{}) |
| 100 | if err != nil { | 100 | if err != nil { | ||
| 101 | t.Fatal(err) | 101 | t.Fatal(err) | ||
| 102 | } | 102 | } | ||
M internal/core/power.go +1 -2
| @@ -2,7 +2,6 @@ package core | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | - | "time" | |
| 6 | 5 | ||
| 7 | 6 | "github.com/bnema/neferwl/internal/ports" | |
| 8 | 7 | ) | |
| @@ -13,7 +12,7 @@ import ( | |||
| 13 | 12 | // they reconnect on. | |
| 14 | 13 | func (c *Core) userActivity(ctx context.Context) error { | |
| 15 | 14 | clear(c.offGone) | |
| 16 | - | now := time.Now() | |
| 15 | + | now := c.now() | |
| 17 | 16 | if now.Sub(c.activity) < ports.ActivityInterval { | |
| 18 | 17 | return nil | |
| 19 | 18 | } | |
| @@ -2,7 +2,6 @@ package core | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | - | "time" | |||
| 6 | 5 | ||||
| 7 | "github.com/bnema/neferwl/internal/ports" | 6 | "github.com/bnema/neferwl/internal/ports" | ||
| 8 | ) | 7 | ) | ||
| @@ -13,7 +12,7 @@ import ( | |||||
| 13 | // they reconnect on. | 12 | // they reconnect on. | ||
| 14 | func (c *Core) userActivity(ctx context.Context) error { | 13 | func (c *Core) userActivity(ctx context.Context) error { | ||
| 15 | clear(c.offGone) | 14 | clear(c.offGone) | ||
| 16 | - | now := time.Now() | 15 | + | now := c.now() |
| 17 | if now.Sub(c.activity) < ports.ActivityInterval { | 16 | if now.Sub(c.activity) < ports.ActivityInterval { | ||
| 18 | return nil | 17 | return nil | ||
| 19 | } | 18 | } | ||
M internal/core/power_test.go +36 -1
| @@ -12,13 +12,18 @@ import ( | |||
| 12 | 12 | ) | |
| 13 | 13 | ||
| 14 | 14 | func powerCore(t *testing.T) (chan ports.InputEvent, chan ports.ClientEvent, chan ports.OutputEvent, chan ports.ClientCommand, chan []ports.Scene) { | |
| 15 | + | t.Helper() | |
| 16 | + | return powerCoreWith(t, core.Options{}) | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | func powerCoreWith(t *testing.T, opts core.Options) (chan ports.InputEvent, chan ports.ClientEvent, chan ports.OutputEvent, chan ports.ClientCommand, chan []ports.Scene) { | |
| 15 | 20 | t.Helper() | |
| 16 | 21 | input := make(chan ports.InputEvent, 4) | |
| 17 | 22 | client := make(chan ports.ClientEvent, 4) | |
| 18 | 23 | output := make(chan ports.OutputEvent, 2) | |
| 19 | 24 | commands := make(chan ports.ClientCommand, 64) | |
| 20 | 25 | scenes := make(chan []ports.Scene, 1) | |
| 21 | - | c, err := core.New(config.Defaults(), core.Channels{Input: input, Client: client, Output: output, Commands: commands, Scenes: scenes, Spawn: make(chan ports.SpawnRequest, 4)}) | |
| 26 | + | c, err := core.New(config.Defaults(), core.Channels{Input: input, Client: client, Output: output, Commands: commands, Scenes: scenes, Spawn: make(chan ports.SpawnRequest, 4)}, opts) | |
| 22 | 27 | if err != nil { | |
| 23 | 28 | t.Fatal(err) | |
| 24 | 29 | } | |
| @@ -164,3 +169,33 @@ func TestUserActivity(t *testing.T) { | |||
| 164 | 169 | input <- ports.PointerMotion{X: 2, Y: 2} | |
| 165 | 170 | commandOf[ports.UserActivity](t, commands) | |
| 166 | 171 | } | |
| 172 | + | ||
| 173 | + | // With an injected clock, the throttle follows it: no second UserActivity | |
| 174 | + | // until the clock has advanced past the interval. | |
| 175 | + | func TestUserActivityFollowsClock(t *testing.T) { | |
| 176 | + | clk := newStepClock(t) | |
| 177 | + | input, _, _, commands, _ := powerCoreWith(t, core.Options{Clock: clk.clock}) | |
| 178 | + | input <- ports.PointerMotion{X: 1, Y: 1} | |
| 179 | + | commandOf[ports.UserActivity](t, commands) | |
| 180 | + | clk.advance(ports.ActivityInterval / 2) | |
| 181 | + | // Core takes input in order and the input channel holds 4: once the | |
| 182 | + | // sixth send returns, the first motion is handled, and a wrongly | |
| 183 | + | // unthrottled activity would already be in commands. | |
| 184 | + | for range 6 { | |
| 185 | + | input <- ports.PointerMotion{X: 1, Y: 1} | |
| 186 | + | } | |
| 187 | + | for { | |
| 188 | + | select { | |
| 189 | + | case c := <-commands: | |
| 190 | + | if _, ok := c.(ports.UserActivity); ok { | |
| 191 | + | t.Fatal("second UserActivity before the interval passed on the clock") | |
| 192 | + | } | |
| 193 | + | continue | |
| 194 | + | default: | |
| 195 | + | } | |
| 196 | + | break | |
| 197 | + | } | |
| 198 | + | clk.advance(ports.ActivityInterval) | |
| 199 | + | input <- ports.PointerMotion{X: 2, Y: 2} | |
| 200 | + | commandOf[ports.UserActivity](t, commands) | |
| 201 | + | } | |
| @@ -12,13 +12,18 @@ import ( | |||||
| 12 | ) | 12 | ) | ||
| 13 | 13 | ||||
| 14 | func powerCore(t *testing.T) (chan ports.InputEvent, chan ports.ClientEvent, chan ports.OutputEvent, chan ports.ClientCommand, chan []ports.Scene) { | 14 | func powerCore(t *testing.T) (chan ports.InputEvent, chan ports.ClientEvent, chan ports.OutputEvent, chan ports.ClientCommand, chan []ports.Scene) { | ||
| 15 | + | t.Helper() | |||
| 16 | + | return powerCoreWith(t, core.Options{}) | |||
| 17 | + | } | |||
| 18 | + | ||||
| 19 | + | func powerCoreWith(t *testing.T, opts core.Options) (chan ports.InputEvent, chan ports.ClientEvent, chan ports.OutputEvent, chan ports.ClientCommand, chan []ports.Scene) { | |||
| 15 | t.Helper() | 20 | t.Helper() | ||
| 16 | input := make(chan ports.InputEvent, 4) | 21 | input := make(chan ports.InputEvent, 4) | ||
| 17 | client := make(chan ports.ClientEvent, 4) | 22 | client := make(chan ports.ClientEvent, 4) | ||
| 18 | output := make(chan ports.OutputEvent, 2) | 23 | output := make(chan ports.OutputEvent, 2) | ||
| 19 | commands := make(chan ports.ClientCommand, 64) | 24 | commands := make(chan ports.ClientCommand, 64) | ||
| 20 | scenes := make(chan []ports.Scene, 1) | 25 | scenes := make(chan []ports.Scene, 1) | ||
| 21 | - | c, err := core.New(config.Defaults(), core.Channels{Input: input, Client: client, Output: output, Commands: commands, Scenes: scenes, Spawn: make(chan ports.SpawnRequest, 4)}) | 26 | + | c, err := core.New(config.Defaults(), core.Channels{Input: input, Client: client, Output: output, Commands: commands, Scenes: scenes, Spawn: make(chan ports.SpawnRequest, 4)}, opts) |
| 22 | if err != nil { | 27 | if err != nil { | ||
| 23 | t.Fatal(err) | 28 | t.Fatal(err) | ||
| 24 | } | 29 | } | ||
| @@ -164,3 +169,33 @@ func TestUserActivity(t *testing.T) { | |||||
| 164 | input <- ports.PointerMotion{X: 2, Y: 2} | 169 | input <- ports.PointerMotion{X: 2, Y: 2} | ||
| 165 | commandOf[ports.UserActivity](t, commands) | 170 | commandOf[ports.UserActivity](t, commands) | ||
| 166 | } | 171 | } | ||
| 172 | + | ||||
| 173 | + | // With an injected clock, the throttle follows it: no second UserActivity | |||
| 174 | + | // until the clock has advanced past the interval. | |||
| 175 | + | func TestUserActivityFollowsClock(t *testing.T) { | |||
| 176 | + | clk := newStepClock(t) | |||
| 177 | + | input, _, _, commands, _ := powerCoreWith(t, core.Options{Clock: clk.clock}) | |||
| 178 | + | input <- ports.PointerMotion{X: 1, Y: 1} | |||
| 179 | + | commandOf[ports.UserActivity](t, commands) | |||
| 180 | + | clk.advance(ports.ActivityInterval / 2) | |||
| 181 | + | // Core takes input in order and the input channel holds 4: once the | |||
| 182 | + | // sixth send returns, the first motion is handled, and a wrongly | |||
| 183 | + | // unthrottled activity would already be in commands. | |||
| 184 | + | for range 6 { | |||
| 185 | + | input <- ports.PointerMotion{X: 1, Y: 1} | |||
| 186 | + | } | |||
| 187 | + | for { | |||
| 188 | + | select { | |||
| 189 | + | case c := <-commands: | |||
| 190 | + | if _, ok := c.(ports.UserActivity); ok { | |||
| 191 | + | t.Fatal("second UserActivity before the interval passed on the clock") | |||
| 192 | + | } | |||
| 193 | + | continue | |||
| 194 | + | default: | |||
| 195 | + | } | |||
| 196 | + | break | |||
| 197 | + | } | |||
| 198 | + | clk.advance(ports.ActivityInterval) | |||
| 199 | + | input <- ports.PointerMotion{X: 2, Y: 2} | |||
| 200 | + | commandOf[ports.UserActivity](t, commands) | |||
| 201 | + | } | |||
M internal/core/publish_alloc_test.go +1 -1
| @@ -42,7 +42,7 @@ func publishRig(t *testing.T, screens int) *Core { | |||
| 42 | 42 | cfg.Border.Width = 2 | |
| 43 | 43 | cfg.Animations.On = true | |
| 44 | 44 | cfg.Animations.Slowdown = 10 | |
| 45 | - | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Commands: make(chan ports.ClientCommand, 64)}) | |
| 45 | + | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Commands: make(chan ports.ClientCommand, 64)}, Options{}) | |
| 46 | 46 | if err != nil { | |
| 47 | 47 | t.Fatal(err) | |
| 48 | 48 | } | |
| @@ -42,7 +42,7 @@ func publishRig(t *testing.T, screens int) *Core { | |||||
| 42 | cfg.Border.Width = 2 | 42 | cfg.Border.Width = 2 | ||
| 43 | cfg.Animations.On = true | 43 | cfg.Animations.On = true | ||
| 44 | cfg.Animations.Slowdown = 10 | 44 | cfg.Animations.Slowdown = 10 | ||
| 45 | - | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Commands: make(chan ports.ClientCommand, 64)}) | 45 | + | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Commands: make(chan ports.ClientCommand, 64)}, Options{}) |
| 46 | if err != nil { | 46 | if err != nil { | ||
| 47 | t.Fatal(err) | 47 | t.Fatal(err) | ||
| 48 | } | 48 | } | ||
M internal/core/pulse.go +3 -3
| @@ -77,7 +77,7 @@ func (c *Core) pulseFocus(window WindowID, held bool) { | |||
| 77 | 77 | if c.justMapped(window) { | |
| 78 | 78 | return | |
| 79 | 79 | } | |
| 80 | - | p.timerC, p.timerStop = newTimer(c.ch.Clock, pulseSettle) | |
| 80 | + | p.timerC, p.timerStop = newTimer(c.opts.Clock, pulseSettle) | |
| 81 | 81 | } | |
| 82 | 82 | ||
| 83 | 83 | // justMapped reports whether the window got the focus as it mapped: opening | |
| @@ -98,14 +98,14 @@ func (c *Core) pulseTick() bool { | |||
| 98 | 98 | } | |
| 99 | 99 | now := c.now() | |
| 100 | 100 | if wait := pulseSettle - now.Sub(p.since); wait > 0 { | |
| 101 | - | p.timerC, p.timerStop = newTimer(c.ch.Clock, wait) | |
| 101 | + | p.timerC, p.timerStop = newTimer(c.opts.Clock, wait) | |
| 102 | 102 | return false | |
| 103 | 103 | } | |
| 104 | 104 | // A swipe or its landing slide still shows the neighbours: decide on | |
| 105 | 105 | // the settled layout, or a window left alone would start a pulse cut | |
| 106 | 106 | // short. | |
| 107 | 107 | if !c.cur().settled() { | |
| 108 | - | p.timerC, p.timerStop = newTimer(c.ch.Clock, pulseRecheck) | |
| 108 | + | p.timerC, p.timerStop = newTimer(c.opts.Clock, pulseRecheck) | |
| 109 | 109 | return false | |
| 110 | 110 | } | |
| 111 | 111 | if p.target == p.last && now.Sub(p.lastAt) < pulseCooldown { | |
| @@ -77,7 +77,7 @@ func (c *Core) pulseFocus(window WindowID, held bool) { | |||||
| 77 | if c.justMapped(window) { | 77 | if c.justMapped(window) { | ||
| 78 | return | 78 | return | ||
| 79 | } | 79 | } | ||
| 80 | - | p.timerC, p.timerStop = newTimer(c.ch.Clock, pulseSettle) | 80 | + | p.timerC, p.timerStop = newTimer(c.opts.Clock, pulseSettle) |
| 81 | } | 81 | } | ||
| 82 | 82 | ||||
| 83 | // justMapped reports whether the window got the focus as it mapped: opening | 83 | // justMapped reports whether the window got the focus as it mapped: opening | ||
| @@ -98,14 +98,14 @@ func (c *Core) pulseTick() bool { | |||||
| 98 | } | 98 | } | ||
| 99 | now := c.now() | 99 | now := c.now() | ||
| 100 | if wait := pulseSettle - now.Sub(p.since); wait > 0 { | 100 | if wait := pulseSettle - now.Sub(p.since); wait > 0 { | ||
| 101 | - | p.timerC, p.timerStop = newTimer(c.ch.Clock, wait) | 101 | + | p.timerC, p.timerStop = newTimer(c.opts.Clock, wait) |
| 102 | return false | 102 | return false | ||
| 103 | } | 103 | } | ||
| 104 | // A swipe or its landing slide still shows the neighbours: decide on | 104 | // A swipe or its landing slide still shows the neighbours: decide on | ||
| 105 | // the settled layout, or a window left alone would start a pulse cut | 105 | // the settled layout, or a window left alone would start a pulse cut | ||
| 106 | // short. | 106 | // short. | ||
| 107 | if !c.cur().settled() { | 107 | if !c.cur().settled() { | ||
| 108 | - | p.timerC, p.timerStop = newTimer(c.ch.Clock, pulseRecheck) | 108 | + | p.timerC, p.timerStop = newTimer(c.opts.Clock, pulseRecheck) |
| 109 | return false | 109 | return false | ||
| 110 | } | 110 | } | ||
| 111 | if p.target == p.last && now.Sub(p.lastAt) < pulseCooldown { | 111 | if p.target == p.last && now.Sub(p.lastAt) < pulseCooldown { | ||
M internal/core/refresh_bench_test.go +1 -1
| @@ -17,7 +17,7 @@ func BenchmarkRefreshShown(b *testing.B) { | |||
| 17 | 17 | cfg.Layout.MaxColumns = 4 | |
| 18 | 18 | cfg.Animations.On = true | |
| 19 | 19 | cfg.Animations.Slowdown = 10 | |
| 20 | - | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Commands: make(chan ports.ClientCommand, 64)}) | |
| 20 | + | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Commands: make(chan ports.ClientCommand, 64)}, Options{}) | |
| 21 | 21 | if err != nil { | |
| 22 | 22 | b.Fatal(err) | |
| 23 | 23 | } | |
| @@ -17,7 +17,7 @@ func BenchmarkRefreshShown(b *testing.B) { | |||||
| 17 | cfg.Layout.MaxColumns = 4 | 17 | cfg.Layout.MaxColumns = 4 | ||
| 18 | cfg.Animations.On = true | 18 | cfg.Animations.On = true | ||
| 19 | cfg.Animations.Slowdown = 10 | 19 | cfg.Animations.Slowdown = 10 | ||
| 20 | - | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Commands: make(chan ports.ClientCommand, 64)}) | 20 | + | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Commands: make(chan ports.ClientCommand, 64)}, Options{}) |
| 21 | if err != nil { | 21 | if err != nil { | ||
| 22 | b.Fatal(err) | 22 | b.Fatal(err) | ||
| 23 | } | 23 | } | ||
M internal/core/screens_test.go +41 -2
| @@ -40,6 +40,12 @@ func startMulti(t *testing.T, edit func(*ports.Config), outs ...ports.OutputInfo | |||
| 40 | 40 | ||
| 41 | 41 | // startRig is startMulti; terminal enables the configured automatic-open policy. | |
| 42 | 42 | func startRig(t *testing.T, terminal bool, edit func(*ports.Config), outs ...ports.OutputInfo) *multiRig { | |
| 43 | + | t.Helper() | |
| 44 | + | return startRigWith(t, core.Options{Terminal: terminal}, edit, outs...) | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | // startRigWith is startRig with explicit core options. | |
| 48 | + | func startRigWith(t *testing.T, opts core.Options, edit func(*ports.Config), outs ...ports.OutputInfo) *multiRig { | |
| 43 | 49 | t.Helper() | |
| 44 | 50 | cfg := altCmdDefaults() | |
| 45 | 51 | cfg.Border.Width = 0 | |
| @@ -51,7 +57,7 @@ func startRig(t *testing.T, terminal bool, edit func(*ports.Config), outs ...por | |||
| 51 | 57 | output: make(chan ports.OutputEvent, 4), reload: make(chan ports.ConfigChanged, 4), | |
| 52 | 58 | commands: make(chan ports.ClientCommand, 1024), scenes: make(chan []ports.Scene, 1), spawn: make(chan ports.SpawnRequest, 16), state: make(chan ports.State, 1), cfg: cfg, | |
| 53 | 59 | } | |
| 54 | - | c, err := core.New(cfg, core.Channels{Client: r.client, Input: r.input, Output: r.output, Config: r.reload, Commands: r.commands, Scenes: r.scenes, Spawn: r.spawn, State: r.state, Terminal: terminal}) | |
| 60 | + | c, err := core.New(cfg, core.Channels{Client: r.client, Input: r.input, Output: r.output, Config: r.reload, Commands: r.commands, Scenes: r.scenes, Spawn: r.spawn, State: r.state}, opts) | |
| 55 | 61 | if err != nil { | |
| 56 | 62 | t.Fatal(err) | |
| 57 | 63 | } | |
| @@ -622,7 +628,7 @@ func TestFirstTerminalFollowsSetOutputs(t *testing.T) { | |||
| 622 | 628 | output := make(chan ports.OutputEvent, 1) | |
| 623 | 629 | scenes := make(chan []ports.Scene, 1) | |
| 624 | 630 | state := make(chan ports.State, 1) | |
| 625 | - | c, err := core.New(cfg, core.Channels{Output: output, Commands: commands, Spawn: spawn, Scenes: scenes, State: state, Terminal: true}) | |
| 631 | + | c, err := core.New(cfg, core.Channels{Output: output, Commands: commands, Spawn: spawn, Scenes: scenes, State: state}, core.Options{Terminal: true}) | |
| 626 | 632 | if err != nil { | |
| 627 | 633 | t.Fatal(err) | |
| 628 | 634 | } | |
| @@ -753,6 +759,39 @@ func TestEmptyWorkspaceGetsTerminal(t *testing.T) { | |||
| 753 | 759 | } | |
| 754 | 760 | } | |
| 755 | 761 | ||
| 762 | + | // A terminal that exits is not respawned before termRetry has passed on | |
| 763 | + | // the injected clock, and is once it has. | |
| 764 | + | func TestTerminalRetryFollowsClock(t *testing.T) { | |
| 765 | + | clk := newStepClock(t) | |
| 766 | + | r := startRigWith(t, core.Options{Terminal: true, Clock: clk.clock}, func(c *ports.Config) { c.Terminal.AutoOpen = "all" }, left) | |
| 767 | + | first := receive(t, r.spawn) | |
| 768 | + | r.client <- ports.WindowMapped{ID: 1, Slot: token(t, first)} | |
| 769 | + | receive(t, r.scenes) | |
| 770 | + | r.client <- ports.WindowUnmapped{ID: 1} | |
| 771 | + | receive(t, r.scenes) | |
| 772 | + | if len(r.spawn) != 0 { | |
| 773 | + | t.Fatal("respawned at once") | |
| 774 | + | } | |
| 775 | + | // churn empties the workspace again, so fillEmpty runs at the clock's time. | |
| 776 | + | churn := func(id ports.WindowID) { | |
| 777 | + | r.client <- ports.WindowMapped{ID: id} | |
| 778 | + | receive(t, r.scenes) | |
| 779 | + | r.client <- ports.WindowUnmapped{ID: id} | |
| 780 | + | receive(t, r.scenes) | |
| 781 | + | } | |
| 782 | + | // termRetry is private to core: stay far below and far above it. | |
| 783 | + | clk.advance(time.Second) | |
| 784 | + | churn(2) | |
| 785 | + | if len(r.spawn) != 0 { | |
| 786 | + | t.Fatal("respawned before the retry interval") | |
| 787 | + | } | |
| 788 | + | clk.advance(time.Minute) | |
| 789 | + | churn(3) | |
| 790 | + | if req := receive(t, r.spawn); req.Argv[0] != "foot" { | |
| 791 | + | t.Fatal(req) | |
| 792 | + | } | |
| 793 | + | } | |
| 794 | + | ||
| 756 | 795 | func TestLastColumnMovesToNeighborOutput(t *testing.T) { | |
| 757 | 796 | r := startRig(t, true, nil, left, right) | |
| 758 | 797 | r.mapWindow(t, 1) | |
| @@ -40,6 +40,12 @@ func startMulti(t *testing.T, edit func(*ports.Config), outs ...ports.OutputInfo | |||||
| 40 | 40 | ||||
| 41 | // startRig is startMulti; terminal enables the configured automatic-open policy. | 41 | // startRig is startMulti; terminal enables the configured automatic-open policy. | ||
| 42 | func startRig(t *testing.T, terminal bool, edit func(*ports.Config), outs ...ports.OutputInfo) *multiRig { | 42 | func startRig(t *testing.T, terminal bool, edit func(*ports.Config), outs ...ports.OutputInfo) *multiRig { | ||
| 43 | + | t.Helper() | |||
| 44 | + | return startRigWith(t, core.Options{Terminal: terminal}, edit, outs...) | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | // startRigWith is startRig with explicit core options. | |||
| 48 | + | func startRigWith(t *testing.T, opts core.Options, edit func(*ports.Config), outs ...ports.OutputInfo) *multiRig { | |||
| 43 | t.Helper() | 49 | t.Helper() | ||
| 44 | cfg := altCmdDefaults() | 50 | cfg := altCmdDefaults() | ||
| 45 | cfg.Border.Width = 0 | 51 | cfg.Border.Width = 0 | ||
| @@ -51,7 +57,7 @@ func startRig(t *testing.T, terminal bool, edit func(*ports.Config), outs ...por | |||||
| 51 | output: make(chan ports.OutputEvent, 4), reload: make(chan ports.ConfigChanged, 4), | 57 | output: make(chan ports.OutputEvent, 4), reload: make(chan ports.ConfigChanged, 4), | ||
| 52 | commands: make(chan ports.ClientCommand, 1024), scenes: make(chan []ports.Scene, 1), spawn: make(chan ports.SpawnRequest, 16), state: make(chan ports.State, 1), cfg: cfg, | 58 | commands: make(chan ports.ClientCommand, 1024), scenes: make(chan []ports.Scene, 1), spawn: make(chan ports.SpawnRequest, 16), state: make(chan ports.State, 1), cfg: cfg, | ||
| 53 | } | 59 | } | ||
| 54 | - | c, err := core.New(cfg, core.Channels{Client: r.client, Input: r.input, Output: r.output, Config: r.reload, Commands: r.commands, Scenes: r.scenes, Spawn: r.spawn, State: r.state, Terminal: terminal}) | 60 | + | c, err := core.New(cfg, core.Channels{Client: r.client, Input: r.input, Output: r.output, Config: r.reload, Commands: r.commands, Scenes: r.scenes, Spawn: r.spawn, State: r.state}, opts) |
| 55 | if err != nil { | 61 | if err != nil { | ||
| 56 | t.Fatal(err) | 62 | t.Fatal(err) | ||
| 57 | } | 63 | } | ||
| @@ -622,7 +628,7 @@ func TestFirstTerminalFollowsSetOutputs(t *testing.T) { | |||||
| 622 | output := make(chan ports.OutputEvent, 1) | 628 | output := make(chan ports.OutputEvent, 1) | ||
| 623 | scenes := make(chan []ports.Scene, 1) | 629 | scenes := make(chan []ports.Scene, 1) | ||
| 624 | state := make(chan ports.State, 1) | 630 | state := make(chan ports.State, 1) | ||
| 625 | - | c, err := core.New(cfg, core.Channels{Output: output, Commands: commands, Spawn: spawn, Scenes: scenes, State: state, Terminal: true}) | 631 | + | c, err := core.New(cfg, core.Channels{Output: output, Commands: commands, Spawn: spawn, Scenes: scenes, State: state}, core.Options{Terminal: true}) |
| 626 | if err != nil { | 632 | if err != nil { | ||
| 627 | t.Fatal(err) | 633 | t.Fatal(err) | ||
| 628 | } | 634 | } | ||
| @@ -753,6 +759,39 @@ func TestEmptyWorkspaceGetsTerminal(t *testing.T) { | |||||
| 753 | } | 759 | } | ||
| 754 | } | 760 | } | ||
| 755 | 761 | ||||
| 762 | + | // A terminal that exits is not respawned before termRetry has passed on | |||
| 763 | + | // the injected clock, and is once it has. | |||
| 764 | + | func TestTerminalRetryFollowsClock(t *testing.T) { | |||
| 765 | + | clk := newStepClock(t) | |||
| 766 | + | r := startRigWith(t, core.Options{Terminal: true, Clock: clk.clock}, func(c *ports.Config) { c.Terminal.AutoOpen = "all" }, left) | |||
| 767 | + | first := receive(t, r.spawn) | |||
| 768 | + | r.client <- ports.WindowMapped{ID: 1, Slot: token(t, first)} | |||
| 769 | + | receive(t, r.scenes) | |||
| 770 | + | r.client <- ports.WindowUnmapped{ID: 1} | |||
| 771 | + | receive(t, r.scenes) | |||
| 772 | + | if len(r.spawn) != 0 { | |||
| 773 | + | t.Fatal("respawned at once") | |||
| 774 | + | } | |||
| 775 | + | // churn empties the workspace again, so fillEmpty runs at the clock's time. | |||
| 776 | + | churn := func(id ports.WindowID) { | |||
| 777 | + | r.client <- ports.WindowMapped{ID: id} | |||
| 778 | + | receive(t, r.scenes) | |||
| 779 | + | r.client <- ports.WindowUnmapped{ID: id} | |||
| 780 | + | receive(t, r.scenes) | |||
| 781 | + | } | |||
| 782 | + | // termRetry is private to core: stay far below and far above it. | |||
| 783 | + | clk.advance(time.Second) | |||
| 784 | + | churn(2) | |||
| 785 | + | if len(r.spawn) != 0 { | |||
| 786 | + | t.Fatal("respawned before the retry interval") | |||
| 787 | + | } | |||
| 788 | + | clk.advance(time.Minute) | |||
| 789 | + | churn(3) | |||
| 790 | + | if req := receive(t, r.spawn); req.Argv[0] != "foot" { | |||
| 791 | + | t.Fatal(req) | |||
| 792 | + | } | |||
| 793 | + | } | |||
| 794 | + | ||||
| 756 | func TestLastColumnMovesToNeighborOutput(t *testing.T) { | 795 | func TestLastColumnMovesToNeighborOutput(t *testing.T) { | ||
| 757 | r := startRig(t, true, nil, left, right) | 796 | r := startRig(t, true, nil, left, right) | ||
| 758 | r.mapWindow(t, 1) | 797 | r.mapWindow(t, 1) | ||
M internal/core/security.go +7 -7
| @@ -13,10 +13,10 @@ func (c *Core) syncSecurity() bool { | |||
| 13 | 13 | if c.inputActive { | |
| 14 | 14 | return false | |
| 15 | 15 | } | |
| 16 | - | if c.ch.Security == nil { | |
| 16 | + | if c.opts.Security == nil { | |
| 17 | 17 | return false | |
| 18 | 18 | } | |
| 19 | - | state := c.ch.Security.Snapshot() | |
| 19 | + | state := c.opts.Security.Snapshot() | |
| 20 | 20 | if state == c.security { | |
| 21 | 21 | return false | |
| 22 | 22 | } | |
| @@ -44,11 +44,11 @@ func (c *Core) syncSecurity() bool { | |||
| 44 | 44 | // Check spawns without adopting a new epoch midway through processing an | |
| 45 | 45 | // event: commands emitted by that event must retain its original owner epoch. | |
| 46 | 46 | func (c *Core) inputEpochChanged() bool { | |
| 47 | - | return c.inputActive && c.ch.Security != nil && c.ch.Security.Snapshot() != c.security | |
| 47 | + | return c.inputActive && c.opts.Security != nil && c.opts.Security.Snapshot() != c.security | |
| 48 | 48 | } | |
| 49 | 49 | ||
| 50 | 50 | func (c *Core) protectionRequested() bool { | |
| 51 | - | return c.security.Protected || c.ch.Security != nil && c.ch.Security.Snapshot() != c.security | |
| 51 | + | return c.security.Protected || c.opts.Security != nil && c.opts.Security.Snapshot() != c.security | |
| 52 | 52 | } | |
| 53 | 53 | ||
| 54 | 54 | func (c *Core) securityCheckpoint(ctx context.Context) error { | |
| @@ -89,7 +89,7 @@ func blockedProtectedEvent(ev ports.ClientEvent) bool { | |||
| 89 | 89 | } | |
| 90 | 90 | ||
| 91 | 91 | func (c *Core) applyLockChanged(v ports.SessionLockChanged) bool { | |
| 92 | - | if c.ch.Security == nil || v.State != c.security || v.State != c.ch.Security.Snapshot() { | |
| 92 | + | if c.opts.Security == nil || v.State != c.security || v.State != c.opts.Security.Snapshot() { | |
| 93 | 93 | return false | |
| 94 | 94 | } | |
| 95 | 95 | if !c.security.Protected { | |
| @@ -211,7 +211,7 @@ func (c *Core) admitInput(ev ports.InputEvent) (ports.InputEvent, bool) { | |||
| 211 | 211 | return nil, false | |
| 212 | 212 | } | |
| 213 | 213 | ev = v.Event | |
| 214 | - | } else if c.ch.Security != nil || c.security != (ports.SecurityState{}) { | |
| 214 | + | } else if c.opts.Security != nil || c.security != (ports.SecurityState{}) { | |
| 215 | 215 | return nil, false | |
| 216 | 216 | } | |
| 217 | 217 | // The producer resets native keymap state and quarantines physically held | |
| @@ -219,7 +219,7 @@ func (c *Core) admitInput(ev ports.InputEvent) (ports.InputEvent, bool) { | |||
| 219 | 219 | // it must not infer modifier meaning from evdev codes or XKB bit positions. | |
| 220 | 220 | // No release without a press in this epoch (password keys must not reach | |
| 221 | 221 | // the desktop after unlock). Standalone legacy channel tests remain raw. | |
| 222 | - | if c.ch.Security != nil { | |
| 222 | + | if c.opts.Security != nil { | |
| 223 | 223 | if b, ok := ev.(ports.PointerButton); ok && !b.Pressed && !c.buttons[b.Button] { | |
| 224 | 224 | return nil, false | |
| 225 | 225 | } | |
| @@ -13,10 +13,10 @@ func (c *Core) syncSecurity() bool { | |||||
| 13 | if c.inputActive { | 13 | if c.inputActive { | ||
| 14 | return false | 14 | return false | ||
| 15 | } | 15 | } | ||
| 16 | - | if c.ch.Security == nil { | 16 | + | if c.opts.Security == nil { |
| 17 | return false | 17 | return false | ||
| 18 | } | 18 | } | ||
| 19 | - | state := c.ch.Security.Snapshot() | 19 | + | state := c.opts.Security.Snapshot() |
| 20 | if state == c.security { | 20 | if state == c.security { | ||
| 21 | return false | 21 | return false | ||
| 22 | } | 22 | } | ||
| @@ -44,11 +44,11 @@ func (c *Core) syncSecurity() bool { | |||||
| 44 | // Check spawns without adopting a new epoch midway through processing an | 44 | // Check spawns without adopting a new epoch midway through processing an | ||
| 45 | // event: commands emitted by that event must retain its original owner epoch. | 45 | // event: commands emitted by that event must retain its original owner epoch. | ||
| 46 | func (c *Core) inputEpochChanged() bool { | 46 | func (c *Core) inputEpochChanged() bool { | ||
| 47 | - | return c.inputActive && c.ch.Security != nil && c.ch.Security.Snapshot() != c.security | 47 | + | return c.inputActive && c.opts.Security != nil && c.opts.Security.Snapshot() != c.security |
| 48 | } | 48 | } | ||
| 49 | 49 | ||||
| 50 | func (c *Core) protectionRequested() bool { | 50 | func (c *Core) protectionRequested() bool { | ||
| 51 | - | return c.security.Protected || c.ch.Security != nil && c.ch.Security.Snapshot() != c.security | 51 | + | return c.security.Protected || c.opts.Security != nil && c.opts.Security.Snapshot() != c.security |
| 52 | } | 52 | } | ||
| 53 | 53 | ||||
| 54 | func (c *Core) securityCheckpoint(ctx context.Context) error { | 54 | func (c *Core) securityCheckpoint(ctx context.Context) error { | ||
| @@ -89,7 +89,7 @@ func blockedProtectedEvent(ev ports.ClientEvent) bool { | |||||
| 89 | } | 89 | } | ||
| 90 | 90 | ||||
| 91 | func (c *Core) applyLockChanged(v ports.SessionLockChanged) bool { | 91 | func (c *Core) applyLockChanged(v ports.SessionLockChanged) bool { | ||
| 92 | - | if c.ch.Security == nil || v.State != c.security || v.State != c.ch.Security.Snapshot() { | 92 | + | if c.opts.Security == nil || v.State != c.security || v.State != c.opts.Security.Snapshot() { |
| 93 | return false | 93 | return false | ||
| 94 | } | 94 | } | ||
| 95 | if !c.security.Protected { | 95 | if !c.security.Protected { | ||
| @@ -211,7 +211,7 @@ func (c *Core) admitInput(ev ports.InputEvent) (ports.InputEvent, bool) { | |||||
| 211 | return nil, false | 211 | return nil, false | ||
| 212 | } | 212 | } | ||
| 213 | ev = v.Event | 213 | ev = v.Event | ||
| 214 | - | } else if c.ch.Security != nil || c.security != (ports.SecurityState{}) { | 214 | + | } else if c.opts.Security != nil || c.security != (ports.SecurityState{}) { |
| 215 | return nil, false | 215 | return nil, false | ||
| 216 | } | 216 | } | ||
| 217 | // The producer resets native keymap state and quarantines physically held | 217 | // The producer resets native keymap state and quarantines physically held | ||
| @@ -219,7 +219,7 @@ func (c *Core) admitInput(ev ports.InputEvent) (ports.InputEvent, bool) { | |||||
| 219 | // it must not infer modifier meaning from evdev codes or XKB bit positions. | 219 | // it must not infer modifier meaning from evdev codes or XKB bit positions. | ||
| 220 | // No release without a press in this epoch (password keys must not reach | 220 | // No release without a press in this epoch (password keys must not reach | ||
| 221 | // the desktop after unlock). Standalone legacy channel tests remain raw. | 221 | // the desktop after unlock). Standalone legacy channel tests remain raw. | ||
| 222 | - | if c.ch.Security != nil { | 222 | + | if c.opts.Security != nil { |
| 223 | if b, ok := ev.(ports.PointerButton); ok && !b.Pressed && !c.buttons[b.Button] { | 223 | if b, ok := ev.(ports.PointerButton); ok && !b.Pressed && !c.buttons[b.Button] { | ||
| 224 | return nil, false | 224 | return nil, false | ||
| 225 | } | 225 | } | ||
M internal/core/security_internal_test.go +3 -3
| @@ -14,7 +14,7 @@ func TestProtectedDropsPrivateOffscreenCaptureAndDesktopEffects(t *testing.T) { | |||
| 14 | 14 | state := ports.SecurityState{Generation: 1, Protected: true} | |
| 15 | 15 | gate := portsmocks.NewMockSessionSecurity(t) | |
| 16 | 16 | gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return state }) | |
| 17 | - | c.ch.Security = gate | |
| 17 | + | c.opts.Security = gate | |
| 18 | 18 | c.captureOpen(ports.CaptureSessionOpen{ID: 7, Workspace: ws.ID}) | |
| 19 | 19 | c.captureExclusionBegin(ports.CaptureExclusionBegin{Session: 7}) | |
| 20 | 20 | c.capt.flashes = []capFlash{{target: capTarget{output: "A"}, until: c.now().Add(time.Hour)}} | |
| @@ -92,7 +92,7 @@ func TestLockFocusMovesToFocusedOutputSurfaceUntilPinned(t *testing.T) { | |||
| 92 | 92 | state := ports.SecurityState{Generation: 1, Protected: true} | |
| 93 | 93 | gate := portsmocks.NewMockSessionSecurity(t) | |
| 94 | 94 | gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return state }) | |
| 95 | - | c.ch.Security = gate | |
| 95 | + | c.opts.Security = gate | |
| 96 | 96 | c.syncSecurity() | |
| 97 | 97 | ctx := context.Background() | |
| 98 | 98 | mapped := func(s ...ports.LockSurfacePlacement) { | |
| @@ -127,7 +127,7 @@ func TestWiredZeroEpochRejectsRawInputAndCommandKeepsOwnerEpoch(t *testing.T) { | |||
| 127 | 127 | gate := portsmocks.NewMockSessionSecurity(t) | |
| 128 | 128 | next := ports.SecurityState{Generation: 1, Protected: true} | |
| 129 | 129 | gate.EXPECT().Snapshot().Return(next) | |
| 130 | - | c.ch.Security = gate | |
| 130 | + | c.opts.Security = gate | |
| 131 | 131 | if _, ok := c.admitInput(ports.KeyEvent{Pressed: true}); ok { | |
| 132 | 132 | t.Fatal("raw wired input admitted") | |
| 133 | 133 | } | |
| @@ -14,7 +14,7 @@ func TestProtectedDropsPrivateOffscreenCaptureAndDesktopEffects(t *testing.T) { | |||||
| 14 | state := ports.SecurityState{Generation: 1, Protected: true} | 14 | state := ports.SecurityState{Generation: 1, Protected: true} | ||
| 15 | gate := portsmocks.NewMockSessionSecurity(t) | 15 | gate := portsmocks.NewMockSessionSecurity(t) | ||
| 16 | gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return state }) | 16 | gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return state }) | ||
| 17 | - | c.ch.Security = gate | 17 | + | c.opts.Security = gate |
| 18 | c.captureOpen(ports.CaptureSessionOpen{ID: 7, Workspace: ws.ID}) | 18 | c.captureOpen(ports.CaptureSessionOpen{ID: 7, Workspace: ws.ID}) | ||
| 19 | c.captureExclusionBegin(ports.CaptureExclusionBegin{Session: 7}) | 19 | c.captureExclusionBegin(ports.CaptureExclusionBegin{Session: 7}) | ||
| 20 | c.capt.flashes = []capFlash{{target: capTarget{output: "A"}, until: c.now().Add(time.Hour)}} | 20 | c.capt.flashes = []capFlash{{target: capTarget{output: "A"}, until: c.now().Add(time.Hour)}} | ||
| @@ -92,7 +92,7 @@ func TestLockFocusMovesToFocusedOutputSurfaceUntilPinned(t *testing.T) { | |||||
| 92 | state := ports.SecurityState{Generation: 1, Protected: true} | 92 | state := ports.SecurityState{Generation: 1, Protected: true} | ||
| 93 | gate := portsmocks.NewMockSessionSecurity(t) | 93 | gate := portsmocks.NewMockSessionSecurity(t) | ||
| 94 | gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return state }) | 94 | gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return state }) | ||
| 95 | - | c.ch.Security = gate | 95 | + | c.opts.Security = gate |
| 96 | c.syncSecurity() | 96 | c.syncSecurity() | ||
| 97 | ctx := context.Background() | 97 | ctx := context.Background() | ||
| 98 | mapped := func(s ...ports.LockSurfacePlacement) { | 98 | mapped := func(s ...ports.LockSurfacePlacement) { | ||
| @@ -127,7 +127,7 @@ func TestWiredZeroEpochRejectsRawInputAndCommandKeepsOwnerEpoch(t *testing.T) { | |||||
| 127 | gate := portsmocks.NewMockSessionSecurity(t) | 127 | gate := portsmocks.NewMockSessionSecurity(t) | ||
| 128 | next := ports.SecurityState{Generation: 1, Protected: true} | 128 | next := ports.SecurityState{Generation: 1, Protected: true} | ||
| 129 | gate.EXPECT().Snapshot().Return(next) | 129 | gate.EXPECT().Snapshot().Return(next) | ||
| 130 | - | c.ch.Security = gate | 130 | + | c.opts.Security = gate |
| 131 | if _, ok := c.admitInput(ports.KeyEvent{Pressed: true}); ok { | 131 | if _, ok := c.admitInput(ports.KeyEvent{Pressed: true}); ok { | ||
| 132 | t.Fatal("raw wired input admitted") | 132 | t.Fatal("raw wired input admitted") | ||
| 133 | } | 133 | } | ||
M internal/core/security_review_test.go +6 -6
| @@ -38,7 +38,7 @@ func TestInputOutputSwitchBackpressureKeepsAdmittedEpoch(t *testing.T) { | |||
| 38 | 38 | } | |
| 39 | 39 | return v | |
| 40 | 40 | }) | |
| 41 | - | c.ch.Security = gate | |
| 41 | + | c.opts.Security = gate | |
| 42 | 42 | c.sentOutputs = ports.SetOutputs{Outputs: c.layout(), Focused: c.cur().name(), Off: c.offOutputs()} | |
| 43 | 43 | commands := make(chan ports.ClientCommand) | |
| 44 | 44 | c.ch.Commands = commands | |
| @@ -68,7 +68,7 @@ func TestExactEpochAdmissionPreservesProducerModifierState(t *testing.T) { | |||
| 68 | 68 | state := ports.SecurityState{Generation: 2} | |
| 69 | 69 | gate := portsmocks.NewMockSessionSecurity(t) | |
| 70 | 70 | gate.EXPECT().Snapshot().Return(state) | |
| 71 | - | c.ch.Security = gate | |
| 71 | + | c.opts.Security = gate | |
| 72 | 72 | c.syncSecurity() | |
| 73 | 73 | key := ports.KeyEvent{Keycode: 58, Keysym: "Control_L", Pressed: true, Mods: ports.ModCtrl, State: ports.ModState{Depressed: 1 << 19, Latched: 1 << 21, Locked: 1 << 23, Group: 2}} | |
| 74 | 74 | ev, ok := c.admitInput(ports.SecurityInput{State: state, Event: key}) | |
| @@ -97,7 +97,7 @@ func TestSlotsPendingBackpressureSuppressesSpawnAndDefersSlots(t *testing.T) { | |||
| 97 | 97 | } | |
| 98 | 98 | return v | |
| 99 | 99 | }) | |
| 100 | - | c.ch.Security = gate | |
| 100 | + | c.opts.Security = gate | |
| 101 | 101 | commands := make(chan ports.ClientCommand) | |
| 102 | 102 | c.ch.Commands = commands | |
| 103 | 103 | spawn := make(chan ports.SpawnRequest, 2) | |
| @@ -132,7 +132,7 @@ func TestTerminalPendingBackpressureSuppressesSpawnAndRestoresRetry(t *testing.T | |||
| 132 | 132 | c, _, _ := hiddenCaptureCommands(t) | |
| 133 | 133 | c.cur().mon.RemoveWindow(1) | |
| 134 | 134 | c.cur().mon.RemoveWindow(2) | |
| 135 | - | c.ch.Terminal = true | |
| 135 | + | c.opts.Terminal = true | |
| 136 | 136 | c.cfg.Terminal.AutoOpen = "always" | |
| 137 | 137 | c.cfg.Terminal.Command = []string{"terminal"} | |
| 138 | 138 | var state atomic.Value | |
| @@ -147,7 +147,7 @@ func TestTerminalPendingBackpressureSuppressesSpawnAndRestoresRetry(t *testing.T | |||
| 147 | 147 | } | |
| 148 | 148 | return v | |
| 149 | 149 | }) | |
| 150 | - | c.ch.Security = gate | |
| 150 | + | c.opts.Security = gate | |
| 151 | 151 | commands := make(chan ports.ClientCommand) | |
| 152 | 152 | c.ch.Commands = commands | |
| 153 | 153 | spawn := make(chan ports.SpawnRequest, 1) | |
| @@ -173,7 +173,7 @@ func TestStartupWorkspaceSnapshotHasNoOutputBeforeOutputAdded(t *testing.T) { | |||
| 173 | 173 | cfg.Keyboard.CmdKey = "super" | |
| 174 | 174 | cfg.Layout.MaxColumns = 2 | |
| 175 | 175 | workspaces := make(chan ports.Workspaces, 1) | |
| 176 | - | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Workspaces: workspaces}) | |
| 176 | + | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Workspaces: workspaces}, Options{}) | |
| 177 | 177 | if err != nil { | |
| 178 | 178 | t.Fatal(err) | |
| 179 | 179 | } | |
| @@ -38,7 +38,7 @@ func TestInputOutputSwitchBackpressureKeepsAdmittedEpoch(t *testing.T) { | |||||
| 38 | } | 38 | } | ||
| 39 | return v | 39 | return v | ||
| 40 | }) | 40 | }) | ||
| 41 | - | c.ch.Security = gate | 41 | + | c.opts.Security = gate |
| 42 | c.sentOutputs = ports.SetOutputs{Outputs: c.layout(), Focused: c.cur().name(), Off: c.offOutputs()} | 42 | c.sentOutputs = ports.SetOutputs{Outputs: c.layout(), Focused: c.cur().name(), Off: c.offOutputs()} | ||
| 43 | commands := make(chan ports.ClientCommand) | 43 | commands := make(chan ports.ClientCommand) | ||
| 44 | c.ch.Commands = commands | 44 | c.ch.Commands = commands | ||
| @@ -68,7 +68,7 @@ func TestExactEpochAdmissionPreservesProducerModifierState(t *testing.T) { | |||||
| 68 | state := ports.SecurityState{Generation: 2} | 68 | state := ports.SecurityState{Generation: 2} | ||
| 69 | gate := portsmocks.NewMockSessionSecurity(t) | 69 | gate := portsmocks.NewMockSessionSecurity(t) | ||
| 70 | gate.EXPECT().Snapshot().Return(state) | 70 | gate.EXPECT().Snapshot().Return(state) | ||
| 71 | - | c.ch.Security = gate | 71 | + | c.opts.Security = gate |
| 72 | c.syncSecurity() | 72 | c.syncSecurity() | ||
| 73 | key := ports.KeyEvent{Keycode: 58, Keysym: "Control_L", Pressed: true, Mods: ports.ModCtrl, State: ports.ModState{Depressed: 1 << 19, Latched: 1 << 21, Locked: 1 << 23, Group: 2}} | 73 | key := ports.KeyEvent{Keycode: 58, Keysym: "Control_L", Pressed: true, Mods: ports.ModCtrl, State: ports.ModState{Depressed: 1 << 19, Latched: 1 << 21, Locked: 1 << 23, Group: 2}} | ||
| 74 | ev, ok := c.admitInput(ports.SecurityInput{State: state, Event: key}) | 74 | ev, ok := c.admitInput(ports.SecurityInput{State: state, Event: key}) | ||
| @@ -97,7 +97,7 @@ func TestSlotsPendingBackpressureSuppressesSpawnAndDefersSlots(t *testing.T) { | |||||
| 97 | } | 97 | } | ||
| 98 | return v | 98 | return v | ||
| 99 | }) | 99 | }) | ||
| 100 | - | c.ch.Security = gate | 100 | + | c.opts.Security = gate |
| 101 | commands := make(chan ports.ClientCommand) | 101 | commands := make(chan ports.ClientCommand) | ||
| 102 | c.ch.Commands = commands | 102 | c.ch.Commands = commands | ||
| 103 | spawn := make(chan ports.SpawnRequest, 2) | 103 | spawn := make(chan ports.SpawnRequest, 2) | ||
| @@ -132,7 +132,7 @@ func TestTerminalPendingBackpressureSuppressesSpawnAndRestoresRetry(t *testing.T | |||||
| 132 | c, _, _ := hiddenCaptureCommands(t) | 132 | c, _, _ := hiddenCaptureCommands(t) | ||
| 133 | c.cur().mon.RemoveWindow(1) | 133 | c.cur().mon.RemoveWindow(1) | ||
| 134 | c.cur().mon.RemoveWindow(2) | 134 | c.cur().mon.RemoveWindow(2) | ||
| 135 | - | c.ch.Terminal = true | 135 | + | c.opts.Terminal = true |
| 136 | c.cfg.Terminal.AutoOpen = "always" | 136 | c.cfg.Terminal.AutoOpen = "always" | ||
| 137 | c.cfg.Terminal.Command = []string{"terminal"} | 137 | c.cfg.Terminal.Command = []string{"terminal"} | ||
| 138 | var state atomic.Value | 138 | var state atomic.Value | ||
| @@ -147,7 +147,7 @@ func TestTerminalPendingBackpressureSuppressesSpawnAndRestoresRetry(t *testing.T | |||||
| 147 | } | 147 | } | ||
| 148 | return v | 148 | return v | ||
| 149 | }) | 149 | }) | ||
| 150 | - | c.ch.Security = gate | 150 | + | c.opts.Security = gate |
| 151 | commands := make(chan ports.ClientCommand) | 151 | commands := make(chan ports.ClientCommand) | ||
| 152 | c.ch.Commands = commands | 152 | c.ch.Commands = commands | ||
| 153 | spawn := make(chan ports.SpawnRequest, 1) | 153 | spawn := make(chan ports.SpawnRequest, 1) | ||
| @@ -173,7 +173,7 @@ func TestStartupWorkspaceSnapshotHasNoOutputBeforeOutputAdded(t *testing.T) { | |||||
| 173 | cfg.Keyboard.CmdKey = "super" | 173 | cfg.Keyboard.CmdKey = "super" | ||
| 174 | cfg.Layout.MaxColumns = 2 | 174 | cfg.Layout.MaxColumns = 2 | ||
| 175 | workspaces := make(chan ports.Workspaces, 1) | 175 | workspaces := make(chan ports.Workspaces, 1) | ||
| 176 | - | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Workspaces: workspaces}) | 176 | + | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Workspaces: workspaces}, Options{}) |
| 177 | if err != nil { | 177 | if err != nil { | ||
| 178 | t.Fatal(err) | 178 | t.Fatal(err) | ||
| 179 | } | 179 | } | ||
M internal/core/security_test.go +1 -1
| @@ -28,7 +28,7 @@ func TestSecurityOwnerAdversarialQueues(t *testing.T) { | |||
| 28 | 28 | cfg.Terminal.Command = []string{"terminal"} | |
| 29 | 29 | cfg.Terminal.AutoOpen = "off" | |
| 30 | 30 | cfg.Binds = map[string]string{"Super+q": "quit"} | |
| 31 | - | c, err := core.New(cfg, core.Channels{Security: gate, Client: client, Input: input, Output: output, Config: reload, Commands: commands, Scenes: scenes, Spawn: spawn, Constraints: constraints, Terminal: true}) | |
| 31 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Config: reload, Commands: commands, Scenes: scenes, Spawn: spawn, Constraints: constraints}, core.Options{Security: gate, Terminal: true}) | |
| 32 | 32 | if err != nil { | |
| 33 | 33 | t.Fatal(err) | |
| 34 | 34 | } | |
| @@ -28,7 +28,7 @@ func TestSecurityOwnerAdversarialQueues(t *testing.T) { | |||||
| 28 | cfg.Terminal.Command = []string{"terminal"} | 28 | cfg.Terminal.Command = []string{"terminal"} | ||
| 29 | cfg.Terminal.AutoOpen = "off" | 29 | cfg.Terminal.AutoOpen = "off" | ||
| 30 | cfg.Binds = map[string]string{"Super+q": "quit"} | 30 | cfg.Binds = map[string]string{"Super+q": "quit"} | ||
| 31 | - | c, err := core.New(cfg, core.Channels{Security: gate, Client: client, Input: input, Output: output, Config: reload, Commands: commands, Scenes: scenes, Spawn: spawn, Constraints: constraints, Terminal: true}) | 31 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Config: reload, Commands: commands, Scenes: scenes, Spawn: spawn, Constraints: constraints}, core.Options{Security: gate, Terminal: true}) |
| 32 | if err != nil { | 32 | if err != nil { | ||
| 33 | t.Fatal(err) | 33 | t.Fatal(err) | ||
| 34 | } | 34 | } | ||
M internal/core/slots_test.go +4 -3
| @@ -62,16 +62,17 @@ func startSlotsMode(t *testing.T, animated *bool, numbered ...bool) *slotRig { | |||
| 62 | 62 | output := make(chan ports.OutputEvent, 1) | |
| 63 | 63 | commands := make(chan ports.ClientCommand, 1024) | |
| 64 | 64 | ch := core.Channels{Client: r.client, Input: r.input, Output: output, Config: r.reload, Commands: commands, Spawn: r.spawn, Scenes: r.scenes, Workspaces: r.workspaces, ConfigErrors: r.errs} | |
| 65 | + | opts := core.Options{} | |
| 65 | 66 | if animated != nil && *animated { | |
| 66 | 67 | r.clk, r.frames = newStepClock(t), make(chan ports.OutputFrame) | |
| 67 | - | ch.Clock, ch.Frames = r.clk.clock, r.frames | |
| 68 | + | opts.Clock, ch.Frames = r.clk.clock, r.frames | |
| 68 | 69 | t.Cleanup(func() { | |
| 69 | 70 | if r.lands == 0 { | |
| 70 | 71 | t.Error("no spring ever ran: the animations-on variant checked nothing") | |
| 71 | 72 | } | |
| 72 | 73 | }) | |
| 73 | 74 | } | |
| 74 | - | c, err := core.New(cfg, ch) | |
| 75 | + | c, err := core.New(cfg, ch, opts) | |
| 75 | 76 | if err != nil { | |
| 76 | 77 | t.Fatal(err) | |
| 77 | 78 | } | |
| @@ -423,7 +424,7 @@ func TestStartupCommands(t *testing.T) { | |||
| 423 | 424 | cfg := config.Defaults() | |
| 424 | 425 | cfg.Startup = [][]string{{"wl-paste", "--watch", "cliphist", "store"}, {"waybar"}} | |
| 425 | 426 | spawn := make(chan ports.SpawnRequest, 4) | |
| 426 | - | c, err := core.New(cfg, core.Channels{Client: make(chan ports.ClientEvent), Input: make(chan ports.InputEvent), Output: make(chan ports.OutputEvent), Config: make(chan ports.ConfigChanged), Commands: make(chan ports.ClientCommand, 16), Spawn: spawn, Scenes: make(chan []ports.Scene, 1), ConfigErrors: make(chan error, 1)}) | |
| 427 | + | c, err := core.New(cfg, core.Channels{Client: make(chan ports.ClientEvent), Input: make(chan ports.InputEvent), Output: make(chan ports.OutputEvent), Config: make(chan ports.ConfigChanged), Commands: make(chan ports.ClientCommand, 16), Spawn: spawn, Scenes: make(chan []ports.Scene, 1), ConfigErrors: make(chan error, 1)}, core.Options{}) | |
| 427 | 428 | if err != nil { | |
| 428 | 429 | t.Fatal(err) | |
| 429 | 430 | } | |
| @@ -62,16 +62,17 @@ func startSlotsMode(t *testing.T, animated *bool, numbered ...bool) *slotRig { | |||||
| 62 | output := make(chan ports.OutputEvent, 1) | 62 | output := make(chan ports.OutputEvent, 1) | ||
| 63 | commands := make(chan ports.ClientCommand, 1024) | 63 | commands := make(chan ports.ClientCommand, 1024) | ||
| 64 | ch := core.Channels{Client: r.client, Input: r.input, Output: output, Config: r.reload, Commands: commands, Spawn: r.spawn, Scenes: r.scenes, Workspaces: r.workspaces, ConfigErrors: r.errs} | 64 | ch := core.Channels{Client: r.client, Input: r.input, Output: output, Config: r.reload, Commands: commands, Spawn: r.spawn, Scenes: r.scenes, Workspaces: r.workspaces, ConfigErrors: r.errs} | ||
| 65 | + | opts := core.Options{} | |||
| 65 | if animated != nil && *animated { | 66 | if animated != nil && *animated { | ||
| 66 | r.clk, r.frames = newStepClock(t), make(chan ports.OutputFrame) | 67 | r.clk, r.frames = newStepClock(t), make(chan ports.OutputFrame) | ||
| 67 | - | ch.Clock, ch.Frames = r.clk.clock, r.frames | 68 | + | opts.Clock, ch.Frames = r.clk.clock, r.frames |
| 68 | t.Cleanup(func() { | 69 | t.Cleanup(func() { | ||
| 69 | if r.lands == 0 { | 70 | if r.lands == 0 { | ||
| 70 | t.Error("no spring ever ran: the animations-on variant checked nothing") | 71 | t.Error("no spring ever ran: the animations-on variant checked nothing") | ||
| 71 | } | 72 | } | ||
| 72 | }) | 73 | }) | ||
| 73 | } | 74 | } | ||
| 74 | - | c, err := core.New(cfg, ch) | 75 | + | c, err := core.New(cfg, ch, opts) |
| 75 | if err != nil { | 76 | if err != nil { | ||
| 76 | t.Fatal(err) | 77 | t.Fatal(err) | ||
| 77 | } | 78 | } | ||
| @@ -423,7 +424,7 @@ func TestStartupCommands(t *testing.T) { | |||||
| 423 | cfg := config.Defaults() | 424 | cfg := config.Defaults() | ||
| 424 | cfg.Startup = [][]string{{"wl-paste", "--watch", "cliphist", "store"}, {"waybar"}} | 425 | cfg.Startup = [][]string{{"wl-paste", "--watch", "cliphist", "store"}, {"waybar"}} | ||
| 425 | spawn := make(chan ports.SpawnRequest, 4) | 426 | spawn := make(chan ports.SpawnRequest, 4) | ||
| 426 | - | c, err := core.New(cfg, core.Channels{Client: make(chan ports.ClientEvent), Input: make(chan ports.InputEvent), Output: make(chan ports.OutputEvent), Config: make(chan ports.ConfigChanged), Commands: make(chan ports.ClientCommand, 16), Spawn: spawn, Scenes: make(chan []ports.Scene, 1), ConfigErrors: make(chan error, 1)}) | 427 | + | c, err := core.New(cfg, core.Channels{Client: make(chan ports.ClientEvent), Input: make(chan ports.InputEvent), Output: make(chan ports.OutputEvent), Config: make(chan ports.ConfigChanged), Commands: make(chan ports.ClientCommand, 16), Spawn: spawn, Scenes: make(chan []ports.Scene, 1), ConfigErrors: make(chan error, 1)}, core.Options{}) |
| 427 | if err != nil { | 428 | if err != nil { | ||
| 428 | t.Fatal(err) | 429 | t.Fatal(err) | ||
| 429 | } | 430 | } | ||
M internal/core/spawn_placement_test.go +2 -2
| @@ -17,7 +17,7 @@ func TestSlotClaimWithoutWorkspace(t *testing.T) { | |||
| 17 | 17 | cfg := ports.Config{} | |
| 18 | 18 | cfg.Keyboard.CmdKey = "alt" | |
| 19 | 19 | cfg.Layout.MaxColumns = 1 | |
| 20 | - | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1)}) | |
| 20 | + | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1)}, Options{}) | |
| 21 | 21 | if err != nil { | |
| 22 | 22 | t.Fatal(err) | |
| 23 | 23 | } | |
| @@ -47,7 +47,7 @@ func TestExpiredTerminalTokenMapsNormally(t *testing.T) { | |||
| 47 | 47 | cfg := ports.Config{} | |
| 48 | 48 | cfg.Keyboard.CmdKey = "alt" | |
| 49 | 49 | cfg.Layout.MaxColumns = 1 | |
| 50 | - | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1)}) | |
| 50 | + | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1)}, Options{}) | |
| 51 | 51 | if err != nil { | |
| 52 | 52 | t.Fatal(err) | |
| 53 | 53 | } | |
| @@ -17,7 +17,7 @@ func TestSlotClaimWithoutWorkspace(t *testing.T) { | |||||
| 17 | cfg := ports.Config{} | 17 | cfg := ports.Config{} | ||
| 18 | cfg.Keyboard.CmdKey = "alt" | 18 | cfg.Keyboard.CmdKey = "alt" | ||
| 19 | cfg.Layout.MaxColumns = 1 | 19 | cfg.Layout.MaxColumns = 1 | ||
| 20 | - | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1)}) | 20 | + | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1)}, Options{}) |
| 21 | if err != nil { | 21 | if err != nil { | ||
| 22 | t.Fatal(err) | 22 | t.Fatal(err) | ||
| 23 | } | 23 | } | ||
| @@ -47,7 +47,7 @@ func TestExpiredTerminalTokenMapsNormally(t *testing.T) { | |||||
| 47 | cfg := ports.Config{} | 47 | cfg := ports.Config{} | ||
| 48 | cfg.Keyboard.CmdKey = "alt" | 48 | cfg.Keyboard.CmdKey = "alt" | ||
| 49 | cfg.Layout.MaxColumns = 1 | 49 | cfg.Layout.MaxColumns = 1 | ||
| 50 | - | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1)}) | 50 | + | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1)}, Options{}) |
| 51 | if err != nil { | 51 | if err != nil { | ||
| 52 | t.Fatal(err) | 52 | t.Fatal(err) | ||
| 53 | } | 53 | } | ||
M internal/core/spawn_security_test.go +5 -5
| @@ -20,7 +20,7 @@ func TestFullSpawnQueueSlotsTerminalsExplicitRemainTransitionSafe(t *testing.T) | |||
| 20 | 20 | epoch.Store(ports.SecurityState{Generation: 2}) | |
| 21 | 21 | gate := portsmocks.NewMockSessionSecurity(t) | |
| 22 | 22 | gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return epoch.Load().(ports.SecurityState) }) | |
| 23 | - | c.ch.Security = gate | |
| 23 | + | c.opts.Security = gate | |
| 24 | 24 | c.syncSecurity() | |
| 25 | 25 | spawn := make(chan ports.SpawnRequest, 1) | |
| 26 | 26 | c.ch.Spawn = spawn | |
| @@ -29,7 +29,7 @@ func TestFullSpawnQueueSlotsTerminalsExplicitRemainTransitionSafe(t *testing.T) | |||
| 29 | 29 | key := slotKey{workspace: "dev", index: 1} | |
| 30 | 30 | c.cfg.Terminal.Command = []string{"terminal"} | |
| 31 | 31 | c.cfg.Terminal.AutoOpen = "always" | |
| 32 | - | c.ch.Terminal = kind == "terminal" | |
| 32 | + | c.opts.Terminal = kind == "terminal" | |
| 33 | 33 | if kind == "slot" { | |
| 34 | 34 | c.slots[key] = &slotState{argv: []string{"slot"}} | |
| 35 | 35 | c.toSpawn = []slotKey{key} | |
| @@ -131,7 +131,7 @@ func TestRunStartupPendingDisabledWhileLockedResumesOnUnlockEvent(t *testing.T) | |||
| 131 | 131 | epoch.Store(locked) | |
| 132 | 132 | gate := portsmocks.NewMockSessionSecurity(t) | |
| 133 | 133 | gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return epoch.Load().(ports.SecurityState) }) | |
| 134 | - | c.ch.Security = gate | |
| 134 | + | c.opts.Security = gate | |
| 135 | 135 | spawn := make(chan ports.SpawnRequest, 1) | |
| 136 | 136 | c.ch.Spawn = spawn | |
| 137 | 137 | client := make(chan ports.ClientEvent, 1) | |
| @@ -175,7 +175,7 @@ func TestStartupSuccessfulHandoffConsumesDespiteImmediateTransition(t *testing.T | |||
| 175 | 175 | } | |
| 176 | 176 | return epoch.Load().(ports.SecurityState) | |
| 177 | 177 | }) | |
| 178 | - | c.ch.Security = gate | |
| 178 | + | c.opts.Security = gate | |
| 179 | 179 | c.spawnStartup(context.Background()) | |
| 180 | 180 | if len(c.startup) != 0 { | |
| 181 | 181 | t.Fatal("successful handoff kept pending startup") | |
| @@ -202,7 +202,7 @@ func TestRunSelectableStartupConsumedOnceAcrossReceiverTransition(t *testing.T) | |||
| 202 | 202 | epoch.Store(ports.SecurityState{}) | |
| 203 | 203 | gate := portsmocks.NewMockSessionSecurity(t) | |
| 204 | 204 | gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return epoch.Load().(ports.SecurityState) }) | |
| 205 | - | c.ch.Security = gate | |
| 205 | + | c.opts.Security = gate | |
| 206 | 206 | spawn := make(chan ports.SpawnRequest) | |
| 207 | 207 | c.ch.Spawn = spawn | |
| 208 | 208 | client := make(chan ports.ClientEvent, 1) | |
| @@ -20,7 +20,7 @@ func TestFullSpawnQueueSlotsTerminalsExplicitRemainTransitionSafe(t *testing.T) | |||||
| 20 | epoch.Store(ports.SecurityState{Generation: 2}) | 20 | epoch.Store(ports.SecurityState{Generation: 2}) | ||
| 21 | gate := portsmocks.NewMockSessionSecurity(t) | 21 | gate := portsmocks.NewMockSessionSecurity(t) | ||
| 22 | gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return epoch.Load().(ports.SecurityState) }) | 22 | gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return epoch.Load().(ports.SecurityState) }) | ||
| 23 | - | c.ch.Security = gate | 23 | + | c.opts.Security = gate |
| 24 | c.syncSecurity() | 24 | c.syncSecurity() | ||
| 25 | spawn := make(chan ports.SpawnRequest, 1) | 25 | spawn := make(chan ports.SpawnRequest, 1) | ||
| 26 | c.ch.Spawn = spawn | 26 | c.ch.Spawn = spawn | ||
| @@ -29,7 +29,7 @@ func TestFullSpawnQueueSlotsTerminalsExplicitRemainTransitionSafe(t *testing.T) | |||||
| 29 | key := slotKey{workspace: "dev", index: 1} | 29 | key := slotKey{workspace: "dev", index: 1} | ||
| 30 | c.cfg.Terminal.Command = []string{"terminal"} | 30 | c.cfg.Terminal.Command = []string{"terminal"} | ||
| 31 | c.cfg.Terminal.AutoOpen = "always" | 31 | c.cfg.Terminal.AutoOpen = "always" | ||
| 32 | - | c.ch.Terminal = kind == "terminal" | 32 | + | c.opts.Terminal = kind == "terminal" |
| 33 | if kind == "slot" { | 33 | if kind == "slot" { | ||
| 34 | c.slots[key] = &slotState{argv: []string{"slot"}} | 34 | c.slots[key] = &slotState{argv: []string{"slot"}} | ||
| 35 | c.toSpawn = []slotKey{key} | 35 | c.toSpawn = []slotKey{key} | ||
| @@ -131,7 +131,7 @@ func TestRunStartupPendingDisabledWhileLockedResumesOnUnlockEvent(t *testing.T) | |||||
| 131 | epoch.Store(locked) | 131 | epoch.Store(locked) | ||
| 132 | gate := portsmocks.NewMockSessionSecurity(t) | 132 | gate := portsmocks.NewMockSessionSecurity(t) | ||
| 133 | gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return epoch.Load().(ports.SecurityState) }) | 133 | gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return epoch.Load().(ports.SecurityState) }) | ||
| 134 | - | c.ch.Security = gate | 134 | + | c.opts.Security = gate |
| 135 | spawn := make(chan ports.SpawnRequest, 1) | 135 | spawn := make(chan ports.SpawnRequest, 1) | ||
| 136 | c.ch.Spawn = spawn | 136 | c.ch.Spawn = spawn | ||
| 137 | client := make(chan ports.ClientEvent, 1) | 137 | client := make(chan ports.ClientEvent, 1) | ||
| @@ -175,7 +175,7 @@ func TestStartupSuccessfulHandoffConsumesDespiteImmediateTransition(t *testing.T | |||||
| 175 | } | 175 | } | ||
| 176 | return epoch.Load().(ports.SecurityState) | 176 | return epoch.Load().(ports.SecurityState) | ||
| 177 | }) | 177 | }) | ||
| 178 | - | c.ch.Security = gate | 178 | + | c.opts.Security = gate |
| 179 | c.spawnStartup(context.Background()) | 179 | c.spawnStartup(context.Background()) | ||
| 180 | if len(c.startup) != 0 { | 180 | if len(c.startup) != 0 { | ||
| 181 | t.Fatal("successful handoff kept pending startup") | 181 | t.Fatal("successful handoff kept pending startup") | ||
| @@ -202,7 +202,7 @@ func TestRunSelectableStartupConsumedOnceAcrossReceiverTransition(t *testing.T) | |||||
| 202 | epoch.Store(ports.SecurityState{}) | 202 | epoch.Store(ports.SecurityState{}) | ||
| 203 | gate := portsmocks.NewMockSessionSecurity(t) | 203 | gate := portsmocks.NewMockSessionSecurity(t) | ||
| 204 | gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return epoch.Load().(ports.SecurityState) }) | 204 | gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return epoch.Load().(ports.SecurityState) }) | ||
| 205 | - | c.ch.Security = gate | 205 | + | c.opts.Security = gate |
| 206 | spawn := make(chan ports.SpawnRequest) | 206 | spawn := make(chan ports.SpawnRequest) | ||
| 207 | c.ch.Spawn = spawn | 207 | c.ch.Spawn = spawn | ||
| 208 | client := make(chan ports.ClientEvent, 1) | 208 | client := make(chan ports.ClientEvent, 1) | ||
M internal/core/stash_cover_test.go +1 -1
| @@ -15,7 +15,7 @@ func stashCoverCore(t *testing.T, overflow Overflow, float bool) (*Core, *Worksp | |||
| 15 | 15 | cfg.Keyboard.CmdKey = "super" | |
| 16 | 16 | cfg.Layout.Overflow = string(overflow) | |
| 17 | 17 | cfg.Layout.MaxColumns = 2 | |
| 18 | - | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1)}) | |
| 18 | + | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1)}, Options{}) | |
| 19 | 19 | if err != nil { | |
| 20 | 20 | t.Fatal(err) | |
| 21 | 21 | } | |
| @@ -15,7 +15,7 @@ func stashCoverCore(t *testing.T, overflow Overflow, float bool) (*Core, *Worksp | |||||
| 15 | cfg.Keyboard.CmdKey = "super" | 15 | cfg.Keyboard.CmdKey = "super" | ||
| 16 | cfg.Layout.Overflow = string(overflow) | 16 | cfg.Layout.Overflow = string(overflow) | ||
| 17 | cfg.Layout.MaxColumns = 2 | 17 | cfg.Layout.MaxColumns = 2 | ||
| 18 | - | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1)}) | 18 | + | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1)}, Options{}) |
| 19 | if err != nil { | 19 | if err != nil { | ||
| 20 | t.Fatal(err) | 20 | t.Fatal(err) | ||
| 21 | } | 21 | } | ||
M internal/core/stash_swipe_internal_test.go +1 -1
| @@ -260,7 +260,7 @@ func TestStashSwipeSecurityLockSettlesTheStash(t *testing.T) { | |||
| 260 | 260 | } | |
| 261 | 261 | gate := portsmocks.NewMockSessionSecurity(t) | |
| 262 | 262 | gate.EXPECT().Snapshot().Return(ports.SecurityState{Generation: 1, Protected: true}) | |
| 263 | - | s.c.ch.Security = gate | |
| 263 | + | s.c.opts.Security = gate | |
| 264 | 264 | if !s.c.syncSecurity() { | |
| 265 | 265 | t.Fatal("the lock was not taken") | |
| 266 | 266 | } | |
| @@ -260,7 +260,7 @@ func TestStashSwipeSecurityLockSettlesTheStash(t *testing.T) { | |||||
| 260 | } | 260 | } | ||
| 261 | gate := portsmocks.NewMockSessionSecurity(t) | 261 | gate := portsmocks.NewMockSessionSecurity(t) | ||
| 262 | gate.EXPECT().Snapshot().Return(ports.SecurityState{Generation: 1, Protected: true}) | 262 | gate.EXPECT().Snapshot().Return(ports.SecurityState{Generation: 1, Protected: true}) | ||
| 263 | - | s.c.ch.Security = gate | 263 | + | s.c.opts.Security = gate |
| 264 | if !s.c.syncSecurity() { | 264 | if !s.c.syncSecurity() { | ||
| 265 | t.Fatal("the lock was not taken") | 265 | t.Fatal("the lock was not taken") | ||
| 266 | } | 266 | } | ||
M internal/core/stash_test.go +1 -1
| @@ -289,7 +289,7 @@ func TestFullscreenStashEdgeGoesToMonitor(t *testing.T) { | |||
| 289 | 289 | cfg.Keyboard.CmdKey = "super" | |
| 290 | 290 | cfg.Layout.MaxColumns = 2 | |
| 291 | 291 | cfg.Layout.Overflow = string(OverflowFixed) | |
| 292 | - | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1)}) | |
| 292 | + | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1)}, Options{}) | |
| 293 | 293 | if err != nil { | |
| 294 | 294 | t.Fatal(err) | |
| 295 | 295 | } | |
| @@ -289,7 +289,7 @@ func TestFullscreenStashEdgeGoesToMonitor(t *testing.T) { | |||||
| 289 | cfg.Keyboard.CmdKey = "super" | 289 | cfg.Keyboard.CmdKey = "super" | ||
| 290 | cfg.Layout.MaxColumns = 2 | 290 | cfg.Layout.MaxColumns = 2 | ||
| 291 | cfg.Layout.Overflow = string(OverflowFixed) | 291 | cfg.Layout.Overflow = string(OverflowFixed) | ||
| 292 | - | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1)}) | 292 | + | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1)}, Options{}) |
| 293 | if err != nil { | 293 | if err != nil { | ||
| 294 | t.Fatal(err) | 294 | t.Fatal(err) | ||
| 295 | } | 295 | } | ||
M internal/core/state_cell_test.go +1 -1
| @@ -12,7 +12,7 @@ func TestStateColumnRow(t *testing.T) { | |||
| 12 | 12 | cfg := ports.Config{} | |
| 13 | 13 | cfg.Keyboard.CmdKey = "super" | |
| 14 | 14 | cfg.Layout.MaxColumns = 1 | |
| 15 | - | c, err := New(cfg, Channels{Commands: make(chan ports.ClientCommand, 16), Scenes: make(chan []ports.Scene, 1)}) | |
| 15 | + | c, err := New(cfg, Channels{Commands: make(chan ports.ClientCommand, 16), Scenes: make(chan []ports.Scene, 1)}, Options{}) | |
| 16 | 16 | if err != nil { | |
| 17 | 17 | t.Fatal(err) | |
| 18 | 18 | } | |
| @@ -12,7 +12,7 @@ func TestStateColumnRow(t *testing.T) { | |||||
| 12 | cfg := ports.Config{} | 12 | cfg := ports.Config{} | ||
| 13 | cfg.Keyboard.CmdKey = "super" | 13 | cfg.Keyboard.CmdKey = "super" | ||
| 14 | cfg.Layout.MaxColumns = 1 | 14 | cfg.Layout.MaxColumns = 1 | ||
| 15 | - | c, err := New(cfg, Channels{Commands: make(chan ports.ClientCommand, 16), Scenes: make(chan []ports.Scene, 1)}) | 15 | + | c, err := New(cfg, Channels{Commands: make(chan ports.ClientCommand, 16), Scenes: make(chan []ports.Scene, 1)}, Options{}) |
| 16 | if err != nil { | 16 | if err != nil { | ||
| 17 | t.Fatal(err) | 17 | t.Fatal(err) | ||
| 18 | } | 18 | } | ||
M internal/core/swipe_test.go +1 -1
| @@ -48,7 +48,7 @@ func startSwipeOn(t *testing.T, edit func(*ports.Config), outs ...ports.OutputIn | |||
| 48 | 48 | output: make(chan ports.OutputEvent, 4), reload: make(chan ports.ConfigChanged, 4), | |
| 49 | 49 | commands: make(chan ports.ClientCommand, 1024), scenes: make(chan []ports.Scene, 1), spawn: make(chan ports.SpawnRequest, 16), state: make(chan ports.State, 1), cfg: cfg, | |
| 50 | 50 | } | |
| 51 | - | c, err := core.New(cfg, core.Channels{Client: r.client, Input: r.input, Output: r.output, Config: r.reload, Commands: r.commands, Scenes: r.scenes, Spawn: r.spawn, State: r.state, Clock: r.clk.clock, Frames: r.frames}) | |
| 51 | + | c, err := core.New(cfg, core.Channels{Client: r.client, Input: r.input, Output: r.output, Config: r.reload, Commands: r.commands, Scenes: r.scenes, Spawn: r.spawn, State: r.state, Frames: r.frames}, core.Options{Clock: r.clk.clock}) | |
| 52 | 52 | if err != nil { | |
| 53 | 53 | t.Fatal(err) | |
| 54 | 54 | } | |
| @@ -48,7 +48,7 @@ func startSwipeOn(t *testing.T, edit func(*ports.Config), outs ...ports.OutputIn | |||||
| 48 | output: make(chan ports.OutputEvent, 4), reload: make(chan ports.ConfigChanged, 4), | 48 | output: make(chan ports.OutputEvent, 4), reload: make(chan ports.ConfigChanged, 4), | ||
| 49 | commands: make(chan ports.ClientCommand, 1024), scenes: make(chan []ports.Scene, 1), spawn: make(chan ports.SpawnRequest, 16), state: make(chan ports.State, 1), cfg: cfg, | 49 | commands: make(chan ports.ClientCommand, 1024), scenes: make(chan []ports.Scene, 1), spawn: make(chan ports.SpawnRequest, 16), state: make(chan ports.State, 1), cfg: cfg, | ||
| 50 | } | 50 | } | ||
| 51 | - | c, err := core.New(cfg, core.Channels{Client: r.client, Input: r.input, Output: r.output, Config: r.reload, Commands: r.commands, Scenes: r.scenes, Spawn: r.spawn, State: r.state, Clock: r.clk.clock, Frames: r.frames}) | 51 | + | c, err := core.New(cfg, core.Channels{Client: r.client, Input: r.input, Output: r.output, Config: r.reload, Commands: r.commands, Scenes: r.scenes, Spawn: r.spawn, State: r.state, Frames: r.frames}, core.Options{Clock: r.clk.clock}) |
| 52 | if err != nil { | 52 | if err != nil { | ||
| 53 | t.Fatal(err) | 53 | t.Fatal(err) | ||
| 54 | } | 54 | } | ||
M internal/core/terminal.go +2 -2
| @@ -16,13 +16,13 @@ const termRetry = 5 * time.Second | |||
| 16 | 16 | ||
| 17 | 17 | // keepsTerminal reports whether empty workspaces get a terminal. | |
| 18 | 18 | func (c *Core) keepsTerminal() bool { | |
| 19 | - | return c.ch.Terminal && c.cfg.Terminal.AutoOpen != "off" && len(c.cfg.Terminal.Command) > 0 | |
| 19 | + | return c.opts.Terminal && c.cfg.Terminal.AutoOpen != "off" && len(c.cfg.Terminal.Command) > 0 | |
| 20 | 20 | } | |
| 21 | 21 | ||
| 22 | 22 | // fillEmpty returns the terminals to spawn for empty workspaces on screen. | |
| 23 | 23 | // Workspaces with slots are left to them. | |
| 24 | 24 | func (c *Core) fillEmpty() []ports.SpawnRequest { | |
| 25 | - | now := time.Now() | |
| 25 | + | now := c.now() | |
| 26 | 26 | c.placement.expireTerminals(now) | |
| 27 | 27 | if !c.keepsTerminal() { | |
| 28 | 28 | return nil | |
| @@ -16,13 +16,13 @@ const termRetry = 5 * time.Second | |||||
| 16 | 16 | ||||
| 17 | // keepsTerminal reports whether empty workspaces get a terminal. | 17 | // keepsTerminal reports whether empty workspaces get a terminal. | ||
| 18 | func (c *Core) keepsTerminal() bool { | 18 | func (c *Core) keepsTerminal() bool { | ||
| 19 | - | return c.ch.Terminal && c.cfg.Terminal.AutoOpen != "off" && len(c.cfg.Terminal.Command) > 0 | 19 | + | return c.opts.Terminal && c.cfg.Terminal.AutoOpen != "off" && len(c.cfg.Terminal.Command) > 0 |
| 20 | } | 20 | } | ||
| 21 | 21 | ||||
| 22 | // fillEmpty returns the terminals to spawn for empty workspaces on screen. | 22 | // fillEmpty returns the terminals to spawn for empty workspaces on screen. | ||
| 23 | // Workspaces with slots are left to them. | 23 | // Workspaces with slots are left to them. | ||
| 24 | func (c *Core) fillEmpty() []ports.SpawnRequest { | 24 | func (c *Core) fillEmpty() []ports.SpawnRequest { | ||
| 25 | - | now := time.Now() | 25 | + | now := c.now() |
| 26 | c.placement.expireTerminals(now) | 26 | c.placement.expireTerminals(now) | ||
| 27 | if !c.keepsTerminal() { | 27 | if !c.keepsTerminal() { | ||
| 28 | return nil | 28 | return nil | ||
M internal/core/transition_unmap_internal_test.go +1 -1
| @@ -312,7 +312,7 @@ func TestUnmapLeavingDroppedByInterruptions(t *testing.T) { | |||
| 312 | 312 | state := ports.SecurityState{Generation: 1, Protected: true} | |
| 313 | 313 | gate := portsmocks.NewMockSessionSecurity(t) | |
| 314 | 314 | gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return state }) | |
| 315 | - | c.ch.Security = gate | |
| 315 | + | c.opts.Security = gate | |
| 316 | 316 | if err := c.publish(context.Background()); err != nil { | |
| 317 | 317 | t.Fatal(err) | |
| 318 | 318 | } | |
| @@ -312,7 +312,7 @@ func TestUnmapLeavingDroppedByInterruptions(t *testing.T) { | |||||
| 312 | state := ports.SecurityState{Generation: 1, Protected: true} | 312 | state := ports.SecurityState{Generation: 1, Protected: true} | ||
| 313 | gate := portsmocks.NewMockSessionSecurity(t) | 313 | gate := portsmocks.NewMockSessionSecurity(t) | ||
| 314 | gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return state }) | 314 | gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return state }) | ||
| 315 | - | c.ch.Security = gate | 315 | + | c.opts.Security = gate |
| 316 | if err := c.publish(context.Background()); err != nil { | 316 | if err := c.publish(context.Background()); err != nil { | ||
| 317 | t.Fatal(err) | 317 | t.Fatal(err) | ||
| 318 | } | 318 | } | ||
M internal/core/visibility_test.go +1 -1
| @@ -16,7 +16,7 @@ func TestConfigureVisible(t *testing.T) { | |||
| 16 | 16 | cfg.Layout.MaxColumns = 1 | |
| 17 | 17 | commands := make(chan ports.ClientCommand, 256) | |
| 18 | 18 | scenes := make(chan []ports.Scene, 1) | |
| 19 | - | c, err := New(cfg, Channels{Commands: commands, Scenes: scenes}) | |
| 19 | + | c, err := New(cfg, Channels{Commands: commands, Scenes: scenes}, Options{}) | |
| 20 | 20 | if err != nil { | |
| 21 | 21 | t.Fatal(err) | |
| 22 | 22 | } | |
| @@ -16,7 +16,7 @@ func TestConfigureVisible(t *testing.T) { | |||||
| 16 | cfg.Layout.MaxColumns = 1 | 16 | cfg.Layout.MaxColumns = 1 | ||
| 17 | commands := make(chan ports.ClientCommand, 256) | 17 | commands := make(chan ports.ClientCommand, 256) | ||
| 18 | scenes := make(chan []ports.Scene, 1) | 18 | scenes := make(chan []ports.Scene, 1) | ||
| 19 | - | c, err := New(cfg, Channels{Commands: commands, Scenes: scenes}) | 19 | + | c, err := New(cfg, Channels{Commands: commands, Scenes: scenes}, Options{}) |
| 20 | if err != nil { | 20 | if err != nil { | ||
| 21 | t.Fatal(err) | 21 | t.Fatal(err) | ||
| 22 | } | 22 | } | ||
M internal/core/workspace_size_internal_test.go +1 -1
| @@ -138,7 +138,7 @@ func TestBringNamedLookupAllocations(t *testing.T) { | |||
| 138 | 138 | // newSizedCore returns a core with buffered output channels and no outputs. | |
| 139 | 139 | func newSizedCore(t *testing.T, cfg ports.Config) *Core { | |
| 140 | 140 | t.Helper() | |
| 141 | - | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Layouts: make(chan ports.Layout, 1), Constraints: make(chan ports.PointerConstraint, 1), State: make(chan ports.State, 1), Workspaces: make(chan ports.Workspaces, 1)}) | |
| 141 | + | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Layouts: make(chan ports.Layout, 1), Constraints: make(chan ports.PointerConstraint, 1), State: make(chan ports.State, 1), Workspaces: make(chan ports.Workspaces, 1)}, Options{}) | |
| 142 | 142 | if err != nil { | |
| 143 | 143 | t.Fatal(err) | |
| 144 | 144 | } | |
| @@ -138,7 +138,7 @@ func TestBringNamedLookupAllocations(t *testing.T) { | |||||
| 138 | // newSizedCore returns a core with buffered output channels and no outputs. | 138 | // newSizedCore returns a core with buffered output channels and no outputs. | ||
| 139 | func newSizedCore(t *testing.T, cfg ports.Config) *Core { | 139 | func newSizedCore(t *testing.T, cfg ports.Config) *Core { | ||
| 140 | t.Helper() | 140 | t.Helper() | ||
| 141 | - | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Layouts: make(chan ports.Layout, 1), Constraints: make(chan ports.PointerConstraint, 1), State: make(chan ports.State, 1), Workspaces: make(chan ports.Workspaces, 1)}) | 141 | + | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Layouts: make(chan ports.Layout, 1), Constraints: make(chan ports.PointerConstraint, 1), State: make(chan ports.State, 1), Workspaces: make(chan ports.Workspaces, 1)}, Options{}) |
| 142 | if err != nil { | 142 | if err != nil { | ||
| 143 | t.Fatal(err) | 143 | t.Fatal(err) | ||
| 144 | } | 144 | } | ||
M internal/core/workspaces_test.go +3 -3
| @@ -10,7 +10,7 @@ import ( | |||
| 10 | 10 | ) | |
| 11 | 11 | ||
| 12 | 12 | func TestWorkspaceChannelCapacity(t *testing.T) { | |
| 13 | - | _, err := core.New(config.Defaults(), core.Channels{Scenes: make(chan []ports.Scene, 1), Workspaces: make(chan ports.Workspaces, 2)}) | |
| 13 | + | _, err := core.New(config.Defaults(), core.Channels{Scenes: make(chan []ports.Scene, 1), Workspaces: make(chan ports.Workspaces, 2)}, core.Options{}) | |
| 14 | 14 | if err == nil { | |
| 15 | 15 | t.Fatal("expected capacity-1 validation") | |
| 16 | 16 | } | |
| @@ -27,7 +27,7 @@ func TestWorkspaceSnapshotsAndActivation(t *testing.T) { | |||
| 27 | 27 | scenes := make(chan []ports.Scene, 1) | |
| 28 | 28 | state := make(chan ports.State, 1) | |
| 29 | 29 | spawn := make(chan ports.SpawnRequest, 8) | |
| 30 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Workspaces: snapshots, State: state, Scenes: scenes, Spawn: spawn}) | |
| 30 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Workspaces: snapshots, State: state, Scenes: scenes, Spawn: spawn}, core.Options{}) | |
| 31 | 31 | if err != nil { | |
| 32 | 32 | t.Fatal(err) | |
| 33 | 33 | } | |
| @@ -114,7 +114,7 @@ func TestMoveWorkspaceReordersSnapshot(t *testing.T) { | |||
| 114 | 114 | scenes := make(chan []ports.Scene, 1) | |
| 115 | 115 | spawn := make(chan ports.SpawnRequest, 8) | |
| 116 | 116 | commands := make(chan ports.ClientCommand, 32) | |
| 117 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Workspaces: snapshots, Scenes: scenes, Spawn: spawn}) | |
| 117 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Workspaces: snapshots, Scenes: scenes, Spawn: spawn}, core.Options{}) | |
| 118 | 118 | if err != nil { | |
| 119 | 119 | t.Fatal(err) | |
| 120 | 120 | } | |
| @@ -10,7 +10,7 @@ import ( | |||||
| 10 | ) | 10 | ) | ||
| 11 | 11 | ||||
| 12 | func TestWorkspaceChannelCapacity(t *testing.T) { | 12 | func TestWorkspaceChannelCapacity(t *testing.T) { | ||
| 13 | - | _, err := core.New(config.Defaults(), core.Channels{Scenes: make(chan []ports.Scene, 1), Workspaces: make(chan ports.Workspaces, 2)}) | 13 | + | _, err := core.New(config.Defaults(), core.Channels{Scenes: make(chan []ports.Scene, 1), Workspaces: make(chan ports.Workspaces, 2)}, core.Options{}) |
| 14 | if err == nil { | 14 | if err == nil { | ||
| 15 | t.Fatal("expected capacity-1 validation") | 15 | t.Fatal("expected capacity-1 validation") | ||
| 16 | } | 16 | } | ||
| @@ -27,7 +27,7 @@ func TestWorkspaceSnapshotsAndActivation(t *testing.T) { | |||||
| 27 | scenes := make(chan []ports.Scene, 1) | 27 | scenes := make(chan []ports.Scene, 1) | ||
| 28 | state := make(chan ports.State, 1) | 28 | state := make(chan ports.State, 1) | ||
| 29 | spawn := make(chan ports.SpawnRequest, 8) | 29 | spawn := make(chan ports.SpawnRequest, 8) | ||
| 30 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Workspaces: snapshots, State: state, Scenes: scenes, Spawn: spawn}) | 30 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Workspaces: snapshots, State: state, Scenes: scenes, Spawn: spawn}, core.Options{}) |
| 31 | if err != nil { | 31 | if err != nil { | ||
| 32 | t.Fatal(err) | 32 | t.Fatal(err) | ||
| 33 | } | 33 | } | ||
| @@ -114,7 +114,7 @@ func TestMoveWorkspaceReordersSnapshot(t *testing.T) { | |||||
| 114 | scenes := make(chan []ports.Scene, 1) | 114 | scenes := make(chan []ports.Scene, 1) | ||
| 115 | spawn := make(chan ports.SpawnRequest, 8) | 115 | spawn := make(chan ports.SpawnRequest, 8) | ||
| 116 | commands := make(chan ports.ClientCommand, 32) | 116 | commands := make(chan ports.ClientCommand, 32) | ||
| 117 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Workspaces: snapshots, Scenes: scenes, Spawn: spawn}) | 117 | + | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Workspaces: snapshots, Scenes: scenes, Spawn: spawn}, core.Options{}) |
| 118 | if err != nil { | 118 | if err != nil { | ||
| 119 | t.Fatal(err) | 119 | t.Fatal(err) | ||
| 120 | } | 120 | } | ||