1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-07-05 00:59:19 +02:00

support tcell simulation screen

This commit is contained in:
Jesse Duffield
2021-04-05 10:20:02 +10:00
parent 011451464f
commit 843b8ceab0
22 changed files with 358 additions and 259 deletions

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: