mirror of
				https://github.com/jesseduffield/lazygit.git
				synced 2025-10-30 23:57:43 +02:00 
			
		
		
		
	There are many situations where this can arise. Some examples are: - the terminal window is small, and you are showing a view that shows more content than fits into the view port, and the view is scrolled all the way down; now you resize the terminal window to a taller size. Previously, the scroll position of the view would stay the same, so it would add blank space at the bottom; now it will scroll to fill that blank space with content - expandFocusedSidePanel is on, you go to the bottom of a list view, now switch to a different panel, then scroll that (now unfocused) panel all the way down with the scroll wheel; now you focus that panel again. It becomes larger because of the accordion behavior, but would show blank space at the bottom. And probably others that I can't remember right now. I only remember that I always found it confusing to look at a view that had blank space at the bottom even though it had more content to scroll into view.
		
			
				
	
	
		
			307 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			307 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package gui
 | |
| 
 | |
| import (
 | |
| 	"github.com/jesseduffield/gocui"
 | |
| 	"github.com/jesseduffield/lazygit/pkg/gui/context"
 | |
| 	"github.com/jesseduffield/lazygit/pkg/gui/types"
 | |
| 	"github.com/samber/lo"
 | |
| 	"golang.org/x/exp/slices"
 | |
| )
 | |
| 
 | |
| // layout is called for every screen re-render e.g. when the screen is resized
 | |
| func (gui *Gui) layout(g *gocui.Gui) error {
 | |
| 	if !gui.ViewsSetup {
 | |
| 		gui.printCommandLogHeader()
 | |
| 
 | |
| 		if _, err := gui.g.SetCurrentView(gui.defaultSideContext().GetViewName()); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	g.Highlight = true
 | |
| 	width, height := g.Size()
 | |
| 
 | |
| 	informationStr := gui.informationStr()
 | |
| 
 | |
| 	appStatus := gui.helpers.AppStatus.GetStatusString()
 | |
| 
 | |
| 	viewDimensions := gui.getWindowDimensions(informationStr, appStatus)
 | |
| 
 | |
| 	// reading more lines into main view buffers upon resize
 | |
| 	prevMainView := gui.Views.Main
 | |
| 	if prevMainView != nil {
 | |
| 		_, prevMainHeight := prevMainView.Size()
 | |
| 		newMainHeight := viewDimensions["main"].Y1 - viewDimensions["main"].Y0 - 1
 | |
| 		heightDiff := newMainHeight - prevMainHeight
 | |
| 		if heightDiff > 0 {
 | |
| 			if manager, ok := gui.viewBufferManagerMap["main"]; ok {
 | |
| 				manager.ReadLines(heightDiff)
 | |
| 			}
 | |
| 			if manager, ok := gui.viewBufferManagerMap["secondary"]; ok {
 | |
| 				manager.ReadLines(heightDiff)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	contextsToRerender := []types.Context{}
 | |
| 
 | |
| 	// we assume that the view has already been created.
 | |
| 	setViewFromDimensions := func(context types.Context) (*gocui.View, error) {
 | |
| 		viewName := context.GetViewName()
 | |
| 		windowName := context.GetWindowName()
 | |
| 
 | |
| 		dimensionsObj, ok := viewDimensions[windowName]
 | |
| 
 | |
| 		view, err := g.View(viewName)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 
 | |
| 		if !ok {
 | |
| 			// view not specified in dimensions object: so create the view and hide it
 | |
| 			// making the view take up the whole space in the background in case it needs
 | |
| 			// to render content as soon as it appears, because lazyloaded content (via a pty task)
 | |
| 			// cares about the size of the view.
 | |
| 			_, err := g.SetView(viewName, 0, 0, width, height, 0)
 | |
| 			view.Visible = false
 | |
| 			return view, err
 | |
| 		}
 | |
| 
 | |
| 		frameOffset := 1
 | |
| 		if view.Frame {
 | |
| 			frameOffset = 0
 | |
| 		}
 | |
| 
 | |
| 		mustRerender := false
 | |
| 		newHeight := dimensionsObj.Y1 - dimensionsObj.Y0 + 2*frameOffset
 | |
| 		maxOriginY := context.TotalContentHeight()
 | |
| 		if !view.CanScrollPastBottom {
 | |
| 			maxOriginY -= newHeight - 1
 | |
| 		}
 | |
| 		if oldOriginY := view.OriginY(); oldOriginY > maxOriginY {
 | |
| 			view.ScrollUp(oldOriginY - maxOriginY)
 | |
| 			// the view might not have scrolled actually (if it was at the limit
 | |
| 			// already), so we need to check if it did
 | |
| 			if oldOriginY != view.OriginY() && context.NeedsRerenderOnHeightChange() {
 | |
| 				mustRerender = true
 | |
| 			}
 | |
| 		}
 | |
| 		if context.NeedsRerenderOnWidthChange() == types.NEEDS_RERENDER_ON_WIDTH_CHANGE_WHEN_WIDTH_CHANGES {
 | |
| 			// view.Width() returns the width -1 for some reason
 | |
| 			oldWidth := view.Width() + 1
 | |
| 			newWidth := dimensionsObj.X1 - dimensionsObj.X0 + 2*frameOffset
 | |
| 			if oldWidth != newWidth {
 | |
| 				mustRerender = true
 | |
| 			}
 | |
| 		}
 | |
| 		if context.NeedsRerenderOnHeightChange() {
 | |
| 			// view.Height() returns the height -1 for some reason
 | |
| 			oldHeight := view.Height() + 1
 | |
| 			newHeight := dimensionsObj.Y1 - dimensionsObj.Y0 + 2*frameOffset
 | |
| 			if oldHeight != newHeight {
 | |
| 				mustRerender = true
 | |
| 			}
 | |
| 		}
 | |
| 		if mustRerender {
 | |
| 			contextsToRerender = append(contextsToRerender, context)
 | |
| 		}
 | |
| 
 | |
| 		_, err = g.SetView(
 | |
| 			viewName,
 | |
| 			dimensionsObj.X0-frameOffset,
 | |
| 			dimensionsObj.Y0-frameOffset,
 | |
| 			dimensionsObj.X1+frameOffset,
 | |
| 			dimensionsObj.Y1+frameOffset,
 | |
| 			0,
 | |
| 		)
 | |
| 		view.Visible = true
 | |
| 
 | |
| 		return view, err
 | |
| 	}
 | |
| 
 | |
| 	for _, context := range gui.State.Contexts.Flatten() {
 | |
| 		if !context.HasControlledBounds() {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		_, err := setViewFromDimensions(context)
 | |
| 		if err != nil && !gocui.IsUnknownView(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() {
 | |
| 		view, err := gui.g.View(context.GetViewName())
 | |
| 		if err != nil && !gocui.IsUnknownView(err) {
 | |
| 			return err
 | |
| 		}
 | |
| 		view.Visible = gui.helpers.Window.GetViewNameForWindow(context.GetWindowName()) == context.GetViewName()
 | |
| 	}
 | |
| 
 | |
| 	if gui.PrevLayout.Information != informationStr {
 | |
| 		gui.c.SetViewContent(gui.Views.Information, informationStr)
 | |
| 		gui.PrevLayout.Information = informationStr
 | |
| 	}
 | |
| 
 | |
| 	if !gui.ViewsSetup {
 | |
| 		if err := gui.onInitialViewsCreation(); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		gui.handleTestMode()
 | |
| 
 | |
| 		gui.ViewsSetup = true
 | |
| 	}
 | |
| 
 | |
| 	if !gui.State.ViewsSetup {
 | |
| 		if err := gui.onInitialViewsCreationForRepo(); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		gui.State.ViewsSetup = true
 | |
| 	}
 | |
| 
 | |
| 	mainViewWidth, mainViewHeight := gui.Views.Main.Size()
 | |
| 	if mainViewWidth != gui.PrevLayout.MainWidth || mainViewHeight != gui.PrevLayout.MainHeight {
 | |
| 		gui.PrevLayout.MainWidth = mainViewWidth
 | |
| 		gui.PrevLayout.MainHeight = mainViewHeight
 | |
| 		if err := gui.onResize(); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	for _, context := range contextsToRerender {
 | |
| 		if err := context.HandleRender(); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// here is a good place log some stuff
 | |
| 	// if you run `lazygit --logs`
 | |
| 	// this will let you see these branches as prettified json
 | |
| 	// gui.c.Log.Info(utils.AsJson(gui.State.Model.Branches[0:4]))
 | |
| 	gui.helpers.Confirmation.ResizeCurrentPopupPanels()
 | |
| 
 | |
| 	gui.renderContextOptionsMap()
 | |
| 
 | |
| outer:
 | |
| 	for {
 | |
| 		select {
 | |
| 		case f := <-gui.afterLayoutFuncs:
 | |
| 			if err := f(); err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 		default:
 | |
| 			break outer
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (gui *Gui) prepareView(viewName string) (*gocui.View, error) {
 | |
| 	// arbitrarily giving the view enough size so that we don't get an error, but
 | |
| 	// it's expected that the view will be given the correct size before being shown
 | |
| 	return gui.g.SetView(viewName, 0, 0, 10, 10, 0)
 | |
| }
 | |
| 
 | |
| func (gui *Gui) onInitialViewsCreationForRepo() error {
 | |
| 	if err := gui.onRepoViewReset(); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// hide any popup views. This only applies when we've just switched repos
 | |
| 	for _, viewName := range gui.popupViewNames() {
 | |
| 		view, err := gui.g.View(viewName)
 | |
| 		if err == nil {
 | |
| 			view.Visible = false
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	initialContext := gui.c.Context().Current()
 | |
| 	if err := gui.c.Context().Activate(initialContext, types.OnFocusOpts{}); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return gui.loadNewRepo()
 | |
| }
 | |
| 
 | |
| func (gui *Gui) popupViewNames() []string {
 | |
| 	popups := lo.Filter(gui.State.Contexts.Flatten(), func(c types.Context, _ int) bool {
 | |
| 		return c.GetKind() == types.PERSISTENT_POPUP || c.GetKind() == types.TEMPORARY_POPUP
 | |
| 	})
 | |
| 
 | |
| 	return lo.Map(popups, func(c types.Context, _ int) string {
 | |
| 		return c.GetViewName()
 | |
| 	})
 | |
| }
 | |
| 
 | |
| func (gui *Gui) onRepoViewReset() error {
 | |
| 	// now we order the views (in order of bottom first)
 | |
| 	for _, view := range gui.orderedViews() {
 | |
| 		if _, err := gui.g.SetViewOnTop(view.Name()); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	gui.g.Mutexes.ViewsMutex.Lock()
 | |
| 	// add tabs to views
 | |
| 	for _, view := range gui.g.Views() {
 | |
| 		// if the view is in our mapping, we'll set the tabs and the tab index
 | |
| 		for _, values := range gui.viewTabMap() {
 | |
| 			index := slices.IndexFunc(values, func(tabContext context.TabView) bool {
 | |
| 				return tabContext.ViewName == view.Name()
 | |
| 			})
 | |
| 
 | |
| 			if index != -1 {
 | |
| 				view.Tabs = lo.Map(values, func(tabContext context.TabView, _ int) string {
 | |
| 					return tabContext.Tab
 | |
| 				})
 | |
| 				view.TabIndex = index
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	gui.g.Mutexes.ViewsMutex.Unlock()
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (gui *Gui) onInitialViewsCreation() error {
 | |
| 	if !gui.c.UserConfig().DisableStartupPopups {
 | |
| 		storedPopupVersion := gui.c.GetAppState().StartupPopupVersion
 | |
| 		if storedPopupVersion < StartupPopupVersion {
 | |
| 			gui.showIntroPopupMessage()
 | |
| 		} else {
 | |
| 			gui.showBreakingChangesMessage()
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	gui.c.GetAppState().LastVersion = gui.Config.GetVersion()
 | |
| 	gui.c.SaveAppStateAndLogError()
 | |
| 
 | |
| 	if gui.showRecentRepos {
 | |
| 		if err := gui.helpers.Repos.CreateRecentReposMenu(); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		gui.showRecentRepos = false
 | |
| 	}
 | |
| 
 | |
| 	gui.helpers.Update.CheckForUpdateInBackground()
 | |
| 
 | |
| 	gui.waitForIntro.Done()
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (gui *Gui) transientContexts() []types.Context {
 | |
| 	return lo.Filter(gui.State.Contexts.Flatten(), func(context types.Context, _ int) bool {
 | |
| 		return context.IsTransient()
 | |
| 	})
 | |
| }
 |