jesseduffield.lazygit/pkg/gui/command_log_panel.go
tmwatchanan da09bb7171
Fix command log menu disabled state and editor export content
Evaluate menu disabled reasons when rendering and invoking so copy items
update while the menu is open. Export actions and git output without
panel
chrome, match copy-notification lines via i18n log templates, and
disable
open-in-editor until there is real log content.
2026-06-21 01:04:19 +07:00

373 lines
11 KiB
Go

package gui
import (
"fmt"
"math/rand"
"strings"
"time"
"github.com/jesseduffield/lazygit/pkg/constants"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/theme"
)
// our UI command log looks like this:
// Stage File:
// git add -- 'filename'
// Unstage File:
// git reset HEAD 'filename'
//
// The 'Stage File' and 'Unstage File' lines are actions i.e they group up a set
// of command logs (typically there's only one command under an action but there may be more).
// So we call logAction to log the 'Stage File' part and then we call logCommand to log the command itself.
// We pass logCommand to our OSCommand struct so that it can handle logging commands
// for us.
func (gui *Gui) LogAction(action string) {
if gui.Views.Extras == nil {
return
}
gui.Views.Extras.Autoscroll = true
gui.GuiLog = append(gui.GuiLog, action)
fmt.Fprint(gui.Views.Extras, "\n"+style.FgYellow.Sprint(action))
}
func (gui *Gui) gitOutputBlocksFromView() []string {
if gui.Views.Extras == nil {
return nil
}
return gitOutputBlocksFromCommandLogLines(
gui.Views.Extras.BufferLines(),
gui.c.Tr.GitOutput,
gui.isCopyToClipboardLogLine,
)
}
func (gui *Gui) lastGitOutput() string {
blocks := gui.gitOutputBlocksFromView()
if len(blocks) == 0 {
return ""
}
return blocks[len(blocks)-1]
}
func (gui *Gui) allGitOutput() string {
return strings.Join(gui.gitOutputBlocksFromView(), "\n\n")
}
func (gui *Gui) hasGitOutput() bool {
return gui.lastGitOutput() != ""
}
func (gui *Gui) hasCommandLogEntries() bool {
return gui.commandLogContent() != ""
}
func (gui *Gui) commandLogContent() string {
if gui.Views.Extras == nil {
return ""
}
introLine := gui.commandLogIntroLine()
var filtered []string
for _, line := range gui.Views.Extras.BufferLines() {
trimmed := strings.TrimSpace(line)
if trimmed == "" {
if len(filtered) > 0 {
filtered = append(filtered, "")
}
continue
}
if trimmed == introLine {
continue
}
if strings.HasPrefix(trimmed, gui.c.Tr.RandomTip+":") {
continue
}
if gui.isCopyToClipboardLogLine(line) {
continue
}
if gui.isCreateFileLogLine(line) {
continue
}
filtered = append(filtered, line)
}
return strings.TrimRight(strings.Join(filtered, "\n"), "\n")
}
func (gui *Gui) commandLogIntroLine() string {
return strings.TrimSpace(fmt.Sprintf(
gui.c.Tr.CommandLogHeader,
gui.c.UserConfig().Keybinding.Universal.ExtrasMenu,
))
}
func (gui *Gui) isCopyToClipboardLogLine(line string) bool {
return logLineMatchesTemplate(line, gui.c.Tr.Log.CopyToClipboard, "{{.str}}")
}
func (gui *Gui) isCreateFileLogLine(line string) bool {
return logLineMatchesTemplate(line, gui.c.Tr.Log.CreateFileWithContent, "{{.path}}")
}
func logLineMatchesTemplate(line string, template string, placeholder string) bool {
parts := strings.Split(template, placeholder)
if len(parts) != 2 {
return false
}
trimmed := strings.TrimSpace(line)
return strings.HasPrefix(trimmed, parts[0]) && strings.HasSuffix(trimmed, parts[1])
}
func gitOutputBlocksFromCommandLogLines(lines []string, gitOutputHeader string, isCopyToClipboardLogLine func(string) bool) []string {
var blocks []string
for i, line := range lines {
if line != gitOutputHeader {
continue
}
block := commandLogEntryBeforeGitOutput(lines, i, gitOutputHeader, isCopyToClipboardLogLine)
if len(block) > 0 {
block = append(block, "")
}
block = append(block, gitOutputHeader)
block = append(block, gitOutputLinesAfterHeader(lines, i+1, gitOutputHeader, isCopyToClipboardLogLine)...)
if trimmed := strings.TrimRight(strings.Join(block, "\n"), "\n"); trimmed != "" {
blocks = append(blocks, trimmed)
}
}
return blocks
}
func commandLogEntryBeforeGitOutput(lines []string, headerIdx int, gitOutputHeader string, isCopyToClipboardLogLine func(string) bool) []string {
i := headerIdx - 1
for i >= 0 && lines[i] == "" {
i--
}
var commands []string
for i >= 0 && strings.HasPrefix(lines[i], " ") && !isCopyToClipboardLogLine(lines[i]) {
commands = append([]string{lines[i]}, commands...)
i--
}
if len(commands) == 0 {
return nil
}
for i >= 0 && lines[i] == "" {
i--
}
var entry []string
if i >= 0 && !strings.HasPrefix(lines[i], " ") && lines[i] != gitOutputHeader {
entry = append(entry, lines[i])
}
entry = append(entry, commands...)
return entry
}
func gitOutputLinesAfterHeader(lines []string, startIdx int, gitOutputHeader string, isCopyToClipboardLogLine func(string) bool) []string {
output := make([]string, 0, len(lines)-startIdx)
for i := startIdx; i < len(lines); i++ {
line := lines[i]
if line == gitOutputHeader {
break
}
if isCopyToClipboardLogLine(line) {
continue
}
if isStartOfNewCommandLogEntry(lines, i, isCopyToClipboardLogLine) {
break
}
output = append(output, line)
}
return output
}
func isStartOfNewCommandLogEntry(lines []string, i int, isCopyToClipboardLogLine func(string) bool) bool {
line := lines[i]
if line == "" || strings.HasPrefix(line, " ") {
return false
}
for j := i + 1; j < len(lines); j++ {
if lines[j] == "" {
continue
}
if isCopyToClipboardLogLine(lines[j]) {
continue
}
return isLazygitCommandLogLine(lines[j], isCopyToClipboardLogLine)
}
return false
}
func isLazygitCommandLogLine(line string, isCopyToClipboardLogLine func(string) bool) bool {
return isCopyToClipboardLogLine(line) || strings.HasPrefix(line, " git ")
}
func (gui *Gui) LogCommand(cmdStr string, commandLine bool) {
if gui.Views.Extras == nil {
return
}
gui.Views.Extras.Autoscroll = true
textStyle := theme.DefaultTextColor
if !commandLine {
// if we're not dealing with a direct command that could be run on the command line,
// we style it differently to communicate that
textStyle = style.FgMagenta
}
gui.GuiLog = append(gui.GuiLog, cmdStr)
indentedCmdStr := " " + strings.ReplaceAll(cmdStr, "\n", "\n ")
fmt.Fprint(gui.Views.Extras, "\n"+textStyle.Sprint(indentedCmdStr))
}
func (gui *Gui) printCommandLogHeader() {
introStr := fmt.Sprintf(
gui.c.Tr.CommandLogHeader,
gui.c.UserConfig().Keybinding.Universal.ExtrasMenu,
)
fmt.Fprintln(gui.Views.Extras, style.FgCyan.Sprint(introStr))
if gui.c.UserConfig().Gui.ShowRandomTip {
fmt.Fprintf(
gui.Views.Extras,
"%s: %s",
style.FgYellow.Sprint(gui.c.Tr.RandomTip),
style.FgGreen.Sprint(gui.getRandomTip()),
)
}
}
func (gui *Gui) getRandomTip() string {
config := gui.c.UserConfig().Keybinding
tips := []string{
// keybindings and lazygit-specific advice
fmt.Sprintf(
"To force push, press '%s' and then if the push is rejected you will be asked if you want to force push",
config.Universal.Push,
),
fmt.Sprintf(
"To filter commits by path, press '%s'",
config.Universal.FilteringMenu,
),
fmt.Sprintf(
"To start an interactive rebase, press '%s' on a commit. You can always abort the rebase by pressing '%s' and selecting 'abort'",
config.Universal.Edit,
config.Universal.CreateRebaseOptionsMenu,
),
fmt.Sprintf(
"In flat file view, merge conflicts are sorted to the top. To switch to flat file view press '%s'",
config.Files.ToggleTreeView,
),
"If you want to learn Go and can think of ways to improve lazygit, join the team! Click 'Ask Question' and express your interest",
fmt.Sprintf(
"If you press '%s'/'%s' you can undo/redo your changes. Be wary though, this only applies to branches/commits, so only do this if your worktree is clear.\nDocs: %s",
config.Universal.Undo,
config.Universal.Redo,
constants.Links.Docs.Undoing,
),
fmt.Sprintf(
"to hard reset onto your current upstream branch, press '%s' in the files panel",
config.Commits.ViewResetOptions,
),
fmt.Sprintf(
"To push a tag, navigate to the tag in the tags tab and press '%s'",
config.Branches.PushTag,
),
fmt.Sprintf(
"You can view the individual files of a stash entry by pressing '%s'",
config.Universal.GoInto,
),
fmt.Sprintf(
"You can diff two commits by pressing '%s' on one commit and then navigating to the other. You can then press '%s' to view the files of the diff",
config.Universal.DiffingMenu,
config.Universal.GoInto,
),
fmt.Sprintf(
"press '%s' on a commit to drop it (delete it)",
config.Universal.Remove,
),
fmt.Sprintf(
"If you need to pull out the big guns to resolve merge conflicts, you can press '%s' in the files panel to open merge options",
config.Files.OpenMergeOptions,
),
fmt.Sprintf(
"To revert a commit, press '%s' on that commit",
config.Commits.RevertCommit,
),
fmt.Sprintf(
"To escape a mode, for example cherry-picking, patch-building, diffing, or filtering mode, you can just spam the '%s' button. Unless of course you have `quitOnTopLevelReturn` enabled in your config",
config.Universal.Return,
),
fmt.Sprintf(
"You can page through the items of a panel using '%s' and '%s'",
config.Universal.PrevPage,
config.Universal.NextPage,
),
fmt.Sprintf(
"You can jump to the top/bottom of a panel using '%s' and '%s'",
config.Universal.GotoTop, config.Universal.GotoBottom,
),
fmt.Sprintf(
"To collapse/expand a directory, press '%s'",
config.Universal.GoInto,
),
fmt.Sprintf(
"You can append your staged changes to an older commit by pressing '%s' on that commit",
config.Commits.AmendToCommit,
),
fmt.Sprintf(
"You can amend the last commit with your new file changes by pressing '%s' in the files panel",
config.Files.AmendLastCommit,
),
fmt.Sprintf(
"You can now navigate the side panels with '%s' and '%s'",
config.Universal.NextBlockAlt2,
config.Universal.PrevBlockAlt2,
),
"You can use lazygit with a bare repo by passing the --git-dir and --work-tree arguments as you would for the git CLI",
// general advice
"`git commit` is really just the programmer equivalent of saving your game. Always do it before embarking on an ambitious change!",
"Try to separate commits that refactor code from commits that add new functionality: if they're squashed into one commit, it can be hard to spot what's new.",
"If you ever want to experiment, it's easy to create a new branch off your current one and go nuts, then delete it afterwards",
"Always read through the diff of your changes before assigning somebody to review your code. Better for you to catch any silly mistakes than your colleagues!",
"If something goes wrong, you can always checkout a commit from your reflog to return to an earlier state",
"The stash is a good place to save snippets of code that you always find yourself adding when debugging.",
// links
fmt.Sprintf(
"If you want a git diff with syntax colouring, check out lazygit's integration with delta:\n%s",
constants.Links.Docs.CustomPagers,
),
fmt.Sprintf(
"You can build your own custom menus and commands to run from within lazygit. For examples see:\n%s",
constants.Links.Docs.CustomCommands,
),
fmt.Sprintf(
"If you ever find a bug, do not hesitate to raise an issue on the repo:\n%s",
constants.Links.Issues,
),
}
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
randomIndex := rnd.Intn(len(tips))
return tips[randomIndex]
}