1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-07-05 00:59:19 +02:00

support tcell simulation screen

This commit is contained in:
Jesse Duffield
2021-04-05 10:20:02 +10:00
parent 011451464f
commit 843b8ceab0
22 changed files with 358 additions and 259 deletions

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"log"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
@ -13,6 +14,7 @@ import (
"github.com/creack/pty" "github.com/creack/pty"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/secureexec" "github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/stretchr/testify/assert"
) )
// To run an integration test, e.g. for test 'commit', go: // To run an integration test, e.g. for test 'commit', go:
@ -209,19 +211,19 @@ func Test() error {
speeds := getTestSpeeds(test.Speed, updateSnapshots) speeds := getTestSpeeds(test.Speed, updateSnapshots)
for i, speed := range speeds { for i, speed := range speeds {
// t.Logf("%s: attempting test at speed %d\n", test.Name, speed) log.Printf("%s: attempting test at speed %d\n", test.Name, speed)
testPath := filepath.Join(testDir, test.Name) testPath := filepath.Join(testDir, test.Name)
actualDir := filepath.Join(testPath, "actual") actualDir := filepath.Join(testPath, "actual")
expectedDir := filepath.Join(testPath, "expected") expectedDir := filepath.Join(testPath, "expected")
// t.Logf("testPath: %s, actualDir: %s, expectedDir: %s", testPath, actualDir, expectedDir) log.Printf("testPath: %s, actualDir: %s, expectedDir: %s", testPath, actualDir, expectedDir)
findOrCreateDir(testPath) findOrCreateDir(testPath)
prepareIntegrationTestDir(actualDir) prepareIntegrationTestDir(actualDir)
err := createFixture(testPath, actualDir) err := createFixture(testPath, actualDir)
if err != nil { if err != nil {
return err // return err
} }
runLazygit(testPath, rootDir, record, speed) runLazygit(testPath, rootDir, record, speed)
@ -268,13 +270,14 @@ func Test() error {
}() }()
if expected == actual { if expected == actual {
// t.Logf("%s: success at speed %d\n", test.Name, speed) log.Printf("%s: success at speed %d\n", test.Name, speed)
break break
} }
// if the snapshots and we haven't tried all playback speeds different we'll retry at a slower speed // if the snapshots and we haven't tried all playback speeds different we'll retry at a slower speed
if i == len(speeds)-1 { if i == len(speeds)-1 {
// assert.Equal(t, expected, actual, fmt.Sprintf("expected:\n%s\nactual:\n%s\n", expected, actual)) assert.Equal(MockTestingT{}, expected, actual, fmt.Sprintf("expected:\n%s\nactual:\n%s\n", expected, actual))
os.Exit(1)
} }
} }
} }
@ -282,6 +285,12 @@ func Test() error {
return nil return nil
} }
type MockTestingT struct{}
func (t MockTestingT) Errorf(format string, args ...interface{}) {
fmt.Printf(format, args...)
}
func createFixture(testPath, actualDir string) error { func createFixture(testPath, actualDir string) error {
osCommand := oscommands.NewDummyOSCommand() osCommand := oscommands.NewDummyOSCommand()
bashScriptPath := filepath.Join(testPath, "setup.sh") bashScriptPath := filepath.Join(testPath, "setup.sh")

View File

@ -458,7 +458,7 @@ func (gui *Gui) Run() error {
playMode = gocui.REPLAYING playMode = gocui.REPLAYING
} }
g, err := gocui.NewGui(gocui.OutputTrue, OverlappingEdges, playMode) g, err := gocui.NewGui(gocui.OutputTrue, OverlappingEdges, playMode, headless())
if err != nil { if err != nil {
return err return err
} }
@ -468,7 +468,7 @@ func (gui *Gui) Run() error {
if replaying() { if replaying() {
g.RecordingConfig = gocui.RecordingConfig{ g.RecordingConfig = gocui.RecordingConfig{
Speed: getRecordingSpeed(), Speed: getRecordingSpeed(),
Leeway: 0, Leeway: 100,
} }
g.Recording, err = gui.loadRecording() g.Recording, err = gui.loadRecording()

View File

@ -22,6 +22,10 @@ func replaying() bool {
return os.Getenv("REPLAY_EVENTS_FROM") != "" return os.Getenv("REPLAY_EVENTS_FROM") != ""
} }
func headless() bool {
return os.Getenv("HEADLESS") != ""
}
func getRecordingSpeed() int { func getRecordingSpeed() int {
// humans are slow so this speeds things up. // humans are slow so this speeds things up.
speed := 1 speed := 1

View File

@ -1,5 +1,5 @@
0000000000000000000000000000000000000000 55bf9e2babc63fb5bae4b42c79fb1ddd1f4205fa CI <CI@example.com> 1617579407 +1000 commit (initial): myfile1 0000000000000000000000000000000000000000 47912ceb0ffa9b205290e75103d9dd6b1878e87b CI <CI@example.com> 1617580570 +1000 commit (initial): myfile1
55bf9e2babc63fb5bae4b42c79fb1ddd1f4205fa e68c1143545a09cac1db6e6f20db13b82b5dc43e CI <CI@example.com> 1617579407 +1000 commit: myfile2 47912ceb0ffa9b205290e75103d9dd6b1878e87b 6bd1486e023f9853f9e6f611cac5ccbc8960ce57 CI <CI@example.com> 1617580570 +1000 commit: myfile2
e68c1143545a09cac1db6e6f20db13b82b5dc43e ba001a041f64109b238f03550d6a21209bd5d4fc CI <CI@example.com> 1617579407 +1000 commit: myfile3 6bd1486e023f9853f9e6f611cac5ccbc8960ce57 980224a1dd75d91fdcab11edc8d25c3ff8f751ba CI <CI@example.com> 1617580570 +1000 commit: myfile3
ba001a041f64109b238f03550d6a21209bd5d4fc 6f31cc35ebabdf3fb71759e3d267677e712f737f CI <CI@example.com> 1617579407 +1000 commit: myfile4 980224a1dd75d91fdcab11edc8d25c3ff8f751ba f4e779d1bd2ad074259ad763210f5b911337054f CI <CI@example.com> 1617580570 +1000 commit: myfile4
6f31cc35ebabdf3fb71759e3d267677e712f737f ecc083de4d216847c184c72a639743bba6520d88 CI <CI@example.com> 1617579409 +1000 commit: commit f4e779d1bd2ad074259ad763210f5b911337054f d0cab53ed70fc66096575c2ccd7ef150b4b470e8 CI <CI@example.com> 1617580572 +1000 commit: commit

View File

@ -1,5 +1,5 @@
0000000000000000000000000000000000000000 55bf9e2babc63fb5bae4b42c79fb1ddd1f4205fa CI <CI@example.com> 1617579407 +1000 commit (initial): myfile1 0000000000000000000000000000000000000000 47912ceb0ffa9b205290e75103d9dd6b1878e87b CI <CI@example.com> 1617580570 +1000 commit (initial): myfile1
55bf9e2babc63fb5bae4b42c79fb1ddd1f4205fa e68c1143545a09cac1db6e6f20db13b82b5dc43e CI <CI@example.com> 1617579407 +1000 commit: myfile2 47912ceb0ffa9b205290e75103d9dd6b1878e87b 6bd1486e023f9853f9e6f611cac5ccbc8960ce57 CI <CI@example.com> 1617580570 +1000 commit: myfile2
e68c1143545a09cac1db6e6f20db13b82b5dc43e ba001a041f64109b238f03550d6a21209bd5d4fc CI <CI@example.com> 1617579407 +1000 commit: myfile3 6bd1486e023f9853f9e6f611cac5ccbc8960ce57 980224a1dd75d91fdcab11edc8d25c3ff8f751ba CI <CI@example.com> 1617580570 +1000 commit: myfile3
ba001a041f64109b238f03550d6a21209bd5d4fc 6f31cc35ebabdf3fb71759e3d267677e712f737f CI <CI@example.com> 1617579407 +1000 commit: myfile4 980224a1dd75d91fdcab11edc8d25c3ff8f751ba f4e779d1bd2ad074259ad763210f5b911337054f CI <CI@example.com> 1617580570 +1000 commit: myfile4
6f31cc35ebabdf3fb71759e3d267677e712f737f ecc083de4d216847c184c72a639743bba6520d88 CI <CI@example.com> 1617579409 +1000 commit: commit f4e779d1bd2ad074259ad763210f5b911337054f d0cab53ed70fc66096575c2ccd7ef150b4b470e8 CI <CI@example.com> 1617580572 +1000 commit: commit

View File

@ -0,0 +1,2 @@
x��A
�0@Q�9�����I��#M&X��R"����~��̖�r��*�J��d7��Y)J�4$��g�ᚲ\z�W�a��>NO�$�V��f ��#��p&DtG=&]��ξuY��16,�

View File

@ -1,2 +0,0 @@
x��A
�0@Ѯs��ʌ'J\y��L���")���#t�y�S5[˥���*�`��2�R���T</|��%��s��^u�i��4��������=��|�FWBDw�s��O��W�M�3�,�

View File

@ -1,4 +0,0 @@
x��A
1 �a�=E��4M�� "�j���)
��
�.<������kk���wU +���k�L�O�:Q�5�+��O��l���C��9ӤIR�4��� ���U&�F����0/p���~�mO=��]��p�6���f�#������e);a

View File

@ -0,0 +1,2 @@
x��K
�0@]�� 2�d��"BW=�4�`���D���#�}<x/o�-]cr�~�hS=6������{�l�K��Y�L �·��N�q��* k�<#Jɱʶ�X�̊���z��0N�p�W����5�I�@�@�������ڷ.�8�#�9}

View File

@ -1 +1 @@
ecc083de4d216847c184c72a639743bba6520d88 d0cab53ed70fc66096575c2ccd7ef150b4b470e8

View File

@ -1 +1 @@
{"KeyEvents":[{"Timestamp":9223372036854,"Mod":0,"Key":256,"Ch":32},{"Timestamp":9223372036854,"Mod":0,"Key":256,"Ch":99},{"Timestamp":9223372036854,"Mod":0,"Key":256,"Ch":99},{"Timestamp":9223372036854,"Mod":0,"Key":256,"Ch":111},{"Timestamp":9223372036854,"Mod":0,"Key":256,"Ch":109},{"Timestamp":9223372036854,"Mod":0,"Key":256,"Ch":109},{"Timestamp":9223372036854,"Mod":0,"Key":256,"Ch":105},{"Timestamp":9223372036854,"Mod":0,"Key":256,"Ch":116},{"Timestamp":9223372036854,"Mod":0,"Key":13,"Ch":13},{"Timestamp":9223372036854,"Mod":0,"Key":256,"Ch":113}]} {"KeyEvents":[{"Timestamp":527,"Mod":0,"Key":256,"Ch":32},{"Timestamp":830,"Mod":0,"Key":256,"Ch":99},{"Timestamp":1127,"Mod":0,"Key":256,"Ch":99},{"Timestamp":1190,"Mod":0,"Key":256,"Ch":111},{"Timestamp":1335,"Mod":0,"Key":256,"Ch":109},{"Timestamp":1447,"Mod":0,"Key":256,"Ch":109},{"Timestamp":1583,"Mod":0,"Key":256,"Ch":105},{"Timestamp":1606,"Mod":0,"Key":256,"Ch":116},{"Timestamp":1935,"Mod":0,"Key":13,"Ch":13},{"Timestamp":2353,"Mod":0,"Key":27,"Ch":0}],"ResizeEvents":[{"Timestamp":0,"Width":127,"Height":35}]}

View File

@ -1 +1 @@
{ "description": "stage a file and commit the change", "speed": 20 } { "description": "stage a file and commit the change", "speed": 15 }

View File

@ -2,7 +2,6 @@ package main
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
@ -16,12 +15,9 @@ import (
"github.com/jesseduffield/lazygit/pkg/secureexec" "github.com/jesseduffield/lazygit/pkg/secureexec"
) )
var errSubProcess = errors.New("subprocess")
type App struct { type App struct {
tests []*IntegrationTest tests []*IntegrationTest
itemIdx int itemIdx int
subProcess *exec.Cmd
testDir string testDir string
editing bool editing bool
g *gocui.Gui g *gocui.Gui
@ -70,9 +66,7 @@ func main() {
app := &App{testDir: testDir} app := &App{testDir: testDir}
app.loadTests() app.loadTests()
Loop: g, err := gocui.NewGui(gocui.OutputTrue, false, gocui.NORMAL, false)
for {
g, err := gocui.NewGui(gocui.OutputTrue, false, gocui.NORMAL)
if err != nil { if err != nil {
log.Panicln(err) log.Panicln(err)
} }
@ -106,10 +100,10 @@ Loop:
return nil return nil
} }
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("RECORD_EVENTS=true go test pkg/gui/gui_test.go -run /%s", currentTest.Name)) cmd := secureexec.Command("sh", "-c", fmt.Sprintf("RECORD_EVENTS=true go run integration/main.go %s", currentTest.Name))
app.subProcess = cmd app.runSubprocess(cmd)
return errSubProcess return nil
}); err != nil { }); err != nil {
log.Panicln(err) log.Panicln(err)
} }
@ -120,10 +114,10 @@ Loop:
return nil return nil
} }
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("go test pkg/gui/gui_test.go -run /%s", currentTest.Name)) cmd := secureexec.Command("sh", "-c", fmt.Sprintf("go run integration/main.go %s", currentTest.Name))
app.subProcess = cmd app.runSubprocess(cmd)
return errSubProcess return nil
}); err != nil { }); err != nil {
log.Panicln(err) log.Panicln(err)
} }
@ -257,10 +251,16 @@ Loop:
if err != nil { if err != nil {
switch err { switch err {
case gocui.ErrQuit: case gocui.ErrQuit:
break Loop return
default:
log.Panicln(err)
}
}
}
func (app *App) runSubprocess(cmd *exec.Cmd) {
gocui.Screen.Suspend()
case errSubProcess:
cmd := app.subProcess
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
@ -274,11 +274,7 @@ Loop:
fmt.Fprintf(os.Stdout, "\n%s", coloredString("press enter to return", color.FgGreen)) fmt.Fprintf(os.Stdout, "\n%s", coloredString("press enter to return", color.FgGreen))
fmt.Scanln() // wait for enter press fmt.Scanln() // wait for enter press
default: gocui.Screen.Resume()
log.Panicln(err)
}
}
}
} }
func (app *App) layout(g *gocui.Gui) error { func (app *App) layout(g *gocui.Gui) error {

View File

@ -86,10 +86,12 @@ const (
type Recording struct { type Recording struct {
KeyEvents []*TcellKeyEventWrapper KeyEvents []*TcellKeyEventWrapper
ResizeEvents []*TcellResizeEventWrapper
} }
type replayedEvents struct { type replayedEvents struct {
keys chan *TcellKeyEventWrapper keys chan *TcellKeyEventWrapper
resizes chan *TcellResizeEventWrapper
} }
type RecordingConfig struct { type RecordingConfig struct {
@ -159,14 +161,19 @@ type Gui struct {
} }
// NewGui returns a new Gui object with a given output mode. // NewGui returns a new Gui object with a given output mode.
func NewGui(mode OutputMode, supportOverlaps bool, playMode PlayMode) (*Gui, error) { func NewGui(mode OutputMode, supportOverlaps bool, playMode PlayMode, headless bool) (*Gui, error) {
err := tcellInit() g := &Gui{}
var err error
if headless {
err = tcellInitSimulation()
} else {
err = tcellInit()
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
g := &Gui{}
g.outputMode = mode g.outputMode = mode
g.stop = make(chan struct{}) g.stop = make(chan struct{})
@ -177,10 +184,12 @@ func NewGui(mode OutputMode, supportOverlaps bool, playMode PlayMode) (*Gui, err
if playMode == RECORDING { if playMode == RECORDING {
g.Recording = &Recording{ g.Recording = &Recording{
KeyEvents: []*TcellKeyEventWrapper{}, KeyEvents: []*TcellKeyEventWrapper{},
ResizeEvents: []*TcellResizeEventWrapper{},
} }
} else if playMode == REPLAYING { } else if playMode == REPLAYING {
g.ReplayedEvents = replayedEvents{ g.ReplayedEvents = replayedEvents{
keys: make(chan *TcellKeyEventWrapper), keys: make(chan *TcellKeyEventWrapper),
resizes: make(chan *TcellResizeEventWrapper),
} }
} }
@ -562,8 +571,10 @@ func (g *Gui) SetManagerFunc(manager func(*Gui) error) {
// MainLoop runs the main loop until an error is returned. A successful // MainLoop runs the main loop until an error is returned. A successful
// finish should return ErrQuit. // finish should return ErrQuit.
func (g *Gui) MainLoop() error { func (g *Gui) MainLoop() error {
g.StartTime = time.Now()
if g.PlayMode == REPLAYING { if g.PlayMode == REPLAYING {
g.replayRecording() go g.replayRecording()
} }
go func() { go func() {
@ -1182,6 +1193,11 @@ func IsQuit(err error) bool {
} }
func (g *Gui) replayRecording() { func (g *Gui) replayRecording() {
waitGroup := sync.WaitGroup{}
waitGroup.Add(2)
go func() {
ticker := time.NewTicker(time.Millisecond) ticker := time.NewTicker(time.Millisecond)
defer ticker.Stop() defer ticker.Stop()
@ -1189,7 +1205,6 @@ func (g *Gui) replayRecording() {
// Therefore we can't just check for a given event whether we've passed its timestamp, // Therefore we can't just check for a given event whether we've passed its timestamp,
// or else we'll have an explosion of keypresses after the test is resumed. // or else we'll have an explosion of keypresses after the test is resumed.
// We need to check if we've waited long enough since the last event was replayed. // We need to check if we've waited long enough since the last event was replayed.
// Only handling key events for now.
for i, event := range g.Recording.KeyEvents { for i, event := range g.Recording.KeyEvents {
var prevEventTimestamp int64 = 0 var prevEventTimestamp int64 = 0
if i > 0 { if i > 0 {
@ -1205,7 +1220,7 @@ func (g *Gui) replayRecording() {
select { select {
case <-ticker.C: case <-ticker.C:
timeWaited += 1 timeWaited += 1
if g != nil && timeWaited >= timeToWait { if timeWaited >= timeToWait {
g.ReplayedEvents.keys <- event g.ReplayedEvents.keys <- event
break middle break middle
} }
@ -1215,6 +1230,44 @@ func (g *Gui) replayRecording() {
} }
} }
waitGroup.Done()
}()
go func() {
ticker := time.NewTicker(time.Millisecond)
defer ticker.Stop()
// duplicating until Go gets generics
for i, event := range g.Recording.ResizeEvents {
var prevEventTimestamp int64 = 0
if i > 0 {
prevEventTimestamp = g.Recording.ResizeEvents[i-1].Timestamp
}
timeToWait := (event.Timestamp - prevEventTimestamp) / int64(g.RecordingConfig.Speed)
if i == 0 {
timeToWait += int64(g.RecordingConfig.Leeway)
}
var timeWaited int64 = 0
middle2:
for {
select {
case <-ticker.C:
timeWaited += 1
if timeWaited >= timeToWait {
g.ReplayedEvents.resizes <- event
break middle2
}
case <-g.stop:
return
}
}
}
waitGroup.Done()
}()
waitGroup.Wait()
// leaving some time for any handlers to execute before quitting // leaving some time for any handlers to execute before quitting
time.Sleep(time.Second * 1) time.Sleep(time.Second * 1)

View File

@ -38,6 +38,17 @@ func tcellInit() error {
} }
} }
// tcellInitSimulation initializes tcell screen for use.
func tcellInitSimulation() error {
s := tcell.NewSimulationScreen("")
if e := s.Init(); e != nil {
return e
} else {
Screen = s
return nil
}
}
// tcellSetCell sets the character cell at a given location to the given // tcellSetCell sets the character cell at a given location to the given
// content (rune) and attributes using provided OutputMode // content (rune) and attributes using provided OutputMode
func tcellSetCell(x, y int, ch rune, fg, bg Attribute, outputMode OutputMode) { func tcellSetCell(x, y int, ch rune, fg, bg Attribute, outputMode OutputMode) {
@ -166,6 +177,26 @@ func (wrapper TcellKeyEventWrapper) toTcellEvent() tcell.Event {
return tcell.NewEventKey(wrapper.Key, wrapper.Ch, wrapper.Mod) return tcell.NewEventKey(wrapper.Key, wrapper.Ch, wrapper.Mod)
} }
type TcellResizeEventWrapper struct {
Timestamp int64
Width int
Height int
}
func NewTcellResizeEventWrapper(event *tcell.EventResize, timestamp int64) *TcellResizeEventWrapper {
w, h := event.Size()
return &TcellResizeEventWrapper{
Timestamp: timestamp,
Width: w,
Height: h,
}
}
func (wrapper TcellResizeEventWrapper) toTcellEvent() tcell.Event {
return tcell.NewEventResize(wrapper.Width, wrapper.Height)
}
func (g *Gui) timeSinceStart() int64 { func (g *Gui) timeSinceStart() int64 {
return time.Since(g.StartTime).Nanoseconds() / 1e6 return time.Since(g.StartTime).Nanoseconds() / 1e6
} }
@ -177,6 +208,8 @@ func (g *Gui) pollEvent() GocuiEvent {
select { select {
case ev := <-g.ReplayedEvents.keys: case ev := <-g.ReplayedEvents.keys:
tev = (ev).toTcellEvent() tev = (ev).toTcellEvent()
case ev := <-g.ReplayedEvents.resizes:
tev = (ev).toTcellEvent()
} }
} else { } else {
tev = Screen.PollEvent() tev = Screen.PollEvent()
@ -186,6 +219,12 @@ func (g *Gui) pollEvent() GocuiEvent {
case *tcell.EventInterrupt: case *tcell.EventInterrupt:
return GocuiEvent{Type: eventInterrupt} return GocuiEvent{Type: eventInterrupt}
case *tcell.EventResize: case *tcell.EventResize:
if g.PlayMode == RECORDING {
g.Recording.ResizeEvents = append(
g.Recording.ResizeEvents, NewTcellResizeEventWrapper(tev, g.timeSinceStart()),
)
}
w, h := tev.Size() w, h := tev.Size()
return GocuiEvent{Type: eventResize, Width: w, Height: h} return GocuiEvent{Type: eventResize, Width: w, Height: h}
case *tcell.EventKey: case *tcell.EventKey: