jesseduffield.lazygit/pkg/integration/clients/tui.go
Stefan Haller ee94e215e7 Remove dead Modifier field from keybindings
Modifiers were moved into Key in 22169e22f, but the separate Modifier field
on types.Binding and gocui.keybinding was left behind. The keypress matcher
already compares modifiers via Key.Equals, so the old field is never read on
the dispatch path; it just got passed through SetKeybinding and stored.

Drop it from gocui.keybinding, types.Binding, and the SetKeybinding /
DeleteKeybinding signatures, and remove every now-redundant Modifier:
gocui.ModNone struct field. Mouse bindings keep their own Modifier (on
ViewMouseBinding) since that path still consults it.
2026-05-06 09:51:23 +02:00

409 lines
9.8 KiB
Go

package clients
import (
"errors"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/jesseduffield/lazycore/pkg/utils"
"github.com/jesseduffield/lazygit/pkg/gocui"
"github.com/jesseduffield/lazygit/pkg/gui"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/integration/components"
"github.com/jesseduffield/lazygit/pkg/integration/tests"
"github.com/samber/lo"
)
// This program lets you run integration tests from a TUI. See pkg/integration/README.md for more info.
var SLOW_INPUT_DELAY = 600
func RunTUI(raceDetector bool) {
rootDir := utils.GetLazyRootDirectory()
testDir := filepath.Join(rootDir, "test", "integration")
app := newApp(testDir)
app.loadTests()
g, err := gocui.NewGui(gocui.NewGuiOpts{
OutputMode: gocui.OutputTrue,
RuneReplacements: gui.RuneReplacements,
})
if err != nil {
log.Panicln(err)
}
g.Cursor = false
app.g = g
g.SetManagerFunc(app.layout)
if err := g.SetKeybinding("list", gocui.NewKeyName(gocui.KeyArrowUp), func(*gocui.Gui, *gocui.View) error {
if app.itemIdx > 0 {
app.itemIdx--
}
listView, err := g.View("list")
if err != nil {
return err
}
listView.FocusPoint(0, app.itemIdx, true)
return nil
}); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("list", gocui.NewKeyName(gocui.KeyArrowDown), func(*gocui.Gui, *gocui.View) error {
if app.itemIdx < len(app.filteredTests)-1 {
app.itemIdx++
}
listView, err := g.View("list")
if err != nil {
return err
}
listView.FocusPoint(0, app.itemIdx, true)
return nil
}); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("list", gocui.NewKeyStrMod("c", gocui.ModCtrl), quit); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("list", gocui.NewKeyRune('q'), quit); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("list", gocui.NewKeyRune('s'), func(*gocui.Gui, *gocui.View) error {
currentTest := app.getCurrentTest()
if currentTest == nil {
return nil
}
suspendAndRunTest(currentTest, true, false, raceDetector, 0)
return nil
}); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("list", gocui.NewKeyName(gocui.KeyEnter), func(*gocui.Gui, *gocui.View) error {
currentTest := app.getCurrentTest()
if currentTest == nil {
return nil
}
suspendAndRunTest(currentTest, false, false, raceDetector, 0)
return nil
}); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("list", gocui.NewKeyRune('t'), func(*gocui.Gui, *gocui.View) error {
currentTest := app.getCurrentTest()
if currentTest == nil {
return nil
}
suspendAndRunTest(currentTest, false, false, raceDetector, SLOW_INPUT_DELAY)
return nil
}); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("list", gocui.NewKeyRune('d'), func(*gocui.Gui, *gocui.View) error {
currentTest := app.getCurrentTest()
if currentTest == nil {
return nil
}
suspendAndRunTest(currentTest, false, true, raceDetector, 0)
return nil
}); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("list", gocui.NewKeyRune('o'), func(*gocui.Gui, *gocui.View) error {
currentTest := app.getCurrentTest()
if currentTest == nil {
return nil
}
cmd := exec.Command("sh", "-c", fmt.Sprintf("code -r pkg/integration/tests/%s.go", currentTest.Name()))
if err := cmd.Run(); err != nil {
return err
}
return nil
}); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("list", gocui.NewKeyRune('O'), func(*gocui.Gui, *gocui.View) error {
currentTest := app.getCurrentTest()
if currentTest == nil {
return nil
}
cmd := exec.Command("sh", "-c", fmt.Sprintf("code test/_results/%s", currentTest.Name()))
if err := cmd.Run(); err != nil {
return err
}
return nil
}); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("list", gocui.NewKeyRune('/'), func(*gocui.Gui, *gocui.View) error {
app.filtering = true
if _, err := g.SetCurrentView("editor"); err != nil {
return err
}
editorView, err := g.View("editor")
if err != nil {
return err
}
editorView.Clear()
return nil
}); err != nil {
log.Panicln(err)
}
// not using the editor yet, but will use it to help filter the list
if err := g.SetKeybinding("editor", gocui.NewKeyName(gocui.KeyEsc), func(*gocui.Gui, *gocui.View) error {
app.filtering = false
if _, err := g.SetCurrentView("list"); err != nil {
return err
}
app.filteredTests = app.allTests
app.renderTests()
app.editorView.TextArea.Clear()
app.editorView.Clear()
app.editorView.Reset()
return nil
}); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("editor", gocui.NewKeyName(gocui.KeyEnter), func(*gocui.Gui, *gocui.View) error {
app.filtering = false
if _, err := g.SetCurrentView("list"); err != nil {
return err
}
app.renderTests()
return nil
}); err != nil {
log.Panicln(err)
}
err = g.MainLoop()
g.Close()
if errors.Is(err, gocui.ErrQuit) {
return
}
log.Panicln(err)
}
type app struct {
allTests []*components.IntegrationTest
filteredTests []*components.IntegrationTest
itemIdx int
testDir string
filtering bool
g *gocui.Gui
listView *gocui.View
editorView *gocui.View
}
func newApp(testDir string) *app {
return &app{testDir: testDir, allTests: tests.GetTests(utils.GetLazyRootDirectory())}
}
func (self *app) getCurrentTest() *components.IntegrationTest {
self.adjustCursor()
if len(self.filteredTests) > 0 {
return self.filteredTests[self.itemIdx]
}
return nil
}
func (self *app) loadTests() {
self.filteredTests = self.allTests
self.adjustCursor()
}
func (self *app) adjustCursor() {
self.itemIdx = utils.Clamp(self.itemIdx, 0, len(self.filteredTests)-1)
}
func (self *app) filterWithString(needle string) {
if needle == "" {
self.filteredTests = self.allTests
} else {
self.filteredTests = lo.Filter(self.allTests, func(test *components.IntegrationTest, _ int) bool {
return strings.Contains(test.Name(), needle)
})
}
self.renderTests()
self.g.Update(func(g *gocui.Gui) error { return nil })
}
func (self *app) renderTests() {
self.listView.Clear()
for _, test := range self.filteredTests {
fmt.Fprintln(self.listView, test.Name())
}
}
func (self *app) wrapEditor(f func(v *gocui.View, key gocui.Key) bool) func(v *gocui.View, key gocui.Key) bool {
return func(v *gocui.View, key gocui.Key) bool {
matched := f(v, key)
if matched {
self.filterWithString(v.TextArea.GetContent())
}
return matched
}
}
func suspendAndRunTest(test *components.IntegrationTest, sandbox bool, waitForDebugger bool, raceDetector bool, inputDelay int) {
if err := gocui.Screen.Suspend(); err != nil {
panic(err)
}
runTuiTest(test, sandbox, waitForDebugger, raceDetector, inputDelay)
fmt.Fprintf(os.Stdout, "\n%s", style.FgGreen.Sprint("press enter to return"))
_, _ = fmt.Scanln() // wait for enter press
if err := gocui.Screen.Resume(); err != nil {
panic(err)
}
}
func (self *app) layout(g *gocui.Gui) error {
maxX, maxY := g.Size()
descriptionViewHeight := 7
keybindingsViewHeight := 3
editorViewHeight := 3
if !self.filtering {
editorViewHeight = 0
} else {
descriptionViewHeight = 0
keybindingsViewHeight = 0
}
g.Cursor = self.filtering
g.FgColor = gocui.ColorGreen
listView, err := g.SetView("list", 0, 0, maxX-1, maxY-descriptionViewHeight-keybindingsViewHeight-editorViewHeight-1, 0)
if err != nil {
if !errors.Is(err, gocui.ErrUnknownView) {
return err
}
if self.listView == nil {
self.listView = listView
}
listView.Highlight = true
listView.SelBgColor = gocui.ColorBlue
self.renderTests()
listView.Title = "Tests"
listView.FgColor = gocui.ColorDefault
if _, err := g.SetCurrentView("list"); err != nil {
return err
}
}
descriptionView, err := g.SetViewBeneath("description", "list", descriptionViewHeight)
if err != nil {
if !errors.Is(err, gocui.ErrUnknownView) {
return err
}
descriptionView.Title = "Test description"
descriptionView.Wrap = true
descriptionView.FgColor = gocui.ColorDefault
}
keybindingsView, err := g.SetViewBeneath("keybindings", "description", keybindingsViewHeight)
if err != nil {
if !errors.Is(err, gocui.ErrUnknownView) {
return err
}
keybindingsView.Title = "Keybindings"
keybindingsView.Wrap = true
keybindingsView.FgColor = gocui.ColorDefault
fmt.Fprintln(keybindingsView, "up/down: navigate, enter: run test, t: run test slow, s: sandbox, d: debug test, o: open test file, shift+o: open test snapshot directory, forward-slash: filter")
}
editorView, err := g.SetViewBeneath("editor", "keybindings", editorViewHeight)
if err != nil {
if !errors.Is(err, gocui.ErrUnknownView) {
return err
}
if self.editorView == nil {
self.editorView = editorView
}
editorView.Title = "Filter"
editorView.FgColor = gocui.ColorDefault
editorView.Editable = true
editorView.Editor = gocui.EditorFunc(self.wrapEditor(gocui.SimpleEditor))
}
currentTest := self.getCurrentTest()
if currentTest == nil {
return nil
}
descriptionView.Clear()
fmt.Fprint(descriptionView, currentTest.Description())
return nil
}
func quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}
func runTuiTest(test *components.IntegrationTest, sandbox bool, waitForDebugger bool, raceDetector bool, inputDelay int) {
err := components.RunTests(components.RunTestArgs{
Tests: []*components.IntegrationTest{test},
Logf: log.Printf,
RunCmd: runCmdInTerminal,
TestWrapper: runAndPrintError,
Sandbox: sandbox,
WaitForDebugger: waitForDebugger,
RaceDetector: raceDetector,
CodeCoverageDir: "",
InputDelay: inputDelay,
MaxAttempts: 1,
})
if err != nil {
log.Println(err.Error())
}
}
func runAndPrintError(test *components.IntegrationTest, f func() error) {
if err := f(); err != nil {
log.Println(err.Error())
}
}