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