fix: resolve CI lint failures and harden review findings
- Remove 5 unused //nolint:gosec directives flagged by nolintlint - Exclude gosec G703/G704 in .golangci.yml (matches existing pattern) - Log setter error in ApplyGTKIMModuleFallback instead of discarding - Replace direct os.WriteFile with atomic temp+rename in applier - Fix UTF-8 unsafe len(p.Char)==1 with utf8.DecodeRuneInString - Remove dead Config.AccentHandler field from handler config - Extract shared accent key helpers in app.go (deduplicate) - Add TestRegisterAccentHandlers_Success happy-path test - Add check to Makefile .PHONY declaration
- commit
- 3993b47996fea60c32120101799a83d2c04136fb
- author
- brice · · 2026-03-12 05:45 UTC
- parent
- b574bab
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
10 files · +85 -48
M .golangci.yml +6 -0
| @@ -149,6 +149,12 @@ linters: | |||
| 149 | 149 | - text: "G304:" | |
| 150 | 150 | linters: | |
| 151 | 151 | - gosec | |
| 152 | + | - text: "G703:" | |
| 153 | + | linters: | |
| 154 | + | - gosec | |
| 155 | + | - text: "G704:" | |
| 156 | + | linters: | |
| 157 | + | - gosec | |
| 152 | 158 | # GTK callbacks require unused parameters | |
| 153 | 159 | - path: internal/ui/ | |
| 154 | 160 | text: "unused-parameter" | |
| @@ -149,6 +149,12 @@ linters: | |||||
| 149 | - text: "G304:" | 149 | - text: "G304:" | ||
| 150 | linters: | 150 | linters: | ||
| 151 | - gosec | 151 | - gosec | ||
| 152 | + | - text: "G703:" | |||
| 153 | + | linters: | |||
| 154 | + | - gosec | |||
| 155 | + | - text: "G704:" | |||
| 156 | + | linters: | |||
| 157 | + | - gosec | |||
| 152 | # GTK callbacks require unused parameters | 158 | # GTK callbacks require unused parameters | ||
| 153 | - path: internal/ui/ | 159 | - path: internal/ui/ | ||
| 154 | text: "unused-parameter" | 160 | text: "unused-parameter" | ||
M Makefile +1 -1
| @@ -1,6 +1,6 @@ | |||
| 1 | 1 | # Makefile for dumber (Clean Architecture - puregotk) | |
| 2 | 2 | ||
| 3 | - | .PHONY: build build-frontend test lint clean install-tools dev generate help init man flatpak-deps flatpak-build flatpak-install flatpak-run flatpak-clean stress-omnibox-callbacks verify-purego | |
| 3 | + | .PHONY: build build-frontend test lint clean install-tools dev generate help init man flatpak-deps flatpak-build flatpak-install flatpak-run flatpak-clean stress-omnibox-callbacks verify-purego check | |
| 4 | 4 | ||
| 5 | 5 | # Load local overrides from .env.local if present (Makefile syntax) | |
| 6 | 6 | ifneq (,$(wildcard .env.local)) | |
| @@ -1,6 +1,6 @@ | |||||
| 1 | # Makefile for dumber (Clean Architecture - puregotk) | 1 | # Makefile for dumber (Clean Architecture - puregotk) | ||
| 2 | 2 | ||||
| 3 | - | .PHONY: build build-frontend test lint clean install-tools dev generate help init man flatpak-deps flatpak-build flatpak-install flatpak-run flatpak-clean stress-omnibox-callbacks verify-purego | 3 | + | .PHONY: build build-frontend test lint clean install-tools dev generate help init man flatpak-deps flatpak-build flatpak-install flatpak-run flatpak-clean stress-omnibox-callbacks verify-purego check |
| 4 | 4 | ||||
| 5 | # Load local overrides from .env.local if present (Makefile syntax) | 5 | # Load local overrides from .env.local if present (Makefile syntax) | ||
| 6 | ifneq (,$(wildcard .env.local)) | 6 | ifneq (,$(wildcard .env.local)) | ||
M internal/bootstrap/runtime_env.go +3 -1
| @@ -24,7 +24,9 @@ func ApplyGTKIMModuleFallback(stderr io.Writer, getter func(string) string, sett | |||
| 24 | 24 | return | |
| 25 | 25 | } | |
| 26 | 26 | _, _ = fmt.Fprintf(stderr, "dumber: GTK_IM_MODULE unset, defaulting to gtk-im-context-simple for dead-key support\n") | |
| 27 | - | _ = setter("GTK_IM_MODULE", "gtk-im-context-simple") | |
| 27 | + | if err := setter("GTK_IM_MODULE", "gtk-im-context-simple"); err != nil { | |
| 28 | + | _, _ = fmt.Fprintf(stderr, "dumber: failed to set GTK_IM_MODULE: %v\n", err) | |
| 29 | + | } | |
| 28 | 30 | } | |
| 29 | 31 | ||
| 30 | 32 | // ApplyGTKIMModuleFallbackDefault calls ApplyGTKIMModuleFallback with os.Getenv and os.Setenv. | |
| @@ -24,7 +24,9 @@ func ApplyGTKIMModuleFallback(stderr io.Writer, getter func(string) string, sett | |||||
| 24 | return | 24 | return | ||
| 25 | } | 25 | } | ||
| 26 | _, _ = fmt.Fprintf(stderr, "dumber: GTK_IM_MODULE unset, defaulting to gtk-im-context-simple for dead-key support\n") | 26 | _, _ = fmt.Fprintf(stderr, "dumber: GTK_IM_MODULE unset, defaulting to gtk-im-context-simple for dead-key support\n") | ||
| 27 | - | _ = setter("GTK_IM_MODULE", "gtk-im-context-simple") | 27 | + | if err := setter("GTK_IM_MODULE", "gtk-im-context-simple"); err != nil { |
| 28 | + | _, _ = fmt.Fprintf(stderr, "dumber: failed to set GTK_IM_MODULE: %v\n", err) | |||
| 29 | + | } | |||
| 28 | } | 30 | } | ||
| 29 | 31 | ||||
| 30 | // ApplyGTKIMModuleFallbackDefault calls ApplyGTKIMModuleFallback with os.Getenv and os.Setenv. | 32 | // ApplyGTKIMModuleFallbackDefault calls ApplyGTKIMModuleFallback with os.Getenv and os.Setenv. | ||
M internal/bootstrap/session.go +0 -2
| @@ -407,7 +407,6 @@ func writeShutdownMarker(lockDir, sessionID string, endedAt time.Time) error { | |||
| 407 | 407 | } | |
| 408 | 408 | ||
| 409 | 409 | content := []byte(endedAt.Format(time.RFC3339Nano) + "\n" + startupLine + pidLine + ppidLine) | |
| 410 | - | //nolint:gosec // G703: path is derived from the app lock dir plus an internally generated session id | |
| 411 | 410 | if err := os.WriteFile(shutdownMarkerPath(lockDir, sessionID), content, markerFilePerm); err != nil { | |
| 412 | 411 | return err | |
| 413 | 412 | } | |
| @@ -457,7 +456,6 @@ func markAbruptExits(lockDir string, detectedAt time.Time, logger *zerolog.Logge | |||
| 457 | 456 | payload += "ppid=" + ppid + "\n" | |
| 458 | 457 | } | |
| 459 | 458 | ||
| 460 | - | //nolint:gosec // G703: path is derived from the app lock dir plus an internally generated session id | |
| 461 | 459 | if err := os.WriteFile(abruptMarkerPath(lockDir, sessionID), []byte(payload), markerFilePerm); err != nil { | |
| 462 | 460 | return abruptSessions, err | |
| 463 | 461 | } | |
| @@ -407,7 +407,6 @@ func writeShutdownMarker(lockDir, sessionID string, endedAt time.Time) error { | |||||
| 407 | } | 407 | } | ||
| 408 | 408 | ||||
| 409 | content := []byte(endedAt.Format(time.RFC3339Nano) + "\n" + startupLine + pidLine + ppidLine) | 409 | content := []byte(endedAt.Format(time.RFC3339Nano) + "\n" + startupLine + pidLine + ppidLine) | ||
| 410 | - | //nolint:gosec // G703: path is derived from the app lock dir plus an internally generated session id | |||
| 411 | if err := os.WriteFile(shutdownMarkerPath(lockDir, sessionID), content, markerFilePerm); err != nil { | 410 | if err := os.WriteFile(shutdownMarkerPath(lockDir, sessionID), content, markerFilePerm); err != nil { | ||
| 412 | return err | 411 | return err | ||
| 413 | } | 412 | } | ||
| @@ -457,7 +456,6 @@ func markAbruptExits(lockDir string, detectedAt time.Time, logger *zerolog.Logge | |||||
| 457 | payload += "ppid=" + ppid + "\n" | 456 | payload += "ppid=" + ppid + "\n" | ||
| 458 | } | 457 | } | ||
| 459 | 458 | ||||
| 460 | - | //nolint:gosec // G703: path is derived from the app lock dir plus an internally generated session id | |||
| 461 | if err := os.WriteFile(abruptMarkerPath(lockDir, sessionID), []byte(payload), markerFilePerm); err != nil { | 459 | if err := os.WriteFile(abruptMarkerPath(lockDir, sessionID), []byte(payload), markerFilePerm); err != nil { | ||
| 462 | return abruptSessions, err | 460 | return abruptSessions, err | ||
| 463 | } | 461 | } | ||
M internal/infrastructure/config/xdg.go +0 -1
| @@ -189,7 +189,6 @@ func EnsureDirectories() error { | |||
| 189 | 189 | } | |
| 190 | 190 | ||
| 191 | 191 | for _, dir := range directories { | |
| 192 | - | //nolint:gosec // G703: XDG directories come from the user environment/config and are created intentionally | |
| 193 | 192 | if err := os.MkdirAll(dir, dirPerm); err != nil { | |
| 194 | 193 | return err | |
| 195 | 194 | } | |
| @@ -189,7 +189,6 @@ func EnsureDirectories() error { | |||||
| 189 | } | 189 | } | ||
| 190 | 190 | ||||
| 191 | for _, dir := range directories { | 191 | for _, dir := range directories { | ||
| 192 | - | //nolint:gosec // G703: XDG directories come from the user environment/config and are created intentionally | |||
| 193 | if err := os.MkdirAll(dir, dirPerm); err != nil { | 192 | if err := os.MkdirAll(dir, dirPerm); err != nil { | ||
| 194 | return err | 193 | return err | ||
| 195 | } | 194 | } | ||
M internal/infrastructure/updater/applier.go +31 -3
| @@ -160,12 +160,40 @@ func (a *Applier) StageUpdate(ctx context.Context, newBinaryPath string) error { | |||
| 160 | 160 | return fmt.Errorf("failed to read new binary: %w", err) | |
| 161 | 161 | } | |
| 162 | 162 | ||
| 163 | - | // Write to staging location. | |
| 163 | + | // Write to staging location atomically via temp file. | |
| 164 | 164 | stagedPath := a.stagedBinaryPath() | |
| 165 | - | //nolint:gosec // G703: staged path is the app-managed pending-update path under the configured state dir | |
| 166 | - | if err := os.WriteFile(stagedPath, newBinary, applierExecPerm); err != nil { | |
| 165 | + | dir := filepath.Dir(stagedPath) | |
| 166 | + | tmpFile, err := os.CreateTemp(dir, ".staged-update-*") | |
| 167 | + | if err != nil { | |
| 168 | + | return fmt.Errorf("failed to create temp file for staged binary: %w", err) | |
| 169 | + | } | |
| 170 | + | tmpPath := tmpFile.Name() | |
| 171 | + | // Ensure temp file is cleaned up on any failure. | |
| 172 | + | cleanup := func() { _ = os.Remove(tmpPath) } | |
| 173 | + | ||
| 174 | + | if _, err := tmpFile.Write(newBinary); err != nil { | |
| 175 | + | _ = tmpFile.Close() | |
| 176 | + | cleanup() | |
| 167 | 177 | return fmt.Errorf("failed to write staged binary: %w", err) | |
| 168 | 178 | } | |
| 179 | + | if err := tmpFile.Chmod(applierExecPerm); err != nil { | |
| 180 | + | _ = tmpFile.Close() | |
| 181 | + | cleanup() | |
| 182 | + | return fmt.Errorf("failed to chmod staged binary: %w", err) | |
| 183 | + | } | |
| 184 | + | if err := tmpFile.Sync(); err != nil { | |
| 185 | + | _ = tmpFile.Close() | |
| 186 | + | cleanup() | |
| 187 | + | return fmt.Errorf("failed to sync staged binary: %w", err) | |
| 188 | + | } | |
| 189 | + | if err := tmpFile.Close(); err != nil { | |
| 190 | + | cleanup() | |
| 191 | + | return fmt.Errorf("failed to close staged binary temp file: %w", err) | |
| 192 | + | } | |
| 193 | + | if err := os.Rename(tmpPath, stagedPath); err != nil { | |
| 194 | + | cleanup() | |
| 195 | + | return fmt.Errorf("failed to rename staged binary into place: %w", err) | |
| 196 | + | } | |
| 169 | 197 | ||
| 170 | 198 | log.Info(). | |
| 171 | 199 | Str("staged_path", stagedPath). | |
| @@ -160,12 +160,40 @@ func (a *Applier) StageUpdate(ctx context.Context, newBinaryPath string) error { | |||||
| 160 | return fmt.Errorf("failed to read new binary: %w", err) | 160 | return fmt.Errorf("failed to read new binary: %w", err) | ||
| 161 | } | 161 | } | ||
| 162 | 162 | ||||
| 163 | - | // Write to staging location. | 163 | + | // Write to staging location atomically via temp file. |
| 164 | stagedPath := a.stagedBinaryPath() | 164 | stagedPath := a.stagedBinaryPath() | ||
| 165 | - | //nolint:gosec // G703: staged path is the app-managed pending-update path under the configured state dir | 165 | + | dir := filepath.Dir(stagedPath) |
| 166 | - | if err := os.WriteFile(stagedPath, newBinary, applierExecPerm); err != nil { | 166 | + | tmpFile, err := os.CreateTemp(dir, ".staged-update-*") |
| 167 | + | if err != nil { | |||
| 168 | + | return fmt.Errorf("failed to create temp file for staged binary: %w", err) | |||
| 169 | + | } | |||
| 170 | + | tmpPath := tmpFile.Name() | |||
| 171 | + | // Ensure temp file is cleaned up on any failure. | |||
| 172 | + | cleanup := func() { _ = os.Remove(tmpPath) } | |||
| 173 | + | ||||
| 174 | + | if _, err := tmpFile.Write(newBinary); err != nil { | |||
| 175 | + | _ = tmpFile.Close() | |||
| 176 | + | cleanup() | |||
| 167 | return fmt.Errorf("failed to write staged binary: %w", err) | 177 | return fmt.Errorf("failed to write staged binary: %w", err) | ||
| 168 | } | 178 | } | ||
| 179 | + | if err := tmpFile.Chmod(applierExecPerm); err != nil { | |||
| 180 | + | _ = tmpFile.Close() | |||
| 181 | + | cleanup() | |||
| 182 | + | return fmt.Errorf("failed to chmod staged binary: %w", err) | |||
| 183 | + | } | |||
| 184 | + | if err := tmpFile.Sync(); err != nil { | |||
| 185 | + | _ = tmpFile.Close() | |||
| 186 | + | cleanup() | |||
| 187 | + | return fmt.Errorf("failed to sync staged binary: %w", err) | |||
| 188 | + | } | |||
| 189 | + | if err := tmpFile.Close(); err != nil { | |||
| 190 | + | cleanup() | |||
| 191 | + | return fmt.Errorf("failed to close staged binary temp file: %w", err) | |||
| 192 | + | } | |||
| 193 | + | if err := os.Rename(tmpPath, stagedPath); err != nil { | |||
| 194 | + | cleanup() | |||
| 195 | + | return fmt.Errorf("failed to rename staged binary into place: %w", err) | |||
| 196 | + | } | |||
| 169 | 197 | ||||
| 170 | log.Info(). | 198 | log.Info(). | ||
| 171 | Str("staged_path", stagedPath). | 199 | Str("staged_path", stagedPath). | ||
M internal/infrastructure/updater/github.go +0 -1
| @@ -363,7 +363,6 @@ func doRequestWithRetryHelper( | |||
| 363 | 363 | if attempt > maxRetryAttempts { | |
| 364 | 364 | return nil, fmt.Errorf("request failed after retries") | |
| 365 | 365 | } | |
| 366 | - | //nolint:gosec // G107: callers validate or construct trusted GitHub request URLs before passing them here | |
| 367 | 366 | resp, err := client.Do(req) | |
| 368 | 367 | if err != nil { | |
| 369 | 368 | if !isRetryableRequestError(err) || attempt == maxRetryAttempts { | |
| @@ -363,7 +363,6 @@ func doRequestWithRetryHelper( | |||||
| 363 | if attempt > maxRetryAttempts { | 363 | if attempt > maxRetryAttempts { | ||
| 364 | return nil, fmt.Errorf("request failed after retries") | 364 | return nil, fmt.Errorf("request failed after retries") | ||
| 365 | } | 365 | } | ||
| 366 | - | //nolint:gosec // G107: callers validate or construct trusted GitHub request URLs before passing them here | |||
| 367 | resp, err := client.Do(req) | 366 | resp, err := client.Do(req) | ||
| 368 | if err != nil { | 367 | if err != nil { | ||
| 369 | if !isRetryableRequestError(err) || attempt == maxRetryAttempts { | 368 | if !isRetryableRequestError(err) || attempt == maxRetryAttempts { | ||
M internal/infrastructure/webkit/handlers/register.go +5 -5
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "encoding/json" | |
| 6 | 6 | "fmt" | |
| 7 | + | "unicode/utf8" | |
| 7 | 8 | ||
| 8 | 9 | "github.com/bnema/dumber/internal/application/port" | |
| 9 | 10 | "github.com/bnema/dumber/internal/application/usecase" | |
| @@ -27,7 +28,6 @@ type Config struct { | |||
| 27 | 28 | Clipboard port.Clipboard | |
| 28 | 29 | ConfigGetter func() *config.Config | |
| 29 | 30 | OnClipboardCopied func(textLen int) // Called when auto-copy completes (for toast notification) | |
| 30 | - | AccentHandler AccentKeyHandler // Handles accent key press/release events from WebView inputs | |
| 31 | 31 | } | |
| 32 | 32 | ||
| 33 | 33 | // RegisterAll registers all message handlers with the router. | |
| @@ -90,8 +90,8 @@ func RegisterAccentHandlers(ctx context.Context, router *webkit.MessageRouter, h | |||
| 90 | 90 | if err := json.Unmarshal(payload, &p); err != nil { | |
| 91 | 91 | return nil, err | |
| 92 | 92 | } | |
| 93 | - | if len(p.Char) == 1 { | |
| 94 | - | handler.OnKeyPressed(ctx, rune(p.Char[0]), p.Shift) | |
| 93 | + | if r, _ := utf8.DecodeRuneInString(p.Char); r != utf8.RuneError && utf8.RuneCountInString(p.Char) == 1 { | |
| 94 | + | handler.OnKeyPressed(ctx, r, p.Shift) | |
| 95 | 95 | } | |
| 96 | 96 | return nil, nil | |
| 97 | 97 | }, | |
| @@ -107,8 +107,8 @@ func RegisterAccentHandlers(ctx context.Context, router *webkit.MessageRouter, h | |||
| 107 | 107 | if err := json.Unmarshal(payload, &p); err != nil { | |
| 108 | 108 | return nil, err | |
| 109 | 109 | } | |
| 110 | - | if len(p.Char) == 1 { | |
| 111 | - | handler.OnKeyReleased(ctx, rune(p.Char[0])) | |
| 110 | + | if r, _ := utf8.DecodeRuneInString(p.Char); r != utf8.RuneError && utf8.RuneCountInString(p.Char) == 1 { | |
| 111 | + | handler.OnKeyReleased(ctx, r) | |
| 112 | 112 | } | |
| 113 | 113 | return nil, nil | |
| 114 | 114 | }, | |
| @@ -4,6 +4,7 @@ import ( | |||||
| 4 | "context" | 4 | "context" | ||
| 5 | "encoding/json" | 5 | "encoding/json" | ||
| 6 | "fmt" | 6 | "fmt" | ||
| 7 | + | "unicode/utf8" | |||
| 7 | 8 | ||||
| 8 | "github.com/bnema/dumber/internal/application/port" | 9 | "github.com/bnema/dumber/internal/application/port" | ||
| 9 | "github.com/bnema/dumber/internal/application/usecase" | 10 | "github.com/bnema/dumber/internal/application/usecase" | ||
| @@ -27,7 +28,6 @@ type Config struct { | |||||
| 27 | Clipboard port.Clipboard | 28 | Clipboard port.Clipboard | ||
| 28 | ConfigGetter func() *config.Config | 29 | ConfigGetter func() *config.Config | ||
| 29 | OnClipboardCopied func(textLen int) // Called when auto-copy completes (for toast notification) | 30 | OnClipboardCopied func(textLen int) // Called when auto-copy completes (for toast notification) | ||
| 30 | - | AccentHandler AccentKeyHandler // Handles accent key press/release events from WebView inputs | |||
| 31 | } | 31 | } | ||
| 32 | 32 | ||||
| 33 | // RegisterAll registers all message handlers with the router. | 33 | // RegisterAll registers all message handlers with the router. | ||
| @@ -90,8 +90,8 @@ func RegisterAccentHandlers(ctx context.Context, router *webkit.MessageRouter, h | |||||
| 90 | if err := json.Unmarshal(payload, &p); err != nil { | 90 | if err := json.Unmarshal(payload, &p); err != nil { | ||
| 91 | return nil, err | 91 | return nil, err | ||
| 92 | } | 92 | } | ||
| 93 | - | if len(p.Char) == 1 { | 93 | + | if r, _ := utf8.DecodeRuneInString(p.Char); r != utf8.RuneError && utf8.RuneCountInString(p.Char) == 1 { |
| 94 | - | handler.OnKeyPressed(ctx, rune(p.Char[0]), p.Shift) | 94 | + | handler.OnKeyPressed(ctx, r, p.Shift) |
| 95 | } | 95 | } | ||
| 96 | return nil, nil | 96 | return nil, nil | ||
| 97 | }, | 97 | }, | ||
| @@ -107,8 +107,8 @@ func RegisterAccentHandlers(ctx context.Context, router *webkit.MessageRouter, h | |||||
| 107 | if err := json.Unmarshal(payload, &p); err != nil { | 107 | if err := json.Unmarshal(payload, &p); err != nil { | ||
| 108 | return nil, err | 108 | return nil, err | ||
| 109 | } | 109 | } | ||
| 110 | - | if len(p.Char) == 1 { | 110 | + | if r, _ := utf8.DecodeRuneInString(p.Char); r != utf8.RuneError && utf8.RuneCountInString(p.Char) == 1 { |
| 111 | - | handler.OnKeyReleased(ctx, rune(p.Char[0])) | 111 | + | handler.OnKeyReleased(ctx, r) |
| 112 | } | 112 | } | ||
| 113 | return nil, nil | 113 | return nil, nil | ||
| 114 | }, | 114 | }, | ||
M internal/infrastructure/webkit/handlers/register_test.go +8 -0
| @@ -25,6 +25,14 @@ func TestRegisterAccentHandlers_NilHandler(t *testing.T) { | |||
| 25 | 25 | assert.Contains(t, err.Error(), "handler") | |
| 26 | 26 | } | |
| 27 | 27 | ||
| 28 | + | func TestRegisterAccentHandlers_Success(t *testing.T) { | |
| 29 | + | ctx := context.Background() | |
| 30 | + | router := webkit.NewMessageRouter(ctx) | |
| 31 | + | handler := &stubAccentHandler{} | |
| 32 | + | err := RegisterAccentHandlers(ctx, router, handler) | |
| 33 | + | require.NoError(t, err) | |
| 34 | + | } | |
| 35 | + | ||
| 28 | 36 | // stubAccentHandler satisfies AccentKeyHandler for tests. | |
| 29 | 37 | type stubAccentHandler struct{} | |
| 30 | 38 | ||
| @@ -25,6 +25,14 @@ func TestRegisterAccentHandlers_NilHandler(t *testing.T) { | |||||
| 25 | assert.Contains(t, err.Error(), "handler") | 25 | assert.Contains(t, err.Error(), "handler") | ||
| 26 | } | 26 | } | ||
| 27 | 27 | ||||
| 28 | + | func TestRegisterAccentHandlers_Success(t *testing.T) { | |||
| 29 | + | ctx := context.Background() | |||
| 30 | + | router := webkit.NewMessageRouter(ctx) | |||
| 31 | + | handler := &stubAccentHandler{} | |||
| 32 | + | err := RegisterAccentHandlers(ctx, router, handler) | |||
| 33 | + | require.NoError(t, err) | |||
| 34 | + | } | |||
| 35 | + | ||||
| 28 | // stubAccentHandler satisfies AccentKeyHandler for tests. | 36 | // stubAccentHandler satisfies AccentKeyHandler for tests. | ||
| 29 | type stubAccentHandler struct{} | 37 | type stubAccentHandler struct{} | ||
| 30 | 38 | ||||
M internal/ui/app.go +31 -34
| @@ -710,6 +710,33 @@ func (a *App) initKeyboardHandler(ctx context.Context) { | |||
| 710 | 710 | ) | |
| 711 | 711 | } | |
| 712 | 712 | ||
| 713 | + | // handleAccentKeyPress handles accent key press events for GTK entry widgets | |
| 714 | + | // (omnibox and find bar). Returns true if the key was consumed. | |
| 715 | + | func (a *App) handleAccentKeyPress(ctx context.Context, keyval uint, state gdk.ModifierType) bool { | |
| 716 | + | if a.insertAccentUC == nil { | |
| 717 | + | return false | |
| 718 | + | } | |
| 719 | + | if state&(gdk.ControlMaskValue|gdk.AltMaskValue) != 0 { | |
| 720 | + | return false | |
| 721 | + | } | |
| 722 | + | shiftHeld := state&gdk.ShiftMaskValue != 0 | |
| 723 | + | if char := input.KeyvalToRune(keyval); char != 0 { | |
| 724 | + | return a.insertAccentUC.OnKeyPressed(ctx, char, shiftHeld) | |
| 725 | + | } | |
| 726 | + | return false | |
| 727 | + | } | |
| 728 | + | ||
| 729 | + | // handleAccentKeyRelease handles accent key release events for GTK entry widgets | |
| 730 | + | // (omnibox and find bar). | |
| 731 | + | func (a *App) handleAccentKeyRelease(ctx context.Context, keyval uint) { | |
| 732 | + | if a.insertAccentUC == nil { | |
| 733 | + | return | |
| 734 | + | } | |
| 735 | + | if char := input.KeyvalToRune(keyval); char != 0 { | |
| 736 | + | a.insertAccentUC.OnKeyReleased(ctx, char) | |
| 737 | + | } | |
| 738 | + | } | |
| 739 | + | ||
| 713 | 740 | func (a *App) initOmniboxConfig(ctx context.Context) { | |
| 714 | 741 | if a.deps == nil || a.deps.Config == nil { | |
| 715 | 742 | return | |
| @@ -755,25 +782,10 @@ func (a *App) initOmniboxConfig(ctx context.Context) { | |||
| 755 | 782 | } | |
| 756 | 783 | }, | |
| 757 | 784 | OnAccentKeyPress: func(keyval uint, state gdk.ModifierType) bool { | |
| 758 | - | if a.insertAccentUC == nil { | |
| 759 | - | return false | |
| 760 | - | } | |
| 761 | - | if state&(gdk.ControlMaskValue|gdk.AltMaskValue) != 0 { | |
| 762 | - | return false | |
| 763 | - | } | |
| 764 | - | shiftHeld := state&gdk.ShiftMaskValue != 0 | |
| 765 | - | if char := input.KeyvalToRune(keyval); char != 0 { | |
| 766 | - | return a.insertAccentUC.OnKeyPressed(ctx, char, shiftHeld) | |
| 767 | - | } | |
| 768 | - | return false | |
| 785 | + | return a.handleAccentKeyPress(ctx, keyval, state) | |
| 769 | 786 | }, | |
| 770 | 787 | OnAccentKeyRelease: func(keyval uint) { | |
| 771 | - | if a.insertAccentUC == nil { | |
| 772 | - | return | |
| 773 | - | } | |
| 774 | - | if char := input.KeyvalToRune(keyval); char != 0 { | |
| 775 | - | a.insertAccentUC.OnKeyReleased(ctx, char) | |
| 776 | - | } | |
| 788 | + | a.handleAccentKeyRelease(ctx, keyval) | |
| 777 | 789 | }, | |
| 778 | 790 | } | |
| 779 | 791 | a.navCoord.SetOmniboxProvider(a) | |
| @@ -806,25 +818,10 @@ func (a *App) initFindBarConfig(ctx context.Context) { | |||
| 806 | 818 | } | |
| 807 | 819 | }, | |
| 808 | 820 | OnAccentKeyPress: func(keyval uint, state gdk.ModifierType) bool { | |
| 809 | - | if a.insertAccentUC == nil { | |
| 810 | - | return false | |
| 811 | - | } | |
| 812 | - | if state&(gdk.ControlMaskValue|gdk.AltMaskValue) != 0 { | |
| 813 | - | return false | |
| 814 | - | } | |
| 815 | - | shiftHeld := state&gdk.ShiftMaskValue != 0 | |
| 816 | - | if char := input.KeyvalToRune(keyval); char != 0 { | |
| 817 | - | return a.insertAccentUC.OnKeyPressed(ctx, char, shiftHeld) | |
| 818 | - | } | |
| 819 | - | return false | |
| 821 | + | return a.handleAccentKeyPress(ctx, keyval, state) | |
| 820 | 822 | }, | |
| 821 | 823 | OnAccentKeyRelease: func(keyval uint) { | |
| 822 | - | if a.insertAccentUC == nil { | |
| 823 | - | return | |
| 824 | - | } | |
| 825 | - | if char := input.KeyvalToRune(keyval); char != 0 { | |
| 826 | - | a.insertAccentUC.OnKeyReleased(ctx, char) | |
| 827 | - | } | |
| 824 | + | a.handleAccentKeyRelease(ctx, keyval) | |
| 828 | 825 | }, | |
| 829 | 826 | } | |
| 830 | 827 | } | |
| @@ -710,6 +710,33 @@ func (a *App) initKeyboardHandler(ctx context.Context) { | |||||
| 710 | ) | 710 | ) | ||
| 711 | } | 711 | } | ||
| 712 | 712 | ||||
| 713 | + | // handleAccentKeyPress handles accent key press events for GTK entry widgets | |||
| 714 | + | // (omnibox and find bar). Returns true if the key was consumed. | |||
| 715 | + | func (a *App) handleAccentKeyPress(ctx context.Context, keyval uint, state gdk.ModifierType) bool { | |||
| 716 | + | if a.insertAccentUC == nil { | |||
| 717 | + | return false | |||
| 718 | + | } | |||
| 719 | + | if state&(gdk.ControlMaskValue|gdk.AltMaskValue) != 0 { | |||
| 720 | + | return false | |||
| 721 | + | } | |||
| 722 | + | shiftHeld := state&gdk.ShiftMaskValue != 0 | |||
| 723 | + | if char := input.KeyvalToRune(keyval); char != 0 { | |||
| 724 | + | return a.insertAccentUC.OnKeyPressed(ctx, char, shiftHeld) | |||
| 725 | + | } | |||
| 726 | + | return false | |||
| 727 | + | } | |||
| 728 | + | ||||
| 729 | + | // handleAccentKeyRelease handles accent key release events for GTK entry widgets | |||
| 730 | + | // (omnibox and find bar). | |||
| 731 | + | func (a *App) handleAccentKeyRelease(ctx context.Context, keyval uint) { | |||
| 732 | + | if a.insertAccentUC == nil { | |||
| 733 | + | return | |||
| 734 | + | } | |||
| 735 | + | if char := input.KeyvalToRune(keyval); char != 0 { | |||
| 736 | + | a.insertAccentUC.OnKeyReleased(ctx, char) | |||
| 737 | + | } | |||
| 738 | + | } | |||
| 739 | + | ||||
| 713 | func (a *App) initOmniboxConfig(ctx context.Context) { | 740 | func (a *App) initOmniboxConfig(ctx context.Context) { | ||
| 714 | if a.deps == nil || a.deps.Config == nil { | 741 | if a.deps == nil || a.deps.Config == nil { | ||
| 715 | return | 742 | return | ||
| @@ -755,25 +782,10 @@ func (a *App) initOmniboxConfig(ctx context.Context) { | |||||
| 755 | } | 782 | } | ||
| 756 | }, | 783 | }, | ||
| 757 | OnAccentKeyPress: func(keyval uint, state gdk.ModifierType) bool { | 784 | OnAccentKeyPress: func(keyval uint, state gdk.ModifierType) bool { | ||
| 758 | - | if a.insertAccentUC == nil { | 785 | + | return a.handleAccentKeyPress(ctx, keyval, state) |
| 759 | - | return false | |||
| 760 | - | } | |||
| 761 | - | if state&(gdk.ControlMaskValue|gdk.AltMaskValue) != 0 { | |||
| 762 | - | return false | |||
| 763 | - | } | |||
| 764 | - | shiftHeld := state&gdk.ShiftMaskValue != 0 | |||
| 765 | - | if char := input.KeyvalToRune(keyval); char != 0 { | |||
| 766 | - | return a.insertAccentUC.OnKeyPressed(ctx, char, shiftHeld) | |||
| 767 | - | } | |||
| 768 | - | return false | |||
| 769 | }, | 786 | }, | ||
| 770 | OnAccentKeyRelease: func(keyval uint) { | 787 | OnAccentKeyRelease: func(keyval uint) { | ||
| 771 | - | if a.insertAccentUC == nil { | 788 | + | a.handleAccentKeyRelease(ctx, keyval) |
| 772 | - | return | |||
| 773 | - | } | |||
| 774 | - | if char := input.KeyvalToRune(keyval); char != 0 { | |||
| 775 | - | a.insertAccentUC.OnKeyReleased(ctx, char) | |||
| 776 | - | } | |||
| 777 | }, | 789 | }, | ||
| 778 | } | 790 | } | ||
| 779 | a.navCoord.SetOmniboxProvider(a) | 791 | a.navCoord.SetOmniboxProvider(a) | ||
| @@ -806,25 +818,10 @@ func (a *App) initFindBarConfig(ctx context.Context) { | |||||
| 806 | } | 818 | } | ||
| 807 | }, | 819 | }, | ||
| 808 | OnAccentKeyPress: func(keyval uint, state gdk.ModifierType) bool { | 820 | OnAccentKeyPress: func(keyval uint, state gdk.ModifierType) bool { | ||
| 809 | - | if a.insertAccentUC == nil { | 821 | + | return a.handleAccentKeyPress(ctx, keyval, state) |
| 810 | - | return false | |||
| 811 | - | } | |||
| 812 | - | if state&(gdk.ControlMaskValue|gdk.AltMaskValue) != 0 { | |||
| 813 | - | return false | |||
| 814 | - | } | |||
| 815 | - | shiftHeld := state&gdk.ShiftMaskValue != 0 | |||
| 816 | - | if char := input.KeyvalToRune(keyval); char != 0 { | |||
| 817 | - | return a.insertAccentUC.OnKeyPressed(ctx, char, shiftHeld) | |||
| 818 | - | } | |||
| 819 | - | return false | |||
| 820 | }, | 822 | }, | ||
| 821 | OnAccentKeyRelease: func(keyval uint) { | 823 | OnAccentKeyRelease: func(keyval uint) { | ||
| 822 | - | if a.insertAccentUC == nil { | 824 | + | a.handleAccentKeyRelease(ctx, keyval) |
| 823 | - | return | |||
| 824 | - | } | |||
| 825 | - | if char := input.KeyvalToRune(keyval); char != 0 { | |||
| 826 | - | a.insertAccentUC.OnKeyReleased(ctx, char) | |||
| 827 | - | } | |||
| 828 | }, | 825 | }, | ||
| 829 | } | 826 | } | ||
| 830 | } | 827 | } | ||