v0.27.0 dcb4c51

permission.go RawHistoryFolder

1// Package dialog provides UI dialog implementations for the application layer.
2package dialog
3
4import (
5	"context"
6	"fmt"
7	"sync"
8
9	"github.com/bnema/dumber/internal/application/port"
10	"github.com/bnema/dumber/internal/domain/entity"
11	"github.com/bnema/dumber/internal/logging"
12	"github.com/bnema/dumber/internal/ui/component"
13)
14
15type permissionPopup interface {
16	Show(ctx context.Context, heading, body string, callback func(allowed, persistent bool))
17}
18
19type permissionDialogRequest struct {
20	ctx       context.Context
21	origin    string
22	permTypes []entity.PermissionType
23	callback  func(result port.PermissionDialogResult)
24}
25
26// PermissionDialog implements the port.PermissionDialogPresenter interface.
27// It uses a custom PermissionPopup overlay to sidestep the purego ConnectResponse bug
28// and match the app's custom UI style.
29type PermissionDialog struct {
30	popup permissionPopup
31
32	mu     sync.Mutex
33	active bool
34	queue  []permissionDialogRequest
35}
36
37// NewPermissionDialog creates a new permission dialog presenter.
38// The popup is created once and reused for each permission request.
39func NewPermissionDialog(popup *component.PermissionPopup) *PermissionDialog {
40	return &PermissionDialog{
41		popup: popup,
42	}
43}
44
45// ShowPermissionDialog displays a permission request dialog to the user.
46func (d *PermissionDialog) ShowPermissionDialog(
47	ctx context.Context,
48	origin string,
49	permTypes []entity.PermissionType,
50	callback func(result port.PermissionDialogResult),
51) {
52	req := permissionDialogRequest{
53		ctx:       ctx,
54		origin:    origin,
55		permTypes: permTypes,
56		callback:  callback,
57	}
58
59	if !d.enqueueOrStart(req) {
60		return
61	}
62
63	d.showRequest(req)
64}
65
66func (d *PermissionDialog) enqueueOrStart(req permissionDialogRequest) bool {
67	d.mu.Lock()
68	defer d.mu.Unlock()
69
70	if d.active {
71		d.queue = append(d.queue, req)
72		return false
73	}
74
75	d.active = true
76	return true
77}
78
79func (d *PermissionDialog) showRequest(req permissionDialogRequest) {
80	ctx := req.ctx
81	origin := req.origin
82	permTypes := req.permTypes
83	callback := req.callback
84
85	log := logging.FromContext(ctx)
86
87	if d.popup == nil {
88		log.Error().Msg("permission popup not available")
89		callback(port.PermissionDialogResult{Allowed: false, Persistent: false})
90		d.showNextQueuedRequest()
91		return
92	}
93
94	// Build dialog text
95	heading := d.buildHeading(permTypes)
96	body := d.buildBody(origin, permTypes)
97
98	d.popup.Show(ctx, heading, body, func(allowed, persistent bool) {
99		log.Debug().
100			Bool("allowed", allowed).
101			Bool("persistent", persistent).
102			Msg("permission popup response")
103		callback(port.PermissionDialogResult{Allowed: allowed, Persistent: persistent})
104		d.showNextQueuedRequest()
105	})
106
107	log.Debug().
108		Str("origin", origin).
109		Strs("types", entity.PermissionTypesToStrings(permTypes)).
110		Msg("showing permission popup")
111}
112
113func (d *PermissionDialog) showNextQueuedRequest() {
114	d.mu.Lock()
115	if len(d.queue) == 0 {
116		d.active = false
117		d.mu.Unlock()
118		return
119	}
120
121	next := d.queue[0]
122	d.queue = d.queue[1:]
123	d.mu.Unlock()
124
125	d.showRequest(next)
126}
127
128// buildHeading creates the dialog heading based on permission types.
129func (d *PermissionDialog) buildHeading(permTypes []entity.PermissionType) string {
130	hasMic := false
131	hasCam := false
132	hasDisplay := false
133
134	for _, pt := range permTypes {
135		switch pt {
136		case entity.PermissionTypeMicrophone:
137			hasMic = true
138		case entity.PermissionTypeCamera:
139			hasCam = true
140		case entity.PermissionTypeDisplay:
141			hasDisplay = true
142		}
143	}
144
145	switch {
146	case hasMic && hasCam && hasDisplay:
147		return "Allow Microphone, Camera, and Screen Sharing?"
148	case hasMic && hasDisplay:
149		return "Allow Microphone and Screen Sharing?"
150	case hasCam && hasDisplay:
151		return "Allow Camera and Screen Sharing?"
152	case hasMic && hasCam:
153		return "Allow Microphone and Camera?"
154	case hasMic:
155		return "Allow Microphone Access?"
156	case hasCam:
157		return "Allow Camera Access?"
158	case hasDisplay:
159		return "Allow Screen Sharing?"
160	default:
161		return "Allow Permission?"
162	}
163}
164
165// buildBody creates the dialog body text.
166func (d *PermissionDialog) buildBody(origin string, permTypes []entity.PermissionType) string {
167	hasMic := false
168	hasCam := false
169	hasDisplay := false
170
171	for _, pt := range permTypes {
172		switch pt {
173		case entity.PermissionTypeMicrophone:
174			hasMic = true
175		case entity.PermissionTypeCamera:
176			hasCam = true
177		case entity.PermissionTypeDisplay:
178			hasDisplay = true
179		}
180	}
181
182	var action string
183	switch {
184	case hasMic && hasCam && hasDisplay:
185		action = "access your microphone and camera, and share your screen"
186	case hasMic && hasDisplay:
187		action = "access your microphone and share your screen"
188	case hasCam && hasDisplay:
189		action = "access your camera and share your screen"
190	case hasMic && hasCam:
191		action = "access your microphone and camera"
192	case hasMic:
193		action = "access your microphone"
194	case hasCam:
195		action = "access your camera"
196	case hasDisplay:
197		action = "share your screen"
198	default:
199		action = "access your device"
200	}
201
202	return fmt.Sprintf("%s wants to %s.", origin, action)
203}
204
205// Ensure PermissionDialog implements the interface.
206var _ port.PermissionDialogPresenter = (*PermissionDialog)(nil)

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