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		QuitHint:   "q to close",
 41		OnClose: func() {
 42			if themeConfig != "" {
 43				if err := os.Remove(themeConfig); err != nil {
 44					slog.Debug("failed to remove lazygit theme config", "error", err, "path", themeConfig)
 45				}
 46			}
 47		},
 48	})
 49}
 50
 51// buildConfigEnv builds the LG_CONFIG_FILE env var, merging user's default
 52// config (if it exists) with our theme override. User config comes first so
 53// our theme settings take precedence.
 54func buildConfigEnv(themeConfig string) string {
 55	userConfig := defaultConfigPath()
 56	if userConfig != "" {
 57		if _, err := os.Stat(userConfig); err == nil {
 58			return "LG_CONFIG_FILE=" + userConfig + "," + themeConfig
 59		}
 60	}
 61	return "LG_CONFIG_FILE=" + themeConfig
 62}
 63
 64// defaultConfigPath returns the default lazygit config path for the current OS.
 65func defaultConfigPath() string {
 66	configDir, err := os.UserConfigDir()
 67	if err != nil {
 68		slog.Debug("failed to get user config directory", "error", err)
 69		return ""
 70	}
 71	return filepath.Join(configDir, "lazygit", "config.yml")
 72}
 73
 74// colorToHex converts a color.Color to a hex string.
 75func colorToHex(c color.Color) string {
 76	r, g, b, _ := c.RGBA()
 77	return fmt.Sprintf("#%02x%02x%02x", r>>8, g>>8, b>>8)
 78}
 79
 80// createThemedConfig creates a temporary lazygit config file with Crush theme.
 81// Theme mappings align with Crush's UX patterns:
 82// - Borders: BorderFocus (purple) for active, Border (gray) for inactive
 83// - Selection: Primary (purple) background matches app's TextSelected style
 84// - Status: Success (green), Error (red), Info (blue), Warning (orange)
 85func createThemedConfig() string {
 86	t := styles.CurrentTheme()
 87
 88	config := fmt.Sprintf(`git:
 89  autoFetch: true
 90gui:
 91  theme:
 92    activeBorderColor:
 93      - "%s"
 94      - bold
 95    inactiveBorderColor:
 96      - "%s"
 97    searchingActiveBorderColor:
 98      - "%s"
 99      - bold
100    optionsTextColor:
101      - "%s"
102    selectedLineBgColor:
103      - "%s"
104    inactiveViewSelectedLineBgColor:
105      - "%s"
106    cherryPickedCommitFgColor:
107      - "%s"
108    cherryPickedCommitBgColor:
109      - "%s"
110    markedBaseCommitFgColor:
111      - "%s"
112    markedBaseCommitBgColor:
113      - "%s"
114    unstagedChangesColor:
115      - "%s"
116    defaultFgColor:
117      - default
118`,
119		colorToHex(t.BorderFocus),
120		colorToHex(t.FgMuted),
121		colorToHex(t.Info),
122		colorToHex(t.FgMuted),
123		colorToHex(t.Primary),
124		colorToHex(t.BgSubtle),
125		colorToHex(t.Success),
126		colorToHex(t.BgSubtle),
127		colorToHex(t.Info),
128		colorToHex(t.BgSubtle),
129		colorToHex(t.Error),
130	)
131
132	f, err := os.CreateTemp("", "crush-lazygit-*.yml")
133	if err != nil {
134		slog.Error("failed to create temporary lazygit config", "error", err)
135		return ""
136	}
137	defer f.Close()
138
139	if _, err := f.WriteString(config); err != nil {
140		slog.Error("failed to write lazygit theme config", "error", err)
141		_ = os.Remove(f.Name()) // remove the empty file
142		return ""
143	}
144	return f.Name()
145}