Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions pkg/commands/oscommands/os.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,15 @@ func (c *OSCommand) PipeCommands(cmdObjs ...*CmdObj) error {
}

func (c *OSCommand) CopyToClipboard(str string) error {
c.logCopyToClipboard(str)
return c.writeToClipboard(str)
}

func (c *OSCommand) CopyToClipboardQuiet(str string) error {
return c.writeToClipboard(str)
}

func (c *OSCommand) logCopyToClipboard(str string) {
escaped := strings.ReplaceAll(str, "\n", "\\n")
truncated := utils.TruncateWithEllipsis(escaped, 40)

Expand All @@ -277,6 +286,9 @@ func (c *OSCommand) CopyToClipboard(str string) error {
},
)
c.LogCommand(msg, false)
}

func (c *OSCommand) writeToClipboard(str string) error {
if c.UserConfig().OS.CopyToClipboardCmd != "" {
cmdStr := utils.ResolvePlaceholderString(c.UserConfig().OS.CopyToClipboardCmd, map[string]string{
"text": c.Cmd.Quote(str),
Expand Down
184 changes: 184 additions & 0 deletions pkg/gui/command_log_panel.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,190 @@ func (gui *Gui) LogAction(action string) {
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
Expand Down
140 changes: 140 additions & 0 deletions pkg/gui/command_log_panel_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package gui

import (
"testing"

"github.com/stretchr/testify/assert"
)

const gitOutputHeader = "Git output:"

func englishCopyToClipboardLogLineMatcher() func(string) bool {
return func(line string) bool {
return logLineMatchesTemplate(line, "Copying '{{.str}}' to clipboard", "{{.str}}")
}
}

func TestGitOutputBlocksFromCommandLogLines(t *testing.T) {
t.Parallel()

lines := []string{
"Push",
" git push",
"",
gitOutputHeader,
"line1",
"line2",
}

assert.Equal(t, []string{"Push\n git push\n\nGit output:\nline1\nline2"}, gitOutputBlocksFromCommandLogLines(lines, gitOutputHeader, englishCopyToClipboardLogLineMatcher()))
}

func TestGitOutputBlocksIncludeIndentedStderr(t *testing.T) {
t.Parallel()

lines := []string{
"Push",
" git push",
gitOutputHeader,
" at foo.go:10",
" at bar.go:20",
"hook failed",
}

assert.Equal(t, []string{"Push\n git push\n\nGit output:\n at foo.go:10\n at bar.go:20\nhook failed"}, gitOutputBlocksFromCommandLogLines(lines, gitOutputHeader, englishCopyToClipboardLogLineMatcher()))
}

func TestGitOutputBlocksIncludeToolErrorWithIndentedContext(t *testing.T) {
t.Parallel()

lines := []string{
"Push",
" git push",
gitOutputHeader,
"Error: validation failed",
" line 42: syntax error",
"more output",
"Stage file",
" git add foo",
gitOutputHeader,
"second command output",
}

assert.Equal(t, []string{
"Push\n git push\n\nGit output:\nError: validation failed\n line 42: syntax error\nmore output",
"Stage file\n git add foo\n\nGit output:\nsecond command output",
}, gitOutputBlocksFromCommandLogLines(lines, gitOutputHeader, englishCopyToClipboardLogLineMatcher()))
}

func TestGitOutputBlocksSkipCopyNotifications(t *testing.T) {
t.Parallel()

lines := []string{
"Push",
" git push",
gitOutputHeader,
"hook line",
" Copying 'hook line' to clipboard",
"hook line 2",
}

assert.Equal(t, []string{"Push\n git push\n\nGit output:\nhook line\nhook line 2"}, gitOutputBlocksFromCommandLogLines(lines, gitOutputHeader, englishCopyToClipboardLogLineMatcher()))
}

func TestGitOutputBlocksEndAtNextCommandLogEntry(t *testing.T) {
t.Parallel()

lines := []string{
"Push",
" git push",
gitOutputHeader,
"first command output",
"Stage file",
" git add foo",
"",
gitOutputHeader,
"second command output",
}

assert.Equal(t, []string{
"Push\n git push\n\nGit output:\nfirst command output",
"Stage file\n git add foo\n\nGit output:\nsecond command output",
}, gitOutputBlocksFromCommandLogLines(lines, gitOutputHeader, englishCopyToClipboardLogLineMatcher()))
}

func TestGitOutputBlocksMultipleBlocksJoined(t *testing.T) {
t.Parallel()

lines := []string{
"Push",
" git push",
gitOutputHeader,
"first command",
"Pull",
" git pull",
gitOutputHeader,
"second command",
}

blocks := gitOutputBlocksFromCommandLogLines(lines, gitOutputHeader, englishCopyToClipboardLogLineMatcher())
assert.Equal(t, "Push\n git push\n\nGit output:\nfirst command\n\nPull\n git pull\n\nGit output:\nsecond command", joinGitOutputBlocks(blocks))
}

func TestLogLineMatchesCopyToClipboardTemplate(t *testing.T) {
t.Parallel()

matcher := englishCopyToClipboardLogLineMatcher()
assert.True(t, matcher(" Copying 'hook line' to clipboard"))
assert.False(t, matcher("Push"))
}

func joinGitOutputBlocks(blocks []string) string {
result := ""
for i, block := range blocks {
if i > 0 {
result += "\n\n"
}
result += block
}
return result
}
16 changes: 9 additions & 7 deletions pkg/gui/context/menu_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ func (self *MenuViewModel) GetDisplayStrings(_ int, _ int) [][]string {

return lo.Map(menuItems, func(item *types.MenuItem, _ int) []string {
displayStrings := item.LabelColumns
if item.DisabledReason != nil {
if item.DisabledReasonAtUse() != nil {
displayStrings[0] = style.FgDefault.SetStrikethrough().Sprint(displayStrings[0])
}

Expand Down Expand Up @@ -237,13 +237,15 @@ func (self *MenuContext) GetKeybindings(opts types.KeybindingsOpts) []*types.Bin
}

func (self *MenuContext) OnMenuPress(selectedItem *types.MenuItem) error {
if selectedItem != nil && selectedItem.DisabledReason != nil {
if selectedItem.DisabledReason.ShowErrorInPanel {
return errors.New(selectedItem.DisabledReason.Text)
}
if selectedItem != nil {
if disabledReason := selectedItem.DisabledReasonAtUse(); disabledReason != nil {
if disabledReason.ShowErrorInPanel {
return errors.New(disabledReason.Text)
}

self.c.ErrorToast(self.c.Tr.DisabledMenuItemPrefix + selectedItem.DisabledReason.Text)
return nil
self.c.ErrorToast(self.c.Tr.DisabledMenuItemPrefix + disabledReason.Text)
return nil
}
}

self.c.Context().Pop()
Expand Down
Loading