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

move recording code into gocui

This commit is contained in:
Jesse Duffield 2021-04-05 08:52:09 +10:00
parent 41747b5b34
commit d4622bac30
5 changed files with 171 additions and 123 deletions

View File

@ -3,6 +3,7 @@ package gui
import ( import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log"
"os" "os"
"runtime" "runtime"
"sync" "sync"
@ -89,11 +90,6 @@ type Gui struct {
// recent repo with the recent repos popup showing // recent repo with the recent repos popup showing
showRecentRepos bool showRecentRepos bool
// this array either includes the events that we're recording in this session
// or the events we've recorded in a prior session
RecordedEvents []RecordedEvent
StartTime time.Time
Mutexes guiStateMutexes Mutexes guiStateMutexes
// findSuggestions will take a string that the user has typed into a prompt // findSuggestions will take a string that the user has typed into a prompt
@ -110,11 +106,6 @@ type Gui struct {
Views Views Views Views
} }
type RecordedEvent struct {
Timestamp int64
Event *gocui.GocuiEvent
}
type listPanelState struct { type listPanelState struct {
SelectedLineIdx int SelectedLineIdx int
} }
@ -446,7 +437,6 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *oscom
statusManager: &statusManager{}, statusManager: &statusManager{},
viewBufferManagerMap: map[string]*tasks.ViewBufferManager{}, viewBufferManagerMap: map[string]*tasks.ViewBufferManager{},
showRecentRepos: showRecentRepos, showRecentRepos: showRecentRepos,
RecordedEvents: []RecordedEvent{},
RepoPathStack: []string{}, RepoPathStack: []string{},
RepoStateMap: map[Repo]*guiState{}, RepoStateMap: map[Repo]*guiState{},
} }
@ -461,16 +451,35 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *oscom
// Run setup the gui with keybindings and start the mainloop // Run setup the gui with keybindings and start the mainloop
func (gui *Gui) Run() error { func (gui *Gui) Run() error {
recordEvents := recordingEvents() recordEvents := recordingEvents()
playMode := gocui.NORMAL
if recordEvents {
playMode = gocui.RECORDING
} else if replaying() {
playMode = gocui.REPLAYING
}
g, err := gocui.NewGui(gocui.OutputTrue, OverlappingEdges, recordEvents) g, err := gocui.NewGui(gocui.OutputTrue, OverlappingEdges, playMode)
if err != nil { if err != nil {
return err return err
} }
gui.g = g // TODO: always use gui.g rather than passing g around everywhere gui.g = g // TODO: always use gui.g rather than passing g around everywhere
defer g.Close() defer g.Close()
if recordEvents { if replaying() {
go utils.Safe(gui.recordEvents) g.RecordingConfig = gocui.RecordingConfig{
Speed: getRecordingSpeed(),
Leeway: 0,
}
g.Recording, err = gui.loadRecording()
if err != nil {
return err
}
go utils.Safe(func() {
time.Sleep(time.Second * 20)
log.Fatal("20 seconds is up, lazygit recording took too long to complete")
})
} }
g.OnSearchEscape = gui.onSearchEscape g.OnSearchEscape = gui.onSearchEscape
@ -518,9 +527,6 @@ func (gui *Gui) Run() error {
// RunAndHandleError // RunAndHandleError
func (gui *Gui) RunAndHandleError() error { func (gui *Gui) RunAndHandleError() error {
gui.StartTime = time.Now()
go utils.Safe(gui.replayRecordedEvents)
gui.stopChan = make(chan struct{}) gui.stopChan = make(chan struct{})
return utils.SafeWithError(func() error { return utils.SafeWithError(func() error {
if err := gui.Run(); err != nil { if err := gui.Run(); err != nil {
@ -542,7 +548,7 @@ func (gui *Gui) RunAndHandleError() error {
} }
} }
if err := gui.saveRecordedEvents(); err != nil { if err := gui.saveRecording(gui.g.Recording); err != nil {
return err return err
} }

View File

@ -6,10 +6,8 @@ import (
"log" "log"
"os" "os"
"strconv" "strconv"
"time"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
) )
func recordingEvents() bool { func recordingEvents() bool {
@ -20,31 +18,11 @@ func recordEventsTo() string {
return os.Getenv("RECORD_EVENTS_TO") return os.Getenv("RECORD_EVENTS_TO")
} }
func (gui *Gui) timeSinceStart() int64 { func replaying() bool {
return time.Since(gui.StartTime).Nanoseconds() / 1e6 return os.Getenv("REPLAY_EVENTS_FROM") != ""
} }
func (gui *Gui) replayRecordedEvents() { func getRecordingSpeed() int {
gui.Log.Warn("going to replay events")
if os.Getenv("REPLAY_EVENTS_FROM") == "" {
return
}
go utils.Safe(func() {
time.Sleep(time.Second * 20)
log.Fatal("20 seconds is up, lazygit recording took too long to complete")
})
events, err := gui.loadRecordedEvents()
if err != nil {
log.Fatal(err)
}
ticker := time.NewTicker(time.Millisecond)
defer ticker.Stop()
// might need to add leeway if this ends up flakey
var leeway int64 = 0
// humans are slow so this speeds things up. // humans are slow so this speeds things up.
speed := 1 speed := 1
envReplaySpeed := os.Getenv("REPLAY_SPEED") envReplaySpeed := os.Getenv("REPLAY_SPEED")
@ -55,49 +33,10 @@ func (gui *Gui) replayRecordedEvents() {
log.Fatal(err) log.Fatal(err)
} }
} }
return speed
// 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 events {
var prevEventTimestamp int64 = 0
if i > 0 {
prevEventTimestamp = events[i-1].Timestamp
}
timeToWait := (event.Timestamp - prevEventTimestamp) / int64(speed)
if i == 0 {
timeToWait += leeway
}
var timeWaited int64 = 0
middle:
for {
select {
case <-ticker.C:
timeWaited += 1
if gui.g != nil && timeWaited >= timeToWait {
gui.Log.Warn("replaying event")
gui.g.ReplayedEvents <- *event.Event
break middle
}
case <-gui.stopChan:
return
}
}
}
time.Sleep(time.Second * 1)
gui.g.Update(func(*gocui.Gui) error {
return gocui.ErrQuit
})
time.Sleep(time.Second * 1)
log.Fatal("lazygit should have already exited")
} }
func (gui *Gui) loadRecordedEvents() ([]RecordedEvent, error) { func (gui *Gui) loadRecording() (*gocui.Recording, error) {
path := os.Getenv("REPLAY_EVENTS_FROM") path := os.Getenv("REPLAY_EVENTS_FROM")
data, err := ioutil.ReadFile(path) data, err := ioutil.ReadFile(path)
@ -105,22 +44,22 @@ func (gui *Gui) loadRecordedEvents() ([]RecordedEvent, error) {
return nil, err return nil, err
} }
events := []RecordedEvent{} recording := &gocui.Recording{}
err = json.Unmarshal(data, &events) err = json.Unmarshal(data, &recording)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return events, nil return recording, nil
} }
func (gui *Gui) saveRecordedEvents() error { func (gui *Gui) saveRecording(recording *gocui.Recording) error {
if !recordingEvents() { if !recordingEvents() {
return nil return nil
} }
jsonEvents, err := json.Marshal(gui.RecordedEvents) jsonEvents, err := json.Marshal(recording)
if err != nil { if err != nil {
return err return err
} }
@ -129,14 +68,3 @@ func (gui *Gui) saveRecordedEvents() error {
return ioutil.WriteFile(path, jsonEvents, 0600) return ioutil.WriteFile(path, jsonEvents, 0600)
} }
func (gui *Gui) recordEvents() {
for event := range gui.g.RecordedEvents {
recordedEvent := RecordedEvent{
Timestamp: gui.timeSinceStart(),
Event: event,
}
gui.RecordedEvents = append(gui.RecordedEvents, recordedEvent)
}
}

View File

@ -75,13 +75,36 @@ type GuiMutexes struct {
ViewsMutex sync.Mutex ViewsMutex sync.Mutex
} }
type PlayMode int
const (
NORMAL PlayMode = iota
RECORDING
REPLAYING
)
type Recording struct {
KeyEvents []*TcellKeyEventWrapper
}
type replayedEvents struct {
keys chan *TcellKeyEventWrapper
}
type RecordingConfig struct {
Speed int
Leeway int
}
// Gui represents the whole User Interface, including the views, layouts // Gui represents the whole User Interface, including the views, layouts
// and keybindings. // and keybindings.
type Gui struct { type Gui struct {
// ReplayedEvents is a channel for passing pre-recorded input events, for the purposes of testing RecordingConfig
ReplayedEvents chan GocuiEvent Recording *Recording
RecordEvents bool // ReplayedEvents is for passing pre-recorded input events, for the purposes of testing
RecordedEvents chan *GocuiEvent ReplayedEvents replayedEvents
PlayMode PlayMode
StartTime time.Time
tabClickBindings []*tabClickBinding tabClickBindings []*tabClickBinding
gEvents chan GocuiEvent gEvents chan GocuiEvent
@ -135,7 +158,7 @@ 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, recordEvents bool) (*Gui, error) { func NewGui(mode OutputMode, supportOverlaps bool, playMode PlayMode) (*Gui, error) {
err := tcellInit() err := tcellInit()
if err != nil { if err != nil {
return nil, err return nil, err
@ -147,10 +170,18 @@ func NewGui(mode OutputMode, supportOverlaps bool, recordEvents bool) (*Gui, err
g.stop = make(chan struct{}) g.stop = make(chan struct{})
g.ReplayedEvents = make(chan GocuiEvent)
g.gEvents = make(chan GocuiEvent, 20) g.gEvents = make(chan GocuiEvent, 20)
g.userEvents = make(chan userEvent, 20) g.userEvents = make(chan userEvent, 20)
g.RecordedEvents = make(chan *GocuiEvent)
if playMode == RECORDING {
g.Recording = &Recording{
KeyEvents: []*TcellKeyEventWrapper{},
}
} else if playMode == REPLAYING {
g.ReplayedEvents = replayedEvents{
keys: make(chan *TcellKeyEventWrapper),
}
}
if runtime.GOOS != "windows" { if runtime.GOOS != "windows" {
g.maxX, g.maxY, err = g.getTermWindowSize() g.maxX, g.maxY, err = g.getTermWindowSize()
@ -173,7 +204,7 @@ func NewGui(mode OutputMode, supportOverlaps bool, recordEvents bool) (*Gui, err
g.NextSearchMatchKey = 'n' g.NextSearchMatchKey = 'n'
g.PrevSearchMatchKey = 'N' g.PrevSearchMatchKey = 'N'
g.RecordEvents = recordEvents g.PlayMode = playMode
return g, nil return g, nil
} }
@ -533,13 +564,17 @@ 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 {
if g.PlayMode == REPLAYING {
g.replayRecording()
}
go func() { go func() {
for { for {
select { select {
case <-g.stop: case <-g.stop:
return return
default: default:
g.gEvents <- pollEvent() g.gEvents <- g.pollEvent()
} }
} }
}() }()
@ -554,10 +589,6 @@ func (g *Gui) MainLoop() error {
if err := g.handleEvent(&ev); err != nil { if err := g.handleEvent(&ev); err != nil {
return err return err
} }
case ev := <-g.ReplayedEvents:
if err := g.handleEvent(&ev); err != nil {
return err
}
case ev := <-g.userEvents: case ev := <-g.userEvents:
if err := ev.f(g); err != nil { if err := ev.f(g); err != nil {
return err return err
@ -580,10 +611,6 @@ func (g *Gui) consumeevents() error {
if err := g.handleEvent(&ev); err != nil { if err := g.handleEvent(&ev); err != nil {
return err return err
} }
case ev := <-g.ReplayedEvents:
if err := g.handleEvent(&ev); err != nil {
return err
}
case ev := <-g.userEvents: case ev := <-g.userEvents:
if err := ev.f(g); err != nil { if err := ev.f(g); err != nil {
return err return err
@ -597,10 +624,6 @@ func (g *Gui) consumeevents() error {
// handleEvent handles an event, based on its type (key-press, error, // handleEvent handles an event, based on its type (key-press, error,
// etc.) // etc.)
func (g *Gui) handleEvent(ev *GocuiEvent) error { func (g *Gui) handleEvent(ev *GocuiEvent) error {
if g.RecordEvents {
g.RecordedEvents <- ev
}
switch ev.Type { switch ev.Type {
case eventKey, eventMouse: case eventKey, eventMouse:
return g.onKey(ev) return g.onKey(ev)

52
vendor/github.com/jesseduffield/gocui/recording.go generated vendored Normal file
View File

@ -0,0 +1,52 @@
package gocui
import (
"log"
"time"
)
func (g *Gui) replayRecording() {
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.
// 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
}
case <-g.stop:
return
}
}
}
// leaving some time for any handlers to execute before quitting
time.Sleep(time.Second * 1)
g.Update(func(*Gui) error {
return ErrQuit
})
time.Sleep(time.Second * 1)
log.Fatal("gocui should have already exited")
}

View File

@ -6,6 +6,7 @@ package gocui
import ( import (
"sync" "sync"
"time"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
) )
@ -144,9 +145,43 @@ var (
lastY int = 0 lastY int = 0
) )
// this wrapper struct has public keys so we can easily serialize/deserialize to JSON
type TcellKeyEventWrapper struct {
Timestamp int64
Mod tcell.ModMask
Key tcell.Key
Ch rune
}
func NewTcellKeyEventWrapper(event *tcell.EventKey, timestamp int64) *TcellKeyEventWrapper {
return &TcellKeyEventWrapper{
Timestamp: timestamp,
Mod: event.Modifiers(),
Key: event.Key(),
Ch: event.Rune(),
}
}
func (wrapper TcellKeyEventWrapper) toTcellEvent() tcell.Event {
return tcell.NewEventKey(wrapper.Key, wrapper.Ch, wrapper.Mod)
}
func (g *Gui) timeSinceStart() int64 {
return time.Since(g.StartTime).Nanoseconds() / 1e6
}
// pollEvent get tcell.Event and transform it into gocuiEvent // pollEvent get tcell.Event and transform it into gocuiEvent
func pollEvent() GocuiEvent { func (g *Gui) pollEvent() GocuiEvent {
tev := Screen.PollEvent() var tev tcell.Event
if g.PlayMode == REPLAYING {
select {
case ev := <-g.ReplayedEvents.keys:
tev = (ev).toTcellEvent()
}
} else {
tev = Screen.PollEvent()
}
switch tev := tev.(type) { switch tev := tev.(type) {
case *tcell.EventInterrupt: case *tcell.EventInterrupt:
return GocuiEvent{Type: eventInterrupt} return GocuiEvent{Type: eventInterrupt}
@ -154,6 +189,10 @@ func pollEvent() GocuiEvent {
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:
if g.PlayMode == RECORDING {
g.Recording.KeyEvents = append(g.Recording.KeyEvents, NewTcellKeyEventWrapper(tev, g.timeSinceStart()))
}
k := tev.Key() k := tev.Key()
ch := rune(0) ch := rune(0)
if k == tcell.KeyRune { if k == tcell.KeyRune {