diff options
author | Jesse Duffield <jessedduffield@gmail.com> | 2021-04-05 08:52:09 +1000 |
---|---|---|
committer | Jesse Duffield <jessedduffield@gmail.com> | 2021-04-06 19:34:32 +1000 |
commit | 6a0066253f648d23966fd97b93cf0453a26be2ec (patch) | |
tree | e727f558c298f7799a96114434126cbe84abb633 /vendor | |
parent | d627b3bfc829f4ccaad910bf6136eda8af183740 (diff) |
move recording code into gocui
Diffstat (limited to 'vendor')
-rw-r--r-- | vendor/github.com/jesseduffield/gocui/gui.go | 65 | ||||
-rw-r--r-- | vendor/github.com/jesseduffield/gocui/recording.go | 52 | ||||
-rw-r--r-- | vendor/github.com/jesseduffield/gocui/tcell_driver.go | 43 |
3 files changed, 137 insertions, 23 deletions
diff --git a/vendor/github.com/jesseduffield/gocui/gui.go b/vendor/github.com/jesseduffield/gocui/gui.go index a15e5b71c..19ea5b2d6 100644 --- a/vendor/github.com/jesseduffield/gocui/gui.go +++ b/vendor/github.com/jesseduffield/gocui/gui.go @@ -75,13 +75,36 @@ type GuiMutexes struct { 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 // and keybindings. type Gui struct { - // ReplayedEvents is a channel for passing pre-recorded input events, for the purposes of testing - ReplayedEvents chan GocuiEvent - RecordEvents bool - RecordedEvents chan *GocuiEvent + RecordingConfig + Recording *Recording + // ReplayedEvents is for passing pre-recorded input events, for the purposes of testing + ReplayedEvents replayedEvents + PlayMode PlayMode + StartTime time.Time tabClickBindings []*tabClickBinding gEvents chan GocuiEvent @@ -135,7 +158,7 @@ type Gui struct { } // 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() if err != nil { return nil, err @@ -147,10 +170,18 @@ func NewGui(mode OutputMode, supportOverlaps bool, recordEvents bool) (*Gui, err g.stop = make(chan struct{}) - g.ReplayedEvents = make(chan GocuiEvent) g.gEvents = make(chan GocuiEvent, 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" { 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.PrevSearchMatchKey = 'N' - g.RecordEvents = recordEvents + g.PlayMode = playMode 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 // finish should return ErrQuit. func (g *Gui) MainLoop() error { + if g.PlayMode == REPLAYING { + g.replayRecording() + } + go func() { for { select { case <-g.stop: return default: - g.gEvents <- pollEvent() + g.gEvents <- g.pollEvent() } } }() @@ -554,10 +589,6 @@ 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 @@ -580,10 +611,6 @@ 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 @@ -597,10 +624,6 @@ func (g *Gui) consumeevents() error { // handleEvent handles an event, based on its type (key-press, error, // etc.) func (g *Gui) handleEvent(ev *GocuiEvent) error { - if g.RecordEvents { - g.RecordedEvents <- ev - } - switch ev.Type { case eventKey, eventMouse: return g.onKey(ev) diff --git a/vendor/github.com/jesseduffield/gocui/recording.go b/vendor/github.com/jesseduffield/gocui/recording.go new file mode 100644 index 000000000..2c3241b94 --- /dev/null +++ b/vendor/github.com/jesseduffield/gocui/recording.go @@ -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") +} diff --git a/vendor/github.com/jesseduffield/gocui/tcell_driver.go b/vendor/github.com/jesseduffield/gocui/tcell_driver.go index 517f102dd..637d650c5 100644 --- a/vendor/github.com/jesseduffield/gocui/tcell_driver.go +++ b/vendor/github.com/jesseduffield/gocui/tcell_driver.go @@ -6,6 +6,7 @@ package gocui import ( "sync" + "time" "github.com/gdamore/tcell/v2" ) @@ -144,9 +145,43 @@ var ( 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 -func pollEvent() GocuiEvent { - tev := Screen.PollEvent() +func (g *Gui) pollEvent() GocuiEvent { + 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) { case *tcell.EventInterrupt: return GocuiEvent{Type: eventInterrupt} @@ -154,6 +189,10 @@ func pollEvent() GocuiEvent { w, h := tev.Size() return GocuiEvent{Type: eventResize, Width: w, Height: h} case *tcell.EventKey: + if g.PlayMode == RECORDING { + g.Recording.KeyEvents = append(g.Recording.KeyEvents, NewTcellKeyEventWrapper(tev, g.timeSinceStart())) + } + k := tev.Key() ch := rune(0) if k == tcell.KeyRune { |