github.com/Ryooooooga/lazygit@v0.8.1/pkg/gui/view_helpers.go (about)

     1  package gui
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  	"strings"
     7  
     8  	"github.com/jesseduffield/gocui"
     9  	"github.com/jesseduffield/lazygit/pkg/utils"
    10  	"github.com/spkg/bom"
    11  )
    12  
    13  var cyclableViews = []string{"status", "files", "branches", "commits", "stash"}
    14  
    15  func (gui *Gui) refreshSidePanels(g *gocui.Gui) error {
    16  	if err := gui.refreshBranches(g); err != nil {
    17  		return err
    18  	}
    19  	if err := gui.refreshFiles(); err != nil {
    20  		return err
    21  	}
    22  	if err := gui.refreshCommits(g); err != nil {
    23  		return err
    24  	}
    25  
    26  	return gui.refreshStashEntries(g)
    27  }
    28  
    29  func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error {
    30  	var focusedViewName string
    31  	if v == nil || v.Name() == cyclableViews[len(cyclableViews)-1] {
    32  		focusedViewName = cyclableViews[0]
    33  	} else {
    34  		// if we're in the commitFiles view we'll act like we're in the commits view
    35  		viewName := v.Name()
    36  		if viewName == "commitFiles" {
    37  			viewName = "commits"
    38  		}
    39  		for i := range cyclableViews {
    40  			if viewName == cyclableViews[i] {
    41  				focusedViewName = cyclableViews[i+1]
    42  				break
    43  			}
    44  			if i == len(cyclableViews)-1 {
    45  				message := gui.Tr.TemplateLocalize(
    46  					"IssntListOfViews",
    47  					Teml{
    48  						"name": viewName,
    49  					},
    50  				)
    51  				gui.Log.Info(message)
    52  				return nil
    53  			}
    54  		}
    55  	}
    56  	focusedView, err := g.View(focusedViewName)
    57  	if err != nil {
    58  		panic(err)
    59  	}
    60  	return gui.switchFocus(g, v, focusedView)
    61  }
    62  
    63  func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error {
    64  	var focusedViewName string
    65  	if v == nil || v.Name() == cyclableViews[0] {
    66  		focusedViewName = cyclableViews[len(cyclableViews)-1]
    67  	} else {
    68  		// if we're in the commitFiles view we'll act like we're in the commits view
    69  		viewName := v.Name()
    70  		if viewName == "commitFiles" {
    71  			viewName = "commits"
    72  		}
    73  		for i := range cyclableViews {
    74  			if viewName == cyclableViews[i] {
    75  				focusedViewName = cyclableViews[i-1] // TODO: make this work properly
    76  				break
    77  			}
    78  			if i == len(cyclableViews)-1 {
    79  				message := gui.Tr.TemplateLocalize(
    80  					"IssntListOfViews",
    81  					Teml{
    82  						"name": viewName,
    83  					},
    84  				)
    85  				gui.Log.Info(message)
    86  				return nil
    87  			}
    88  		}
    89  	}
    90  	focusedView, err := g.View(focusedViewName)
    91  	if err != nil {
    92  		panic(err)
    93  	}
    94  	return gui.switchFocus(g, v, focusedView)
    95  }
    96  
    97  func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error {
    98  	switch v.Name() {
    99  	case "menu":
   100  		return gui.handleMenuSelect(g, v)
   101  	case "status":
   102  		return gui.handleStatusSelect(g, v)
   103  	case "files":
   104  		return gui.handleFileSelect(g, v, false)
   105  	case "branches":
   106  		return gui.handleBranchSelect(g, v)
   107  	case "commits":
   108  		return gui.handleCommitSelect(g, v)
   109  	case "commitFiles":
   110  		return gui.handleCommitFileSelect(g, v)
   111  	case "stash":
   112  		return gui.handleStashEntrySelect(g, v)
   113  	case "confirmation":
   114  		return nil
   115  	case "commitMessage":
   116  		return gui.handleCommitFocused(g, v)
   117  	case "credentials":
   118  		return gui.handleCredentialsViewFocused(g, v)
   119  	case "main":
   120  		if gui.State.Contexts["main"] == "merging" {
   121  			return gui.refreshMergePanel()
   122  		}
   123  		v.Highlight = false
   124  		return nil
   125  	default:
   126  		panic(gui.Tr.SLocalize("NoViewMachingNewLineFocusedSwitchStatement"))
   127  	}
   128  }
   129  
   130  func (gui *Gui) returnFocus(g *gocui.Gui, v *gocui.View) error {
   131  	previousView, err := g.View(gui.State.PreviousView)
   132  	if err != nil {
   133  		// always fall back to files view if there's no 'previous' view stored
   134  		previousView, err = g.View("files")
   135  		if err != nil {
   136  			gui.Log.Error(err)
   137  		}
   138  	}
   139  	return gui.switchFocus(g, v, previousView)
   140  }
   141  
   142  // pass in oldView = nil if you don't want to be able to return to your old view
   143  // TODO: move some of this logic into our onFocusLost and onFocus hooks
   144  func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error {
   145  	// we assume we'll never want to return focus to a popup panel i.e.
   146  	// we should never stack popup panels
   147  	if oldView != nil && !gui.isPopupPanel(oldView.Name()) {
   148  		gui.State.PreviousView = oldView.Name()
   149  	}
   150  
   151  	gui.Log.Info("setting highlight to true for view" + newView.Name())
   152  	message := gui.Tr.TemplateLocalize(
   153  		"newFocusedViewIs",
   154  		Teml{
   155  			"newFocusedView": newView.Name(),
   156  		},
   157  	)
   158  	gui.Log.Info(message)
   159  	if _, err := g.SetCurrentView(newView.Name()); err != nil {
   160  		return err
   161  	}
   162  	if _, err := g.SetViewOnTop(newView.Name()); err != nil {
   163  		return err
   164  	}
   165  
   166  	g.Cursor = newView.Editable
   167  
   168  	if err := gui.renderPanelOptions(); err != nil {
   169  		return err
   170  	}
   171  
   172  	return gui.newLineFocused(g, newView)
   173  }
   174  
   175  func (gui *Gui) resetOrigin(v *gocui.View) error {
   176  	_ = v.SetCursor(0, 0)
   177  	return v.SetOrigin(0, 0)
   178  }
   179  
   180  // if the cursor down past the last item, move it to the last line
   181  func (gui *Gui) focusPoint(cx int, cy int, lineCount int, v *gocui.View) error {
   182  	if cy < 0 || cy > lineCount {
   183  		return nil
   184  	}
   185  	ox, oy := v.Origin()
   186  	_, height := v.Size()
   187  
   188  	ly := height - 1
   189  	if ly == -1 {
   190  		ly = 0
   191  	}
   192  
   193  	// if line is above origin, move origin and set cursor to zero
   194  	// if line is below origin + height, move origin and set cursor to max
   195  	// otherwise set cursor to value - origin
   196  	if ly > lineCount {
   197  		_ = v.SetCursor(cx, cy)
   198  		_ = v.SetOrigin(ox, 0)
   199  	} else if cy < oy {
   200  		_ = v.SetCursor(cx, 0)
   201  		_ = v.SetOrigin(ox, cy)
   202  	} else if cy > oy+ly {
   203  		_ = v.SetCursor(cx, ly)
   204  		_ = v.SetOrigin(ox, cy-ly)
   205  	} else {
   206  		_ = v.SetCursor(cx, cy-oy)
   207  	}
   208  	return nil
   209  }
   210  
   211  func (gui *Gui) cleanString(s string) string {
   212  	output := string(bom.Clean([]byte(s)))
   213  	return utils.NormalizeLinefeeds(output)
   214  }
   215  
   216  func (gui *Gui) setViewContent(g *gocui.Gui, v *gocui.View, s string) error {
   217  	v.Clear()
   218  	fmt.Fprint(v, gui.cleanString(s))
   219  	return nil
   220  }
   221  
   222  // renderString resets the origin of a view and sets its content
   223  func (gui *Gui) renderString(g *gocui.Gui, viewName, s string) error {
   224  	g.Update(func(*gocui.Gui) error {
   225  		v, err := g.View(viewName)
   226  		if err != nil {
   227  			return nil // return gracefully if view has been deleted
   228  		}
   229  		if err := v.SetOrigin(0, 0); err != nil {
   230  			return err
   231  		}
   232  		return gui.setViewContent(gui.g, v, s)
   233  	})
   234  	return nil
   235  }
   236  
   237  func (gui *Gui) optionsMapToString(optionsMap map[string]string) string {
   238  	optionsArray := make([]string, 0)
   239  	for key, description := range optionsMap {
   240  		optionsArray = append(optionsArray, key+": "+description)
   241  	}
   242  	sort.Strings(optionsArray)
   243  	return strings.Join(optionsArray, ", ")
   244  }
   245  
   246  func (gui *Gui) renderOptionsMap(optionsMap map[string]string) error {
   247  	return gui.renderString(gui.g, "options", gui.optionsMapToString(optionsMap))
   248  }
   249  
   250  // TODO: refactor properly
   251  // i'm so sorry but had to add this getBranchesView
   252  func (gui *Gui) getFilesView() *gocui.View {
   253  	v, _ := gui.g.View("files")
   254  	return v
   255  }
   256  
   257  func (gui *Gui) getCommitsView() *gocui.View {
   258  	v, _ := gui.g.View("commits")
   259  	return v
   260  }
   261  
   262  func (gui *Gui) getCommitMessageView() *gocui.View {
   263  	v, _ := gui.g.View("commitMessage")
   264  	return v
   265  }
   266  
   267  func (gui *Gui) getBranchesView() *gocui.View {
   268  	v, _ := gui.g.View("branches")
   269  	return v
   270  }
   271  
   272  func (gui *Gui) getMainView() *gocui.View {
   273  	v, _ := gui.g.View("main")
   274  	return v
   275  }
   276  
   277  func (gui *Gui) getStashView() *gocui.View {
   278  	v, _ := gui.g.View("stash")
   279  	return v
   280  }
   281  
   282  func (gui *Gui) getCommitFilesView() *gocui.View {
   283  	v, _ := gui.g.View("commitFiles")
   284  	return v
   285  }
   286  
   287  func (gui *Gui) trimmedContent(v *gocui.View) string {
   288  	return strings.TrimSpace(v.Buffer())
   289  }
   290  
   291  func (gui *Gui) currentViewName() string {
   292  	currentView := gui.g.CurrentView()
   293  	return currentView.Name()
   294  }
   295  
   296  func (gui *Gui) resizeCurrentPopupPanel(g *gocui.Gui) error {
   297  	v := g.CurrentView()
   298  	if gui.isPopupPanel(v.Name()) {
   299  		return gui.resizePopupPanel(g, v)
   300  	}
   301  	return nil
   302  }
   303  
   304  func (gui *Gui) resizePopupPanel(g *gocui.Gui, v *gocui.View) error {
   305  	// If the confirmation panel is already displayed, just resize the width,
   306  	// otherwise continue
   307  	content := v.Buffer()
   308  	x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, v.Wrap, content)
   309  	vx0, vy0, vx1, vy1 := v.Dimensions()
   310  	if vx0 == x0 && vy0 == y0 && vx1 == x1 && vy1 == y1 {
   311  		return nil
   312  	}
   313  	gui.Log.Info(gui.Tr.SLocalize("resizingPopupPanel"))
   314  	_, err := g.SetView(v.Name(), x0, y0, x1, y1, 0)
   315  	return err
   316  }
   317  
   318  // generalFocusLine takes a lineNumber to focus, and a bottomLine to ensure we can see
   319  func (gui *Gui) generalFocusLine(lineNumber int, bottomLine int, v *gocui.View) error {
   320  	_, height := v.Size()
   321  	overScroll := bottomLine - height + 1
   322  	if overScroll < 0 {
   323  		overScroll = 0
   324  	}
   325  	if err := v.SetOrigin(0, overScroll); err != nil {
   326  		return err
   327  	}
   328  	if err := v.SetCursor(0, lineNumber-overScroll); err != nil {
   329  		return err
   330  	}
   331  	return nil
   332  }
   333  
   334  func (gui *Gui) changeSelectedLine(line *int, total int, up bool) {
   335  	if up {
   336  		if *line == -1 || *line == 0 {
   337  			return
   338  		}
   339  
   340  		*line -= 1
   341  	} else {
   342  		if *line == -1 || *line == total-1 {
   343  			return
   344  		}
   345  
   346  		*line += 1
   347  	}
   348  }
   349  
   350  func (gui *Gui) refreshSelectedLine(line *int, total int) {
   351  	if *line == -1 && total > 0 {
   352  		*line = 0
   353  	} else if total-1 < *line {
   354  		*line = total - 1
   355  	}
   356  }
   357  
   358  func (gui *Gui) renderListPanel(v *gocui.View, items interface{}) error {
   359  	gui.g.Update(func(g *gocui.Gui) error {
   360  		isFocused := gui.g.CurrentView().Name() == v.Name()
   361  		list, err := utils.RenderList(items, isFocused)
   362  		if err != nil {
   363  			return gui.createErrorPanel(gui.g, err.Error())
   364  		}
   365  		v.Clear()
   366  		fmt.Fprint(v, list)
   367  		return nil
   368  	})
   369  	return nil
   370  }
   371  
   372  func (gui *Gui) renderPanelOptions() error {
   373  	currentView := gui.g.CurrentView()
   374  	switch currentView.Name() {
   375  	case "menu":
   376  		return gui.renderMenuOptions()
   377  	case "main":
   378  		if gui.State.Contexts["main"] == "merging" {
   379  			return gui.renderMergeOptions()
   380  		}
   381  	}
   382  	return gui.renderGlobalOptions()
   383  }
   384  
   385  func (gui *Gui) handleFocusView(g *gocui.Gui, v *gocui.View) error {
   386  	_, err := gui.g.SetCurrentView(v.Name())
   387  	return err
   388  }
   389  
   390  func (gui *Gui) isPopupPanel(viewName string) bool {
   391  	return viewName == "commitMessage" || viewName == "credentials" || viewName == "confirmation" || viewName == "menu"
   392  }
   393  
   394  func (gui *Gui) popupPanelFocused() bool {
   395  	return gui.isPopupPanel(gui.currentViewName())
   396  }