mirror of
https://github.com/jroimartin/gocui.git
synced 2025-04-28 13:48:51 +08:00
support recording events
This commit is contained in:
parent
d958b7dc83
commit
439abd8b6e
187
gui.go
187
gui.go
@ -7,6 +7,7 @@ package gocui
|
||||
import (
|
||||
standardErrors "errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -75,13 +76,38 @@ type GuiMutexes struct {
|
||||
ViewsMutex sync.Mutex
|
||||
}
|
||||
|
||||
type PlayMode int
|
||||
|
||||
const (
|
||||
NORMAL PlayMode = iota
|
||||
RECORDING
|
||||
REPLAYING
|
||||
)
|
||||
|
||||
type Recording struct {
|
||||
KeyEvents []*TcellKeyEventWrapper
|
||||
ResizeEvents []*TcellResizeEventWrapper
|
||||
}
|
||||
|
||||
type replayedEvents struct {
|
||||
keys chan *TcellKeyEventWrapper
|
||||
resizes chan *TcellResizeEventWrapper
|
||||
}
|
||||
|
||||
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,22 +161,37 @@ type Gui struct {
|
||||
}
|
||||
|
||||
// NewGui returns a new Gui object with a given output mode.
|
||||
func NewGui(mode OutputMode, supportOverlaps bool, recordEvents bool) (*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{})
|
||||
|
||||
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{},
|
||||
ResizeEvents: []*TcellResizeEventWrapper{},
|
||||
}
|
||||
} else if playMode == REPLAYING {
|
||||
g.ReplayedEvents = replayedEvents{
|
||||
keys: make(chan *TcellKeyEventWrapper),
|
||||
resizes: make(chan *TcellResizeEventWrapper),
|
||||
}
|
||||
}
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
g.maxX, g.maxY, err = g.getTermWindowSize()
|
||||
@ -173,7 +214,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
|
||||
}
|
||||
@ -535,13 +576,19 @@ 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 {
|
||||
go g.replayRecording()
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-g.stop:
|
||||
return
|
||||
default:
|
||||
g.gEvents <- pollEvent()
|
||||
g.gEvents <- g.pollEvent()
|
||||
}
|
||||
}
|
||||
}()
|
||||
@ -556,10 +603,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
|
||||
@ -582,10 +625,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
|
||||
@ -1000,12 +1039,7 @@ func (g *Gui) draw(v *View) error {
|
||||
func (g *Gui) onKey(ev *GocuiEvent) error {
|
||||
switch ev.Type {
|
||||
case eventKey:
|
||||
if g.currentView != nil && g.currentView.Editable && g.currentView.Editor != nil {
|
||||
matched := g.currentView.Editor.Edit(g.currentView, Key(ev.Key), ev.Ch, Modifier(ev.Mod))
|
||||
if matched {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
_, err := g.execKeybindings(g.currentView, ev)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -1013,7 +1047,7 @@ func (g *Gui) onKey(ev *GocuiEvent) error {
|
||||
|
||||
case eventMouse:
|
||||
mx, my := ev.MouseX, ev.MouseY
|
||||
v, err := g.ViewByPosition(mx, my)
|
||||
v, err := g.VisibleViewByPosition(mx, my)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
@ -1093,6 +1127,14 @@ func (g *Gui) execKeybindings(v *View, ev *GocuiEvent) (matched bool, err error)
|
||||
if matchingParentViewKb != nil {
|
||||
return g.execKeybinding(v.ParentView, matchingParentViewKb)
|
||||
}
|
||||
|
||||
if g.currentView != nil && g.currentView.Editable && g.currentView.Editor != nil {
|
||||
matched := g.currentView.Editor.Edit(g.currentView, Key(ev.Key), ev.Ch, Modifier(ev.Mod))
|
||||
if matched {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
if globalKb != nil {
|
||||
return g.execKeybinding(v, globalKb)
|
||||
}
|
||||
@ -1154,3 +1196,94 @@ func IsUnknownView(err error) bool {
|
||||
func IsQuit(err error) bool {
|
||||
return err != nil && err.Error() == ErrQuit.Error()
|
||||
}
|
||||
|
||||
func (g *Gui) replayRecording() {
|
||||
waitGroup := sync.WaitGroup{}
|
||||
|
||||
waitGroup.Add(2)
|
||||
|
||||
// lots of duplication here due to lack of generics. Also we don't support mouse
|
||||
// events because it would be awkward to replicate but it would be trivial to add
|
||||
// support
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
g.Update(func(*Gui) error {
|
||||
return ErrQuit
|
||||
})
|
||||
|
||||
time.Sleep(time.Second * 1)
|
||||
|
||||
log.Fatal("gocui should have already exited")
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ package gocui
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
)
|
||||
@ -37,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) {
|
||||
@ -144,16 +156,84 @@ 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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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()
|
||||
case ev := <-g.ReplayedEvents.resizes:
|
||||
tev = (ev).toTcellEvent()
|
||||
}
|
||||
} else {
|
||||
tev = Screen.PollEvent()
|
||||
}
|
||||
|
||||
switch tev := tev.(type) {
|
||||
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:
|
||||
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 {
|
||||
|
2
view.go
2
view.go
@ -170,7 +170,7 @@ func (v *View) gotoNextMatch() error {
|
||||
if len(v.searcher.searchPositions) == 0 {
|
||||
return nil
|
||||
}
|
||||
if v.searcher.currentSearchIndex == len(v.searcher.searchPositions)-1 {
|
||||
if v.searcher.currentSearchIndex >= len(v.searcher.searchPositions)-1 {
|
||||
v.searcher.currentSearchIndex = 0
|
||||
} else {
|
||||
v.searcher.currentSearchIndex++
|
||||
|
Loading…
x
Reference in New Issue
Block a user