1package permissions
2
3import (
4 "fmt"
5 "strings"
6
7 "github.com/charmbracelet/bubbles/v2/help"
8 "github.com/charmbracelet/bubbles/v2/key"
9 "github.com/charmbracelet/bubbles/v2/viewport"
10 tea "github.com/charmbracelet/bubbletea/v2"
11 "github.com/charmbracelet/crush/internal/fsext"
12 "github.com/charmbracelet/crush/internal/llm/tools"
13 "github.com/charmbracelet/crush/internal/permission"
14 "github.com/charmbracelet/crush/internal/tui/components/core"
15 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
16 "github.com/charmbracelet/crush/internal/tui/styles"
17 "github.com/charmbracelet/crush/internal/tui/util"
18 "github.com/charmbracelet/lipgloss/v2"
19 "github.com/charmbracelet/x/ansi"
20)
21
22type PermissionAction string
23
24// Permission responses
25const (
26 PermissionAllow PermissionAction = "allow"
27 PermissionAllowForSession PermissionAction = "allow_session"
28 PermissionDeny PermissionAction = "deny"
29
30 PermissionsDialogID dialogs.DialogID = "permissions"
31)
32
33// PermissionResponseMsg represents the user's response to a permission request
34type PermissionResponseMsg struct {
35 Permission permission.PermissionRequest
36 Action PermissionAction
37}
38
39// PermissionDialogCmp interface for permission dialog component
40type PermissionDialogCmp interface {
41 dialogs.DialogModel
42}
43
44// permissionDialogCmp is the implementation of PermissionDialog
45type permissionDialogCmp struct {
46 wWidth int
47 wHeight int
48 width int
49 height int
50 permission permission.PermissionRequest
51 contentViewPort viewport.Model
52 selectedOption int // 0: Allow, 1: Allow for session, 2: Deny
53
54 // Diff view state
55 defaultDiffSplitMode bool // true for split, false for unified
56 diffSplitMode *bool // nil means use defaultDiffSplitMode
57 diffXOffset int // horizontal scroll offset
58 diffYOffset int // vertical scroll offset
59
60 // Caching
61 cachedContent string
62 contentDirty bool
63
64 positionRow int // Row position for dialog
65 positionCol int // Column position for dialog
66
67 keyMap KeyMap
68}
69
70func NewPermissionDialogCmp(permission permission.PermissionRequest) PermissionDialogCmp {
71 // Create viewport for content
72 contentViewport := viewport.New()
73 return &permissionDialogCmp{
74 contentViewPort: contentViewport,
75 selectedOption: 0, // Default to "Allow"
76 permission: permission,
77 keyMap: DefaultKeyMap(),
78 contentDirty: true, // Mark as dirty initially
79 }
80}
81
82func (p *permissionDialogCmp) Init() tea.Cmd {
83 return p.contentViewPort.Init()
84}
85
86func (p *permissionDialogCmp) supportsDiffView() bool {
87 return p.permission.ToolName == tools.EditToolName || p.permission.ToolName == tools.WriteToolName || p.permission.ToolName == tools.MultiEditToolName
88}
89
90func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
91 var cmds []tea.Cmd
92
93 switch msg := msg.(type) {
94 case tea.WindowSizeMsg:
95 p.wWidth = msg.Width
96 p.wHeight = msg.Height
97 p.contentDirty = true // Mark content as dirty on window resize
98 cmd := p.SetSize()
99 cmds = append(cmds, cmd)
100 case tea.KeyPressMsg:
101 switch {
102 case key.Matches(msg, p.keyMap.Right) || key.Matches(msg, p.keyMap.Tab):
103 p.selectedOption = (p.selectedOption + 1) % 3
104 return p, nil
105 case key.Matches(msg, p.keyMap.Left):
106 p.selectedOption = (p.selectedOption + 2) % 3
107 case key.Matches(msg, p.keyMap.Select):
108 return p, p.selectCurrentOption()
109 case key.Matches(msg, p.keyMap.Allow):
110 return p, tea.Batch(
111 util.CmdHandler(dialogs.CloseDialogMsg{}),
112 util.CmdHandler(PermissionResponseMsg{Action: PermissionAllow, Permission: p.permission}),
113 )
114 case key.Matches(msg, p.keyMap.AllowSession):
115 return p, tea.Batch(
116 util.CmdHandler(dialogs.CloseDialogMsg{}),
117 util.CmdHandler(PermissionResponseMsg{Action: PermissionAllowForSession, Permission: p.permission}),
118 )
119 case key.Matches(msg, p.keyMap.Deny):
120 return p, tea.Batch(
121 util.CmdHandler(dialogs.CloseDialogMsg{}),
122 util.CmdHandler(PermissionResponseMsg{Action: PermissionDeny, Permission: p.permission}),
123 )
124 case key.Matches(msg, p.keyMap.ToggleDiffMode):
125 if p.supportsDiffView() {
126 if p.diffSplitMode == nil {
127 diffSplitMode := !p.defaultDiffSplitMode
128 p.diffSplitMode = &diffSplitMode
129 } else {
130 *p.diffSplitMode = !*p.diffSplitMode
131 }
132 p.contentDirty = true // Mark content as dirty when diff mode changes
133 return p, nil
134 }
135 case key.Matches(msg, p.keyMap.ScrollDown):
136 if p.supportsDiffView() {
137 p.diffYOffset += 1
138 p.contentDirty = true // Mark content as dirty when scrolling
139 return p, nil
140 }
141 case key.Matches(msg, p.keyMap.ScrollUp):
142 if p.supportsDiffView() {
143 p.diffYOffset = max(0, p.diffYOffset-1)
144 p.contentDirty = true // Mark content as dirty when scrolling
145 return p, nil
146 }
147 case key.Matches(msg, p.keyMap.ScrollLeft):
148 if p.supportsDiffView() {
149 p.diffXOffset = max(0, p.diffXOffset-5)
150 p.contentDirty = true // Mark content as dirty when scrolling
151 return p, nil
152 }
153 case key.Matches(msg, p.keyMap.ScrollRight):
154 if p.supportsDiffView() {
155 p.diffXOffset += 5
156 p.contentDirty = true // Mark content as dirty when scrolling
157 return p, nil
158 }
159 default:
160 // Pass other keys to viewport
161 viewPort, cmd := p.contentViewPort.Update(msg)
162 p.contentViewPort = viewPort
163 cmds = append(cmds, cmd)
164 }
165 }
166
167 return p, tea.Batch(cmds...)
168}
169
170func (p *permissionDialogCmp) selectCurrentOption() tea.Cmd {
171 var action PermissionAction
172
173 switch p.selectedOption {
174 case 0:
175 action = PermissionAllow
176 case 1:
177 action = PermissionAllowForSession
178 case 2:
179 action = PermissionDeny
180 }
181
182 return tea.Batch(
183 util.CmdHandler(PermissionResponseMsg{Action: action, Permission: p.permission}),
184 util.CmdHandler(dialogs.CloseDialogMsg{}),
185 )
186}
187
188func (p *permissionDialogCmp) renderButtons() string {
189 t := styles.CurrentTheme()
190 baseStyle := t.S().Base
191
192 buttons := []core.ButtonOpts{
193 {
194 Text: "Allow",
195 UnderlineIndex: 0, // "A"
196 Selected: p.selectedOption == 0,
197 },
198 {
199 Text: "Allow for Session",
200 UnderlineIndex: 10, // "S" in "Session"
201 Selected: p.selectedOption == 1,
202 },
203 {
204 Text: "Deny",
205 UnderlineIndex: 0, // "D"
206 Selected: p.selectedOption == 2,
207 },
208 }
209
210 content := core.SelectableButtons(buttons, " ")
211 if lipgloss.Width(content) > p.width-4 {
212 content = core.SelectableButtonsVertical(buttons, 1)
213 return baseStyle.AlignVertical(lipgloss.Center).
214 AlignHorizontal(lipgloss.Center).
215 Width(p.width - 4).
216 Render(content)
217 }
218
219 return baseStyle.AlignHorizontal(lipgloss.Right).Width(p.width - 4).Render(content)
220}
221
222func (p *permissionDialogCmp) renderHeader() string {
223 t := styles.CurrentTheme()
224 baseStyle := t.S().Base
225
226 toolKey := t.S().Muted.Render("Tool")
227 toolValue := t.S().Text.
228 Width(p.width - lipgloss.Width(toolKey)).
229 Render(fmt.Sprintf(" %s", p.permission.ToolName))
230
231 pathKey := t.S().Muted.Render("Path")
232 pathValue := t.S().Text.
233 Width(p.width - lipgloss.Width(pathKey)).
234 Render(fmt.Sprintf(" %s", fsext.PrettyPath(p.permission.Path)))
235
236 headerParts := []string{
237 lipgloss.JoinHorizontal(
238 lipgloss.Left,
239 toolKey,
240 toolValue,
241 ),
242 baseStyle.Render(strings.Repeat(" ", p.width)),
243 lipgloss.JoinHorizontal(
244 lipgloss.Left,
245 pathKey,
246 pathValue,
247 ),
248 baseStyle.Render(strings.Repeat(" ", p.width)),
249 }
250
251 // Add tool-specific header information
252 switch p.permission.ToolName {
253 case tools.BashToolName:
254 headerParts = append(headerParts, t.S().Muted.Width(p.width).Render("Command"))
255 case tools.DownloadToolName:
256 params := p.permission.Params.(tools.DownloadPermissionsParams)
257 urlKey := t.S().Muted.Render("URL")
258 urlValue := t.S().Text.
259 Width(p.width - lipgloss.Width(urlKey)).
260 Render(fmt.Sprintf(" %s", params.URL))
261 fileKey := t.S().Muted.Render("File")
262 filePath := t.S().Text.
263 Width(p.width - lipgloss.Width(fileKey)).
264 Render(fmt.Sprintf(" %s", fsext.PrettyPath(params.FilePath)))
265 headerParts = append(headerParts,
266 lipgloss.JoinHorizontal(
267 lipgloss.Left,
268 urlKey,
269 urlValue,
270 ),
271 baseStyle.Render(strings.Repeat(" ", p.width)),
272 lipgloss.JoinHorizontal(
273 lipgloss.Left,
274 fileKey,
275 filePath,
276 ),
277 baseStyle.Render(strings.Repeat(" ", p.width)),
278 )
279 case tools.EditToolName:
280 params := p.permission.Params.(tools.EditPermissionsParams)
281 fileKey := t.S().Muted.Render("File")
282 filePath := t.S().Text.
283 Width(p.width - lipgloss.Width(fileKey)).
284 Render(fmt.Sprintf(" %s", fsext.PrettyPath(params.FilePath)))
285 headerParts = append(headerParts,
286 lipgloss.JoinHorizontal(
287 lipgloss.Left,
288 fileKey,
289 filePath,
290 ),
291 baseStyle.Render(strings.Repeat(" ", p.width)),
292 )
293
294 case tools.WriteToolName:
295 params := p.permission.Params.(tools.WritePermissionsParams)
296 fileKey := t.S().Muted.Render("File")
297 filePath := t.S().Text.
298 Width(p.width - lipgloss.Width(fileKey)).
299 Render(fmt.Sprintf(" %s", fsext.PrettyPath(params.FilePath)))
300 headerParts = append(headerParts,
301 lipgloss.JoinHorizontal(
302 lipgloss.Left,
303 fileKey,
304 filePath,
305 ),
306 baseStyle.Render(strings.Repeat(" ", p.width)),
307 )
308 case tools.MultiEditToolName:
309 params := p.permission.Params.(tools.MultiEditPermissionsParams)
310 fileKey := t.S().Muted.Render("File")
311 filePath := t.S().Text.
312 Width(p.width - lipgloss.Width(fileKey)).
313 Render(fmt.Sprintf(" %s", fsext.PrettyPath(params.FilePath)))
314 headerParts = append(headerParts,
315 lipgloss.JoinHorizontal(
316 lipgloss.Left,
317 fileKey,
318 filePath,
319 ),
320 baseStyle.Render(strings.Repeat(" ", p.width)),
321 )
322 case tools.FetchToolName:
323 headerParts = append(headerParts, t.S().Muted.Width(p.width).Bold(true).Render("URL"))
324 case tools.ViewToolName:
325 params := p.permission.Params.(tools.ViewPermissionsParams)
326 fileKey := t.S().Muted.Render("File")
327 filePath := t.S().Text.
328 Width(p.width - lipgloss.Width(fileKey)).
329 Render(fmt.Sprintf(" %s", fsext.PrettyPath(params.FilePath)))
330 headerParts = append(headerParts,
331 lipgloss.JoinHorizontal(
332 lipgloss.Left,
333 fileKey,
334 filePath,
335 ),
336 baseStyle.Render(strings.Repeat(" ", p.width)),
337 )
338 case tools.LSToolName:
339 params := p.permission.Params.(tools.LSPermissionsParams)
340 pathKey := t.S().Muted.Render("Directory")
341 pathValue := t.S().Text.
342 Width(p.width - lipgloss.Width(pathKey)).
343 Render(fmt.Sprintf(" %s", fsext.PrettyPath(params.Path)))
344 headerParts = append(headerParts,
345 lipgloss.JoinHorizontal(
346 lipgloss.Left,
347 pathKey,
348 pathValue,
349 ),
350 baseStyle.Render(strings.Repeat(" ", p.width)),
351 )
352 }
353
354 return baseStyle.Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
355}
356
357func (p *permissionDialogCmp) getOrGenerateContent() string {
358 // Return cached content if available and not dirty
359 if !p.contentDirty && p.cachedContent != "" {
360 return p.cachedContent
361 }
362
363 // Generate new content
364 var content string
365 switch p.permission.ToolName {
366 case tools.BashToolName:
367 content = p.generateBashContent()
368 case tools.DownloadToolName:
369 content = p.generateDownloadContent()
370 case tools.EditToolName:
371 content = p.generateEditContent()
372 case tools.WriteToolName:
373 content = p.generateWriteContent()
374 case tools.MultiEditToolName:
375 content = p.generateMultiEditContent()
376 case tools.FetchToolName:
377 content = p.generateFetchContent()
378 case tools.ViewToolName:
379 content = p.generateViewContent()
380 case tools.LSToolName:
381 content = p.generateLSContent()
382 default:
383 content = p.generateDefaultContent()
384 }
385
386 // Cache the result
387 p.cachedContent = content
388 p.contentDirty = false
389
390 return content
391}
392
393func (p *permissionDialogCmp) generateBashContent() string {
394 t := styles.CurrentTheme()
395 baseStyle := t.S().Base.Background(t.BgSubtle)
396 if pr, ok := p.permission.Params.(tools.BashPermissionsParams); ok {
397 content := pr.Command
398 t := styles.CurrentTheme()
399 content = strings.TrimSpace(content)
400 lines := strings.Split(content, "\n")
401
402 width := p.width - 4
403 var out []string
404 for _, ln := range lines {
405 out = append(out, t.S().Muted.
406 Width(width).
407 Padding(0, 3).
408 Foreground(t.FgBase).
409 Background(t.BgSubtle).
410 Render(ln))
411 }
412
413 // Use the cache for markdown rendering
414 renderedContent := strings.Join(out, "\n")
415 finalContent := baseStyle.
416 Width(p.contentViewPort.Width()).
417 Padding(1, 0).
418 Render(renderedContent)
419
420 return finalContent
421 }
422 return ""
423}
424
425func (p *permissionDialogCmp) generateEditContent() string {
426 if pr, ok := p.permission.Params.(tools.EditPermissionsParams); ok {
427 formatter := core.DiffFormatter().
428 Before(fsext.PrettyPath(pr.FilePath), pr.OldContent).
429 After(fsext.PrettyPath(pr.FilePath), pr.NewContent).
430 Height(p.contentViewPort.Height()).
431 Width(p.contentViewPort.Width()).
432 XOffset(p.diffXOffset).
433 YOffset(p.diffYOffset)
434 if p.useDiffSplitMode() {
435 formatter = formatter.Split()
436 } else {
437 formatter = formatter.Unified()
438 }
439
440 diff := formatter.String()
441 return diff
442 }
443 return ""
444}
445
446func (p *permissionDialogCmp) generateWriteContent() string {
447 if pr, ok := p.permission.Params.(tools.WritePermissionsParams); ok {
448 // Use the cache for diff rendering
449 formatter := core.DiffFormatter().
450 Before(fsext.PrettyPath(pr.FilePath), pr.OldContent).
451 After(fsext.PrettyPath(pr.FilePath), pr.NewContent).
452 Height(p.contentViewPort.Height()).
453 Width(p.contentViewPort.Width()).
454 XOffset(p.diffXOffset).
455 YOffset(p.diffYOffset)
456 if p.useDiffSplitMode() {
457 formatter = formatter.Split()
458 } else {
459 formatter = formatter.Unified()
460 }
461
462 diff := formatter.String()
463 return diff
464 }
465 return ""
466}
467
468func (p *permissionDialogCmp) generateDownloadContent() string {
469 t := styles.CurrentTheme()
470 baseStyle := t.S().Base.Background(t.BgSubtle)
471 if pr, ok := p.permission.Params.(tools.DownloadPermissionsParams); ok {
472 content := fmt.Sprintf("URL: %s\nFile: %s", pr.URL, fsext.PrettyPath(pr.FilePath))
473 if pr.Timeout > 0 {
474 content += fmt.Sprintf("\nTimeout: %ds", pr.Timeout)
475 }
476
477 finalContent := baseStyle.
478 Padding(1, 2).
479 Width(p.contentViewPort.Width()).
480 Render(content)
481 return finalContent
482 }
483 return ""
484}
485
486func (p *permissionDialogCmp) generateMultiEditContent() string {
487 if pr, ok := p.permission.Params.(tools.MultiEditPermissionsParams); ok {
488 // Use the cache for diff rendering
489 formatter := core.DiffFormatter().
490 Before(fsext.PrettyPath(pr.FilePath), pr.OldContent).
491 After(fsext.PrettyPath(pr.FilePath), pr.NewContent).
492 Height(p.contentViewPort.Height()).
493 Width(p.contentViewPort.Width()).
494 XOffset(p.diffXOffset).
495 YOffset(p.diffYOffset)
496 if p.useDiffSplitMode() {
497 formatter = formatter.Split()
498 } else {
499 formatter = formatter.Unified()
500 }
501
502 diff := formatter.String()
503 return diff
504 }
505 return ""
506}
507
508func (p *permissionDialogCmp) generateFetchContent() string {
509 t := styles.CurrentTheme()
510 baseStyle := t.S().Base.Background(t.BgSubtle)
511 if pr, ok := p.permission.Params.(tools.FetchPermissionsParams); ok {
512 finalContent := baseStyle.
513 Padding(1, 2).
514 Width(p.contentViewPort.Width()).
515 Render(pr.URL)
516 return finalContent
517 }
518 return ""
519}
520
521func (p *permissionDialogCmp) generateViewContent() string {
522 t := styles.CurrentTheme()
523 baseStyle := t.S().Base.Background(t.BgSubtle)
524 if pr, ok := p.permission.Params.(tools.ViewPermissionsParams); ok {
525 content := fmt.Sprintf("File: %s", fsext.PrettyPath(pr.FilePath))
526 if pr.Offset > 0 {
527 content += fmt.Sprintf("\nStarting from line: %d", pr.Offset+1)
528 }
529 if pr.Limit > 0 && pr.Limit != 2000 { // 2000 is the default limit
530 content += fmt.Sprintf("\nLines to read: %d", pr.Limit)
531 }
532
533 finalContent := baseStyle.
534 Padding(1, 2).
535 Width(p.contentViewPort.Width()).
536 Render(content)
537 return finalContent
538 }
539 return ""
540}
541
542func (p *permissionDialogCmp) generateLSContent() string {
543 t := styles.CurrentTheme()
544 baseStyle := t.S().Base.Background(t.BgSubtle)
545 if pr, ok := p.permission.Params.(tools.LSPermissionsParams); ok {
546 content := fmt.Sprintf("Directory: %s", fsext.PrettyPath(pr.Path))
547 if len(pr.Ignore) > 0 {
548 content += fmt.Sprintf("\nIgnore patterns: %s", strings.Join(pr.Ignore, ", "))
549 }
550
551 finalContent := baseStyle.
552 Padding(1, 2).
553 Width(p.contentViewPort.Width()).
554 Render(content)
555 return finalContent
556 }
557 return ""
558}
559
560func (p *permissionDialogCmp) generateDefaultContent() string {
561 t := styles.CurrentTheme()
562 baseStyle := t.S().Base.Background(t.BgSubtle)
563
564 content := p.permission.Description
565
566 content = strings.TrimSpace(content)
567 content = "\n" + content + "\n"
568 lines := strings.Split(content, "\n")
569
570 width := p.width - 4
571 var out []string
572 for _, ln := range lines {
573 ln = " " + ln // left padding
574 if len(ln) > width {
575 ln = ansi.Truncate(ln, width, "…")
576 }
577 out = append(out, t.S().Muted.
578 Width(width).
579 Foreground(t.FgBase).
580 Background(t.BgSubtle).
581 Render(ln))
582 }
583
584 // Use the cache for markdown rendering
585 renderedContent := strings.Join(out, "\n")
586 finalContent := baseStyle.
587 Width(p.contentViewPort.Width()).
588 Render(renderedContent)
589
590 if renderedContent == "" {
591 return ""
592 }
593
594 return finalContent
595}
596
597func (p *permissionDialogCmp) useDiffSplitMode() bool {
598 if p.diffSplitMode != nil {
599 return *p.diffSplitMode
600 } else {
601 return p.defaultDiffSplitMode
602 }
603}
604
605func (p *permissionDialogCmp) styleViewport() string {
606 t := styles.CurrentTheme()
607 return t.S().Base.Render(p.contentViewPort.View())
608}
609
610func (p *permissionDialogCmp) render() string {
611 t := styles.CurrentTheme()
612 baseStyle := t.S().Base
613 title := core.Title("Permission Required", p.width-4)
614 // Render header
615 headerContent := p.renderHeader()
616 // Render buttons
617 buttons := p.renderButtons()
618
619 p.contentViewPort.SetWidth(p.width - 4)
620
621 // Get cached or generate content
622 contentFinal := p.getOrGenerateContent()
623
624 // Always set viewport content (the caching is handled in getOrGenerateContent)
625 const minContentHeight = 9
626 contentHeight := min(
627 max(minContentHeight, p.height-minContentHeight),
628 lipgloss.Height(contentFinal),
629 )
630 p.contentViewPort.SetHeight(contentHeight)
631 p.contentViewPort.SetContent(contentFinal)
632
633 p.positionRow = p.wHeight / 2
634 p.positionRow -= (contentHeight + 9) / 2
635 p.positionRow -= 3 // Move dialog slightly higher than middle
636
637 var contentHelp string
638 if p.supportsDiffView() {
639 contentHelp = help.New().View(p.keyMap)
640 }
641
642 // Calculate content height dynamically based on window size
643 strs := []string{
644 title,
645 "",
646 headerContent,
647 p.styleViewport(),
648 "",
649 buttons,
650 "",
651 }
652 if contentHelp != "" {
653 strs = append(strs, "", contentHelp)
654 }
655 content := lipgloss.JoinVertical(lipgloss.Top, strs...)
656
657 return baseStyle.
658 Padding(0, 1).
659 Border(lipgloss.RoundedBorder()).
660 BorderForeground(t.BorderFocus).
661 Width(p.width).
662 Render(
663 content,
664 )
665}
666
667func (p *permissionDialogCmp) View() string {
668 return p.render()
669}
670
671func (p *permissionDialogCmp) SetSize() tea.Cmd {
672 if p.permission.ID == "" {
673 return nil
674 }
675
676 oldWidth, oldHeight := p.width, p.height
677
678 switch p.permission.ToolName {
679 case tools.BashToolName:
680 p.width = int(float64(p.wWidth) * 0.8)
681 p.height = int(float64(p.wHeight) * 0.3)
682 case tools.DownloadToolName:
683 p.width = int(float64(p.wWidth) * 0.8)
684 p.height = int(float64(p.wHeight) * 0.4)
685 case tools.EditToolName:
686 p.width = int(float64(p.wWidth) * 0.8)
687 p.height = int(float64(p.wHeight) * 0.8)
688 case tools.WriteToolName:
689 p.width = int(float64(p.wWidth) * 0.8)
690 p.height = int(float64(p.wHeight) * 0.8)
691 case tools.MultiEditToolName:
692 p.width = int(float64(p.wWidth) * 0.8)
693 p.height = int(float64(p.wHeight) * 0.8)
694 case tools.FetchToolName:
695 p.width = int(float64(p.wWidth) * 0.8)
696 p.height = int(float64(p.wHeight) * 0.3)
697 case tools.ViewToolName:
698 p.width = int(float64(p.wWidth) * 0.8)
699 p.height = int(float64(p.wHeight) * 0.4)
700 case tools.LSToolName:
701 p.width = int(float64(p.wWidth) * 0.8)
702 p.height = int(float64(p.wHeight) * 0.4)
703 default:
704 p.width = int(float64(p.wWidth) * 0.7)
705 p.height = int(float64(p.wHeight) * 0.5)
706 }
707
708 // Default to diff split mode when dialog is wide enough.
709 p.defaultDiffSplitMode = p.width >= 140
710
711 // Set a maximum width for the dialog
712 p.width = min(p.width, 180)
713
714 // Mark content as dirty if size changed
715 if oldWidth != p.width || oldHeight != p.height {
716 p.contentDirty = true
717 }
718 p.positionRow = p.wHeight / 2
719 p.positionRow -= p.height / 2
720 p.positionRow -= 3 // Move dialog slightly higher than middle
721 p.positionCol = p.wWidth / 2
722 p.positionCol -= p.width / 2
723 return nil
724}
725
726func (c *permissionDialogCmp) GetOrSetMarkdown(key string, generator func() (string, error)) string {
727 content, err := generator()
728 if err != nil {
729 return fmt.Sprintf("Error rendering markdown: %v", err)
730 }
731
732 return content
733}
734
735// ID implements PermissionDialogCmp.
736func (p *permissionDialogCmp) ID() dialogs.DialogID {
737 return PermissionsDialogID
738}
739
740// Position implements PermissionDialogCmp.
741func (p *permissionDialogCmp) Position() (int, int) {
742 return p.positionRow, p.positionCol
743}