diff --git a/internal/tui/components/dialogs/commands/commands.go b/internal/tui/components/dialogs/commands/commands.go index 25a1048b41c43aab3279eda97bcaa5587f4dc644..e801a59b1faeee710439e4cef9652ab36096d613 100644 --- a/internal/tui/components/dialogs/commands/commands.go +++ b/internal/tui/components/dialogs/commands/commands.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "os/exec" "slices" "strings" @@ -18,7 +19,6 @@ import ( "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/csync" "github.com/charmbracelet/crush/internal/pubsub" - "github.com/charmbracelet/crush/internal/shell" "github.com/charmbracelet/crush/internal/tui/components/chat" "github.com/charmbracelet/crush/internal/tui/components/core" "github.com/charmbracelet/crush/internal/tui/components/dialogs" @@ -441,8 +441,7 @@ func (c *commandDialogCmp) defaultCommands() []Command { } // Add lazygit command if lazygit is installed. - sh := shell.NewShell(nil) - if _, _, err := sh.Exec(c.ctx, "which lazygit"); err == nil { + if _, err := exec.LookPath("lazygit"); err == nil { commands = append(commands, Command{ ID: "lazygit", Title: "Open Lazygit", diff --git a/internal/tui/components/dialogs/lazygit/lazygit.go b/internal/tui/components/dialogs/lazygit/lazygit.go index 0a6820d4c874f5d6224fbdb7d3c9b4e6052e151e..ce88aed8029eedf0810810ed12f2098e73f18560 100644 --- a/internal/tui/components/dialogs/lazygit/lazygit.go +++ b/internal/tui/components/dialogs/lazygit/lazygit.go @@ -5,7 +5,9 @@ import ( "context" "fmt" "image/color" + "log/slog" "os" + "path/filepath" "github.com/charmbracelet/crush/internal/terminal" "github.com/charmbracelet/crush/internal/tui/components/dialogs" @@ -13,35 +15,62 @@ import ( "github.com/charmbracelet/crush/internal/tui/styles" ) -// DialogID is the unique identifier for the lazygit dialog. -const DialogID dialogs.DialogID = "lazygit" +// LazygitDialogID is the unique identifier for the lazygit dialog. +const LazygitDialogID dialogs.DialogID = "lazygit" // NewDialog creates a new lazygit dialog. The context controls the lifetime // of the lazygit process - when cancelled, the process will be killed. func NewDialog(ctx context.Context, workingDir string) *termdialog.Dialog { - configFile := createThemedConfig() + themeConfig := createThemedConfig() + configEnv := buildConfigEnv(themeConfig) cmd := terminal.PrepareCmd( ctx, "lazygit", nil, workingDir, - []string{"LG_CONFIG_FILE=" + configFile}, + []string{configEnv}, ) return termdialog.New(termdialog.Config{ - ID: DialogID, + ID: LazygitDialogID, Title: "Lazygit", LoadingMsg: "Starting lazygit...", Term: terminal.New(terminal.Config{Context: ctx, Cmd: cmd}), + QuitHint: "q to close", OnClose: func() { - if configFile != "" { - _ = os.Remove(configFile) + if themeConfig != "" { + if err := os.Remove(themeConfig); err != nil { + slog.Debug("failed to remove lazygit theme config", "error", err, "path", themeConfig) + } } }, }) } +// buildConfigEnv builds the LG_CONFIG_FILE env var, merging user's default +// config (if it exists) with our theme override. User config comes first so +// our theme settings take precedence. +func buildConfigEnv(themeConfig string) string { + userConfig := defaultConfigPath() + if userConfig != "" { + if _, err := os.Stat(userConfig); err == nil { + return "LG_CONFIG_FILE=" + userConfig + "," + themeConfig + } + } + return "LG_CONFIG_FILE=" + themeConfig +} + +// defaultConfigPath returns the default lazygit config path for the current OS. +func defaultConfigPath() string { + configDir, err := os.UserConfigDir() + if err != nil { + slog.Debug("failed to get user config directory", "error", err) + return "" + } + return filepath.Join(configDir, "lazygit", "config.yml") +} + // colorToHex converts a color.Color to a hex string. func colorToHex(c color.Color) string { r, g, b, _ := c.RGBA() @@ -56,15 +85,9 @@ func colorToHex(c color.Color) string { func createThemedConfig() string { t := styles.CurrentTheme() - config := fmt.Sprintf(`gui: - border: rounded - showFileTree: true - showRandomTip: false - showCommandLog: false - showBottomLine: true - showPanelJumps: false - nerdFontsVersion: "" - showFileIcons: false + config := fmt.Sprintf(`git: + autoFetch: true +gui: theme: activeBorderColor: - "%s" @@ -108,10 +131,15 @@ func createThemedConfig() string { f, err := os.CreateTemp("", "crush-lazygit-*.yml") if err != nil { + slog.Error("failed to create temporary lazygit config", "error", err) return "" } defer f.Close() - _, _ = f.WriteString(config) + if _, err := f.WriteString(config); err != nil { + slog.Error("failed to write lazygit theme config", "error", err) + _ = os.Remove(f.Name()) // remove the empty file + return "" + } return f.Name() } diff --git a/internal/tui/components/dialogs/termdialog/termdialog.go b/internal/tui/components/dialogs/termdialog/termdialog.go index 88f3091e1f5b4c083016107b19f22a35d013db07..83e5eb1232af7bb47f848c299f05e6d3710e24f4 100644 --- a/internal/tui/components/dialogs/termdialog/termdialog.go +++ b/internal/tui/components/dialogs/termdialog/termdialog.go @@ -17,8 +17,11 @@ const ( // headerHeight is the height of the dialog header (title + padding). headerHeight = 2 // fullscreenWidthBreakpoint is the width below which the dialog goes - // fullscreen. Matches CompactModeWidthBreakpoint in chat.go. - fullscreenWidthBreakpoint = 120 + // fullscreen. + fullscreenWidthBreakpoint = 125 + // fullscreenHeightBreakpoint is the height below which the dialog goes + // fullscreen. Lazygit degrades significantly below 40 rows. + fullscreenHeightBreakpoint = 40 ) // Config holds configuration for a terminal dialog. @@ -33,6 +36,8 @@ type Config struct { Term *terminal.Terminal // OnClose is called when the dialog is closed (optional). OnClose func() + // QuitHint is shown in the header (e.g., "q to close"). If empty, no hint is shown. + QuitHint string } // Dialog is a dialog that embeds a terminal application. @@ -42,6 +47,7 @@ type Dialog struct { loadingMsg string term *terminal.Terminal onClose func() + quitHint string wWidth int wHeight int @@ -63,6 +69,7 @@ func New(cfg Config) *Dialog { loadingMsg: loadingMsg, term: cfg.Term, onClose: cfg.OnClose, + quitHint: cfg.QuitHint, } } @@ -102,8 +109,8 @@ func (d *Dialog) handleResize(msg tea.WindowSizeMsg) (util.Model, tea.Cmd) { d.wWidth = msg.Width d.wHeight = msg.Height - // Go fullscreen when window is below compact mode breakpoint. - d.fullscreen = msg.Width < fullscreenWidthBreakpoint + // Go fullscreen when window is below size breakpoints. + d.fullscreen = msg.Width < fullscreenWidthBreakpoint || msg.Height < fullscreenHeightBreakpoint var outerWidth, outerHeight int if d.fullscreen { @@ -192,7 +199,20 @@ func (d *Dialog) View() string { termContent = d.loadingMsg } - header := t.S().Base.Padding(0, 1, 1, 1).Render(core.Title(d.title, d.width-2)) + // Build header with title and optional quit hint on the right. + var header string + if d.quitHint != "" { + hintStyle := t.S().Base.Foreground(t.Secondary) + hint := hintStyle.Render(d.quitHint) + hintWidth := lipgloss.Width(hint) + titleWidth := d.width - 2 - hintWidth - 1 // -1 for space between title and hint + title := core.Title(d.title, titleWidth) + headerContent := title + " " + hint + header = t.S().Base.Padding(0, 1, 1, 1).Render(headerContent) + } else { + header = t.S().Base.Padding(0, 1, 1, 1).Render(core.Title(d.title, d.width-2)) + } + content := lipgloss.JoinVertical(lipgloss.Left, header, termContent) dialogStyle := t.S().Base. diff --git a/internal/tui/tui.go b/internal/tui/tui.go index ff84cda848f9fc99a37134c68c78b0435da97c08..a519fec0978bf5c72d35dce5207fad518ea40e7f 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -307,7 +307,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { }) // Lazygit case commands.OpenLazygitMsg: - if a.dialog.ActiveDialogID() == lazygit.DialogID { + if a.dialog.ActiveDialogID() == lazygit.LazygitDialogID { return a, util.CmdHandler(dialogs.CloseDialogMsg{}) } return a, util.CmdHandler(dialogs.OpenDialogMsg{ @@ -467,8 +467,9 @@ func (a *appModel) handleWindowResize(width, height int) tea.Cmd { cmds = append(cmds, pageCmd) } - // Update the dialogs - dialog, cmd := a.dialog.Update(tea.WindowSizeMsg{Width: width, Height: height}) + // Update the dialogs with full window dimensions so they can overlay + // everything including the status bar. + dialog, cmd := a.dialog.Update(tea.WindowSizeMsg{Width: a.wWidth, Height: a.wHeight}) if model, ok := dialog.(dialogs.DialogCmp); ok { a.dialog = model }