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