Refactor loop code into its own package

This commit is contained in:
Kovid Goyal
2022-08-26 12:47:58 +05:30
parent 0aa05b02e8
commit 1b90c03304
11 changed files with 893 additions and 846 deletions

184
tools/tui/loop/api.go Normal file
View File

@@ -0,0 +1,184 @@
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
package loop
import (
"kitty/tools/tty"
"time"
"golang.org/x/sys/unix"
"kitty/tools/wcswidth"
)
type ScreenSize struct {
WidthCells, HeightCells, WidthPx, HeightPx, CellWidth, CellHeight uint
updated bool
}
type IdType uint64
type TimerCallback func(timer_id IdType) error
type timer struct {
interval time.Duration
deadline time.Time
repeats bool
id IdType
callback TimerCallback
}
func (self *timer) update_deadline(now time.Time) {
self.deadline = now.Add(self.interval)
}
type Loop struct {
controlling_term *tty.Term
terminal_options TerminalStateOptions
screen_size ScreenSize
escape_code_parser wcswidth.EscapeCodeParser
keep_going bool
death_signal unix.Signal
exit_code int
timers []*timer
timer_id_counter, write_msg_id_counter IdType
tty_read_channel chan []byte
tty_write_channel chan *write_msg
write_done_channel chan IdType
err_channel chan error
tty_writing_done_channel, tty_reading_done_channel, wakeup_channel chan byte
pending_writes []*write_msg
// Callbacks
// Called when the terminal has been fully setup. Any string returned is sent to
// the terminal on shutdown
OnInitialize func() (string, error)
// Called when a key event happens
OnKeyEvent func(event *KeyEvent) error
// Called when text is received either from a key event or directly from the terminal
OnText func(text string, from_key_event bool, in_bracketed_paste bool) error
// Called when the terminal is resize
OnResize func(old_size ScreenSize, new_size ScreenSize) error
// Called when writing is done
OnWriteComplete func(msg_id IdType) error
// Called when a response to an rc command is received
OnRCResponse func(data []byte) error
// Called when any input form tty is received
OnReceivedData func(data []byte) error
}
func New() (*Loop, error) {
l := Loop{controlling_term: nil, timers: make([]*timer, 0)}
l.terminal_options.alternate_screen = true
l.escape_code_parser.HandleCSI = l.handle_csi
l.escape_code_parser.HandleOSC = l.handle_osc
l.escape_code_parser.HandleDCS = l.handle_dcs
l.escape_code_parser.HandleAPC = l.handle_apc
l.escape_code_parser.HandleSOS = l.handle_sos
l.escape_code_parser.HandlePM = l.handle_pm
l.escape_code_parser.HandleRune = l.handle_rune
return &l, nil
}
func (self *Loop) AddTimer(interval time.Duration, repeats bool, callback TimerCallback) IdType {
self.timer_id_counter++
t := timer{interval: interval, repeats: repeats, callback: callback, id: self.timer_id_counter}
t.update_deadline(time.Now())
self.timers = append(self.timers, &t)
self.sort_timers()
return t.id
}
func (self *Loop) RemoveTimer(id IdType) bool {
for i := 0; i < len(self.timers); i++ {
if self.timers[i].id == id {
self.timers = append(self.timers[:i], self.timers[i+1:]...)
return true
}
}
return false
}
func (self *Loop) NoAlternateScreen() {
self.terminal_options.alternate_screen = false
}
func (self *Loop) MouseTracking(mt MouseTracking) {
self.terminal_options.mouse_tracking = mt
}
func (self *Loop) DeathSignalName() string {
if self.death_signal != SIGNULL {
return self.death_signal.String()
}
return ""
}
func (self *Loop) ScreenSize() (ScreenSize, error) {
if self.screen_size.updated {
return self.screen_size, nil
}
err := self.update_screen_size()
return self.screen_size, err
}
func (self *Loop) KillIfSignalled() {
if self.death_signal != SIGNULL {
kill_self(self.death_signal)
}
}
func (self *Loop) DebugPrintln(args ...interface{}) {
if self.controlling_term != nil {
self.controlling_term.DebugPrintln(args...)
}
}
func (self *Loop) Run() (err error) {
return self.run()
}
func (self *Loop) WakeupMainThread() {
self.wakeup_channel <- 1
}
func (self *Loop) QueueWriteString(data string) IdType {
self.write_msg_id_counter++
msg := write_msg{str: data, id: self.write_msg_id_counter}
self.queue_write_to_tty(&msg)
return msg.id
}
// This is dangerous as it is upto the calling code
// to ensure the data in the underlying array does not change
func (self *Loop) QueueWriteBytesDangerous(data []byte) IdType {
self.write_msg_id_counter++
msg := write_msg{bytes: data, id: self.write_msg_id_counter}
self.queue_write_to_tty(&msg)
return msg.id
}
func (self *Loop) QueueWriteBytesCopy(data []byte) IdType {
d := make([]byte, len(data))
copy(d, data)
return self.QueueWriteBytesDangerous(d)
}
func (self *Loop) ExitCode() int {
return self.exit_code
}
func (self *Loop) Beep() {
self.QueueWriteString("\a")
}
func (self *Loop) Quit(exit_code int) {
self.exit_code = exit_code
self.keep_going = false
}

View File

@@ -0,0 +1,293 @@
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
package loop
import (
"fmt"
"strconv"
"strings"
"kitty"
)
// key encoding mappings {{{
// start csi mapping (auto generated by gen-key-constants.py do not edit)
var functional_key_number_to_name_map = map[int]string{57344: "ESCAPE", 57345: "ENTER", 57346: "TAB", 57347: "BACKSPACE", 57348: "INSERT", 57349: "DELETE", 57350: "LEFT", 57351: "RIGHT", 57352: "UP", 57353: "DOWN", 57354: "PAGE_UP", 57355: "PAGE_DOWN", 57356: "HOME", 57357: "END", 57358: "CAPS_LOCK", 57359: "SCROLL_LOCK", 57360: "NUM_LOCK", 57361: "PRINT_SCREEN", 57362: "PAUSE", 57363: "MENU", 57364: "F1", 57365: "F2", 57366: "F3", 57367: "F4", 57368: "F5", 57369: "F6", 57370: "F7", 57371: "F8", 57372: "F9", 57373: "F10", 57374: "F11", 57375: "F12", 57376: "F13", 57377: "F14", 57378: "F15", 57379: "F16", 57380: "F17", 57381: "F18", 57382: "F19", 57383: "F20", 57384: "F21", 57385: "F22", 57386: "F23", 57387: "F24", 57388: "F25", 57389: "F26", 57390: "F27", 57391: "F28", 57392: "F29", 57393: "F30", 57394: "F31", 57395: "F32", 57396: "F33", 57397: "F34", 57398: "F35", 57399: "KP_0", 57400: "KP_1", 57401: "KP_2", 57402: "KP_3", 57403: "KP_4", 57404: "KP_5", 57405: "KP_6", 57406: "KP_7", 57407: "KP_8", 57408: "KP_9", 57409: "KP_DECIMAL", 57410: "KP_DIVIDE", 57411: "KP_MULTIPLY", 57412: "KP_SUBTRACT", 57413: "KP_ADD", 57414: "KP_ENTER", 57415: "KP_EQUAL", 57416: "KP_SEPARATOR", 57417: "KP_LEFT", 57418: "KP_RIGHT", 57419: "KP_UP", 57420: "KP_DOWN", 57421: "KP_PAGE_UP", 57422: "KP_PAGE_DOWN", 57423: "KP_HOME", 57424: "KP_END", 57425: "KP_INSERT", 57426: "KP_DELETE", 57427: "KP_BEGIN", 57428: "MEDIA_PLAY", 57429: "MEDIA_PAUSE", 57430: "MEDIA_PLAY_PAUSE", 57431: "MEDIA_REVERSE", 57432: "MEDIA_STOP", 57433: "MEDIA_FAST_FORWARD", 57434: "MEDIA_REWIND", 57435: "MEDIA_TRACK_NEXT", 57436: "MEDIA_TRACK_PREVIOUS", 57437: "MEDIA_RECORD", 57438: "LOWER_VOLUME", 57439: "RAISE_VOLUME", 57440: "MUTE_VOLUME", 57441: "LEFT_SHIFT", 57442: "LEFT_CONTROL", 57443: "LEFT_ALT", 57444: "LEFT_SUPER", 57445: "LEFT_HYPER", 57446: "LEFT_META", 57447: "RIGHT_SHIFT", 57448: "RIGHT_CONTROL", 57449: "RIGHT_ALT", 57450: "RIGHT_SUPER", 57451: "RIGHT_HYPER", 57452: "RIGHT_META", 57453: "ISO_LEVEL3_SHIFT", 57454: "ISO_LEVEL5_SHIFT"}
var csi_number_to_functional_number_map = map[int]int{2: 57348, 3: 57349, 5: 57354, 6: 57355, 7: 57356, 8: 57357, 9: 57346, 11: 57364, 12: 57365, 13: 57345, 14: 57367, 15: 57368, 17: 57369, 18: 57370, 19: 57371, 20: 57372, 21: 57373, 23: 57374, 24: 57375, 27: 57344, 127: 57347}
var letter_trailer_to_csi_number_map = map[string]int{"A": 57352, "B": 57353, "C": 57351, "D": 57350, "E": 57427, "F": 8, "H": 7, "P": 11, "Q": 12, "R": 13, "S": 14}
// end csi mapping
// }}}
var name_to_functional_number_map map[string]int
type KeyEventType uint8
type KeyModifiers uint16
const (
PRESS KeyEventType = 1
REPEAT KeyEventType = 2
RELEASE KeyEventType = 4
)
const (
SHIFT KeyModifiers = 1
ALT KeyModifiers = 2
CTRL KeyModifiers = 4
SUPER KeyModifiers = 8
HYPER KeyModifiers = 16
META KeyModifiers = 32
CAPS_LOCK KeyModifiers = 64
NUM_LOCK KeyModifiers = 128
)
func (self KeyModifiers) WithoutLocks() KeyModifiers {
return self & ^(CAPS_LOCK | NUM_LOCK)
}
func (self KeyEventType) String() string {
switch self {
case PRESS:
return "PRESS"
case REPEAT:
return "REPEAT"
case RELEASE:
return "RELEASE"
default:
return fmt.Sprintf("KeyEventType:%d", int(self))
}
}
func (self KeyModifiers) String() string {
ans := make([]string, 0)
if self&SHIFT != 0 {
ans = append(ans, "shift")
}
if self&ALT != 0 {
ans = append(ans, "alt")
}
if self&CTRL != 0 {
ans = append(ans, "ctrl")
}
if self&SUPER != 0 {
ans = append(ans, "super")
}
if self&HYPER != 0 {
ans = append(ans, "hyper")
}
if self&META != 0 {
ans = append(ans, "meta")
}
if self&CAPS_LOCK != 0 {
ans = append(ans, "caps_lock")
}
if self&NUM_LOCK != 0 {
ans = append(ans, "num_lock")
}
return strings.Join(ans, "+")
}
type KeyEvent struct {
Type KeyEventType
Mods KeyModifiers
Key string
ShiftedKey string
AlternateKey string
Text string
Handled bool
}
func (self *KeyEvent) String() string {
key := self.Key
if self.Mods > 0 {
key = self.Mods.String() + "+" + key
}
ans := fmt.Sprint(self.Type, "{ ", key, " ")
if self.Text != "" {
ans += "Text: " + self.Text + " "
}
if self.ShiftedKey != "" {
ans += "ShiftedKey: " + self.ShiftedKey + " "
}
if self.AlternateKey != "" {
ans += "AlternateKey: " + self.AlternateKey + " "
}
return ans + "}"
}
func KeyEventFromCSI(csi string) *KeyEvent {
if len(csi) == 0 {
return nil
}
last_char := csi[len(csi)-1:]
if !strings.Contains("u~ABCDEHFPQRS", last_char) || (last_char == "~" && (csi == "200~" || csi == "201~")) {
return nil
}
csi = csi[:len(csi)-1]
sections := strings.Split(csi, ";")
get_sub_sections := func(section string) []int {
p := strings.Split(section, ":")
ans := make([]int, len(p))
for i, x := range p {
q, err := strconv.Atoi(x)
if err != nil {
return nil
}
ans[i] = q
}
return ans
}
first_section := get_sub_sections(sections[0])
second_section := make([]int, 0)
third_section := make([]int, 0)
if len(sections) > 1 {
second_section = get_sub_sections(sections[1])
}
if len(sections) > 2 {
third_section = get_sub_sections(sections[2])
}
var ans = KeyEvent{Type: PRESS}
keynum := first_section[0]
if val, ok := letter_trailer_to_csi_number_map[last_char]; ok {
keynum = val
}
key_name := func(keynum int) string {
switch keynum {
case 0:
return ""
case 13:
if last_char == "u" {
return "ENTER"
}
return "F3"
default:
if val, ok := csi_number_to_functional_number_map[keynum]; ok {
keynum = val
}
ans := ""
if val, ok := functional_key_number_to_name_map[keynum]; ok {
ans = val
} else {
ans = string(rune(keynum))
}
return ans
}
}
ans.Key = key_name(keynum)
if len(first_section) > 1 {
ans.ShiftedKey = key_name(first_section[1])
}
if len(first_section) > 2 {
ans.AlternateKey = key_name(first_section[2])
}
if len(second_section) > 0 {
ans.Mods = KeyModifiers(second_section[0] - 1)
}
if len(second_section) > 1 {
switch second_section[1] {
case 2:
ans.Type = REPEAT
case 3:
ans.Type = RELEASE
}
}
if len(third_section) > 0 {
runes := make([]rune, len(third_section))
for i, ch := range third_section {
runes[i] = rune(ch)
}
ans.Text = string(runes)
}
return &ans
}
type ParsedShortcut struct {
Mods KeyModifiers
KeyName string
}
func (self *ParsedShortcut) String() string {
ans := self.KeyName
if self.Mods > 0 {
ans = self.Mods.String() + "+" + ans
}
return ans
}
var parsed_shortcut_cache map[string]*ParsedShortcut
func ParseShortcut(spec string) *ParsedShortcut {
if parsed_shortcut_cache == nil {
parsed_shortcut_cache = make(map[string]*ParsedShortcut, 128)
}
if val, ok := parsed_shortcut_cache[spec]; ok {
return val
}
ospec := spec
if strings.HasSuffix(spec, "+") {
ospec = spec[:len(spec)-1] + "plus"
}
parts := strings.Split(ospec, "+")
key_name := parts[len(parts)-1]
if val, ok := kitty.FunctionalKeyNameAliases[strings.ToUpper(key_name)]; ok {
key_name = val
}
if _, is_functional_key := name_to_functional_number_map[strings.ToUpper(key_name)]; is_functional_key {
key_name = strings.ToUpper(key_name)
} else {
if val, ok := kitty.CharacterKeyNameAliases[strings.ToUpper(key_name)]; ok {
key_name = val
}
}
ans := ParsedShortcut{KeyName: key_name}
if len(parts) > 1 {
for _, q := range parts[:len(parts)-1] {
val, ok := kitty.ConfigModMap[strings.ToUpper(q)]
if ok {
ans.Mods |= KeyModifiers(val)
} else {
ans.Mods |= META << 8
}
}
}
parsed_shortcut_cache[spec] = &ans
return &ans
}
func (self *KeyEvent) MatchesParsedShortcut(ps *ParsedShortcut, event_type KeyEventType) bool {
if self.Type&event_type == 0 {
return false
}
mods := self.Mods.WithoutLocks()
if mods == ps.Mods && self.Key == ps.KeyName {
return true
}
if self.ShiftedKey != "" && mods&SHIFT != 0 && (mods & ^SHIFT) == ps.Mods && self.ShiftedKey == ps.KeyName {
return true
}
return false
}
func (self *KeyEvent) Matches(spec string, event_type KeyEventType) bool {
return self.MatchesParsedShortcut(ParseShortcut(spec), event_type)
}
func (self *KeyEvent) MatchesPressOrRepeat(spec string) bool {
return self.MatchesParsedShortcut(ParseShortcut(spec), PRESS|REPEAT)
}
func (self *KeyEvent) MatchesRelease(spec string) bool {
return self.MatchesParsedShortcut(ParseShortcut(spec), RELEASE)
}
func init() {
name_to_functional_number_map = make(map[string]int, len(functional_key_number_to_name_map))
for k, v := range functional_key_number_to_name_map {
name_to_functional_number_map[v] = k
}
}

99
tools/tui/loop/read.go Normal file
View File

@@ -0,0 +1,99 @@
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
package loop
import (
"io"
"os"
"golang.org/x/sys/unix"
"kitty/tools/tty"
"kitty/tools/utils"
)
func (self *Loop) dispatch_input_data(data []byte) error {
if self.OnReceivedData != nil {
err := self.OnReceivedData(data)
if err != nil {
return err
}
}
err := self.escape_code_parser.Parse(data)
if err != nil {
return err
}
return nil
}
func read_ignoring_temporary_errors(f *tty.Term, buf []byte) (int, error) {
n, err := f.Read(buf)
if err == unix.EINTR || err == unix.EAGAIN || err == unix.EWOULDBLOCK {
return 0, nil
}
if n == 0 {
return 0, io.EOF
}
return n, err
}
func read_from_tty(pipe_r *os.File, term *tty.Term, results_channel chan<- []byte, err_channel chan<- error, quit_channel <-chan byte) {
keep_going := true
pipe_fd := int(pipe_r.Fd())
tty_fd := term.Fd()
selector := utils.CreateSelect(2)
selector.RegisterRead(pipe_fd)
selector.RegisterRead(tty_fd)
defer func() {
close(results_channel)
pipe_r.Close()
}()
const bufsize = 2 * utils.DEFAULT_IO_BUFFER_SIZE
wait_for_read_available := func() {
_, err := selector.WaitForever()
if err != nil {
err_channel <- err
keep_going = false
return
}
if selector.IsReadyToRead(pipe_fd) {
keep_going = false
return
}
if selector.IsReadyToRead(tty_fd) {
return
}
}
buf := make([]byte, bufsize)
for keep_going {
if len(buf) == 0 {
buf = make([]byte, bufsize)
}
if wait_for_read_available(); !keep_going {
break
}
n, err := read_ignoring_temporary_errors(term, buf)
if err != nil {
err_channel <- err
keep_going = false
break
}
if n == 0 {
err_channel <- io.EOF
keep_going = false
break
}
send := buf[:n]
buf = buf[n:]
select {
case results_channel <- send:
case <-quit_channel:
keep_going = false
break
}
}
}

298
tools/tui/loop/run.go Normal file
View File

@@ -0,0 +1,298 @@
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
package loop
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"os/signal"
"runtime/debug"
"time"
"golang.org/x/sys/unix"
"kitty/tools/tty"
)
var SIGNULL unix.Signal
func is_temporary_error(err error) bool {
return errors.Is(err, unix.EINTR) || errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EWOULDBLOCK) || errors.Is(err, io.ErrShortWrite)
}
func kill_self(sig unix.Signal) {
unix.Kill(os.Getpid(), sig)
// Give the signal time to be delivered
time.Sleep(20 * time.Millisecond)
}
func (self *Loop) print_stack() {
self.DebugPrintln(string(debug.Stack()))
}
func (self *Loop) update_screen_size() error {
if self.controlling_term != nil {
return fmt.Errorf("No controlling terminal cannot update screen size")
}
ws, err := self.controlling_term.GetSize()
if err != nil {
return err
}
s := &self.screen_size
s.updated = true
s.HeightCells, s.WidthCells = uint(ws.Row), uint(ws.Col)
s.HeightPx, s.WidthPx = uint(ws.Ypixel), uint(ws.Xpixel)
s.CellWidth = s.WidthPx / s.WidthCells
s.CellHeight = s.HeightPx / s.HeightCells
return nil
}
func (self *Loop) handle_csi(raw []byte) error {
csi := string(raw)
ke := KeyEventFromCSI(csi)
if ke != nil {
return self.handle_key_event(ke)
}
return nil
}
func (self *Loop) handle_key_event(ev *KeyEvent) error {
// self.DebugPrintln(ev)
if self.OnKeyEvent != nil {
err := self.OnKeyEvent(ev)
if err != nil {
return err
}
if ev.Handled {
return nil
}
}
if ev.MatchesPressOrRepeat("ctrl+c") {
ev.Handled = true
return self.on_SIGINT()
}
if ev.MatchesPressOrRepeat("ctrl+z") {
ev.Handled = true
return self.on_SIGTSTP()
}
if ev.Text != "" && self.OnText != nil {
return self.OnText(ev.Text, true, false)
}
return nil
}
func (self *Loop) handle_osc(raw []byte) error {
return nil
}
func (self *Loop) handle_dcs(raw []byte) error {
if self.OnRCResponse != nil && bytes.HasPrefix(raw, []byte("@kitty-cmd")) {
return self.OnRCResponse(raw[len("@kitty-cmd"):])
}
return nil
}
func (self *Loop) handle_apc(raw []byte) error {
return nil
}
func (self *Loop) handle_sos(raw []byte) error {
return nil
}
func (self *Loop) handle_pm(raw []byte) error {
return nil
}
func (self *Loop) handle_rune(raw rune) error {
if self.OnText != nil {
return self.OnText(string(raw), false, self.escape_code_parser.InBracketedPaste())
}
return nil
}
func (self *Loop) on_signal(s unix.Signal) error {
switch s {
case unix.SIGINT:
return self.on_SIGINT()
case unix.SIGPIPE:
return self.on_SIGPIPE()
case unix.SIGWINCH:
return self.on_SIGWINCH()
case unix.SIGTERM:
return self.on_SIGTERM()
case unix.SIGTSTP:
return self.on_SIGTSTP()
case unix.SIGHUP:
return self.on_SIGHUP()
default:
return nil
}
}
func (self *Loop) on_SIGINT() error {
self.death_signal = unix.SIGINT
self.keep_going = false
return nil
}
func (self *Loop) on_SIGPIPE() error {
return nil
}
func (self *Loop) on_SIGWINCH() error {
self.screen_size.updated = false
if self.OnResize != nil {
old_size := self.screen_size
err := self.update_screen_size()
if err != nil {
return err
}
return self.OnResize(old_size, self.screen_size)
}
return nil
}
func (self *Loop) on_SIGTERM() error {
self.death_signal = unix.SIGTERM
self.keep_going = false
return nil
}
func (self *Loop) on_SIGTSTP() error {
return nil
}
func (self *Loop) on_SIGHUP() error {
self.death_signal = unix.SIGHUP
self.keep_going = false
return nil
}
func (self *Loop) run() (err error) {
sigchnl := make(chan os.Signal, 256)
handled_signals := []os.Signal{unix.SIGINT, unix.SIGTERM, unix.SIGTSTP, unix.SIGHUP, unix.SIGWINCH, unix.SIGPIPE}
signal.Notify(sigchnl, handled_signals...)
defer signal.Reset(handled_signals...)
controlling_term, err := tty.OpenControllingTerm()
if err != nil {
return err
}
self.controlling_term = controlling_term
defer func() {
self.controlling_term.RestoreAndClose()
self.controlling_term = nil
}()
err = self.controlling_term.ApplyOperations(tty.TCSANOW, tty.SetRaw)
if err != nil {
return nil
}
self.keep_going = true
self.tty_read_channel = make(chan []byte)
self.tty_write_channel = make(chan *write_msg, 1) // buffered so there is no race between initial queueing and startup of writer thread
self.write_done_channel = make(chan IdType)
self.tty_writing_done_channel = make(chan byte)
self.tty_reading_done_channel = make(chan byte)
self.wakeup_channel = make(chan byte, 256)
self.pending_writes = make([]*write_msg, 0, 256)
self.err_channel = make(chan error, 8)
self.death_signal = SIGNULL
self.escape_code_parser.Reset()
self.exit_code = 0
no_timeout_channel := make(<-chan time.Time)
finalizer := ""
w_r, w_w, err := os.Pipe()
var r_r, r_w *os.File
if err == nil {
r_r, r_w, err = os.Pipe()
if err != nil {
w_r.Close()
w_w.Close()
return err
}
} else {
return err
}
self.QueueWriteBytesDangerous(self.terminal_options.SetStateEscapeCodes())
defer func() {
// notify tty reader that we are shutting down
r_w.Close()
close(self.tty_reading_done_channel)
if finalizer != "" {
self.QueueWriteString(finalizer)
}
self.QueueWriteBytesDangerous(self.terminal_options.ResetStateEscapeCodes())
// flush queued data and wait for it to be written for a timeout, then wait for writer to shutdown
flush_writer(w_w, self.tty_write_channel, self.tty_writing_done_channel, self.pending_writes, 2*time.Second)
self.pending_writes = nil
// wait for tty reader to exit cleanly
for more := true; more; _, more = <-self.tty_read_channel {
}
}()
go write_to_tty(w_r, self.controlling_term, self.tty_write_channel, self.err_channel, self.write_done_channel, self.tty_writing_done_channel)
go read_from_tty(r_r, self.controlling_term, self.tty_read_channel, self.err_channel, self.tty_reading_done_channel)
if self.OnInitialize != nil {
finalizer, err = self.OnInitialize()
if err != nil {
return err
}
}
for self.keep_going {
self.queue_write_to_tty(nil)
timeout_chan := no_timeout_channel
if len(self.timers) > 0 {
now := time.Now()
err = self.dispatch_timers(now)
if err != nil {
return err
}
timeout := self.timers[0].deadline.Sub(now)
if timeout < 0 {
timeout = 0
}
timeout_chan = time.After(timeout)
}
select {
case <-timeout_chan:
case <-self.wakeup_channel:
for len(self.wakeup_channel) > 0 {
<-self.wakeup_channel
}
case msg_id := <-self.write_done_channel:
self.queue_write_to_tty(nil)
if self.OnWriteComplete != nil {
err = self.OnWriteComplete(msg_id)
if err != nil {
return err
}
}
case s := <-sigchnl:
err = self.on_signal(s.(unix.Signal))
if err != nil {
return err
}
case input_data, more := <-self.tty_read_channel:
if !more {
return io.EOF
}
err := self.dispatch_input_data(input_data)
if err != nil {
return err
}
}
}
return nil
}

