Detailed changes
@@ -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",
@@ -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()
}
@@ -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.
@@ -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
}