diff --git a/docs/Config.md b/docs/Config.md index a48eb4a44..63a7beea8 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -18,6 +18,7 @@ - blue commitLength: show: true + mouseEvents: true git: merging: # only applicable to unix users diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index ca17efd3d..50690c8c5 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -242,7 +242,7 @@ func GetDefaultConfig() []byte { ## stuff relating to the UI scrollHeight: 2 scrollPastBottom: true - mouseEvents: false # will default to true when the feature is complete + mouseEvents: true theme: lightTheme: false activeBorderColor: diff --git a/pkg/gui/branches_panel.go b/pkg/gui/branches_panel.go index 063fc330c..5cf440909 100644 --- a/pkg/gui/branches_panel.go +++ b/pkg/gui/branches_panel.go @@ -19,6 +19,14 @@ func (gui *Gui) getSelectedBranch() *commands.Branch { return gui.State.Branches[selectedLine] } +func (gui *Gui) handleBranchesClick(g *gocui.Gui, v *gocui.View) error { + itemCount := len(gui.State.Branches) + handleSelect := gui.handleBranchSelect + selectedLine := &gui.State.Panels.Branches.SelectedLine + + return gui.handleClick(v, itemCount, selectedLine, handleSelect) +} + // may want to standardise how these select methods work func (gui *Gui) handleBranchSelect(g *gocui.Gui, v *gocui.View) error { if gui.popupPanelFocused() { @@ -30,6 +38,9 @@ func (gui *Gui) handleBranchSelect(g *gocui.Gui, v *gocui.View) error { if _, err := gui.g.SetCurrentView(v.Name()); err != nil { return err } + + gui.getMainView().Title = "Log" + // This really shouldn't happen: there should always be a master branch if len(gui.State.Branches) == 0 { return gui.renderString(g, "main", gui.Tr.SLocalize("NoBranchesThisRepo")) diff --git a/pkg/gui/commit_files_panel.go b/pkg/gui/commit_files_panel.go index 845f7b02d..ab7b56ae7 100644 --- a/pkg/gui/commit_files_panel.go +++ b/pkg/gui/commit_files_panel.go @@ -15,11 +15,21 @@ func (gui *Gui) getSelectedCommitFile(g *gocui.Gui) *commands.CommitFile { return gui.State.CommitFiles[selectedLine] } +func (gui *Gui) handleCommitFilesClick(g *gocui.Gui, v *gocui.View) error { + itemCount := len(gui.State.CommitFiles) + handleSelect := gui.handleCommitFileSelect + selectedLine := &gui.State.Panels.CommitFiles.SelectedLine + + return gui.handleClick(v, itemCount, selectedLine, handleSelect) +} + func (gui *Gui) handleCommitFileSelect(g *gocui.Gui, v *gocui.View) error { if gui.popupPanelFocused() { return nil } + gui.getMainView().Title = "Patch" + commitFile := gui.getSelectedCommitFile(g) if commitFile == nil { return gui.renderString(g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles")) @@ -96,7 +106,7 @@ func (gui *Gui) refreshCommitFilesView() error { return err } - if err := gui.refreshPatchBuildingPanel(); err != nil { + if err := gui.refreshPatchBuildingPanel(-1); err != nil { return err } @@ -177,16 +187,20 @@ func (gui *Gui) startPatchManager() error { } func (gui *Gui) handleEnterCommitFile(g *gocui.Gui, v *gocui.View) error { + return gui.enterCommitFile(-1) +} + +func (gui *Gui) enterCommitFile(selectedLineIdx int) error { if ok, err := gui.validateNormalWorkingTreeState(); !ok { return err } - commitFile := gui.getSelectedCommitFile(g) + commitFile := gui.getSelectedCommitFile(gui.g) if commitFile == nil { - return gui.renderString(g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles")) + return gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles")) } - enterTheFile := func() error { + enterTheFile := func(selectedLineIdx int) error { if !gui.GitCommand.PatchManager.CommitSelected() { if err := gui.startPatchManager(); err != nil { return err @@ -196,18 +210,21 @@ func (gui *Gui) handleEnterCommitFile(g *gocui.Gui, v *gocui.View) error { if err := gui.changeContext("main", "patch-building"); err != nil { return err } - if err := gui.switchFocus(g, v, gui.getMainView()); err != nil { + if err := gui.changeContext("secondary", "patch-building"); err != nil { return err } - return gui.refreshPatchBuildingPanel() + if err := gui.switchFocus(gui.g, gui.getCommitFilesView(), gui.getMainView()); err != nil { + return err + } + return gui.refreshPatchBuildingPanel(selectedLineIdx) } if gui.GitCommand.PatchManager.CommitSelected() && gui.GitCommand.PatchManager.CommitSha != commitFile.Sha { - return gui.createConfirmationPanel(g, v, false, gui.Tr.SLocalize("DiscardPatch"), gui.Tr.SLocalize("DiscardPatchConfirm"), func(g *gocui.Gui, v *gocui.View) error { + return gui.createConfirmationPanel(gui.g, gui.getCommitFilesView(), false, gui.Tr.SLocalize("DiscardPatch"), gui.Tr.SLocalize("DiscardPatchConfirm"), func(g *gocui.Gui, v *gocui.View) error { gui.GitCommand.PatchManager.Reset() - return enterTheFile() + return enterTheFile(selectedLineIdx) }, nil) } - return enterTheFile() + return enterTheFile(selectedLineIdx) } diff --git a/pkg/gui/commits_panel.go b/pkg/gui/commits_panel.go index ee4841aab..ecc1a95ef 100644 --- a/pkg/gui/commits_panel.go +++ b/pkg/gui/commits_panel.go @@ -23,6 +23,14 @@ func (gui *Gui) getSelectedCommit(g *gocui.Gui) *commands.Commit { return gui.State.Commits[selectedLine] } +func (gui *Gui) handleCommitsClick(g *gocui.Gui, v *gocui.View) error { + itemCount := len(gui.State.Commits) + handleSelect := gui.handleCommitSelect + selectedLine := &gui.State.Panels.Commits.SelectedLine + + return gui.handleClick(v, itemCount, selectedLine, handleSelect) +} + func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error { if gui.popupPanelFocused() { return nil @@ -36,6 +44,10 @@ func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error { if _, err := gui.g.SetCurrentView(v.Name()); err != nil { return err } + + gui.getMainView().Title = "Patch" + gui.getSecondaryView().Title = "Custom Patch" + commit := gui.getSelectedCommit(g) if commit == nil { return gui.renderString(g, "main", gui.Tr.SLocalize("NoCommitsThisBranch")) @@ -458,7 +470,7 @@ func (gui *Gui) handleSwitchToCommitFilesPanel(g *gocui.Gui, v *gocui.View) erro return err } - return gui.switchFocus(g, v, gui.getCommitFilesView()) + return gui.switchFocus(g, gui.getCommitsView(), gui.getCommitFilesView()) } func (gui *Gui) handleToggleDiffCommit(g *gocui.Gui, v *gocui.View) error { diff --git a/pkg/gui/context.go b/pkg/gui/context.go index 38bf7c225..fd8877ba0 100644 --- a/pkg/gui/context.go +++ b/pkg/gui/context.go @@ -1,44 +1,5 @@ package gui -func (gui *Gui) titleMap() map[string]string { - return map[string]string{ - "commits": gui.Tr.SLocalize("DiffTitle"), - "branches": gui.Tr.SLocalize("LogTitle"), - "files": gui.Tr.SLocalize("DiffTitle"), - "status": "", - "stash": gui.Tr.SLocalize("DiffTitle"), - } -} - -func (gui *Gui) contextTitleMap() map[string]map[string]string { - return map[string]map[string]string{ - "main": { - "staging": gui.Tr.SLocalize("StagingMainTitle"), - "patch-building": gui.Tr.SLocalize("PatchBuildingMainTitle"), - "merging": gui.Tr.SLocalize("MergingMainTitle"), - "normal": "", - }, - } -} - -func (gui *Gui) setMainTitle() error { - currentView := gui.g.CurrentView() - if currentView == nil { - return nil - } - currentViewName := currentView.Name() - var newTitle string - if context, ok := gui.State.Contexts[currentViewName]; ok { - newTitle = gui.contextTitleMap()[currentViewName][context] - } else if title, ok := gui.titleMap()[currentViewName]; ok { - newTitle = title - } else { - return nil - } - gui.getMainView().Title = newTitle - return nil -} - func (gui *Gui) changeContext(viewName, context string) error { if gui.State.Contexts[viewName] == context { return nil @@ -50,19 +11,20 @@ func (gui *Gui) changeContext(viewName, context string) error { bindings := contextMap[viewName][context] for _, binding := range bindings { - if err := gui.g.SetKeybinding(viewName, binding.Key, binding.Modifier, binding.Handler); err != nil { + if err := gui.g.SetKeybinding(binding.ViewName, binding.Key, binding.Modifier, binding.Handler); err != nil { return err } } gui.State.Contexts[viewName] = context - return gui.setMainTitle() + return nil } func (gui *Gui) setInitialContexts() error { contextMap := gui.GetContextMap() initialContexts := map[string]string{ - "main": "normal", + "main": "normal", + "secondary": "normal", } for viewName, context := range initialContexts { diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go index f73d84cdb..6b8847e8c 100644 --- a/pkg/gui/files_panel.go +++ b/pkg/gui/files_panel.go @@ -27,24 +27,21 @@ func (gui *Gui) getSelectedFile(g *gocui.Gui) (*commands.File, error) { return gui.State.Files[selectedLine], nil } -func (gui *Gui) handleFilesFocus(g *gocui.Gui, v *gocui.View) error { +func (gui *Gui) handleFilesClick(g *gocui.Gui, v *gocui.View) error { if gui.popupPanelFocused() { return nil } - cx, cy := v.Cursor() - _, oy := v.Origin() + prevSelectedLineIdx := gui.State.Panels.Files.SelectedLine + newSelectedLineIdx := v.SelectedLineIdx() - prevSelectedLine := gui.State.Panels.Files.SelectedLine - newSelectedLine := cy - oy - - if newSelectedLine > len(gui.State.Files)-1 || len(utils.Decolorise(gui.State.Files[newSelectedLine].DisplayString)) < cx { + if newSelectedLineIdx > len(gui.State.Files)-1 { return gui.handleFileSelect(gui.g, v, false) } - gui.State.Panels.Files.SelectedLine = newSelectedLine + gui.State.Panels.Files.SelectedLine = newSelectedLineIdx - if prevSelectedLine == newSelectedLine && gui.currentViewName() == v.Name() { + if prevSelectedLineIdx == newSelectedLineIdx && gui.currentViewName() == v.Name() { return gui.handleFilePress(gui.g, v) } else { return gui.handleFileSelect(gui.g, v, true) @@ -77,12 +74,16 @@ func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View, alreadySelected bo leftContent := content if file.HasStagedChanges && file.HasUnstagedChanges { gui.State.SplitMainPanel = true + gui.getMainView().Title = gui.Tr.SLocalize("UnstagedChanges") + gui.getSecondaryView().Title = gui.Tr.SLocalize("StagedChanges") } else { gui.State.SplitMainPanel = false if file.HasUnstagedChanges { leftContent = content + gui.getMainView().Title = gui.Tr.SLocalize("UnstagedChanges") } else { leftContent = contentCached + gui.getMainView().Title = gui.Tr.SLocalize("StagedChanges") } } @@ -189,7 +190,11 @@ func (gui *Gui) stageSelectedFile(g *gocui.Gui) error { } func (gui *Gui) handleEnterFile(g *gocui.Gui, v *gocui.View) error { - file, err := gui.getSelectedFile(g) + return gui.enterFile(false, -1) +} + +func (gui *Gui) enterFile(forceSecondaryFocused bool, selectedLineIdx int) error { + file, err := gui.getSelectedFile(gui.g) if err != nil { if err != gui.Errors.ErrNoFiles { return err @@ -197,18 +202,21 @@ func (gui *Gui) handleEnterFile(g *gocui.Gui, v *gocui.View) error { return nil } if file.HasInlineMergeConflicts { - return gui.handleSwitchToMerge(g, v) + return gui.handleSwitchToMerge(gui.g, gui.getFilesView()) } if file.HasMergeConflicts { - return gui.createErrorPanel(g, gui.Tr.SLocalize("FileStagingRequirements")) + return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("FileStagingRequirements")) } if err := gui.changeContext("main", "staging"); err != nil { return err } - if err := gui.switchFocus(g, v, gui.getMainView()); err != nil { + if err := gui.changeContext("secondary", "staging"); err != nil { return err } - return gui.refreshStagingPanel() + if err := gui.switchFocus(gui.g, gui.getFilesView(), gui.getMainView()); err != nil { + return err + } + return gui.refreshStagingPanel(forceSecondaryFocused, selectedLineIdx) } func (gui *Gui) handleFilePress(g *gocui.Gui, v *gocui.View) error { diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 8a14fde36..b69b30965 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -118,12 +118,18 @@ type stashPanelState struct { type menuPanelState struct { SelectedLine int + OnPress func(g *gocui.Gui, v *gocui.View) error } type commitFilesPanelState struct { SelectedLine int } +type statusPanelState struct { + pushables string + pullables string +} + type panelStates struct { Files *filePanelState Branches *branchPanelState @@ -133,6 +139,7 @@ type panelStates struct { LineByLine *lineByLinePanelState Merging *mergingPanelState CommitFiles *commitFilesPanelState + Status *statusPanelState } type guiState struct { @@ -179,6 +186,7 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *comma Conflicts: []commands.Conflict{}, EditHistory: stack.New(), }, + Status: &statusPanelState{}, }, } @@ -257,7 +265,7 @@ func (gui *Gui) onFocusChange() error { for _, view := range gui.g.Views() { view.Highlight = view == currentView } - return gui.setMainTitle() + return nil } func (gui *Gui) onFocusLost(v *gocui.View, newView *gocui.View) error { @@ -683,6 +691,8 @@ func (gui *Gui) Run() error { } defer g.Close() + g.Log = gui.Log + if gui.Config.GetUserConfig().GetBool("gui.mouseEvents") { g.Mouse = true } @@ -795,3 +805,31 @@ func (gui *Gui) setColorScheme() error { return nil } + +func (gui *Gui) handleMouseDownMain(g *gocui.Gui, v *gocui.View) error { + if gui.popupPanelFocused() { + return nil + } + + switch g.CurrentView().Name() { + case "files": + return gui.enterFile(false, v.SelectedLineIdx()) + case "commitFiles": + return gui.enterCommitFile(v.SelectedLineIdx()) + } + + return nil +} + +func (gui *Gui) handleMouseDownSecondary(g *gocui.Gui, v *gocui.View) error { + if gui.popupPanelFocused() { + return nil + } + + switch g.CurrentView().Name() { + case "files": + return gui.enterFile(true, v.SelectedLineIdx()) + } + + return nil +} diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 4bd28c74c..c0d9fc52c 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -144,6 +144,11 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Key: 'x', Modifier: gocui.ModNone, Handler: gui.handleCreateOptionsMenu, + }, { + ViewName: "", + Key: gocui.MouseMiddle, + Modifier: gocui.ModNone, + Handler: gui.handleCreateOptionsMenu, }, { ViewName: "", Key: gocui.KeyCtrlP, @@ -558,15 +563,15 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { listPanelMap := map[string]struct { prevLine func(*gocui.Gui, *gocui.View) error nextLine func(*gocui.Gui, *gocui.View) error - focus func(*gocui.Gui, *gocui.View) error + onClick func(*gocui.Gui, *gocui.View) error }{ - "menu": {prevLine: gui.handleMenuPrevLine, nextLine: gui.handleMenuNextLine, focus: gui.handleMenuSelect}, - "files": {prevLine: gui.handleFilesPrevLine, nextLine: gui.handleFilesNextLine, focus: gui.handleFilesFocus}, - "branches": {prevLine: gui.handleBranchesPrevLine, nextLine: gui.handleBranchesNextLine, focus: gui.handleBranchSelect}, - "commits": {prevLine: gui.handleCommitsPrevLine, nextLine: gui.handleCommitsNextLine, focus: gui.handleCommitSelect}, - "stash": {prevLine: gui.handleStashPrevLine, nextLine: gui.handleStashNextLine, focus: gui.handleStashEntrySelect}, - "status": {focus: gui.handleStatusSelect}, - "commitFiles": {prevLine: gui.handleCommitFilesPrevLine, nextLine: gui.handleCommitFilesNextLine, focus: gui.handleCommitFileSelect}, + "menu": {prevLine: gui.handleMenuPrevLine, nextLine: gui.handleMenuNextLine, onClick: gui.handleMenuClick}, + "files": {prevLine: gui.handleFilesPrevLine, nextLine: gui.handleFilesNextLine, onClick: gui.handleFilesClick}, + "branches": {prevLine: gui.handleBranchesPrevLine, nextLine: gui.handleBranchesNextLine, onClick: gui.handleBranchesClick}, + "commits": {prevLine: gui.handleCommitsPrevLine, nextLine: gui.handleCommitsNextLine, onClick: gui.handleCommitsClick}, + "stash": {prevLine: gui.handleStashPrevLine, nextLine: gui.handleStashNextLine, onClick: gui.handleStashEntrySelect}, + "status": {onClick: gui.handleStatusClick}, + "commitFiles": {prevLine: gui.handleCommitFilesPrevLine, nextLine: gui.handleCommitFilesNextLine, onClick: gui.handleCommitFilesClick}, } for viewName, functions := range listPanelMap { @@ -577,7 +582,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { {ViewName: viewName, Key: 'j', Modifier: gocui.ModNone, Handler: functions.nextLine}, {ViewName: viewName, Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: functions.nextLine}, {ViewName: viewName, Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, Handler: functions.nextLine}, - {ViewName: viewName, Key: gocui.MouseLeft, Modifier: gocui.ModNone, Handler: functions.focus}, + {ViewName: viewName, Key: gocui.MouseLeft, Modifier: gocui.ModNone, Handler: functions.onClick}, }...) } @@ -610,6 +615,24 @@ func (gui *Gui) keybindings(g *gocui.Gui) error { func (gui *Gui) GetContextMap() map[string]map[string][]*Binding { return map[string]map[string][]*Binding{ + "secondary": { + "normal": { + { + ViewName: "secondary", + Key: gocui.MouseLeft, + Modifier: gocui.ModNone, + Handler: gui.handleMouseDownSecondary, + }, + }, + "staging": { + { + ViewName: "secondary", + Key: gocui.MouseLeft, + Modifier: gocui.ModNone, + Handler: gui.handleTogglePanelClick, + }, + }, + }, "main": { "normal": { { @@ -626,6 +649,11 @@ func (gui *Gui) GetContextMap() map[string]map[string][]*Binding { Handler: gui.scrollUpMain, Description: gui.Tr.SLocalize("ScrollUp"), Alternative: "fn+down", + }, { + ViewName: "main", + Key: gocui.MouseLeft, + Modifier: gocui.ModNone, + Handler: gui.handleMouseDownMain, }, }, "staging": { @@ -657,16 +685,6 @@ func (gui *Gui) GetContextMap() map[string]map[string][]*Binding { Key: 'j', Modifier: gocui.ModNone, Handler: gui.handleSelectNextLine, - }, { - ViewName: "main", - Key: gocui.MouseWheelUp, - Modifier: gocui.ModNone, - Handler: gui.handleSelectPrevLine, - }, { - ViewName: "main", - Key: gocui.MouseWheelDown, - Modifier: gocui.ModNone, - Handler: gui.handleSelectNextLine, }, { ViewName: "main", Key: gocui.KeyArrowLeft, @@ -719,6 +737,26 @@ func (gui *Gui) GetContextMap() map[string]map[string][]*Binding { Modifier: gocui.ModNone, Handler: gui.handleTogglePanel, Description: gui.Tr.SLocalize("TogglePanel"), + }, { + ViewName: "main", + Key: gocui.MouseLeft, + Modifier: gocui.ModNone, + Handler: gui.handleMouseDown, + }, { + ViewName: "main", + Key: gocui.MouseLeft, + Modifier: gocui.ModMotion, + Handler: gui.handleMouseDrag, + }, { + ViewName: "main", + Key: gocui.MouseWheelUp, + Modifier: gocui.ModNone, + Handler: gui.handleMouseScrollUp, + }, { + ViewName: "main", + Key: gocui.MouseWheelDown, + Modifier: gocui.ModNone, + Handler: gui.handleMouseScrollDown, }, }, "patch-building": { @@ -806,6 +844,26 @@ func (gui *Gui) GetContextMap() map[string]map[string][]*Binding { Modifier: gocui.ModNone, Handler: gui.handleToggleSelectHunk, Description: gui.Tr.SLocalize("ToggleSelectHunk"), + }, { + ViewName: "main", + Key: gocui.MouseLeft, + Modifier: gocui.ModNone, + Handler: gui.handleMouseDown, + }, { + ViewName: "main", + Key: gocui.MouseLeft, + Modifier: gocui.ModMotion, + Handler: gui.handleMouseDrag, + }, { + ViewName: "main", + Key: gocui.MouseWheelUp, + Modifier: gocui.ModNone, + Handler: gui.handleMouseScrollUp, + }, { + ViewName: "main", + Key: gocui.MouseWheelDown, + Modifier: gocui.ModNone, + Handler: gui.handleMouseScrollDown, }, }, "merging": { diff --git a/pkg/gui/line_by_line_panel.go b/pkg/gui/line_by_line_panel.go index f36441823..3ad105b0c 100644 --- a/pkg/gui/line_by_line_panel.go +++ b/pkg/gui/line_by_line_panel.go @@ -21,7 +21,7 @@ const ( // returns whether the patch is empty so caller can escape if necessary // both diffs should be non-coloured because we'll parse them and colour them here -func (gui *Gui) refreshLineByLinePanel(diff string, secondaryDiff string, secondaryFocused bool) (bool, error) { +func (gui *Gui) refreshLineByLinePanel(diff string, secondaryDiff string, secondaryFocused bool, selectedLineIdx int) (bool, error) { state := gui.State.Panels.LineByLine patchParser, err := commands.NewPatchParser(gui.Log, diff) @@ -33,11 +33,14 @@ func (gui *Gui) refreshLineByLinePanel(diff string, secondaryDiff string, second return true, nil } - var selectedLineIdx int var firstLineIdx int var lastLineIdx int selectMode := LINE - if state != nil { + // if we have clicked from the outside to focus the main view we'll pass in a non-negative line index so that we can instantly select that line + if selectedLineIdx >= 0 { + selectMode = RANGE + firstLineIdx, lastLineIdx = selectedLineIdx, selectedLineIdx + } else if state != nil { if state.SelectMode == HUNK { // this is tricky: we need to find out which hunk we just staged based on our old `state.PatchParser` (as opposed to the new `patchParser`) // we do this by getting the first line index of the original hunk, then @@ -96,20 +99,25 @@ func (gui *Gui) handleSelectPrevLine(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleSelectNextLine(g *gocui.Gui, v *gocui.View) error { - return gui.handleCycleLine(1) + return gui.handleCycleLine(+1) } func (gui *Gui) handleSelectPrevHunk(g *gocui.Gui, v *gocui.View) error { - return gui.handleCycleHunk(-1) + state := gui.State.Panels.LineByLine + newHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, -1) + + return gui.selectNewHunk(newHunk) } func (gui *Gui) handleSelectNextHunk(g *gocui.Gui, v *gocui.View) error { - return gui.handleCycleHunk(1) + state := gui.State.Panels.LineByLine + newHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 1) + + return gui.selectNewHunk(newHunk) } -func (gui *Gui) handleCycleHunk(change int) error { +func (gui *Gui) selectNewHunk(newHunk *commands.PatchHunk) error { state := gui.State.Panels.LineByLine - newHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, change) state.SelectedLineIdx = state.PatchParser.GetNextStageableLineIndex(newHunk.FirstLineIdx) if state.SelectMode == HUNK { state.FirstLineIdx, state.LastLineIdx = newHunk.FirstLineIdx, newHunk.LastLineIdx @@ -128,10 +136,16 @@ func (gui *Gui) handleCycleLine(change int) error { state := gui.State.Panels.LineByLine if state.SelectMode == HUNK { - return gui.handleCycleHunk(change) + newHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, change) + return gui.selectNewHunk(newHunk) } - newSelectedLineIdx := state.SelectedLineIdx + change + return gui.handleSelectNewLine(state.SelectedLineIdx + change) +} + +func (gui *Gui) handleSelectNewLine(newSelectedLineIdx int) error { + state := gui.State.Panels.LineByLine + if newSelectedLineIdx < 0 { newSelectedLineIdx = 0 } else if newSelectedLineIdx > len(state.PatchParser.PatchLines)-1 { @@ -158,6 +172,54 @@ func (gui *Gui) handleCycleLine(change int) error { return gui.focusSelection(false) } +func (gui *Gui) handleMouseDown(g *gocui.Gui, v *gocui.View) error { + state := gui.State.Panels.LineByLine + + if gui.popupPanelFocused() { + return nil + } + + newSelectedLineIdx := v.SelectedLineIdx() + state.FirstLineIdx = newSelectedLineIdx + state.LastLineIdx = newSelectedLineIdx + + state.SelectMode = RANGE + + return gui.handleSelectNewLine(newSelectedLineIdx) +} + +func (gui *Gui) handleMouseDrag(g *gocui.Gui, v *gocui.View) error { + if gui.popupPanelFocused() { + return nil + } + + return gui.handleSelectNewLine(v.SelectedLineIdx()) +} + +func (gui *Gui) handleMouseScrollUp(g *gocui.Gui, v *gocui.View) error { + state := gui.State.Panels.LineByLine + + if gui.popupPanelFocused() { + return nil + } + + state.SelectMode = LINE + + return gui.handleCycleLine(-1) +} + +func (gui *Gui) handleMouseScrollDown(g *gocui.Gui, v *gocui.View) error { + state := gui.State.Panels.LineByLine + + if gui.popupPanelFocused() { + return nil + } + + state.SelectMode = LINE + + return gui.handleCycleLine(1) +} + func (gui *Gui) refreshMainView() error { state := gui.State.Panels.LineByLine diff --git a/pkg/gui/menu_panel.go b/pkg/gui/menu_panel.go index 2d08a010b..f15b99d76 100644 --- a/pkg/gui/menu_panel.go +++ b/pkg/gui/menu_panel.go @@ -82,7 +82,9 @@ func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handl return gui.returnFocus(gui.g, menuView) } - for _, key := range []gocui.Key{gocui.KeySpace, gocui.KeyEnter} { + gui.State.Panels.Menu.OnPress = wrappedHandlePress + + for _, key := range []gocui.Key{gocui.KeySpace, gocui.KeyEnter, 'y'} { _ = gui.g.DeleteKeybinding("menu", key, gocui.ModNone) if err := gui.g.SetKeybinding("menu", key, gocui.ModNone, wrappedHandlePress); err != nil { @@ -101,3 +103,15 @@ func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handl }) return nil } + +func (gui *Gui) handleMenuClick(g *gocui.Gui, v *gocui.View) error { + itemCount := gui.State.MenuItemCount + handleSelect := gui.handleMenuSelect + selectedLine := &gui.State.Panels.Menu.SelectedLine + + if err := gui.handleClick(v, itemCount, selectedLine, handleSelect); err != nil { + return err + } + + return gui.State.Panels.Menu.OnPress(g, v) +} diff --git a/pkg/gui/patch_building_panel.go b/pkg/gui/patch_building_panel.go index 5667b45da..a4b4de3eb 100644 --- a/pkg/gui/patch_building_panel.go +++ b/pkg/gui/patch_building_panel.go @@ -4,13 +4,16 @@ import ( "github.com/jesseduffield/gocui" ) -func (gui *Gui) refreshPatchBuildingPanel() error { +func (gui *Gui) refreshPatchBuildingPanel(selectedLineIdx int) error { if !gui.GitCommand.PatchManager.CommitSelected() { return gui.handleEscapePatchBuildingPanel(gui.g, nil) } gui.State.SplitMainPanel = true + gui.getMainView().Title = "Patch" + gui.getSecondaryView().Title = "Custom Patch" + // get diff from commit file that's currently selected commitFile := gui.getSelectedCommitFile(gui.g) if commitFile == nil { @@ -27,7 +30,7 @@ func (gui *Gui) refreshPatchBuildingPanel() error { return err } - empty, err := gui.refreshLineByLinePanel(diff, secondaryDiff, false) + empty, err := gui.refreshLineByLinePanel(diff, secondaryDiff, false, selectedLineIdx) if err != nil { return err } @@ -54,7 +57,7 @@ func (gui *Gui) handleAddSelectionToPatch(g *gocui.Gui, v *gocui.View) error { return err } - if err := gui.refreshPatchBuildingPanel(); err != nil { + if err := gui.refreshPatchBuildingPanel(-1); err != nil { return err } @@ -76,7 +79,7 @@ func (gui *Gui) handleRemoveSelectionFromPatch(g *gocui.Gui, v *gocui.View) erro return err } - if err := gui.refreshPatchBuildingPanel(); err != nil { + if err := gui.refreshPatchBuildingPanel(-1); err != nil { return err } diff --git a/pkg/gui/rebase_options_panel.go b/pkg/gui/rebase_options_panel.go index bc3dd13f0..bf29d19c3 100644 --- a/pkg/gui/rebase_options_panel.go +++ b/pkg/gui/rebase_options_panel.go @@ -26,8 +26,13 @@ func (gui *Gui) handleCreateRebaseOptionsMenu(g *gocui.Gui, v *gocui.View) error options = append(options, &option{value: "skip"}) } + options = append(options, &option{value: "cancel"}) + handleMenuPress := func(index int) error { command := options[index].value + if command == "cancel" { + return nil + } return gui.genericMergeCommand(command) } diff --git a/pkg/gui/staging_panel.go b/pkg/gui/staging_panel.go index 35692757c..02fab93b1 100644 --- a/pkg/gui/staging_panel.go +++ b/pkg/gui/staging_panel.go @@ -7,7 +7,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands" ) -func (gui *Gui) refreshStagingPanel() error { +func (gui *Gui) refreshStagingPanel(forceSecondaryFocused bool, selectedLineIdx int) error { gui.State.SplitMainPanel = true state := gui.State.Panels.LineByLine @@ -25,14 +25,26 @@ func (gui *Gui) refreshStagingPanel() error { } secondaryFocused := false - if state != nil { - secondaryFocused = state.SecondaryFocused + if forceSecondaryFocused { + secondaryFocused = true + } else { + if state != nil { + secondaryFocused = state.SecondaryFocused + } } if (secondaryFocused && !file.HasStagedChanges) || (!secondaryFocused && !file.HasUnstagedChanges) { secondaryFocused = !secondaryFocused } + if secondaryFocused { + gui.getMainView().Title = gui.Tr.SLocalize("StagedChanges") + gui.getSecondaryView().Title = gui.Tr.SLocalize("UnstagedChanges") + } else { + gui.getMainView().Title = gui.Tr.SLocalize("UnstagedChanges") + gui.getSecondaryView().Title = gui.Tr.SLocalize("StagedChanges") + } + // note for custom diffs, we'll need to send a flag here saying not to use the custom diff diff := gui.GitCommand.Diff(file, true, secondaryFocused) secondaryDiff := gui.GitCommand.Diff(file, true, !secondaryFocused) @@ -47,7 +59,7 @@ func (gui *Gui) refreshStagingPanel() error { diff, secondaryDiff = secondaryDiff, diff } - empty, err := gui.refreshLineByLinePanel(diff, secondaryDiff, secondaryFocused) + empty, err := gui.refreshLineByLinePanel(diff, secondaryDiff, secondaryFocused, selectedLineIdx) if err != nil { return err } @@ -59,11 +71,19 @@ func (gui *Gui) refreshStagingPanel() error { return nil } +func (gui *Gui) handleTogglePanelClick(g *gocui.Gui, v *gocui.View) error { + state := gui.State.Panels.LineByLine + + state.SecondaryFocused = !state.SecondaryFocused + + return gui.refreshStagingPanel(false, v.SelectedLineIdx()) +} + func (gui *Gui) handleTogglePanel(g *gocui.Gui, v *gocui.View) error { state := gui.State.Panels.LineByLine state.SecondaryFocused = !state.SecondaryFocused - return gui.refreshStagingPanel() + return gui.refreshStagingPanel(false, -1) } func (gui *Gui) handleStagingEscape(g *gocui.Gui, v *gocui.View) error { @@ -116,8 +136,16 @@ func (gui *Gui) applySelection(reverse bool) error { if err := gui.refreshFiles(); err != nil { return err } - if err := gui.refreshStagingPanel(); err != nil { + if err := gui.refreshStagingPanel(false, -1); err != nil { return err } return nil } + +func (gui *Gui) handleMouseDownSecondaryWhileStaging(g *gocui.Gui, v *gocui.View) error { + state := gui.State.Panels.LineByLine + + state.SecondaryFocused = !state.SecondaryFocused + + return gui.refreshStagingPanel(false, -1) +} diff --git a/pkg/gui/stash_panel.go b/pkg/gui/stash_panel.go index 8ac7b6cd1..4ed35489d 100644 --- a/pkg/gui/stash_panel.go +++ b/pkg/gui/stash_panel.go @@ -29,6 +29,9 @@ func (gui *Gui) handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error { if _, err := gui.g.SetCurrentView(v.Name()); err != nil { return err } + + gui.getMainView().Title = "Stash" + stashEntry := gui.getSelectedStashEntry(v) if stashEntry == nil { return gui.renderString(g, "main", gui.Tr.SLocalize("NoStashEntries")) diff --git a/pkg/gui/status_panel.go b/pkg/gui/status_panel.go index ff558f82d..6ad1c9d56 100644 --- a/pkg/gui/status_panel.go +++ b/pkg/gui/status_panel.go @@ -10,6 +10,8 @@ import ( ) func (gui *Gui) refreshStatus(g *gocui.Gui) error { + state := gui.State.Panels.Status + v, err := g.View("status") if err != nil { panic(err) @@ -19,34 +21,69 @@ func (gui *Gui) refreshStatus(g *gocui.Gui) error { // contents end up cleared g.Update(func(*gocui.Gui) error { v.Clear() - pushables, pullables := gui.GitCommand.GetCurrentBranchUpstreamDifferenceCount() - fmt.Fprint(v, "↑"+pushables+"↓"+pullables) - branches := gui.State.Branches + state.pushables, state.pullables = gui.GitCommand.GetCurrentBranchUpstreamDifferenceCount() if err := gui.updateWorkTreeState(); err != nil { return err } + status := fmt.Sprintf("↑%s↓%s", state.pushables, state.pullables) + branches := gui.State.Branches + if gui.State.WorkingTreeState != "normal" { - fmt.Fprint(v, utils.ColoredString(fmt.Sprintf(" (%s)", gui.State.WorkingTreeState), color.FgYellow)) + status += utils.ColoredString(fmt.Sprintf(" (%s)", gui.State.WorkingTreeState), color.FgYellow) } - if len(branches) == 0 { - return nil + if len(branches) > 0 { + branch := branches[0] + name := utils.ColoredString(branch.Name, branch.GetColor()) + repoName := utils.GetCurrentRepoName() + status += fmt.Sprintf(" %s → %s", repoName, name) } - branch := branches[0] - name := utils.ColoredString(branch.Name, branch.GetColor()) - repo := utils.GetCurrentRepoName() - fmt.Fprint(v, " "+repo+" → "+name) + + fmt.Fprint(v, status) return nil }) return nil } +func runeCount(str string) int { + return len([]rune(str)) +} + +func cursorInSubstring(cx int, prefix string, substring string) bool { + return cx >= runeCount(prefix) && cx < runeCount(prefix+substring) +} + func (gui *Gui) handleCheckForUpdate(g *gocui.Gui, v *gocui.View) error { gui.Updater.CheckForNewUpdate(gui.onUserUpdateCheckFinish, true) return gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("CheckingForUpdates")) } +func (gui *Gui) handleStatusClick(g *gocui.Gui, v *gocui.View) error { + state := gui.State.Panels.Status + + cx, _ := v.Cursor() + upstreamStatus := fmt.Sprintf("↑%s↓%s", state.pushables, state.pullables) + repoName := utils.GetCurrentRepoName() + gui.Log.Warn(gui.State.WorkingTreeState) + switch gui.State.WorkingTreeState { + case "rebasing", "merging": + workingTreeStatus := fmt.Sprintf("(%s)", gui.State.WorkingTreeState) + if cursorInSubstring(cx, upstreamStatus+" ", workingTreeStatus) { + return gui.handleCreateRebaseOptionsMenu(gui.g, v) + } + if cursorInSubstring(cx, upstreamStatus+" "+workingTreeStatus+" ", repoName) { + return gui.handleCreateRecentReposMenu(gui.g, v) + } + default: + if cursorInSubstring(cx, upstreamStatus+" ", repoName) { + return gui.handleCreateRecentReposMenu(gui.g, v) + } + } + + return gui.handleStatusSelect(gui.g, v) +} + func (gui *Gui) handleStatusSelect(g *gocui.Gui, v *gocui.View) error { if gui.popupPanelFocused() { return nil @@ -57,6 +94,9 @@ func (gui *Gui) handleStatusSelect(g *gocui.Gui, v *gocui.View) error { if _, err := gui.g.SetCurrentView(v.Name()); err != nil { return err } + + gui.getMainView().Title = "" + magenta := color.New(color.FgMagenta) dashboardString := strings.Join( @@ -67,7 +107,7 @@ func (gui *Gui) handleStatusSelect(g *gocui.Gui, v *gocui.View) error { "Config Options: https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md", "Tutorial: https://youtu.be/VDXvbHZYeKY", "Raise an Issue: https://github.com/jesseduffield/lazygit/issues", - magenta.Sprint("Buy Jesse a coffee: https://donorbox.org/lazygit"), // caffeine ain't free + magenta.Sprint("Become a sponsor (github is matching all donations for 12 months): https://github.com/sponsors/jesseduffield"), // caffeine ain't free }, "\n\n") return gui.renderString(g, "main", dashboardString) diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index 525100c77..29180418c 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -425,3 +425,27 @@ func (gui *Gui) isPopupPanel(viewName string) bool { func (gui *Gui) popupPanelFocused() bool { return gui.isPopupPanel(gui.currentViewName()) } + +func (gui *Gui) handleClick(v *gocui.View, itemCount int, selectedLine *int, handleSelect func(*gocui.Gui, *gocui.View) error) error { + if gui.popupPanelFocused() && v != nil && !gui.isPopupPanel(v.Name()) { + return nil + } + + if _, err := gui.g.SetCurrentView(v.Name()); err != nil { + return err + } + + newSelectedLine := v.SelectedLineIdx() + + if newSelectedLine < 0 { + newSelectedLine = 0 + } + + if newSelectedLine > itemCount-1 { + newSelectedLine = itemCount - 1 + } + + *selectedLine = newSelectedLine + + return handleSelect(gui.g, v) +} diff --git a/pkg/i18n/dutch.go b/pkg/i18n/dutch.go index 43b36a8f0..b68acf54f 100644 --- a/pkg/i18n/dutch.go +++ b/pkg/i18n/dutch.go @@ -38,8 +38,11 @@ func addDutch(i18nObject *i18n.Bundle) error { ID: "StashTitle", Other: "Stash", }, &i18n.Message{ - ID: "StagingMainTitle", - Other: `Stage Lines/Hunks`, + ID: "UnstagedChanges", + Other: `Unstaged Changes`, + }, &i18n.Message{ + ID: "StagedChanges", + Other: `Staged Changes`, }, &i18n.Message{ ID: "MergingMainTitle", Other: "Resolve merge conflicts", diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 856fd7d85..3d19c0e8a 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -46,8 +46,11 @@ func addEnglish(i18nObject *i18n.Bundle) error { ID: "StashTitle", Other: "Stash", }, &i18n.Message{ - ID: "StagingMainTitle", - Other: `Stage Lines/Hunks`, + ID: "UnstagedChanges", + Other: `Unstaged Changes`, + }, &i18n.Message{ + ID: "StagedChanges", + Other: `Staged Changes`, }, &i18n.Message{ ID: "PatchBuildingMainTitle", Other: `Add Lines/Hunks To Patch`, diff --git a/pkg/i18n/polish.go b/pkg/i18n/polish.go index 21c1f2231..8100cb6ba 100644 --- a/pkg/i18n/polish.go +++ b/pkg/i18n/polish.go @@ -36,8 +36,11 @@ func addPolish(i18nObject *i18n.Bundle) error { ID: "StashTitle", Other: "Schowek", }, &i18n.Message{ - ID: "StagingMainTitle", - Other: `Stage Lines/Hunks`, + ID: "UnstagedChanges", + Other: `Unstaged Changes`, + }, &i18n.Message{ + ID: "StagedChanges", + Other: `Staged Changes`, }, &i18n.Message{ ID: "MergingMainTitle", Other: "Resolve merge conflicts",