View File

@@ -0,0 +1,145 @@
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
package loop
import (
"fmt"
"strings"
"kitty"
)
const (
SAVE_CURSOR = "\0337"
RESTORE_CURSOR = "\0338"
S7C1T = "\033 F"
SAVE_PRIVATE_MODE_VALUES = "\033[?s"
RESTORE_PRIVATE_MODE_VALUES = "\033[?r"
SAVE_COLORS = "\033[#P"
RESTORE_COLORS = "\033[#Q"
DECSACE_DEFAULT_REGION_SELECT = "\033[*x"
CLEAR_SCREEN = "\033[H\033[2J"
)
type Mode uint32
const private Mode = 1 << 31
const (
LNM Mode = 20
IRM Mode = 4
DECKM Mode = 1 | private
DECSCNM Mode = 5 | private
DECOM Mode = 6 | private
DECAWM Mode = 7 | private
DECARM Mode = 8 | private
DECTCEM Mode = 25 | private
MOUSE_BUTTON_TRACKING Mode = 1000 | private
MOUSE_MOTION_TRACKING Mode = 1002 | private
MOUSE_MOVE_TRACKING Mode = 1003 | private
FOCUS_TRACKING Mode = 1004 | private
MOUSE_UTF8_MODE Mode = 1005 | private
MOUSE_SGR_MODE Mode = 1006 | private
MOUSE_URXVT_MODE Mode = 1015 | private
MOUSE_SGR_PIXEL_MODE Mode = 1016 | private
ALTERNATE_SCREEN Mode = 1049 | private
BRACKETED_PASTE Mode = 2004 | private
PENDING_UPDATE Mode = 2026 | private
HANDLE_TERMIOS_SIGNALS Mode = kitty.HandleTermiosSignals | private
)
func (self Mode) escape_code(which string) string {
num := self
priv := ""
if num&private > 0 {
priv = "?"
num &^= private
}
return fmt.Sprintf("\033[%s%d%s", priv, uint32(num), which)
}
func (self Mode) EscapeCodeToSet() string {
return self.escape_code("h")
}
func (self Mode) EscapeCodeToReset() string {
return self.escape_code("l")
}
type MouseTracking uint8
const (
NO_MOUSE_TRACKING MouseTracking = iota
BUTTONS_ONLY_MOUSE_TRACKING
BUTTONS_AND_DRAG_MOUSE_TRACKING
FULL_MOUSE_TRACKING
)
type TerminalStateOptions struct {
alternate_screen, no_kitty_keyboard_mode bool
mouse_tracking MouseTracking
}
func set_modes(sb *strings.Builder, modes ...Mode) {
for _, m := range modes {
sb.WriteString(m.EscapeCodeToSet())
}
}
func reset_modes(sb *strings.Builder, modes ...Mode) {
for _, m := range modes {
sb.WriteString(m.EscapeCodeToReset())
}
}
func (self *TerminalStateOptions) SetStateEscapeCodes() []byte {
var sb strings.Builder
sb.Grow(256)
sb.WriteString(S7C1T)
if self.alternate_screen {
sb.WriteString(SAVE_CURSOR)
}
sb.WriteString(SAVE_PRIVATE_MODE_VALUES)
sb.WriteString(SAVE_COLORS)
sb.WriteString(DECSACE_DEFAULT_REGION_SELECT)
reset_modes(&sb,
IRM, DECKM, DECSCNM, BRACKETED_PASTE, FOCUS_TRACKING,
MOUSE_BUTTON_TRACKING, MOUSE_MOTION_TRACKING, MOUSE_MOVE_TRACKING, MOUSE_UTF8_MODE, MOUSE_SGR_MODE)
set_modes(&sb, DECARM, DECAWM, DECTCEM)
if self.alternate_screen {
set_modes(&sb, ALTERNATE_SCREEN)
sb.WriteString(CLEAR_SCREEN)
}
if self.no_kitty_keyboard_mode {
sb.WriteString("\033[>u")
} else {
sb.WriteString("\033[>31u")
}
if self.mouse_tracking != NO_MOUSE_TRACKING {
sb.WriteString(MOUSE_SGR_PIXEL_MODE.EscapeCodeToSet())
switch self.mouse_tracking {
case BUTTONS_ONLY_MOUSE_TRACKING:
sb.WriteString(MOUSE_BUTTON_TRACKING.EscapeCodeToSet())
case BUTTONS_AND_DRAG_MOUSE_TRACKING:
sb.WriteString(MOUSE_MOTION_TRACKING.EscapeCodeToSet())
case FULL_MOUSE_TRACKING:
sb.WriteString(MOUSE_MOVE_TRACKING.EscapeCodeToSet())
}
}
return []byte(sb.String())
}
func (self *TerminalStateOptions) ResetStateEscapeCodes() []byte {
var sb strings.Builder
sb.Grow(64)
sb.WriteString("\033[<u")
if self.alternate_screen {
sb.WriteString(ALTERNATE_SCREEN.EscapeCodeToReset())
} else {
sb.WriteString(SAVE_CURSOR)
}
sb.WriteString(RESTORE_PRIVATE_MODE_VALUES)
sb.WriteString(RESTORE_CURSOR)
sb.WriteString(RESTORE_COLORS)
return []byte(sb.String())
}

44
tools/tui/loop/timers.go Normal file
View File

@@ -0,0 +1,44 @@
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
package loop
import (
"sort"
"time"
)
func (self *Loop) dispatch_timers(now time.Time) error {
updated := false
remove := make(map[IdType]bool, 0)
for _, t := range self.timers {
if now.After(t.deadline) {
err := t.callback(t.id)
if err != nil {
return err
}
if t.repeats {
t.update_deadline(now)
updated = true
} else {
remove[t.id] = true
}
}
}
if len(remove) > 0 {
timers := make([]*timer, len(self.timers)-len(remove))
for _, t := range self.timers {
if !remove[t.id] {
timers = append(timers, t)
}
}
self.timers = timers
}
if updated {
self.sort_timers()
}
return nil
}
func (self *Loop) sort_timers() {
sort.SliceStable(self.timers, func(a, b int) bool { return self.timers[a].deadline.Before(self.timers[b].deadline) })
}

211
tools/tui/loop/write.go Normal file
View File

@@ -0,0 +1,211 @@
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
package loop
import (
"fmt"
"io"
"os"
"time"
"kitty/tools/tty"
"kitty/tools/utils"
)
type write_msg struct {
id IdType
bytes []byte
str string
}
func (self *write_msg) String() string {
return fmt.Sprintf("write_msg{%v %#v %#v}", self.id, string(self.bytes), self.str)
}
type write_dispatcher struct {
str string
bytes []byte
is_string bool
is_empty bool
}
func write_ignoring_temporary_errors(f *tty.Term, buf []byte) (int, error) {
n, err := f.Write(buf)
if err != nil {
if is_temporary_error(err) {
err = nil
}
return n, err
}
if n == 0 {
return 0, io.EOF
}
return n, err
}
func writestring_ignoring_temporary_errors(f *tty.Term, buf string) (int, error) {
n, err := f.WriteString(buf)
if err != nil {
if is_temporary_error(err) {
err = nil
}
return n, err
}
if n == 0 {
return 0, io.EOF
}
return n, err
}
func (self *Loop) queue_write_to_tty(data *write_msg) {
for len(self.pending_writes) > 0 {
select {
case self.tty_write_channel <- self.pending_writes[0]:
n := copy(self.pending_writes, self.pending_writes[1:])
self.pending_writes = self.pending_writes[:n]
default:
if data != nil {
self.pending_writes = append(self.pending_writes, data)
}
return
}
}
if data != nil {
select {
case self.tty_write_channel <- data:
default:
self.pending_writes = append(self.pending_writes, data)
}
}
}
func create_write_dispatcher(msg *write_msg) *write_dispatcher {
self := write_dispatcher{str: msg.str, bytes: msg.bytes, is_string: msg.bytes == nil}
if self.is_string {
self.is_empty = self.str == ""
} else {
self.is_empty = len(self.bytes) == 0
}
return &self
}
func (self *write_dispatcher) write(f *tty.Term) (int, error) {
if self.is_string {
return writestring_ignoring_temporary_errors(f, self.str)
}
return write_ignoring_temporary_errors(f, self.bytes)
}
func (self *write_dispatcher) slice(n int) {
if self.is_string {
self.str = self.str[n:]
self.is_empty = self.str == ""
} else {
self.bytes = self.bytes[n:]
self.is_empty = len(self.bytes) == 0
}
}
func write_to_tty(
pipe_r *os.File, term *tty.Term,
job_channel <-chan *write_msg, err_channel chan<- error, write_done_channel chan<- IdType, completed_channel chan<- byte,
) {
keep_going := true
defer func() {
pipe_r.Close()
close(completed_channel)
}()
selector := utils.CreateSelect(2)
pipe_fd := int(pipe_r.Fd())
tty_fd := term.Fd()
selector.RegisterRead(pipe_fd)
selector.RegisterWrite(tty_fd)
wait_for_write_available := func() {
_, err := selector.WaitForever()
if err != nil {
err_channel <- err
keep_going = false
return
}
if selector.IsReadyToWrite(tty_fd) {
return
}
if selector.IsReadyToRead(pipe_fd) {
keep_going = false
}
}
write_data := func(msg *write_msg) {
data := create_write_dispatcher(msg)
for !data.is_empty {
wait_for_write_available()
if !keep_going {
return
}
n, err := data.write(term)
if err != nil {
err_channel <- err
keep_going = false
return
}
if n > 0 {
data.slice(n)
}
}
}
for {
data, more := <-job_channel
if !more {
keep_going = false
break
}
write_data(data)
if keep_going {
write_done_channel <- data.id
} else {
break
}
}
}
func flush_writer(pipe_w *os.File, tty_write_channel chan<- *write_msg, tty_writing_done_channel <-chan byte, pending_writes []*write_msg, timeout time.Duration) {
writer_quit := false
defer func() {
if tty_write_channel != nil {
close(tty_write_channel)
tty_write_channel = nil
}
pipe_w.Close()
if !writer_quit {
<-tty_writing_done_channel
writer_quit = true
}
}()
deadline := time.Now().Add(timeout)
for len(pending_writes) > 0 {
timeout = deadline.Sub(time.Now())
if timeout <= 0 {
return
}
select {
case <-time.After(timeout):
return
case tty_write_channel <- pending_writes[0]:
pending_writes = pending_writes[1:]
}
}
close(tty_write_channel)
tty_write_channel = nil
timeout = deadline.Sub(time.Now())
if timeout <= 0 {
return
}
select {
case <-tty_writing_done_channel:
writer_quit = true
case <-time.After(timeout):
}
return
}