1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2024-12-12 11:15:00 +02:00

Merge pull request #1925 from jesseduffield/details-view

This commit is contained in:
Jesse Duffield 2022-05-08 13:24:51 +10:00 committed by GitHub
commit 8247089e53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 382 additions and 330 deletions

View File

@ -13,6 +13,84 @@ import (
const INFO_SECTION_PADDING = " " const INFO_SECTION_PADDING = " "
func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions {
width, height := gui.g.Size()
sideSectionWeight, mainSectionWeight := gui.getMidSectionWeights()
sidePanelsDirection := boxlayout.COLUMN
portraitMode := width <= 84 && height > 45
if portraitMode {
sidePanelsDirection = boxlayout.ROW
}
mainPanelsDirection := boxlayout.ROW
if gui.splitMainPanelSideBySide() {
mainPanelsDirection = boxlayout.COLUMN
}
extrasWindowSize := gui.getExtrasWindowSize(height)
showInfoSection := gui.c.UserConfig.Gui.ShowBottomLine || (gui.State.Searching.isSearching || gui.isAnyModeActive())
infoSectionSize := 0
if showInfoSection {
infoSectionSize = 1
}
root := &boxlayout.Box{
Direction: boxlayout.ROW,
Children: []*boxlayout.Box{
{
Direction: sidePanelsDirection,
Weight: 1,
Children: []*boxlayout.Box{
{
Direction: boxlayout.ROW,
Weight: sideSectionWeight,
ConditionalChildren: gui.sidePanelChildren,
},
{
Direction: boxlayout.ROW,
Weight: mainSectionWeight,
Children: []*boxlayout.Box{
{
Direction: mainPanelsDirection,
Children: gui.mainSectionChildren(),
Weight: 1,
},
{
Window: "extras",
Size: extrasWindowSize,
},
},
},
},
},
{
Direction: boxlayout.COLUMN,
Size: infoSectionSize,
Children: gui.infoSectionChildren(informationStr, appStatus),
},
},
}
layerOneWindows := boxlayout.ArrangeWindows(root, 0, 0, width, height)
limitWindows := boxlayout.ArrangeWindows(&boxlayout.Box{Window: "limit"}, 0, 0, width, height)
return MergeMaps(layerOneWindows, limitWindows)
}
func MergeMaps[K comparable, V any](maps ...map[K]V) map[K]V {
result := map[K]V{}
for _, currMap := range maps {
for key, value := range currMap {
result[key] = value
}
}
return result
}
func (gui *Gui) mainSectionChildren() []*boxlayout.Box { func (gui *Gui) mainSectionChildren() []*boxlayout.Box {
currentWindow := gui.currentWindow() currentWindow := gui.currentWindow()
@ -156,70 +234,6 @@ func (gui *Gui) getExtrasWindowSize(screenHeight int) int {
return baseSize + frameSize return baseSize + frameSize
} }
func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions {
width, height := gui.g.Size()
sideSectionWeight, mainSectionWeight := gui.getMidSectionWeights()
sidePanelsDirection := boxlayout.COLUMN
portraitMode := width <= 84 && height > 45
if portraitMode {
sidePanelsDirection = boxlayout.ROW
}
mainPanelsDirection := boxlayout.ROW
if gui.splitMainPanelSideBySide() {
mainPanelsDirection = boxlayout.COLUMN
}
extrasWindowSize := gui.getExtrasWindowSize(height)
showInfoSection := gui.c.UserConfig.Gui.ShowBottomLine || (gui.State.Searching.isSearching || gui.isAnyModeActive())
infoSectionSize := 0
if showInfoSection {
infoSectionSize = 1
}
root := &boxlayout.Box{
Direction: boxlayout.ROW,
Children: []*boxlayout.Box{
{
Direction: sidePanelsDirection,
Weight: 1,
Children: []*boxlayout.Box{
{
Direction: boxlayout.ROW,
Weight: sideSectionWeight,
ConditionalChildren: gui.sidePanelChildren,
},
{
Direction: boxlayout.ROW,
Weight: mainSectionWeight,
Children: []*boxlayout.Box{
{
Direction: mainPanelsDirection,
Children: gui.mainSectionChildren(),
Weight: 1,
},
{
Window: "extras",
Size: extrasWindowSize,
},
},
},
},
},
{
Direction: boxlayout.COLUMN,
Size: infoSectionSize,
Children: gui.infoSectionChildren(informationStr, appStatus),
},
},
}
return boxlayout.ArrangeWindows(root, 0, 0, width, height)
}
// The stash window by default only contains one line so that it's not hogging // The stash window by default only contains one line so that it's not hogging
// too much space, but if you access it it should take up some space. This is // too much space, but if you access it it should take up some space. This is
// the default behaviour when accordion mode is NOT in effect. If it is in effect // the default behaviour when accordion mode is NOT in effect. If it is in effect

View File

@ -91,8 +91,7 @@ func (gui *Gui) getConfirmationPanelDimensions(wrap bool, prompt string) (int, i
return gui.getConfirmationPanelDimensionsAux(panelWidth, panelHeight) return gui.getConfirmationPanelDimensionsAux(panelWidth, panelHeight)
} }
func (gui *Gui) getConfirmationPanelDimensionsForContentHeight(contentHeight int) (int, int, int, int) { func (gui *Gui) getConfirmationPanelDimensionsForContentHeight(panelWidth, contentHeight int) (int, int, int, int) {
panelWidth := gui.getConfirmationPanelWidth()
return gui.getConfirmationPanelDimensionsAux(panelWidth, contentHeight) return gui.getConfirmationPanelDimensionsAux(panelWidth, contentHeight)
} }
@ -131,13 +130,6 @@ func (gui *Gui) prepareConfirmationPanel(
editable bool, editable bool,
mask bool, mask bool,
) error { ) error {
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(true, prompt)
// calling SetView on an existing view returns the same view, so I'm not bothering
// to reassign to gui.Views.Confirmation
_, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0)
if err != nil {
return err
}
gui.Views.Confirmation.HasLoader = hasLoader gui.Views.Confirmation.HasLoader = hasLoader
if hasLoader { if hasLoader {
gui.g.StartTicking() gui.g.StartTicking()
@ -150,11 +142,7 @@ func (gui *Gui) prepareConfirmationPanel(
gui.findSuggestions = findSuggestionsFunc gui.findSuggestions = findSuggestionsFunc
if findSuggestionsFunc != nil { if findSuggestionsFunc != nil {
suggestionsViewHeight := 11 suggestionsView := gui.Views.Suggestions
suggestionsView, err := gui.g.SetView("suggestions", x0, y1+1, x1, y1+suggestionsViewHeight, 0)
if err != nil {
return err
}
suggestionsView.Wrap = false suggestionsView.Wrap = false
suggestionsView.FgColor = theme.GocuiDefaultTextColor suggestionsView.FgColor = theme.GocuiDefaultTextColor
gui.setSuggestions(findSuggestionsFunc("")) gui.setSuggestions(findSuggestionsFunc(""))

View File

@ -19,15 +19,18 @@ var _ types.IListContext = (*MenuContext)(nil)
func NewMenuContext( func NewMenuContext(
view *gocui.View, view *gocui.View,
onFocus func(...types.OnFocusOpts) error,
onRenderToMain func(...types.OnFocusOpts) error,
onFocusLost func() error,
c *types.HelperCommon, c *types.HelperCommon,
getOptionsMap func() map[string]string, getOptionsMap func() map[string]string,
renderToDescriptionView func(string),
) *MenuContext { ) *MenuContext {
viewModel := NewMenuViewModel() viewModel := NewMenuViewModel()
onFocus := func(...types.OnFocusOpts) error {
selectedMenuItem := viewModel.GetSelected()
renderToDescriptionView(selectedMenuItem.Tooltip)
return nil
}
return &MenuContext{ return &MenuContext{
MenuViewModel: viewModel, MenuViewModel: viewModel,
ListContextTrait: &ListContextTrait{ ListContextTrait: &ListContextTrait{
@ -38,9 +41,7 @@ func NewMenuContext(
OnGetOptionsMap: getOptionsMap, OnGetOptionsMap: getOptionsMap,
Focusable: true, Focusable: true,
}), ContextCallbackOpts{ }), ContextCallbackOpts{
OnFocus: onFocus, OnFocus: onFocus,
OnFocusLost: onFocusLost,
OnRenderToMain: onRenderToMain,
}), }),
getDisplayStrings: viewModel.GetDisplayStrings, getDisplayStrings: viewModel.GetDisplayStrings,
list: viewModel, list: viewModel,

View File

@ -55,11 +55,13 @@ func (self *UndoController) GetKeybindings(opts types.KeybindingsOpts) []*types.
Key: opts.GetKey(opts.Config.Universal.Undo), Key: opts.GetKey(opts.Config.Universal.Undo),
Handler: self.reflogUndo, Handler: self.reflogUndo,
Description: self.c.Tr.LcUndoReflog, Description: self.c.Tr.LcUndoReflog,
Tooltip: self.c.Tr.UndoTooltip,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.Redo), Key: opts.GetKey(opts.Config.Universal.Redo),
Handler: self.reflogRedo, Handler: self.reflogRedo,
Description: self.c.Tr.LcRedoReflog, Description: self.c.Tr.LcRedoReflog,
Tooltip: self.c.Tr.RedoTooltip,
}, },
} }

View File

@ -31,7 +31,8 @@ func (self *FilesController) createResetMenu() error {
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}) return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
}, },
Key: 'D', Key: 'D',
Tooltip: self.c.Tr.NukeDescription,
}, },
{ {
DisplayStrings: []string{ DisplayStrings: []string{

View File

@ -223,30 +223,6 @@ type panelStates struct {
Merging *MergingPanelState Merging *MergingPanelState
} }
type Views struct {
Status *gocui.View
Files *gocui.View
Branches *gocui.View
RemoteBranches *gocui.View
Commits *gocui.View
Stash *gocui.View
Main *gocui.View
Secondary *gocui.View
Options *gocui.View
Confirmation *gocui.View
Menu *gocui.View
CommitMessage *gocui.View
CommitFiles *gocui.View
SubCommits *gocui.View
Information *gocui.View
AppStatus *gocui.View
Search *gocui.View
SearchPrefix *gocui.View
Limit *gocui.View
Suggestions *gocui.View
Extras *gocui.View
}
type searchingState struct { type searchingState struct {
view *gocui.View view *gocui.View
isSearching bool isSearching bool
@ -389,25 +365,6 @@ func (gui *Gui) syncViewContexts() {
} }
} }
func initialViewContextMapping(contextTree *context.ContextTree) map[string]types.Context {
return map[string]types.Context{
"status": contextTree.Status,
"files": contextTree.Files,
"branches": contextTree.Branches,
"remoteBranches": contextTree.RemoteBranches,
"commits": contextTree.LocalCommits,
"commitFiles": contextTree.CommitFiles,
"subCommits": contextTree.SubCommits,
"stash": contextTree.Stash,
"menu": contextTree.Menu,
"confirmation": contextTree.Confirmation,
"commitMessage": contextTree.CommitMessage,
"main": contextTree.Normal,
"secondary": contextTree.Normal,
"extras": contextTree.CommandLog,
}
}
// for now the split view will always be on // for now the split view will always be on
// NewGui builds a new gui handler // NewGui builds a new gui handler
func NewGui( func NewGui(
@ -596,121 +553,6 @@ func (gui *Gui) Run(filterPath string) error {
return gui.g.MainLoop() return gui.g.MainLoop()
} }
func (gui *Gui) createAllViews() error {
viewNameMappings := []struct {
viewPtr **gocui.View
name string
}{
{viewPtr: &gui.Views.Status, name: "status"},
{viewPtr: &gui.Views.Files, name: "files"},
{viewPtr: &gui.Views.Branches, name: "branches"},
{viewPtr: &gui.Views.RemoteBranches, name: "remoteBranches"},
{viewPtr: &gui.Views.Commits, name: "commits"},
{viewPtr: &gui.Views.Stash, name: "stash"},
{viewPtr: &gui.Views.CommitFiles, name: "commitFiles"},
{viewPtr: &gui.Views.SubCommits, name: "subCommits"},
{viewPtr: &gui.Views.Main, name: "main"},
{viewPtr: &gui.Views.Secondary, name: "secondary"},
{viewPtr: &gui.Views.Options, name: "options"},
{viewPtr: &gui.Views.AppStatus, name: "appStatus"},
{viewPtr: &gui.Views.Information, name: "information"},
{viewPtr: &gui.Views.Search, name: "search"},
{viewPtr: &gui.Views.SearchPrefix, name: "searchPrefix"},
{viewPtr: &gui.Views.CommitMessage, name: "commitMessage"},
{viewPtr: &gui.Views.Menu, name: "menu"},
{viewPtr: &gui.Views.Suggestions, name: "suggestions"},
{viewPtr: &gui.Views.Confirmation, name: "confirmation"},
{viewPtr: &gui.Views.Limit, name: "limit"},
{viewPtr: &gui.Views.Extras, name: "extras"},
}
var err error
for _, mapping := range viewNameMappings {
*mapping.viewPtr, err = gui.prepareView(mapping.name)
if err != nil && err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
}
gui.Views.Options.Frame = false
gui.Views.Options.FgColor = theme.OptionsColor
gui.Views.SearchPrefix.BgColor = gocui.ColorDefault
gui.Views.SearchPrefix.FgColor = gocui.ColorGreen
gui.Views.SearchPrefix.Frame = false
gui.setViewContent(gui.Views.SearchPrefix, SEARCH_PREFIX)
gui.Views.Stash.Title = gui.c.Tr.StashTitle
gui.Views.Stash.FgColor = theme.GocuiDefaultTextColor
gui.Views.Commits.Title = gui.c.Tr.CommitsTitle
gui.Views.Commits.FgColor = theme.GocuiDefaultTextColor
gui.Views.CommitFiles.Title = gui.c.Tr.CommitFiles
gui.Views.CommitFiles.FgColor = theme.GocuiDefaultTextColor
gui.Views.SubCommits.FgColor = theme.GocuiDefaultTextColor
gui.Views.Branches.Title = gui.c.Tr.BranchesTitle
gui.Views.Branches.FgColor = theme.GocuiDefaultTextColor
gui.Views.RemoteBranches.FgColor = theme.GocuiDefaultTextColor
gui.Views.Files.Title = gui.c.Tr.FilesTitle
gui.Views.Files.FgColor = theme.GocuiDefaultTextColor
gui.Views.Secondary.Title = gui.c.Tr.DiffTitle
gui.Views.Secondary.Wrap = true
gui.Views.Secondary.FgColor = theme.GocuiDefaultTextColor
gui.Views.Secondary.IgnoreCarriageReturns = true
gui.Views.Secondary.CanScrollPastBottom = gui.c.UserConfig.Gui.ScrollPastBottom
gui.Views.Main.Title = gui.c.Tr.DiffTitle
gui.Views.Main.Wrap = true
gui.Views.Main.FgColor = theme.GocuiDefaultTextColor
gui.Views.Main.IgnoreCarriageReturns = true
gui.Views.Main.CanScrollPastBottom = gui.c.UserConfig.Gui.ScrollPastBottom
gui.Views.Limit.Title = gui.c.Tr.NotEnoughSpace
gui.Views.Limit.Wrap = true
gui.Views.Status.Title = gui.c.Tr.StatusTitle
gui.Views.Status.FgColor = theme.GocuiDefaultTextColor
gui.Views.Search.BgColor = gocui.ColorDefault
gui.Views.Search.FgColor = gocui.ColorGreen
gui.Views.Search.Frame = false
gui.Views.Search.Editable = true
gui.Views.AppStatus.BgColor = gocui.ColorDefault
gui.Views.AppStatus.FgColor = gocui.ColorCyan
gui.Views.AppStatus.Frame = false
gui.Views.AppStatus.Visible = false
gui.Views.CommitMessage.Visible = false
gui.Views.CommitMessage.Title = gui.c.Tr.CommitMessage
gui.Views.CommitMessage.FgColor = theme.GocuiDefaultTextColor
gui.Views.CommitMessage.Editable = true
gui.Views.CommitMessage.Editor = gocui.EditorFunc(gui.commitMessageEditor)
gui.Views.Confirmation.Visible = false
gui.Views.Suggestions.Visible = false
gui.Views.Menu.Visible = false
gui.Views.Information.BgColor = gocui.ColorDefault
gui.Views.Information.FgColor = gocui.ColorGreen
gui.Views.Information.Frame = false
gui.Views.Extras.Title = gui.c.Tr.CommandLog
gui.Views.Extras.FgColor = theme.GocuiDefaultTextColor
gui.Views.Extras.Autoscroll = true
gui.Views.Extras.Wrap = true
return nil
}
func (gui *Gui) RunAndHandleError(filterPath string) error { func (gui *Gui) RunAndHandleError(filterPath string) error {
gui.stopChan = make(chan struct{}) gui.stopChan = make(chan struct{})
return utils.SafeWithError(func() error { return utils.SafeWithError(func() error {

View File

@ -20,15 +20,6 @@ func (gui *Gui) layout(g *gocui.Gui) error {
g.Highlight = true g.Highlight = true
width, height := g.Size() width, height := g.Size()
minimumHeight := 9
minimumWidth := 10
var err error
_, err = g.SetView("limit", 0, 0, width-1, height-1, 0)
if err != nil && err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
gui.Views.Limit.Visible = height < minimumHeight || width < minimumWidth
informationStr := gui.informationStr() informationStr := gui.informationStr()
appStatus := gui.statusManager.getStatusString() appStatus := gui.statusManager.getStatusString()
@ -77,6 +68,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
dimensionsObj.Y1+frameOffset, dimensionsObj.Y1+frameOffset,
0, 0,
) )
view.Frame = frame
if view != nil { if view != nil {
view.Visible = true view.Visible = true
@ -85,36 +77,19 @@ func (gui *Gui) layout(g *gocui.Gui) error {
return view, err return view, err
} }
args := []struct { for _, arg := range gui.controlledViews() {
viewName string _, err := setViewFromDimensions(arg.viewName, arg.windowName, arg.frame)
windowName string
frame bool
}{
{viewName: "main", windowName: "main", frame: true},
{viewName: "secondary", windowName: "secondary", frame: true},
{viewName: "status", windowName: "status", frame: true},
{viewName: "files", windowName: "files", frame: true},
{viewName: "branches", windowName: "branches", frame: true},
{viewName: "remoteBranches", windowName: "branches", frame: true},
{viewName: "commitFiles", windowName: gui.State.Contexts.CommitFiles.GetWindowName(), frame: true},
{viewName: "subCommits", windowName: gui.State.Contexts.SubCommits.GetWindowName(), frame: true},
{viewName: "commits", windowName: "commits", frame: true},
{viewName: "stash", windowName: "stash", frame: true},
{viewName: "options", windowName: "options", frame: false},
{viewName: "searchPrefix", windowName: "searchPrefix", frame: false},
{viewName: "search", windowName: "search", frame: false},
{viewName: "appStatus", windowName: "appStatus", frame: false},
{viewName: "information", windowName: "information", frame: false},
{viewName: "extras", windowName: "extras", frame: true},
}
for _, arg := range args {
_, err = setViewFromDimensions(arg.viewName, arg.windowName, arg.frame)
if err != nil && err.Error() != UNKNOWN_VIEW_ERROR_MSG { if err != nil && err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err return err
} }
} }
minimumHeight := 9
minimumWidth := 10
gui.Views.Limit.Visible = height < minimumHeight || width < minimumWidth
gui.Views.Tooltip.Visible = gui.Views.Menu.Visible && gui.Views.Tooltip.Buffer() != ""
for _, context := range gui.TransientContexts() { for _, context := range gui.TransientContexts() {
view, err := gui.g.View(context.GetViewName()) view, err := gui.g.View(context.GetViewName())
if err != nil && err.Error() != UNKNOWN_VIEW_ERROR_MSG { if err != nil && err.Error() != UNKNOWN_VIEW_ERROR_MSG {
@ -205,40 +180,7 @@ func (gui *Gui) onInitialViewsCreationForRepo() error {
func (gui *Gui) onInitialViewsCreation() error { func (gui *Gui) onInitialViewsCreation() error {
// now we order the views (in order of bottom first) // now we order the views (in order of bottom first)
layerOneViews := []*gocui.View{ for _, view := range gui.orderedViews() {
// first layer. Ordering within this layer does not matter because there are
// no overlapping views
gui.Views.Status,
gui.Views.Files,
gui.Views.Branches,
gui.Views.RemoteBranches,
gui.Views.Commits,
gui.Views.Stash,
gui.Views.SubCommits,
gui.Views.CommitFiles,
gui.Views.Main,
gui.Views.Secondary,
gui.Views.Extras,
// bottom line
gui.Views.Options,
gui.Views.AppStatus,
gui.Views.Information,
gui.Views.Search,
gui.Views.SearchPrefix, // this view takes up one character. Its only purpose is to show the slash when searching
// popups. Ordering within this layer does not matter because there should
// only be one popup shown at a time
gui.Views.CommitMessage,
gui.Views.Menu,
gui.Views.Suggestions,
gui.Views.Confirmation,
// this guy will cover everything else when it appears
gui.Views.Limit,
}
for _, view := range layerOneViews {
if _, err := gui.g.SetViewOnTop(view.Name()); err != nil { if _, err := gui.g.SetViewOnTop(view.Name()); err != nil {
return err return err
} }

View File

@ -15,11 +15,11 @@ import (
func (gui *Gui) menuListContext() *context.MenuContext { func (gui *Gui) menuListContext() *context.MenuContext {
return context.NewMenuContext( return context.NewMenuContext(
gui.Views.Menu, gui.Views.Menu,
nil,
nil,
nil,
gui.c, gui.c,
gui.getMenuOptions, gui.getMenuOptions,
func(content string) {
gui.Views.Tooltip.SetContent(content)
},
) )
} }

View File

@ -36,16 +36,18 @@ func (gui *Gui) createMenu(opts types.CreateMenuOptions) error {
} }
} }
x0, y0, x1, y1 := gui.getConfirmationPanelDimensionsForContentHeight(len(opts.Items)) gui.State.Contexts.Menu.SetMenuItems(opts.Items)
menuView, _ := gui.g.SetView("menu", x0, y0, x1, y1, 0) gui.State.Contexts.Menu.SetSelectedLineIdx(0)
menuView.Title = opts.Title
menuView.FgColor = theme.GocuiDefaultTextColor gui.Views.Menu.Title = opts.Title
menuView.SetOnSelectItem(gui.onSelectItemWrapper(func(selectedLine int) error { gui.Views.Menu.FgColor = theme.GocuiDefaultTextColor
gui.Views.Menu.SetOnSelectItem(gui.onSelectItemWrapper(func(selectedLine int) error {
return nil return nil
})) }))
gui.State.Contexts.Menu.SetMenuItems(opts.Items) gui.Views.Tooltip.Wrap = true
gui.State.Contexts.Menu.SetSelectedLineIdx(0) gui.Views.Tooltip.FgColor = theme.GocuiDefaultTextColor
gui.Views.Tooltip.Visible = true
// resetting keybindings so that the menu-specific keybindings are registered // resetting keybindings so that the menu-specific keybindings are registered
if err := gui.resetKeybindings(); err != nil { if err := gui.resetKeybindings(); err != nil {

View File

@ -73,7 +73,8 @@ func (gui *Gui) handleCreateOptionsMenu() error {
return binding.Handler() return binding.Handler()
}, },
Key: binding.Key, Key: binding.Key,
Tooltip: binding.Tooltip,
} }
}) })

View File

@ -118,6 +118,9 @@ type MenuItem struct {
// if Key is defined it allows the user to press the key to invoke the menu // if Key is defined it allows the user to press the key to invoke the menu
// item, as opposed to having to navigate to it // item, as opposed to having to navigate to it
Key Key Key Key
// the tooltip will be displayed upon highlighting the menu item
Tooltip string
} }
type Model struct { type Model struct {

View File

@ -17,6 +17,9 @@ type Binding struct {
Alternative string Alternative string
Tag string // e.g. 'navigation'. Used for grouping things in the cheatsheet Tag string // e.g. 'navigation'. Used for grouping things in the cheatsheet
OpensMenu bool OpensMenu bool
// to be displayed if the keybinding is highlighted from within a menu
Tooltip string
} }
// A guard is a decorator which checks something before executing a handler // A guard is a decorator which checks something before executing a handler

View File

@ -47,24 +47,53 @@ func (gui *Gui) resizeCurrentPopupPanel() error {
if v == nil { if v == nil {
return nil return nil
} }
if gui.isPopupPanel(v.Name()) {
if v == gui.Views.Menu {
gui.resizeMenu()
} else if v == gui.Views.Confirmation || v == gui.Views.Suggestions {
gui.resizeConfirmationPanel()
} else if gui.isPopupPanel(v.Name()) {
return gui.resizePopupPanel(v, v.Buffer()) return gui.resizePopupPanel(v, v.Buffer())
} }
return nil return nil
} }
func (gui *Gui) resizePopupPanel(v *gocui.View, content string) error { func (gui *Gui) resizePopupPanel(v *gocui.View, content string) error {
// If the confirmation panel is already displayed, just resize the width,
// otherwise continue
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(v.Wrap, content) x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(v.Wrap, content)
vx0, vy0, vx1, vy1 := v.Dimensions()
if vx0 == x0 && vy0 == y0 && vx1 == x1 && vy1 == y1 {
return nil
}
_, err := gui.g.SetView(v.Name(), x0, y0, x1, y1, 0) _, err := gui.g.SetView(v.Name(), x0, y0, x1, y1, 0)
return err return err
} }
func (gui *Gui) resizeMenu() {
itemCount := gui.State.Contexts.Menu.GetList().Len()
offset := 3
panelWidth := gui.getConfirmationPanelWidth()
x0, y0, x1, y1 := gui.getConfirmationPanelDimensionsForContentHeight(panelWidth, itemCount+offset)
menuBottom := y1 - offset
_, _ = gui.g.SetView(gui.Views.Menu.Name(), x0, y0, x1, menuBottom, 0)
tooltipTop := menuBottom + 1
tooltipHeight := gui.getMessageHeight(true, gui.State.Contexts.Menu.GetSelected().Tooltip, panelWidth) + 2 // plus 2 for the frame
_, _ = gui.g.SetView(gui.Views.Tooltip.Name(), x0, tooltipTop, x1, tooltipTop+tooltipHeight-1, 0)
}
func (gui *Gui) resizeConfirmationPanel() {
suggestionsViewHeight := 0
if gui.Views.Suggestions.Visible {
suggestionsViewHeight = 11
}
panelWidth := gui.getConfirmationPanelWidth()
prompt := gui.Views.Confirmation.Buffer()
panelHeight := gui.getMessageHeight(true, prompt, panelWidth) + suggestionsViewHeight
x0, y0, x1, y1 := gui.getConfirmationPanelDimensionsAux(panelWidth, panelHeight)
confirmationViewBottom := y1 - suggestionsViewHeight
_, _ = gui.g.SetView(gui.Views.Confirmation.Name(), x0, y0, x1, confirmationViewBottom, 0)
suggestionsViewTop := confirmationViewBottom + 1
_, _ = gui.g.SetView(gui.Views.Suggestions.Name(), x0, suggestionsViewTop, x1, suggestionsViewTop+suggestionsViewHeight, 0)
}
func (gui *Gui) globalOptionsMap() map[string]string { func (gui *Gui) globalOptionsMap() map[string]string {
keybindingConfig := gui.c.UserConfig.Keybinding keybindingConfig := gui.c.UserConfig.Keybinding

218
pkg/gui/views.go Normal file
View File

@ -0,0 +1,218 @@
package gui
import (
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/theme"
)
type Views struct {
Status *gocui.View
Files *gocui.View
Branches *gocui.View
RemoteBranches *gocui.View
Commits *gocui.View
Stash *gocui.View
Main *gocui.View
Secondary *gocui.View
Options *gocui.View
Confirmation *gocui.View
Menu *gocui.View
CommitMessage *gocui.View
CommitFiles *gocui.View
SubCommits *gocui.View
Information *gocui.View
AppStatus *gocui.View
Search *gocui.View
SearchPrefix *gocui.View
Limit *gocui.View
Suggestions *gocui.View
Tooltip *gocui.View
Extras *gocui.View
}
type viewNameMapping struct {
viewPtr **gocui.View
name string
}
func (gui *Gui) orderedViews() []*gocui.View {
return slices.Map(gui.orderedViewNameMappings(), func(v viewNameMapping) *gocui.View {
return *v.viewPtr
})
}
func (gui *Gui) orderedViewNameMappings() []viewNameMapping {
return []viewNameMapping{
// first layer. Ordering within this layer does not matter because there are
// no overlapping views
{viewPtr: &gui.Views.Status, name: "status"},
{viewPtr: &gui.Views.Files, name: "files"},
{viewPtr: &gui.Views.Branches, name: "branches"},
{viewPtr: &gui.Views.RemoteBranches, name: "remoteBranches"},
{viewPtr: &gui.Views.Commits, name: "commits"},
{viewPtr: &gui.Views.Stash, name: "stash"},
{viewPtr: &gui.Views.SubCommits, name: "subCommits"},
{viewPtr: &gui.Views.CommitFiles, name: "commitFiles"},
{viewPtr: &gui.Views.Main, name: "main"},
{viewPtr: &gui.Views.Secondary, name: "secondary"},
{viewPtr: &gui.Views.Extras, name: "extras"},
// bottom line
{viewPtr: &gui.Views.Options, name: "options"},
{viewPtr: &gui.Views.AppStatus, name: "appStatus"},
{viewPtr: &gui.Views.Information, name: "information"},
{viewPtr: &gui.Views.Search, name: "search"},
// this view takes up one character. Its only purpose is to show the slash when searching
{viewPtr: &gui.Views.SearchPrefix, name: "searchPrefix"},
// popups.
{viewPtr: &gui.Views.CommitMessage, name: "commitMessage"},
{viewPtr: &gui.Views.Menu, name: "menu"},
{viewPtr: &gui.Views.Suggestions, name: "suggestions"},
{viewPtr: &gui.Views.Confirmation, name: "confirmation"},
{viewPtr: &gui.Views.Tooltip, name: "tooltip"},
// this guy will cover everything else when it appears
{viewPtr: &gui.Views.Limit, name: "limit"},
}
}
type controlledView struct {
viewName string
windowName string
frame bool
}
// controlled views have their size and position determined in arrangement.go.
// Some views, like the confirmation panel, are currently sized at the time of
// displaying the view, based on the view's contents.
func (gui *Gui) controlledViews() []controlledView {
return []controlledView{
{viewName: "main", windowName: "main", frame: true},
{viewName: "secondary", windowName: "secondary", frame: true},
{viewName: "status", windowName: "status", frame: true},
{viewName: "files", windowName: "files", frame: true},
{viewName: "branches", windowName: "branches", frame: true},
{viewName: "remoteBranches", windowName: "branches", frame: true},
{viewName: "commitFiles", windowName: gui.State.Contexts.CommitFiles.GetWindowName(), frame: true},
{viewName: "subCommits", windowName: gui.State.Contexts.SubCommits.GetWindowName(), frame: true},
{viewName: "commits", windowName: "commits", frame: true},
{viewName: "stash", windowName: "stash", frame: true},
{viewName: "options", windowName: "options", frame: false},
{viewName: "searchPrefix", windowName: "searchPrefix", frame: false},
{viewName: "search", windowName: "search", frame: false},
{viewName: "appStatus", windowName: "appStatus", frame: false},
{viewName: "information", windowName: "information", frame: false},
{viewName: "extras", windowName: "extras", frame: true},
{viewName: "limit", windowName: "limit", frame: true},
}
}
func (gui *Gui) createAllViews() error {
var err error
for _, mapping := range gui.orderedViewNameMappings() {
*mapping.viewPtr, err = gui.prepareView(mapping.name)
if err != nil && err.Error() != UNKNOWN_VIEW_ERROR_MSG {
return err
}
}
gui.Views.Options.FgColor = theme.OptionsColor
gui.Views.SearchPrefix.BgColor = gocui.ColorDefault
gui.Views.SearchPrefix.FgColor = gocui.ColorGreen
gui.setViewContent(gui.Views.SearchPrefix, SEARCH_PREFIX)
gui.Views.Stash.Title = gui.c.Tr.StashTitle
gui.Views.Stash.FgColor = theme.GocuiDefaultTextColor
gui.Views.Commits.Title = gui.c.Tr.CommitsTitle
gui.Views.Commits.FgColor = theme.GocuiDefaultTextColor
gui.Views.CommitFiles.Title = gui.c.Tr.CommitFiles
gui.Views.CommitFiles.FgColor = theme.GocuiDefaultTextColor
gui.Views.SubCommits.FgColor = theme.GocuiDefaultTextColor
gui.Views.Branches.Title = gui.c.Tr.BranchesTitle
gui.Views.Branches.FgColor = theme.GocuiDefaultTextColor
gui.Views.RemoteBranches.FgColor = theme.GocuiDefaultTextColor
gui.Views.Files.Title = gui.c.Tr.FilesTitle
gui.Views.Files.FgColor = theme.GocuiDefaultTextColor
gui.Views.Secondary.Title = gui.c.Tr.DiffTitle
gui.Views.Secondary.Wrap = true
gui.Views.Secondary.FgColor = theme.GocuiDefaultTextColor
gui.Views.Secondary.IgnoreCarriageReturns = true
gui.Views.Secondary.CanScrollPastBottom = gui.c.UserConfig.Gui.ScrollPastBottom
gui.Views.Main.Title = gui.c.Tr.DiffTitle
gui.Views.Main.Wrap = true
gui.Views.Main.FgColor = theme.GocuiDefaultTextColor
gui.Views.Main.IgnoreCarriageReturns = true
gui.Views.Main.CanScrollPastBottom = gui.c.UserConfig.Gui.ScrollPastBottom
gui.Views.Limit.Title = gui.c.Tr.NotEnoughSpace
gui.Views.Limit.Wrap = true
gui.Views.Status.Title = gui.c.Tr.StatusTitle
gui.Views.Status.FgColor = theme.GocuiDefaultTextColor
gui.Views.Search.BgColor = gocui.ColorDefault
gui.Views.Search.FgColor = gocui.ColorGreen
gui.Views.Search.Editable = true
gui.Views.AppStatus.BgColor = gocui.ColorDefault
gui.Views.AppStatus.FgColor = gocui.ColorCyan
gui.Views.AppStatus.Visible = false
gui.Views.CommitMessage.Visible = false
gui.Views.CommitMessage.Title = gui.c.Tr.CommitMessage
gui.Views.CommitMessage.FgColor = theme.GocuiDefaultTextColor
gui.Views.CommitMessage.Editable = true
gui.Views.CommitMessage.Editor = gocui.EditorFunc(gui.commitMessageEditor)
gui.Views.Confirmation.Visible = false
gui.Views.Suggestions.Visible = false
gui.Views.Tooltip.FgColor = theme.GocuiDefaultTextColor
gui.Views.Menu.Visible = false
gui.Views.Tooltip.Visible = false
gui.Views.Information.BgColor = gocui.ColorDefault
gui.Views.Information.FgColor = gocui.ColorGreen
gui.Views.Extras.Title = gui.c.Tr.CommandLog
gui.Views.Extras.FgColor = theme.GocuiDefaultTextColor
gui.Views.Extras.Autoscroll = true
gui.Views.Extras.Wrap = true
return nil
}
func initialViewContextMapping(contextTree *context.ContextTree) map[string]types.Context {
return map[string]types.Context{
"status": contextTree.Status,
"files": contextTree.Files,
"branches": contextTree.Branches,
"remoteBranches": contextTree.RemoteBranches,
"commits": contextTree.LocalCommits,
"commitFiles": contextTree.CommitFiles,
"subCommits": contextTree.SubCommits,
"stash": contextTree.Stash,
"menu": contextTree.Menu,
"confirmation": contextTree.Confirmation,
"commitMessage": contextTree.CommitMessage,
"main": contextTree.Normal,
"secondary": contextTree.Normal,
"extras": contextTree.CommandLog,
}
}

View File

@ -108,6 +108,8 @@ type TranslationSet struct {
LcUndo string LcUndo string
LcUndoReflog string LcUndoReflog string
LcRedoReflog string LcRedoReflog string
UndoTooltip string
RedoTooltip string
LcPop string LcPop string
LcDrop string LcDrop string
LcApply string LcApply string
@ -486,6 +488,7 @@ type TranslationSet struct {
CheckoutPrompt string CheckoutPrompt string
HardResetAutostashPrompt string HardResetAutostashPrompt string
UpstreamGone string UpstreamGone string
NukeDescription string
Actions Actions Actions Actions
Bisect Bisect Bisect Bisect
} }
@ -719,6 +722,8 @@ func EnglishTranslationSet() TranslationSet {
LcUndo: "undo", LcUndo: "undo",
LcUndoReflog: "undo (via reflog) (experimental)", LcUndoReflog: "undo (via reflog) (experimental)",
LcRedoReflog: "redo (via reflog) (experimental)", LcRedoReflog: "redo (via reflog) (experimental)",
UndoTooltip: "The reflog will be used to determine what git command to run to undo the last git command. This does not include changes to the working tree; only commits are taken into consideration.",
RedoTooltip: "The reflog will be used to determine what git command to run to redo the last git command. This does not include changes to the working tree; only commits are taken into consideration.",
LcPop: "pop", LcPop: "pop",
LcDrop: "drop", LcDrop: "drop",
LcApply: "apply", LcApply: "apply",
@ -1098,6 +1103,7 @@ func EnglishTranslationSet() TranslationSet {
HardResetAutostashPrompt: "Are you sure you want to hard reset to '%s'? An auto-stash will be performed if necessary.", HardResetAutostashPrompt: "Are you sure you want to hard reset to '%s'? An auto-stash will be performed if necessary.",
CheckoutPrompt: "Are you sure you want to checkout '%s'?", CheckoutPrompt: "Are you sure you want to checkout '%s'?",
UpstreamGone: "(upstream gone)", UpstreamGone: "(upstream gone)",
NukeDescription: "If you want to make all the changes in the worktree go away, this is the way to do it. If there are dirty submodule changes this will stash those changes in the submodule(s).",
Actions: Actions{ Actions: Actions{
// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm) // TODO: combine this with the original keybinding descriptions (those are all in lowercase atm)
CheckoutCommit: "Checkout commit", CheckoutCommit: "Checkout commit",