detached ef3f82a

feat(permission): wire permission system into main application

Initialize PermissionRepository and HandlePermissionUseCase in main.
Add PermissionUC and PermissionRepo to Dependencies struct.
Add PermissionRepository to LazyRepositories convenience struct.
Update WebView port interface with OnPermissionRequest callback.
Wire permission dialog presenter when UI window is ready.
commit
ef3f82a0eefc7255017bf789c096c2be5c628afb
author
brice · · 2026-01-30 05:01 UTC
committer
brice · · 2026-02-07 11:58 UTC
parent
b7f1b12
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +95 -11
M cmd/dumber/main.go +10 -0
@@ -388,6 +388,7 @@ type repositories struct {
388388 folder repository.FolderRepository
389389 tag repository.TagRepository
390390 zoom repository.ZoomRepository
391+ permission repository.PermissionRepository
391392 session repository.SessionRepository
392393 sessionState repository.SessionStateRepository
393394 }
@@ -399,6 +400,7 @@ func createRepositories(db *sql.DB) *repositories {
399400 folder: sqlite.NewFolderRepository(db),
400401 tag: sqlite.NewTagRepository(db),
401402 zoom: sqlite.NewZoomRepository(db),
403+ permission: sqlite.NewPermissionRepository(db),
402404 session: sqlite.NewSessionRepository(db),
403405 sessionState: sqlite.NewSessionStateRepository(db),
404406 }
@@ -411,6 +413,7 @@ func createLazyRepositories(provider port.DatabaseProvider) *repositories {
411413 folder: sqlite.NewLazyFolderRepository(provider),
412414 tag: sqlite.NewLazyTagRepository(provider),
413415 zoom: sqlite.NewLazyZoomRepository(provider),
416+ permission: sqlite.NewLazyPermissionRepository(provider),
414417 session: sqlite.NewLazySessionRepository(provider),
415418 sessionState: sqlite.NewLazySessionStateRepository(provider),
416419 }
@@ -423,6 +426,7 @@ type useCases struct {
423426 history *usecase.SearchHistoryUseCase
424427 favorites *usecase.ManageFavoritesUseCase
425428 zoom *usecase.ManageZoomUseCase
429+ permission *usecase.HandlePermissionUseCase
426430 navigate *usecase.NavigateUseCase
427431 copyURL *usecase.CopyURLUseCase
428432 snapshot *usecase.SnapshotSessionUseCase
@@ -458,12 +462,16 @@ func createUseCases(repos *repositories, cfg *config.Config) *useCases {
458462 updateDownloader := updater.NewGitHubDownloader()
459463 updateApplier := updater.NewApplier(stateDir)
460464
465+ // Permission use case will be initialized later with dialog presenter
466+ permissionUC := usecase.NewHandlePermissionUseCase(repos.permission, nil)
467+
461468 return &useCases{
462469 tabs: usecase.NewManageTabsUseCase(idGenerator),
463470 panes: usecase.NewManagePanesUseCase(idGenerator),
464471 history: usecase.NewSearchHistoryUseCase(repos.history),
465472 favorites: usecase.NewManageFavoritesUseCase(repos.favorite, repos.folder, repos.tag),
466473 zoom: usecase.NewManageZoomUseCase(repos.zoom, defaultZoom, zoomCache),
474+ permission: permissionUC,
467475 navigate: usecase.NewNavigateUseCase(repos.history, repos.zoom, defaultZoom),
468476 copyURL: usecase.NewCopyURLUseCase(clipboardAdapter),
469477 snapshot: usecase.NewSnapshotSessionUseCase(repos.sessionState),
@@ -505,11 +513,13 @@ func buildUIDependencies(
505513 HistoryRepo: repos.history,
506514 FavoriteRepo: repos.favorite,
507515 ZoomRepo: repos.zoom,
516+ PermissionRepo: repos.permission,
508517 TabsUC: uc.tabs,
509518 PanesUC: uc.panes,
510519 HistoryUC: uc.history,
511520 FavoritesUC: uc.favorites,
512521 ZoomUC: uc.zoom,
522+ PermissionUC: uc.permission,
513523 NavigateUC: uc.navigate,
514524 CopyURLUC: uc.copyURL,
515525 Clipboard: uc.clipboard,
@@ -388,6 +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.PermissionRepository
391 session repository.SessionRepository392 session repository.SessionRepository
392 sessionState repository.SessionStateRepository393 sessionState repository.SessionStateRepository
393 }394 }
@@ -399,6 +400,7 @@ func createRepositories(db *sql.DB) *repositories {
399 folder: sqlite.NewFolderRepository(db),400 folder: sqlite.NewFolderRepository(db),
400 tag: sqlite.NewTagRepository(db),401 tag: sqlite.NewTagRepository(db),
401 zoom: sqlite.NewZoomRepository(db),402 zoom: sqlite.NewZoomRepository(db),
403+ permission: sqlite.NewPermissionRepository(db),
402 session: sqlite.NewSessionRepository(db),404 session: sqlite.NewSessionRepository(db),
403 sessionState: sqlite.NewSessionStateRepository(db),405 sessionState: sqlite.NewSessionStateRepository(db),
404 }406 }
@@ -411,6 +413,7 @@ func createLazyRepositories(provider port.DatabaseProvider) *repositories {
411 folder: sqlite.NewLazyFolderRepository(provider),413 folder: sqlite.NewLazyFolderRepository(provider),
412 tag: sqlite.NewLazyTagRepository(provider),414 tag: sqlite.NewLazyTagRepository(provider),
413 zoom: sqlite.NewLazyZoomRepository(provider),415 zoom: sqlite.NewLazyZoomRepository(provider),
416+ permission: sqlite.NewLazyPermissionRepository(provider),
414 session: sqlite.NewLazySessionRepository(provider),417 session: sqlite.NewLazySessionRepository(provider),
415 sessionState: sqlite.NewLazySessionStateRepository(provider),418 sessionState: sqlite.NewLazySessionStateRepository(provider),
416 }419 }
@@ -423,6 +426,7 @@ type useCases struct {
423 history *usecase.SearchHistoryUseCase426 history *usecase.SearchHistoryUseCase
424 favorites *usecase.ManageFavoritesUseCase427 favorites *usecase.ManageFavoritesUseCase
425 zoom *usecase.ManageZoomUseCase428 zoom *usecase.ManageZoomUseCase
429+ permission *usecase.HandlePermissionUseCase
426 navigate *usecase.NavigateUseCase430 navigate *usecase.NavigateUseCase
427 copyURL *usecase.CopyURLUseCase431 copyURL *usecase.CopyURLUseCase
428 snapshot *usecase.SnapshotSessionUseCase432 snapshot *usecase.SnapshotSessionUseCase
@@ -458,12 +462,16 @@ func createUseCases(repos *repositories, cfg *config.Config) *useCases {
458 updateDownloader := updater.NewGitHubDownloader()462 updateDownloader := updater.NewGitHubDownloader()
459 updateApplier := updater.NewApplier(stateDir)463 updateApplier := updater.NewApplier(stateDir)
460 464
465+ // Permission use case will be initialized later with dialog presenter
466+ permissionUC := usecase.NewHandlePermissionUseCase(repos.permission, nil)
467+
461 return &useCases{468 return &useCases{
462 tabs: usecase.NewManageTabsUseCase(idGenerator),469 tabs: usecase.NewManageTabsUseCase(idGenerator),
463 panes: usecase.NewManagePanesUseCase(idGenerator),470 panes: usecase.NewManagePanesUseCase(idGenerator),
464 history: usecase.NewSearchHistoryUseCase(repos.history),471 history: usecase.NewSearchHistoryUseCase(repos.history),
465 favorites: usecase.NewManageFavoritesUseCase(repos.favorite, repos.folder, repos.tag),472 favorites: usecase.NewManageFavoritesUseCase(repos.favorite, repos.folder, repos.tag),
466 zoom: usecase.NewManageZoomUseCase(repos.zoom, defaultZoom, zoomCache),473 zoom: usecase.NewManageZoomUseCase(repos.zoom, defaultZoom, zoomCache),
474+ permission: permissionUC,
467 navigate: usecase.NewNavigateUseCase(repos.history, repos.zoom, defaultZoom),475 navigate: usecase.NewNavigateUseCase(repos.history, repos.zoom, defaultZoom),
468 copyURL: usecase.NewCopyURLUseCase(clipboardAdapter),476 copyURL: usecase.NewCopyURLUseCase(clipboardAdapter),
469 snapshot: usecase.NewSnapshotSessionUseCase(repos.sessionState),477 snapshot: usecase.NewSnapshotSessionUseCase(repos.sessionState),
@@ -505,11 +513,13 @@ func buildUIDependencies(
505 HistoryRepo: repos.history,513 HistoryRepo: repos.history,
506 FavoriteRepo: repos.favorite,514 FavoriteRepo: repos.favorite,
507 ZoomRepo: repos.zoom,515 ZoomRepo: repos.zoom,
516+ PermissionRepo: repos.permission,
508 TabsUC: uc.tabs,517 TabsUC: uc.tabs,
509 PanesUC: uc.panes,518 PanesUC: uc.panes,
510 HistoryUC: uc.history,519 HistoryUC: uc.history,
511 FavoritesUC: uc.favorites,520 FavoritesUC: uc.favorites,
512 ZoomUC: uc.zoom,521 ZoomUC: uc.zoom,
522+ PermissionUC: uc.permission,
513 NavigateUC: uc.navigate,523 NavigateUC: uc.navigate,
514 CopyURLUC: uc.copyURL,524 CopyURLUC: uc.copyURL,
515 Clipboard: uc.clipboard,525 Clipboard: uc.clipboard,
M internal/application/port/webview.go +5 -0
@@ -104,6 +104,11 @@ type WebViewCallbacks struct {
104104 OnLinkHover func(uri string)
105105 // OnWebProcessTerminated is called when the web process exits unexpectedly.
106106 OnWebProcessTerminated func(reason WebProcessTerminationReason, reasonLabel string, uri string)
107+
108+ // OnPermissionRequest is called when a site requests permission (mic, camera, screen sharing).
109+ // Return true to indicate the request was handled. Call allow()/deny() to respond.
110+ // The permission types are passed as strings ("microphone", "camera", "display", etc.).
111+ OnPermissionRequest func(origin string, permTypes []string, allow, deny func()) bool
107112 }
108113
109114 // FindOptions configures search behavior.
@@ -104,6 +104,11 @@ type WebViewCallbacks struct {
104 OnLinkHover func(uri string)104 OnLinkHover func(uri string)
105 // OnWebProcessTerminated is called when the web process exits unexpectedly.105 // OnWebProcessTerminated is called when the web process exits unexpectedly.
106 OnWebProcessTerminated func(reason WebProcessTerminationReason, reasonLabel string, uri string)106 OnWebProcessTerminated func(reason WebProcessTerminationReason, reasonLabel string, uri string)
107+
108+ // OnPermissionRequest is called when a site requests permission (mic, camera, screen sharing).
109+ // Return true to indicate the request was handled. Call allow()/deny() to respond.
110+ // The permission types are passed as strings ("microphone", "camera", "display", etc.).
111+ OnPermissionRequest func(origin string, permTypes []string, allow, deny func()) bool
107 }112 }
108 113
109 // FindOptions configures search behavior.114 // FindOptions configures search behavior.
M internal/infrastructure/persistence/sqlite/lazy_repos.go +59 -0
@@ -40,6 +40,63 @@ func NewLazyHistoryRepository(provider port.DatabaseProvider) repository.History
4040 return &LazyHistoryRepository{provider: provider}
4141 }
4242
43+// LazyPermissionRepository wraps a permission repository with lazy database initialization.
44+type LazyPermissionRepository struct {
45+ provider port.DatabaseProvider
46+ repo repository.PermissionRepository
47+ once sync.Once
48+ initErr error
49+}
50+
51+// NewLazyPermissionRepository creates a lazy-loading permission repository.
52+func NewLazyPermissionRepository(provider port.DatabaseProvider) repository.PermissionRepository {
53+ return &LazyPermissionRepository{provider: provider}
54+}
55+
56+func (r *LazyPermissionRepository) init(ctx context.Context) error {
57+ r.once.Do(func() {
58+ db, err := r.provider.DB(ctx)
59+ if err != nil {
60+ r.initErr = err
61+ return
62+ }
63+ r.repo = NewPermissionRepository(db)
64+ })
65+ return r.initErr
66+}
67+
68+func (r *LazyPermissionRepository) Get(
69+ ctx context.Context,
70+ origin string,
71+ permType entity.PermissionType,
72+) (*entity.PermissionRecord, error) {
73+ if err := r.init(ctx); err != nil {
74+ return nil, err
75+ }
76+ return r.repo.Get(ctx, origin, permType)
77+}
78+
79+func (r *LazyPermissionRepository) Set(ctx context.Context, record *entity.PermissionRecord) error {
80+ if err := r.init(ctx); err != nil {
81+ return err
82+ }
83+ return r.repo.Set(ctx, record)
84+}
85+
86+func (r *LazyPermissionRepository) Delete(ctx context.Context, origin string, permType entity.PermissionType) error {
87+ if err := r.init(ctx); err != nil {
88+ return err
89+ }
90+ return r.repo.Delete(ctx, origin, permType)
91+}
92+
93+func (r *LazyPermissionRepository) GetAll(ctx context.Context, origin string) ([]*entity.PermissionRecord, error) {
94+ if err := r.init(ctx); err != nil {
95+ return nil, err
96+ }
97+ return r.repo.GetAll(ctx, origin)
98+}
99+
43100 func (r *LazyHistoryRepository) init(ctx context.Context) error {
44101 r.once.Do(func() {
45102 db, err := r.provider.DB(ctx)
@@ -696,6 +753,7 @@ type LazyRepositories struct {
696753 Session repository.SessionRepository
697754 SessionState repository.SessionStateRepository
698755 Filter repository.ContentWhitelistRepository
756+ Permission repository.PermissionRepository
699757 }
700758
701759 // NewLazyRepositories creates all lazy repositories from a database provider.
@@ -709,6 +767,7 @@ func NewLazyRepositories(provider port.DatabaseProvider) *LazyRepositories {
709767 Session: NewLazySessionRepository(provider),
710768 SessionState: NewLazySessionStateRepository(provider),
711769 Filter: NewLazyContentWhitelistRepository(provider),
770+ Permission: NewLazyPermissionRepository(provider),
712771 }
713772 }
714773
@@ -40,6 +40,63 @@ func NewLazyHistoryRepository(provider port.DatabaseProvider) repository.History
40 return &LazyHistoryRepository{provider: provider}40 return &LazyHistoryRepository{provider: provider}
41 }41 }
42 42
43+// LazyPermissionRepository wraps a permission repository with lazy database initialization.
44+type LazyPermissionRepository struct {
45+ provider port.DatabaseProvider
46+ repo repository.PermissionRepository
47+ once sync.Once
48+ initErr error
49+}
50+
51+// NewLazyPermissionRepository creates a lazy-loading permission repository.
52+func NewLazyPermissionRepository(provider port.DatabaseProvider) repository.PermissionRepository {
53+ return &LazyPermissionRepository{provider: provider}
54+}
55+
56+func (r *LazyPermissionRepository) init(ctx context.Context) error {
57+ r.once.Do(func() {
58+ db, err := r.provider.DB(ctx)
59+ if err != nil {
60+ r.initErr = err
61+ return
62+ }
63+ r.repo = NewPermissionRepository(db)
64+ })
65+ return r.initErr
66+}
67+
68+func (r *LazyPermissionRepository) Get(
69+ ctx context.Context,
70+ origin string,
71+ permType entity.PermissionType,
72+) (*entity.PermissionRecord, error) {
73+ if err := r.init(ctx); err != nil {
74+ return nil, err
75+ }
76+ return r.repo.Get(ctx, origin, permType)
77+}
78+
79+func (r *LazyPermissionRepository) Set(ctx context.Context, record *entity.PermissionRecord) error {
80+ if err := r.init(ctx); err != nil {
81+ return err
82+ }
83+ return r.repo.Set(ctx, record)
84+}
85+
86+func (r *LazyPermissionRepository) Delete(ctx context.Context, origin string, permType entity.PermissionType) error {
87+ if err := r.init(ctx); err != nil {
88+ return err
89+ }
90+ return r.repo.Delete(ctx, origin, permType)
91+}
92+
93+func (r *LazyPermissionRepository) GetAll(ctx context.Context, origin string) ([]*entity.PermissionRecord, error) {
94+ if err := r.init(ctx); err != nil {
95+ return nil, err
96+ }
97+ return r.repo.GetAll(ctx, origin)
98+}
99+
43 func (r *LazyHistoryRepository) init(ctx context.Context) error {100 func (r *LazyHistoryRepository) init(ctx context.Context) error {
44 r.once.Do(func() {101 r.once.Do(func() {
45 db, err := r.provider.DB(ctx)102 db, err := r.provider.DB(ctx)
@@ -696,6 +753,7 @@ type LazyRepositories struct {
696 Session repository.SessionRepository753 Session repository.SessionRepository
697 SessionState repository.SessionStateRepository754 SessionState repository.SessionStateRepository
698 Filter repository.ContentWhitelistRepository755 Filter repository.ContentWhitelistRepository
756+ Permission repository.PermissionRepository
699 }757 }
700 758
701 // NewLazyRepositories creates all lazy repositories from a database provider.759 // NewLazyRepositories creates all lazy repositories from a database provider.
@@ -709,6 +767,7 @@ func NewLazyRepositories(provider port.DatabaseProvider) *LazyRepositories {
709 Session: NewLazySessionRepository(provider),767 Session: NewLazySessionRepository(provider),
710 SessionState: NewLazySessionStateRepository(provider),768 SessionState: NewLazySessionStateRepository(provider),
711 Filter: NewLazyContentWhitelistRepository(provider),769 Filter: NewLazyContentWhitelistRepository(provider),
770+ Permission: NewLazyPermissionRepository(provider),
712 }771 }
713 }772 }
714 773
M internal/ui/app.go +8 -0
@@ -24,6 +24,7 @@ import (
2424 "github.com/bnema/dumber/internal/ui/adapter"
2525 "github.com/bnema/dumber/internal/ui/component"
2626 "github.com/bnema/dumber/internal/ui/coordinator"
27+ "github.com/bnema/dumber/internal/ui/dialog"
2728 "github.com/bnema/dumber/internal/ui/dispatcher"
2829 "github.com/bnema/dumber/internal/ui/focus"
2930 "github.com/bnema/dumber/internal/ui/input"
@@ -348,6 +349,12 @@ func (a *App) createMainWindow(ctx context.Context) error {
348349 }
349350 a.mainWindow = mainWindow
350351
352+ // Create and set the permission dialog presenter
353+ if a.deps != nil && a.deps.PermissionUC != nil {
354+ permDialog := dialog.NewPermissionDialog(a.mainWindow.Window())
355+ a.deps.PermissionUC.SetDialogPresenter(permDialog)
356+ }
357+
351358 // Apply GTK CSS styling from theme manager.
352359 if a.deps == nil || a.deps.Theme == nil {
353360 return nil
@@ -1279,6 +1286,7 @@ func (a *App) initCoordinators(ctx context.Context) {
12791286 a.faviconAdapter,
12801287 getActiveWS,
12811288 a.deps.ZoomUC,
1289+ a.deps.PermissionUC,
12821290 )
12831291
12841292 // Set idle inhibitor for fullscreen video playback
@@ -24,6 +24,7 @@ import (
24 "github.com/bnema/dumber/internal/ui/adapter"24 "github.com/bnema/dumber/internal/ui/adapter"
25 "github.com/bnema/dumber/internal/ui/component"25 "github.com/bnema/dumber/internal/ui/component"
26 "github.com/bnema/dumber/internal/ui/coordinator"26 "github.com/bnema/dumber/internal/ui/coordinator"
27+ "github.com/bnema/dumber/internal/ui/dialog"
27 "github.com/bnema/dumber/internal/ui/dispatcher"28 "github.com/bnema/dumber/internal/ui/dispatcher"
28 "github.com/bnema/dumber/internal/ui/focus"29 "github.com/bnema/dumber/internal/ui/focus"
29 "github.com/bnema/dumber/internal/ui/input"30 "github.com/bnema/dumber/internal/ui/input"
@@ -348,6 +349,12 @@ func (a *App) createMainWindow(ctx context.Context) error {
348 }349 }
349 a.mainWindow = mainWindow350 a.mainWindow = mainWindow
350 351
352+ // Create and set the permission dialog presenter
353+ if a.deps != nil && a.deps.PermissionUC != nil {
354+ permDialog := dialog.NewPermissionDialog(a.mainWindow.Window())
355+ a.deps.PermissionUC.SetDialogPresenter(permDialog)
356+ }
357+
351 // Apply GTK CSS styling from theme manager.358 // Apply GTK CSS styling from theme manager.
352 if a.deps == nil || a.deps.Theme == nil {359 if a.deps == nil || a.deps.Theme == nil {
353 return nil360 return nil
@@ -1279,6 +1286,7 @@ func (a *App) initCoordinators(ctx context.Context) {
1279 a.faviconAdapter,1286 a.faviconAdapter,
1280 getActiveWS,1287 getActiveWS,
1281 a.deps.ZoomUC,1288 a.deps.ZoomUC,
1289+ a.deps.PermissionUC,
1282 )1290 )
1283 1291
1284 // Set idle inhibitor for fullscreen video playback1292 // Set idle inhibitor for fullscreen video playback
M internal/ui/deps.go +13 -11
@@ -44,19 +44,21 @@ type Dependencies struct {
4444 MessageRouter *webkit.MessageRouter
4545
4646 // Repositories
47- HistoryRepo repository.HistoryRepository
48- FavoriteRepo repository.FavoriteRepository
49- ZoomRepo repository.ZoomRepository
50- FilterRepo repository.ContentWhitelistRepository
47+ HistoryRepo repository.HistoryRepository
48+ FavoriteRepo repository.FavoriteRepository
49+ ZoomRepo repository.ZoomRepository
50+ PermissionRepo repository.PermissionRepository
51+ FilterRepo repository.ContentWhitelistRepository
5152
5253 // Use Cases
53- TabsUC *usecase.ManageTabsUseCase
54- PanesUC *usecase.ManagePanesUseCase
55- NavigateUC *usecase.NavigateUseCase
56- ZoomUC *usecase.ManageZoomUseCase
57- FavoritesUC *usecase.ManageFavoritesUseCase
58- HistoryUC *usecase.SearchHistoryUseCase
59- CopyURLUC *usecase.CopyURLUseCase
54+ TabsUC *usecase.ManageTabsUseCase
55+ PanesUC *usecase.ManagePanesUseCase
56+ NavigateUC *usecase.NavigateUseCase
57+ ZoomUC *usecase.ManageZoomUseCase
58+ PermissionUC *usecase.HandlePermissionUseCase
59+ FavoritesUC *usecase.ManageFavoritesUseCase
60+ HistoryUC *usecase.SearchHistoryUseCase
61+ CopyURLUC *usecase.CopyURLUseCase
6062
6163 // Infrastructure Adapters
6264 Clipboard port.Clipboard
@@ -44,19 +44,21 @@ type Dependencies struct {
44 MessageRouter *webkit.MessageRouter44 MessageRouter *webkit.MessageRouter
45 45
46 // Repositories46 // Repositories
47- HistoryRepo repository.HistoryRepository47+ HistoryRepo repository.HistoryRepository
48- FavoriteRepo repository.FavoriteRepository48+ FavoriteRepo repository.FavoriteRepository
49- ZoomRepo repository.ZoomRepository49+ ZoomRepo repository.ZoomRepository
50- FilterRepo repository.ContentWhitelistRepository50+ PermissionRepo repository.PermissionRepository
51+ FilterRepo repository.ContentWhitelistRepository
51 52
52 // Use Cases53 // Use Cases
53- TabsUC *usecase.ManageTabsUseCase54+ TabsUC *usecase.ManageTabsUseCase
54- PanesUC *usecase.ManagePanesUseCase55+ PanesUC *usecase.ManagePanesUseCase
55- NavigateUC *usecase.NavigateUseCase56+ NavigateUC *usecase.NavigateUseCase
56- ZoomUC *usecase.ManageZoomUseCase57+ ZoomUC *usecase.ManageZoomUseCase
57- FavoritesUC *usecase.ManageFavoritesUseCase58+ PermissionUC *usecase.HandlePermissionUseCase
58- HistoryUC *usecase.SearchHistoryUseCase59+ FavoritesUC *usecase.ManageFavoritesUseCase
59- CopyURLUC *usecase.CopyURLUseCase60+ HistoryUC *usecase.SearchHistoryUseCase
61+ CopyURLUC *usecase.CopyURLUseCase
60 62
61 // Infrastructure Adapters63 // Infrastructure Adapters
62 Clipboard port.Clipboard64 Clipboard port.Clipboard

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