detached 3993b47

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
10 files · +85 -48
M .golangci.yml +6 -0
@@ -149,6 +149,12 @@ linters:
149149 - text: "G304:"
150150 linters:
151151 - gosec
152+ - text: "G703:"
153+ linters:
154+ - gosec
155+ - text: "G704:"
156+ linters:
157+ - gosec
152158 # GTK callbacks require unused parameters
153159 - path: internal/ui/
154160 text: "unused-parameter"
@@ -149,6 +149,12 @@ linters:
149 - text: "G304:"149 - text: "G304:"
150 linters:150 linters:
151 - gosec151 - gosec
152+ - text: "G703:"
153+ linters:
154+ - gosec
155+ - text: "G704:"
156+ linters:
157+ - gosec
152 # GTK callbacks require unused parameters158 # 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 @@
11 # Makefile for dumber (Clean Architecture - puregotk)
22
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
44
55 # Load local overrides from .env.local if present (Makefile syntax)
66 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-purego3+.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
2424 return
2525 }
2626 _, _ = 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+ }
2830 }
2931
3032 // ApplyGTKIMModuleFallbackDefault calls ApplyGTKIMModuleFallback with os.Getenv and os.Setenv.
@@ -24,7 +24,9 @@ func ApplyGTKIMModuleFallback(stderr io.Writer, getter func(string) string, sett
24 return24 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 {
407407 }
408408
409409 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
411410 if err := os.WriteFile(shutdownMarkerPath(lockDir, sessionID), content, markerFilePerm); err != nil {
412411 return err
413412 }
@@ -457,7 +456,6 @@ func markAbruptExits(lockDir string, detectedAt time.Time, logger *zerolog.Logge
457456 payload += "ppid=" + ppid + "\n"
458457 }
459458
460- //nolint:gosec // G703: path is derived from the app lock dir plus an internally generated session id
461459 if err := os.WriteFile(abruptMarkerPath(lockDir, sessionID), []byte(payload), markerFilePerm); err != nil {
462460 return abruptSessions, err
463461 }
@@ -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 err411 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, err460 return abruptSessions, err
463 }461 }
M internal/infrastructure/config/xdg.go +0 -1
@@ -189,7 +189,6 @@ func EnsureDirectories() error {
189189 }
190190
191191 for _, dir := range directories {
192- //nolint:gosec // G703: XDG directories come from the user environment/config and are created intentionally
193192 if err := os.MkdirAll(dir, dirPerm); err != nil {
194193 return err
195194 }
@@ -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 err193 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 {
160160 return fmt.Errorf("failed to read new binary: %w", err)
161161 }
162162
163- // Write to staging location.
163+ // Write to staging location atomically via temp file.
164164 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()
167177 return fmt.Errorf("failed to write staged binary: %w", err)
168178 }
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+ }
169197
170198 log.Info().
171199 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 dir165+ 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(
363363 if attempt > maxRetryAttempts {
364364 return nil, fmt.Errorf("request failed after retries")
365365 }
366- //nolint:gosec // G107: callers validate or construct trusted GitHub request URLs before passing them here
367366 resp, err := client.Do(req)
368367 if err != nil {
369368 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 (
44 "context"
55 "encoding/json"
66 "fmt"
7+ "unicode/utf8"
78
89 "github.com/bnema/dumber/internal/application/port"
910 "github.com/bnema/dumber/internal/application/usecase"
@@ -27,7 +28,6 @@ type Config struct {
2728 Clipboard port.Clipboard
2829 ConfigGetter func() *config.Config
2930 OnClipboardCopied func(textLen int) // Called when auto-copy completes (for toast notification)
30- AccentHandler AccentKeyHandler // Handles accent key press/release events from WebView inputs
3131 }
3232
3333 // RegisterAll registers all message handlers with the router.
@@ -90,8 +90,8 @@ func RegisterAccentHandlers(ctx context.Context, router *webkit.MessageRouter, h
9090 if err := json.Unmarshal(payload, &p); err != nil {
9191 return nil, err
9292 }
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)
9595 }
9696 return nil, nil
9797 },
@@ -107,8 +107,8 @@ func RegisterAccentHandlers(ctx context.Context, router *webkit.MessageRouter, h
107107 if err := json.Unmarshal(payload, &p); err != nil {
108108 return nil, err
109109 }
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)
112112 }
113113 return nil, nil
114114 },
@@ -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.Clipboard28 Clipboard port.Clipboard
28 ConfigGetter func() *config.Config29 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, err91 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, nil96 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, err108 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, nil113 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) {
2525 assert.Contains(t, err.Error(), "handler")
2626 }
2727
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+
2836 // stubAccentHandler satisfies AccentKeyHandler for tests.
2937 type stubAccentHandler struct{}
3038
@@ -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) {
710710 )
711711 }
712712
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+
713740 func (a *App) initOmniboxConfig(ctx context.Context) {
714741 if a.deps == nil || a.deps.Config == nil {
715742 return
@@ -755,25 +782,10 @@ func (a *App) initOmniboxConfig(ctx context.Context) {
755782 }
756783 },
757784 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)
769786 },
770787 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)
777789 },
778790 }
779791 a.navCoord.SetOmniboxProvider(a)
@@ -806,25 +818,10 @@ func (a *App) initFindBarConfig(ctx context.Context) {
806818 }
807819 },
808820 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)
820822 },
821823 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)
828825 },
829826 }
830827 }
@@ -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 return742 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 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close