1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-03-17 21:18:31 +02:00

support tcell simulation screen

This commit is contained in:
Jesse Duffield 2021-04-05 10:20:02 +10:00
parent 7782fe92d8
commit b3696e2005
22 changed files with 358 additions and 259 deletions

View File

@ -5,6 +5,7 @@ import (
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"strconv"
@ -13,6 +14,7 @@ import (
"github.com/creack/pty"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/stretchr/testify/assert"
)
// To run an integration test, e.g. for test 'commit', go:
@ -209,19 +211,19 @@ func Test() error {
speeds := getTestSpeeds(test.Speed, updateSnapshots)
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)
actualDir := filepath.Join(testPath, "actual")
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)
prepareIntegrationTestDir(actualDir)
err := createFixture(testPath, actualDir)
if err != nil {
return err
// return err
}
runLazygit(testPath, rootDir, record, speed)
@ -268,13 +270,14 @@ func Test() error {
}()
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
}
// if the snapshots and we haven't tried all playback speeds different we'll retry at a slower speed
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
}
type MockTestingT struct{}
func (t MockTestingT) Errorf(format string, args ...interface{}) {
fmt.Printf(format, args...)
}
func createFixture(testPath, actualDir string) error {
osCommand := oscommands.NewDummyOSCommand()
bashScriptPath := filepath.Join(testPath, "setup.sh")

View File

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

View File

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

View File

@ -1,5 +1,5 @@
0000000000000000000000000000000000000000 55bf9e2babc63fb5bae4b42c79fb1ddd1f4205fa CI <CI@example.com> 1617579407 +1000 commit (initial): myfile1
55bf9e2babc63fb5bae4b42c79fb1ddd1f4205fa e68c1143545a09cac1db6e6f20db13b82b5dc43e CI <CI@example.com> 1617579407 +1000 commit: myfile2
e68c1143545a09cac1db6e6f20db13b82b5dc43e ba001a041f64109b238f03550d6a21209bd5d4fc CI <CI@example.com> 1617579407 +1000 commit: myfile3
ba001a041f64109b238f03550d6a21209bd5d4fc 6f31cc35ebabdf3fb71759e3d267677e712f737f CI <CI@example.com> 1617579407 +1000 commit: myfile4
6f31cc35ebabdf3fb71759e3d267677e712f737f ecc083de4d216847c184c72a639743bba6520d88 CI <CI@example.com> 1617579409 +1000 commit: commit
0000000000000000000000000000000000000000 47912ceb0ffa9b205290e75103d9dd6b1878e87b CI <CI@example.com> 1617580570 +1000 commit (initial): myfile1
47912ceb0ffa9b205290e75103d9dd6b1878e87b 6bd1486e023f9853f9e6f611cac5ccbc8960ce57 CI <CI@example.com> 1617580570 +1000 commit: myfile2
6bd1486e023f9853f9e6f611cac5ccbc8960ce57 980224a1dd75d91fdcab11edc8d25c3ff8f751ba CI <CI@example.com> 1617580570 +1000 commit: myfile3
980224a1dd75d91fdcab11edc8d25c3ff8f751ba f4e779d1bd2ad074259ad763210f5b911337054f CI <CI@example.com> 1617580570 +1000 commit: myfile4
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
55bf9e2babc63fb5bae4b42c79fb1ddd1f4205fa e68c1143545a09cac1db6e6f20db13b82b5dc43e CI <CI@example.com> 1617579407 +1000 commit: myfile2
e68c1143545a09cac1db6e6f20db13b82b5dc43e ba001a041f64109b238f03550d6a21209bd5d4fc CI <CI@example.com> 1617579407 +1000 commit: myfile3
ba001a041f64109b238f03550d6a21209bd5d4fc 6f31cc35ebabdf3fb71759e3d267677e712f737f CI <CI@example.com> 1617579407 +1000 commit: myfile4
6f31cc35ebabdf3fb71759e3d267677e712f737f ecc083de4d216847c184c72a639743bba6520d88 CI <CI@example.com> 1617579409 +1000 commit: commit
0000000000000000000000000000000000000000 47912ceb0ffa9b205290e75103d9dd6b1878e87b CI <CI@example.com> 1617580570 +1000 commit (initial): myfile1
47912ceb0ffa9b205290e75103d9dd6b1878e87b 6bd1486e023f9853f9e6f611cac5ccbc8960ce57 CI <CI@example.com> 1617580570 +1000 commit: myfile2
6bd1486e023f9853f9e6f611cac5ccbc8960ce57 980224a1dd75d91fdcab11edc8d25c3ff8f751ba CI <CI@example.com> 1617580570 +1000 commit: myfile3
980224a1dd75d91fdcab11edc8d25c3ff8f751ba f4e779d1bd2ad074259ad763210f5b911337054f CI <CI@example.com> 1617580570 +1000 commit: myfile4
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 (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
@ -16,15 +15,12 @@ import (
"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
tests []*IntegrationTest
itemIdx int
testDir string
editing bool
g *gocui.Gui
}
func (app *App) getCurrentTest() *IntegrationTest {
@ -70,217 +66,217 @@ func main() {
app := &App{testDir: testDir}
app.loadTests()
Loop:
for {
g, err := gocui.NewGui(gocui.OutputTrue, false, gocui.NORMAL)
g, err := gocui.NewGui(gocui.OutputTrue, false, gocui.NORMAL, false)
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 run integration/main.go %s", currentTest.Name))
app.runSubprocess(cmd)
return nil
}); 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 run integration/main.go %s", currentTest.Name))
app.runSubprocess(cmd)
return nil
}); 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 {
log.Panicln(err)
return err
}
editorView.Clear()
fmt.Fprint(editorView, currentTest.Name)
g.Cursor = false
return nil
}); err != nil {
log.Panicln(err)
}
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--
}
if err := g.SetKeybinding("list", nil, 'd', gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
currentTest := app.getCurrentTest()
if currentTest == nil {
return nil
}); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("list", nil, gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
log.Panicln(err)
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
}
if err := g.SetKeybinding("list", nil, 'q', gocui.ModNone, quit); err != nil {
log.Panicln(err)
}
app.refreshTests()
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("editor", nil, gocui.KeyEnter, gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
currentTest := app.getCurrentTest()
if currentTest == nil {
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)
app.editing = false
if _, err := g.SetCurrentView("list"); err != nil {
return 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()
editorView, err := g.View("editor")
if err != nil {
switch err {
case gocui.ErrQuit:
break Loop
return err
}
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
dir := app.testDir + "/" + app.getCurrentTest().Name
newDir := app.testDir + "/" + editorView.Buffer()
fmt.Fprintf(os.Stdout, "\n%s", coloredString("press enter to return", color.FgGreen))
fmt.Scanln() // wait for enter press
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("mv %s %s", dir, newDir))
if err := cmd.Run(); err != nil {
return err
}
default:
log.Panicln(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:
return
default:
log.Panicln(err)
}
}
}
func (app *App) runSubprocess(cmd *exec.Cmd) {
gocui.Screen.Suspend()
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
gocui.Screen.Resume()
}
func (app *App) layout(g *gocui.Gui) error {
maxX, maxY := g.Size()
descriptionViewHeight := 7

View File

@ -85,11 +85,13 @@ const (
)
type Recording struct {
KeyEvents []*TcellKeyEventWrapper
KeyEvents []*TcellKeyEventWrapper
ResizeEvents []*TcellResizeEventWrapper
}
type replayedEvents struct {
keys chan *TcellKeyEventWrapper
keys chan *TcellKeyEventWrapper
resizes chan *TcellResizeEventWrapper
}
type RecordingConfig struct {
@ -159,14 +161,19 @@ type Gui struct {
}
// NewGui returns a new Gui object with a given output mode.
func NewGui(mode OutputMode, supportOverlaps bool, playMode PlayMode) (*Gui, error) {
err := tcellInit()
func NewGui(mode OutputMode, supportOverlaps bool, playMode PlayMode, headless bool) (*Gui, error) {
g := &Gui{}
var err error
if headless {
err = tcellInitSimulation()
} else {
err = tcellInit()
}
if err != nil {
return nil, err
}
g := &Gui{}
g.outputMode = mode
g.stop = make(chan struct{})
@ -176,11 +183,13 @@ func NewGui(mode OutputMode, supportOverlaps bool, playMode PlayMode) (*Gui, err
if playMode == RECORDING {
g.Recording = &Recording{
KeyEvents: []*TcellKeyEventWrapper{},
KeyEvents: []*TcellKeyEventWrapper{},
ResizeEvents: []*TcellResizeEventWrapper{},
}
} else if playMode == REPLAYING {
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
// finish should return ErrQuit.
func (g *Gui) MainLoop() error {
g.StartTime = time.Now()
if g.PlayMode == REPLAYING {
g.replayRecording()
go g.replayRecording()
}
go func() {
@ -1182,38 +1193,80 @@ func IsQuit(err error) bool {
}
func (g *Gui) replayRecording() {
ticker := time.NewTicker(time.Millisecond)
defer ticker.Stop()
waitGroup := sync.WaitGroup{}
// The playback could be paused at any time because integration tests run concurrently.
// 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.
// 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 {
var prevEventTimestamp int64 = 0
if i > 0 {
prevEventTimestamp = g.Recording.KeyEvents[i-1].Timestamp
}
timeToWait := (event.Timestamp - prevEventTimestamp) / int64(g.RecordingConfig.Speed)
if i == 0 {
timeToWait += int64(g.RecordingConfig.Leeway)
}
var timeWaited int64 = 0
middle:
for {
select {
case <-ticker.C:
timeWaited += 1
if g != nil && timeWaited >= timeToWait {
g.ReplayedEvents.keys <- event
break middle
waitGroup.Add(2)
go func() {
ticker := time.NewTicker(time.Millisecond)
defer ticker.Stop()
// The playback could be paused at any time because integration tests run concurrently.
// 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.
// We need to check if we've waited long enough since the last event was replayed.
for i, event := range g.Recording.KeyEvents {
var prevEventTimestamp int64 = 0
if i > 0 {
prevEventTimestamp = g.Recording.KeyEvents[i-1].Timestamp
}
timeToWait := (event.Timestamp - prevEventTimestamp) / int64(g.RecordingConfig.Speed)
if i == 0 {
timeToWait += int64(g.RecordingConfig.Leeway)
}
var timeWaited int64 = 0
middle:
for {
select {
case <-ticker.C:
timeWaited += 1
if timeWaited >= timeToWait {
g.ReplayedEvents.keys <- event
break middle
}
case <-g.stop:
return
}
case <-g.stop:
return
}
}
}
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
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
// content (rune) and attributes using provided 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)
}
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 {
return time.Since(g.StartTime).Nanoseconds() / 1e6
}
@ -177,6 +208,8 @@ func (g *Gui) pollEvent() GocuiEvent {
select {
case ev := <-g.ReplayedEvents.keys:
tev = (ev).toTcellEvent()
case ev := <-g.ReplayedEvents.resizes:
tev = (ev).toTcellEvent()
}
} else {
tev = Screen.PollEvent()
@ -186,6 +219,12 @@ func (g *Gui) pollEvent() GocuiEvent {
case *tcell.EventInterrupt:
return GocuiEvent{Type: eventInterrupt}
case *tcell.EventResize:
if g.PlayMode == RECORDING {
g.Recording.ResizeEvents = append(
g.Recording.ResizeEvents, NewTcellResizeEventWrapper(tev, g.timeSinceStart()),
)
}
w, h := tev.Size()
return GocuiEvent{Type: eventResize, Width: w, Height: h}
case *tcell.EventKey: