mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-06-09 22:05:16 +02:00
22169e22ff
This changes not only how we store modifiers (inside of Key instead of passing it separately), but also how we parse keybinding strings: it supports all combinations of modifiers now (if the terminal supports it, that is).
402 lines
9.2 KiB
Go
402 lines
9.2 KiB
Go
// Copyright 2020 The gocui Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package gocui
|
|
|
|
import (
|
|
"github.com/gdamore/tcell/v3"
|
|
"github.com/gdamore/tcell/v3/vt"
|
|
)
|
|
|
|
// We probably don't want this being a global variable for YOLO for now
|
|
var Screen tcell.Screen
|
|
|
|
// oldStyle is a representation of how a cell would be styled when we were using termbox
|
|
type oldStyle struct {
|
|
fg Attribute
|
|
bg Attribute
|
|
outputMode OutputMode
|
|
}
|
|
|
|
var runeReplacements = map[rune]string{
|
|
'┌': "+",
|
|
'┐': "+",
|
|
'└': "+",
|
|
'┘': "+",
|
|
'╭': "+",
|
|
'╮': "+",
|
|
'╰': "+",
|
|
'╯': "+",
|
|
'─': "-",
|
|
'═': "-",
|
|
'║': "|",
|
|
'╔': "+",
|
|
'╗': "+",
|
|
'╚': "+",
|
|
'╝': "+",
|
|
|
|
// using a hyphen here actually looks weird.
|
|
// We see these characters when in portrait mode
|
|
'╶': " ",
|
|
'╴': " ",
|
|
|
|
'┴': "+",
|
|
'┬': "+",
|
|
'╷': "|",
|
|
'├': "+",
|
|
'│': "|",
|
|
'▼': "v",
|
|
'►': ">",
|
|
'▲': "^",
|
|
'◄': "<",
|
|
}
|
|
|
|
// tcellInit initializes tcell screen for use.
|
|
func (g *Gui) tcellInit(runeReplacements map[rune]string) error {
|
|
tcell.SetEncodingFallback(tcell.EncodingFallbackASCII)
|
|
|
|
s, e := tcell.NewScreen()
|
|
if e != nil {
|
|
return e
|
|
}
|
|
|
|
if e = s.Init(); e != nil {
|
|
return e
|
|
}
|
|
|
|
registerRuneFallbacks(s, runeReplacements)
|
|
|
|
g.screen = s
|
|
Screen = s
|
|
return nil
|
|
}
|
|
|
|
func registerRuneFallbacks(s tcell.Screen, additional map[rune]string) {
|
|
for before, after := range runeReplacements {
|
|
s.RegisterRuneFallback(before, after)
|
|
}
|
|
|
|
for before, after := range additional {
|
|
s.RegisterRuneFallback(before, after)
|
|
}
|
|
}
|
|
|
|
// tcellInitSimulation initializes tcell screen for use.
|
|
func (g *Gui) tcellInitSimulation(width int, height int) error {
|
|
mt := vt.NewMockTerm(vt.MockOptSize{X: vt.Col(width), Y: vt.Row(height)})
|
|
s, e := tcell.NewTerminfoScreenFromTty(mt)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
if e = s.Init(); e != nil {
|
|
return e
|
|
}
|
|
|
|
g.screen = s
|
|
Screen = s
|
|
s.Sync()
|
|
return nil
|
|
}
|
|
|
|
// tcellSetCell sets the character cell at a given location to the given
|
|
// content (grapheme cluster) and attributes using provided OutputMode
|
|
func tcellSetCell(x, y int, ch string, fg, bg Attribute, outputMode OutputMode) {
|
|
st := getTcellStyle(oldStyle{fg: fg, bg: bg, outputMode: outputMode})
|
|
Screen.Put(x, y, ch, st)
|
|
}
|
|
|
|
// getTcellStyle creates tcell.Style from Attributes
|
|
func getTcellStyle(input oldStyle) tcell.Style {
|
|
st := tcell.StyleDefault
|
|
|
|
// extract colors and attributes
|
|
if input.fg != ColorDefault {
|
|
st = st.Foreground(getTcellColor(input.fg, input.outputMode))
|
|
st = setTcellFontEffectStyle(st, input.fg)
|
|
}
|
|
if input.bg != ColorDefault {
|
|
st = st.Background(getTcellColor(input.bg, input.outputMode))
|
|
st = setTcellFontEffectStyle(st, input.bg)
|
|
}
|
|
|
|
return st
|
|
}
|
|
|
|
// setTcellFontEffectStyle add additional attributes to tcell.Style
|
|
func setTcellFontEffectStyle(st tcell.Style, attr Attribute) tcell.Style {
|
|
if attr&AttrBold != 0 {
|
|
st = st.Bold(true)
|
|
}
|
|
if attr&AttrUnderline != 0 {
|
|
st = st.Underline(true)
|
|
}
|
|
if attr&AttrReverse != 0 {
|
|
st = st.Reverse(true)
|
|
}
|
|
if attr&AttrBlink != 0 {
|
|
st = st.Blink(true)
|
|
}
|
|
if attr&AttrDim != 0 {
|
|
st = st.Dim(true)
|
|
}
|
|
if attr&AttrItalic != 0 {
|
|
st = st.Italic(true)
|
|
}
|
|
if attr&AttrStrikeThrough != 0 {
|
|
st = st.StrikeThrough(true)
|
|
}
|
|
return st
|
|
}
|
|
|
|
// gocuiEventType represents the type of event.
|
|
type gocuiEventType uint8
|
|
|
|
// GocuiEvent represents events like a keys, mouse actions, or window resize.
|
|
//
|
|
// The 'Mod', 'Key' and 'Ch' fields are valid if 'Type' is 'eventKey'.
|
|
// The 'MouseX' and 'MouseY' fields are valid if 'Type' is 'eventMouse'.
|
|
// The 'Width' and 'Height' fields are valid if 'Type' is 'eventResize'.
|
|
// The 'Focused' field is valid if 'Type' is 'eventFocus'.
|
|
// The 'Start' field is valid if 'Type' is 'eventPaste'. It is true for the
|
|
// beginning of a paste operation, false for the end.
|
|
// The 'Err' field is valid if 'Type' is 'eventError'.
|
|
type GocuiEvent struct {
|
|
Type gocuiEventType
|
|
Key Key
|
|
Width int
|
|
Height int
|
|
Err error
|
|
MouseX int
|
|
MouseY int
|
|
Focused bool
|
|
Start bool
|
|
N int
|
|
}
|
|
|
|
// Event types.
|
|
const (
|
|
eventNone gocuiEventType = iota
|
|
eventKey
|
|
eventResize
|
|
eventMouse
|
|
eventMouseMove // only used when no button is down, otherwise it's eventMouse
|
|
eventFocus
|
|
eventPaste
|
|
eventInterrupt
|
|
eventError
|
|
eventRaw
|
|
)
|
|
|
|
const (
|
|
NOT_DRAGGING int = iota
|
|
MAYBE_DRAGGING
|
|
DRAGGING
|
|
)
|
|
|
|
var (
|
|
lastMouseKey tcell.ButtonMask = tcell.ButtonNone
|
|
lastMouseMod tcell.ModMask = tcell.ModNone
|
|
dragState = NOT_DRAGGING
|
|
lastX = 0
|
|
lastY = 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 string
|
|
}
|
|
|
|
func NewTcellKeyEventWrapper(event *tcell.EventKey, timestamp int64) *TcellKeyEventWrapper {
|
|
return &TcellKeyEventWrapper{
|
|
Timestamp: timestamp,
|
|
Mod: event.Modifiers(),
|
|
Key: event.Key(),
|
|
Ch: event.Str(),
|
|
}
|
|
}
|
|
|
|
func (wrapper TcellKeyEventWrapper) toTcellEvent() tcell.Event {
|
|
return tcell.NewEventKey(wrapper.Key, wrapper.Ch, wrapper.Mod)
|
|
}
|
|
|
|
type TcellMouseEventWrapper struct {
|
|
Timestamp int64
|
|
X int
|
|
Y int
|
|
ButtonMask tcell.ButtonMask
|
|
ModMask tcell.ModMask
|
|
}
|
|
|
|
func NewTcellMouseEventWrapper(event *tcell.EventMouse, timestamp int64) *TcellMouseEventWrapper {
|
|
x, y := event.Position()
|
|
return &TcellMouseEventWrapper{
|
|
Timestamp: timestamp,
|
|
X: x,
|
|
Y: y,
|
|
ButtonMask: event.Buttons(),
|
|
ModMask: event.Modifiers(),
|
|
}
|
|
}
|
|
|
|
func (wrapper TcellMouseEventWrapper) toTcellEvent() tcell.Event {
|
|
return tcell.NewEventMouse(wrapper.X, wrapper.Y, wrapper.ButtonMask, wrapper.ModMask)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// pollEvent get tcell.Event and transform it into gocuiEvent
|
|
func (g *Gui) pollEvent() GocuiEvent {
|
|
var tev tcell.Event
|
|
if g.playRecording {
|
|
select {
|
|
case ev := <-g.ReplayedEvents.Keys:
|
|
tev = (ev).toTcellEvent()
|
|
case ev := <-g.ReplayedEvents.Resizes:
|
|
tev = (ev).toTcellEvent()
|
|
case ev := <-g.ReplayedEvents.MouseEvents:
|
|
tev = (ev).toTcellEvent()
|
|
}
|
|
} else {
|
|
tev = <-Screen.EventQ()
|
|
}
|
|
|
|
switch tev := tev.(type) {
|
|
case *tcell.EventInterrupt:
|
|
return GocuiEvent{Type: eventInterrupt}
|
|
case *tcell.EventResize:
|
|
w, h := tev.Size()
|
|
return GocuiEvent{Type: eventResize, Width: w, Height: h}
|
|
case *tcell.EventKey:
|
|
k := tev.Key()
|
|
ch := ""
|
|
if k == tcell.KeyRune {
|
|
ch = tev.Str()
|
|
} else if k >= tcell.KeyCtrlA && k <= tcell.KeyCtrlZ {
|
|
ch = string(rune('a' + (k - tcell.KeyCtrlA)))
|
|
k = tcell.KeyRune
|
|
}
|
|
mod := tev.Modifiers()
|
|
|
|
return GocuiEvent{
|
|
Type: eventKey,
|
|
Key: NewKey(KeyName(k), ch, Modifier(mod)),
|
|
}
|
|
case *tcell.EventMouse:
|
|
x, y := tev.Position()
|
|
button := tev.Buttons()
|
|
mouseKey := MouseRelease
|
|
mouseMod := ModNone
|
|
// process mouse wheel
|
|
if button&tcell.WheelUp != 0 {
|
|
mouseKey = MouseWheelUp
|
|
}
|
|
if button&tcell.WheelDown != 0 {
|
|
mouseKey = MouseWheelDown
|
|
}
|
|
if button&tcell.WheelLeft != 0 {
|
|
mouseKey = MouseWheelLeft
|
|
}
|
|
if button&tcell.WheelRight != 0 {
|
|
mouseKey = MouseWheelRight
|
|
}
|
|
|
|
wheeling := mouseKey == MouseWheelUp || mouseKey == MouseWheelDown || mouseKey == MouseWheelLeft || mouseKey == MouseWheelRight
|
|
|
|
// process button events (not wheel events)
|
|
button &= tcell.ButtonMask(0xff)
|
|
if button != tcell.ButtonNone && lastMouseKey == tcell.ButtonNone {
|
|
lastMouseKey = button
|
|
lastMouseMod = tev.Modifiers()
|
|
switch button {
|
|
case tcell.ButtonPrimary:
|
|
mouseKey = MouseLeft
|
|
dragState = MAYBE_DRAGGING
|
|
lastX = x
|
|
lastY = y
|
|
case tcell.ButtonSecondary:
|
|
mouseKey = MouseRight
|
|
case tcell.ButtonMiddle:
|
|
mouseKey = MouseMiddle
|
|
default:
|
|
}
|
|
}
|
|
|
|
switch tev.Buttons() {
|
|
case tcell.ButtonNone:
|
|
if lastMouseKey != tcell.ButtonNone {
|
|
switch lastMouseKey {
|
|
case tcell.ButtonPrimary:
|
|
dragState = NOT_DRAGGING
|
|
case tcell.ButtonSecondary:
|
|
case tcell.ButtonMiddle:
|
|
default:
|
|
}
|
|
mouseMod = Modifier(lastMouseMod)
|
|
lastMouseMod = tcell.ModNone
|
|
lastMouseKey = tcell.ButtonNone
|
|
}
|
|
default:
|
|
}
|
|
|
|
if !wheeling {
|
|
switch dragState {
|
|
case NOT_DRAGGING:
|
|
return GocuiEvent{
|
|
Type: eventMouseMove,
|
|
MouseX: x,
|
|
MouseY: y,
|
|
}
|
|
// if we haven't released the left mouse button and we've moved the cursor then we're dragging
|
|
case MAYBE_DRAGGING:
|
|
if x != lastX || y != lastY {
|
|
dragState = DRAGGING
|
|
}
|
|
case DRAGGING:
|
|
mouseMod = ModMotion
|
|
mouseKey = MouseLeft
|
|
}
|
|
}
|
|
|
|
return GocuiEvent{
|
|
Type: eventMouse,
|
|
MouseX: x,
|
|
MouseY: y,
|
|
Key: NewKey(mouseKey, "", mouseMod),
|
|
}
|
|
case *tcell.EventFocus:
|
|
return GocuiEvent{
|
|
Type: eventFocus,
|
|
Focused: tev.Focused,
|
|
}
|
|
case *tcell.EventPaste:
|
|
return GocuiEvent{
|
|
Type: eventPaste,
|
|
Start: tev.Start(),
|
|
}
|
|
default:
|
|
return GocuiEvent{Type: eventNone}
|
|
}
|
|
}
|