summaryrefslogtreecommitdiffhomepage
path: root/internal/tui/components/dialog
diff options
context:
space:
mode:
authorKujtim Hoxha <[email protected]>2025-04-16 20:06:23 +0200
committerKujtim Hoxha <[email protected]>2025-04-21 13:42:00 +0200
commitbbfa60c787f2ec459f1689b9a650ddbec9693ed9 (patch)
treef7f2aa31c460c8cc22ec40cc299c386277152241 /internal/tui/components/dialog
parent76b4065f17b87a63092acfd98c997bab53700b35 (diff)
downloadopencode-bbfa60c787f2ec459f1689b9a650ddbec9693ed9.tar.gz
opencode-bbfa60c787f2ec459f1689b9a650ddbec9693ed9.zip
reimplement agent,provider and add file history
Diffstat (limited to 'internal/tui/components/dialog')
-rw-r--r--internal/tui/components/dialog/help.go182
-rw-r--r--internal/tui/components/dialog/permission.go682
-rw-r--r--internal/tui/components/dialog/quit.go156
3 files changed, 605 insertions, 415 deletions
diff --git a/internal/tui/components/dialog/help.go b/internal/tui/components/dialog/help.go
new file mode 100644
index 000000000..1d3c2b077
--- /dev/null
+++ b/internal/tui/components/dialog/help.go
@@ -0,0 +1,182 @@
+package dialog
+
+import (
+ "strings"
+
+ "github.com/charmbracelet/bubbles/key"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/kujtimiihoxha/termai/internal/tui/styles"
+)
+
+type helpCmp struct {
+ width int
+ height int
+ keys []key.Binding
+}
+
+func (h *helpCmp) Init() tea.Cmd {
+ return nil
+}
+
+func (h *helpCmp) SetBindings(k []key.Binding) {
+ h.keys = k
+}
+
+func (h *helpCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ switch msg := msg.(type) {
+ case tea.WindowSizeMsg:
+ h.width = 80
+ h.height = msg.Height
+ }
+ return h, nil
+}
+
+func removeDuplicateBindings(bindings []key.Binding) []key.Binding {
+ seen := make(map[string]struct{})
+ result := make([]key.Binding, 0, len(bindings))
+
+ // Process bindings in reverse order
+ for i := len(bindings) - 1; i >= 0; i-- {
+ b := bindings[i]
+ k := strings.Join(b.Keys(), " ")
+ if _, ok := seen[k]; ok {
+ // duplicate, skip
+ continue
+ }
+ seen[k] = struct{}{}
+ // Add to the beginning of result to maintain original order
+ result = append([]key.Binding{b}, result...)
+ }
+
+ return result
+}
+
+func (h *helpCmp) render() string {
+ helpKeyStyle := styles.Bold.Background(styles.Background).Foreground(styles.Forground).Padding(0, 1, 0, 0)
+ helpDescStyle := styles.Regular.Background(styles.Background).Foreground(styles.ForgroundMid)
+ // Compile list of bindings to render
+ bindings := removeDuplicateBindings(h.keys)
+ // Enumerate through each group of bindings, populating a series of
+ // pairs of columns, one for keys, one for descriptions
+ var (
+ pairs []string
+ width int
+ rows = 12 - 2
+ )
+ for i := 0; i < len(bindings); i += rows {
+ var (
+ keys []string
+ descs []string
+ )
+ for j := i; j < min(i+rows, len(bindings)); j++ {
+ keys = append(keys, helpKeyStyle.Render(bindings[j].Help().Key))
+ descs = append(descs, helpDescStyle.Render(bindings[j].Help().Desc))
+ }
+ // Render pair of columns; beyond the first pair, render a three space
+ // left margin, in order to visually separate the pairs.
+ var cols []string
+ if len(pairs) > 0 {
+ cols = []string{styles.BaseStyle.Render(" ")}
+ }
+
+ maxDescWidth := 0
+ for _, desc := range descs {
+ if maxDescWidth < lipgloss.Width(desc) {
+ maxDescWidth = lipgloss.Width(desc)
+ }
+ }
+ for i := range descs {
+ remainingWidth := maxDescWidth - lipgloss.Width(descs[i])
+ if remainingWidth > 0 {
+ descs[i] = descs[i] + styles.BaseStyle.Render(strings.Repeat(" ", remainingWidth))
+ }
+ }
+ maxKeyWidth := 0
+ for _, key := range keys {
+ if maxKeyWidth < lipgloss.Width(key) {
+ maxKeyWidth = lipgloss.Width(key)
+ }
+ }
+ for i := range keys {
+ remainingWidth := maxKeyWidth - lipgloss.Width(keys[i])
+ if remainingWidth > 0 {
+ keys[i] = keys[i] + styles.BaseStyle.Render(strings.Repeat(" ", remainingWidth))
+ }
+ }
+
+ cols = append(cols,
+ strings.Join(keys, "\n"),
+ strings.Join(descs, "\n"),
+ )
+
+ pair := styles.BaseStyle.Render(lipgloss.JoinHorizontal(lipgloss.Top, cols...))
+ // check whether it exceeds the maximum width avail (the width of the
+ // terminal, subtracting 2 for the borders).
+ width += lipgloss.Width(pair)
+ if width > h.width-2 {
+ break
+ }
+ pairs = append(pairs, pair)
+ }
+
+ // https://github.com/charmbracelet/lipgloss/issues/209
+ if len(pairs) > 1 {
+ prefix := pairs[:len(pairs)-1]
+ lastPair := pairs[len(pairs)-1]
+ prefix = append(prefix, lipgloss.Place(
+ lipgloss.Width(lastPair), // width
+ lipgloss.Height(prefix[0]), // height
+ lipgloss.Left, // x
+ lipgloss.Top, // y
+ lastPair, // content
+ lipgloss.WithWhitespaceBackground(styles.Background), // background
+ ))
+ content := styles.BaseStyle.Width(h.width).Render(
+ lipgloss.JoinHorizontal(
+ lipgloss.Top,
+ prefix...,
+ ),
+ )
+ return content
+ }
+ // Join pairs of columns and enclose in a border
+ content := styles.BaseStyle.Width(h.width).Render(
+ lipgloss.JoinHorizontal(
+ lipgloss.Top,
+ pairs...,
+ ),
+ )
+ return content
+}
+
+func (h *helpCmp) View() string {
+ content := h.render()
+ header := styles.BaseStyle.
+ Bold(true).
+ Width(lipgloss.Width(content)).
+ Foreground(styles.PrimaryColor).
+ Render("Keyboard Shortcuts")
+
+ return styles.BaseStyle.Padding(1).
+ Border(lipgloss.RoundedBorder()).
+ BorderForeground(styles.ForgroundDim).
+ Width(h.width).
+ BorderBackground(styles.Background).
+ Render(
+ lipgloss.JoinVertical(lipgloss.Center,
+ header,
+ styles.BaseStyle.Render(strings.Repeat(" ", lipgloss.Width(header))),
+ content,
+ ),
+ )
+}
+
+type HelpCmp interface {
+ tea.Model
+ SetBindings([]key.Binding)
+}
+
+func NewHelpCmp() HelpCmp {
+ return &helpCmp{}
+}
diff --git a/internal/tui/components/dialog/permission.go b/internal/tui/components/dialog/permission.go
index d147f89cd..9c55effde 100644
--- a/internal/tui/components/dialog/permission.go
+++ b/internal/tui/components/dialog/permission.go
@@ -12,12 +12,9 @@ import (
"github.com/kujtimiihoxha/termai/internal/diff"
"github.com/kujtimiihoxha/termai/internal/llm/tools"
"github.com/kujtimiihoxha/termai/internal/permission"
- "github.com/kujtimiihoxha/termai/internal/tui/components/core"
"github.com/kujtimiihoxha/termai/internal/tui/layout"
"github.com/kujtimiihoxha/termai/internal/tui/styles"
"github.com/kujtimiihoxha/termai/internal/tui/util"
-
- "github.com/charmbracelet/huh"
)
type PermissionAction string
@@ -35,69 +32,64 @@ type PermissionResponseMsg struct {
Action PermissionAction
}
-// PermissionDialog interface for permission dialog component
-type PermissionDialog interface {
+// PermissionDialogCmp interface for permission dialog component
+type PermissionDialogCmp interface {
tea.Model
- layout.Sizeable
layout.Bindings
+ SetPermissions(permission permission.PermissionRequest)
}
-type keyMap struct {
- ChangeFocus key.Binding
+type permissionsMapping struct {
+ LeftRight key.Binding
+ EnterSpace key.Binding
+ Allow key.Binding
+ AllowSession key.Binding
+ Deny key.Binding
+ Tab key.Binding
}
-var keyMapValue = keyMap{
- ChangeFocus: key.NewBinding(
+var permissionsKeys = permissionsMapping{
+ LeftRight: key.NewBinding(
+ key.WithKeys("left", "right"),
+ key.WithHelp("←/→", "switch options"),
+ ),
+ EnterSpace: key.NewBinding(
+ key.WithKeys("enter", " "),
+ key.WithHelp("enter/space", "confirm"),
+ ),
+ Allow: key.NewBinding(
+ key.WithKeys("a"),
+ key.WithHelp("a", "allow"),
+ ),
+ AllowSession: key.NewBinding(
+ key.WithKeys("A"),
+ key.WithHelp("A", "allow for session"),
+ ),
+ Deny: key.NewBinding(
+ key.WithKeys("d"),
+ key.WithHelp("d", "deny"),
+ ),
+ Tab: key.NewBinding(
key.WithKeys("tab"),
- key.WithHelp("tab", "change focus"),
+ key.WithHelp("tab", "switch options"),
),
}
// permissionDialogCmp is the implementation of PermissionDialog
type permissionDialogCmp struct {
- form *huh.Form
width int
height int
permission permission.PermissionRequest
windowSize tea.WindowSizeMsg
- r *glamour.TermRenderer
contentViewPort viewport.Model
- isViewportFocus bool
- selectOption *huh.Select[string]
-}
+ selectedOption int // 0: Allow, 1: Allow for session, 2: Deny
-// formatDiff formats a diff string with colors for additions and deletions
-func formatDiff(diffText string) string {
- lines := strings.Split(diffText, "\n")
- var formattedLines []string
-
- // Define styles for different line types
- addStyle := lipgloss.NewStyle().Foreground(styles.Green)
- removeStyle := lipgloss.NewStyle().Foreground(styles.Red)
- headerStyle := lipgloss.NewStyle().Bold(true).Foreground(styles.Blue)
- contextStyle := lipgloss.NewStyle().Foreground(styles.SubText0)
-
- // Process each line
- for _, line := range lines {
- if strings.HasPrefix(line, "+") {
- formattedLines = append(formattedLines, addStyle.Render(line))
- } else if strings.HasPrefix(line, "-") {
- formattedLines = append(formattedLines, removeStyle.Render(line))
- } else if strings.HasPrefix(line, "Changes:") || strings.HasPrefix(line, " ...") {
- formattedLines = append(formattedLines, headerStyle.Render(line))
- } else if strings.HasPrefix(line, " ") {
- formattedLines = append(formattedLines, contextStyle.Render(line))
- } else {
- formattedLines = append(formattedLines, line)
- }
- }
-
- // Join all formatted lines
- return strings.Join(formattedLines, "\n")
+ diffCache map[string]string
+ markdownCache map[string]string
}
func (p *permissionDialogCmp) Init() tea.Cmd {
- return nil
+ return p.contentViewPort.Init()
}
func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -106,373 +98,363 @@ func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
p.windowSize = msg
+ p.SetSize()
+ p.markdownCache = make(map[string]string)
+ p.diffCache = make(map[string]string)
case tea.KeyMsg:
- if key.Matches(msg, keyMapValue.ChangeFocus) {
- p.isViewportFocus = !p.isViewportFocus
- if p.isViewportFocus {
- p.selectOption.Blur()
- // Add a visual indicator for focus change
- cmds = append(cmds, tea.Batch(
- util.ReportInfo("Viewing content - use arrow keys to scroll"),
- ))
- } else {
- p.selectOption.Focus()
- // Add a visual indicator for focus change
- cmds = append(cmds, tea.Batch(
- util.CmdHandler(util.ReportInfo("Select an action")),
- ))
- }
- return p, tea.Batch(cmds...)
- }
- }
-
- if p.isViewportFocus {
- viewPort, cmd := p.contentViewPort.Update(msg)
- p.contentViewPort = viewPort
- cmds = append(cmds, cmd)
- } else {
- form, cmd := p.form.Update(msg)
- if f, ok := form.(*huh.Form); ok {
- p.form = f
+ switch {
+ case key.Matches(msg, permissionsKeys.LeftRight) || key.Matches(msg, permissionsKeys.Tab):
+ // Change selected option
+ p.selectedOption = (p.selectedOption + 1) % 3
+ return p, nil
+ case key.Matches(msg, permissionsKeys.EnterSpace):
+ // Select current option
+ return p, p.selectCurrentOption()
+ case key.Matches(msg, permissionsKeys.Allow):
+ // Select Allow
+ return p, util.CmdHandler(PermissionResponseMsg{Action: PermissionAllow, Permission: p.permission})
+ case key.Matches(msg, permissionsKeys.AllowSession):
+ // Select Allow for session
+ return p, util.CmdHandler(PermissionResponseMsg{Action: PermissionAllowForSession, Permission: p.permission})
+ case key.Matches(msg, permissionsKeys.Deny):
+ // Select Deny
+ return p, util.CmdHandler(PermissionResponseMsg{Action: PermissionDeny, Permission: p.permission})
+ default:
+ // Pass other keys to viewport
+ viewPort, cmd := p.contentViewPort.Update(msg)
+ p.contentViewPort = viewPort
cmds = append(cmds, cmd)
}
-
- if p.form.State == huh.StateCompleted {
- // Get the selected action
- action := p.form.GetString("action")
-
- // Close the dialog and return the response
- return p, tea.Batch(
- util.CmdHandler(core.DialogCloseMsg{}),
- util.CmdHandler(PermissionResponseMsg{Action: PermissionAction(action), Permission: p.permission}),
- )
- }
}
+
return p, tea.Batch(cmds...)
}
-func (p *permissionDialogCmp) render() string {
- keyStyle := lipgloss.NewStyle().Bold(true).Foreground(styles.Rosewater)
- valueStyle := lipgloss.NewStyle().Foreground(styles.Peach)
+func (p *permissionDialogCmp) selectCurrentOption() tea.Cmd {
+ var action PermissionAction
- form := p.form.View()
-
- headerParts := []string{
- lipgloss.JoinHorizontal(lipgloss.Left, keyStyle.Render("Tool:"), " ", valueStyle.Render(p.permission.ToolName)),
- " ",
- lipgloss.JoinHorizontal(lipgloss.Left, keyStyle.Render("Path:"), " ", valueStyle.Render(p.permission.Path)),
- " ",
+ switch p.selectedOption {
+ case 0:
+ action = PermissionAllow
+ case 1:
+ action = PermissionAllowForSession
+ case 2:
+ action = PermissionDeny
}
- // Create the header content first so it can be used in all cases
- headerContent := lipgloss.NewStyle().Padding(0, 1).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
-
- r, _ := glamour.NewTermRenderer(
- glamour.WithStyles(styles.CatppuccinMarkdownStyle()),
- glamour.WithWordWrap(p.width-10),
- glamour.WithEmoji(),
- )
-
- // Handle different tool types
- switch p.permission.ToolName {
- case tools.BashToolName:
- pr := p.permission.Params.(tools.BashPermissionsParams)
- headerParts = append(headerParts, keyStyle.Render("Command:"))
- content := fmt.Sprintf("```bash\n%s\n```", pr.Command)
-
- renderedContent, _ := r.Render(content)
- p.contentViewPort.Width = p.width - 2 - 2
-
- // Calculate content height dynamically based on content
- contentLines := len(strings.Split(renderedContent, "\n"))
- // Set a reasonable min/max for the viewport height
- minContentHeight := 3
- maxContentHeight := p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
-
- // Add some padding to the content lines
- contentHeight := contentLines + 2
- contentHeight = max(contentHeight, minContentHeight)
- contentHeight = min(contentHeight, maxContentHeight)
- p.contentViewPort.Height = contentHeight
-
- p.contentViewPort.SetContent(renderedContent)
+ return util.CmdHandler(PermissionResponseMsg{Action: action, Permission: p.permission})
+}
- // Style the viewport
- var contentBorder lipgloss.Border
- var borderColor lipgloss.TerminalColor
+func (p *permissionDialogCmp) renderButtons() string {
+ allowStyle := styles.BaseStyle
+ allowSessionStyle := styles.BaseStyle
+ denyStyle := styles.BaseStyle
+ spacerStyle := styles.BaseStyle.Background(styles.Background)
+
+ // Style the selected button
+ switch p.selectedOption {
+ case 0:
+ allowStyle = allowStyle.Background(styles.PrimaryColor).Foreground(styles.Background)
+ allowSessionStyle = allowSessionStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
+ denyStyle = denyStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
+ case 1:
+ allowStyle = allowStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
+ allowSessionStyle = allowSessionStyle.Background(styles.PrimaryColor).Foreground(styles.Background)
+ denyStyle = denyStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
+ case 2:
+ allowStyle = allowStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
+ allowSessionStyle = allowSessionStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
+ denyStyle = denyStyle.Background(styles.PrimaryColor).Foreground(styles.Background)
+ }
- if p.isViewportFocus {
- contentBorder = lipgloss.DoubleBorder()
- borderColor = styles.Blue
- } else {
- contentBorder = lipgloss.RoundedBorder()
- borderColor = styles.Flamingo
- }
+ allowButton := allowStyle.Padding(0, 1).Render("Allow (a)")
+ allowSessionButton := allowSessionStyle.Padding(0, 1).Render("Allow for session (A)")
+ denyButton := denyStyle.Padding(0, 1).Render("Deny (d)")
+
+ content := lipgloss.JoinHorizontal(
+ lipgloss.Left,
+ allowButton,
+ spacerStyle.Render(" "),
+ allowSessionButton,
+ spacerStyle.Render(" "),
+ denyButton,
+ spacerStyle.Render(" "),
+ )
- contentStyle := lipgloss.NewStyle().
- MarginTop(1).
- Padding(0, 1).
- Border(contentBorder).
- BorderForeground(borderColor)
+ remainingWidth := p.width - lipgloss.Width(content)
+ if remainingWidth > 0 {
+ content = spacerStyle.Render(strings.Repeat(" ", remainingWidth)) + content
+ }
+ return content
+}
- if p.isViewportFocus {
- contentStyle = contentStyle.BorderBackground(styles.Surface0)
- }
+func (p *permissionDialogCmp) renderHeader() string {
+ toolKey := styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("Tool")
+ toolValue := styles.BaseStyle.
+ Foreground(styles.Forground).
+ Width(p.width - lipgloss.Width(toolKey)).
+ Render(fmt.Sprintf(": %s", p.permission.ToolName))
- contentFinal := contentStyle.Render(p.contentViewPort.View())
+ pathKey := styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("Path")
+ pathValue := styles.BaseStyle.
+ Foreground(styles.Forground).
+ Width(p.width - lipgloss.Width(pathKey)).
+ Render(fmt.Sprintf(": %s", p.permission.Path))
- return lipgloss.JoinVertical(
- lipgloss.Top,
- headerContent,
- contentFinal,
- form,
- )
+ headerParts := []string{
+ lipgloss.JoinHorizontal(
+ lipgloss.Left,
+ toolKey,
+ toolValue,
+ ),
+ styles.BaseStyle.Render(strings.Repeat(" ", p.width)),
+ lipgloss.JoinHorizontal(
+ lipgloss.Left,
+ pathKey,
+ pathValue,
+ ),
+ styles.BaseStyle.Render(strings.Repeat(" ", p.width)),
+ }
+ // Add tool-specific header information
+ switch p.permission.ToolName {
+ case tools.BashToolName:
+ headerParts = append(headerParts, styles.BaseStyle.Foreground(styles.ForgroundDim).Width(p.width).Bold(true).Render("Command"))
case tools.EditToolName:
- pr := p.permission.Params.(tools.EditPermissionsParams)
- headerParts = append(headerParts, keyStyle.Render("Update"))
- // Recreate header content with the updated headerParts
- headerContent = lipgloss.NewStyle().Padding(0, 1).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
-
- // Format the diff with colors
-
- // Set up viewport for the diff content
- p.contentViewPort.Width = p.width - 2 - 2
-
- // Calculate content height dynamically based on window size
- maxContentHeight := p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
- p.contentViewPort.Height = maxContentHeight
- diff, err := diff.FormatDiff(pr.Diff, diff.WithTotalWidth(p.contentViewPort.Width))
- if err != nil {
- diff = fmt.Sprintf("Error formatting diff: %v", err)
- }
- p.contentViewPort.SetContent(diff)
+ headerParts = append(headerParts, styles.BaseStyle.Foreground(styles.ForgroundDim).Width(p.width).Bold(true).Render("Diff"))
+ case tools.WriteToolName:
+ headerParts = append(headerParts, styles.BaseStyle.Foreground(styles.ForgroundDim).Width(p.width).Bold(true).Render("Diff"))
+ case tools.FetchToolName:
+ headerParts = append(headerParts, styles.BaseStyle.Foreground(styles.ForgroundDim).Width(p.width).Bold(true).Render("URL"))
+ }
- // Style the viewport
- var contentBorder lipgloss.Border
- var borderColor lipgloss.TerminalColor
+ return lipgloss.NewStyle().Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
+}
- if p.isViewportFocus {
- contentBorder = lipgloss.DoubleBorder()
- borderColor = styles.Blue
- } else {
- contentBorder = lipgloss.RoundedBorder()
- borderColor = styles.Flamingo
- }
+func (p *permissionDialogCmp) renderBashContent() string {
+ if pr, ok := p.permission.Params.(tools.BashPermissionsParams); ok {
+ content := fmt.Sprintf("```bash\n%s\n```", pr.Command)
- contentStyle := lipgloss.NewStyle().
- MarginTop(1).
- Padding(0, 1).
- Border(contentBorder).
- BorderForeground(borderColor)
+ // Use the cache for markdown rendering
+ renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
+ r, _ := glamour.NewTermRenderer(
+ glamour.WithStyles(styles.MarkdownTheme(true)),
+ glamour.WithWordWrap(p.width-10),
+ )
+ s, err := r.Render(content)
+ return styles.ForceReplaceBackgroundWithLipgloss(s, styles.Background), err
+ })
+
+ finalContent := styles.BaseStyle.
+ Width(p.contentViewPort.Width).
+ Render(renderedContent)
+ p.contentViewPort.SetContent(finalContent)
+ return p.styleViewport()
+ }
+ return ""
+}
- if p.isViewportFocus {
- contentStyle = contentStyle.BorderBackground(styles.Surface0)
- }
+func (p *permissionDialogCmp) renderEditContent() string {
+ if pr, ok := p.permission.Params.(tools.EditPermissionsParams); ok {
+ diff := p.GetOrSetDiff(p.permission.ID, func() (string, error) {
+ return diff.FormatDiff(pr.Diff, diff.WithTotalWidth(p.contentViewPort.Width))
+ })
- contentFinal := contentStyle.Render(p.contentViewPort.View())
+ p.contentViewPort.SetContent(diff)
+ return p.styleViewport()
+ }
+ return ""
+}
- return lipgloss.JoinVertical(
- lipgloss.Top,
- headerContent,
- contentFinal,
- form,
- )
+func (p *permissionDialogCmp) renderWriteContent() string {
+ if pr, ok := p.permission.Params.(tools.WritePermissionsParams); ok {
+ // Use the cache for diff rendering
+ diff := p.GetOrSetDiff(p.permission.ID, func() (string, error) {
+ return diff.FormatDiff(pr.Diff, diff.WithTotalWidth(p.contentViewPort.Width))
+ })
- case tools.WriteToolName:
- pr := p.permission.Params.(tools.WritePermissionsParams)
- headerParts = append(headerParts, keyStyle.Render("Content"))
- // Recreate header content with the updated headerParts
- headerContent = lipgloss.NewStyle().Padding(0, 1).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
-
- // Set up viewport for the content
- p.contentViewPort.Width = p.width - 2 - 2
-
- // Calculate content height dynamically based on window size
- maxContentHeight := p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
- p.contentViewPort.Height = maxContentHeight
- diff, err := diff.FormatDiff(pr.Diff, diff.WithTotalWidth(p.contentViewPort.Width))
- if err != nil {
- diff = fmt.Sprintf("Error formatting diff: %v", err)
- }
p.contentViewPort.SetContent(diff)
+ return p.styleViewport()
+ }
+ return ""
+}
- // Style the viewport
- var contentBorder lipgloss.Border
- var borderColor lipgloss.TerminalColor
+func (p *permissionDialogCmp) renderFetchContent() string {
+ if pr, ok := p.permission.Params.(tools.FetchPermissionsParams); ok {
+ content := fmt.Sprintf("```bash\n%s\n```", pr.URL)
- if p.isViewportFocus {
- contentBorder = lipgloss.DoubleBorder()
- borderColor = styles.Blue
- } else {
- contentBorder = lipgloss.RoundedBorder()
- borderColor = styles.Flamingo
- }
-
- contentStyle := lipgloss.NewStyle().
- MarginTop(1).
- Padding(0, 1).
- Border(contentBorder).
- BorderForeground(borderColor)
+ // Use the cache for markdown rendering
+ renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
+ r, _ := glamour.NewTermRenderer(
+ glamour.WithStyles(styles.MarkdownTheme(true)),
+ glamour.WithWordWrap(p.width-10),
+ )
+ s, err := r.Render(content)
+ return styles.ForceReplaceBackgroundWithLipgloss(s, styles.Background), err
+ })
- if p.isViewportFocus {
- contentStyle = contentStyle.BorderBackground(styles.Surface0)
- }
+ p.contentViewPort.SetContent(renderedContent)
+ return p.styleViewport()
+ }
+ return ""
+}
- contentFinal := contentStyle.Render(p.contentViewPort.View())
+func (p *permissionDialogCmp) renderDefaultContent() string {
+ content := p.permission.Description
- return lipgloss.JoinVertical(
- lipgloss.Top,
- headerContent,
- contentFinal,
- form,
+ // Use the cache for markdown rendering
+ renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
+ r, _ := glamour.NewTermRenderer(
+ glamour.WithStyles(styles.CatppuccinMarkdownStyle()),
+ glamour.WithWordWrap(p.width-10),
)
+ s, err := r.Render(content)
+ return styles.ForceReplaceBackgroundWithLipgloss(s, styles.Background), err
+ })
- case tools.FetchToolName:
- pr := p.permission.Params.(tools.FetchPermissionsParams)
- headerParts = append(headerParts, keyStyle.Render("URL: "+pr.URL))
- content := p.permission.Description
+ p.contentViewPort.SetContent(renderedContent)
- renderedContent, _ := r.Render(content)
- p.contentViewPort.Width = p.width - 2 - 2
- p.contentViewPort.Height = p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
- p.contentViewPort.SetContent(renderedContent)
+ if renderedContent == "" {
+ return ""
+ }
- // Style the viewport
- contentStyle := lipgloss.NewStyle().
- MarginTop(1).
- Padding(0, 1).
- Border(lipgloss.RoundedBorder()).
- BorderForeground(styles.Flamingo)
+ return p.styleViewport()
+}
- contentFinal := contentStyle.Render(p.contentViewPort.View())
- if renderedContent == "" {
- contentFinal = ""
- }
+func (p *permissionDialogCmp) styleViewport() string {
+ contentStyle := lipgloss.NewStyle().
+ Background(styles.Background)
- return lipgloss.JoinVertical(
- lipgloss.Top,
- headerContent,
- contentFinal,
- form,
- )
+ return contentStyle.Render(p.contentViewPort.View())
+}
+func (p *permissionDialogCmp) render() string {
+ title := styles.BaseStyle.
+ Bold(true).
+ Width(p.width - 4).
+ Foreground(styles.PrimaryColor).
+ Render("Permission Required")
+ // Render header
+ headerContent := p.renderHeader()
+ // Render buttons
+ buttons := p.renderButtons()
+
+ // Calculate content height dynamically based on window size
+ p.contentViewPort.Height = p.height - lipgloss.Height(headerContent) - lipgloss.Height(buttons) - 2 - lipgloss.Height(title)
+ p.contentViewPort.Width = p.width - 4
+
+ // Render content based on tool type
+ var contentFinal string
+ switch p.permission.ToolName {
+ case tools.BashToolName:
+ contentFinal = p.renderBashContent()
+ case tools.EditToolName:
+ contentFinal = p.renderEditContent()
+ case tools.WriteToolName:
+ contentFinal = p.renderWriteContent()
+ case tools.FetchToolName:
+ contentFinal = p.renderFetchContent()
default:
- content := p.permission.Description
-
- renderedContent, _ := r.Render(content)
- p.contentViewPort.Width = p.width - 2 - 2
- p.contentViewPort.Height = p.height - lipgloss.Height(headerContent) - lipgloss.Height(form) - 2 - 2 - 1
- p.contentViewPort.SetContent(renderedContent)
-
- // Style the viewport
- contentStyle := lipgloss.NewStyle().
- MarginTop(1).
- Padding(0, 1).
- Border(lipgloss.RoundedBorder()).
- BorderForeground(styles.Flamingo)
+ contentFinal = p.renderDefaultContent()
+ }
- contentFinal := contentStyle.Render(p.contentViewPort.View())
- if renderedContent == "" {
- contentFinal = ""
- }
+ content := lipgloss.JoinVertical(
+ lipgloss.Top,
+ title,
+ styles.BaseStyle.Render(strings.Repeat(" ", lipgloss.Width(title))),
+ headerContent,
+ contentFinal,
+ buttons,
+ )
- return lipgloss.JoinVertical(
- lipgloss.Top,
- headerContent,
- contentFinal,
- form,
+ return styles.BaseStyle.
+ Padding(1, 0, 0, 1).
+ Border(lipgloss.RoundedBorder()).
+ BorderBackground(styles.Background).
+ BorderForeground(styles.ForgroundDim).
+ Width(p.width).
+ Height(p.height).
+ Render(
+ content,
)
- }
}
func (p *permissionDialogCmp) View() string {
return p.render()
}
-func (p *permissionDialogCmp) GetSize() (int, int) {
- return p.width, p.height
+func (p *permissionDialogCmp) BindingKeys() []key.Binding {
+ return layout.KeyMapToSlice(helpKeys)
}
-func (p *permissionDialogCmp) SetSize(width int, height int) {
- p.width = width
- p.height = height
- p.form = p.form.WithWidth(width)
+func (p *permissionDialogCmp) SetSize() {
+ if p.permission.ID == "" {
+ return
+ }
+ switch p.permission.ToolName {
+ case tools.BashToolName:
+ p.width = int(float64(p.windowSize.Width) * 0.4)
+ p.height = int(float64(p.windowSize.Height) * 0.3)
+ case tools.EditToolName:
+ p.width = int(float64(p.windowSize.Width) * 0.8)
+ p.height = int(float64(p.windowSize.Height) * 0.8)
+ case tools.WriteToolName:
+ p.width = int(float64(p.windowSize.Width) * 0.8)
+ p.height = int(float64(p.windowSize.Height) * 0.8)
+ case tools.FetchToolName:
+ p.width = int(float64(p.windowSize.Width) * 0.4)
+ p.height = int(float64(p.windowSize.Height) * 0.3)
+ default:
+ p.width = int(float64(p.windowSize.Width) * 0.7)
+ p.height = int(float64(p.windowSize.Height) * 0.5)
+ }
}
-func (p *permissionDialogCmp) BindingKeys() []key.Binding {
- return p.form.KeyBinds()
+func (p *permissionDialogCmp) SetPermissions(permission permission.PermissionRequest) {
+ p.permission = permission
+ p.SetSize()
}
-func newPermissionDialogCmp(permission permission.PermissionRequest) PermissionDialog {
- // Create a note field for displaying the content
+// Helper to get or set cached diff content
+func (c *permissionDialogCmp) GetOrSetDiff(key string, generator func() (string, error)) string {
+ if cached, ok := c.diffCache[key]; ok {
+ return cached
+ }
- // Create select field for the permission options
- selectOption := huh.NewSelect[string]().
- Key("action").
- Options(
- huh.NewOption("Allow", string(PermissionAllow)),
- huh.NewOption("Allow for this session", string(PermissionAllowForSession)),
- huh.NewOption("Deny", string(PermissionDeny)),
- ).
- Title("Select an action")
+ content, err := generator()
+ if err != nil {
+ return fmt.Sprintf("Error formatting diff: %v", err)
+ }
- // Apply theme
- theme := styles.HuhTheme()
+ c.diffCache[key] = content
- // Setup form width and height
- form := huh.NewForm(huh.NewGroup(selectOption)).
- WithShowHelp(false).
- WithTheme(theme).
- WithShowErrors(false)
+ return content
+}
- // Focus the form for immediate interaction
- selectOption.Focus()
+// Helper to get or set cached markdown content
+func (c *permissionDialogCmp) GetOrSetMarkdown(key string, generator func() (string, error)) string {
+ if cached, ok := c.markdownCache[key]; ok {
+ return cached
+ }
- return &permissionDialogCmp{
- permission: permission,
- form: form,
- selectOption: selectOption,
+ content, err := generator()
+ if err != nil {
+ return fmt.Sprintf("Error rendering markdown: %v", err)
}
-}
-// NewPermissionDialogCmd creates a new permission dialog command
-func NewPermissionDialogCmd(permission permission.PermissionRequest) tea.Cmd {
- permDialog := newPermissionDialogCmp(permission)
-
- // Create the dialog layout
- dialogPane := layout.NewSinglePane(
- permDialog.(*permissionDialogCmp),
- layout.WithSinglePaneBordered(true),
- layout.WithSinglePaneFocusable(true),
- layout.WithSinglePaneActiveColor(styles.Warning),
- layout.WithSinglePaneBorderText(map[layout.BorderPosition]string{
- layout.TopMiddleBorder: " Permission Required ",
- }),
- )
+ c.markdownCache[key] = content
- // Focus the dialog
- dialogPane.Focus()
- widthRatio := 0.7
- heightRatio := 0.6
- minWidth := 100
- minHeight := 30
+ return content
+}
- // Make the dialog size more appropriate for different tools
- switch permission.ToolName {
- case tools.BashToolName:
- // For bash commands, use a more compact dialog
- widthRatio = 0.7
- heightRatio = 0.4 // Reduced from 0.5
- minWidth = 100
- minHeight = 20 // Reduced from 30
+func NewPermissionDialogCmp() PermissionDialogCmp {
+ // Create viewport for content
+ contentViewport := viewport.New(0, 0)
+
+ return &permissionDialogCmp{
+ contentViewPort: contentViewport,
+ selectedOption: 0, // Default to "Allow"
+ diffCache: make(map[string]string),
+ markdownCache: make(map[string]string),
}
- // Return the dialog command
- return util.CmdHandler(core.DialogMsg{
- Content: dialogPane,
- WidthRatio: widthRatio,
- HeightRatio: heightRatio,
- MinWidth: minWidth,
- MinHeight: minHeight,
- })
}
diff --git a/internal/tui/components/dialog/quit.go b/internal/tui/components/dialog/quit.go
index 60c1fc0d2..10d9ba8a2 100644
--- a/internal/tui/components/dialog/quit.go
+++ b/internal/tui/components/dialog/quit.go
@@ -1,28 +1,58 @@
package dialog
import (
+ "strings"
+
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
- "github.com/kujtimiihoxha/termai/internal/tui/components/core"
+ "github.com/charmbracelet/lipgloss"
"github.com/kujtimiihoxha/termai/internal/tui/layout"
"github.com/kujtimiihoxha/termai/internal/tui/styles"
"github.com/kujtimiihoxha/termai/internal/tui/util"
-
- "github.com/charmbracelet/huh"
)
const question = "Are you sure you want to quit?"
+type CloseQuitMsg struct{}
+
type QuitDialog interface {
tea.Model
- layout.Sizeable
layout.Bindings
}
type quitDialogCmp struct {
- form *huh.Form
- width int
- height int
+ selectedNo bool
+}
+
+type helpMapping struct {
+ LeftRight key.Binding
+ EnterSpace key.Binding
+ Yes key.Binding
+ No key.Binding
+ Tab key.Binding
+}
+
+var helpKeys = helpMapping{
+ LeftRight: key.NewBinding(
+ key.WithKeys("left", "right"),
+ key.WithHelp("←/→", "switch options"),
+ ),
+ EnterSpace: key.NewBinding(
+ key.WithKeys("enter", " "),
+ key.WithHelp("enter/space", "confirm"),
+ ),
+ Yes: key.NewBinding(
+ key.WithKeys("y", "Y"),
+ key.WithHelp("y/Y", "yes"),
+ ),
+ No: key.NewBinding(
+ key.WithKeys("n", "N"),
+ key.WithHelp("n/N", "no"),
+ ),
+ Tab: key.NewBinding(
+ key.WithKeys("tab"),
+ key.WithHelp("tab", "switch options"),
+ ),
}
func (q *quitDialogCmp) Init() tea.Cmd {
@@ -30,77 +60,73 @@ func (q *quitDialogCmp) Init() tea.Cmd {
}
func (q *quitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- var cmds []tea.Cmd
- form, cmd := q.form.Update(msg)
- if f, ok := form.(*huh.Form); ok {
- q.form = f
- cmds = append(cmds, cmd)
- }
-
- if q.form.State == huh.StateCompleted {
- v := q.form.GetBool("quit")
- if v {
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ switch {
+ case key.Matches(msg, helpKeys.LeftRight) || key.Matches(msg, helpKeys.Tab):
+ q.selectedNo = !q.selectedNo
+ return q, nil
+ case key.Matches(msg, helpKeys.EnterSpace):
+ if !q.selectedNo {
+ return q, tea.Quit
+ }
+ return q, util.CmdHandler(CloseQuitMsg{})
+ case key.Matches(msg, helpKeys.Yes):
return q, tea.Quit
+ case key.Matches(msg, helpKeys.No):
+ return q, util.CmdHandler(CloseQuitMsg{})
}
- cmds = append(cmds, util.CmdHandler(core.DialogCloseMsg{}))
}
-
- return q, tea.Batch(cmds...)
+ return q, nil
}
func (q *quitDialogCmp) View() string {
- return q.form.View()
-}
+ yesStyle := styles.BaseStyle
+ noStyle := styles.BaseStyle
+ spacerStyle := styles.BaseStyle.Background(styles.Background)
+
+ if q.selectedNo {
+ noStyle = noStyle.Background(styles.PrimaryColor).Foreground(styles.Background)
+ yesStyle = yesStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
+ } else {
+ yesStyle = yesStyle.Background(styles.PrimaryColor).Foreground(styles.Background)
+ noStyle = noStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
+ }
-func (q *quitDialogCmp) GetSize() (int, int) {
- return q.width, q.height
-}
+ yesButton := yesStyle.Padding(0, 1).Render("Yes")
+ noButton := noStyle.Padding(0, 1).Render("No")
+
+ buttons := lipgloss.JoinHorizontal(lipgloss.Left, yesButton, spacerStyle.Render(" "), noButton)
+
+ width := lipgloss.Width(question)
+ remainingWidth := width - lipgloss.Width(buttons)
+ if remainingWidth > 0 {
+ buttons = spacerStyle.Render(strings.Repeat(" ", remainingWidth)) + buttons
+ }
-func (q *quitDialogCmp) SetSize(width int, height int) {
- q.width = width
- q.height = height
- q.form = q.form.WithWidth(width).WithHeight(height)
+ content := styles.BaseStyle.Render(
+ lipgloss.JoinVertical(
+ lipgloss.Center,
+ question,
+ "",
+ buttons,
+ ),
+ )
+
+ return styles.BaseStyle.Padding(1, 2).
+ Border(lipgloss.RoundedBorder()).
+ BorderBackground(styles.Background).
+ BorderForeground(styles.ForgroundDim).
+ Width(lipgloss.Width(content) + 4).
+ Render(content)
}
func (q *quitDialogCmp) BindingKeys() []key.Binding {
- return q.form.KeyBinds()
+ return layout.KeyMapToSlice(helpKeys)
}
-func newQuitDialogCmp() QuitDialog {
- confirm := huh.NewConfirm().
- Title(question).
- Affirmative("Yes!").
- Key("quit").
- Negative("No.")
-
- theme := styles.HuhTheme()
- theme.Focused.FocusedButton = theme.Focused.FocusedButton.Background(styles.Warning)
- theme.Blurred.FocusedButton = theme.Blurred.FocusedButton.Background(styles.Warning)
- form := huh.NewForm(huh.NewGroup(confirm)).
- WithShowHelp(false).
- WithWidth(0).
- WithHeight(0).
- WithTheme(theme).
- WithShowErrors(false)
- confirm.Focus()
+func NewQuitCmp() QuitDialog {
return &quitDialogCmp{
- form: form,
+ selectedNo: true,
}
}
-
-func NewQuitDialogCmd() tea.Cmd {
- content := layout.NewSinglePane(
- newQuitDialogCmp().(*quitDialogCmp),
- layout.WithSinglePaneBordered(true),
- layout.WithSinglePaneFocusable(true),
- layout.WithSinglePaneActiveColor(styles.Warning),
- )
- content.Focus()
- return util.CmdHandler(core.DialogMsg{
- Content: content,
- WidthRatio: 0.2,
- HeightRatio: 0.1,
- MinWidth: 40,
- MinHeight: 5,
- })
-}