1package gui 2 3import ( 4 "fmt" 5 "sort" 6 "strings" 7 "sync" 8 9 "github.com/jesseduffield/gocui" 10 "github.com/jesseduffield/lazygit/pkg/utils" 11 "github.com/spkg/bom" 12) 13 14func (gui *Gui) getCyclableWindows() []string { 15 return []string{"status", "files", "branches", "commits", "stash"} 16} 17 18// models/views that we can refresh 19type RefreshableView int 20 21const ( 22 COMMITS RefreshableView = iota 23 BRANCHES 24 FILES 25 STASH 26 REFLOG 27 TAGS 28 REMOTES 29 STATUS 30 SUBMODULES 31) 32 33func getScopeNames(scopes []RefreshableView) []string { 34 scopeNameMap := map[RefreshableView]string{ 35 COMMITS: "commits", 36 BRANCHES: "branches", 37 FILES: "files", 38 SUBMODULES: "submodules", 39 STASH: "stash", 40 REFLOG: "reflog", 41 TAGS: "tags", 42 REMOTES: "remotes", 43 STATUS: "status", 44 } 45 46 scopeNames := make([]string, len(scopes)) 47 for i, scope := range scopes { 48 scopeNames[i] = scopeNameMap[scope] 49 } 50 51 return scopeNames 52} 53 54func getModeName(mode RefreshMode) string { 55 switch mode { 56 case SYNC: 57 return "sync" 58 case ASYNC: 59 return "async" 60 case BLOCK_UI: 61 return "block-ui" 62 default: 63 return "unknown mode" 64 } 65} 66 67type RefreshMode int 68 69const ( 70 SYNC RefreshMode = iota // wait until everything is done before returning 71 ASYNC // return immediately, allowing each independent thing to update itself 72 BLOCK_UI // wrap code in an update call to ensure UI updates all at once and keybindings aren't executed till complete 73) 74 75type refreshOptions struct { 76 then func() 77 scope []RefreshableView // e.g. []int{COMMITS, BRANCHES}. Leave empty to refresh everything 78 mode RefreshMode // one of SYNC (default), ASYNC, and BLOCK_UI 79} 80 81func arrToMap(arr []RefreshableView) map[RefreshableView]bool { 82 output := map[RefreshableView]bool{} 83 for _, el := range arr { 84 output[el] = true 85 } 86 return output 87} 88 89func (gui *Gui) refreshSidePanels(options refreshOptions) error { 90 if options.scope == nil { 91 gui.Log.Infof( 92 "refreshing all scopes in %s mode", 93 getModeName(options.mode), 94 ) 95 } else { 96 gui.Log.Infof( 97 "refreshing the following scopes in %s mode: %s", 98 getModeName(options.mode), 99 strings.Join(getScopeNames(options.scope), ","), 100 ) 101 } 102 103 wg := sync.WaitGroup{} 104 105 f := func() { 106 var scopeMap map[RefreshableView]bool 107 if len(options.scope) == 0 { 108 scopeMap = arrToMap([]RefreshableView{COMMITS, BRANCHES, FILES, STASH, REFLOG, TAGS, REMOTES, STATUS}) 109 } else { 110 scopeMap = arrToMap(options.scope) 111 } 112 113 if scopeMap[COMMITS] || scopeMap[BRANCHES] || scopeMap[REFLOG] { 114 wg.Add(1) 115 func() { 116 if options.mode == ASYNC { 117 go utils.Safe(func() { _ = gui.refreshCommits() }) 118 } else { 119 _ = gui.refreshCommits() 120 } 121 wg.Done() 122 }() 123 } 124 125 if scopeMap[FILES] || scopeMap[SUBMODULES] { 126 wg.Add(1) 127 func() { 128 if options.mode == ASYNC { 129 go utils.Safe(func() { _ = gui.refreshFilesAndSubmodules() }) 130 } else { 131 _ = gui.refreshFilesAndSubmodules() 132 } 133 wg.Done() 134 }() 135 } 136 137 if scopeMap[STASH] { 138 wg.Add(1) 139 func() { 140 if options.mode == ASYNC { 141 go utils.Safe(func() { _ = gui.refreshStashEntries() }) 142 } else { 143 _ = gui.refreshStashEntries() 144 } 145 wg.Done() 146 }() 147 } 148 149 if scopeMap[TAGS] { 150 wg.Add(1) 151 func() { 152 if options.mode == ASYNC { 153 go utils.Safe(func() { _ = gui.refreshTags() }) 154 } else { 155 _ = gui.refreshTags() 156 } 157 wg.Done() 158 }() 159 } 160 161 if scopeMap[REMOTES] { 162 wg.Add(1) 163 func() { 164 if options.mode == ASYNC { 165 go utils.Safe(func() { _ = gui.refreshRemotes() }) 166 } else { 167 _ = gui.refreshRemotes() 168 } 169 wg.Done() 170 }() 171 } 172 173 wg.Wait() 174 175 gui.refreshStatus() 176 177 if options.then != nil { 178 options.then() 179 } 180 } 181 182 if options.mode == BLOCK_UI { 183 gui.g.Update(func(g *gocui.Gui) error { 184 f() 185 return nil 186 }) 187 } else { 188 f() 189 } 190 191 return nil 192} 193 194func (gui *Gui) resetOrigin(v *gocui.View) error { 195 _ = v.SetCursor(0, 0) 196 return v.SetOrigin(0, 0) 197} 198 199func (gui *Gui) cleanString(s string) string { 200 output := string(bom.Clean([]byte(s))) 201 return utils.NormalizeLinefeeds(output) 202} 203 204func (gui *Gui) setViewContentSync(v *gocui.View, s string) { 205 v.SetContent(gui.cleanString(s)) 206} 207 208func (gui *Gui) setViewContent(v *gocui.View, s string) { 209 gui.g.Update(func(*gocui.Gui) error { 210 gui.setViewContentSync(v, s) 211 return nil 212 }) 213} 214 215// renderString resets the origin of a view and sets its content 216func (gui *Gui) renderString(view *gocui.View, s string) { 217 gui.g.Update(func(*gocui.Gui) error { 218 return gui.renderStringSync(view, s) 219 }) 220} 221 222func (gui *Gui) renderStringSync(view *gocui.View, s string) error { 223 if err := view.SetOrigin(0, 0); err != nil { 224 return err 225 } 226 if err := view.SetCursor(0, 0); err != nil { 227 return err 228 } 229 gui.setViewContentSync(view, s) 230 return nil 231} 232 233func (gui *Gui) optionsMapToString(optionsMap map[string]string) string { 234 optionsArray := make([]string, 0) 235 for key, description := range optionsMap { 236 optionsArray = append(optionsArray, key+": "+description) 237 } 238 sort.Strings(optionsArray) 239 return strings.Join(optionsArray, ", ") 240} 241 242func (gui *Gui) renderOptionsMap(optionsMap map[string]string) { 243 gui.renderString(gui.Views.Options, gui.optionsMapToString(optionsMap)) 244} 245 246func (gui *Gui) currentViewName() string { 247 currentView := gui.g.CurrentView() 248 if currentView == nil { 249 return "" 250 } 251 return currentView.Name() 252} 253 254func (gui *Gui) resizeCurrentPopupPanel() error { 255 v := gui.g.CurrentView() 256 if v == nil { 257 return nil 258 } 259 if gui.isPopupPanel(v.Name()) { 260 return gui.resizePopupPanel(v, v.Buffer()) 261 } 262 return nil 263} 264 265func (gui *Gui) resizePopupPanel(v *gocui.View, content string) error { 266 // If the confirmation panel is already displayed, just resize the width, 267 // otherwise continue 268 x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(v.Wrap, content) 269 vx0, vy0, vx1, vy1 := v.Dimensions() 270 if vx0 == x0 && vy0 == y0 && vx1 == x1 && vy1 == y1 { 271 return nil 272 } 273 _, err := gui.g.SetView(v.Name(), x0, y0, x1, y1, 0) 274 return err 275} 276 277func (gui *Gui) changeSelectedLine(panelState IListPanelState, total int, change int) { 278 // TODO: find out why we're doing this 279 line := panelState.GetSelectedLineIdx() 280 281 if line == -1 { 282 return 283 } 284 var newLine int 285 if line+change < 0 { 286 newLine = 0 287 } else if line+change >= total { 288 newLine = total - 1 289 } else { 290 newLine = line + change 291 } 292 293 panelState.SetSelectedLineIdx(newLine) 294} 295 296func (gui *Gui) refreshSelectedLine(panelState IListPanelState, total int) { 297 line := panelState.GetSelectedLineIdx() 298 299 if line == -1 && total > 0 { 300 panelState.SetSelectedLineIdx(0) 301 } else if total-1 < line { 302 panelState.SetSelectedLineIdx(total - 1) 303 } 304} 305 306func (gui *Gui) renderDisplayStrings(v *gocui.View, displayStrings [][]string) { 307 list := utils.RenderDisplayStrings(displayStrings) 308 v.SetContent(list) 309} 310 311func (gui *Gui) renderDisplayStringsAtPos(v *gocui.View, y int, displayStrings [][]string) { 312 list := utils.RenderDisplayStrings(displayStrings) 313 v.OverwriteLines(y, list) 314} 315 316func (gui *Gui) globalOptionsMap() map[string]string { 317 keybindingConfig := gui.Config.GetUserConfig().Keybinding 318 319 return map[string]string{ 320 fmt.Sprintf("%s/%s", gui.getKeyDisplay(keybindingConfig.Universal.ScrollUpMain), gui.getKeyDisplay(keybindingConfig.Universal.ScrollDownMain)): gui.Tr.LcScroll, 321 fmt.Sprintf("%s %s %s %s", gui.getKeyDisplay(keybindingConfig.Universal.PrevBlock), gui.getKeyDisplay(keybindingConfig.Universal.NextBlock), gui.getKeyDisplay(keybindingConfig.Universal.PrevItem), gui.getKeyDisplay(keybindingConfig.Universal.NextItem)): gui.Tr.LcNavigate, 322 gui.getKeyDisplay(keybindingConfig.Universal.Return): gui.Tr.LcCancel, 323 gui.getKeyDisplay(keybindingConfig.Universal.Quit): gui.Tr.LcQuit, 324 gui.getKeyDisplay(keybindingConfig.Universal.OptionMenu): gui.Tr.LcMenu, 325 fmt.Sprintf("%s-%s", gui.getKeyDisplay(keybindingConfig.Universal.JumpToBlock[0]), gui.getKeyDisplay(keybindingConfig.Universal.JumpToBlock[len(keybindingConfig.Universal.JumpToBlock)-1])): gui.Tr.LcJump, 326 fmt.Sprintf("%s/%s", gui.getKeyDisplay(keybindingConfig.Universal.ScrollLeft), gui.getKeyDisplay(keybindingConfig.Universal.ScrollRight)): gui.Tr.LcScrollLeftRight, 327 } 328} 329 330func (gui *Gui) isPopupPanel(viewName string) bool { 331 return viewName == "commitMessage" || viewName == "credentials" || viewName == "confirmation" || viewName == "menu" 332} 333 334func (gui *Gui) popupPanelFocused() bool { 335 return gui.isPopupPanel(gui.currentViewName()) 336} 337 338// secondaryViewFocused tells us whether it appears that the secondary view is focused. The view is actually never focused for real: we just swap the main and secondary views and then you're still focused on the main view so that we can give you access to all its keybindings for free. I will probably regret this design decision soon enough. 339func (gui *Gui) secondaryViewFocused() bool { 340 state := gui.State.Panels.LineByLine 341 return state != nil && state.SecondaryFocused 342} 343 344func (gui *Gui) onViewTabClick(viewName string, tabIndex int) error { 345 context := gui.State.ViewTabContextMap[viewName][tabIndex].contexts[0] 346 347 return gui.pushContext(context) 348} 349 350func (gui *Gui) handleNextTab() error { 351 v := getTabbedView(gui) 352 if v == nil { 353 return nil 354 } 355 356 return gui.onViewTabClick( 357 v.Name(), 358 utils.ModuloWithWrap(v.TabIndex+1, len(v.Tabs)), 359 ) 360} 361 362func (gui *Gui) handlePrevTab() error { 363 v := getTabbedView(gui) 364 if v == nil { 365 return nil 366 } 367 368 return gui.onViewTabClick( 369 v.Name(), 370 utils.ModuloWithWrap(v.TabIndex-1, len(v.Tabs)), 371 ) 372} 373 374// this is the distance we will move the cursor when paging up or down in a view 375func (gui *Gui) pageDelta(view *gocui.View) int { 376 _, height := view.Size() 377 378 delta := height - 1 379 if delta == 0 { 380 return 1 381 } 382 383 return delta 384} 385 386func getTabbedView(gui *Gui) *gocui.View { 387 // It safe assumption that only static contexts have tabs 388 context := gui.currentStaticContext() 389 view, _ := gui.g.View(context.GetViewName()) 390 return view 391} 392 393func (gui *Gui) render() { 394 gui.g.Update(func(g *gocui.Gui) error { return nil }) 395} 396