mirror of
https://github.com/jesseduffield/lazygit.git
synced 2024-12-10 11:10:18 +02:00
439 lines
9.6 KiB
Go
439 lines
9.6 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/fatih/color"
|
|
"github.com/jesseduffield/gocui"
|
|
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
|
)
|
|
|
|
var errSubProcess = errors.New("subprocess")
|
|
|
|
type App struct {
|
|
tests []*IntegrationTest
|
|
itemIdx int
|
|
subProcess *exec.Cmd
|
|
testDir string
|
|
editing bool
|
|
g *gocui.Gui
|
|
}
|
|
|
|
func (app *App) getCurrentTest() *IntegrationTest {
|
|
if len(app.tests) > 0 {
|
|
return app.tests[app.itemIdx]
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (app *App) refreshTests() {
|
|
app.loadTests()
|
|
app.g.Update(func(*gocui.Gui) error {
|
|
listView, err := app.g.View("list")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
listView.Clear()
|
|
for _, test := range app.tests {
|
|
fmt.Fprintln(listView, test.Name)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (app *App) loadTests() {
|
|
tests, err := loadTests(app.testDir)
|
|
if err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
app.tests = tests
|
|
if app.itemIdx > len(app.tests)-1 {
|
|
app.itemIdx = len(app.tests) - 1
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
rootDir := getRootDirectory()
|
|
testDir := filepath.Join(rootDir, "test", "integration")
|
|
|
|
app := &App{testDir: testDir}
|
|
app.loadTests()
|
|
|
|
Loop:
|
|
for {
|
|
g, err := gocui.NewGui(gocui.OutputTrue, false, gocui.NORMAL)
|
|
if err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
g.Cursor = false
|
|
|
|
app.g = g
|
|
|
|
g.SetManagerFunc(app.layout)
|
|
|
|
if err := g.SetKeybinding("list", nil, gocui.KeyArrowUp, gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
|
|
if app.itemIdx > 0 {
|
|
app.itemIdx--
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
if err := g.SetKeybinding("list", nil, gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
if err := g.SetKeybinding("list", nil, 'q', gocui.ModNone, quit); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
if err := g.SetKeybinding("list", nil, 'r', gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
|
|
currentTest := app.getCurrentTest()
|
|
if currentTest == nil {
|
|
return nil
|
|
}
|
|
|
|
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("RECORD_EVENTS=true go test pkg/gui/gui_test.go -run /%s", currentTest.Name))
|
|
app.subProcess = cmd
|
|
|
|
return errSubProcess
|
|
}); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
if err := g.SetKeybinding("list", nil, gocui.KeyEnter, gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
|
|
currentTest := app.getCurrentTest()
|
|
if currentTest == nil {
|
|
return nil
|
|
}
|
|
|
|
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("go test pkg/gui/gui_test.go -run /%s", currentTest.Name))
|
|
app.subProcess = cmd
|
|
|
|
return errSubProcess
|
|
}); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
if err := g.SetKeybinding("list", nil, 'o', gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
|
|
currentTest := app.getCurrentTest()
|
|
if currentTest == nil {
|
|
return nil
|
|
}
|
|
|
|
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("code -r %s/%s/test.json", app.testDir, currentTest.Name))
|
|
if err := cmd.Run(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
if err := g.SetKeybinding("list", nil, 'n', gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
|
|
currentTest := app.getCurrentTest()
|
|
if currentTest == nil {
|
|
return nil
|
|
}
|
|
|
|
// need to duplicate that folder and then re-fetch our tests.
|
|
dir := app.testDir + "/" + app.getCurrentTest().Name
|
|
newDir := dir + "_Copy"
|
|
|
|
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("cp -r %s %s", dir, newDir))
|
|
if err := cmd.Run(); err != nil {
|
|
return err
|
|
}
|
|
|
|
app.loadTests()
|
|
|
|
app.refreshTests()
|
|
return nil
|
|
}); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
if err := g.SetKeybinding("list", nil, 'm', gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
|
|
currentTest := app.getCurrentTest()
|
|
if currentTest == nil {
|
|
return nil
|
|
}
|
|
|
|
app.editing = true
|
|
if _, err := g.SetCurrentView("editor"); err != nil {
|
|
return err
|
|
}
|
|
editorView, err := g.View("editor")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
editorView.Clear()
|
|
fmt.Fprint(editorView, currentTest.Name)
|
|
|
|
return nil
|
|
}); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
if err := g.SetKeybinding("list", nil, 'd', gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
|
|
currentTest := app.getCurrentTest()
|
|
if currentTest == nil {
|
|
return nil
|
|
}
|
|
|
|
dir := app.testDir + "/" + app.getCurrentTest().Name
|
|
|
|
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("rm -rf %s", dir))
|
|
if err := cmd.Run(); err != nil {
|
|
return err
|
|
}
|
|
|
|
app.refreshTests()
|
|
|
|
return nil
|
|
}); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
if err := g.SetKeybinding("editor", nil, gocui.KeyEnter, gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
|
|
currentTest := app.getCurrentTest()
|
|
if currentTest == nil {
|
|
return nil
|
|
}
|
|
|
|
app.editing = false
|
|
if _, err := g.SetCurrentView("list"); err != nil {
|
|
return err
|
|
}
|
|
|
|
editorView, err := g.View("editor")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dir := app.testDir + "/" + app.getCurrentTest().Name
|
|
newDir := app.testDir + "/" + editorView.Buffer()
|
|
|
|
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("mv %s %s", dir, newDir))
|
|
if err := cmd.Run(); err != nil {
|
|
return err
|
|
}
|
|
|
|
editorView.Clear()
|
|
|
|
app.refreshTests()
|
|
return nil
|
|
}); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
if err := g.SetKeybinding("editor", nil, gocui.KeyEsc, gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
|
|
app.editing = false
|
|
if _, err := g.SetCurrentView("list"); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
err = g.MainLoop()
|
|
g.Close()
|
|
if err != nil {
|
|
switch err {
|
|
case gocui.ErrQuit:
|
|
break Loop
|
|
|
|
case errSubProcess:
|
|
cmd := app.subProcess
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stderr = os.Stderr
|
|
cmd.Stdout = os.Stdout
|
|
if err := cmd.Run(); err != nil {
|
|
log.Println(err.Error())
|
|
}
|
|
cmd.Stdin = nil
|
|
cmd.Stderr = nil
|
|
cmd.Stdout = nil
|
|
|
|
fmt.Fprintf(os.Stdout, "\n%s", coloredString("press enter to return", color.FgGreen))
|
|
fmt.Scanln() // wait for enter press
|
|
|
|
default:
|
|
log.Panicln(err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (app *App) layout(g *gocui.Gui) error {
|
|
maxX, maxY := g.Size()
|
|
descriptionViewHeight := 7
|
|
keybindingsViewHeight := 3
|
|
editorViewHeight := 3
|
|
if !app.editing {
|
|
editorViewHeight = 0
|
|
} else {
|
|
descriptionViewHeight = 0
|
|
keybindingsViewHeight = 0
|
|
}
|
|
g.Cursor = app.editing
|
|
g.FgColor = gocui.ColorGreen
|
|
listView, err := g.SetView("list", 0, 0, maxX-1, maxY-descriptionViewHeight-keybindingsViewHeight-editorViewHeight-1, 0)
|
|
if err != nil {
|
|
if err.Error() != "unknown view" {
|
|
return err
|
|
}
|
|
listView.Highlight = true
|
|
listView.Clear()
|
|
for _, test := range app.tests {
|
|
fmt.Fprintln(listView, test.Name)
|
|
}
|
|
listView.Title = "Tests"
|
|
listView.FgColor = gocui.ColorDefault
|
|
if _, err := g.SetCurrentView("list"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := listView.SetCursor(0, app.itemIdx); err != nil {
|
|
return err
|
|
}
|
|
|
|
descriptionView, err := g.SetViewBeneath("description", "list", descriptionViewHeight)
|
|
if err != nil {
|
|
if err.Error() != "unknown view" {
|
|
return err
|
|
}
|
|
descriptionView.Title = "Test description"
|
|
descriptionView.Wrap = true
|
|
descriptionView.FgColor = gocui.ColorDefault
|
|
}
|
|
|
|
keybindingsView, err := g.SetViewBeneath("keybindings", "description", keybindingsViewHeight)
|
|
if err != nil {
|
|
if err.Error() != "unknown view" {
|
|
return err
|
|
}
|
|
keybindingsView.Title = "Keybindings"
|
|
keybindingsView.Wrap = true
|
|
keybindingsView.FgColor = gocui.ColorDefault
|
|
fmt.Fprintln(keybindingsView, "up/down: navigate, enter: run test, r: record test, o: open test config, n: duplicate test, m: rename test, d: delete test")
|
|
}
|
|
|
|
editorView, err := g.SetViewBeneath("editor", "keybindings", editorViewHeight)
|
|
if err != nil {
|
|
if err.Error() != "unknown view" {
|
|
return err
|
|
}
|
|
editorView.Title = "Enter Name"
|
|
editorView.FgColor = gocui.ColorDefault
|
|
editorView.Editable = true
|
|
}
|
|
|
|
currentTest := app.getCurrentTest()
|
|
if currentTest == nil {
|
|
return nil
|
|
}
|
|
|
|
descriptionView.Clear()
|
|
fmt.Fprintf(descriptionView, "Speed: %d. %s", currentTest.Speed, currentTest.Description)
|
|
|
|
if err := g.SetKeybinding("list", nil, gocui.KeyArrowDown, gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
|
|
if app.itemIdx < len(app.tests)-1 {
|
|
app.itemIdx++
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func quit(g *gocui.Gui, v *gocui.View) error {
|
|
return gocui.ErrQuit
|
|
}
|
|
|
|
func getRootDirectory() string {
|
|
path, err := os.Getwd()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
for {
|
|
_, err := os.Stat(filepath.Join(path, ".git"))
|
|
|
|
if err == nil {
|
|
return path
|
|
}
|
|
|
|
if !os.IsNotExist(err) {
|
|
panic(err)
|
|
}
|
|
|
|
path = filepath.Dir(path)
|
|
|
|
if path == "/" {
|
|
panic("must run in lazygit folder or child folder")
|
|
}
|
|
}
|
|
}
|
|
|
|
type IntegrationTest struct {
|
|
Name string `json:"name"`
|
|
Speed int `json:"speed"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
func loadTests(testDir string) ([]*IntegrationTest, error) {
|
|
paths, err := filepath.Glob(filepath.Join(testDir, "/*/test.json"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tests := make([]*IntegrationTest, len(paths))
|
|
|
|
for i, path := range paths {
|
|
data, err := ioutil.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
test := &IntegrationTest{}
|
|
|
|
err = json.Unmarshal(data, test)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
test.Name = strings.TrimPrefix(filepath.Dir(path), testDir+"/")
|
|
|
|
tests[i] = test
|
|
}
|
|
|
|
return tests, nil
|
|
}
|
|
|
|
func coloredString(str string, colorAttributes ...color.Attribute) string {
|
|
colour := color.New(colorAttributes...)
|
|
return coloredStringDirect(str, colour)
|
|
}
|
|
|
|
// coloredStringDirect used for aggregating a few color attributes rather than
|
|
// just sending a single one
|
|
func coloredStringDirect(str string, colour *color.Color) string {
|
|
return colour.SprintFunc()(fmt.Sprint(str))
|
|
}
|