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}