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}