diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index e4a554181..c1df0ee93 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -31,6 +31,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/theme" "github.com/jesseduffield/lazygit/pkg/updates" "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/jesseduffield/termbox-go" "github.com/mattn/go-runewidth" "github.com/sirupsen/logrus" ) @@ -107,6 +108,16 @@ type Gui struct { showRecentRepos bool Contexts ContextTree ViewTabContextMap map[string][]tabContext + + // 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 +} + +type RecordedEvent struct { + Timestamp int64 + Event *termbox.Event } type listPanelState struct { @@ -399,6 +410,7 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *oscom statusManager: &statusManager{}, viewBufferManagerMap: map[string]*tasks.ViewBufferManager{}, showRecentRepos: showRecentRepos, + RecordedEvents: []RecordedEvent{}, } gui.resetState() @@ -417,12 +429,18 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *oscom func (gui *Gui) Run() error { gui.resetState() - g, err := gocui.NewGui(gocui.Output256, OverlappingEdges) + recordEvents := recordingEvents() + + g, err := gocui.NewGui(gocui.Output256, OverlappingEdges, recordEvents) if err != nil { return err } defer g.Close() + if recordEvents { + go gui.recordEvents() + } + if gui.State.Modes.Filtering.Active() { gui.State.ScreenMode = SCREEN_HALF } else { @@ -475,6 +493,9 @@ func (gui *Gui) Run() error { // if the error returned from a run is a ErrSubProcess, it runs the subprocess // otherwise it handles the error, possibly by quitting the application func (gui *Gui) RunWithSubprocesses() error { + gui.StartTime = time.Now() + go gui.replayRecordedEvents() + for { gui.stopChan = make(chan struct{}) if err := gui.Run(); err != nil { @@ -497,6 +518,10 @@ func (gui *Gui) RunWithSubprocesses() error { } } + if err := gui.saveRecordedEvents(); err != nil { + return err + } + return nil case gui.Errors.ErrSwitchRepo, gui.Errors.ErrRestart: continue diff --git a/pkg/gui/recording.go b/pkg/gui/recording.go new file mode 100644 index 000000000..5e0914303 --- /dev/null +++ b/pkg/gui/recording.go @@ -0,0 +1,85 @@ +package gui + +import ( + "encoding/json" + "io/ioutil" + "log" + "os" + "time" +) + +func recordingEvents() bool { + return os.Getenv("RECORD_EVENTS") == "true" +} + +func (gui *Gui) timeSinceStart() int64 { + return time.Since(gui.StartTime).Milliseconds() +} + +func (gui *Gui) replayRecordedEvents() { + if os.Getenv("REPLAY_EVENTS_FROM") == "" { + return + } + + events, err := gui.loadRecordedEvents() + if err != nil { + log.Fatal(err) + } + + ticker := time.NewTicker(time.Millisecond) + defer ticker.Stop() + + var leeway int64 = 1000 + + for _, event := range events { + for range ticker.C { + now := gui.timeSinceStart() - leeway + if gui.g != nil && now >= event.Timestamp { + gui.g.ReplayedEvents <- *event.Event + break + } + } + } +} + +func (gui *Gui) loadRecordedEvents() ([]RecordedEvent, error) { + path := os.Getenv("REPLAY_EVENTS_FROM") + + data, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + events := []RecordedEvent{} + + err = json.Unmarshal(data, &events) + if err != nil { + return nil, err + } + + return events, nil +} + +func (gui *Gui) saveRecordedEvents() error { + if !recordingEvents() { + return nil + } + + jsonEvents, err := json.Marshal(gui.RecordedEvents) + if err != nil { + return err + } + + return ioutil.WriteFile("recorded_events.json", 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) + } +} diff --git a/vendor/github.com/jesseduffield/gocui/gui.go b/vendor/github.com/jesseduffield/gocui/gui.go index 99cbf002b..a90cce202 100644 --- a/vendor/github.com/jesseduffield/gocui/gui.go +++ b/vendor/github.com/jesseduffield/gocui/gui.go @@ -59,8 +59,14 @@ type GuiMutexes struct { // Gui represents the whole User Interface, including the views, layouts // and keybindings. type Gui struct { - tbEvents chan termbox.Event - userEvents chan userEvent + tbEvents chan termbox.Event + userEvents chan userEvent + + // ReplayedEvents is a channel for passing pre-recorded input events, for the purposes of testing + ReplayedEvents chan termbox.Event + RecordEvents bool + RecordedEvents chan *termbox.Event + views []*View currentView *View managers []Manager @@ -110,7 +116,7 @@ type Gui struct { } // NewGui returns a new Gui object with a given output mode. -func NewGui(mode OutputMode, supportOverlaps bool) (*Gui, error) { +func NewGui(mode OutputMode, supportOverlaps bool, recordEvents bool) (*Gui, error) { g := &Gui{} var err error @@ -128,7 +134,9 @@ func NewGui(mode OutputMode, supportOverlaps bool) (*Gui, error) { g.stop = make(chan struct{}, 0) g.tbEvents = make(chan termbox.Event, 20) + g.ReplayedEvents = make(chan termbox.Event) g.userEvents = make(chan userEvent, 20) + g.RecordedEvents = make(chan *termbox.Event) g.BgColor, g.FgColor = ColorDefault, ColorDefault g.SelBgColor, g.SelFgColor = ColorDefault, ColorDefault @@ -142,6 +150,8 @@ func NewGui(mode OutputMode, supportOverlaps bool) (*Gui, error) { g.NextSearchMatchKey = 'n' g.PrevSearchMatchKey = 'N' + g.RecordEvents = recordEvents + return g, nil } @@ -489,6 +499,10 @@ func (g *Gui) MainLoop() error { if err := g.handleEvent(&ev); err != nil { return err } + case ev := <-g.ReplayedEvents: + if err := g.handleEvent(&ev); err != nil { + return err + } case ev := <-g.userEvents: if err := ev.f(g); err != nil { return err @@ -511,6 +525,10 @@ func (g *Gui) consumeevents() error { if err := g.handleEvent(&ev); err != nil { return err } + case ev := <-g.ReplayedEvents: + if err := g.handleEvent(&ev); err != nil { + return err + } case ev := <-g.userEvents: if err := ev.f(g); err != nil { return err @@ -524,6 +542,10 @@ func (g *Gui) consumeevents() error { // handleEvent handles an event, based on its type (key-press, error, // etc.) func (g *Gui) handleEvent(ev *termbox.Event) error { + if g.RecordEvents { + g.RecordedEvents <- ev + } + switch ev.Type { case termbox.EventKey, termbox.EventMouse: return g.onKey(ev) diff --git a/vendor/github.com/jesseduffield/gocui/view.go b/vendor/github.com/jesseduffield/gocui/view.go index 37d4fecc4..89cb7ab28 100644 --- a/vendor/github.com/jesseduffield/gocui/view.go +++ b/vendor/github.com/jesseduffield/gocui/view.go @@ -153,10 +153,18 @@ func (v *View) gotoPreviousMatch() error { } func (v *View) SelectSearchResult(index int) error { + itemCount := len(v.searcher.searchPositions) + if itemCount == 0 { + return nil + } + if index > itemCount-1 { + index = itemCount - 1 + } + y := v.searcher.searchPositions[index].y v.FocusPoint(0, y) if v.searcher.onSelectItem != nil { - return v.searcher.onSelectItem(y, index, len(v.searcher.searchPositions)) + return v.searcher.onSelectItem(y, index, itemCount) } return nil } @@ -183,7 +191,6 @@ func (v *View) Search(str string) error { } else { return v.searcher.onSelectItem(-1, -1, 0) } - return nil } func (v *View) ClearSearch() {