feat: ghdash integration

Kujtim Hoxha created

Change summary

internal/tui/components/dialogs/commands/commands.go | 15 ++
internal/tui/components/dialogs/ghdash/ghdash.go     | 91 ++++++++++++++
internal/tui/tui.go                                  |  9 +
3 files changed, 115 insertions(+)

Detailed changes

internal/tui/components/dialogs/commands/commands.go 🔗

@@ -87,6 +87,7 @@ type (
 	OpenExternalEditorMsg  struct{}
 	ToggleYoloModeMsg      struct{}
 	OpenLazygitMsg         struct{}
+	OpenGhDashMsg          struct{}
 	CompactMsg             struct {
 		SessionID string
 	}
@@ -452,6 +453,20 @@ func (c *commandDialogCmp) defaultCommands() []Command {
 		})
 	}
 
+	// Add gh-dash command if gh CLI with dash extension is installed.
+	if out, _, err := sh.Exec(c.ctx, "gh extension list"); err == nil {
+		if strings.Contains(out, "dash") {
+			commands = append(commands, Command{
+				ID:          "ghdash",
+				Title:       "Open GitHub Dashboard",
+				Description: "Open gh-dash for GitHub pull requests and issues",
+				Handler: func(cmd Command) tea.Cmd {
+					return util.CmdHandler(OpenGhDashMsg{})
+				},
+			})
+		}
+	}
+
 	return append(commands, []Command{
 		{
 			ID:          "toggle_yolo",

internal/tui/components/dialogs/ghdash/ghdash.go 🔗

@@ -0,0 +1,91 @@
+// Package ghdash provides a dialog component for embedding gh-dash in the TUI.
+package ghdash
+
+import (
+	"context"
+	"fmt"
+	"image/color"
+	"os"
+
+	"github.com/charmbracelet/crush/internal/terminal"
+	"github.com/charmbracelet/crush/internal/tui/components/dialogs"
+	"github.com/charmbracelet/crush/internal/tui/components/dialogs/termdialog"
+	"github.com/charmbracelet/crush/internal/tui/styles"
+)
+
+// DialogID is the unique identifier for the gh-dash dialog.
+const DialogID dialogs.DialogID = "ghdash"
+
+// NewDialog creates a new gh-dash dialog. The context controls the lifetime
+// of the gh-dash process - when cancelled, the process will be killed.
+func NewDialog(ctx context.Context, workingDir string) *termdialog.Dialog {
+	configFile := createThemedConfig()
+
+	cmd := terminal.PrepareCmd(
+		ctx,
+		"gh",
+		[]string{"dash", "--config", configFile},
+		workingDir,
+		nil,
+	)
+
+	return termdialog.New(termdialog.Config{
+		ID:         DialogID,
+		Title:      "GitHub Dashboard",
+		LoadingMsg: "Starting gh-dash...",
+		Term:       terminal.New(terminal.Config{Context: ctx, Cmd: cmd}),
+		OnClose: func() {
+			if configFile != "" {
+				_ = os.Remove(configFile)
+			}
+		},
+	})
+}
+
+// colorToHex converts a color.Color to a hex string.
+func colorToHex(c color.Color) string {
+	r, g, b, _ := c.RGBA()
+	return fmt.Sprintf("#%02x%02x%02x", r>>8, g>>8, b>>8)
+}
+
+// createThemedConfig creates a temporary gh-dash config file with Crush theme.
+func createThemedConfig() string {
+	t := styles.CurrentTheme()
+
+	config := fmt.Sprintf(`theme:
+  colors:
+    text:
+      primary: "%s"
+      secondary: "%s"
+      inverted: "%s"
+      faint: "%s"
+      warning: "%s"
+      success: "%s"
+    background:
+      selected: "%s"
+    border:
+      primary: "%s"
+      secondary: "%s"
+      faint: "%s"
+`,
+		colorToHex(t.FgBase),
+		colorToHex(t.FgMuted),
+		colorToHex(t.BgBase),
+		colorToHex(t.FgHalfMuted),
+		colorToHex(t.Warning),
+		colorToHex(t.Success),
+		colorToHex(t.Primary),
+		colorToHex(t.BorderFocus),
+		colorToHex(t.Border),
+		colorToHex(t.BgSubtle),
+	)
+
+	f, err := os.CreateTemp("", "crush-ghdash-*.yml")
+	if err != nil {
+		return ""
+	}
+	defer f.Close()
+
+	_, _ = f.WriteString(config)
+	return f.Name()
+}

internal/tui/tui.go 🔗

@@ -28,6 +28,7 @@ import (
 	"github.com/charmbracelet/crush/internal/tui/components/dialogs"
 	"github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
 	"github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker"
+	"github.com/charmbracelet/crush/internal/tui/components/dialogs/ghdash"
 	"github.com/charmbracelet/crush/internal/tui/components/dialogs/lazygit"
 	"github.com/charmbracelet/crush/internal/tui/components/dialogs/models"
 	"github.com/charmbracelet/crush/internal/tui/components/dialogs/permissions"
@@ -312,6 +313,14 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		return a, util.CmdHandler(dialogs.OpenDialogMsg{
 			Model: lazygit.NewDialog(a.app.Context(), a.app.Config().WorkingDir()),
 		})
+	// GhDash
+	case commands.OpenGhDashMsg:
+		if a.dialog.ActiveDialogID() == ghdash.DialogID {
+			return a, util.CmdHandler(dialogs.CloseDialogMsg{})
+		}
+		return a, util.CmdHandler(dialogs.OpenDialogMsg{
+			Model: ghdash.NewDialog(a.app.Context(), a.app.Config().WorkingDir()),
+		})
 	// Permissions
 	case pubsub.Event[permission.PermissionNotification]:
 		item, ok := a.pages[a.currentPage]