detached 2595e45

fix(permission): honor overrides and enforce persistability

QueryPermissionState now checks stored decisions before auto-allow defaults so manual denies are respected, and manual decision writes reject non-persistable permission types. Also moves permission repository contract to application port, updates docs for removed revoke API, and hardens IPv6 origin plus GObject helper safety/logging.
commit
2595e459f35d8dd63260819a01a8827b6508a7f3
author
brice · · 2026-02-08 10:10 UTC
parent
d037679
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
11 files · +82 -24
M cmd/dumber/main.go +1 -1
@@ -388,7 +388,7 @@ type repositories struct {
388388 folder repository.FolderRepository
389389 tag repository.TagRepository
390390 zoom repository.ZoomRepository
391- permission repository.PermissionRepository
391+ permission port.PermissionRepository
392392 session repository.SessionRepository
393393 sessionState repository.SessionStateRepository
394394 }
@@ -388,7 +388,7 @@ type repositories struct {
388 folder repository.FolderRepository388 folder repository.FolderRepository
389 tag repository.TagRepository389 tag repository.TagRepository
390 zoom repository.ZoomRepository390 zoom repository.ZoomRepository
391- permission repository.PermissionRepository391+ permission port.PermissionRepository
392 session repository.SessionRepository392 session repository.SessionRepository
393 sessionState repository.SessionStateRepository393 sessionState repository.SessionStateRepository
394 }394 }
M docs/reference/permissions.md +1 -1
@@ -58,7 +58,7 @@ The permission system follows W3C specifications:
5858
5959 Currently, stored permissions can only be cleared by:
6060
61-1. **Site-specific reset**: The site can use `navigator.permissions.revoke()` (if supported)
61+1. **Site-specific reset**: `navigator.permissions.revoke()` was removed from the Permissions API and is not supported in modern browsers; direct users to browser/site permission settings (or account-level controls when available) to reset access.
6262 2. **Full reset**: Remove `~/.local/share/dumber/dumber.db` (deletes all data including permissions)
6363
6464 A permission management UI is planned for a future release.
@@ -58,7 +58,7 @@ The permission system follows W3C specifications:
58 58
59 Currently, stored permissions can only be cleared by:59 Currently, stored permissions can only be cleared by:
60 60
61-1. **Site-specific reset**: The site can use `navigator.permissions.revoke()` (if supported)61+1. **Site-specific reset**: `navigator.permissions.revoke()` was removed from the Permissions API and is not supported in modern browsers; direct users to browser/site permission settings (or account-level controls when available) to reset access.
62 2. **Full reset**: Remove `~/.local/share/dumber/dumber.db` (deletes all data including permissions)62 2. **Full reset**: Remove `~/.local/share/dumber/dumber.db` (deletes all data including permissions)
63 63
64 A permission management UI is planned for a future release.64 A permission management UI is planned for a future release.
R internal/domain/repository/permission.go → internal/application/port/permission_repository.go +1 -1 renamed, 97% similar
@@ -1,4 +1,4 @@
1-package repository
1+package port
22
33 import (
44 "context"
@@ -1,4 +1,4 @@
1-package repository1+package port
2 2
3 import (3 import (
4 "context"4 "context"
M internal/application/usecase/handle_permission.go +16 -15
@@ -9,7 +9,6 @@ import (
99
1010 "github.com/bnema/dumber/internal/application/port"
1111 "github.com/bnema/dumber/internal/domain/entity"
12- "github.com/bnema/dumber/internal/domain/repository"
1312 "github.com/bnema/dumber/internal/logging"
1413 )
1514
@@ -25,14 +24,14 @@ type PermissionCallback struct {
2524 // - Device enumeration: auto-allow (low risk)
2625 // - Mic/Camera: check stored → dialog → persist if "Always"
2726 type HandlePermissionUseCase struct {
28- permRepo repository.PermissionRepository
27+ permRepo port.PermissionRepository
2928 dialog port.PermissionDialogPresenter
3029 dialogMu sync.RWMutex
3130 }
3231
3332 // NewHandlePermissionUseCase creates a new permission handling use case.
3433 func NewHandlePermissionUseCase(
35- permRepo repository.PermissionRepository,
34+ permRepo port.PermissionRepository,
3635 dialog port.PermissionDialogPresenter,
3736 ) *HandlePermissionUseCase {
3837 return &HandlePermissionUseCase{
@@ -135,26 +134,25 @@ func (uc *HandlePermissionUseCase) QueryPermissionState(
135134 Str("type", string(permType)).
136135 Logger()
137136
138- // Auto-allow types
139- if entity.IsAutoAllow(permType) {
140- log.Debug().Msg("query: auto-allow type returns granted")
141- return entity.PermissionGranted
142- }
143-
144- // Check stored permission
137+ // Check stored permission first (including manual overrides on auto-allow types).
145138 record, err := uc.permRepo.Get(ctx, origin, permType)
146139 if err != nil {
147140 log.Warn().Err(err).Msg("query: failed to get stored permission, returning prompt")
148141 return entity.PermissionPrompt
149142 }
150143
151- if record == nil {
152- log.Debug().Msg("query: no stored permission, returning prompt")
153- return entity.PermissionPrompt
144+ if record != nil {
145+ log.Debug().Str("decision", string(record.Decision)).Msg("query: returning stored permission")
146+ return record.Decision
154147 }
155148
156- log.Debug().Str("decision", string(record.Decision)).Msg("query: returning stored permission")
157- return record.Decision
149+ if entity.IsAutoAllow(permType) {
150+ log.Debug().Msg("query: no stored permission for auto-allow type, returning granted")
151+ return entity.PermissionGranted
152+ }
153+
154+ log.Debug().Msg("query: no stored permission, returning prompt")
155+ return entity.PermissionPrompt
158156 }
159157
160158 // isAutoAllowSet returns true if all permission types in the set are auto-allow.
@@ -318,6 +316,9 @@ func (uc *HandlePermissionUseCase) SetManualPermissionDecision(
318316 if origin == "" {
319317 return errors.New("origin is required")
320318 }
319+ if !entity.CanPersist(permType) {
320+ return errors.New("permission type not persistable")
321+ }
321322 if decision == entity.PermissionPrompt {
322323 return uc.permRepo.Delete(ctx, origin, permType)
323324 }
@@ -9,7 +9,6 @@ import (
9 9
10 "github.com/bnema/dumber/internal/application/port"10 "github.com/bnema/dumber/internal/application/port"
11 "github.com/bnema/dumber/internal/domain/entity"11 "github.com/bnema/dumber/internal/domain/entity"
12- "github.com/bnema/dumber/internal/domain/repository"
13 "github.com/bnema/dumber/internal/logging"12 "github.com/bnema/dumber/internal/logging"
14 )13 )
15 14
@@ -25,14 +24,14 @@ type PermissionCallback struct {
25 // - Device enumeration: auto-allow (low risk)24 // - Device enumeration: auto-allow (low risk)
26 // - Mic/Camera: check stored → dialog → persist if "Always"25 // - Mic/Camera: check stored → dialog → persist if "Always"
27 type HandlePermissionUseCase struct {26 type HandlePermissionUseCase struct {
28- permRepo repository.PermissionRepository27+ permRepo port.PermissionRepository
29 dialog port.PermissionDialogPresenter28 dialog port.PermissionDialogPresenter
30 dialogMu sync.RWMutex29 dialogMu sync.RWMutex
31 }30 }
32 31
33 // NewHandlePermissionUseCase creates a new permission handling use case.32 // NewHandlePermissionUseCase creates a new permission handling use case.
34 func NewHandlePermissionUseCase(33 func NewHandlePermissionUseCase(
35- permRepo repository.PermissionRepository,34+ permRepo port.PermissionRepository,
36 dialog port.PermissionDialogPresenter,35 dialog port.PermissionDialogPresenter,
37 ) *HandlePermissionUseCase {36 ) *HandlePermissionUseCase {
38 return &HandlePermissionUseCase{37 return &HandlePermissionUseCase{
@@ -135,26 +134,25 @@ func (uc *HandlePermissionUseCase) QueryPermissionState(
135 Str("type", string(permType)).134 Str("type", string(permType)).
136 Logger()135 Logger()
137 136
138- // Auto-allow types137+ // Check stored permission first (including manual overrides on auto-allow types).
139- if entity.IsAutoAllow(permType) {
140- log.Debug().Msg("query: auto-allow type returns granted")
141- return entity.PermissionGranted
142- }
143-
144- // Check stored permission
145 record, err := uc.permRepo.Get(ctx, origin, permType)138 record, err := uc.permRepo.Get(ctx, origin, permType)
146 if err != nil {139 if err != nil {
147 log.Warn().Err(err).Msg("query: failed to get stored permission, returning prompt")140 log.Warn().Err(err).Msg("query: failed to get stored permission, returning prompt")
148 return entity.PermissionPrompt141 return entity.PermissionPrompt
149 }142 }
150 143
151- if record == nil {144+ if record != nil {
152- log.Debug().Msg("query: no stored permission, returning prompt")145+ log.Debug().Str("decision", string(record.Decision)).Msg("query: returning stored permission")
153- return entity.PermissionPrompt146+ return record.Decision
154 }147 }
155 148
156- log.Debug().Str("decision", string(record.Decision)).Msg("query: returning stored permission")149+ if entity.IsAutoAllow(permType) {
157- return record.Decision150+ log.Debug().Msg("query: no stored permission for auto-allow type, returning granted")
151+ return entity.PermissionGranted
152+ }
153+
154+ log.Debug().Msg("query: no stored permission, returning prompt")
155+ return entity.PermissionPrompt
158 }156 }
159 157
160 // isAutoAllowSet returns true if all permission types in the set are auto-allow.158 // isAutoAllowSet returns true if all permission types in the set are auto-allow.
@@ -318,6 +316,9 @@ func (uc *HandlePermissionUseCase) SetManualPermissionDecision(
318 if origin == "" {316 if origin == "" {
319 return errors.New("origin is required")317 return errors.New("origin is required")
320 }318 }
319+ if !entity.CanPersist(permType) {
320+ return errors.New("permission type not persistable")
321+ }
321 if decision == entity.PermissionPrompt {322 if decision == entity.PermissionPrompt {
322 return uc.permRepo.Delete(ctx, origin, permType)323 return uc.permRepo.Delete(ctx, origin, permType)
323 }324 }
M internal/application/usecase/handle_permission_test.go +39 -0
@@ -299,12 +299,33 @@ func TestHandlePermissionUseCase_QueryPermissionState_AutoAllow(t *testing.T) {
299299 dialog := portmocks.NewMockPermissionDialogPresenter(t)
300300
301301 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog)
302+ permRepo.EXPECT().Get(mock.Anything, "https://example.com", entity.PermissionTypeDisplay).
303+ Return(nil, nil)
302304
303305 state := uc.QueryPermissionState(ctx, "https://example.com", entity.PermissionTypeDisplay)
304306
305307 assert.Equal(t, entity.PermissionGranted, state, "display capture should auto-return granted")
306308 }
307309
310+func TestHandlePermissionUseCase_QueryPermissionState_AutoAllowManualOverride(t *testing.T) {
311+ ctx := testContext()
312+ permRepo := repomocks.NewMockPermissionRepository(t)
313+ dialog := portmocks.NewMockPermissionDialogPresenter(t)
314+
315+ uc := usecase.NewHandlePermissionUseCase(permRepo, dialog)
316+
317+ permRepo.EXPECT().Get(mock.Anything, "https://example.com", entity.PermissionTypeDisplay).
318+ Return(&entity.PermissionRecord{
319+ Origin: "https://example.com",
320+ Type: entity.PermissionTypeDisplay,
321+ Decision: entity.PermissionDenied,
322+ }, nil)
323+
324+ state := uc.QueryPermissionState(ctx, "https://example.com", entity.PermissionTypeDisplay)
325+
326+ assert.Equal(t, entity.PermissionDenied, state, "stored manual deny should override auto-allow")
327+}
328+
308329 func TestHandlePermissionUseCase_QueryPermissionState_Stored(t *testing.T) {
309330 ctx := testContext()
310331 permRepo := repomocks.NewMockPermissionRepository(t)
@@ -457,3 +478,21 @@ func TestHandlePermissionUseCase_GetSetResetManualDecision(t *testing.T) {
457478 err = uc.ResetManualPermissionDecision(ctx, "https://app.zoom.us", entity.PermissionTypeMicrophone)
458479 require.NoError(t, err)
459480 }
481+
482+func TestHandlePermissionUseCase_SetManualPermissionDecision_NonPersistable(t *testing.T) {
483+ ctx := testContext()
484+ permRepo := repomocks.NewMockPermissionRepository(t)
485+ uc := usecase.NewHandlePermissionUseCase(permRepo, nil)
486+
487+ err := uc.SetManualPermissionDecision(
488+ ctx,
489+ "https://app.zoom.us",
490+ entity.PermissionTypeDisplay,
491+ entity.PermissionDenied,
492+ )
493+
494+ require.Error(t, err)
495+ require.ErrorContains(t, err, "permission type not persistable")
496+ permRepo.AssertNotCalled(t, "Set")
497+ permRepo.AssertNotCalled(t, "Delete")
498+}
@@ -299,12 +299,33 @@ func TestHandlePermissionUseCase_QueryPermissionState_AutoAllow(t *testing.T) {
299 dialog := portmocks.NewMockPermissionDialogPresenter(t)299 dialog := portmocks.NewMockPermissionDialogPresenter(t)
300 300
301 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog)301 uc := usecase.NewHandlePermissionUseCase(permRepo, dialog)
302+ permRepo.EXPECT().Get(mock.Anything, "https://example.com", entity.PermissionTypeDisplay).
303+ Return(nil, nil)
302 304
303 state := uc.QueryPermissionState(ctx, "https://example.com", entity.PermissionTypeDisplay)305 state := uc.QueryPermissionState(ctx, "https://example.com", entity.PermissionTypeDisplay)
304 306
305 assert.Equal(t, entity.PermissionGranted, state, "display capture should auto-return granted")307 assert.Equal(t, entity.PermissionGranted, state, "display capture should auto-return granted")
306 }308 }
307 309
310+func TestHandlePermissionUseCase_QueryPermissionState_AutoAllowManualOverride(t *testing.T) {
311+ ctx := testContext()
312+ permRepo := repomocks.NewMockPermissionRepository(t)
313+ dialog := portmocks.NewMockPermissionDialogPresenter(t)
314+
315+ uc := usecase.NewHandlePermissionUseCase(permRepo, dialog)
316+
317+ permRepo.EXPECT().Get(mock.Anything, "https://example.com", entity.PermissionTypeDisplay).
318+ Return(&entity.PermissionRecord{
319+ Origin: "https://example.com",
320+ Type: entity.PermissionTypeDisplay,
321+ Decision: entity.PermissionDenied,
322+ }, nil)
323+
324+ state := uc.QueryPermissionState(ctx, "https://example.com", entity.PermissionTypeDisplay)
325+
326+ assert.Equal(t, entity.PermissionDenied, state, "stored manual deny should override auto-allow")
327+}
328+
308 func TestHandlePermissionUseCase_QueryPermissionState_Stored(t *testing.T) {329 func TestHandlePermissionUseCase_QueryPermissionState_Stored(t *testing.T) {
309 ctx := testContext()330 ctx := testContext()
310 permRepo := repomocks.NewMockPermissionRepository(t)331 permRepo := repomocks.NewMockPermissionRepository(t)
@@ -457,3 +478,21 @@ func TestHandlePermissionUseCase_GetSetResetManualDecision(t *testing.T) {
457 err = uc.ResetManualPermissionDecision(ctx, "https://app.zoom.us", entity.PermissionTypeMicrophone)478 err = uc.ResetManualPermissionDecision(ctx, "https://app.zoom.us", entity.PermissionTypeMicrophone)
458 require.NoError(t, err)479 require.NoError(t, err)
459 }480 }
481+
482+func TestHandlePermissionUseCase_SetManualPermissionDecision_NonPersistable(t *testing.T) {
483+ ctx := testContext()
484+ permRepo := repomocks.NewMockPermissionRepository(t)
485+ uc := usecase.NewHandlePermissionUseCase(permRepo, nil)
486+
487+ err := uc.SetManualPermissionDecision(
488+ ctx,
489+ "https://app.zoom.us",
490+ entity.PermissionTypeDisplay,
491+ entity.PermissionDenied,
492+ )
493+
494+ require.Error(t, err)
495+ require.ErrorContains(t, err, "permission type not persistable")
496+ permRepo.AssertNotCalled(t, "Set")
497+ permRepo.AssertNotCalled(t, "Delete")
498+}
M internal/domain/url/normalize.go +3 -0
@@ -222,6 +222,9 @@ func ExtractOrigin(uri string) (string, error) {
222222 // Canonicalize: lowercase scheme and hostname
223223 scheme := strings.ToLower(parsed.Scheme)
224224 hostname := strings.ToLower(parsed.Hostname())
225+ if strings.Contains(hostname, ":") && !strings.HasPrefix(hostname, "[") {
226+ hostname = "[" + hostname + "]"
227+ }
225228
226229 // Determine if we should include the port
227230 port := parsed.Port()
@@ -222,6 +222,9 @@ func ExtractOrigin(uri string) (string, error) {
222 // Canonicalize: lowercase scheme and hostname222 // Canonicalize: lowercase scheme and hostname
223 scheme := strings.ToLower(parsed.Scheme)223 scheme := strings.ToLower(parsed.Scheme)
224 hostname := strings.ToLower(parsed.Hostname())224 hostname := strings.ToLower(parsed.Hostname())
225+ if strings.Contains(hostname, ":") && !strings.HasPrefix(hostname, "[") {
226+ hostname = "[" + hostname + "]"
227+ }
225 228
226 // Determine if we should include the port229 // Determine if we should include the port
227 port := parsed.Port()230 port := parsed.Port()
M internal/domain/url/normalize_test.go +5 -0
@@ -561,6 +561,11 @@ func TestExtractOrigin_ValidURI(t *testing.T) {
561561 uri: "HTTPS://EXAMPLE.COM:443/path",
562562 expected: "https://example.com",
563563 },
564+ {
565+ name: "ipv6 with explicit port",
566+ uri: "https://[::1]:8443/path",
567+ expected: "https://[::1]:8443",
568+ },
564569 }
565570
566571 for _, tt := range tests {
@@ -561,6 +561,11 @@ func TestExtractOrigin_ValidURI(t *testing.T) {
561 uri: "HTTPS://EXAMPLE.COM:443/path",561 uri: "HTTPS://EXAMPLE.COM:443/path",
562 expected: "https://example.com",562 expected: "https://example.com",
563 },563 },
564+ {
565+ name: "ipv6 with explicit port",
566+ uri: "https://[::1]:8443/path",
567+ expected: "https://[::1]:8443",
568+ },
564 }569 }
565 570
566 for _, tt := range tests {571 for _, tt := range tests {
M internal/infrastructure/persistence/sqlite/lazy_repos.go +3 -3
@@ -43,13 +43,13 @@ func NewLazyHistoryRepository(provider port.DatabaseProvider) repository.History
4343 // LazyPermissionRepository wraps a permission repository with lazy database initialization.
4444 type LazyPermissionRepository struct {
4545 provider port.DatabaseProvider
46- repo repository.PermissionRepository
46+ repo port.PermissionRepository
4747 once sync.Once
4848 initErr error
4949 }
5050
5151 // NewLazyPermissionRepository creates a lazy-loading permission repository.
52-func NewLazyPermissionRepository(provider port.DatabaseProvider) repository.PermissionRepository {
52+func NewLazyPermissionRepository(provider port.DatabaseProvider) port.PermissionRepository {
5353 return &LazyPermissionRepository{provider: provider}
5454 }
5555
@@ -753,7 +753,7 @@ type LazyRepositories struct {
753753 Session repository.SessionRepository
754754 SessionState repository.SessionStateRepository
755755 Filter repository.ContentWhitelistRepository
756- Permission repository.PermissionRepository
756+ Permission port.PermissionRepository
757757 }
758758
759759 // NewLazyRepositories creates all lazy repositories from a database provider.
@@ -43,13 +43,13 @@ func NewLazyHistoryRepository(provider port.DatabaseProvider) repository.History
43 // LazyPermissionRepository wraps a permission repository with lazy database initialization.43 // LazyPermissionRepository wraps a permission repository with lazy database initialization.
44 type LazyPermissionRepository struct {44 type LazyPermissionRepository struct {
45 provider port.DatabaseProvider45 provider port.DatabaseProvider
46- repo repository.PermissionRepository46+ repo port.PermissionRepository
47 once sync.Once47 once sync.Once
48 initErr error48 initErr error
49 }49 }
50 50
51 // NewLazyPermissionRepository creates a lazy-loading permission repository.51 // NewLazyPermissionRepository creates a lazy-loading permission repository.
52-func NewLazyPermissionRepository(provider port.DatabaseProvider) repository.PermissionRepository {52+func NewLazyPermissionRepository(provider port.DatabaseProvider) port.PermissionRepository {
53 return &LazyPermissionRepository{provider: provider}53 return &LazyPermissionRepository{provider: provider}
54 }54 }
55 55
@@ -753,7 +753,7 @@ type LazyRepositories struct {
753 Session repository.SessionRepository753 Session repository.SessionRepository
754 SessionState repository.SessionStateRepository754 SessionState repository.SessionStateRepository
755 Filter repository.ContentWhitelistRepository755 Filter repository.ContentWhitelistRepository
756- Permission repository.PermissionRepository756+ Permission port.PermissionRepository
757 }757 }
758 758
759 // NewLazyRepositories creates all lazy repositories from a database provider.759 // NewLazyRepositories creates all lazy repositories from a database provider.
M internal/infrastructure/persistence/sqlite/permission_repo.go +2 -2
@@ -5,8 +5,8 @@ import (
55 "database/sql"
66 "errors"
77
8+ "github.com/bnema/dumber/internal/application/port"
89 "github.com/bnema/dumber/internal/domain/entity"
9- "github.com/bnema/dumber/internal/domain/repository"
1010 "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite/sqlc"
1111 "github.com/bnema/dumber/internal/logging"
1212 )
@@ -16,7 +16,7 @@ type permissionRepo struct {
1616 }
1717
1818 // NewPermissionRepository creates a new SQLite-backed permission repository.
19-func NewPermissionRepository(db *sql.DB) repository.PermissionRepository {
19+func NewPermissionRepository(db *sql.DB) port.PermissionRepository {
2020 return &permissionRepo{queries: sqlc.New(db)}
2121 }
2222
@@ -5,8 +5,8 @@ import (
5 "database/sql"5 "database/sql"
6 "errors"6 "errors"
7 7
8+ "github.com/bnema/dumber/internal/application/port"
8 "github.com/bnema/dumber/internal/domain/entity"9 "github.com/bnema/dumber/internal/domain/entity"
9- "github.com/bnema/dumber/internal/domain/repository"
10 "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite/sqlc"10 "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite/sqlc"
11 "github.com/bnema/dumber/internal/logging"11 "github.com/bnema/dumber/internal/logging"
12 )12 )
@@ -16,7 +16,7 @@ type permissionRepo struct {
16 }16 }
17 17
18 // NewPermissionRepository creates a new SQLite-backed permission repository.18 // NewPermissionRepository creates a new SQLite-backed permission repository.
19-func NewPermissionRepository(db *sql.DB) repository.PermissionRepository {19+func NewPermissionRepository(db *sql.DB) port.PermissionRepository {
20 return &permissionRepo{queries: sqlc.New(db)}20 return &permissionRepo{queries: sqlc.New(db)}
21 }21 }
22 22
M internal/infrastructure/webkit/gobject_type_helpers.go +10 -0
@@ -45,6 +45,10 @@ func permissionRequestTypeName(ctx context.Context, instancePtr uintptr) string
4545 }
4646
4747 func loadGObjectTypeFns(ctx context.Context) {
48+ if ctx == nil {
49+ ctx = context.Background()
50+ }
51+
4852 loadGObjectTypeFnsOnce.Do(func() {
4953 log := logging.FromContext(ctx)
5054 libs := make([]uintptr, 0, 2)
@@ -62,6 +66,12 @@ func loadGObjectTypeFns(ctx context.Context) {
6266 }
6367
6468 core.PuregoSafeRegister(&typeCheckInstanceIsA, libs, "g_type_check_instance_is_a")
69+ if typeCheckInstanceIsA == nil {
70+ log.Debug().Msg("failed to register g_type_check_instance_is_a")
71+ }
6572 core.PuregoSafeRegister(&typeNameFromInstance, libs, "g_type_name_from_instance")
73+ if typeNameFromInstance == nil {
74+ log.Debug().Msg("failed to register g_type_name_from_instance")
75+ }
6676 })
6777 }
@@ -45,6 +45,10 @@ func permissionRequestTypeName(ctx context.Context, instancePtr uintptr) string
45 }45 }
46 46
47 func loadGObjectTypeFns(ctx context.Context) {47 func loadGObjectTypeFns(ctx context.Context) {
48+ if ctx == nil {
49+ ctx = context.Background()
50+ }
51+
48 loadGObjectTypeFnsOnce.Do(func() {52 loadGObjectTypeFnsOnce.Do(func() {
49 log := logging.FromContext(ctx)53 log := logging.FromContext(ctx)
50 libs := make([]uintptr, 0, 2)54 libs := make([]uintptr, 0, 2)
@@ -62,6 +66,12 @@ func loadGObjectTypeFns(ctx context.Context) {
62 }66 }
63 67
64 core.PuregoSafeRegister(&typeCheckInstanceIsA, libs, "g_type_check_instance_is_a")68 core.PuregoSafeRegister(&typeCheckInstanceIsA, libs, "g_type_check_instance_is_a")
69+ if typeCheckInstanceIsA == nil {
70+ log.Debug().Msg("failed to register g_type_check_instance_is_a")
71+ }
65 core.PuregoSafeRegister(&typeNameFromInstance, libs, "g_type_name_from_instance")72 core.PuregoSafeRegister(&typeNameFromInstance, libs, "g_type_name_from_instance")
73+ if typeNameFromInstance == nil {
74+ log.Debug().Msg("failed to register g_type_name_from_instance")
75+ }
66 })76 })
67 }77 }
M internal/ui/deps.go +1 -1
@@ -47,7 +47,7 @@ type Dependencies struct {
4747 HistoryRepo repository.HistoryRepository
4848 FavoriteRepo repository.FavoriteRepository
4949 ZoomRepo repository.ZoomRepository
50- PermissionRepo repository.PermissionRepository
50+ PermissionRepo port.PermissionRepository
5151 FilterRepo repository.ContentWhitelistRepository
5252
5353 // Use Cases
@@ -47,7 +47,7 @@ type Dependencies struct {
47 HistoryRepo repository.HistoryRepository47 HistoryRepo repository.HistoryRepository
48 FavoriteRepo repository.FavoriteRepository48 FavoriteRepo repository.FavoriteRepository
49 ZoomRepo repository.ZoomRepository49 ZoomRepo repository.ZoomRepository
50- PermissionRepo repository.PermissionRepository50+ PermissionRepo port.PermissionRepository
51 FilterRepo repository.ContentWhitelistRepository51 FilterRepo repository.ContentWhitelistRepository
52 52
53 // Use Cases53 // Use Cases

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