perf(app): start nothing for hint mode when it is disabled
- commit
- 9b0b5e5716815f46761d2f7745bbf7ada11d2f80
- author
- brice · · 2026-10-19 14:57 UTC
- parent
- e757cab
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
8 files · +148 -19
M internal/app/app.go +7 -0
| @@ -84,6 +84,7 @@ const busRetry = 2 * time.Second | |||
| 84 | 84 | func relayConfig(ctx context.Context, cur ports.Config, in <-chan ports.ConfigChanged, out chan<- ports.ConfigChanged, keymaps chan *xkb.Keymap, deviceConfigs chan ports.InputDevicesConfig, curs *cursors, commands chan<- ports.ClientCommand, log zerowrap.Logger) { | |
| 85 | 85 | kb := cur.Keyboard | |
| 86 | 86 | devCfg := cur.InputDevicesConfig | |
| 87 | + | hintOn := cur.Hint.Enable | |
| 87 | 88 | for { | |
| 88 | 89 | var ev ports.ConfigChanged | |
| 89 | 90 | select { | |
| @@ -91,6 +92,12 @@ func relayConfig(ctx context.Context, cur ports.Config, in <-chan ports.ConfigCh | |||
| 91 | 92 | return | |
| 92 | 93 | case ev = <-in: | |
| 93 | 94 | } | |
| 95 | + | // hint.enable is read at startup: nothing of hint mode starts or | |
| 96 | + | // stops on a reload. | |
| 97 | + | if ev.Config.Hint.Enable != hintOn { | |
| 98 | + | log.Info().Bool("running", hintOn).Msg("hint.enable changes apply after restart") | |
| 99 | + | ev.Config.Hint.Enable = hintOn | |
| 100 | + | } | |
| 94 | 101 | next := ev.Config.Keyboard | |
| 95 | 102 | layoutChanged := next.Layout != kb.Layout || next.Variant != kb.Variant || next.Options != kb.Options | |
| 96 | 103 | repeatChanged := next.RepeatRate != kb.RepeatRate || next.RepeatDelay != kb.RepeatDelay | |
| @@ -84,6 +84,7 @@ const busRetry = 2 * time.Second | |||||
| 84 | func relayConfig(ctx context.Context, cur ports.Config, in <-chan ports.ConfigChanged, out chan<- ports.ConfigChanged, keymaps chan *xkb.Keymap, deviceConfigs chan ports.InputDevicesConfig, curs *cursors, commands chan<- ports.ClientCommand, log zerowrap.Logger) { | 84 | func relayConfig(ctx context.Context, cur ports.Config, in <-chan ports.ConfigChanged, out chan<- ports.ConfigChanged, keymaps chan *xkb.Keymap, deviceConfigs chan ports.InputDevicesConfig, curs *cursors, commands chan<- ports.ClientCommand, log zerowrap.Logger) { | ||
| 85 | kb := cur.Keyboard | 85 | kb := cur.Keyboard | ||
| 86 | devCfg := cur.InputDevicesConfig | 86 | devCfg := cur.InputDevicesConfig | ||
| 87 | + | hintOn := cur.Hint.Enable | |||
| 87 | for { | 88 | for { | ||
| 88 | var ev ports.ConfigChanged | 89 | var ev ports.ConfigChanged | ||
| 89 | select { | 90 | select { | ||
| @@ -91,6 +92,12 @@ func relayConfig(ctx context.Context, cur ports.Config, in <-chan ports.ConfigCh | |||||
| 91 | return | 92 | return | ||
| 92 | case ev = <-in: | 93 | case ev = <-in: | ||
| 93 | } | 94 | } | ||
| 95 | + | // hint.enable is read at startup: nothing of hint mode starts or | |||
| 96 | + | // stops on a reload. | |||
| 97 | + | if ev.Config.Hint.Enable != hintOn { | |||
| 98 | + | log.Info().Bool("running", hintOn).Msg("hint.enable changes apply after restart") | |||
| 99 | + | ev.Config.Hint.Enable = hintOn | |||
| 100 | + | } | |||
| 94 | next := ev.Config.Keyboard | 101 | next := ev.Config.Keyboard | ||
| 95 | layoutChanged := next.Layout != kb.Layout || next.Variant != kb.Variant || next.Options != kb.Options | 102 | layoutChanged := next.Layout != kb.Layout || next.Variant != kb.Variant || next.Options != kb.Options | ||
| 96 | repeatChanged := next.RepeatRate != kb.RepeatRate || next.RepeatDelay != kb.RepeatDelay | 103 | repeatChanged := next.RepeatRate != kb.RepeatRate || next.RepeatDelay != kb.RepeatDelay | ||
M internal/app/hintdebug.go +7 -0
| @@ -46,3 +46,10 @@ func hintDump(ctx context.Context) hints.Options { | |||
| 46 | 46 | log.Warn().Str("dir", dir).Msg("hint debug dumps enabled") | |
| 47 | 47 | return hints.Options{DumpDir: dir} | |
| 48 | 48 | } | |
| 49 | + | ||
| 50 | + | // hintDisabledNote returns the once-only notice for a hint bind pressed while | |
| 51 | + | // hint mode is off. The logger is made now; nothing else of hint mode exists. | |
| 52 | + | func hintDisabledNote(ctx context.Context) func() { | |
| 53 | + | log := logging.For(ctx, "core") | |
| 54 | + | return func() { log.Info().Msg("hint mode is disabled; set hint.enable = on and restart") } | |
| 55 | + | } | |
| @@ -46,3 +46,10 @@ func hintDump(ctx context.Context) hints.Options { | |||||
| 46 | log.Warn().Str("dir", dir).Msg("hint debug dumps enabled") | 46 | log.Warn().Str("dir", dir).Msg("hint debug dumps enabled") | ||
| 47 | return hints.Options{DumpDir: dir} | 47 | return hints.Options{DumpDir: dir} | ||
| 48 | } | 48 | } | ||
| 49 | + | ||||
| 50 | + | // hintDisabledNote returns the once-only notice for a hint bind pressed while | |||
| 51 | + | // hint mode is off. The logger is made now; nothing else of hint mode exists. | |||
| 52 | + | func hintDisabledNote(ctx context.Context) func() { | |||
| 53 | + | log := logging.For(ctx, "core") | |||
| 54 | + | return func() { log.Info().Msg("hint mode is disabled; set hint.enable = on and restart") } | |||
| 55 | + | } | |||
A internal/app/hintoptin_test.go +64 -0
| @@ -0,0 +1,64 @@ | |||
| 1 | + | package app | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "runtime" | |
| 6 | + | "testing" | |
| 7 | + | "time" | |
| 8 | + | ||
| 9 | + | "github.com/stretchr/testify/require" | |
| 10 | + | ||
| 11 | + | "github.com/bnema/neferwl/internal/adapters/clock" | |
| 12 | + | "github.com/bnema/neferwl/internal/adapters/config" | |
| 13 | + | "github.com/bnema/neferwl/internal/adapters/logging" | |
| 14 | + | "github.com/bnema/neferwl/internal/adapters/xkb" | |
| 15 | + | "github.com/bnema/neferwl/internal/ports" | |
| 16 | + | ) | |
| 17 | + | ||
| 18 | + | func TestHintPipesOffByDefault(t *testing.T) { | |
| 19 | + | require.False(t, config.Defaults().Hint.Enable) | |
| 20 | + | off := newPipes(false, false) | |
| 21 | + | require.Nil(t, off.hintScans) | |
| 22 | + | require.Nil(t, off.hintResults) | |
| 23 | + | require.Nil(t, off.hintCaptured) | |
| 24 | + | ch := off.core() | |
| 25 | + | require.Nil(t, ch.HintScans) | |
| 26 | + | require.Nil(t, ch.Hints) | |
| 27 | + | on := newPipes(false, true) | |
| 28 | + | require.NotNil(t, on.hintScans) | |
| 29 | + | require.NotNil(t, on.hintResults) | |
| 30 | + | require.NotNil(t, on.hintCaptured) | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | func TestHintOffStartsNoHintGoroutine(t *testing.T) { | |
| 34 | + | t.Setenv("XDG_RUNTIME_DIR", t.TempDir()) | |
| 35 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 36 | + | defer cancel() | |
| 37 | + | done := make(chan error, 1) | |
| 38 | + | go func() { | |
| 39 | + | done <- Run(ctx, Options{Backend: "headless", NoXwayland: true, Config: config.Defaults(), NoTerminal: true, Timeout: 800 * time.Millisecond}) | |
| 40 | + | }() | |
| 41 | + | time.Sleep(400 * time.Millisecond) | |
| 42 | + | buf := make([]byte, 1<<20) | |
| 43 | + | dump := string(buf[:runtime.Stack(buf, true)]) | |
| 44 | + | require.NotContains(t, dump, "adapters/hints.") | |
| 45 | + | require.NotContains(t, dump, "adapters/atspi.") | |
| 46 | + | select { | |
| 47 | + | case <-done: | |
| 48 | + | case <-time.After(3 * time.Second): | |
| 49 | + | t.Fatal("session did not stop") | |
| 50 | + | } | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | func TestRelayConfigKeepsStartupHintEnable(t *testing.T) { | |
| 54 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 55 | + | defer cancel() | |
| 56 | + | cur := config.Defaults() | |
| 57 | + | in := make(chan ports.ConfigChanged, 1) | |
| 58 | + | out := make(chan ports.ConfigChanged, 1) | |
| 59 | + | go relayConfig(ctx, cur, in, out, make(chan *xkb.Keymap, 1), make(chan ports.InputDevicesConfig, 1), newCursors(clock.System{}, 0), make(chan ports.ClientCommand, 1), logging.For(ctx, "config")) | |
| 60 | + | next := config.Defaults() | |
| 61 | + | next.Hint.Enable = true | |
| 62 | + | in <- ports.ConfigChanged{Config: next} | |
| 63 | + | require.False(t, (<-out).Config.Hint.Enable) | |
| 64 | + | } | |
| @@ -0,0 +1,64 @@ | |||||
| 1 | + | package app | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "runtime" | |||
| 6 | + | "testing" | |||
| 7 | + | "time" | |||
| 8 | + | ||||
| 9 | + | "github.com/stretchr/testify/require" | |||
| 10 | + | ||||
| 11 | + | "github.com/bnema/neferwl/internal/adapters/clock" | |||
| 12 | + | "github.com/bnema/neferwl/internal/adapters/config" | |||
| 13 | + | "github.com/bnema/neferwl/internal/adapters/logging" | |||
| 14 | + | "github.com/bnema/neferwl/internal/adapters/xkb" | |||
| 15 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 16 | + | ) | |||
| 17 | + | ||||
| 18 | + | func TestHintPipesOffByDefault(t *testing.T) { | |||
| 19 | + | require.False(t, config.Defaults().Hint.Enable) | |||
| 20 | + | off := newPipes(false, false) | |||
| 21 | + | require.Nil(t, off.hintScans) | |||
| 22 | + | require.Nil(t, off.hintResults) | |||
| 23 | + | require.Nil(t, off.hintCaptured) | |||
| 24 | + | ch := off.core() | |||
| 25 | + | require.Nil(t, ch.HintScans) | |||
| 26 | + | require.Nil(t, ch.Hints) | |||
| 27 | + | on := newPipes(false, true) | |||
| 28 | + | require.NotNil(t, on.hintScans) | |||
| 29 | + | require.NotNil(t, on.hintResults) | |||
| 30 | + | require.NotNil(t, on.hintCaptured) | |||
| 31 | + | } | |||
| 32 | + | ||||
| 33 | + | func TestHintOffStartsNoHintGoroutine(t *testing.T) { | |||
| 34 | + | t.Setenv("XDG_RUNTIME_DIR", t.TempDir()) | |||
| 35 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 36 | + | defer cancel() | |||
| 37 | + | done := make(chan error, 1) | |||
| 38 | + | go func() { | |||
| 39 | + | done <- Run(ctx, Options{Backend: "headless", NoXwayland: true, Config: config.Defaults(), NoTerminal: true, Timeout: 800 * time.Millisecond}) | |||
| 40 | + | }() | |||
| 41 | + | time.Sleep(400 * time.Millisecond) | |||
| 42 | + | buf := make([]byte, 1<<20) | |||
| 43 | + | dump := string(buf[:runtime.Stack(buf, true)]) | |||
| 44 | + | require.NotContains(t, dump, "adapters/hints.") | |||
| 45 | + | require.NotContains(t, dump, "adapters/atspi.") | |||
| 46 | + | select { | |||
| 47 | + | case <-done: | |||
| 48 | + | case <-time.After(3 * time.Second): | |||
| 49 | + | t.Fatal("session did not stop") | |||
| 50 | + | } | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | func TestRelayConfigKeepsStartupHintEnable(t *testing.T) { | |||
| 54 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 55 | + | defer cancel() | |||
| 56 | + | cur := config.Defaults() | |||
| 57 | + | in := make(chan ports.ConfigChanged, 1) | |||
| 58 | + | out := make(chan ports.ConfigChanged, 1) | |||
| 59 | + | go relayConfig(ctx, cur, in, out, make(chan *xkb.Keymap, 1), make(chan ports.InputDevicesConfig, 1), newCursors(clock.System{}, 0), make(chan ports.ClientCommand, 1), logging.For(ctx, "config")) | |||
| 60 | + | next := config.Defaults() | |||
| 61 | + | next.Hint.Enable = true | |||
| 62 | + | in <- ports.ConfigChanged{Config: next} | |||
| 63 | + | require.False(t, (<-out).Config.Hint.Enable) | |||
| 64 | + | } | |||
M internal/app/session.go +14 -9
| @@ -92,17 +92,20 @@ func assembleSession(ctx context.Context, opts Options) (s *session, err error) | |||
| 92 | 92 | return s, err | |
| 93 | 93 | } | |
| 94 | 94 | } | |
| 95 | - | s.p = newPipes(s.hw != nil) | |
| 95 | + | s.p = newPipes(s.hw != nil, opts.Config.Hint.Enable) | |
| 96 | 96 | // core() carries channels only; the gate, clock and terminal flag are | |
| 97 | 97 | // explicit core.Options. | |
| 98 | - | if s.core, err = core.New(opts.Config, s.p.core(), core.Options{Security: s.security, Clock: s.clock, Terminal: !opts.NoTerminal}); err != nil { | |
| 98 | + | if s.core, err = core.New(opts.Config, s.p.core(), core.Options{Security: s.security, Clock: s.clock, Terminal: !opts.NoTerminal, HintDisabled: hintDisabledNote(ctx)}); err != nil { | |
| 99 | 99 | return s, err | |
| 100 | 100 | } | |
| 101 | 101 | 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 { | |
| 102 | 102 | return s, err | |
| 103 | 103 | } | |
| 104 | 104 | runtimeDir := os.Getenv("XDG_RUNTIME_DIR") | |
| 105 | - | s.hintOpts = hintOptions(ctx) | |
| 105 | + | // Hint mode off: no accessibility connection, no debug env, nothing. | |
| 106 | + | if opts.Config.Hint.Enable { | |
| 107 | + | s.hintOpts = hintOptions(ctx) | |
| 108 | + | } | |
| 106 | 109 | // Every output renders on the same GPU: its formats are the clients'. | |
| 107 | 110 | s.dmabuf = vulkan.Probe() | |
| 108 | 111 | log.Info().Int("formats", len(s.dmabuf.Formats)).Msg("dmabuf") | |
| @@ -345,12 +348,14 @@ func (s *session) run(ctx context.Context, inject func(chan<- ports.InputEvent)) | |||
| 345 | 348 | }() | |
| 346 | 349 | workers.Add(1) | |
| 347 | 350 | go func() { defer workers.Done(); routeCaptured(ctx, p.capturedAll, p.captured, p.internalCaptured()) }() | |
| 348 | - | workers.Add(1) | |
| 349 | - | go func() { | |
| 350 | - | defer workers.Done() | |
| 351 | - | // Run only returns when ctx is done: nothing to report. | |
| 352 | - | _ = hints.Run(ctx, p.hintScans, p.captures, p.hintCaptured, p.hintResults, s.hintOpts) | |
| 353 | - | }() | |
| 351 | + | if p.hintScans != nil { | |
| 352 | + | workers.Add(1) | |
| 353 | + | go func() { | |
| 354 | + | defer workers.Done() | |
| 355 | + | // Run only returns when ctx is done: nothing to report. | |
| 356 | + | _ = hints.Run(ctx, p.hintScans, p.captures, p.hintCaptured, p.hintResults, s.hintOpts) | |
| 357 | + | }() | |
| 358 | + | } | |
| 354 | 359 | go func() { defer workers.Done(); done <- child.Run(ctx, p.spawn) }() | |
| 355 | 360 | go func() { defer workers.Done(); done <- c.Run(ctx) }() | |
| 356 | 361 | workers.Add(1) | |
| @@ -92,17 +92,20 @@ func assembleSession(ctx context.Context, opts Options) (s *session, err error) | |||||
| 92 | return s, err | 92 | return s, err | ||
| 93 | } | 93 | } | ||
| 94 | } | 94 | } | ||
| 95 | - | s.p = newPipes(s.hw != nil) | 95 | + | s.p = newPipes(s.hw != nil, opts.Config.Hint.Enable) |
| 96 | // core() carries channels only; the gate, clock and terminal flag are | 96 | // core() carries channels only; the gate, clock and terminal flag are | ||
| 97 | // explicit core.Options. | 97 | // explicit core.Options. | ||
| 98 | - | if s.core, err = core.New(opts.Config, s.p.core(), core.Options{Security: s.security, Clock: s.clock, Terminal: !opts.NoTerminal}); err != nil { | 98 | + | if s.core, err = core.New(opts.Config, s.p.core(), core.Options{Security: s.security, Clock: s.clock, Terminal: !opts.NoTerminal, HintDisabled: hintDisabledNote(ctx)}); err != nil { |
| 99 | return s, err | 99 | return s, err | ||
| 100 | } | 100 | } | ||
| 101 | 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 { | 101 | 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 { | ||
| 102 | return s, err | 102 | return s, err | ||
| 103 | } | 103 | } | ||
| 104 | runtimeDir := os.Getenv("XDG_RUNTIME_DIR") | 104 | runtimeDir := os.Getenv("XDG_RUNTIME_DIR") | ||
| 105 | - | s.hintOpts = hintOptions(ctx) | 105 | + | // Hint mode off: no accessibility connection, no debug env, nothing. |
| 106 | + | if opts.Config.Hint.Enable { | |||
| 107 | + | s.hintOpts = hintOptions(ctx) | |||
| 108 | + | } | |||
| 106 | // Every output renders on the same GPU: its formats are the clients'. | 109 | // Every output renders on the same GPU: its formats are the clients'. | ||
| 107 | s.dmabuf = vulkan.Probe() | 110 | s.dmabuf = vulkan.Probe() | ||
| 108 | log.Info().Int("formats", len(s.dmabuf.Formats)).Msg("dmabuf") | 111 | log.Info().Int("formats", len(s.dmabuf.Formats)).Msg("dmabuf") | ||
| @@ -345,12 +348,14 @@ func (s *session) run(ctx context.Context, inject func(chan<- ports.InputEvent)) | |||||
| 345 | }() | 348 | }() | ||
| 346 | workers.Add(1) | 349 | workers.Add(1) | ||
| 347 | go func() { defer workers.Done(); routeCaptured(ctx, p.capturedAll, p.captured, p.internalCaptured()) }() | 350 | go func() { defer workers.Done(); routeCaptured(ctx, p.capturedAll, p.captured, p.internalCaptured()) }() | ||
| 348 | - | workers.Add(1) | 351 | + | if p.hintScans != nil { |
| 349 | - | go func() { | 352 | + | workers.Add(1) |
| 350 | - | defer workers.Done() | 353 | + | go func() { |
| 351 | - | // Run only returns when ctx is done: nothing to report. | 354 | + | defer workers.Done() |
| 352 | - | _ = hints.Run(ctx, p.hintScans, p.captures, p.hintCaptured, p.hintResults, s.hintOpts) | 355 | + | // Run only returns when ctx is done: nothing to report. |
| 353 | - | }() | 356 | + | _ = hints.Run(ctx, p.hintScans, p.captures, p.hintCaptured, p.hintResults, s.hintOpts) |
| 357 | + | }() | |||
| 358 | + | } | |||
| 354 | go func() { defer workers.Done(); done <- child.Run(ctx, p.spawn) }() | 359 | go func() { defer workers.Done(); done <- child.Run(ctx, p.spawn) }() | ||
| 355 | go func() { defer workers.Done(); done <- c.Run(ctx) }() | 360 | go func() { defer workers.Done(); done <- c.Run(ctx) }() | ||
| 356 | workers.Add(1) | 361 | workers.Add(1) | ||
M internal/app/wiring.go +12 -8
| @@ -79,7 +79,10 @@ type pipes struct { | |||
| 79 | 79 | castCommands chan ports.CastCommand | |
| 80 | 80 | } | |
| 81 | 81 | ||
| 82 | - | func newPipes(realSession bool) *pipes { | |
| 82 | + | // newPipes makes the channels. The hint channels exist only with hint set | |
| 83 | + | // (hint.enable = on): without them core's hint action is a no-op and nothing | |
| 84 | + | // of hint mode is allocated. | |
| 85 | + | func newPipes(realSession, hint bool) *pipes { | |
| 83 | 86 | p := &pipes{ | |
| 84 | 87 | securityChanges: make(chan ports.SecurityState, 8), | |
| 85 | 88 | securityEvents: make(chan ports.SecurityBackendEvent, 64), | |
| @@ -107,13 +110,9 @@ func newPipes(realSession bool) *pipes { | |||
| 107 | 110 | ||
| 108 | 111 | frames: make(chan ports.OutputFrame, 8), | |
| 109 | 112 | ||
| 110 | - | hintScans: make(chan ports.HintScanRequest, 1), | |
| 111 | - | hintResults: make(chan ports.HintScanResult, 1), | |
| 112 | - | ||
| 113 | - | captures: make(chan ports.CaptureRequest, ports.MaxCaptureInflight), | |
| 114 | - | capturedAll: make(chan ports.CaptureDone, ports.MaxCaptureInflight), | |
| 115 | - | captured: make(chan ports.CaptureDone, ports.MaxCaptureInflight), | |
| 116 | - | hintCaptured: make(chan ports.CaptureDone, 1), | |
| 113 | + | captures: make(chan ports.CaptureRequest, ports.MaxCaptureInflight), | |
| 114 | + | capturedAll: make(chan ports.CaptureDone, ports.MaxCaptureInflight), | |
| 115 | + | captured: make(chan ports.CaptureDone, ports.MaxCaptureInflight), | |
| 117 | 116 | ||
| 118 | 117 | outputFormats: make(chan ports.OutputFormats, 8), | |
| 119 | 118 | outputHeads: make(chan ports.OutputHeads, 8), | |
| @@ -127,6 +126,11 @@ func newPipes(realSession bool) *pipes { | |||
| 127 | 126 | ||
| 128 | 127 | castCommands: make(chan ports.CastCommand, 64), | |
| 129 | 128 | } | |
| 129 | + | if hint { | |
| 130 | + | p.hintScans = make(chan ports.HintScanRequest, 1) | |
| 131 | + | p.hintResults = make(chan ports.HintScanResult, 1) | |
| 132 | + | p.hintCaptured = make(chan ports.CaptureDone, 1) | |
| 133 | + | } | |
| 130 | 134 | if realSession { | |
| 131 | 135 | p.scales = make(chan ports.ScaleChanged, 8) | |
| 132 | 136 | } | |
| @@ -79,7 +79,10 @@ type pipes struct { | |||||
| 79 | castCommands chan ports.CastCommand | 79 | castCommands chan ports.CastCommand | ||
| 80 | } | 80 | } | ||
| 81 | 81 | ||||
| 82 | - | func newPipes(realSession bool) *pipes { | 82 | + | // newPipes makes the channels. The hint channels exist only with hint set |
| 83 | + | // (hint.enable = on): without them core's hint action is a no-op and nothing | |||
| 84 | + | // of hint mode is allocated. | |||
| 85 | + | func newPipes(realSession, hint bool) *pipes { | |||
| 83 | p := &pipes{ | 86 | p := &pipes{ | ||
| 84 | securityChanges: make(chan ports.SecurityState, 8), | 87 | securityChanges: make(chan ports.SecurityState, 8), | ||
| 85 | securityEvents: make(chan ports.SecurityBackendEvent, 64), | 88 | securityEvents: make(chan ports.SecurityBackendEvent, 64), | ||
| @@ -107,13 +110,9 @@ func newPipes(realSession bool) *pipes { | |||||
| 107 | 110 | ||||
| 108 | frames: make(chan ports.OutputFrame, 8), | 111 | frames: make(chan ports.OutputFrame, 8), | ||
| 109 | 112 | ||||
| 110 | - | hintScans: make(chan ports.HintScanRequest, 1), | 113 | + | captures: make(chan ports.CaptureRequest, ports.MaxCaptureInflight), |
| 111 | - | hintResults: make(chan ports.HintScanResult, 1), | 114 | + | capturedAll: make(chan ports.CaptureDone, ports.MaxCaptureInflight), |
| 112 | - | 115 | + | captured: make(chan ports.CaptureDone, ports.MaxCaptureInflight), | |
| 113 | - | captures: make(chan ports.CaptureRequest, ports.MaxCaptureInflight), | |||
| 114 | - | capturedAll: make(chan ports.CaptureDone, ports.MaxCaptureInflight), | |||
| 115 | - | captured: make(chan ports.CaptureDone, ports.MaxCaptureInflight), | |||
| 116 | - | hintCaptured: make(chan ports.CaptureDone, 1), | |||
| 117 | 116 | ||||
| 118 | outputFormats: make(chan ports.OutputFormats, 8), | 117 | outputFormats: make(chan ports.OutputFormats, 8), | ||
| 119 | outputHeads: make(chan ports.OutputHeads, 8), | 118 | outputHeads: make(chan ports.OutputHeads, 8), | ||
| @@ -127,6 +126,11 @@ func newPipes(realSession bool) *pipes { | |||||
| 127 | 126 | ||||
| 128 | castCommands: make(chan ports.CastCommand, 64), | 127 | castCommands: make(chan ports.CastCommand, 64), | ||
| 129 | } | 128 | } | ||
| 129 | + | if hint { | |||
| 130 | + | p.hintScans = make(chan ports.HintScanRequest, 1) | |||
| 131 | + | p.hintResults = make(chan ports.HintScanResult, 1) | |||
| 132 | + | p.hintCaptured = make(chan ports.CaptureDone, 1) | |||
| 133 | + | } | |||
| 130 | if realSession { | 134 | if realSession { | ||
| 131 | p.scales = make(chan ports.ScaleChanged, 8) | 135 | p.scales = make(chan ports.ScaleChanged, 8) | ||
| 132 | } | 136 | } | ||
M internal/core/core.go +3 -0
| @@ -63,6 +63,9 @@ type Options struct { | |||
| 63 | 63 | Clock ports.Clock | |
| 64 | 64 | // Terminal enables automatic terminal opening according to terminal.auto-open. | |
| 65 | 65 | Terminal bool | |
| 66 | + | // HintDisabled, when set, is called the first time the hint action runs | |
| 67 | + | // without a scanner (hint mode off). It must not block. | |
| 68 | + | HintDisabled func() | |
| 66 | 69 | } | |
| 67 | 70 | ||
| 68 | 71 | // fullscreenGrace is how long after mapping a window's fullscreen request | |
| @@ -63,6 +63,9 @@ type Options struct { | |||||
| 63 | Clock ports.Clock | 63 | Clock ports.Clock | ||
| 64 | // Terminal enables automatic terminal opening according to terminal.auto-open. | 64 | // Terminal enables automatic terminal opening according to terminal.auto-open. | ||
| 65 | Terminal bool | 65 | Terminal bool | ||
| 66 | + | // HintDisabled, when set, is called the first time the hint action runs | |||
| 67 | + | // without a scanner (hint mode off). It must not block. | |||
| 68 | + | HintDisabled func() | |||
| 66 | } | 69 | } | ||
| 67 | 70 | ||||
| 68 | // fullscreenGrace is how long after mapping a window's fullscreen request | 71 | // fullscreenGrace is how long after mapping a window's fullscreen request | ||
M internal/core/hint.go +9 -2
| @@ -50,10 +50,17 @@ type hintState struct { | |||
| 50 | 50 | } | |
| 51 | 51 | ||
| 52 | 52 | // hintRequest starts hint mode on the focused screen. It does nothing | |
| 53 | - | // without a scanner, while the session is protected, while hint mode is | |
| 53 | + | // (reporting once through Options.HintDisabled) without a scanner, while the session is protected, while hint mode is | |
| 54 | 54 | // active, or on an output with a non-normal transform. | |
| 55 | 55 | func (c *Core) hintRequest() { | |
| 56 | - | if c.ch.HintScans == nil || c.security.Protected || c.hint.phase != hintIdle || c.hintPointerBusy() { | |
| 56 | + | if c.ch.HintScans == nil { | |
| 57 | + | if c.opts.HintDisabled != nil { | |
| 58 | + | c.opts.HintDisabled() | |
| 59 | + | c.opts.HintDisabled = nil | |
| 60 | + | } | |
| 61 | + | return | |
| 62 | + | } | |
| 63 | + | if c.security.Protected || c.hint.phase != hintIdle || c.hintPointerBusy() { | |
| 57 | 64 | return | |
| 58 | 65 | } | |
| 59 | 66 | sc := c.cur() | |
| @@ -50,10 +50,17 @@ type hintState struct { | |||||
| 50 | } | 50 | } | ||
| 51 | 51 | ||||
| 52 | // hintRequest starts hint mode on the focused screen. It does nothing | 52 | // hintRequest starts hint mode on the focused screen. It does nothing | ||
| 53 | - | // without a scanner, while the session is protected, while hint mode is | 53 | + | // (reporting once through Options.HintDisabled) without a scanner, while the session is protected, while hint mode is |
| 54 | // active, or on an output with a non-normal transform. | 54 | // active, or on an output with a non-normal transform. | ||
| 55 | func (c *Core) hintRequest() { | 55 | func (c *Core) hintRequest() { | ||
| 56 | - | if c.ch.HintScans == nil || c.security.Protected || c.hint.phase != hintIdle || c.hintPointerBusy() { | 56 | + | if c.ch.HintScans == nil { |
| 57 | + | if c.opts.HintDisabled != nil { | |||
| 58 | + | c.opts.HintDisabled() | |||
| 59 | + | c.opts.HintDisabled = nil | |||
| 60 | + | } | |||
| 61 | + | return | |||
| 62 | + | } | |||
| 63 | + | if c.security.Protected || c.hint.phase != hintIdle || c.hintPointerBusy() { | |||
| 57 | return | 64 | return | ||
| 58 | } | 65 | } | ||
| 59 | sc := c.cur() | 66 | sc := c.cur() | ||
M internal/core/hint_test.go +32 -0
| @@ -706,3 +706,35 @@ func TestHintTextClickNoModifiersWhenNotClickable(t *testing.T) { | |||
| 706 | 706 | t.Fatalf("commands %v under a constraint", got) | |
| 707 | 707 | } | |
| 708 | 708 | } | |
| 709 | + | ||
| 710 | + | func TestHintDisabledNotesOnce(t *testing.T) { | |
| 711 | + | cfg := scrollDefaults() | |
| 712 | + | cfg.Binds["Cmd+g"] = "hint" | |
| 713 | + | var noted atomic.Int32 | |
| 714 | + | h := &hintHarness{ | |
| 715 | + | input: make(chan ports.InputEvent, 32), | |
| 716 | + | client: make(chan ports.ClientEvent, 32), | |
| 717 | + | output: make(chan ports.OutputEvent, 8), | |
| 718 | + | commands: make(chan ports.ClientCommand, 256), | |
| 719 | + | scenes: make(chan []ports.Scene, 1), | |
| 720 | + | } | |
| 721 | + | ch := core.Channels{Client: h.client, Input: h.input, Output: h.output, Commands: h.commands, Scenes: h.scenes} | |
| 722 | + | c, err := core.New(cfg, ch, core.Options{HintDisabled: func() { noted.Add(1) }}) | |
| 723 | + | if err != nil { | |
| 724 | + | t.Fatal(err) | |
| 725 | + | } | |
| 726 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 727 | + | done := make(chan error, 1) | |
| 728 | + | go func() { done <- c.Run(ctx) }() | |
| 729 | + | t.Cleanup(func() { cancel(); receive(t, done) }) | |
| 730 | + | h.output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 1000, Height: 800}} | |
| 731 | + | scene(t, h.scenes) | |
| 732 | + | for range 3 { | |
| 733 | + | h.press("g", ports.ModSuper) | |
| 734 | + | h.release("g", 0) | |
| 735 | + | } | |
| 736 | + | c.Ping(ctx) | |
| 737 | + | if got := noted.Load(); got != 1 { | |
| 738 | + | t.Fatalf("disabled note ran %d times, want 1", got) | |
| 739 | + | } | |
| 740 | + | } | |
| @@ -706,3 +706,35 @@ func TestHintTextClickNoModifiersWhenNotClickable(t *testing.T) { | |||||
| 706 | t.Fatalf("commands %v under a constraint", got) | 706 | t.Fatalf("commands %v under a constraint", got) | ||
| 707 | } | 707 | } | ||
| 708 | } | 708 | } | ||
| 709 | + | ||||
| 710 | + | func TestHintDisabledNotesOnce(t *testing.T) { | |||
| 711 | + | cfg := scrollDefaults() | |||
| 712 | + | cfg.Binds["Cmd+g"] = "hint" | |||
| 713 | + | var noted atomic.Int32 | |||
| 714 | + | h := &hintHarness{ | |||
| 715 | + | input: make(chan ports.InputEvent, 32), | |||
| 716 | + | client: make(chan ports.ClientEvent, 32), | |||
| 717 | + | output: make(chan ports.OutputEvent, 8), | |||
| 718 | + | commands: make(chan ports.ClientCommand, 256), | |||
| 719 | + | scenes: make(chan []ports.Scene, 1), | |||
| 720 | + | } | |||
| 721 | + | ch := core.Channels{Client: h.client, Input: h.input, Output: h.output, Commands: h.commands, Scenes: h.scenes} | |||
| 722 | + | c, err := core.New(cfg, ch, core.Options{HintDisabled: func() { noted.Add(1) }}) | |||
| 723 | + | if err != nil { | |||
| 724 | + | t.Fatal(err) | |||
| 725 | + | } | |||
| 726 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 727 | + | done := make(chan error, 1) | |||
| 728 | + | go func() { done <- c.Run(ctx) }() | |||
| 729 | + | t.Cleanup(func() { cancel(); receive(t, done) }) | |||
| 730 | + | h.output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 1000, Height: 800}} | |||
| 731 | + | scene(t, h.scenes) | |||
| 732 | + | for range 3 { | |||
| 733 | + | h.press("g", ports.ModSuper) | |||
| 734 | + | h.release("g", 0) | |||
| 735 | + | } | |||
| 736 | + | c.Ping(ctx) | |||
| 737 | + | if got := noted.Load(); got != 1 { | |||
| 738 | + | t.Fatalf("disabled note ran %d times, want 1", got) | |||
| 739 | + | } | |||
| 740 | + | } | |||