detached da71e78

Apply go fix updates

commit
da71e7872ac3d5a0d85cdc16d0946f0bec946942
author
brice · · 2026-07-04 06:56 UTC
parent
3170de9
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
92 files · +228 -369
M cmd/dumber/main.go +2 -2
@@ -825,8 +825,8 @@ func isCEFSubprocess(args []string) bool {
825825 return false
826826 }
827827
828- if strings.HasPrefix(args[1], "--type=") {
829- value := strings.TrimPrefix(args[1], "--type=")
828+ if after, ok := strings.CutPrefix(args[1], "--type="); ok {
829+ value := after
830830 return value != "" && !strings.HasPrefix(value, "-")
831831 }
832832
@@ -825,8 +825,8 @@ func isCEFSubprocess(args []string) bool {
825 return false825 return false
826 }826 }
827 827
828- if strings.HasPrefix(args[1], "--type=") {828+ if after, ok := strings.CutPrefix(args[1], "--type="); ok {
829- value := strings.TrimPrefix(args[1], "--type=")829+ value := after
830 return value != "" && !strings.HasPrefix(value, "-")830 return value != "" && !strings.HasPrefix(value, "-")
831 }831 }
832 832
M internal/application/usecase/check_runtime_dependencies.go +2 -5
@@ -159,12 +159,9 @@ func compareVersion(a, b string) (cmp int, ok bool) {
159159 return 0, false
160160 }
161161
162- maxLen := len(av)
163- if len(bv) > maxLen {
164- maxLen = len(bv)
165- }
162+ maxLen := max(len(bv), len(av))
166163
167- for i := 0; i < maxLen; i++ {
164+ for i := range maxLen {
168165 x := 0
169166 if i < len(av) {
170167 x = av[i]
@@ -159,12 +159,9 @@ func compareVersion(a, b string) (cmp int, ok bool) {
159 return 0, false159 return 0, false
160 }160 }
161 161
162- maxLen := len(av)162+ maxLen := max(len(bv), len(av))
163- if len(bv) > maxLen {
164- maxLen = len(bv)
165- }
166 163
167- for i := 0; i < maxLen; i++ {164+ for i := range maxLen {
168 x := 0165 x := 0
169 if i < len(av) {166 if i < len(av) {
170 x = av[i]167 x = av[i]
M internal/application/usecase/favicon.go +2 -7
@@ -4,6 +4,7 @@ import (
44 "context"
55 "errors"
66 "fmt"
7+ "slices"
78 "sync"
89 "time"
910
@@ -395,13 +396,7 @@ func (uc *FaviconUseCase) observeFetched(ctx context.Context, requestedPageURL s
395396 return ErrFaviconMiss
396397 }
397398 if fetched.ResolvedKey != "" {
398- allowed := false
399- for _, candidate := range favicon.Candidates(requestedPageURL) {
400- if candidate == fetched.ResolvedKey {
401- allowed = true
402- break
403- }
404- }
399+ allowed := slices.Contains(favicon.Candidates(requestedPageURL), fetched.ResolvedKey)
405400 if !allowed {
406401 return ErrFaviconMiss
407402 }
@@ -4,6 +4,7 @@ import (
4 "context"4 "context"
5 "errors"5 "errors"
6 "fmt"6 "fmt"
7+ "slices"
7 "sync"8 "sync"
8 "time"9 "time"
9 10
@@ -395,13 +396,7 @@ func (uc *FaviconUseCase) observeFetched(ctx context.Context, requestedPageURL s
395 return ErrFaviconMiss396 return ErrFaviconMiss
396 }397 }
397 if fetched.ResolvedKey != "" {398 if fetched.ResolvedKey != "" {
398- allowed := false399+ allowed := slices.Contains(favicon.Candidates(requestedPageURL), fetched.ResolvedKey)
399- for _, candidate := range favicon.Candidates(requestedPageURL) {
400- if candidate == fetched.ResolvedKey {
401- allowed = true
402- break
403- }
404- }
405 if !allowed {400 if !allowed {
406 return ErrFaviconMiss401 return ErrFaviconMiss
407 }402 }
M internal/application/usecase/favicon_test.go +1 -1
@@ -128,7 +128,7 @@ func TestFaviconResolveRepairOrphanWithoutConverterReturnsMiss(t *testing.T) {
128128 func TestFaviconResolveStaleSchedulesDedupedBackgroundRefresh(t *testing.T) {
129129 fx := newFaviconFixture(t)
130130 fx.seed("example.com", []byte("old"), true)
131- for i := 0; i < 2; i++ {
131+ for i := range 2 {
132132 got, err := fx.uc.Resolve(context.Background(), "https://example.com", 32, ResolveOptions{ScheduleBackgroundRefresh: true})
133133 if err != nil {
134134 t.Fatal(err)
@@ -128,7 +128,7 @@ func TestFaviconResolveRepairOrphanWithoutConverterReturnsMiss(t *testing.T) {
128 func TestFaviconResolveStaleSchedulesDedupedBackgroundRefresh(t *testing.T) {128 func TestFaviconResolveStaleSchedulesDedupedBackgroundRefresh(t *testing.T) {
129 fx := newFaviconFixture(t)129 fx := newFaviconFixture(t)
130 fx.seed("example.com", []byte("old"), true)130 fx.seed("example.com", []byte("old"), true)
131- for i := 0; i < 2; i++ {131+ for i := range 2 {
132 got, err := fx.uc.Resolve(context.Background(), "https://example.com", 32, ResolveOptions{ScheduleBackgroundRefresh: true})132 got, err := fx.uc.Resolve(context.Background(), "https://example.com", 32, ResolveOptions{ScheduleBackgroundRefresh: true})
133 if err != nil {133 if err != nil {
134 t.Fatal(err)134 t.Fatal(err)
M internal/application/usecase/handle_permission.go +2 -6
@@ -4,6 +4,7 @@ package usecase
44 import (
55 "context"
66 "errors"
7+ "slices"
78 "sync"
89 "time"
910
@@ -59,12 +60,7 @@ func (uc *HandlePermissionUseCase) logger(ctx context.Context) *zerolog.Logger {
5960 }
6061
6162 func isWebsiteDataAccessPermission(permTypes []entity.PermissionType) bool {
62- for _, permType := range permTypes {
63- if permType == entity.PermissionTypeWebsiteDataAccess {
64- return true
65- }
66- }
67- return false
63+ return slices.Contains(permTypes, entity.PermissionTypeWebsiteDataAccess)
6864 }
6965
7066 func permissionLoggerWithMetadata(log *zerolog.Logger, metadata entity.PermissionMetadata) *zerolog.Logger {
@@ -4,6 +4,7 @@ package usecase
4 import (4 import (
5 "context"5 "context"
6 "errors"6 "errors"
7+ "slices"
7 "sync"8 "sync"
8 "time"9 "time"
9 10
@@ -59,12 +60,7 @@ func (uc *HandlePermissionUseCase) logger(ctx context.Context) *zerolog.Logger {
59 }60 }
60 61
61 func isWebsiteDataAccessPermission(permTypes []entity.PermissionType) bool {62 func isWebsiteDataAccessPermission(permTypes []entity.PermissionType) bool {
62- for _, permType := range permTypes {63+ return slices.Contains(permTypes, entity.PermissionTypeWebsiteDataAccess)
63- if permType == entity.PermissionTypeWebsiteDataAccess {
64- return true
65- }
66- }
67- return false
68 }64 }
69 65
70 func permissionLoggerWithMetadata(log *zerolog.Logger, metadata entity.PermissionMetadata) *zerolog.Logger {66 func permissionLoggerWithMetadata(log *zerolog.Logger, metadata entity.PermissionMetadata) *zerolog.Logger {
M internal/application/usecase/history_recorder.go +3 -3
@@ -634,7 +634,7 @@ func (uc *HistoryRecorderUseCase) persistHistory(ctx context.Context, record his
634634 return delta
635635 }
636636 written := 0
637- for i := 0; i < delta; i++ {
637+ for range delta {
638638 if err := uc.historyRepo.IncrementVisitCount(ctx, record.url); err != nil {
639639 log.Warn().Err(err).Str("url", logging.RedactURL(record.url)).Msg("failed to increment visit count")
640640 return written
@@ -748,8 +748,8 @@ func normalizePathForHistory(path string) string {
748748 if path == "/" {
749749 return ""
750750 }
751- if strings.HasSuffix(path, "/") {
752- return strings.TrimSuffix(path, "/")
751+ if before, ok := strings.CutSuffix(path, "/"); ok {
752+ return before
753753 }
754754 return path
755755 }
@@ -634,7 +634,7 @@ func (uc *HistoryRecorderUseCase) persistHistory(ctx context.Context, record his
634 return delta634 return delta
635 }635 }
636 written := 0636 written := 0
637- for i := 0; i < delta; i++ {637+ for range delta {
638 if err := uc.historyRepo.IncrementVisitCount(ctx, record.url); err != nil {638 if err := uc.historyRepo.IncrementVisitCount(ctx, record.url); err != nil {
639 log.Warn().Err(err).Str("url", logging.RedactURL(record.url)).Msg("failed to increment visit count")639 log.Warn().Err(err).Str("url", logging.RedactURL(record.url)).Msg("failed to increment visit count")
640 return written640 return written
@@ -748,8 +748,8 @@ func normalizePathForHistory(path string) string {
748 if path == "/" {748 if path == "/" {
749 return ""749 return ""
750 }750 }
751- if strings.HasSuffix(path, "/") {751+ if before, ok := strings.CutSuffix(path, "/"); ok {
752- return strings.TrimSuffix(path, "/")752+ return before
753 }753 }
754 return path754 return path
755 }755 }
M internal/application/usecase/manage_panes_consume_expel_test.go +5 -4
@@ -2,6 +2,7 @@ package usecase
22
33 import (
44 "context"
5+ "strings"
56 "testing"
67
78 "github.com/bnema/dumber/internal/domain/entity"
@@ -424,15 +425,15 @@ func panesInOrder(stackNode *entity.PaneNode) string {
424425 if stackNode == nil || len(stackNode.Children) == 0 {
425426 return ""
426427 }
427- out := ""
428+ var out strings.Builder
428429 for i, child := range stackNode.Children {
429430 if child == nil || child.Pane == nil {
430431 continue
431432 }
432433 if i > 0 {
433- out += ","
434+ out.WriteString(",")
434435 }
435- out += string(child.Pane.ID)
436+ out.WriteString(string(child.Pane.ID))
436437 }
437- return out
438+ return out.String()
438439 }
@@ -2,6 +2,7 @@ package usecase
2 2
3 import (3 import (
4 "context"4 "context"
5+ "strings"
5 "testing"6 "testing"
6 7
7 "github.com/bnema/dumber/internal/domain/entity"8 "github.com/bnema/dumber/internal/domain/entity"
@@ -424,15 +425,15 @@ func panesInOrder(stackNode *entity.PaneNode) string {
424 if stackNode == nil || len(stackNode.Children) == 0 {425 if stackNode == nil || len(stackNode.Children) == 0 {
425 return ""426 return ""
426 }427 }
427- out := ""428+ var out strings.Builder
428 for i, child := range stackNode.Children {429 for i, child := range stackNode.Children {
429 if child == nil || child.Pane == nil {430 if child == nil || child.Pane == nil {
430 continue431 continue
431 }432 }
432 if i > 0 {433 if i > 0 {
433- out += ","434+ out.WriteString(",")
434 }435 }
435- out += string(child.Pane.ID)436+ out.WriteString(string(child.Pane.ID))
436 }437 }
437- return out438+ return out.String()
438 }439 }
M internal/bootstrap/crash_report_test.go +1 -1
@@ -61,7 +61,7 @@ func TestReadRedactedLogTail_RingBuffer(t *testing.T) {
6161 path := filepath.Join(dir, "test.log")
6262
6363 var lines []string
64- for i := 0; i < 10; i++ {
64+ for i := range 10 {
6565 lines = append(lines, fmt.Sprintf("line %d", i))
6666 }
6767 require.NoError(t, os.WriteFile(path, []byte(strings.Join(lines, "\n")+"\n"), filePerm))
@@ -61,7 +61,7 @@ func TestReadRedactedLogTail_RingBuffer(t *testing.T) {
61 path := filepath.Join(dir, "test.log")61 path := filepath.Join(dir, "test.log")
62 62
63 var lines []string63 var lines []string
64- for i := 0; i < 10; i++ {64+ for i := range 10 {
65 lines = append(lines, fmt.Sprintf("line %d", i))65 lines = append(lines, fmt.Sprintf("line %d", i))
66 }66 }
67 require.NoError(t, os.WriteFile(path, []byte(strings.Join(lines, "\n")+"\n"), filePerm))67 require.NoError(t, os.WriteFile(path, []byte(strings.Join(lines, "\n")+"\n"), filePerm))
M internal/bootstrap/layershell_preload.go +2 -3
@@ -1,6 +1,7 @@
11 package bootstrap
22
33 import (
4+ "maps"
45 "os"
56 "path/filepath"
67 "strings"
@@ -83,9 +84,7 @@ func layerShellLibrarySearchPaths(env map[string]string) []string {
8384
8485 func LayerShellPreloadEnv(env map[string]string, libraryPath string) map[string]string {
8586 cloned := make(map[string]string, len(env)+1)
86- for key, value := range env {
87- cloned[key] = value
88- }
87+ maps.Copy(cloned, env)
8988
9089 if libraryPath == "" {
9190 return cloned
@@ -1,6 +1,7 @@
1 package bootstrap1 package bootstrap
2 2
3 import (3 import (
4+ "maps"
4 "os"5 "os"
5 "path/filepath"6 "path/filepath"
6 "strings"7 "strings"
@@ -83,9 +84,7 @@ func layerShellLibrarySearchPaths(env map[string]string) []string {
83 84
84 func LayerShellPreloadEnv(env map[string]string, libraryPath string) map[string]string {85 func LayerShellPreloadEnv(env map[string]string, libraryPath string) map[string]string {
85 cloned := make(map[string]string, len(env)+1)86 cloned := make(map[string]string, len(env)+1)
86- for key, value := range env {87+ maps.Copy(cloned, env)
87- cloned[key] = value
88- }
89 88
90 if libraryPath == "" {89 if libraryPath == "" {
91 return cloned90 return cloned
M internal/bootstrap/layershell_preload_test.go +3 -6
@@ -1,5 +1,7 @@
11 package bootstrap
22
3+import "slices"
4+
35 import "testing"
46
57 func TestShouldPreloadLayerShell_WaylandWithoutPreloadReturnsTrue(t *testing.T) {
@@ -116,10 +118,5 @@ func TestLayerShellPreloadEnv_PrependsLibraryToExistingLDPreload(t *testing.T) {
116118 }
117119
118120 func containsString(items []string, want string) bool {
119- for _, item := range items {
120- if item == want {
121- return true
122- }
123- }
124- return false
121+ return slices.Contains(items, want)
125122 }
@@ -1,5 +1,7 @@
1 package bootstrap1 package bootstrap
2 2
3+import "slices"
4+
3 import "testing"5 import "testing"
4 6
5 func TestShouldPreloadLayerShell_WaylandWithoutPreloadReturnsTrue(t *testing.T) {7 func TestShouldPreloadLayerShell_WaylandWithoutPreloadReturnsTrue(t *testing.T) {
@@ -116,10 +118,5 @@ func TestLayerShellPreloadEnv_PrependsLibraryToExistingLDPreload(t *testing.T) {
116 }118 }
117 119
118 func containsString(items []string, want string) bool {120 func containsString(items []string, want string) bool {
119- for _, item := range items {121+ return slices.Contains(items, want)
120- if item == want {
121- return true
122- }
123- }
124- return false
125 }122 }
M internal/bootstrap/runtime_config.go +3 -3
@@ -1,6 +1,8 @@
11 package bootstrap
22
33 import (
4+ "maps"
5+
46 "github.com/bnema/dumber/internal/application/port"
57 "github.com/bnema/dumber/internal/domain/entity"
68 "github.com/bnema/dumber/internal/infrastructure/config"
@@ -131,9 +133,7 @@ func cloneRuntimeSearchShortcuts(in map[string]entity.RuntimeSearchShortcut) map
131133 return nil
132134 }
133135 out := make(map[string]entity.RuntimeSearchShortcut, len(in))
134- for key, shortcut := range in {
135- out[key] = shortcut
136- }
136+ maps.Copy(out, in)
137137 return out
138138 }
139139
@@ -1,6 +1,8 @@
1 package bootstrap1 package bootstrap
2 2
3 import (3 import (
4+ "maps"
5+
4 "github.com/bnema/dumber/internal/application/port"6 "github.com/bnema/dumber/internal/application/port"
5 "github.com/bnema/dumber/internal/domain/entity"7 "github.com/bnema/dumber/internal/domain/entity"
6 "github.com/bnema/dumber/internal/infrastructure/config"8 "github.com/bnema/dumber/internal/infrastructure/config"
@@ -131,9 +133,7 @@ func cloneRuntimeSearchShortcuts(in map[string]entity.RuntimeSearchShortcut) map
131 return nil133 return nil
132 }134 }
133 out := make(map[string]entity.RuntimeSearchShortcut, len(in))135 out := make(map[string]entity.RuntimeSearchShortcut, len(in))
134- for key, shortcut := range in {136+ maps.Copy(out, in)
135- out[key] = shortcut
136- }
137 return out137 return out
138 }138 }
139 139
M internal/cli/cmd/sessions_test.go +1 -1
@@ -67,7 +67,7 @@ func TestFindSessionByIDOrSuffixSearchesBeyondDefaultLimit(t *testing.T) {
6767 targetID := entity.SessionID("20251224_000000_targetolder")
6868 sessions := make([]*entity.Session, 0, defaultSessionsLimit*5+1)
6969 startedAt := time.Date(2025, 12, 24, 12, 0, 0, 0, time.UTC)
70- for i := 0; i < defaultSessionsLimit*5; i++ {
70+ for i := range defaultSessionsLimit * 5 {
7171 sessions = append(sessions, &entity.Session{
7272 ID: entity.SessionID(fmt.Sprintf("20251224_1200%02d_recent", i)),
7373 Type: entity.SessionTypeBrowser,
@@ -67,7 +67,7 @@ func TestFindSessionByIDOrSuffixSearchesBeyondDefaultLimit(t *testing.T) {
67 targetID := entity.SessionID("20251224_000000_targetolder")67 targetID := entity.SessionID("20251224_000000_targetolder")
68 sessions := make([]*entity.Session, 0, defaultSessionsLimit*5+1)68 sessions := make([]*entity.Session, 0, defaultSessionsLimit*5+1)
69 startedAt := time.Date(2025, 12, 24, 12, 0, 0, 0, time.UTC)69 startedAt := time.Date(2025, 12, 24, 12, 0, 0, 0, time.UTC)
70- for i := 0; i < defaultSessionsLimit*5; i++ {70+ for i := range defaultSessionsLimit * 5 {
71 sessions = append(sessions, &entity.Session{71 sessions = append(sessions, &entity.Session{
72 ID: entity.SessionID(fmt.Sprintf("20251224_1200%02d_recent", i)),72 ID: entity.SessionID(fmt.Sprintf("20251224_1200%02d_recent", i)),
73 Type: entity.SessionTypeBrowser,73 Type: entity.SessionTypeBrowser,
M internal/cli/model/dmenu.go +3 -4
@@ -172,10 +172,9 @@ func (m *DmenuModel) updateList() {
172172 }
173173
174174 // Create list
175- listHeight := m.height - 6 // Account for search, help
176- if listHeight < 5 {
177- listHeight = 5
178- }
175+ listHeight := max(
176+ // Account for search, help
177+ m.height-6, 5)
179178
180179 m.list = styles.NewHistoryList(m.theme, items, m.width, listHeight)
181180 }
@@ -172,10 +172,9 @@ func (m *DmenuModel) updateList() {
172 }172 }
173 173
174 // Create list174 // Create list
175- listHeight := m.height - 6 // Account for search, help175+ listHeight := max(
176- if listHeight < 5 {176+ // Account for search, help
177- listHeight = 5177+ m.height-6, 5)
178- }
179 178
180 m.list = styles.NewHistoryList(m.theme, items, m.width, listHeight)179 m.list = styles.NewHistoryList(m.theme, items, m.width, listHeight)
181 }180 }
M internal/cli/model/history.go +3 -4
@@ -412,10 +412,9 @@ func (m *HistoryModel) updateList() {
412412 }
413413
414414 // Create new list
415- listHeight := m.height - 8 // Account for tabs, search, help
416- if listHeight < 5 {
417- listHeight = 5
418- }
415+ listHeight := max(
416+ // Account for tabs, search, help
417+ m.height-8, 5)
419418
420419 m.list = styles.NewHistoryList(m.theme, items, m.width, listHeight)
421420 }
@@ -412,10 +412,9 @@ func (m *HistoryModel) updateList() {
412 }412 }
413 413
414 // Create new list414 // Create new list
415- listHeight := m.height - 8 // Account for tabs, search, help415+ listHeight := max(
416- if listHeight < 5 {416+ // Account for tabs, search, help
417- listHeight = 5417+ m.height-8, 5)
418- }
419 418
420 m.list = styles.NewHistoryList(m.theme, items, m.width, listHeight)419 m.list = styles.NewHistoryList(m.theme, items, m.width, listHeight)
421 }420 }
M internal/cli/model/sessions.go +4 -8
@@ -386,10 +386,9 @@ func (m SessionsModel) View() string {
386386 // are many sessions.
387387 prefixH := lipgloss.Height(prefix.String())
388388 helpH := lipgloss.Height(helpView)
389- listH := m.height - prefixH - 1 - helpH // one spacer line between list and help
390- if listH < 1 {
391- listH = 1
392- }
389+ listH := max(
390+ // one spacer line between list and help
391+ m.height-prefixH-1-helpH, 1)
393392
394393 listView := m.renderSessionsList(listH)
395394
@@ -436,10 +435,7 @@ func (m SessionsModel) renderSessionsList(maxHeight int) string {
436435
437436 // Determine starting index so the selected row stays visible even when
438437 // rows above it are expanded and consume multiple lines.
439- start := m.selectedIdx
440- if start < 0 {
441- start = 0
442- }
438+ start := max(m.selectedIdx, 0)
443439 if start >= len(m.sessions) {
444440 start = len(m.sessions) - 1
445441 }
@@ -386,10 +386,9 @@ func (m SessionsModel) View() string {
386 // are many sessions.386 // are many sessions.
387 prefixH := lipgloss.Height(prefix.String())387 prefixH := lipgloss.Height(prefix.String())
388 helpH := lipgloss.Height(helpView)388 helpH := lipgloss.Height(helpView)
389- listH := m.height - prefixH - 1 - helpH // one spacer line between list and help389+ listH := max(
390- if listH < 1 {390+ // one spacer line between list and help
391- listH = 1391+ m.height-prefixH-1-helpH, 1)
392- }
393 392
394 listView := m.renderSessionsList(listH)393 listView := m.renderSessionsList(listH)
395 394
@@ -436,10 +435,7 @@ func (m SessionsModel) renderSessionsList(maxHeight int) string {
436 435
437 // Determine starting index so the selected row stays visible even when436 // Determine starting index so the selected row stays visible even when
438 // rows above it are expanded and consume multiple lines.437 // rows above it are expanded and consume multiple lines.
439- start := m.selectedIdx438+ start := max(m.selectedIdx, 0)
440- if start < 0 {
441- start = 0
442- }
443 if start >= len(m.sessions) {439 if start >= len(m.sessions) {
444 start = len(m.sessions) - 1440 start = len(m.sessions) - 1
445 }441 }
M internal/cli/model/sessions_test.go +2 -2
@@ -18,7 +18,7 @@ func TestRenderSessionsList_KeepsSelectedVisibleWhenExpandedRowAbove(t *testing.
1818 m := NewSessionsModel(context.Background(), theme, SessionsModelConfig{})
1919
2020 m.sessions = make([]entity.SessionInfo, 0, 6)
21- for i := 0; i < 6; i++ {
21+ for i := range 6 {
2222 m.sessions = append(m.sessions, testSessionInfo(fmt.Sprintf("session-%02d", i), 1))
2323 }
2424
@@ -33,7 +33,7 @@ func TestRenderSessionsList_KeepsSelectedVisibleWhenExpandedRowAbove(t *testing.
3333
3434 func testSessionInfo(id string, tabCount int) entity.SessionInfo {
3535 tabs := make([]entity.TabSnapshot, 0, tabCount)
36- for i := 0; i < tabCount; i++ {
36+ for i := range tabCount {
3737 tabs = append(tabs, entity.TabSnapshot{Name: fmt.Sprintf("Tab %d", i+1)})
3838 }
3939
@@ -18,7 +18,7 @@ func TestRenderSessionsList_KeepsSelectedVisibleWhenExpandedRowAbove(t *testing.
18 m := NewSessionsModel(context.Background(), theme, SessionsModelConfig{})18 m := NewSessionsModel(context.Background(), theme, SessionsModelConfig{})
19 19
20 m.sessions = make([]entity.SessionInfo, 0, 6)20 m.sessions = make([]entity.SessionInfo, 0, 6)
21- for i := 0; i < 6; i++ {21+ for i := range 6 {
22 m.sessions = append(m.sessions, testSessionInfo(fmt.Sprintf("session-%02d", i), 1))22 m.sessions = append(m.sessions, testSessionInfo(fmt.Sprintf("session-%02d", i), 1))
23 }23 }
24 24
@@ -33,7 +33,7 @@ func TestRenderSessionsList_KeepsSelectedVisibleWhenExpandedRowAbove(t *testing.
33 33
34 func testSessionInfo(id string, tabCount int) entity.SessionInfo {34 func testSessionInfo(id string, tabCount int) entity.SessionInfo {
35 tabs := make([]entity.TabSnapshot, 0, tabCount)35 tabs := make([]entity.TabSnapshot, 0, tabCount)
36- for i := 0; i < tabCount; i++ {36+ for i := range tabCount {
37 tabs = append(tabs, entity.TabSnapshot{Name: fmt.Sprintf("Tab %d", i+1)})37 tabs = append(tabs, entity.TabSnapshot{Name: fmt.Sprintf("Tab %d", i+1)})
38 }38 }
39 39
M internal/cli/model/stats.go +6 -14
@@ -3,6 +3,7 @@ package model
33 import (
44 "context"
55 "fmt"
6+ "strings"
67
78 "github.com/charmbracelet/bubbles/table"
89 tea "github.com/charmbracelet/bubbletea"
@@ -104,13 +105,7 @@ func (m *StatsModel) updateTable() {
104105 }
105106 }
106107
107- tableHeight := len(rows)
108- if tableHeight > m.height-10 {
109- tableHeight = m.height - 10
110- }
111- if tableHeight < 3 {
112- tableHeight = 3
113- }
108+ tableHeight := max(min(len(rows), m.height-10), 3)
114109
115110 m.table = styles.NewStyledTable(m.theme, columns, rows, m.width-4, tableHeight)
116111 }
@@ -213,7 +208,7 @@ func (m StatsModel) renderDailyActivity() string {
213208 sparklineMaxDays = 30
214209 sparklineMaxIndex = 7
215210 )
216- var result string
211+ var result strings.Builder
217212
218213 // Show last 30 days or available data
219214 days := m.analytics.DailyVisits
@@ -223,14 +218,11 @@ func (m StatsModel) renderDailyActivity() string {
223218
224219 for _, d := range days {
225220 // Scale to block index (0-7)
226- idx := int(float64(d.Visits) / float64(maxVisits) * sparklineMaxIndex)
227- if idx > sparklineMaxIndex {
228- idx = sparklineMaxIndex
229- }
230- result += string(blocks[idx])
221+ idx := min(int(float64(d.Visits)/float64(maxVisits)*sparklineMaxIndex), sparklineMaxIndex)
222+ result.WriteString(string(blocks[idx]))
231223 }
232224
233- return lipgloss.NewStyle().Foreground(m.theme.Accent).Render(result)
225+ return lipgloss.NewStyle().Foreground(m.theme.Accent).Render(result.String())
234226 }
235227
236228 // formatNumber formats a number for display.
@@ -3,6 +3,7 @@ package model
3 import (3 import (
4 "context"4 "context"
5 "fmt"5 "fmt"
6+ "strings"
6 7
7 "github.com/charmbracelet/bubbles/table"8 "github.com/charmbracelet/bubbles/table"
8 tea "github.com/charmbracelet/bubbletea"9 tea "github.com/charmbracelet/bubbletea"
@@ -104,13 +105,7 @@ func (m *StatsModel) updateTable() {
104 }105 }
105 }106 }
106 107
107- tableHeight := len(rows)108+ tableHeight := max(min(len(rows), m.height-10), 3)
108- if tableHeight > m.height-10 {
109- tableHeight = m.height - 10
110- }
111- if tableHeight < 3 {
112- tableHeight = 3
113- }
114 109
115 m.table = styles.NewStyledTable(m.theme, columns, rows, m.width-4, tableHeight)110 m.table = styles.NewStyledTable(m.theme, columns, rows, m.width-4, tableHeight)
116 }111 }
@@ -213,7 +208,7 @@ func (m StatsModel) renderDailyActivity() string {
213 sparklineMaxDays = 30208 sparklineMaxDays = 30
214 sparklineMaxIndex = 7209 sparklineMaxIndex = 7
215 )210 )
216- var result string211+ var result strings.Builder
217 212
218 // Show last 30 days or available data213 // Show last 30 days or available data
219 days := m.analytics.DailyVisits214 days := m.analytics.DailyVisits
@@ -223,14 +218,11 @@ func (m StatsModel) renderDailyActivity() string {
223 218
224 for _, d := range days {219 for _, d := range days {
225 // Scale to block index (0-7)220 // Scale to block index (0-7)
226- idx := int(float64(d.Visits) / float64(maxVisits) * sparklineMaxIndex)221+ idx := min(int(float64(d.Visits)/float64(maxVisits)*sparklineMaxIndex), sparklineMaxIndex)
227- if idx > sparklineMaxIndex {222+ result.WriteString(string(blocks[idx]))
228- idx = sparklineMaxIndex
229- }
230- result += string(blocks[idx])
231 }223 }
232 224
233- return lipgloss.NewStyle().Foreground(m.theme.Accent).Render(result)225+ return lipgloss.NewStyle().Foreground(m.theme.Accent).Render(result.String())
234 }226 }
235 227
236 // formatNumber formats a number for display.228 // formatNumber formats a number for display.
M internal/cli/styles/tabs.go +5 -4
@@ -2,6 +2,7 @@ package styles
22
33 import (
44 "fmt"
5+ "strings"
56
67 "github.com/charmbracelet/lipgloss"
78 )
@@ -56,16 +57,16 @@ func (m TabsModel) View() string {
5657 Foreground(m.theme.Border).
5758 Render(" │ ")
5859
59- row := ""
60+ var row strings.Builder
6061 for i, tab := range tabs {
6162 if i > 0 {
62- row += gap
63+ row.WriteString(gap)
6364 }
64- row += tab
65+ row.WriteString(tab)
6566 }
6667
6768 const tabBarWidth = 80
68- return m.theme.TabBar.Width(tabBarWidth).Render(row)
69+ return m.theme.TabBar.Width(tabBarWidth).Render(row.String())
6970 }
7071
7172 // ViewCompact renders a compact tab bar without separators.
@@ -2,6 +2,7 @@ package styles
2 2
3 import (3 import (
4 "fmt"4 "fmt"
5+ "strings"
5 6
6 "github.com/charmbracelet/lipgloss"7 "github.com/charmbracelet/lipgloss"
7 )8 )
@@ -56,16 +57,16 @@ func (m TabsModel) View() string {
56 Foreground(m.theme.Border).57 Foreground(m.theme.Border).
57 Render(" │ ")58 Render(" │ ")
58 59
59- row := ""60+ var row strings.Builder
60 for i, tab := range tabs {61 for i, tab := range tabs {
61 if i > 0 {62 if i > 0 {
62- row += gap63+ row.WriteString(gap)
63 }64 }
64- row += tab65+ row.WriteString(tab)
65 }66 }
66 67
67 const tabBarWidth = 8068 const tabBarWidth = 80
68- return m.theme.TabBar.Width(tabBarWidth).Render(row)69+ return m.theme.TabBar.Width(tabBarWidth).Render(row.String())
69 }70 }
70 71
71 // ViewCompact renders a compact tab bar without separators.72 // ViewCompact renders a compact tab bar without separators.
M internal/domain/entity/engine_settings_test.go +3 -3
@@ -6,13 +6,13 @@ import (
66 )
77
88 func TestEngineSettingsPayloadContainsRuntimeWebContentFields(t *testing.T) {
9- payloadType := reflect.TypeOf(EngineSettingsPayload{})
9+ payloadType := reflect.TypeFor[EngineSettingsPayload]()
1010 for _, field := range []string{"DefaultUIScale", "WebContent"} {
1111 if _, ok := payloadType.FieldByName(field); !ok {
1212 t.Fatalf("EngineSettingsPayload missing %s", field)
1313 }
1414 }
15- webContentType := reflect.TypeOf(EngineWebContentSettingsPayload{})
15+ webContentType := reflect.TypeFor[EngineWebContentSettingsPayload]()
1616 for _, field := range []string{
1717 "SansFont",
1818 "SerifFont",
@@ -31,7 +31,7 @@ func TestEngineSettingsPayloadContainsRuntimeWebContentFields(t *testing.T) {
3131 }
3232
3333 func TestEngineSettingsUpdateHasNoRawBridge(t *testing.T) {
34- updateType := reflect.TypeOf(EngineSettingsUpdate{})
34+ updateType := reflect.TypeFor[EngineSettingsUpdate]()
3535 if _, ok := updateType.FieldByName("Raw"); ok {
3636 t.Fatal("EngineSettingsUpdate must not expose Raw legacy bridge")
3737 }
@@ -6,13 +6,13 @@ import (
6 )6 )
7 7
8 func TestEngineSettingsPayloadContainsRuntimeWebContentFields(t *testing.T) {8 func TestEngineSettingsPayloadContainsRuntimeWebContentFields(t *testing.T) {
9- payloadType := reflect.TypeOf(EngineSettingsPayload{})9+ payloadType := reflect.TypeFor[EngineSettingsPayload]()
10 for _, field := range []string{"DefaultUIScale", "WebContent"} {10 for _, field := range []string{"DefaultUIScale", "WebContent"} {
11 if _, ok := payloadType.FieldByName(field); !ok {11 if _, ok := payloadType.FieldByName(field); !ok {
12 t.Fatalf("EngineSettingsPayload missing %s", field)12 t.Fatalf("EngineSettingsPayload missing %s", field)
13 }13 }
14 }14 }
15- webContentType := reflect.TypeOf(EngineWebContentSettingsPayload{})15+ webContentType := reflect.TypeFor[EngineWebContentSettingsPayload]()
16 for _, field := range []string{16 for _, field := range []string{
17 "SansFont",17 "SansFont",
18 "SerifFont",18 "SerifFont",
@@ -31,7 +31,7 @@ func TestEngineSettingsPayloadContainsRuntimeWebContentFields(t *testing.T) {
31 }31 }
32 32
33 func TestEngineSettingsUpdateHasNoRawBridge(t *testing.T) {33 func TestEngineSettingsUpdateHasNoRawBridge(t *testing.T) {
34- updateType := reflect.TypeOf(EngineSettingsUpdate{})34+ updateType := reflect.TypeFor[EngineSettingsUpdate]()
35 if _, ok := updateType.FieldByName("Raw"); ok {35 if _, ok := updateType.FieldByName("Raw"); ok {
36 t.Fatal("EngineSettingsUpdate must not expose Raw legacy bridge")36 t.Fatal("EngineSettingsUpdate must not expose Raw legacy bridge")
37 }37 }
M internal/infrastructure/audio/null/output_test.go +1 -1
@@ -64,7 +64,7 @@ func TestStream_Write_AcceptsMultipleCalls(t *testing.T) {
6464 }
6565
6666 // Act & Assert: multiple writes should succeed
67- for i := 0; i < 3; i++ {
67+ for i := range 3 {
6868 if err := stream.Write(samples); err != nil {
6969 t.Errorf("Write #%d failed: %v", i+1, err)
7070 }
@@ -64,7 +64,7 @@ func TestStream_Write_AcceptsMultipleCalls(t *testing.T) {
64 }64 }
65 65
66 // Act & Assert: multiple writes should succeed66 // Act & Assert: multiple writes should succeed
67- for i := 0; i < 3; i++ {67+ for i := range 3 {
68 if err := stream.Write(samples); err != nil {68 if err := stream.Write(samples); err != nil {
69 t.Errorf("Write #%d failed: %v", i+1, err)69 t.Errorf("Write #%d failed: %v", i+1, err)
70 }70 }
M internal/infrastructure/audio/pipewire/output.go +2 -14
@@ -213,13 +213,7 @@ func copyToPCM(pcm *pwpipewire.PCMBuffer, samples [][]float32) int {
213213 }
214214
215215 frames := pcm.Frames
216- channels := len(samples)
217- if channels > pcm.Channels {
218- channels = pcm.Channels
219- }
220- if channels > len(pcm.Samples) {
221- channels = len(pcm.Samples)
222- }
216+ channels := min(min(len(samples), pcm.Channels), len(pcm.Samples))
223217 if channels == 0 {
224218 return 0
225219 }
@@ -234,13 +228,7 @@ func copyToPCM(pcm *pwpipewire.PCMBuffer, samples [][]float32) int {
234228 continue
235229 }
236230 srcFrames := len(src)
237- n := frames
238- if srcFrames < n {
239- n = srcFrames
240- }
241- if len(dst) < n {
242- n = len(dst)
243- }
231+ n := min(len(dst), min(srcFrames, frames))
244232 if n < minFrames {
245233 minFrames = n
246234 }
@@ -213,13 +213,7 @@ func copyToPCM(pcm *pwpipewire.PCMBuffer, samples [][]float32) int {
213 }213 }
214 214
215 frames := pcm.Frames215 frames := pcm.Frames
216- channels := len(samples)216+ channels := min(min(len(samples), pcm.Channels), len(pcm.Samples))
217- if channels > pcm.Channels {
218- channels = pcm.Channels
219- }
220- if channels > len(pcm.Samples) {
221- channels = len(pcm.Samples)
222- }
223 if channels == 0 {217 if channels == 0 {
224 return 0218 return 0
225 }219 }
@@ -234,13 +228,7 @@ func copyToPCM(pcm *pwpipewire.PCMBuffer, samples [][]float32) int {
234 continue228 continue
235 }229 }
236 srcFrames := len(src)230 srcFrames := len(src)
237- n := frames231+ n := min(len(dst), min(srcFrames, frames))
238- if srcFrames < n {
239- n = srcFrames
240- }
241- if len(dst) < n {
242- n = len(dst)
243- }
244 if n < minFrames {232 if n < minFrames {
245 minFrames = n233 minFrames = n
246 }234 }
M internal/infrastructure/audio/pipewire/output_test.go +3 -3
@@ -448,7 +448,7 @@ func TestPlayerStreamAdapter_Write_DropsWhenBufferFull(t *testing.T) {
448448 }
449449
450450 // Fill the buffer to capacity
451- for i := 0; i < 4; i++ {
451+ for i := range 4 {
452452 require.NoError(t, adapter.Write([][]float32{{float32(i)}}))
453453 }
454454
@@ -474,7 +474,7 @@ func TestPlayerStreamAdapter_Write_DropsLogOnlyOnFirstAndPowerOfTwo(t *testing.T
474474 require.NoError(t, adapter.Write([][]float32{{0.0}}))
475475
476476 // Drop 10 packets
477- for i := 0; i < 10; i++ {
477+ for i := range 10 {
478478 _ = adapter.Write([][]float32{{float32(i)}})
479479 }
480480
@@ -495,7 +495,7 @@ func TestPlayerStreamAdapter_ConcurrentWriteClose_NoRace(t *testing.T) {
495495 done := make(chan struct{})
496496 go func() {
497497 defer close(done)
498- for i := 0; i < 100; i++ {
498+ for i := range 100 {
499499 err := adapter.Write([][]float32{{float32(i)}})
500500 if err != nil {
501501 assert.ErrorIs(t, err, ErrStreamClosed)
@@ -448,7 +448,7 @@ func TestPlayerStreamAdapter_Write_DropsWhenBufferFull(t *testing.T) {
448 }448 }
449 449
450 // Fill the buffer to capacity450 // Fill the buffer to capacity
451- for i := 0; i < 4; i++ {451+ for i := range 4 {
452 require.NoError(t, adapter.Write([][]float32{{float32(i)}}))452 require.NoError(t, adapter.Write([][]float32{{float32(i)}}))
453 }453 }
454 454
@@ -474,7 +474,7 @@ func TestPlayerStreamAdapter_Write_DropsLogOnlyOnFirstAndPowerOfTwo(t *testing.T
474 require.NoError(t, adapter.Write([][]float32{{0.0}}))474 require.NoError(t, adapter.Write([][]float32{{0.0}}))
475 475
476 // Drop 10 packets476 // Drop 10 packets
477- for i := 0; i < 10; i++ {477+ for i := range 10 {
478 _ = adapter.Write([][]float32{{float32(i)}})478 _ = adapter.Write([][]float32{{float32(i)}})
479 }479 }
480 480
@@ -495,7 +495,7 @@ func TestPlayerStreamAdapter_ConcurrentWriteClose_NoRace(t *testing.T) {
495 done := make(chan struct{})495 done := make(chan struct{})
496 go func() {496 go func() {
497 defer close(done)497 defer close(done)
498- for i := 0; i < 100; i++ {498+ for i := range 100 {
499 err := adapter.Write([][]float32{{float32(i)}})499 err := adapter.Write([][]float32{{float32(i)}})
500 if err != nil {500 if err != nil {
501 assert.ErrorIs(t, err, ErrStreamClosed)501 assert.ErrorIs(t, err, ErrStreamClosed)
M internal/infrastructure/cache/lru_test.go +3 -3
@@ -145,7 +145,7 @@ func TestLRU_ConcurrentAccess(t *testing.T) {
145145 var wg sync.WaitGroup
146146
147147 // Concurrent writes
148- for i := 0; i < 100; i++ {
148+ for i := range 100 {
149149 wg.Add(1)
150150 go func(i int) {
151151 defer wg.Done()
@@ -155,7 +155,7 @@ func TestLRU_ConcurrentAccess(t *testing.T) {
155155 wg.Wait()
156156
157157 // Concurrent reads
158- for i := 0; i < 100; i++ {
158+ for i := range 100 {
159159 wg.Add(1)
160160 go func(i int) {
161161 defer wg.Done()
@@ -165,7 +165,7 @@ func TestLRU_ConcurrentAccess(t *testing.T) {
165165 wg.Wait()
166166
167167 // Concurrent mixed operations
168- for i := 0; i < 50; i++ {
168+ for i := range 50 {
169169 wg.Add(3)
170170 go func(i int) {
171171 defer wg.Done()
@@ -145,7 +145,7 @@ func TestLRU_ConcurrentAccess(t *testing.T) {
145 var wg sync.WaitGroup145 var wg sync.WaitGroup
146 146
147 // Concurrent writes147 // Concurrent writes
148- for i := 0; i < 100; i++ {148+ for i := range 100 {
149 wg.Add(1)149 wg.Add(1)
150 go func(i int) {150 go func(i int) {
151 defer wg.Done()151 defer wg.Done()
@@ -155,7 +155,7 @@ func TestLRU_ConcurrentAccess(t *testing.T) {
155 wg.Wait()155 wg.Wait()
156 156
157 // Concurrent reads157 // Concurrent reads
158- for i := 0; i < 100; i++ {158+ for i := range 100 {
159 wg.Add(1)159 wg.Add(1)
160 go func(i int) {160 go func(i int) {
161 defer wg.Done()161 defer wg.Done()
@@ -165,7 +165,7 @@ func TestLRU_ConcurrentAccess(t *testing.T) {
165 wg.Wait()165 wg.Wait()
166 166
167 // Concurrent mixed operations167 // Concurrent mixed operations
168- for i := 0; i < 50; i++ {168+ for i := range 50 {
169 wg.Add(3)169 wg.Add(3)
170 go func(i int) {170 go func(i int) {
171 defer wg.Done()171 defer wg.Done()
M internal/infrastructure/cef/app.go +1 -1
@@ -193,7 +193,7 @@ func appendUniqueCommaSeparatedSwitchValues(commandLine purecef.CommandLine, nam
193193
194194 seen := make(map[string]struct{}, len(values))
195195 combined := make([]string, 0, len(values))
196- for _, value := range strings.Split(commandLine.GetSwitchValue(name), ",") {
196+ for value := range strings.SplitSeq(commandLine.GetSwitchValue(name), ",") {
197197 value = strings.TrimSpace(value)
198198 if value == "" {
199199 continue
@@ -193,7 +193,7 @@ func appendUniqueCommaSeparatedSwitchValues(commandLine purecef.CommandLine, nam
193 193
194 seen := make(map[string]struct{}, len(values))194 seen := make(map[string]struct{}, len(values))
195 combined := make([]string, 0, len(values))195 combined := make([]string, 0, len(values))
196- for _, value := range strings.Split(commandLine.GetSwitchValue(name), ",") {196+ for value := range strings.SplitSeq(commandLine.GetSwitchValue(name), ",") {
197 value = strings.TrimSpace(value)197 value = strings.TrimSpace(value)
198 if value == "" {198 if value == "" {
199 continue199 continue
M internal/infrastructure/cef/engine_init.go +4 -5
@@ -7,6 +7,7 @@ import (
77 "math"
88 "os"
99 "path/filepath"
10+ "slices"
1011 "strconv"
1112 "strings"
1213 "syscall"
@@ -214,7 +215,7 @@ func loadedLibCEFPath() string {
214215 }
215216
216217 func parseLoadedLibCEFPath(maps string) string {
217- for _, line := range strings.Split(maps, "\n") {
218+ for line := range strings.SplitSeq(maps, "\n") {
218219 if !strings.Contains(line, "libcef.so") {
219220 continue
220221 }
@@ -339,10 +340,8 @@ func registerEngineSchemeFactory(
339340
340341 // appendIfMissing appends flag to args if it's not already present.
341342 func appendIfMissing(args []string, flag string) []string {
342- for _, a := range args {
343- if a == flag {
344- return args
345- }
343+ if slices.Contains(args, flag) {
344+ return args
346345 }
347346 return append(args, flag)
348347 }
@@ -7,6 +7,7 @@ import (
7 "math"7 "math"
8 "os"8 "os"
9 "path/filepath"9 "path/filepath"
10+ "slices"
10 "strconv"11 "strconv"
11 "strings"12 "strings"
12 "syscall"13 "syscall"
@@ -214,7 +215,7 @@ func loadedLibCEFPath() string {
214 }215 }
215 216
216 func parseLoadedLibCEFPath(maps string) string {217 func parseLoadedLibCEFPath(maps string) string {
217- for _, line := range strings.Split(maps, "\n") {218+ for line := range strings.SplitSeq(maps, "\n") {
218 if !strings.Contains(line, "libcef.so") {219 if !strings.Contains(line, "libcef.so") {
219 continue220 continue
220 }221 }
@@ -339,10 +340,8 @@ func registerEngineSchemeFactory(
339 340
340 // appendIfMissing appends flag to args if it's not already present.341 // appendIfMissing appends flag to args if it's not already present.
341 func appendIfMissing(args []string, flag string) []string {342 func appendIfMissing(args []string, flag string) []string {
342- for _, a := range args {343+ if slices.Contains(args, flag) {
343- if a == flag {344+ return args
344- return args
345- }
346 }345 }
347 return append(args, flag)346 return append(args, flag)
348 }347 }
M internal/infrastructure/cef/engine_settings_test.go +1 -1
@@ -35,7 +35,7 @@ func TestEngineUpdateSettingsUsesZeroScaleWhenPayloadIsEmpty(t *testing.T) {
3535 func TestEngineApplicationScaleConcurrentAccess(_ *testing.T) {
3636 e := &Engine{applicationScale: 1}
3737 var wg sync.WaitGroup
38- for i := 0; i < 16; i++ {
38+ for i := range 16 {
3939 wg.Add(2)
4040 go func() {
4141 defer wg.Done()
@@ -35,7 +35,7 @@ func TestEngineUpdateSettingsUsesZeroScaleWhenPayloadIsEmpty(t *testing.T) {
35 func TestEngineApplicationScaleConcurrentAccess(_ *testing.T) {35 func TestEngineApplicationScaleConcurrentAccess(_ *testing.T) {
36 e := &Engine{applicationScale: 1}36 e := &Engine{applicationScale: 1}
37 var wg sync.WaitGroup37 var wg sync.WaitGroup
38- for i := 0; i < 16; i++ {38+ for i := range 16 {
39 wg.Add(2)39 wg.Add(2)
40 go func() {40 go func() {
41 defer wg.Done()41 defer wg.Done()
M internal/infrastructure/cef/favicon_discovery.go +2 -2
@@ -77,8 +77,8 @@ func parseFaviconHrefs(htmlSource string) []string {
7777 }
7878
7979 func faviconRel(rel string) bool {
80- fields := strings.Fields(strings.ToLower(rel))
81- for _, field := range fields {
80+ fields := strings.FieldsSeq(strings.ToLower(rel))
81+ for field := range fields {
8282 if field == "icon" || field == "apple-touch-icon" || field == "apple-touch-icon-precomposed" {
8383 return true
8484 }
@@ -77,8 +77,8 @@ func parseFaviconHrefs(htmlSource string) []string {
77 }77 }
78 78
79 func faviconRel(rel string) bool {79 func faviconRel(rel string) bool {
80- fields := strings.Fields(strings.ToLower(rel))80+ fields := strings.FieldsSeq(strings.ToLower(rel))
81- for _, field := range fields {81+ for field := range fields {
82 if field == "icon" || field == "apple-touch-icon" || field == "apple-touch-icon-precomposed" {82 if field == "icon" || field == "apple-touch-icon" || field == "apple-touch-icon-precomposed" {
83 return true83 return true
84 }84 }
M internal/infrastructure/cef/file_dialog.go +1 -1
@@ -307,7 +307,7 @@ func dialogFilterTokens(spec, extensions string) []string {
307307 if spec = strings.TrimSpace(spec); spec != "" {
308308 tokens = append(tokens, spec)
309309 }
310- for _, part := range strings.Split(extensions, ";") {
310+ for part := range strings.SplitSeq(extensions, ";") {
311311 part = strings.TrimSpace(part)
312312 if part != "" {
313313 tokens = append(tokens, part)
@@ -307,7 +307,7 @@ func dialogFilterTokens(spec, extensions string) []string {
307 if spec = strings.TrimSpace(spec); spec != "" {307 if spec = strings.TrimSpace(spec); spec != "" {
308 tokens = append(tokens, spec)308 tokens = append(tokens, spec)
309 }309 }
310- for _, part := range strings.Split(extensions, ";") {310+ for part := range strings.SplitSeq(extensions, ";") {
311 part = strings.TrimSpace(part)311 part = strings.TrimSpace(part)
312 if part != "" {312 if part != "" {
313 tokens = append(tokens, part)313 tokens = append(tokens, part)
M internal/infrastructure/cef/file_dialog_test.go +1 -1
@@ -162,7 +162,7 @@ func TestFolderUploadPathsTruncationCancels(t *testing.T) {
162162 defer func() { maxExpandedFolderUploadFiles = prev }()
163163
164164 dir := t.TempDir()
165- for i := 0; i < 5; i++ {
165+ for i := range 5 {
166166 p := filepath.Join(dir, fmt.Sprintf("file_%d.txt", i))
167167 require.NoError(t, os.WriteFile(p, []byte("data"), 0o644))
168168 }
@@ -162,7 +162,7 @@ func TestFolderUploadPathsTruncationCancels(t *testing.T) {
162 defer func() { maxExpandedFolderUploadFiles = prev }()162 defer func() { maxExpandedFolderUploadFiles = prev }()
163 163
164 dir := t.TempDir()164 dir := t.TempDir()
165- for i := 0; i < 5; i++ {165+ for i := range 5 {
166 p := filepath.Join(dir, fmt.Sprintf("file_%d.txt", i))166 p := filepath.Join(dir, fmt.Sprintf("file_%d.txt", i))
167 require.NoError(t, os.WriteFile(p, []byte("data"), 0o644))167 require.NoError(t, os.WriteFile(p, []byte("data"), 0o644))
168 }168 }
M internal/infrastructure/cef/process_diagnostics.go +5 -5
@@ -168,7 +168,7 @@ func procPPID(pid int) int {
168168 if err != nil {
169169 return 0
170170 }
171- for _, line := range strings.Split(string(b), "\n") {
171+ for line := range strings.SplitSeq(string(b), "\n") {
172172 if !strings.HasPrefix(line, "PPid:") {
173173 continue
174174 }
@@ -190,9 +190,9 @@ func procCmdline(pid int) string {
190190 }
191191
192192 func chromiumProcessType(cmdline string) string {
193- for _, field := range strings.Fields(cmdline) {
194- if strings.HasPrefix(field, "--type=") {
195- return strings.TrimPrefix(field, "--type=")
193+ for field := range strings.FieldsSeq(cmdline) {
194+ if after, ok := strings.CutPrefix(field, "--type="); ok {
195+ return after
196196 }
197197 }
198198 return "browser"
@@ -366,7 +366,7 @@ func gpuFDInfoSummaries(pid int) []string {
366366 if err != nil {
367367 continue
368368 }
369- for _, line := range strings.Split(string(b), "\n") {
369+ for line := range strings.SplitSeq(string(b), "\n") {
370370 if strings.Contains(line, "drm-engine-") || strings.HasPrefix(line, "drm-client-id") || strings.HasPrefix(line, "drm-pdev") {
371371 out = append(out, "fd="+entry.Name()+" "+strings.TrimSpace(line))
372372 }
@@ -168,7 +168,7 @@ func procPPID(pid int) int {
168 if err != nil {168 if err != nil {
169 return 0169 return 0
170 }170 }
171- for _, line := range strings.Split(string(b), "\n") {171+ for line := range strings.SplitSeq(string(b), "\n") {
172 if !strings.HasPrefix(line, "PPid:") {172 if !strings.HasPrefix(line, "PPid:") {
173 continue173 continue
174 }174 }
@@ -190,9 +190,9 @@ func procCmdline(pid int) string {
190 }190 }
191 191
192 func chromiumProcessType(cmdline string) string {192 func chromiumProcessType(cmdline string) string {
193- for _, field := range strings.Fields(cmdline) {193+ for field := range strings.FieldsSeq(cmdline) {
194- if strings.HasPrefix(field, "--type=") {194+ if after, ok := strings.CutPrefix(field, "--type="); ok {
195- return strings.TrimPrefix(field, "--type=")195+ return after
196 }196 }
197 }197 }
198 return "browser"198 return "browser"
@@ -366,7 +366,7 @@ func gpuFDInfoSummaries(pid int) []string {
366 if err != nil {366 if err != nil {
367 continue367 continue
368 }368 }
369- for _, line := range strings.Split(string(b), "\n") {369+ for line := range strings.SplitSeq(string(b), "\n") {
370 if strings.Contains(line, "drm-engine-") || strings.HasPrefix(line, "drm-client-id") || strings.HasPrefix(line, "drm-pdev") {370 if strings.Contains(line, "drm-engine-") || strings.HasPrefix(line, "drm-client-id") || strings.HasPrefix(line, "drm-pdev") {
371 out = append(out, "fd="+entry.Name()+" "+strings.TrimSpace(line))371 out = append(out, "fd="+entry.Name()+" "+strings.TrimSpace(line))
372 }372 }
M internal/infrastructure/cef/profile.go +1 -4
@@ -154,10 +154,7 @@ func (wv *WebView) latestCEF2GTKProfileSnapshot(now time.Time) (cef2gtk.ProfileS
154154 if snapshotAt.IsZero() {
155155 return cef2gtk.ProfileSnapshot{}, 0, false
156156 }
157- age := now.Sub(snapshotAt)
158- if age < 0 {
159- age = 0
160- }
157+ age := max(now.Sub(snapshotAt), 0)
161158 return snapshot, age, true
162159 }
163160
@@ -154,10 +154,7 @@ func (wv *WebView) latestCEF2GTKProfileSnapshot(now time.Time) (cef2gtk.ProfileS
154 if snapshotAt.IsZero() {154 if snapshotAt.IsZero() {
155 return cef2gtk.ProfileSnapshot{}, 0, false155 return cef2gtk.ProfileSnapshot{}, 0, false
156 }156 }
157- age := now.Sub(snapshotAt)157+ age := max(now.Sub(snapshotAt), 0)
158- if age < 0 {
159- age = 0
160- }
161 return snapshot, age, true158 return snapshot, age, true
162 }159 }
163 160
M internal/infrastructure/cef/scale_probe.go +2 -2
@@ -49,8 +49,8 @@ func cefScaleProbeSnapshot(wv *WebView) cefScaleProbeMetrics {
4949 return m
5050 }
5151
52-func (m cefScaleProbeMetrics) logFields() map[string]interface{} {
53- return map[string]interface{}{
52+func (m cefScaleProbeMetrics) logFields() map[string]any {
53+ return map[string]any{
5454 "surface_width": m.SurfaceWidth,
5555 "surface_height": m.SurfaceHeight,
5656 "surface_scale": roundedScale(m.SurfaceScale),
@@ -49,8 +49,8 @@ func cefScaleProbeSnapshot(wv *WebView) cefScaleProbeMetrics {
49 return m49 return m
50 }50 }
51 51
52-func (m cefScaleProbeMetrics) logFields() map[string]interface{} {52+func (m cefScaleProbeMetrics) logFields() map[string]any {
53- return map[string]interface{}{53+ return map[string]any{
54 "surface_width": m.SurfaceWidth,54 "surface_width": m.SurfaceWidth,
55 "surface_height": m.SurfaceHeight,55 "surface_height": m.SurfaceHeight,
56 "surface_scale": roundedScale(m.SurfaceScale),56 "surface_scale": roundedScale(m.SurfaceScale),
M internal/infrastructure/cef/scheme_handler.go +4 -11
@@ -10,6 +10,7 @@ import (
1010 "html"
1111 "io"
1212 "io/fs"
13+ "maps"
1314 "net/http"
1415 "net/url"
1516 "path"
@@ -780,9 +781,7 @@ func apiResponseHeaders(extra map[string]string) map[string]string {
780781 "Access-Control-Max-Age": "86400",
781782 "Cache-Control": "no-store",
782783 }
783- for name, value := range extra {
784- headers[name] = value
785- }
784+ maps.Copy(headers, extra)
786785 return headers
787786 }
788787
@@ -942,10 +941,7 @@ func (rh *faviconResourceHandler) Read(dataOut unsafe.Pointer, bytesToRead int32
942941 return 0
943942 }
944943 remaining := len(rh.data) - rh.offset
945- toRead := int(bytesToRead)
946- if toRead > remaining {
947- toRead = remaining
948- }
944+ toRead := min(int(bytesToRead), remaining)
949945 dst := unsafe.Slice((*byte)(dataOut), toRead)
950946 copy(dst, rh.data[rh.offset:rh.offset+toRead])
951947 rh.offset += toRead
@@ -1017,10 +1013,7 @@ func (rh *staticResourceHandler) Read(
10171013 }
10181014
10191015 remaining := len(rh.data) - rh.offset
1020- toRead := int(bytesToRead)
1021- if toRead > remaining {
1022- toRead = remaining
1023- }
1016+ toRead := min(int(bytesToRead), remaining)
10241017
10251018 // Copy data to the output buffer.
10261019 dst := unsafe.Slice((*byte)(dataOut), toRead)
@@ -10,6 +10,7 @@ import (
10 "html"10 "html"
11 "io"11 "io"
12 "io/fs"12 "io/fs"
13+ "maps"
13 "net/http"14 "net/http"
14 "net/url"15 "net/url"
15 "path"16 "path"
@@ -780,9 +781,7 @@ func apiResponseHeaders(extra map[string]string) map[string]string {
780 "Access-Control-Max-Age": "86400",781 "Access-Control-Max-Age": "86400",
781 "Cache-Control": "no-store",782 "Cache-Control": "no-store",
782 }783 }
783- for name, value := range extra {784+ maps.Copy(headers, extra)
784- headers[name] = value
785- }
786 return headers785 return headers
787 }786 }
788 787
@@ -942,10 +941,7 @@ func (rh *faviconResourceHandler) Read(dataOut unsafe.Pointer, bytesToRead int32
942 return 0941 return 0
943 }942 }
944 remaining := len(rh.data) - rh.offset943 remaining := len(rh.data) - rh.offset
945- toRead := int(bytesToRead)944+ toRead := min(int(bytesToRead), remaining)
946- if toRead > remaining {
947- toRead = remaining
948- }
949 dst := unsafe.Slice((*byte)(dataOut), toRead)945 dst := unsafe.Slice((*byte)(dataOut), toRead)
950 copy(dst, rh.data[rh.offset:rh.offset+toRead])946 copy(dst, rh.data[rh.offset:rh.offset+toRead])
951 rh.offset += toRead947 rh.offset += toRead
@@ -1017,10 +1013,7 @@ func (rh *staticResourceHandler) Read(
1017 }1013 }
1018 1014
1019 remaining := len(rh.data) - rh.offset1015 remaining := len(rh.data) - rh.offset
1020- toRead := int(bytesToRead)1016+ toRead := min(int(bytesToRead), remaining)
1021- if toRead > remaining {
1022- toRead = remaining
1023- }
1024 1017
1025 // Copy data to the output buffer.1018 // Copy data to the output buffer.
1026 dst := unsafe.Slice((*byte)(dataOut), toRead)1019 dst := unsafe.Slice((*byte)(dataOut), toRead)
M internal/infrastructure/colorscheme/resolver_test.go +9 -13
@@ -355,29 +355,25 @@ func TestResolver_ConcurrentAccess(_ *testing.T) {
355355 const goroutines = 10
356356
357357 // Concurrent Resolve calls
358- for i := 0; i < goroutines; i++ {
359- wg.Add(1)
360- go func() {
361- defer wg.Done()
362- for j := 0; j < 100; j++ {
358+ for range goroutines {
359+ wg.Go(func() {
360+ for range 100 {
363361 resolver.Resolve()
364362 }
365- }()
363+ })
366364 }
367365
368366 // Concurrent Refresh calls
369- for i := 0; i < goroutines; i++ {
370- wg.Add(1)
371- go func() {
372- defer wg.Done()
373- for j := 0; j < 100; j++ {
367+ for range goroutines {
368+ wg.Go(func() {
369+ for range 100 {
374370 resolver.Refresh()
375371 }
376- }()
372+ })
377373 }
378374
379375 // Concurrent RegisterDetector calls
380- for i := 0; i < goroutines; i++ {
376+ for i := range goroutines {
381377 wg.Add(1)
382378 go func(id int) {
383379 defer wg.Done()
@@ -355,29 +355,25 @@ func TestResolver_ConcurrentAccess(_ *testing.T) {
355 const goroutines = 10355 const goroutines = 10
356 356
357 // Concurrent Resolve calls357 // Concurrent Resolve calls
358- for i := 0; i < goroutines; i++ {358+ for range goroutines {
359- wg.Add(1)359+ wg.Go(func() {
360- go func() {360+ for range 100 {
361- defer wg.Done()
362- for j := 0; j < 100; j++ {
363 resolver.Resolve()361 resolver.Resolve()
364 }362 }
365- }()363+ })
366 }364 }
367 365
368 // Concurrent Refresh calls366 // Concurrent Refresh calls
369- for i := 0; i < goroutines; i++ {367+ for range goroutines {
370- wg.Add(1)368+ wg.Go(func() {
371- go func() {369+ for range 100 {
372- defer wg.Done()
373- for j := 0; j < 100; j++ {
374 resolver.Refresh()370 resolver.Refresh()
375 }371 }
376- }()372+ })
377 }373 }
378 374
379 // Concurrent RegisterDetector calls375 // Concurrent RegisterDetector calls
380- for i := 0; i < goroutines; i++ {376+ for i := range goroutines {
381 wg.Add(1)377 wg.Add(1)
382 go func(id int) {378 go func(id int) {
383 defer wg.Done()379 defer wg.Done()
M internal/infrastructure/config/migrate.go +3 -6
@@ -384,10 +384,7 @@ func keysAreSimilar(oldKey, newKey string) bool {
384384 }
385385
386386 // If most tokens match, consider similar
387- minTokens := len(oldTokens)
388- if len(newTokens) < minTokens {
389- minTokens = len(newTokens)
390- }
387+ minTokens := min(len(newTokens), len(oldTokens))
391388 return matches >= minTokens-1 && matches > 0
392389 }
393390
@@ -652,8 +649,8 @@ func (m *Migrator) defaultValueForKey(key string) any {
652649 }
653650
654651 for _, actionMap := range m.defaultActionMaps() {
655- if strings.HasPrefix(key, actionMap.key+".") {
656- actionName := strings.TrimPrefix(key, actionMap.key+".")
652+ if after, ok := strings.CutPrefix(key, actionMap.key+"."); ok {
653+ actionName := after
657654 if action, ok := actionMap.actions[actionName]; ok {
658655 return action
659656 }
@@ -384,10 +384,7 @@ func keysAreSimilar(oldKey, newKey string) bool {
384 }384 }
385 385
386 // If most tokens match, consider similar386 // If most tokens match, consider similar
387- minTokens := len(oldTokens)387+ minTokens := min(len(newTokens), len(oldTokens))
388- if len(newTokens) < minTokens {
389- minTokens = len(newTokens)
390- }
391 return matches >= minTokens-1 && matches > 0388 return matches >= minTokens-1 && matches > 0
392 }389 }
393 390
@@ -652,8 +649,8 @@ func (m *Migrator) defaultValueForKey(key string) any {
652 }649 }
653 650
654 for _, actionMap := range m.defaultActionMaps() {651 for _, actionMap := range m.defaultActionMaps() {
655- if strings.HasPrefix(key, actionMap.key+".") {652+ if after, ok := strings.CutPrefix(key, actionMap.key+"."); ok {
656- actionName := strings.TrimPrefix(key, actionMap.key+".")653+ actionName := after
657 if action, ok := actionMap.actions[actionName]; ok {654 if action, ok := actionMap.actions[actionName]; ok {
658 return action655 return action
659 }656 }
M internal/infrastructure/config/profile_test.go +1 -7
@@ -108,13 +108,7 @@ func TestResolvePerformanceProfile_Max_NoHardware(t *testing.T) {
108108
109109 // Max without hardware info should use fallback values
110110 // CPU threads: NumCPU/2 (assuming hyperthreading), clamped to [4, 8]
111- expectedCPUThreads := runtime.NumCPU() / 2
112- if expectedCPUThreads < 4 {
113- expectedCPUThreads = 4
114- }
115- if expectedCPUThreads > 8 {
116- expectedCPUThreads = 8
117- }
111+ expectedCPUThreads := min(max(runtime.NumCPU()/2, 4), 8)
118112 if result.SkiaCPUPaintingThreads != expectedCPUThreads {
119113 t.Errorf("expected SkiaCPUPaintingThreads=%d, got %d", expectedCPUThreads, result.SkiaCPUPaintingThreads)
120114 }
@@ -108,13 +108,7 @@ func TestResolvePerformanceProfile_Max_NoHardware(t *testing.T) {
108 108
109 // Max without hardware info should use fallback values109 // Max without hardware info should use fallback values
110 // CPU threads: NumCPU/2 (assuming hyperthreading), clamped to [4, 8]110 // CPU threads: NumCPU/2 (assuming hyperthreading), clamped to [4, 8]
111- expectedCPUThreads := runtime.NumCPU() / 2111+ expectedCPUThreads := min(max(runtime.NumCPU()/2, 4), 8)
112- if expectedCPUThreads < 4 {
113- expectedCPUThreads = 4
114- }
115- if expectedCPUThreads > 8 {
116- expectedCPUThreads = 8
117- }
118 if result.SkiaCPUPaintingThreads != expectedCPUThreads {112 if result.SkiaCPUPaintingThreads != expectedCPUThreads {
119 t.Errorf("expected SkiaCPUPaintingThreads=%d, got %d", expectedCPUThreads, result.SkiaCPUPaintingThreads)113 t.Errorf("expected SkiaCPUPaintingThreads=%d, got %d", expectedCPUThreads, result.SkiaCPUPaintingThreads)
120 }114 }
M internal/infrastructure/deps/env.go +1 -1
@@ -107,7 +107,7 @@ func prependPathList(existing string, values ...string) string {
107107 }
108108
109109 if existing != "" {
110- for _, v := range strings.Split(existing, ":") {
110+ for v := range strings.SplitSeq(existing, ":") {
111111 add(v)
112112 }
113113 }
@@ -107,7 +107,7 @@ func prependPathList(existing string, values ...string) string {
107 }107 }
108 108
109 if existing != "" {109 if existing != "" {
110- for _, v := range strings.Split(existing, ":") {110+ for v := range strings.SplitSeq(existing, ":") {
111 add(v)111 add(v)
112 }112 }
113 }113 }
M internal/infrastructure/desktop/adapter_test.go +1 -1
@@ -144,7 +144,7 @@ func readEnvFile(t *testing.T, path string) map[string]string {
144144 data, err := os.ReadFile(path)
145145 if err == nil {
146146 env := make(map[string]string)
147- for _, line := range strings.Split(strings.TrimSpace(string(data)), "\n") {
147+ for line := range strings.SplitSeq(strings.TrimSpace(string(data)), "\n") {
148148 if line == "" {
149149 continue
150150 }
@@ -144,7 +144,7 @@ func readEnvFile(t *testing.T, path string) map[string]string {
144 data, err := os.ReadFile(path)144 data, err := os.ReadFile(path)
145 if err == nil {145 if err == nil {
146 env := make(map[string]string)146 env := make(map[string]string)
147- for _, line := range strings.Split(strings.TrimSpace(string(data)), "\n") {147+ for line := range strings.SplitSeq(strings.TrimSpace(string(data)), "\n") {
148 if line == "" {148 if line == "" {
149 continue149 continue
150 }150 }
M internal/infrastructure/desktop/browser_launch_relay_test.go +9 -18
@@ -127,8 +127,7 @@ func TestBrowserLaunchRelay_SameProfileAndEngine_UsesSameSocket(t *testing.T) {
127127 clientRelay := NewBrowserLaunchRelay(ipc)
128128
129129 received := make(chan string, 1)
130- ctx, cancel := context.WithCancel(context.Background())
131- defer cancel()
130+ ctx := t.Context()
132131
133132 closer, err := listenerRelay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
134133 received <- url
@@ -229,8 +228,7 @@ func TestBrowserLaunchRelay_DeliverOpenFreshWindow_RoundTrip(t *testing.T) {
229228 relay := NewBrowserLaunchRelay(ipc)
230229
231230 received := make(chan string, 1)
232- ctx, cancel := context.WithCancel(context.Background())
233- defer cancel()
231+ ctx := t.Context()
234232
235233 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
236234 received <- url
@@ -267,8 +265,7 @@ func TestBrowserLaunchRelay_SecondListenWhileLiveFailsWithoutReplacingListener(t
267265 relay := NewBrowserLaunchRelay(ipc)
268266
269267 firstReceived := make(chan string, 1)
270- ctx, cancel := context.WithCancel(context.Background())
271- defer cancel()
268+ ctx := t.Context()
272269
273270 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
274271 firstReceived <- url
@@ -302,8 +299,7 @@ func TestBrowserLaunchRelay_RebindsStaleSocketPath(t *testing.T) {
302299 require.NoError(t, os.WriteFile(ipc.BrowserLaunchSocket, []byte("stale"), 0o600))
303300
304301 received := make(chan string, 1)
305- ctx, cancel := context.WithCancel(context.Background())
306- defer cancel()
302+ ctx := t.Context()
307303
308304 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
309305 received <- url
@@ -331,8 +327,7 @@ func TestBrowserLaunchRelay_RejectsMalformedPayloads(t *testing.T) {
331327 relay := NewBrowserLaunchRelay(ipc)
332328
333329 received := make(chan string, 1)
334- ctx, cancel := context.WithCancel(context.Background())
335- defer cancel()
330+ ctx := t.Context()
336331
337332 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
338333 received <- url
@@ -367,8 +362,7 @@ func TestBrowserLaunchRelay_CloseRemovesSocketPath(t *testing.T) {
367362 ipc := testIPC(shortTempDir(t))
368363 relay := NewBrowserLaunchRelay(ipc)
369364
370- ctx, cancel := context.WithCancel(context.Background())
371- defer cancel()
365+ ctx := t.Context()
372366
373367 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(context.Context, string) error { return nil }))
374368 require.NoError(t, err)
@@ -486,8 +480,7 @@ func TestBrowserLaunchRelay_AcknowledgesBeforeOpenerReturns(t *testing.T) {
486480
487481 started := make(chan struct{})
488482 release := make(chan struct{})
489- ctx, cancel := context.WithCancel(context.Background())
490- defer cancel()
483+ ctx := t.Context()
491484
492485 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, _ string) error {
493486 close(started)
@@ -524,8 +517,7 @@ func TestBrowserLaunchRelay_AcknowledgesAcceptedEvenWhenOpenerReturnsLateError(t
524517 relay := NewBrowserLaunchRelay(ipc)
525518
526519 started := make(chan struct{})
527- ctx, cancel := context.WithCancel(context.Background())
528- defer cancel()
520+ ctx := t.Context()
529521
530522 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, _ string) error {
531523 close(started)
@@ -560,8 +552,7 @@ func TestBrowserLaunchRelay_SilentClientDoesNotStallListener(t *testing.T) {
560552 relay := NewBrowserLaunchRelay(ipc)
561553
562554 received := make(chan string, 1)
563- ctx, cancel := context.WithCancel(context.Background())
564- defer cancel()
555+ ctx := t.Context()
565556
566557 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
567558 received <- url
@@ -127,8 +127,7 @@ func TestBrowserLaunchRelay_SameProfileAndEngine_UsesSameSocket(t *testing.T) {
127 clientRelay := NewBrowserLaunchRelay(ipc)127 clientRelay := NewBrowserLaunchRelay(ipc)
128 128
129 received := make(chan string, 1)129 received := make(chan string, 1)
130- ctx, cancel := context.WithCancel(context.Background())130+ ctx := t.Context()
131- defer cancel()
132 131
133 closer, err := listenerRelay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {132 closer, err := listenerRelay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
134 received <- url133 received <- url
@@ -229,8 +228,7 @@ func TestBrowserLaunchRelay_DeliverOpenFreshWindow_RoundTrip(t *testing.T) {
229 relay := NewBrowserLaunchRelay(ipc)228 relay := NewBrowserLaunchRelay(ipc)
230 229
231 received := make(chan string, 1)230 received := make(chan string, 1)
232- ctx, cancel := context.WithCancel(context.Background())231+ ctx := t.Context()
233- defer cancel()
234 232
235 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {233 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
236 received <- url234 received <- url
@@ -267,8 +265,7 @@ func TestBrowserLaunchRelay_SecondListenWhileLiveFailsWithoutReplacingListener(t
267 relay := NewBrowserLaunchRelay(ipc)265 relay := NewBrowserLaunchRelay(ipc)
268 266
269 firstReceived := make(chan string, 1)267 firstReceived := make(chan string, 1)
270- ctx, cancel := context.WithCancel(context.Background())268+ ctx := t.Context()
271- defer cancel()
272 269
273 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {270 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
274 firstReceived <- url271 firstReceived <- url
@@ -302,8 +299,7 @@ func TestBrowserLaunchRelay_RebindsStaleSocketPath(t *testing.T) {
302 require.NoError(t, os.WriteFile(ipc.BrowserLaunchSocket, []byte("stale"), 0o600))299 require.NoError(t, os.WriteFile(ipc.BrowserLaunchSocket, []byte("stale"), 0o600))
303 300
304 received := make(chan string, 1)301 received := make(chan string, 1)
305- ctx, cancel := context.WithCancel(context.Background())302+ ctx := t.Context()
306- defer cancel()
307 303
308 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {304 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
309 received <- url305 received <- url
@@ -331,8 +327,7 @@ func TestBrowserLaunchRelay_RejectsMalformedPayloads(t *testing.T) {
331 relay := NewBrowserLaunchRelay(ipc)327 relay := NewBrowserLaunchRelay(ipc)
332 328
333 received := make(chan string, 1)329 received := make(chan string, 1)
334- ctx, cancel := context.WithCancel(context.Background())330+ ctx := t.Context()
335- defer cancel()
336 331
337 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {332 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
338 received <- url333 received <- url
@@ -367,8 +362,7 @@ func TestBrowserLaunchRelay_CloseRemovesSocketPath(t *testing.T) {
367 ipc := testIPC(shortTempDir(t))362 ipc := testIPC(shortTempDir(t))
368 relay := NewBrowserLaunchRelay(ipc)363 relay := NewBrowserLaunchRelay(ipc)
369 364
370- ctx, cancel := context.WithCancel(context.Background())365+ ctx := t.Context()
371- defer cancel()
372 366
373 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(context.Context, string) error { return nil }))367 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(context.Context, string) error { return nil }))
374 require.NoError(t, err)368 require.NoError(t, err)
@@ -486,8 +480,7 @@ func TestBrowserLaunchRelay_AcknowledgesBeforeOpenerReturns(t *testing.T) {
486 480
487 started := make(chan struct{})481 started := make(chan struct{})
488 release := make(chan struct{})482 release := make(chan struct{})
489- ctx, cancel := context.WithCancel(context.Background())483+ ctx := t.Context()
490- defer cancel()
491 484
492 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, _ string) error {485 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, _ string) error {
493 close(started)486 close(started)
@@ -524,8 +517,7 @@ func TestBrowserLaunchRelay_AcknowledgesAcceptedEvenWhenOpenerReturnsLateError(t
524 relay := NewBrowserLaunchRelay(ipc)517 relay := NewBrowserLaunchRelay(ipc)
525 518
526 started := make(chan struct{})519 started := make(chan struct{})
527- ctx, cancel := context.WithCancel(context.Background())520+ ctx := t.Context()
528- defer cancel()
529 521
530 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, _ string) error {522 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, _ string) error {
531 close(started)523 close(started)
@@ -560,8 +552,7 @@ func TestBrowserLaunchRelay_SilentClientDoesNotStallListener(t *testing.T) {
560 relay := NewBrowserLaunchRelay(ipc)552 relay := NewBrowserLaunchRelay(ipc)
561 553
562 received := make(chan string, 1)554 received := make(chan string, 1)
563- ctx, cancel := context.WithCancel(context.Background())555+ ctx := t.Context()
564- defer cancel()
565 556
566 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {557 closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error {
567 received <- url558 received <- url
M internal/infrastructure/env/hardware_survey.go +2 -2
@@ -276,9 +276,9 @@ func (*HardwareSurveyor) detectGPUName(ctx context.Context, cardPath string) str
276276 scanner := bufio.NewScanner(file)
277277 for scanner.Scan() {
278278 line := scanner.Text()
279- if strings.HasPrefix(line, "PCI_SLOT_NAME=") {
279+ if after, ok := strings.CutPrefix(line, "PCI_SLOT_NAME="); ok {
280280 // This gives us the PCI address, useful for debugging
281- log.Debug().Str("pci_slot", strings.TrimPrefix(line, "PCI_SLOT_NAME=")).Msg("GPU PCI slot")
281+ log.Debug().Str("pci_slot", after).Msg("GPU PCI slot")
282282 }
283283 }
284284 _ = file.Close()
@@ -276,9 +276,9 @@ func (*HardwareSurveyor) detectGPUName(ctx context.Context, cardPath string) str
276 scanner := bufio.NewScanner(file)276 scanner := bufio.NewScanner(file)
277 for scanner.Scan() {277 for scanner.Scan() {
278 line := scanner.Text()278 line := scanner.Text()
279- if strings.HasPrefix(line, "PCI_SLOT_NAME=") {279+ if after, ok := strings.CutPrefix(line, "PCI_SLOT_NAME="); ok {
280 // This gives us the PCI address, useful for debugging280 // This gives us the PCI address, useful for debugging
281- log.Debug().Str("pci_slot", strings.TrimPrefix(line, "PCI_SLOT_NAME=")).Msg("GPU PCI slot")281+ log.Debug().Str("pci_slot", after).Msg("GPU PCI slot")
282 }282 }
283 }283 }
284 _ = file.Close()284 _ = file.Close()
M internal/infrastructure/env/manager.go +3 -7
@@ -4,6 +4,7 @@ package env
44 import (
55 "context"
66 "fmt"
7+ "maps"
78 "os"
89 "os/exec"
910 "path/filepath"
@@ -118,10 +119,7 @@ func (m *Manager) applyGStreamerEnv(settings RenderingSettings) {
118119
119120 // GStreamer debug level
120121 if settings.GStreamerDebugLevel > 0 {
121- level := settings.GStreamerDebugLevel
122- if level > 5 {
123- level = 5
124- }
122+ level := min(settings.GStreamerDebugLevel, 5)
125123 m.setEnv("GST_DEBUG", fmt.Sprintf("%d", level))
126124 }
127125
@@ -221,9 +219,7 @@ func (m *Manager) setEnv(key, value string) {
221219 // GetAppliedVars returns a copy of applied environment variables.
222220 func (m *Manager) GetAppliedVars() map[string]string {
223221 result := make(map[string]string, len(m.appliedVars))
224- for k, v := range m.appliedVars {
225- result[k] = v
226- }
222+ maps.Copy(result, m.appliedVars)
227223 return result
228224 }
229225
@@ -4,6 +4,7 @@ package env
4 import (4 import (
5 "context"5 "context"
6 "fmt"6 "fmt"
7+ "maps"
7 "os"8 "os"
8 "os/exec"9 "os/exec"
9 "path/filepath"10 "path/filepath"
@@ -118,10 +119,7 @@ func (m *Manager) applyGStreamerEnv(settings RenderingSettings) {
118 119
119 // GStreamer debug level120 // GStreamer debug level
120 if settings.GStreamerDebugLevel > 0 {121 if settings.GStreamerDebugLevel > 0 {
121- level := settings.GStreamerDebugLevel122+ level := min(settings.GStreamerDebugLevel, 5)
122- if level > 5 {
123- level = 5
124- }
125 m.setEnv("GST_DEBUG", fmt.Sprintf("%d", level))123 m.setEnv("GST_DEBUG", fmt.Sprintf("%d", level))
126 }124 }
127 125
@@ -221,9 +219,7 @@ func (m *Manager) setEnv(key, value string) {
221 // GetAppliedVars returns a copy of applied environment variables.219 // GetAppliedVars returns a copy of applied environment variables.
222 func (m *Manager) GetAppliedVars() map[string]string {220 func (m *Manager) GetAppliedVars() map[string]string {
223 result := make(map[string]string, len(m.appliedVars))221 result := make(map[string]string, len(m.appliedVars))
224- for k, v := range m.appliedVars {222+ maps.Copy(result, m.appliedVars)
225- result[k] = v
226- }
227 return result223 return result
228 }224 }
229 225
M internal/infrastructure/favicon/converter.go +3 -3
@@ -118,7 +118,7 @@ func decodeICO(data []byte) (image.Image, error) {
118118 var best []byte
119119 bestBitCount := 0
120120 bestArea := -1
121- for i := 0; i < count; i++ {
121+ for i := range count {
122122 entry := data[6+i*16 : 6+(i+1)*16]
123123 w, h := int(entry[0]), int(entry[1])
124124 if w == 0 {
@@ -192,10 +192,10 @@ func decodeICODIB(data []byte, bitCountHint int) (image.Image, error) {
192192 }
193193 img := image.NewNRGBA(image.Rect(0, 0, width, height))
194194 const opaqueAlpha = 255
195- for y := 0; y < height; y++ {
195+ for y := range height {
196196 srcY := height - 1 - y
197197 row := data[pixelOffset+srcY*stride:]
198- for x := 0; x < width; x++ {
198+ for x := range width {
199199 i := x * bytesPerPixel
200200 b, g, r := row[i], row[i+1], row[i+2]
201201 a := byte(opaqueAlpha)
@@ -118,7 +118,7 @@ func decodeICO(data []byte) (image.Image, error) {
118 var best []byte118 var best []byte
119 bestBitCount := 0119 bestBitCount := 0
120 bestArea := -1120 bestArea := -1
121- for i := 0; i < count; i++ {121+ for i := range count {
122 entry := data[6+i*16 : 6+(i+1)*16]122 entry := data[6+i*16 : 6+(i+1)*16]
123 w, h := int(entry[0]), int(entry[1])123 w, h := int(entry[0]), int(entry[1])
124 if w == 0 {124 if w == 0 {
@@ -192,10 +192,10 @@ func decodeICODIB(data []byte, bitCountHint int) (image.Image, error) {
192 }192 }
193 img := image.NewNRGBA(image.Rect(0, 0, width, height))193 img := image.NewNRGBA(image.Rect(0, 0, width, height))
194 const opaqueAlpha = 255194 const opaqueAlpha = 255
195- for y := 0; y < height; y++ {195+ for y := range height {
196 srcY := height - 1 - y196 srcY := height - 1 - y
197 row := data[pixelOffset+srcY*stride:]197 row := data[pixelOffset+srcY*stride:]
198- for x := 0; x < width; x++ {198+ for x := range width {
199 i := x * bytesPerPixel199 i := x * bytesPerPixel
200 b, g, r := row[i], row[i+1], row[i+2]200 b, g, r := row[i], row[i+1], row[i+2]
201 a := byte(opaqueAlpha)201 a := byte(opaqueAlpha)
M internal/infrastructure/favicon/converter_test.go +2 -2
@@ -139,8 +139,8 @@ func testICOWithPayload(t *testing.T, payload []byte, bitCount byte) []byte {
139139 func testPNG(t *testing.T, w, h int) []byte {
140140 t.Helper()
141141 img := image.NewRGBA(image.Rect(0, 0, w, h))
142- for y := 0; y < h; y++ {
143- for x := 0; x < w; x++ {
142+ for y := range h {
143+ for x := range w {
144144 img.Set(x, y, color.RGBA{R: 200, A: 255})
145145 }
146146 }
@@ -139,8 +139,8 @@ func testICOWithPayload(t *testing.T, payload []byte, bitCount byte) []byte {
139 func testPNG(t *testing.T, w, h int) []byte {139 func testPNG(t *testing.T, w, h int) []byte {
140 t.Helper()140 t.Helper()
141 img := image.NewRGBA(image.Rect(0, 0, w, h))141 img := image.NewRGBA(image.Rect(0, 0, w, h))
142- for y := 0; y < h; y++ {142+ for y := range h {
143- for x := 0; x < w; x++ {143+ for x := range w {
144 img.Set(x, y, color.RGBA{R: 200, A: 255})144 img.Set(x, y, color.RGBA{R: 200, A: 255})
145 }145 }
146 }146 }
M internal/infrastructure/filtering/downloader.go +2 -2
@@ -340,8 +340,8 @@ func validateFilterFilename(filename string) (string, error) {
340340 return "", fmt.Errorf("invalid filter filename %q: URL control characters are not allowed", filename)
341341 }
342342
343- parts := strings.Split(filename, "/")
344- for _, part := range parts {
343+ parts := strings.SplitSeq(filename, "/")
344+ for part := range parts {
345345 if part == "" || part == "." || part == ".." {
346346 return "", fmt.Errorf("invalid filter filename %q: path traversal components are not allowed", filename)
347347 }
@@ -340,8 +340,8 @@ func validateFilterFilename(filename string) (string, error) {
340 return "", fmt.Errorf("invalid filter filename %q: URL control characters are not allowed", filename)340 return "", fmt.Errorf("invalid filter filename %q: URL control characters are not allowed", filename)
341 }341 }
342 342
343- parts := strings.Split(filename, "/")343+ parts := strings.SplitSeq(filename, "/")
344- for _, part := range parts {344+ for part := range parts {
345 if part == "" || part == "." || part == ".." {345 if part == "" || part == "." || part == ".." {
346 return "", fmt.Errorf("invalid filter filename %q: path traversal components are not allowed", filename)346 return "", fmt.Errorf("invalid filter filename %q: path traversal components are not allowed", filename)
347 }347 }
M internal/infrastructure/filtering/store.go +2 -6
@@ -3,6 +3,7 @@ package filtering
33 import (
44 "context"
55 "fmt"
6+ "slices"
67 "sync"
78
89 "github.com/bnema/dumber/internal/logging"
@@ -248,10 +249,5 @@ func (s *Store) HasCompiledFilter(ctx context.Context, identifier string) bool {
248249 if err != nil {
249250 return false
250251 }
251- for _, id := range identifiers {
252- if id == identifier {
253- return true
254- }
255- }
256- return false
252+ return slices.Contains(identifiers, identifier)
257253 }
@@ -3,6 +3,7 @@ package filtering
3 import (3 import (
4 "context"4 "context"
5 "fmt"5 "fmt"
6+ "slices"
6 "sync"7 "sync"
7 8
8 "github.com/bnema/dumber/internal/logging"9 "github.com/bnema/dumber/internal/logging"
@@ -248,10 +249,5 @@ func (s *Store) HasCompiledFilter(ctx context.Context, identifier string) bool {
248 if err != nil {249 if err != nil {
249 return false250 return false
250 }251 }
251- for _, id := range identifiers {252+ return slices.Contains(identifiers, identifier)
252- if id == identifier {
253- return true
254- }
255- }
256- return false
257 }253 }
M internal/infrastructure/fonts/detector.go +2 -2
@@ -180,8 +180,8 @@ func (*Detector) queryFonts(ctx context.Context) ([]string, error) {
180180 }
181181 // fc-list may return comma-separated families for fonts with aliases.
182182 // e.g., "DejaVu Sans,DejaVu Sans Light"
183- families := strings.Split(line, ",")
184- for _, family := range families {
183+ families := strings.SplitSeq(line, ",")
184+ for family := range families {
185185 family = strings.TrimSpace(family)
186186 if family != "" {
187187 fontSet[family] = struct{}{}
@@ -180,8 +180,8 @@ func (*Detector) queryFonts(ctx context.Context) ([]string, error) {
180 }180 }
181 // fc-list may return comma-separated families for fonts with aliases.181 // fc-list may return comma-separated families for fonts with aliases.
182 // e.g., "DejaVu Sans,DejaVu Sans Light"182 // e.g., "DejaVu Sans,DejaVu Sans Light"
183- families := strings.Split(line, ",")183+ families := strings.SplitSeq(line, ",")
184- for _, family := range families {184+ for family := range families {
185 family = strings.TrimSpace(family)185 family = strings.TrimSpace(family)
186 if family != "" {186 if family != "" {
187 fontSet[family] = struct{}{}187 fontSet[family] = struct{}{}
M internal/infrastructure/fonts/detector_test.go +4 -21
@@ -2,6 +2,7 @@ package fonts
22
33 import (
44 "context"
5+ "slices"
56 "testing"
67
78 "github.com/bnema/dumber/internal/application/port"
@@ -71,13 +72,7 @@ func TestDetector_SelectBestFont_SansSerif(t *testing.T) {
7172 assert.NotEmpty(t, font)
7273
7374 // Should either be a font from the chain or the generic fallback
74- isFromChain := false
75- for _, f := range SansSerifFallbackChain() {
76- if font == f {
77- isFromChain = true
78- break
79- }
80- }
75+ isFromChain := slices.Contains(SansSerifFallbackChain(), font)
8176 if !isFromChain {
8277 assert.Equal(t, "sans-serif", font, "should fall back to generic if no chain fonts available")
8378 }
@@ -96,13 +91,7 @@ func TestDetector_SelectBestFont_Serif(t *testing.T) {
9691 font := detector.SelectBestFont(ctx, port.FontCategorySerif, SerifFallbackChain())
9792 assert.NotEmpty(t, font)
9893
99- isFromChain := false
100- for _, f := range SerifFallbackChain() {
101- if font == f {
102- isFromChain = true
103- break
104- }
105- }
94+ isFromChain := slices.Contains(SerifFallbackChain(), font)
10695 if !isFromChain {
10796 assert.Equal(t, "serif", font, "should fall back to generic if no chain fonts available")
10897 }
@@ -121,13 +110,7 @@ func TestDetector_SelectBestFont_Monospace(t *testing.T) {
121110 font := detector.SelectBestFont(ctx, port.FontCategoryMonospace, MonospaceFallbackChain())
122111 assert.NotEmpty(t, font)
123112
124- isFromChain := false
125- for _, f := range MonospaceFallbackChain() {
126- if font == f {
127- isFromChain = true
128- break
129- }
130- }
113+ isFromChain := slices.Contains(MonospaceFallbackChain(), font)
131114 if !isFromChain {
132115 assert.Equal(t, "monospace", font, "should fall back to generic if no chain fonts available")
133116 }
@@ -2,6 +2,7 @@ package fonts
2 2
3 import (3 import (
4 "context"4 "context"
5+ "slices"
5 "testing"6 "testing"
6 7
7 "github.com/bnema/dumber/internal/application/port"8 "github.com/bnema/dumber/internal/application/port"
@@ -71,13 +72,7 @@ func TestDetector_SelectBestFont_SansSerif(t *testing.T) {
71 assert.NotEmpty(t, font)72 assert.NotEmpty(t, font)
72 73
73 // Should either be a font from the chain or the generic fallback74 // Should either be a font from the chain or the generic fallback
74- isFromChain := false75+ isFromChain := slices.Contains(SansSerifFallbackChain(), font)
75- for _, f := range SansSerifFallbackChain() {
76- if font == f {
77- isFromChain = true
78- break
79- }
80- }
81 if !isFromChain {76 if !isFromChain {
82 assert.Equal(t, "sans-serif", font, "should fall back to generic if no chain fonts available")77 assert.Equal(t, "sans-serif", font, "should fall back to generic if no chain fonts available")
83 }78 }
@@ -96,13 +91,7 @@ func TestDetector_SelectBestFont_Serif(t *testing.T) {
96 font := detector.SelectBestFont(ctx, port.FontCategorySerif, SerifFallbackChain())91 font := detector.SelectBestFont(ctx, port.FontCategorySerif, SerifFallbackChain())
97 assert.NotEmpty(t, font)92 assert.NotEmpty(t, font)
98 93
99- isFromChain := false94+ isFromChain := slices.Contains(SerifFallbackChain(), font)
100- for _, f := range SerifFallbackChain() {
101- if font == f {
102- isFromChain = true
103- break
104- }
105- }
106 if !isFromChain {95 if !isFromChain {
107 assert.Equal(t, "serif", font, "should fall back to generic if no chain fonts available")96 assert.Equal(t, "serif", font, "should fall back to generic if no chain fonts available")
108 }97 }
@@ -121,13 +110,7 @@ func TestDetector_SelectBestFont_Monospace(t *testing.T) {
121 font := detector.SelectBestFont(ctx, port.FontCategoryMonospace, MonospaceFallbackChain())110 font := detector.SelectBestFont(ctx, port.FontCategoryMonospace, MonospaceFallbackChain())
122 assert.NotEmpty(t, font)111 assert.NotEmpty(t, font)
123 112
124- isFromChain := false113+ isFromChain := slices.Contains(MonospaceFallbackChain(), font)
125- for _, f := range MonospaceFallbackChain() {
126- if font == f {
127- isFromChain = true
128- break
129- }
130- }
131 if !isFromChain {114 if !isFromChain {
132 assert.Equal(t, "monospace", font, "should fall back to generic if no chain fonts available")115 assert.Equal(t, "monospace", font, "should fall back to generic if no chain fonts available")
133 }116 }
M internal/infrastructure/media/diagnostics.go +7 -7
@@ -93,8 +93,8 @@ func (a *Adapter) parseVADecoders(output string, r *port.MediaDiagnosticsResult)
9393 if a == nil {
9494 return
9595 }
96- lines := strings.Split(output, "\n")
97- for _, line := range lines {
96+ lines := strings.SplitSeq(output, "\n")
97+ for line := range lines {
9898 line = strings.TrimSpace(line)
9999 // Format: " vah264dec: VA-API H.264 Decoder"
100100 if strings.HasPrefix(line, "vaav1dec") {
@@ -117,8 +117,8 @@ func (a *Adapter) parseVAAPIDecoders(output string, r *port.MediaDiagnosticsResu
117117 if a == nil {
118118 return
119119 }
120- lines := strings.Split(output, "\n")
121- for _, line := range lines {
120+ lines := strings.SplitSeq(output, "\n")
121+ for line := range lines {
122122 line = strings.TrimSpace(line)
123123 // Format: " vaapih264dec: VA-API H264 decoder"
124124 if strings.HasPrefix(line, "vaapiav1dec") {
@@ -141,8 +141,8 @@ func (a *Adapter) parseNVCodecDecoders(output string, r *port.MediaDiagnosticsRe
141141 if a == nil {
142142 return
143143 }
144- lines := strings.Split(output, "\n")
145- for _, line := range lines {
144+ lines := strings.SplitSeq(output, "\n")
145+ for line := range lines {
146146 line = strings.TrimSpace(line)
147147 // Format: " nvav1dec: NVDEC AV1 Decoder"
148148 if strings.HasPrefix(line, "nvav1dec") {
@@ -189,7 +189,7 @@ func (a *Adapter) checkVAAPI(ctx context.Context, r *port.MediaDiagnosticsResult
189189 r.VAAPIAvailable = true
190190
191191 // Parse driver name and version from vainfo output
192- for _, line := range strings.Split(output, "\n") {
192+ for line := range strings.SplitSeq(output, "\n") {
193193 line = strings.TrimSpace(line)
194194
195195 // Extract driver from "vainfo: Driver version: ..."
@@ -93,8 +93,8 @@ func (a *Adapter) parseVADecoders(output string, r *port.MediaDiagnosticsResult)
93 if a == nil {93 if a == nil {
94 return94 return
95 }95 }
96- lines := strings.Split(output, "\n")96+ lines := strings.SplitSeq(output, "\n")
97- for _, line := range lines {97+ for line := range lines {
98 line = strings.TrimSpace(line)98 line = strings.TrimSpace(line)
99 // Format: " vah264dec: VA-API H.264 Decoder"99 // Format: " vah264dec: VA-API H.264 Decoder"
100 if strings.HasPrefix(line, "vaav1dec") {100 if strings.HasPrefix(line, "vaav1dec") {
@@ -117,8 +117,8 @@ func (a *Adapter) parseVAAPIDecoders(output string, r *port.MediaDiagnosticsResu
117 if a == nil {117 if a == nil {
118 return118 return
119 }119 }
120- lines := strings.Split(output, "\n")120+ lines := strings.SplitSeq(output, "\n")
121- for _, line := range lines {121+ for line := range lines {
122 line = strings.TrimSpace(line)122 line = strings.TrimSpace(line)
123 // Format: " vaapih264dec: VA-API H264 decoder"123 // Format: " vaapih264dec: VA-API H264 decoder"
124 if strings.HasPrefix(line, "vaapiav1dec") {124 if strings.HasPrefix(line, "vaapiav1dec") {
@@ -141,8 +141,8 @@ func (a *Adapter) parseNVCodecDecoders(output string, r *port.MediaDiagnosticsRe
141 if a == nil {141 if a == nil {
142 return142 return
143 }143 }
144- lines := strings.Split(output, "\n")144+ lines := strings.SplitSeq(output, "\n")
145- for _, line := range lines {145+ for line := range lines {
146 line = strings.TrimSpace(line)146 line = strings.TrimSpace(line)
147 // Format: " nvav1dec: NVDEC AV1 Decoder"147 // Format: " nvav1dec: NVDEC AV1 Decoder"
148 if strings.HasPrefix(line, "nvav1dec") {148 if strings.HasPrefix(line, "nvav1dec") {
@@ -189,7 +189,7 @@ func (a *Adapter) checkVAAPI(ctx context.Context, r *port.MediaDiagnosticsResult
189 r.VAAPIAvailable = true189 r.VAAPIAvailable = true
190 190
191 // Parse driver name and version from vainfo output191 // Parse driver name and version from vainfo output
192- for _, line := range strings.Split(output, "\n") {192+ for line := range strings.SplitSeq(output, "\n") {
193 line = strings.TrimSpace(line)193 line = strings.TrimSpace(line)
194 194
195 // Extract driver from "vainfo: Driver version: ..."195 // Extract driver from "vainfo: Driver version: ..."
M internal/infrastructure/persistence/sqlite/favorite_repo.go +1 -4
@@ -149,10 +149,7 @@ func (r *favoriteRepo) hydrateTags(ctx context.Context, favorites []*entity.Favo
149149 }
150150
151151 for start := 0; start < len(ids); start += favoriteTagHydrationBatchSize {
152- end := start + favoriteTagHydrationBatchSize
153- if end > len(ids) {
154- end = len(ids)
155- }
152+ end := min(start+favoriteTagHydrationBatchSize, len(ids))
156153 rows, err := r.queries.GetTagsForFavorites(ctx, ids[start:end])
157154 if err != nil {
158155 return err
@@ -149,10 +149,7 @@ func (r *favoriteRepo) hydrateTags(ctx context.Context, favorites []*entity.Favo
149 }149 }
150 150
151 for start := 0; start < len(ids); start += favoriteTagHydrationBatchSize {151 for start := 0; start < len(ids); start += favoriteTagHydrationBatchSize {
152- end := start + favoriteTagHydrationBatchSize152+ end := min(start+favoriteTagHydrationBatchSize, len(ids))
153- if end > len(ids) {
154- end = len(ids)
155- }
156 rows, err := r.queries.GetTagsForFavorites(ctx, ids[start:end])153 rows, err := r.queries.GetTagsForFavorites(ctx, ids[start:end])
157 if err != nil {154 if err != nil {
158 return err155 return err
M internal/infrastructure/persistence/sqlite/history_repo_test.go +5 -5
@@ -93,10 +93,10 @@ func TestHistoryRepository_GetMostVisited(t *testing.T) {
9393
9494 // Increment visit counts to create different popularities
9595 // github: 5 visits, google: 3 visits, example: 1 visit
96- for i := 0; i < 4; i++ {
96+ for range 4 {
9797 require.NoError(t, repo.IncrementVisitCount(ctx, "https://github.com"))
9898 }
99- for i := 0; i < 2; i++ {
99+ for range 2 {
100100 require.NoError(t, repo.IncrementVisitCount(ctx, "https://google.com"))
101101 }
102102
@@ -315,7 +315,7 @@ func TestHistoryRepository_GetAllMostVisited(t *testing.T) {
315315 require.NoError(t, repo.Save(ctx, entry2))
316316
317317 // Increment github visits
318- for i := 0; i < 3; i++ {
318+ for range 3 {
319319 require.NoError(t, repo.IncrementVisitCount(ctx, "https://github.com"))
320320 }
321321
@@ -756,7 +756,7 @@ func TestHistoryRepository_AboutBlank_CappedVisitCount(t *testing.T) {
756756 repo := sqlite.NewHistoryRepository(db)
757757
758758 // Save about:blank multiple times - visit count should stay at 1
759- for i := 0; i < 5; i++ {
759+ for range 5 {
760760 require.NoError(t, repo.Save(ctx, &entity.HistoryEntry{
761761 URL: "about:blank",
762762 Title: "New Tab",
@@ -788,7 +788,7 @@ func TestHistoryRepository_AboutBlank_NotDominant(t *testing.T) {
788788 repo := sqlite.NewHistoryRepository(db)
789789
790790 // Save about:blank many times
791- for i := 0; i < 10; i++ {
791+ for range 10 {
792792 require.NoError(t, repo.Save(ctx, &entity.HistoryEntry{
793793 URL: "about:blank",
794794 Title: "New Tab",
@@ -93,10 +93,10 @@ func TestHistoryRepository_GetMostVisited(t *testing.T) {
93 93
94 // Increment visit counts to create different popularities94 // Increment visit counts to create different popularities
95 // github: 5 visits, google: 3 visits, example: 1 visit95 // github: 5 visits, google: 3 visits, example: 1 visit
96- for i := 0; i < 4; i++ {96+ for range 4 {
97 require.NoError(t, repo.IncrementVisitCount(ctx, "https://github.com"))97 require.NoError(t, repo.IncrementVisitCount(ctx, "https://github.com"))
98 }98 }
99- for i := 0; i < 2; i++ {99+ for range 2 {
100 require.NoError(t, repo.IncrementVisitCount(ctx, "https://google.com"))100 require.NoError(t, repo.IncrementVisitCount(ctx, "https://google.com"))
101 }101 }
102 102
@@ -315,7 +315,7 @@ func TestHistoryRepository_GetAllMostVisited(t *testing.T) {
315 require.NoError(t, repo.Save(ctx, entry2))315 require.NoError(t, repo.Save(ctx, entry2))
316 316
317 // Increment github visits317 // Increment github visits
318- for i := 0; i < 3; i++ {318+ for range 3 {
319 require.NoError(t, repo.IncrementVisitCount(ctx, "https://github.com"))319 require.NoError(t, repo.IncrementVisitCount(ctx, "https://github.com"))
320 }320 }
321 321
@@ -756,7 +756,7 @@ func TestHistoryRepository_AboutBlank_CappedVisitCount(t *testing.T) {
756 repo := sqlite.NewHistoryRepository(db)756 repo := sqlite.NewHistoryRepository(db)
757 757
758 // Save about:blank multiple times - visit count should stay at 1758 // Save about:blank multiple times - visit count should stay at 1
759- for i := 0; i < 5; i++ {759+ for range 5 {
760 require.NoError(t, repo.Save(ctx, &entity.HistoryEntry{760 require.NoError(t, repo.Save(ctx, &entity.HistoryEntry{
761 URL: "about:blank",761 URL: "about:blank",
762 Title: "New Tab",762 Title: "New Tab",
@@ -788,7 +788,7 @@ func TestHistoryRepository_AboutBlank_NotDominant(t *testing.T) {
788 repo := sqlite.NewHistoryRepository(db)788 repo := sqlite.NewHistoryRepository(db)
789 789
790 // Save about:blank many times790 // Save about:blank many times
791- for i := 0; i < 10; i++ {791+ for range 10 {
792 require.NoError(t, repo.Save(ctx, &entity.HistoryEntry{792 require.NoError(t, repo.Save(ctx, &entity.HistoryEntry{
793 URL: "about:blank",793 URL: "about:blank",
794 Title: "New Tab",794 Title: "New Tab",
M internal/infrastructure/persistence/sqlite/lazy_db_test.go +3 -3
@@ -58,7 +58,7 @@ func TestLazyDB_ConcurrentAccess(t *testing.T) {
5858 wg.Add(goroutines)
5959
6060 results := make(chan *struct {
61- db interface{}
61+ db any
6262 err error
6363 }, goroutines)
6464
@@ -67,7 +67,7 @@ func TestLazyDB_ConcurrentAccess(t *testing.T) {
6767 defer wg.Done()
6868 db, err := lazy.DB(ctx)
6969 results <- &struct {
70- db interface{}
70+ db any
7171 err error
7272 }{db, err}
7373 }()
@@ -76,7 +76,7 @@ func TestLazyDB_ConcurrentAccess(t *testing.T) {
7676 wg.Wait()
7777 close(results)
7878
79- var firstDB interface{}
79+ var firstDB any
8080 for result := range results {
8181 require.NoError(t, result.err)
8282 require.NotNil(t, result.db)
@@ -58,7 +58,7 @@ func TestLazyDB_ConcurrentAccess(t *testing.T) {
58 wg.Add(goroutines)58 wg.Add(goroutines)
59 59
60 results := make(chan *struct {60 results := make(chan *struct {
61- db interface{}61+ db any
62 err error62 err error
63 }, goroutines)63 }, goroutines)
64 64
@@ -67,7 +67,7 @@ func TestLazyDB_ConcurrentAccess(t *testing.T) {
67 defer wg.Done()67 defer wg.Done()
68 db, err := lazy.DB(ctx)68 db, err := lazy.DB(ctx)
69 results <- &struct {69 results <- &struct {
70- db interface{}70+ db any
71 err error71 err error
72 }{db, err}72 }{db, err}
73 }()73 }()
@@ -76,7 +76,7 @@ func TestLazyDB_ConcurrentAccess(t *testing.T) {
76 wg.Wait()76 wg.Wait()
77 close(results)77 close(results)
78 78
79- var firstDB interface{}79+ var firstDB any
80 for result := range results {80 for result := range results {
81 require.NoError(t, result.err)81 require.NoError(t, result.err)
82 require.NotNil(t, result.db)82 require.NotNil(t, result.db)
M internal/infrastructure/persistence/sqlite/lazy_repos.go +1 -1
@@ -233,7 +233,7 @@ func (r *LazyHistoryRepository) IncrementVisitCountBy(ctx context.Context, url s
233233 if delta > fallbackCap {
234234 return fmt.Errorf("visit delta %d exceeds fallback cap %d and repo does not support IncrementVisitCountBy", delta, fallbackCap)
235235 }
236- for i := 0; i < delta; i++ {
236+ for range delta {
237237 if err := r.repo.IncrementVisitCount(ctx, url); err != nil {
238238 return err
239239 }
@@ -233,7 +233,7 @@ func (r *LazyHistoryRepository) IncrementVisitCountBy(ctx context.Context, url s
233 if delta > fallbackCap {233 if delta > fallbackCap {
234 return fmt.Errorf("visit delta %d exceeds fallback cap %d and repo does not support IncrementVisitCountBy", delta, fallbackCap)234 return fmt.Errorf("visit delta %d exceeds fallback cap %d and repo does not support IncrementVisitCountBy", delta, fallbackCap)
235 }235 }
236- for i := 0; i < delta; i++ {236+ for range delta {
237 if err := r.repo.IncrementVisitCount(ctx, url); err != nil {237 if err := r.repo.IncrementVisitCount(ctx, url); err != nil {
238 return err238 return err
239 }239 }
M internal/infrastructure/webkit/context_menu.go +1 -1
@@ -105,7 +105,7 @@ func resolveDataURIImageData(imageURI string) (entity.ImageData, error) {
105105
106106 isBase64 := false
107107 mimeType := ""
108- for _, part := range strings.Split(meta, ";") {
108+ for part := range strings.SplitSeq(meta, ";") {
109109 part = strings.TrimSpace(part)
110110 if part == "" {
111111 continue
@@ -105,7 +105,7 @@ func resolveDataURIImageData(imageURI string) (entity.ImageData, error) {
105 105
106 isBase64 := false106 isBase64 := false
107 mimeType := ""107 mimeType := ""
108- for _, part := range strings.Split(meta, ";") {108+ for part := range strings.SplitSeq(meta, ";") {
109 part = strings.TrimSpace(part)109 part = strings.TrimSpace(part)
110 if part == "" {110 if part == "" {
111 continue111 continue
M internal/infrastructure/webkit/context_menu_test.go +2 -5
@@ -404,11 +404,8 @@ func (r *sizedByteReader) Read(p []byte) (int, error) {
404404 if r.remaining <= 0 {
405405 return 0, io.EOF
406406 }
407- n := len(p)
408- if n > r.remaining {
409- n = r.remaining
410- }
411- for i := 0; i < n; i++ {
407+ n := min(len(p), r.remaining)
408+ for i := range n {
412409 p[i] = 'x'
413410 }
414411 r.remaining -= n
@@ -404,11 +404,8 @@ func (r *sizedByteReader) Read(p []byte) (int, error) {
404 if r.remaining <= 0 {404 if r.remaining <= 0 {
405 return 0, io.EOF405 return 0, io.EOF
406 }406 }
407- n := len(p)407+ n := min(len(p), r.remaining)
408- if n > r.remaining {408+ for i := range n {
409- n = r.remaining
410- }
411- for i := 0; i < n; i++ {
412 p[i] = 'x'409 p[i] = 'x'
413 }410 }
414 r.remaining -= n411 r.remaining -= n
M internal/infrastructure/webkit/messaging.go +1 -1
@@ -40,7 +40,7 @@ type MessageRouter struct {
4040 baseCtx context.Context
4141
4242 mu sync.RWMutex
43- callbacks []interface{}
43+ callbacks []any
4444 signals []uint
4545
4646 idMu sync.Mutex
@@ -40,7 +40,7 @@ type MessageRouter struct {
40 baseCtx context.Context40 baseCtx context.Context
41 41
42 mu sync.RWMutex42 mu sync.RWMutex
43- callbacks []interface{}43+ callbacks []any
44 signals []uint44 signals []uint
45 45
46 idMu sync.Mutex46 idMu sync.Mutex
M internal/infrastructure/webkit/pool.go +1 -1
@@ -497,7 +497,7 @@ func (p *WebViewPool) RefreshScripts(ctx context.Context) {
497497
498498 refreshed := 0
499499 refreshLoop:
500- for i := 0; i < count; i++ {
500+ for range count {
501501 select {
502502 case wv := <-p.pool:
503503 if wv == nil || wv.IsDestroyed() {
@@ -497,7 +497,7 @@ func (p *WebViewPool) RefreshScripts(ctx context.Context) {
497 497
498 refreshed := 0498 refreshed := 0
499 refreshLoop:499 refreshLoop:
500- for i := 0; i < count; i++ {500+ for range count {
501 select {501 select {
502 case wv := <-p.pool:502 case wv := <-p.pool:
503 if wv == nil || wv.IsDestroyed() {503 if wv == nil || wv.IsDestroyed() {
M internal/infrastructure/webkit/scheme_handler.go +1 -1
@@ -296,7 +296,7 @@ func isTrustedSystemviewURL(raw string) bool {
296296
297297 func jsonErrorResponse(status int, message string) *SchemeResponse {
298298 return &SchemeResponse{
299- Data: []byte(fmt.Sprintf(`{"error":%q}`, message)),
299+ Data: fmt.Appendf(nil, `{"error":%q}`, message),
300300 ContentType: "application/json",
301301 StatusCode: status,
302302 }
@@ -296,7 +296,7 @@ func isTrustedSystemviewURL(raw string) bool {
296 296
297 func jsonErrorResponse(status int, message string) *SchemeResponse {297 func jsonErrorResponse(status int, message string) *SchemeResponse {
298 return &SchemeResponse{298 return &SchemeResponse{
299- Data: []byte(fmt.Sprintf(`{"error":%q}`, message)),299+ Data: fmt.Appendf(nil, `{"error":%q}`, message),
300 ContentType: "application/json",300 ContentType: "application/json",
301 StatusCode: status,301 StatusCode: status,
302 }302 }
M internal/infrastructure/webkit/webview.go +1 -1
@@ -180,7 +180,7 @@ type WebView struct {
180180 navigationActive atomic.Bool
181181
182182 // asyncCallbacks keeps references to async JS callbacks to prevent GC
183- asyncCallbacks []interface{}
183+ asyncCallbacks []any
184184
185185 // runJSErrorStats aggregates repeated non-fatal RunJavaScript errors by domain+signature.
186186 runJSErrorStats map[string]runJSErrorStat
@@ -180,7 +180,7 @@ type WebView struct {
180 navigationActive atomic.Bool180 navigationActive atomic.Bool
181 181
182 // asyncCallbacks keeps references to async JS callbacks to prevent GC182 // asyncCallbacks keeps references to async JS callbacks to prevent GC
183- asyncCallbacks []interface{}183+ asyncCallbacks []any
184 184
185 // runJSErrorStats aggregates repeated non-fatal RunJavaScript errors by domain+signature.185 // runJSErrorStats aggregates repeated non-fatal RunJavaScript errors by domain+signature.
186 runJSErrorStats map[string]runJSErrorStat186 runJSErrorStats map[string]runJSErrorStat
M internal/ui/app_browser_launch_test.go +7 -16
@@ -11,6 +11,7 @@ import (
1111 "os"
1212 "path/filepath"
1313 "reflect"
14+ "slices"
1415 "strconv"
1516 "strings"
1617 "testing"
@@ -97,7 +98,7 @@ func testGDKBackendAllows(backend string) bool {
9798 if configured == "" {
9899 return true
99100 }
100- for _, candidate := range strings.Split(configured, ",") {
101+ for candidate := range strings.SplitSeq(configured, ",") {
101102 if strings.TrimSpace(candidate) == backend {
102103 return true
103104 }
@@ -117,12 +118,7 @@ func testHasUsableWaylandDisplay() bool {
117118 if runtimeDir := os.Getenv("XDG_RUNTIME_DIR"); runtimeDir != "" && !filepath.IsAbs(waylandDisplay) {
118119 candidates = append([]string{filepath.Join(runtimeDir, waylandDisplay)}, candidates...)
119120 }
120- for _, candidate := range candidates {
121- if testPathIsSocket(candidate) {
122- return true
123- }
124- }
125- return false
121+ return slices.ContainsFunc(candidates, testPathIsSocket)
126122 }
127123
128124 func testHasUsableX11Display() bool {
@@ -133,8 +129,8 @@ func testHasUsableX11Display() bool {
133129 if display == "" {
134130 return false
135131 }
136- if strings.HasPrefix(display, ":") {
137- displayNum := strings.TrimPrefix(display, ":")
132+ if after, ok := strings.CutPrefix(display, ":"); ok {
133+ displayNum := after
138134 displayNum = strings.SplitN(displayNum, ".", 2)[0]
139135 if displayNum == "" {
140136 return false
@@ -143,12 +139,7 @@ func testHasUsableX11Display() bool {
143139 if fallback := "/tmp/.X11-unix/X" + displayNum; fallback != x11SocketCandidates[0] {
144140 x11SocketCandidates = append(x11SocketCandidates, fallback)
145141 }
146- for _, candidate := range x11SocketCandidates {
147- if testPathIsSocket(candidate) {
148- return true
149- }
150- }
151- return false
142+ return slices.ContainsFunc(x11SocketCandidates, testPathIsSocket)
152143 }
153144 return true // TCP display
154145 }
@@ -2191,7 +2182,7 @@ func TestApp_HandlePaneFullscreenChanged_ClearsBottomInset(t *testing.T) {
21912182 }
21922183
21932184 func TestApp_RemoveBrowserWindowPromotesDeterministicFallbackWithMainWindow(t *testing.T) {
2194- for i := 0; i < 20; i++ {
2185+ for range 20 {
21952186 removed := &browserWindow{id: "window-z", mainWindow: &window.MainWindow{}}
21962187 nilWindow := &browserWindow{id: "window-a"}
21972188 firstValid := &browserWindow{id: "window-b", mainWindow: &window.MainWindow{}}
@@ -11,6 +11,7 @@ import (
11 "os"11 "os"
12 "path/filepath"12 "path/filepath"
13 "reflect"13 "reflect"
14+ "slices"
14 "strconv"15 "strconv"
15 "strings"16 "strings"
16 "testing"17 "testing"
@@ -97,7 +98,7 @@ func testGDKBackendAllows(backend string) bool {
97 if configured == "" {98 if configured == "" {
98 return true99 return true
99 }100 }
100- for _, candidate := range strings.Split(configured, ",") {101+ for candidate := range strings.SplitSeq(configured, ",") {
101 if strings.TrimSpace(candidate) == backend {102 if strings.TrimSpace(candidate) == backend {
102 return true103 return true
103 }104 }
@@ -117,12 +118,7 @@ func testHasUsableWaylandDisplay() bool {
117 if runtimeDir := os.Getenv("XDG_RUNTIME_DIR"); runtimeDir != "" && !filepath.IsAbs(waylandDisplay) {118 if runtimeDir := os.Getenv("XDG_RUNTIME_DIR"); runtimeDir != "" && !filepath.IsAbs(waylandDisplay) {
118 candidates = append([]string{filepath.Join(runtimeDir, waylandDisplay)}, candidates...)119 candidates = append([]string{filepath.Join(runtimeDir, waylandDisplay)}, candidates...)
119 }120 }
120- for _, candidate := range candidates {121+ return slices.ContainsFunc(candidates, testPathIsSocket)
121- if testPathIsSocket(candidate) {
122- return true
123- }
124- }
125- return false
126 }122 }
127 123
128 func testHasUsableX11Display() bool {124 func testHasUsableX11Display() bool {
@@ -133,8 +129,8 @@ func testHasUsableX11Display() bool {
133 if display == "" {129 if display == "" {
134 return false130 return false
135 }131 }
136- if strings.HasPrefix(display, ":") {132+ if after, ok := strings.CutPrefix(display, ":"); ok {
137- displayNum := strings.TrimPrefix(display, ":")133+ displayNum := after
138 displayNum = strings.SplitN(displayNum, ".", 2)[0]134 displayNum = strings.SplitN(displayNum, ".", 2)[0]
139 if displayNum == "" {135 if displayNum == "" {
140 return false136 return false
@@ -143,12 +139,7 @@ func testHasUsableX11Display() bool {
143 if fallback := "/tmp/.X11-unix/X" + displayNum; fallback != x11SocketCandidates[0] {139 if fallback := "/tmp/.X11-unix/X" + displayNum; fallback != x11SocketCandidates[0] {
144 x11SocketCandidates = append(x11SocketCandidates, fallback)140 x11SocketCandidates = append(x11SocketCandidates, fallback)
145 }141 }
146- for _, candidate := range x11SocketCandidates {142+ return slices.ContainsFunc(x11SocketCandidates, testPathIsSocket)
147- if testPathIsSocket(candidate) {
148- return true
149- }
150- }
151- return false
152 }143 }
153 return true // TCP display144 return true // TCP display
154 }145 }
@@ -2191,7 +2182,7 @@ func TestApp_HandlePaneFullscreenChanged_ClearsBottomInset(t *testing.T) {
2191 }2182 }
2192 2183
2193 func TestApp_RemoveBrowserWindowPromotesDeterministicFallbackWithMainWindow(t *testing.T) {2184 func TestApp_RemoveBrowserWindowPromotesDeterministicFallbackWithMainWindow(t *testing.T) {
2194- for i := 0; i < 20; i++ {2185+ for range 20 {
2195 removed := &browserWindow{id: "window-z", mainWindow: &window.MainWindow{}}2186 removed := &browserWindow{id: "window-z", mainWindow: &window.MainWindow{}}
2196 nilWindow := &browserWindow{id: "window-a"}2187 nilWindow := &browserWindow{id: "window-a"}
2197 firstValid := &browserWindow{id: "window-b", mainWindow: &window.MainWindow{}}2188 firstValid := &browserWindow{id: "window-b", mainWindow: &window.MainWindow{}}
M internal/ui/browser_window.go +4 -5
@@ -3,6 +3,7 @@ package ui
33 import (
44 "context"
55 "fmt"
6+ "slices"
67 "sort"
78
89 "github.com/bnema/dumber/internal/application/port"
@@ -327,10 +328,8 @@ func (a *App) registerBrowserWindow(bw *browserWindow) {
327328 a.browserWindows[bw.id] = bw
328329
329330 // Track registration order, guarding against duplicates.
330- for _, id := range a.browserWindowOrder {
331- if id == bw.id {
332- return
333- }
331+ if slices.Contains(a.browserWindowOrder, bw.id) {
332+ return
334333 }
335334 a.browserWindowOrder = append(a.browserWindowOrder, bw.id)
336335 }
@@ -436,7 +435,7 @@ func sortedTabIDs(tabs map[entity.TabID]*entity.Tab) []entity.TabID {
436435 for tabID := range tabs {
437436 ids = append(ids, tabID)
438437 }
439- sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
438+ slices.Sort(ids)
440439 return ids
441440 }
442441
@@ -3,6 +3,7 @@ package ui
3 import (3 import (
4 "context"4 "context"
5 "fmt"5 "fmt"
6+ "slices"
6 "sort"7 "sort"
7 8
8 "github.com/bnema/dumber/internal/application/port"9 "github.com/bnema/dumber/internal/application/port"
@@ -327,10 +328,8 @@ func (a *App) registerBrowserWindow(bw *browserWindow) {
327 a.browserWindows[bw.id] = bw328 a.browserWindows[bw.id] = bw
328 329
329 // Track registration order, guarding against duplicates.330 // Track registration order, guarding against duplicates.
330- for _, id := range a.browserWindowOrder {331+ if slices.Contains(a.browserWindowOrder, bw.id) {
331- if id == bw.id {332+ return
332- return
333- }
334 }333 }
335 a.browserWindowOrder = append(a.browserWindowOrder, bw.id)334 a.browserWindowOrder = append(a.browserWindowOrder, bw.id)
336 }335 }
@@ -436,7 +435,7 @@ func sortedTabIDs(tabs map[entity.TabID]*entity.Tab) []entity.TabID {
436 for tabID := range tabs {435 for tabID := range tabs {
437 ids = append(ids, tabID)436 ids = append(ids, tabID)
438 }437 }
439- sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })438+ slices.Sort(ids)
440 return ids439 return ids
441 }440 }
442 441
M internal/ui/component/accent_picker.go +2 -2
@@ -35,7 +35,7 @@ type AccentPicker struct {
3535 cancelCb func()
3636 visible bool
3737 keyController *gtk.EventControllerKey
38- retainedClicked []interface{} // Keep click callbacks alive
38+ retainedClicked []any // Keep click callbacks alive
3939
4040 mu sync.Mutex
4141 }
@@ -100,7 +100,7 @@ func (ap *AccentPicker) Show(accents []rune, selectedCb func(rune), cancelCb fun
100100
101101 // Create new labels for each accent
102102 ap.accentLabels = make([]layout.LabelWidget, len(accents))
103- ap.retainedClicked = make([]interface{}, len(accents))
103+ ap.retainedClicked = make([]any, len(accents))
104104
105105 for i, accent := range accents {
106106 label := ap.factory.NewLabel(string(accent))
@@ -35,7 +35,7 @@ type AccentPicker struct {
35 cancelCb func()35 cancelCb func()
36 visible bool36 visible bool
37 keyController *gtk.EventControllerKey37 keyController *gtk.EventControllerKey
38- retainedClicked []interface{} // Keep click callbacks alive38+ retainedClicked []any // Keep click callbacks alive
39 39
40 mu sync.Mutex40 mu sync.Mutex
41 }41 }
@@ -100,7 +100,7 @@ func (ap *AccentPicker) Show(accents []rune, selectedCb func(rune), cancelCb fun
100 100
101 // Create new labels for each accent101 // Create new labels for each accent
102 ap.accentLabels = make([]layout.LabelWidget, len(accents))102 ap.accentLabels = make([]layout.LabelWidget, len(accents))
103- ap.retainedClicked = make([]interface{}, len(accents))103+ ap.retainedClicked = make([]any, len(accents))
104 104
105 for i, accent := range accents {105 for i, accent := range accents {
106 label := ap.factory.NewLabel(string(accent))106 label := ap.factory.NewLabel(string(accent))
M internal/ui/component/favorites_sidebar.go +2 -2
@@ -64,8 +64,8 @@ type FavoritesSidebar struct {
6464 confirmDelete bool
6565 confirmDeleteID entity.FavoriteID
6666
67- retainedCallbacks []interface{}
68- tagCallbacks []interface{}
67+ retainedCallbacks []any
68+ tagCallbacks []any
6969 ctx context.Context
7070 cancel context.CancelFunc
7171 mu sync.RWMutex
@@ -64,8 +64,8 @@ type FavoritesSidebar struct {
64 confirmDelete bool64 confirmDelete bool
65 confirmDeleteID entity.FavoriteID65 confirmDeleteID entity.FavoriteID
66 66
67- retainedCallbacks []interface{}67+ retainedCallbacks []any
68- tagCallbacks []interface{}68+ tagCallbacks []any
69 ctx context.Context69 ctx context.Context
70 cancel context.CancelFunc70 cancel context.CancelFunc
71 mu sync.RWMutex71 mu sync.RWMutex
M internal/ui/component/favorites_sidebar_rows.go +1 -1
@@ -89,7 +89,7 @@ func (fs *FavoritesSidebar) renderTags() {
8989 fs.mu.RUnlock()
9090
9191 clearBoxChildren(tagBox)
92- callbacks := make([]interface{}, 0, len(tags)+1)
92+ callbacks := make([]any, 0, len(tags)+1)
9393
9494 allButton := gtk.NewButtonWithLabel("All")
9595 if allButton != nil {
@@ -89,7 +89,7 @@ func (fs *FavoritesSidebar) renderTags() {
89 fs.mu.RUnlock()89 fs.mu.RUnlock()
90 90
91 clearBoxChildren(tagBox)91 clearBoxChildren(tagBox)
92- callbacks := make([]interface{}, 0, len(tags)+1)92+ callbacks := make([]any, 0, len(tags)+1)
93 93
94 allButton := gtk.NewButtonWithLabel("All")94 allButton := gtk.NewButtonWithLabel("All")
95 if allButton != nil {95 if allButton != nil {
M internal/ui/component/find_bar.go +1 -1
@@ -45,7 +45,7 @@ type FindBar struct {
4545 onClose func()
4646 onAccentKeyPress func(keyval uint, state gdk.ModifierType) bool
4747 onAccentKeyRelease func(keyval uint)
48- retainedCallbacks []interface{} // Keep callbacks alive for GC
48+ retainedCallbacks []any // Keep callbacks alive for GC
4949 }
5050
5151 // FindBarConfig holds configuration for creating a FindBar.
@@ -45,7 +45,7 @@ type FindBar struct {
45 onClose func()45 onClose func()
46 onAccentKeyPress func(keyval uint, state gdk.ModifierType) bool46 onAccentKeyPress func(keyval uint, state gdk.ModifierType) bool
47 onAccentKeyRelease func(keyval uint)47 onAccentKeyRelease func(keyval uint)
48- retainedCallbacks []interface{} // Keep callbacks alive for GC48+ retainedCallbacks []any // Keep callbacks alive for GC
49 }49 }
50 50
51 // FindBarConfig holds configuration for creating a FindBar.51 // FindBarConfig holds configuration for creating a FindBar.
M internal/ui/component/floating_pane_test.go +1 -1
@@ -130,7 +130,7 @@ func TestFloatingPane_ConcurrentResizeAndParentSwitch(t *testing.T) {
130130 })
131131
132132 var wg sync.WaitGroup
133- for i := 0; i < 200; i++ {
133+ for i := range 200 {
134134 wg.Add(2)
135135 go func() {
136136 defer wg.Done()
@@ -130,7 +130,7 @@ func TestFloatingPane_ConcurrentResizeAndParentSwitch(t *testing.T) {
130 })130 })
131 131
132 var wg sync.WaitGroup132 var wg sync.WaitGroup
133- for i := 0; i < 200; i++ {133+ for i := range 200 {
134 wg.Add(2)134 wg.Add(2)
135 go func() {135 go func() {
136 defer wg.Done()136 defer wg.Done()
M internal/ui/component/history_model_test.go +1 -1
@@ -248,7 +248,7 @@ func makeGroups(dayCounts ...int) []historyGroup {
248248 groups := make([]historyGroup, len(dayCounts))
249249 for i, count := range dayCounts {
250250 entries := make([]*entity.HistoryEntry, count)
251- for j := 0; j < count; j++ {
251+ for j := range count {
252252 entries[j] = &entity.HistoryEntry{
253253 ID: int64(i*100 + j),
254254 URL: fmt.Sprintf("https://entry-%d-%d.com", i, j),
@@ -248,7 +248,7 @@ func makeGroups(dayCounts ...int) []historyGroup {
248 groups := make([]historyGroup, len(dayCounts))248 groups := make([]historyGroup, len(dayCounts))
249 for i, count := range dayCounts {249 for i, count := range dayCounts {
250 entries := make([]*entity.HistoryEntry, count)250 entries := make([]*entity.HistoryEntry, count)
251- for j := 0; j < count; j++ {251+ for j := range count {
252 entries[j] = &entity.HistoryEntry{252 entries[j] = &entity.HistoryEntry{
253 ID: int64(i*100 + j),253 ID: int64(i*100 + j),
254 URL: fmt.Sprintf("https://entry-%d-%d.com", i, j),254 URL: fmt.Sprintf("https://entry-%d-%d.com", i, j),
M internal/ui/component/history_sidebar.go +1 -1
@@ -99,7 +99,7 @@ type HistorySidebar struct {
9999 relativeTimeDayKeySet bool
100100
101101 // Retained callbacks
102- retainedCallbacks []interface{}
102+ retainedCallbacks []any
103103
104104 // idleScheduler dispatches work onto the GTK main thread.
105105 // Tests may override it to exercise scheduled callbacks deterministically.
@@ -99,7 +99,7 @@ type HistorySidebar struct {
99 relativeTimeDayKeySet bool99 relativeTimeDayKeySet bool
100 100
101 // Retained callbacks101 // Retained callbacks
102- retainedCallbacks []interface{}102+ retainedCallbacks []any
103 103
104 // idleScheduler dispatches work onto the GTK main thread.104 // idleScheduler dispatches work onto the GTK main thread.
105 // Tests may override it to exercise scheduled callbacks deterministically.105 // Tests may override it to exercise scheduled callbacks deterministically.
M internal/ui/component/history_sidebar_keyboard.go +1 -1
@@ -468,7 +468,7 @@ func (hs *HistorySidebar) ensureRowVisible(index int) {
468468
469469 // Sum allocated heights of all preceding rows to estimate Y position.
470470 var yPos int
471- for i := 0; i < index; i++ {
471+ for i := range index {
472472 r := hs.listBox.GetRowAtIndex(i)
473473 if r == nil {
474474 continue
@@ -468,7 +468,7 @@ func (hs *HistorySidebar) ensureRowVisible(index int) {
468 468
469 // Sum allocated heights of all preceding rows to estimate Y position.469 // Sum allocated heights of all preceding rows to estimate Y position.
470 var yPos int470 var yPos int
471- for i := 0; i < index; i++ {471+ for i := range index {
472 r := hs.listBox.GetRowAtIndex(i)472 r := hs.listBox.GetRowAtIndex(i)
473 if r == nil {473 if r == nil {
474 continue474 continue
M internal/ui/component/modal_sizing.go +1 -4
@@ -165,10 +165,7 @@ func CalculateModalDimensions(parent layout.OverlayWidget, cfg ModalSizeConfig)
165165 if cfg.FixedWidth > 0 {
166166 width = cfg.FixedWidth
167167 } else {
168- width = int(float64(parentWidth) * cfg.WidthPct)
169- if width > cfg.MaxWidth {
170- width = cfg.MaxWidth
171- }
168+ width = min(int(float64(parentWidth)*cfg.WidthPct), cfg.MaxWidth)
172169 }
173170
174171 if cfg.UseFixedTopMargin {
@@ -165,10 +165,7 @@ func CalculateModalDimensions(parent layout.OverlayWidget, cfg ModalSizeConfig)
165 if cfg.FixedWidth > 0 {165 if cfg.FixedWidth > 0 {
166 width = cfg.FixedWidth166 width = cfg.FixedWidth
167 } else {167 } else {
168- width = int(float64(parentWidth) * cfg.WidthPct)168+ width = min(int(float64(parentWidth)*cfg.WidthPct), cfg.MaxWidth)
169- if width > cfg.MaxWidth {
170- width = cfg.MaxWidth
171- }
172 }169 }
173 170
174 if cfg.UseFixedTopMargin {171 if cfg.UseFixedTopMargin {
M internal/ui/component/omnibox.go +1 -1
@@ -159,7 +159,7 @@ type Omnibox struct {
159159 }
160160
161161 // Callback retention: keep GTK signal callbacks reachable by Go GC.
162- retainedCallbacks []interface{}
162+ retainedCallbacks []any
163163
164164 // Click outside handler
165165 clickOutsideController *gtk.GestureClick
@@ -159,7 +159,7 @@ type Omnibox struct {
159 }159 }
160 160
161 // Callback retention: keep GTK signal callbacks reachable by Go GC.161 // Callback retention: keep GTK signal callbacks reachable by Go GC.
162- retainedCallbacks []interface{}162+ retainedCallbacks []any
163 163
164 // Click outside handler164 // Click outside handler
165 clickOutsideController *gtk.GestureClick165 clickOutsideController *gtk.GestureClick
M internal/ui/component/permission_popup.go +1 -1
@@ -35,7 +35,7 @@ type PermissionPopup struct {
3535 visible bool
3636 callback func(allowed, persistent bool)
3737
38- retainedCallbacks []interface{}
38+ retainedCallbacks []any
3939 }
4040
4141 // NewPermissionPopup creates a new permission popup component.
@@ -35,7 +35,7 @@ type PermissionPopup struct {
35 visible bool35 visible bool
36 callback func(allowed, persistent bool)36 callback func(allowed, persistent bool)
37 37
38- retainedCallbacks []interface{}38+ retainedCallbacks []any
39 }39 }
40 40
41 // NewPermissionPopup creates a new permission popup component.41 // NewPermissionPopup creates a new permission popup component.
M internal/ui/component/session_manager.go +6 -5
@@ -58,7 +58,7 @@ type SessionManager struct {
5858 onToast func(ctx context.Context, message string, level ToastLevel)
5959
6060 // GTK callback retention
61- retainedCallbacks []interface{}
61+ retainedCallbacks []any
6262
6363 // Scaling
6464 uiScale float64
@@ -712,9 +712,10 @@ func (sm *SessionManager) createTreeRow(text, nodeType string, hasSibling bool,
712712
713713 // Build indent and branch prefix
714714 var prefix string
715- indent := " " // Base indent (minimal, tree branches provide visual alignment)
716- for i := 0; i < depth; i++ {
717- indent += " " // 3 spaces per depth level
715+ var indent strings.Builder
716+ indent.WriteString(" ") // Base indent (minimal, tree branches provide visual alignment)
717+ for range depth {
718+ indent.WriteString(" ") // 3 spaces per depth level
718719 }
719720
720721 // Branch character
@@ -738,7 +739,7 @@ func (sm *SessionManager) createTreeRow(text, nodeType string, hasSibling bool,
738739 }
739740
740741 // Create the label with all parts
741- labelText := indent + prefix + icon + text
742+ labelText := indent.String() + prefix + icon + text
742743 label := gtk.NewLabel(&labelText)
743744 if label == nil {
744745 return nil
@@ -58,7 +58,7 @@ type SessionManager struct {
58 onToast func(ctx context.Context, message string, level ToastLevel)58 onToast func(ctx context.Context, message string, level ToastLevel)
59 59
60 // GTK callback retention60 // GTK callback retention
61- retainedCallbacks []interface{}61+ retainedCallbacks []any
62 62
63 // Scaling63 // Scaling
64 uiScale float6464 uiScale float64
@@ -712,9 +712,10 @@ func (sm *SessionManager) createTreeRow(text, nodeType string, hasSibling bool,
712 712
713 // Build indent and branch prefix713 // Build indent and branch prefix
714 var prefix string714 var prefix string
715- indent := " " // Base indent (minimal, tree branches provide visual alignment)715+ var indent strings.Builder
716- for i := 0; i < depth; i++ {716+ indent.WriteString(" ") // Base indent (minimal, tree branches provide visual alignment)
717- indent += " " // 3 spaces per depth level717+ for range depth {
718+ indent.WriteString(" ") // 3 spaces per depth level
718 }719 }
719 720
720 // Branch character721 // Branch character
@@ -738,7 +739,7 @@ func (sm *SessionManager) createTreeRow(text, nodeType string, hasSibling bool,
738 }739 }
739 740
740 // Create the label with all parts741 // Create the label with all parts
741- labelText := indent + prefix + icon + text742+ labelText := indent.String() + prefix + icon + text
742 label := gtk.NewLabel(&labelText)743 label := gtk.NewLabel(&labelText)
743 if label == nil {744 if label == nil {
744 return nil745 return nil
M internal/ui/component/tab_picker.go +2 -8
@@ -42,7 +42,7 @@ type TabPicker struct {
4242 onClose func()
4343 onSelect func(item TabPickerItem)
4444
45- retainedCallbacks []interface{}
45+ retainedCallbacks []any
4646 ctx context.Context
4747 }
4848
@@ -293,13 +293,7 @@ func (tp *TabPicker) resizeAndCenter() {
293293 if tp.scrolledWindow != nil {
294294 rowH := ScaleValue(DefaultRowHeights.Standard, tp.uiScale)
295295 maxH := TabPickerListDefaults.MaxVisibleRows * rowH
296- h := count * rowH
297- if h > maxH {
298- h = maxH
299- }
300- if h < rowH {
301- h = rowH
302- }
296+ h := max(min(count*rowH, maxH), rowH)
303297 cb := glib.SourceFunc(func(_ uintptr) bool {
304298 SetScrolledWindowHeight(tp.scrolledWindow, h)
305299 tp.outerBox.QueueResize()
@@ -42,7 +42,7 @@ type TabPicker struct {
42 onClose func()42 onClose func()
43 onSelect func(item TabPickerItem)43 onSelect func(item TabPickerItem)
44 44
45- retainedCallbacks []interface{}45+ retainedCallbacks []any
46 ctx context.Context46 ctx context.Context
47 }47 }
48 48
@@ -293,13 +293,7 @@ func (tp *TabPicker) resizeAndCenter() {
293 if tp.scrolledWindow != nil {293 if tp.scrolledWindow != nil {
294 rowH := ScaleValue(DefaultRowHeights.Standard, tp.uiScale)294 rowH := ScaleValue(DefaultRowHeights.Standard, tp.uiScale)
295 maxH := TabPickerListDefaults.MaxVisibleRows * rowH295 maxH := TabPickerListDefaults.MaxVisibleRows * rowH
296- h := count * rowH296+ h := max(min(count*rowH, maxH), rowH)
297- if h > maxH {
298- h = maxH
299- }
300- if h < rowH {
301- h = rowH
302- }
303 cb := glib.SourceFunc(func(_ uintptr) bool {297 cb := glib.SourceFunc(func(_ uintptr) bool {
304 SetScrolledWindowHeight(tp.scrolledWindow, h)298 SetScrolledWindowHeight(tp.scrolledWindow, h)
305 tp.outerBox.QueueResize()299 tp.outerBox.QueueResize()
M internal/ui/component/webrtc_permission_indicator.go +1 -1
@@ -41,7 +41,7 @@ type WebRTCPermissionIndicator struct {
4141
4242 mu sync.Mutex
4343 expanded bool
44- retainedCallbacks []interface{}
44+ retainedCallbacks []any
4545 }
4646
4747 // NewWebRTCPermissionIndicator creates a top-right WebRTC activity indicator.
@@ -41,7 +41,7 @@ type WebRTCPermissionIndicator struct {
41 41
42 mu sync.Mutex42 mu sync.Mutex
43 expanded bool43 expanded bool
44- retainedCallbacks []interface{}44+ retainedCallbacks []any
45 }45 }
46 46
47 // NewWebRTCPermissionIndicator creates a top-right WebRTC activity indicator.47 // NewWebRTCPermissionIndicator creates a top-right WebRTC activity indicator.
M internal/ui/coordinator/content/appearance.go +2 -3
@@ -4,6 +4,7 @@ import (
44 "context"
55 "encoding/json"
66 "fmt"
7+ "maps"
78 "strings"
89 "time"
910
@@ -44,9 +45,7 @@ func (c *Coordinator) ApplyWebUIThemeToAll(ctx context.Context, prefersDark bool
4445 // Snapshot webViews under lock to avoid data race with concurrent popup create/close.
4546 c.webViewsMu.RLock()
4647 snapshot := make(map[entity.PaneID]port.WebView, len(c.webViews))
47- for k, v := range c.webViews {
48- snapshot[k] = v
49- }
48+ maps.Copy(snapshot, c.webViews)
5049 c.webViewsMu.RUnlock()
5150
5251 for paneID, wv := range snapshot {
@@ -4,6 +4,7 @@ import (
4 "context"4 "context"
5 "encoding/json"5 "encoding/json"
6 "fmt"6 "fmt"
7+ "maps"
7 "strings"8 "strings"
8 "time"9 "time"
9 10
@@ -44,9 +45,7 @@ func (c *Coordinator) ApplyWebUIThemeToAll(ctx context.Context, prefersDark bool
44 // Snapshot webViews under lock to avoid data race with concurrent popup create/close.45 // Snapshot webViews under lock to avoid data race with concurrent popup create/close.
45 c.webViewsMu.RLock()46 c.webViewsMu.RLock()
46 snapshot := make(map[entity.PaneID]port.WebView, len(c.webViews))47 snapshot := make(map[entity.PaneID]port.WebView, len(c.webViews))
47- for k, v := range c.webViews {48+ maps.Copy(snapshot, c.webViews)
48- snapshot[k] = v
49- }
50 c.webViewsMu.RUnlock()49 c.webViewsMu.RUnlock()
51 50
52 for paneID, wv := range snapshot {51 for paneID, wv := range snapshot {
M internal/ui/coordinator/content/bulk.go +2 -3
@@ -2,6 +2,7 @@ package content
22
33 import (
44 "context"
5+ "maps"
56
67 "github.com/bnema/dumber/internal/application/port"
78 "github.com/bnema/dumber/internal/domain/entity"
@@ -63,9 +64,7 @@ func (c *Coordinator) RefreshInjectedScriptsToAll(ctx context.Context) {
6364 // Snapshot webViews under lock to avoid data race with concurrent popup create/close.
6465 c.webViewsMu.RLock()
6566 snapshot := make(map[entity.PaneID]port.WebView, len(c.webViews))
66- for k, v := range c.webViews {
67- snapshot[k] = v
68- }
67+ maps.Copy(snapshot, c.webViews)
6968 c.webViewsMu.RUnlock()
7069
7170 for paneID, wv := range snapshot {
@@ -2,6 +2,7 @@ package content
2 2
3 import (3 import (
4 "context"4 "context"
5+ "maps"
5 6
6 "github.com/bnema/dumber/internal/application/port"7 "github.com/bnema/dumber/internal/application/port"
7 "github.com/bnema/dumber/internal/domain/entity"8 "github.com/bnema/dumber/internal/domain/entity"
@@ -63,9 +64,7 @@ func (c *Coordinator) RefreshInjectedScriptsToAll(ctx context.Context) {
63 // Snapshot webViews under lock to avoid data race with concurrent popup create/close.64 // Snapshot webViews under lock to avoid data race with concurrent popup create/close.
64 c.webViewsMu.RLock()65 c.webViewsMu.RLock()
65 snapshot := make(map[entity.PaneID]port.WebView, len(c.webViews))66 snapshot := make(map[entity.PaneID]port.WebView, len(c.webViews))
66- for k, v := range c.webViews {67+ maps.Copy(snapshot, c.webViews)
67- snapshot[k] = v
68- }
69 c.webViewsMu.RUnlock()68 c.webViewsMu.RUnlock()
70 69
71 for paneID, wv := range snapshot {70 for paneID, wv := range snapshot {
M internal/ui/coordinator/content/content_reveal_test.go +1 -1
@@ -152,7 +152,7 @@ func TestPendingReveal_ConcurrentAccess(t *testing.T) {
152152 paneIDs := []entity.PaneID{"pane-1", "pane-2", "pane-3", "pane-4", "pane-5"}
153153
154154 // Concurrently mark and clear panes
155- for i := 0; i < 100; i++ {
155+ for range 100 {
156156 for _, paneID := range paneIDs {
157157 wg.Add(3)
158158
@@ -152,7 +152,7 @@ func TestPendingReveal_ConcurrentAccess(t *testing.T) {
152 paneIDs := []entity.PaneID{"pane-1", "pane-2", "pane-3", "pane-4", "pane-5"}152 paneIDs := []entity.PaneID{"pane-1", "pane-2", "pane-3", "pane-4", "pane-5"}
153 153
154 // Concurrently mark and clear panes154 // Concurrently mark and clear panes
155- for i := 0; i < 100; i++ {155+ for range 100 {
156 for _, paneID := range paneIDs {156 for _, paneID := range paneIDs {
157 wg.Add(3)157 wg.Add(3)
158 158
M internal/ui/coordinator/content/popup_test.go +5 -5
@@ -806,7 +806,7 @@ func TestPendingPopups_ConcurrentAccess(t *testing.T) {
806806 wg.Add(workers * 2)
807807
808808 // Writers
809- for i := 0; i < workers; i++ {
809+ for i := range workers {
810810 go func(id int) {
811811 defer wg.Done()
812812 popupID := port.WebViewID(id)
@@ -820,7 +820,7 @@ func TestPendingPopups_ConcurrentAccess(t *testing.T) {
820820 }
821821
822822 // Readers
823- for i := 0; i < workers; i++ {
823+ for i := range workers {
824824 go func(id int) {
825825 defer wg.Done()
826826 popupID := port.WebViewID(id)
@@ -847,7 +847,7 @@ func TestPendingPopups_ConcurrentDeleteAndRead(t *testing.T) {
847847 }
848848
849849 // Pre-populate
850- for i := 0; i < 10; i++ {
850+ for i := range 10 {
851851 c.popups.pendingPopups[port.WebViewID(i)] = &PendingPopup{PopupType: PopupTypePopup}
852852 }
853853
@@ -855,7 +855,7 @@ func TestPendingPopups_ConcurrentDeleteAndRead(t *testing.T) {
855855 wg.Add(20)
856856
857857 // Deleters
858- for i := 0; i < 10; i++ {
858+ for i := range 10 {
859859 go func(id int) {
860860 defer wg.Done()
861861 c.popups.mu.Lock()
@@ -865,7 +865,7 @@ func TestPendingPopups_ConcurrentDeleteAndRead(t *testing.T) {
865865 }
866866
867867 // Readers
868- for i := 0; i < 10; i++ {
868+ for i := range 10 {
869869 go func(id int) {
870870 defer wg.Done()
871871 c.popups.mu.RLock()
@@ -806,7 +806,7 @@ func TestPendingPopups_ConcurrentAccess(t *testing.T) {
806 wg.Add(workers * 2)806 wg.Add(workers * 2)
807 807
808 // Writers808 // Writers
809- for i := 0; i < workers; i++ {809+ for i := range workers {
810 go func(id int) {810 go func(id int) {
811 defer wg.Done()811 defer wg.Done()
812 popupID := port.WebViewID(id)812 popupID := port.WebViewID(id)
@@ -820,7 +820,7 @@ func TestPendingPopups_ConcurrentAccess(t *testing.T) {
820 }820 }
821 821
822 // Readers822 // Readers
823- for i := 0; i < workers; i++ {823+ for i := range workers {
824 go func(id int) {824 go func(id int) {
825 defer wg.Done()825 defer wg.Done()
826 popupID := port.WebViewID(id)826 popupID := port.WebViewID(id)
@@ -847,7 +847,7 @@ func TestPendingPopups_ConcurrentDeleteAndRead(t *testing.T) {
847 }847 }
848 848
849 // Pre-populate849 // Pre-populate
850- for i := 0; i < 10; i++ {850+ for i := range 10 {
851 c.popups.pendingPopups[port.WebViewID(i)] = &PendingPopup{PopupType: PopupTypePopup}851 c.popups.pendingPopups[port.WebViewID(i)] = &PendingPopup{PopupType: PopupTypePopup}
852 }852 }
853 853
@@ -855,7 +855,7 @@ func TestPendingPopups_ConcurrentDeleteAndRead(t *testing.T) {
855 wg.Add(20)855 wg.Add(20)
856 856
857 // Deleters857 // Deleters
858- for i := 0; i < 10; i++ {858+ for i := range 10 {
859 go func(id int) {859 go func(id int) {
860 defer wg.Done()860 defer wg.Done()
861 c.popups.mu.Lock()861 c.popups.mu.Lock()
@@ -865,7 +865,7 @@ func TestPendingPopups_ConcurrentDeleteAndRead(t *testing.T) {
865 }865 }
866 866
867 // Readers867 // Readers
868- for i := 0; i < 10; i++ {868+ for i := range 10 {
869 go func(id int) {869 go func(id int) {
870 defer wg.Done()870 defer wg.Done()
871 c.popups.mu.RLock()871 c.popups.mu.RLock()
M internal/ui/coordinator/content/title_test.go +2 -2
@@ -137,7 +137,7 @@ func TestPaneTitles_ConcurrentAccess(t *testing.T) {
137137 paneIDs := []entity.PaneID{"pane-1", "pane-2", "pane-3"}
138138
139139 var wg sync.WaitGroup
140- for i := 0; i < 50; i++ {
140+ for range 50 {
141141 for _, paneID := range paneIDs {
142142 wg.Add(2)
143143
@@ -265,7 +265,7 @@ func TestNavOrigins_ConcurrentAccess(t *testing.T) {
265265 paneIDs := []entity.PaneID{"pane-1", "pane-2", "pane-3"}
266266
267267 var wg sync.WaitGroup
268- for i := 0; i < 50; i++ {
268+ for range 50 {
269269 for _, paneID := range paneIDs {
270270 wg.Add(2)
271271
@@ -137,7 +137,7 @@ func TestPaneTitles_ConcurrentAccess(t *testing.T) {
137 paneIDs := []entity.PaneID{"pane-1", "pane-2", "pane-3"}137 paneIDs := []entity.PaneID{"pane-1", "pane-2", "pane-3"}
138 138
139 var wg sync.WaitGroup139 var wg sync.WaitGroup
140- for i := 0; i < 50; i++ {140+ for range 50 {
141 for _, paneID := range paneIDs {141 for _, paneID := range paneIDs {
142 wg.Add(2)142 wg.Add(2)
143 143
@@ -265,7 +265,7 @@ func TestNavOrigins_ConcurrentAccess(t *testing.T) {
265 paneIDs := []entity.PaneID{"pane-1", "pane-2", "pane-3"}265 paneIDs := []entity.PaneID{"pane-1", "pane-2", "pane-3"}
266 266
267 var wg sync.WaitGroup267 var wg sync.WaitGroup
268- for i := 0; i < 50; i++ {268+ for range 50 {
269 for _, paneID := range paneIDs {269 for _, paneID := range paneIDs {
270 wg.Add(2)270 wg.Add(2)
271 271
M internal/ui/coordinator/workspace_close_test.go +1 -1
@@ -171,7 +171,7 @@ func TestDeriveIncrementalCloseTreeContext_ConcurrentPaneAndTabCloseSnapshots(t
171171
172172 run := func(node *entity.PaneNode, wg *sync.WaitGroup, errCh chan<- error) {
173173 defer wg.Done()
174- for i := 0; i < 200; i++ {
174+ for range 200 {
175175 if _, err := deriveIncrementalCloseTreeContext(node); err != nil {
176176 errCh <- err
177177 return
@@ -171,7 +171,7 @@ func TestDeriveIncrementalCloseTreeContext_ConcurrentPaneAndTabCloseSnapshots(t
171 171
172 run := func(node *entity.PaneNode, wg *sync.WaitGroup, errCh chan<- error) {172 run := func(node *entity.PaneNode, wg *sync.WaitGroup, errCh chan<- error) {
173 defer wg.Done()173 defer wg.Done()
174- for i := 0; i < 200; i++ {174+ for range 200 {
175 if _, err := deriveIncrementalCloseTreeContext(node); err != nil {175 if _, err := deriveIncrementalCloseTreeContext(node); err != nil {
176 errCh <- err176 errCh <- err
177 return177 return
M internal/ui/floating_pane_test.go +1 -1
@@ -277,7 +277,7 @@ func TestFloatingPane_ProfileRapidToggleNeverShowsAboutBlank(t *testing.T) {
277277 baseURL := "https://mail.google.com"
278278
279279 // Rapid toggle 10 times: should never end up on about:blank
280- for i := 0; i < 10; i++ {
280+ for i := range 10 {
281281 require.NoError(t, app.OpenFloatingPaneProfileURL(context.Background(), "profile:gmail", baseURL))
282282 assert.NotEqual(t, "about:blank", session.pane.CurrentURL(),
283283 "iteration %d: profile session should never show about:blank", i)
@@ -277,7 +277,7 @@ func TestFloatingPane_ProfileRapidToggleNeverShowsAboutBlank(t *testing.T) {
277 baseURL := "https://mail.google.com"277 baseURL := "https://mail.google.com"
278 278
279 // Rapid toggle 10 times: should never end up on about:blank279 // Rapid toggle 10 times: should never end up on about:blank
280- for i := 0; i < 10; i++ {280+ for i := range 10 {
281 require.NoError(t, app.OpenFloatingPaneProfileURL(context.Background(), "profile:gmail", baseURL))281 require.NoError(t, app.OpenFloatingPaneProfileURL(context.Background(), "profile:gmail", baseURL))
282 assert.NotEqual(t, "about:blank", session.pane.CurrentURL(),282 assert.NotEqual(t, "about:blank", session.pane.CurrentURL(),
283 "iteration %d: profile session should never show about:blank", i)283 "iteration %d: profile session should never show about:blank", i)
M internal/ui/input/global_shortcuts.go +3 -6
@@ -4,6 +4,7 @@ package input
44 import (
55 "context"
66 "fmt"
7+ "maps"
78 "strings"
89 "time"
910
@@ -99,9 +100,7 @@ func NewGlobalShortcutHandler(
99100 }
100101
101102 occupied := make(map[KeyBinding]Action, len(h.registered))
102- for binding, action := range h.registered {
103- occupied[binding] = action
104- }
103+ maps.Copy(occupied, h.registered)
105104 for _, shortcut := range collectFloatingProfileShortcutsFromWorkspace(ctx, workspace, occupied) {
106105 if !h.registerShortcut(shortcut.Binding.Keyval, gdk.ModifierType(shortcut.Binding.Modifiers), shortcut.Action) {
107106 continue
@@ -553,9 +552,7 @@ func (h *GlobalShortcutHandler) ReloadShortcuts(ctx context.Context, workspace *
553552 }
554553
555554 occupied := make(map[KeyBinding]Action, len(h.registered))
556- for binding, action := range h.registered {
557- occupied[binding] = action
558- }
555+ maps.Copy(occupied, h.registered)
559556 for _, shortcut := range collectFloatingProfileShortcutsFromWorkspace(ctx, workspace, occupied) {
560557 if h.registerShortcut(shortcut.Binding.Keyval, gdk.ModifierType(shortcut.Binding.Modifiers), shortcut.Action) {
561558 if url, ok := ParseFloatingProfileAction(shortcut.Action); ok {
@@ -4,6 +4,7 @@ package input
4 import (4 import (
5 "context"5 "context"
6 "fmt"6 "fmt"
7+ "maps"
7 "strings"8 "strings"
8 "time"9 "time"
9 10
@@ -99,9 +100,7 @@ func NewGlobalShortcutHandler(
99 }100 }
100 101
101 occupied := make(map[KeyBinding]Action, len(h.registered))102 occupied := make(map[KeyBinding]Action, len(h.registered))
102- for binding, action := range h.registered {103+ maps.Copy(occupied, h.registered)
103- occupied[binding] = action
104- }
105 for _, shortcut := range collectFloatingProfileShortcutsFromWorkspace(ctx, workspace, occupied) {104 for _, shortcut := range collectFloatingProfileShortcutsFromWorkspace(ctx, workspace, occupied) {
106 if !h.registerShortcut(shortcut.Binding.Keyval, gdk.ModifierType(shortcut.Binding.Modifiers), shortcut.Action) {105 if !h.registerShortcut(shortcut.Binding.Keyval, gdk.ModifierType(shortcut.Binding.Modifiers), shortcut.Action) {
107 continue106 continue
@@ -553,9 +552,7 @@ func (h *GlobalShortcutHandler) ReloadShortcuts(ctx context.Context, workspace *
553 }552 }
554 553
555 occupied := make(map[KeyBinding]Action, len(h.registered))554 occupied := make(map[KeyBinding]Action, len(h.registered))
556- for binding, action := range h.registered {555+ maps.Copy(occupied, h.registered)
557- occupied[binding] = action
558- }
559 for _, shortcut := range collectFloatingProfileShortcutsFromWorkspace(ctx, workspace, occupied) {556 for _, shortcut := range collectFloatingProfileShortcutsFromWorkspace(ctx, workspace, occupied) {
560 if h.registerShortcut(shortcut.Binding.Keyval, gdk.ModifierType(shortcut.Binding.Modifiers), shortcut.Action) {557 if h.registerShortcut(shortcut.Binding.Keyval, gdk.ModifierType(shortcut.Binding.Modifiers), shortcut.Action) {
561 if url, ok := ParseFloatingProfileAction(shortcut.Action); ok {558 if url, ok := ParseFloatingProfileAction(shortcut.Action); ok {
M internal/ui/input/global_shortcuts_test.go +1 -1
@@ -15,7 +15,7 @@ func TestGlobalShortcutHandlerCallbackBudgetIsIndependentOfShortcutCount(t *test
1515 shortcutRefs: make(map[string]globalShortcutRegistration),
1616 }
1717
18- for i := 0; i < 57; i++ {
18+ for i := range 57 {
1919 binding := KeyBinding{Keyval: uint(gdk.KEY_1) + uint(i), Modifiers: Modifier(gdk.AltMaskValue)}
2020 action := ActionSwitchTabIndex1
2121 shortcutID := encodeGlobalShortcutID(action, binding, h.generation)
@@ -15,7 +15,7 @@ func TestGlobalShortcutHandlerCallbackBudgetIsIndependentOfShortcutCount(t *test
15 shortcutRefs: make(map[string]globalShortcutRegistration),15 shortcutRefs: make(map[string]globalShortcutRegistration),
16 }16 }
17 17
18- for i := 0; i < 57; i++ {18+ for i := range 57 {
19 binding := KeyBinding{Keyval: uint(gdk.KEY_1) + uint(i), Modifiers: Modifier(gdk.AltMaskValue)}19 binding := KeyBinding{Keyval: uint(gdk.KEY_1) + uint(i), Modifiers: Modifier(gdk.AltMaskValue)}
20 action := ActionSwitchTabIndex120 action := ActionSwitchTabIndex1
21 shortcutID := encodeGlobalShortcutID(action, binding, h.generation)21 shortcutID := encodeGlobalShortcutID(action, binding, h.generation)
M internal/ui/input/modal_test.go +4 -6
@@ -237,11 +237,9 @@ func TestModalState_ConcurrentAccess(t *testing.T) {
237237 iterations := 100
238238
239239 // Multiple goroutines entering/exiting modes
240- for i := 0; i < 10; i++ {
241- wg.Add(1)
242- go func() {
243- defer wg.Done()
244- for j := 0; j < iterations; j++ {
240+ for range 10 {
241+ wg.Go(func() {
242+ for range iterations {
245243 ms.EnterTabMode(ctx, 0)
246244 _ = ms.Mode()
247245 ms.ExitMode(ctx)
@@ -249,7 +247,7 @@ func TestModalState_ConcurrentAccess(t *testing.T) {
249247 _ = ms.Mode()
250248 ms.ExitMode(ctx)
251249 }
252- }()
250+ })
253251 }
254252
255253 wg.Wait()
@@ -237,11 +237,9 @@ func TestModalState_ConcurrentAccess(t *testing.T) {
237 iterations := 100237 iterations := 100
238 238
239 // Multiple goroutines entering/exiting modes239 // Multiple goroutines entering/exiting modes
240- for i := 0; i < 10; i++ {240+ for range 10 {
241- wg.Add(1)241+ wg.Go(func() {
242- go func() {242+ for range iterations {
243- defer wg.Done()
244- for j := 0; j < iterations; j++ {
245 ms.EnterTabMode(ctx, 0)243 ms.EnterTabMode(ctx, 0)
246 _ = ms.Mode()244 _ = ms.Mode()
247 ms.ExitMode(ctx)245 ms.ExitMode(ctx)
@@ -249,7 +247,7 @@ func TestModalState_ConcurrentAccess(t *testing.T) {
249 _ = ms.Mode()247 _ = ms.Mode()
250 ms.ExitMode(ctx)248 ms.ExitMode(ctx)
251 }249 }
252- }()250+ })
253 }251 }
254 252
255 wg.Wait()253 wg.Wait()
M internal/ui/input/shortcuts.go +2 -3
@@ -3,6 +3,7 @@ package input
33
44 import (
55 "context"
6+ "maps"
67 "sort"
78 "strings"
89
@@ -380,9 +381,7 @@ func (s *ShortcutSet) registerConfiguredShortcuts(cfg *entity.WorkspaceConfig) {
380381
381382 func (s *ShortcutSet) registerFloatingProfileShortcutsFromWorkspace(ctx context.Context, workspace *entity.WorkspaceConfig) {
382383 occupied := make(map[KeyBinding]Action, len(s.Global))
383- for binding, action := range s.Global {
384- occupied[binding] = action
385- }
384+ maps.Copy(occupied, s.Global)
386385
387386 for _, shortcut := range collectFloatingProfileShortcutsFromWorkspace(ctx, workspace, occupied) {
388387 s.Global[shortcut.Binding] = shortcut.Action
@@ -3,6 +3,7 @@ package input
3 3
4 import (4 import (
5 "context"5 "context"
6+ "maps"
6 "sort"7 "sort"
7 "strings"8 "strings"
8 9
@@ -380,9 +381,7 @@ func (s *ShortcutSet) registerConfiguredShortcuts(cfg *entity.WorkspaceConfig) {
380 381
381 func (s *ShortcutSet) registerFloatingProfileShortcutsFromWorkspace(ctx context.Context, workspace *entity.WorkspaceConfig) {382 func (s *ShortcutSet) registerFloatingProfileShortcutsFromWorkspace(ctx context.Context, workspace *entity.WorkspaceConfig) {
382 occupied := make(map[KeyBinding]Action, len(s.Global))383 occupied := make(map[KeyBinding]Action, len(s.Global))
383- for binding, action := range s.Global {384+ maps.Copy(occupied, s.Global)
384- occupied[binding] = action
385- }
386 385
387 for _, shortcut := range collectFloatingProfileShortcutsFromWorkspace(ctx, workspace, occupied) {386 for _, shortcut := range collectFloatingProfileShortcutsFromWorkspace(ctx, workspace, occupied) {
388 s.Global[shortcut.Binding] = shortcut.Action387 s.Global[shortcut.Binding] = shortcut.Action
M internal/ui/layout/stacked.go +2 -2
@@ -39,7 +39,7 @@ type stackedPane struct {
3939 closeButton ButtonWidget // stored for signal disconnection
4040
4141 // Retained callback for GestureClick to prevent GC
42- titleClickCallback interface{}
42+ titleClickCallback any
4343 }
4444
4545 // StackedView manages a stack of panes where only one is visible at a time.
@@ -183,7 +183,7 @@ func (sv *StackedView) AddPane(ctx context.Context, paneID, title, faviconIconNa
183183 // Returns the retained callback (to prevent GC) and close signal ID for disconnection.
184184 func (sv *StackedView) connectTitleBarHandlers(
185185 tb titleBarComponents, paneID string,
186-) (titleClickCallback interface{}, closeSignalID uint) {
186+) (titleClickCallback any, closeSignalID uint) {
187187 // Connect title bar click handler using GestureClick
188188 // This prevents event propagation issues with nested buttons
189189 clickCtrl := gtk.NewGestureClick()
@@ -39,7 +39,7 @@ type stackedPane struct {
39 closeButton ButtonWidget // stored for signal disconnection39 closeButton ButtonWidget // stored for signal disconnection
40 40
41 // Retained callback for GestureClick to prevent GC41 // Retained callback for GestureClick to prevent GC
42- titleClickCallback interface{}42+ titleClickCallback any
43 }43 }
44 44
45 // StackedView manages a stack of panes where only one is visible at a time.45 // StackedView manages a stack of panes where only one is visible at a time.
@@ -183,7 +183,7 @@ func (sv *StackedView) AddPane(ctx context.Context, paneID, title, faviconIconNa
183 // Returns the retained callback (to prevent GC) and close signal ID for disconnection.183 // Returns the retained callback (to prevent GC) and close signal ID for disconnection.
184 func (sv *StackedView) connectTitleBarHandlers(184 func (sv *StackedView) connectTitleBarHandlers(
185 tb titleBarComponents, paneID string,185 tb titleBarComponents, paneID string,
186-) (titleClickCallback interface{}, closeSignalID uint) {186+) (titleClickCallback any, closeSignalID uint) {
187 // Connect title bar click handler using GestureClick187 // Connect title bar click handler using GestureClick
188 // This prevents event propagation issues with nested buttons188 // This prevents event propagation issues with nested buttons
189 clickCtrl := gtk.NewGestureClick()189 clickCtrl := gtk.NewGestureClick()
M internal/ui/layout/stacked_test.go +7 -7
@@ -167,12 +167,12 @@ func TestRemovePane_MiddlePane(t *testing.T) {
167167 containers := make([]*mocks.MockWidget, 3)
168168 titleBars := make([]*mocks.MockBoxWidget, 3)
169169
170- for i := 0; i < 3; i++ {
170+ for i := range 3 {
171171 titleBars[i], _, _, containers[i] = setupPaneMocks(t, mockFactory, mockBox)
172172 }
173173
174174 // Allow any visibility calls during setup and removal
175- for i := 0; i < 3; i++ {
175+ for i := range 3 {
176176 containers[i].EXPECT().SetVisible(mock.Anything).Maybe()
177177 titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe()
178178 titleBars[i].EXPECT().AddCssClass("active").Maybe()
@@ -263,7 +263,7 @@ func TestSetActive_ValidIndex(t *testing.T) {
263263 containers := make([]*mocks.MockWidget, 2)
264264 titleBars := make([]*mocks.MockBoxWidget, 2)
265265
266- for i := 0; i < 2; i++ {
266+ for i := range 2 {
267267 titleBar, mockFavicon, mockLabel, container := setupPaneMocks(t, mockFactory, mockBox)
268268 _ = mockFavicon
269269 _ = mockLabel
@@ -436,7 +436,7 @@ func TestNavigateNext_WrapsAround(t *testing.T) {
436436 containers := make([]*mocks.MockWidget, 2)
437437 titleBars := make([]*mocks.MockBoxWidget, 2)
438438
439- for i := 0; i < 2; i++ {
439+ for i := range 2 {
440440 titleBar, mockFavicon, mockLabel, container := setupPaneMocks(t, mockFactory, mockBox)
441441 _ = mockFavicon
442442 _ = mockLabel
@@ -473,7 +473,7 @@ func TestNavigatePrevious_WrapsAround(t *testing.T) {
473473 containers := make([]*mocks.MockWidget, 2)
474474 titleBars := make([]*mocks.MockBoxWidget, 2)
475475
476- for i := 0; i < 2; i++ {
476+ for i := range 2 {
477477 titleBar, mockFavicon, mockLabel, container := setupPaneMocks(t, mockFactory, mockBox)
478478 _ = mockFavicon
479479 _ = mockLabel
@@ -657,7 +657,7 @@ func TestInsertPaneAfter_InMiddle(t *testing.T) {
657657 containers := make([]*mocks.MockWidget, 2)
658658 titleBars := make([]*mocks.MockBoxWidget, 2)
659659
660- for i := 0; i < 2; i++ {
660+ for i := range 2 {
661661 titleBars[i], _, _, containers[i] = setupPaneMocks(t, mockFactory, mockBox)
662662 titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe()
663663 containers[i].EXPECT().SetVisible(mock.Anything).Maybe()
@@ -733,7 +733,7 @@ func TestInsertPaneAfter_MaintainsOrder(t *testing.T) {
733733 containers := make([]*mocks.MockWidget, 3)
734734 titleBars := make([]*mocks.MockBoxWidget, 3)
735735
736- for i := 0; i < 3; i++ {
736+ for i := range 3 {
737737 titleBar, mockFavicon, mockLabel, container := setupPaneMocks(t, mockFactory, mockBox)
738738 _ = mockFavicon
739739 _ = mockLabel
@@ -167,12 +167,12 @@ func TestRemovePane_MiddlePane(t *testing.T) {
167 containers := make([]*mocks.MockWidget, 3)167 containers := make([]*mocks.MockWidget, 3)
168 titleBars := make([]*mocks.MockBoxWidget, 3)168 titleBars := make([]*mocks.MockBoxWidget, 3)
169 169
170- for i := 0; i < 3; i++ {170+ for i := range 3 {
171 titleBars[i], _, _, containers[i] = setupPaneMocks(t, mockFactory, mockBox)171 titleBars[i], _, _, containers[i] = setupPaneMocks(t, mockFactory, mockBox)
172 }172 }
173 173
174 // Allow any visibility calls during setup and removal174 // Allow any visibility calls during setup and removal
175- for i := 0; i < 3; i++ {175+ for i := range 3 {
176 containers[i].EXPECT().SetVisible(mock.Anything).Maybe()176 containers[i].EXPECT().SetVisible(mock.Anything).Maybe()
177 titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe()177 titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe()
178 titleBars[i].EXPECT().AddCssClass("active").Maybe()178 titleBars[i].EXPECT().AddCssClass("active").Maybe()
@@ -263,7 +263,7 @@ func TestSetActive_ValidIndex(t *testing.T) {
263 containers := make([]*mocks.MockWidget, 2)263 containers := make([]*mocks.MockWidget, 2)
264 titleBars := make([]*mocks.MockBoxWidget, 2)264 titleBars := make([]*mocks.MockBoxWidget, 2)
265 265
266- for i := 0; i < 2; i++ {266+ for i := range 2 {
267 titleBar, mockFavicon, mockLabel, container := setupPaneMocks(t, mockFactory, mockBox)267 titleBar, mockFavicon, mockLabel, container := setupPaneMocks(t, mockFactory, mockBox)
268 _ = mockFavicon268 _ = mockFavicon
269 _ = mockLabel269 _ = mockLabel
@@ -436,7 +436,7 @@ func TestNavigateNext_WrapsAround(t *testing.T) {
436 containers := make([]*mocks.MockWidget, 2)436 containers := make([]*mocks.MockWidget, 2)
437 titleBars := make([]*mocks.MockBoxWidget, 2)437 titleBars := make([]*mocks.MockBoxWidget, 2)
438 438
439- for i := 0; i < 2; i++ {439+ for i := range 2 {
440 titleBar, mockFavicon, mockLabel, container := setupPaneMocks(t, mockFactory, mockBox)440 titleBar, mockFavicon, mockLabel, container := setupPaneMocks(t, mockFactory, mockBox)
441 _ = mockFavicon441 _ = mockFavicon
442 _ = mockLabel442 _ = mockLabel
@@ -473,7 +473,7 @@ func TestNavigatePrevious_WrapsAround(t *testing.T) {
473 containers := make([]*mocks.MockWidget, 2)473 containers := make([]*mocks.MockWidget, 2)
474 titleBars := make([]*mocks.MockBoxWidget, 2)474 titleBars := make([]*mocks.MockBoxWidget, 2)
475 475
476- for i := 0; i < 2; i++ {476+ for i := range 2 {
477 titleBar, mockFavicon, mockLabel, container := setupPaneMocks(t, mockFactory, mockBox)477 titleBar, mockFavicon, mockLabel, container := setupPaneMocks(t, mockFactory, mockBox)
478 _ = mockFavicon478 _ = mockFavicon
479 _ = mockLabel479 _ = mockLabel
@@ -657,7 +657,7 @@ func TestInsertPaneAfter_InMiddle(t *testing.T) {
657 containers := make([]*mocks.MockWidget, 2)657 containers := make([]*mocks.MockWidget, 2)
658 titleBars := make([]*mocks.MockBoxWidget, 2)658 titleBars := make([]*mocks.MockBoxWidget, 2)
659 659
660- for i := 0; i < 2; i++ {660+ for i := range 2 {
661 titleBars[i], _, _, containers[i] = setupPaneMocks(t, mockFactory, mockBox)661 titleBars[i], _, _, containers[i] = setupPaneMocks(t, mockFactory, mockBox)
662 titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe()662 titleBars[i].EXPECT().SetVisible(mock.Anything).Maybe()
663 containers[i].EXPECT().SetVisible(mock.Anything).Maybe()663 containers[i].EXPECT().SetVisible(mock.Anything).Maybe()
@@ -733,7 +733,7 @@ func TestInsertPaneAfter_MaintainsOrder(t *testing.T) {
733 containers := make([]*mocks.MockWidget, 3)733 containers := make([]*mocks.MockWidget, 3)
734 titleBars := make([]*mocks.MockBoxWidget, 3)734 titleBars := make([]*mocks.MockBoxWidget, 3)
735 735
736- for i := 0; i < 3; i++ {736+ for i := range 3 {
737 titleBar, mockFavicon, mockLabel, container := setupPaneMocks(t, mockFactory, mockBox)737 titleBar, mockFavicon, mockLabel, container := setupPaneMocks(t, mockFactory, mockBox)
738 _ = mockFavicon738 _ = mockFavicon
739 _ = mockLabel739 _ = mockLabel
M internal/ui/pane_window_eject.go +3 -6
@@ -3,6 +3,7 @@ package ui
33 import (
44 "context"
55 "fmt"
6+ "maps"
67 "strings"
78
89 "github.com/bnema/dumber/internal/application/usecase"
@@ -85,17 +86,13 @@ func (a *App) rollbackEjectTargetUIFailure(targetWindow *browserWindow, snapshot
8586
8687 func cloneWindowForTabMap(source map[entity.TabID]*browserWindow) map[entity.TabID]*browserWindow {
8788 cloned := make(map[entity.TabID]*browserWindow, len(source))
88- for tabID, bw := range source {
89- cloned[tabID] = bw
90- }
89+ maps.Copy(cloned, source)
9190 return cloned
9291 }
9392
9493 func cloneWorkspaceViewsMap(source map[entity.TabID]*component.WorkspaceView) map[entity.TabID]*component.WorkspaceView {
9594 cloned := make(map[entity.TabID]*component.WorkspaceView, len(source))
96- for tabID, wsView := range source {
97- cloned[tabID] = wsView
98- }
95+ maps.Copy(cloned, source)
9996 return cloned
10097 }
10198
@@ -3,6 +3,7 @@ package ui
3 import (3 import (
4 "context"4 "context"
5 "fmt"5 "fmt"
6+ "maps"
6 "strings"7 "strings"
7 8
8 "github.com/bnema/dumber/internal/application/usecase"9 "github.com/bnema/dumber/internal/application/usecase"
@@ -85,17 +86,13 @@ func (a *App) rollbackEjectTargetUIFailure(targetWindow *browserWindow, snapshot
85 86
86 func cloneWindowForTabMap(source map[entity.TabID]*browserWindow) map[entity.TabID]*browserWindow {87 func cloneWindowForTabMap(source map[entity.TabID]*browserWindow) map[entity.TabID]*browserWindow {
87 cloned := make(map[entity.TabID]*browserWindow, len(source))88 cloned := make(map[entity.TabID]*browserWindow, len(source))
88- for tabID, bw := range source {89+ maps.Copy(cloned, source)
89- cloned[tabID] = bw
90- }
91 return cloned90 return cloned
92 }91 }
93 92
94 func cloneWorkspaceViewsMap(source map[entity.TabID]*component.WorkspaceView) map[entity.TabID]*component.WorkspaceView {93 func cloneWorkspaceViewsMap(source map[entity.TabID]*component.WorkspaceView) map[entity.TabID]*component.WorkspaceView {
95 cloned := make(map[entity.TabID]*component.WorkspaceView, len(source))94 cloned := make(map[entity.TabID]*component.WorkspaceView, len(source))
96- for tabID, wsView := range source {95+ maps.Copy(cloned, source)
97- cloned[tabID] = wsView
98- }
99 return cloned96 return cloned
100 }97 }
101 98
M internal/ui/systemviews/app_test.go +1 -2
@@ -404,8 +404,7 @@ func TestAppCloseStopsActionWorkerAndReleasesDOM(t *testing.T) {
404404 history := &recordingHistoryService{entries: []*entity.HistoryEntry{{ID: 1, URL: "https://example.com"}}}
405405 app := NewApp(Dependencies{DOM: dom, History: history, LocationURI: "dumb://history"})
406406
407- ctx, cancel := context.WithCancel(context.Background())
408- defer cancel()
407+ ctx := t.Context()
409408 require.NoError(t, app.RunWithContext(ctx))
410409 require.NotNil(t, dom.handler)
411410
@@ -404,8 +404,7 @@ func TestAppCloseStopsActionWorkerAndReleasesDOM(t *testing.T) {
404 history := &recordingHistoryService{entries: []*entity.HistoryEntry{{ID: 1, URL: "https://example.com"}}}404 history := &recordingHistoryService{entries: []*entity.HistoryEntry{{ID: 1, URL: "https://example.com"}}}
405 app := NewApp(Dependencies{DOM: dom, History: history, LocationURI: "dumb://history"})405 app := NewApp(Dependencies{DOM: dom, History: history, LocationURI: "dumb://history"})
406 406
407- ctx, cancel := context.WithCancel(context.Background())407+ ctx := t.Context()
408- defer cancel()
409 require.NoError(t, app.RunWithContext(ctx))408 require.NoError(t, app.RunWithContext(ctx))
410 require.NotNil(t, dom.handler)409 require.NotNil(t, dom.handler)
411 410
M internal/ui/systemviews/config_actions.go +2 -3
@@ -3,6 +3,7 @@ package systemviews
33 import (
44 "context"
55 "fmt"
6+ "maps"
67 "math"
78 "net/url"
89 "strconv"
@@ -413,9 +414,7 @@ func webUIConfigFromPayload(payload dto.SystemviewConfigPayload) dto.WebUIConfig
413414
414415 func cloneSearchShortcuts(shortcuts map[string]dto.SearchShortcut) map[string]dto.SearchShortcut {
415416 clone := make(map[string]dto.SearchShortcut, len(shortcuts))
416- for key, shortcut := range shortcuts {
417- clone[key] = shortcut
418- }
417+ maps.Copy(clone, shortcuts)
419418 return clone
420419 }
421420
@@ -3,6 +3,7 @@ package systemviews
3 import (3 import (
4 "context"4 "context"
5 "fmt"5 "fmt"
6+ "maps"
6 "math"7 "math"
7 "net/url"8 "net/url"
8 "strconv"9 "strconv"
@@ -413,9 +414,7 @@ func webUIConfigFromPayload(payload dto.SystemviewConfigPayload) dto.WebUIConfig
413 414
414 func cloneSearchShortcuts(shortcuts map[string]dto.SearchShortcut) map[string]dto.SearchShortcut {415 func cloneSearchShortcuts(shortcuts map[string]dto.SearchShortcut) map[string]dto.SearchShortcut {
415 clone := make(map[string]dto.SearchShortcut, len(shortcuts))416 clone := make(map[string]dto.SearchShortcut, len(shortcuts))
416- for key, shortcut := range shortcuts {417+ maps.Copy(clone, shortcuts)
417- clone[key] = shortcut
418- }
419 return clone418 return clone
420 }419 }
421 420
M internal/ui/theme/history_sidebar_css_test.go +3 -3
@@ -91,11 +91,11 @@ func TestGenerateHistorySidebarCSS_NoEmptyCSSBlocks(t *testing.T) {
9191 if i == 0 {
9292 continue
9393 }
94- closeIdx := strings.Index(section, "}")
95- if closeIdx < 0 {
94+ before, _, ok := strings.Cut(section, "}")
95+ if !ok {
9696 continue
9797 }
98- blockContent := strings.TrimSpace(section[:closeIdx])
98+ blockContent := strings.TrimSpace(before)
9999 assert.NotEmpty(t, blockContent, "CSS block must not be empty: section %d", i)
100100 }
101101 }
@@ -91,11 +91,11 @@ func TestGenerateHistorySidebarCSS_NoEmptyCSSBlocks(t *testing.T) {
91 if i == 0 {91 if i == 0 {
92 continue92 continue
93 }93 }
94- closeIdx := strings.Index(section, "}")94+ before, _, ok := strings.Cut(section, "}")
95- if closeIdx < 0 {95+ if !ok {
96 continue96 continue
97 }97 }
98- blockContent := strings.TrimSpace(section[:closeIdx])98+ blockContent := strings.TrimSpace(before)
99 assert.NotEmpty(t, blockContent, "CSS block must not be empty: section %d", i)99 assert.NotEmpty(t, blockContent, "CSS block must not be empty: section %d", i)
100 }100 }
101 }101 }
M internal/ui/window/main_window_sidebar.go +1 -7
@@ -45,13 +45,7 @@ func (mw *MainWindow) SetSidebarWidth(cfg SidebarWidthConfig) {
4545 mw.logger.Warn().Int("min_px", cfg.MinPx).Int("max_px", cfg.MaxPx).Msg("invalid sidebar width bounds; swapping")
4646 cfg.MinPx, cfg.MaxPx = cfg.MaxPx, cfg.MinPx
4747 }
48- clamped := cfg.WidthPx
49- if clamped < cfg.MinPx {
50- clamped = cfg.MinPx
51- }
52- if clamped > cfg.MaxPx {
53- clamped = cfg.MaxPx
54- }
48+ clamped := min(max(cfg.WidthPx, cfg.MinPx), cfg.MaxPx)
5549 mw.sidebarBox.SetSizeRequest(clamped, -1)
5650 mw.logger.Debug().Int("sidebar_width", clamped).Msg("sidebar width set")
5751 }
@@ -45,13 +45,7 @@ func (mw *MainWindow) SetSidebarWidth(cfg SidebarWidthConfig) {
45 mw.logger.Warn().Int("min_px", cfg.MinPx).Int("max_px", cfg.MaxPx).Msg("invalid sidebar width bounds; swapping")45 mw.logger.Warn().Int("min_px", cfg.MinPx).Int("max_px", cfg.MaxPx).Msg("invalid sidebar width bounds; swapping")
46 cfg.MinPx, cfg.MaxPx = cfg.MaxPx, cfg.MinPx46 cfg.MinPx, cfg.MaxPx = cfg.MaxPx, cfg.MinPx
47 }47 }
48- clamped := cfg.WidthPx48+ clamped := min(max(cfg.WidthPx, cfg.MinPx), cfg.MaxPx)
49- if clamped < cfg.MinPx {
50- clamped = cfg.MinPx
51- }
52- if clamped > cfg.MaxPx {
53- clamped = cfg.MaxPx
54- }
55 mw.sidebarBox.SetSizeRequest(clamped, -1)49 mw.sidebarBox.SetSizeRequest(clamped, -1)
56 mw.logger.Debug().Int("sidebar_width", clamped).Msg("sidebar width set")50 mw.logger.Debug().Int("sidebar_width", clamped).Msg("sidebar width set")
57 }51 }

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