lazygit.go

  1// Package lazygit provides a dialog component for embedding lazygit in the TUI.
  2package lazygit
  3
  4import (
  5	"context"
  6	"fmt"
  7	"image/color"
  8	"log/slog"
  9	"os"
 10	"path/filepath"
 11
 12	"github.com/charmbracelet/crush/internal/terminal"
 13	"github.com/charmbracelet/crush/internal/tui/components/dialogs"
 14	"github.com/charmbracelet/crush/internal/tui/components/dialogs/termdialog"
 15	"github.com/charmbracelet/crush/internal/tui/styles"
 16)
 17
 18// LazygitDialogID is the unique identifier for the lazygit dialog.
 19const LazygitDialogID dialogs.DialogID = "lazygit"
 20
 21// NewDialog creates a new lazygit dialog. The context controls the lifetime
 22// of the lazygit process - when cancelled, the process will be killed.
 23func NewDialog(ctx context.Context, workingDir string) *termdialog.Dialog {
 24	themeConfig := createThemedConfig()
 25	configEnv := buildConfigEnv(themeConfig)
 26
 27	cmd := terminal.PrepareCmd(
 28		ctx,
 29		"lazygit",
 30		nil,
 31		workingDir,
 32		[]string{configEnv},
 33	)
 34
 35	return termdialog.New(termdialog.Config{
 36		ID:         LazygitDialogID,
 37		Title:      "Lazygit",
 38		LoadingMsg: "Starting lazygit...",
 39		Term:       terminal.New(terminal.Config{Context: ctx, Cmd: cmd}),
 40		OnClose: func() {
 41			if themeConfig != "" {
 42				if err := os.Remove(themeConfig); err != nil {
 43					slog.Debug("failed to remove lazygit theme config", "error", err, "path", themeConfig)
 44				}
 45			}
 46		},
 47	})
 48}
 49
 50// buildConfigEnv builds the LG_CONFIG_FILE env var, merging user's default
 51// config (if it exists) with our theme override. User config comes first so
 52// our theme settings take precedence.
 53func buildConfigEnv(themeConfig string) string {
 54	userConfig := defaultConfigPath()
 55	if userConfig != "" {
 56		if _, err := os.Stat(userConfig); err == nil {
 57			return "LG_CONFIG_FILE=" + userConfig + "," + themeConfig
 58		}
 59	}
 60	return "LG_CONFIG_FILE=" + themeConfig
 61}
 62
 63// defaultConfigPath returns the default lazygit config path for the current OS.
 64func defaultConfigPath() string {
 65	configDir, err := os.UserConfigDir()
 66	if err != nil {
 67		slog.Debug("failed to get user config directory", "error", err)
 68		return ""
 69	}
 70	return filepath.Join(configDir, "lazygit", "config.yml")
 71}
 72
 73// colorToHex converts a color.Color to a hex string.
 74func colorToHex(c color.Color) string {
 75	r, g, b, _ := c.RGBA()
 76	return fmt.Sprintf("#%02x%02x%02x", r>>8, g>>8, b>>8)
 77}
 78
 79// createThemedConfig creates a temporary lazygit config file with Crush theme.
 80// Theme mappings align with Crush's UX patterns:
 81// - Borders: BorderFocus (purple) for active, Border (gray) for inactive
 82// - Selection: Primary (purple) background matches app's TextSelected style
 83// - Status: Success (green), Error (red), Info (blue), Warning (orange)
 84func createThemedConfig() string {
 85	t := styles.CurrentTheme()
 86
 87	config := fmt.Sprintf(`git:
 88  autoFetch: true
 89gui:
 90  theme:
 91    activeBorderColor:
 92      - "%s"
 93      - bold
 94    inactiveBorderColor:
 95      - "%s"
 96    searchingActiveBorderColor:
 97      - "%s"
 98      - bold
 99    optionsTextColor:
100      - "%s"
101    selectedLineBgColor:
102      - "%s"
103    inactiveViewSelectedLineBgColor:
104      - "%s"
105    cherryPickedCommitFgColor:
106      - "%s"
107    cherryPickedCommitBgColor:
108      - "%s"
109    markedBaseCommitFgColor:
110      - "%s"
111    markedBaseCommitBgColor:
112      - "%s"
113    unstagedChangesColor:
114      - "%s"
115    defaultFgColor:
116      - default
117`,
118		colorToHex(t.BorderFocus),
119		colorToHex(t.FgMuted),
120		colorToHex(t.Info),
121		colorToHex(t.FgMuted),
122		colorToHex(t.Primary),
123		colorToHex(t.BgSubtle),
124		colorToHex(t.Success),
125		colorToHex(t.BgSubtle),
126		colorToHex(t.Info),
127		colorToHex(t.BgSubtle),
128		colorToHex(t.Error),
129	)
130
131	f, err := os.CreateTemp("", "crush-lazygit-*.yml")
132	if err != nil {
133		slog.Error("failed to create temporary lazygit config", "error", err)
134		return ""
135	}
136	defer f.Close()
137
138	if _, err := f.WriteString(config); err != nil {
139		slog.Error("failed to write lazygit theme config", "error", err)
140		_ = os.Remove(f.Name()) // remove the empty file
141		return ""
142	}
143	return f.Name()
144}