Start work on proper TUI support

This commit is contained in:
Kovid Goyal 2022-08-23 18:53:55 +05:30
parent 67f03621ae
commit 3c3e7b7f70
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
7 changed files with 483 additions and 40 deletions

View File

@ -3,10 +3,10 @@
import os import os
import subprocess import subprocess
import sys
from typing import Dict, List, Tuple from typing import Dict, List, Tuple
import kitty.constants as kc import kitty.constants as kc
from kittens.tui.operations import Mode
from kitty.cli import OptionDict, OptionSpecSeq, parse_option_spec from kitty.cli import OptionDict, OptionSpecSeq, parse_option_spec
from kitty.rc.base import RemoteCommand, all_command_names, command_for_name from kitty.rc.base import RemoteCommand, all_command_names, command_for_name
@ -184,9 +184,6 @@ def build_go_code(name: str, cmd: RemoteCommand, seq: OptionSpecSeq, template: s
def main() -> None: def main() -> None:
if 'prewarmed' in getattr(sys, 'kitty_run_data'):
os.environ.pop('KITTY_PREWARM_SOCKET')
os.execlp(sys.executable, sys.executable, '+launch', __file__, *sys.argv[1:])
with open('constants_generated.go', 'w') as f: with open('constants_generated.go', 'w') as f:
dp = ", ".join(map(lambda x: f'"{serialize_as_go_string(x)}"', kc.default_pager_for_help)) dp = ", ".join(map(lambda x: f'"{serialize_as_go_string(x)}"', kc.default_pager_for_help))
f.write(f'''\ f.write(f'''\
@ -197,13 +194,14 @@ package kitty
type VersionType struct {{ type VersionType struct {{
Major, Minor, Patch int Major, Minor, Patch int
}} }}
var VersionString string = "{kc.str_version}" const VersionString string = "{kc.str_version}"
var WebsiteBaseURL string = "{kc.website_base_url}" const WebsiteBaseURL string = "{kc.website_base_url}"
const VCSRevision string = ""
const RC_ENCRYPTION_PROTOCOL_VERSION string = "{kc.RC_ENCRYPTION_PROTOCOL_VERSION}"
const IsFrozenBuild bool = false
const HandleTermiosSignals = {Mode.HANDLE_TERMIOS_SIGNALS.value[0]}
var Version VersionType = VersionType{{Major: {kc.version.major}, Minor: {kc.version.minor}, Patch: {kc.version.patch},}} var Version VersionType = VersionType{{Major: {kc.version.major}, Minor: {kc.version.minor}, Patch: {kc.version.patch},}}
var DefaultPager []string = []string{{ {dp} }} var DefaultPager []string = []string{{ {dp} }}
var VCSRevision string = ""
var RC_ENCRYPTION_PROTOCOL_VERSION string = "{kc.RC_ENCRYPTION_PROTOCOL_VERSION}"
var IsFrozenBuild bool = false
''') ''')
with open('tools/cmd/at/template.go') as f: with open('tools/cmd/at/template.go') as f:
template = f.read() template = f.read()

135
tools/tui/loop.go Normal file
View File

@ -0,0 +1,135 @@
package tui
import (
"fmt"
"io"
"kitty/tools/tty"
"os"
"time"
)
type TerminalState struct {
alternate_screen, grab_mouse bool
}
type Loop struct {
controlling_term *tty.Term
keep_going bool
flush_write_buf bool
write_buf []byte
}
func CreateLoop() (*Loop, error) {
l := Loop{controlling_term: nil}
return &l, nil
}
func (self *Loop) Run() (err error) {
signal_read_file, signal_write_file, err := os.Pipe()
if err != nil {
return err
}
defer func() {
signal_read_file.Close()
signal_write_file.Close()
}()
sigchnl := make(chan os.Signal, 256)
reset_signals := notify_signals(sigchnl, SIGINT, SIGTERM, SIGTSTP, SIGHUP)
defer reset_signals()
go func() {
for {
s := <-sigchnl
if write_signal(signal_write_file, s) != nil {
break
}
}
}()
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
}
var selector Select
selector.RegisterRead(int(signal_read_file.Fd()))
selector.RegisterRead(controlling_term.Fd())
self.keep_going = true
self.flush_write_buf = true
defer func() {
if self.flush_write_buf {
self.flush()
}
}()
for self.keep_going {
num_ready, err := selector.WaitForever()
if err != nil {
return fmt.Errorf("Failed to call select() with error: %w", err)
}
if num_ready == 0 {
continue
}
}
return nil
}
func (self *Loop) write() error {
if len(self.write_buf) == 0 || self.controlling_term == nil {
return nil
}
n, err := self.controlling_term.Write(self.write_buf)
if err != nil {
return err
}
if n == 0 {
return io.EOF
}
remainder := self.write_buf[n:]
if len(remainder) > 0 {
self.write_buf = self.write_buf[:len(remainder)]
copy(self.write_buf, remainder)
} else {
self.write_buf = self.write_buf[:0]
}
return nil
}
func (self *Loop) flush() error {
var selector Select
if self.controlling_term == nil {
return nil
}
selector.RegisterWrite(self.controlling_term.Fd())
deadline := time.Now().Add(2 * time.Second)
for len(self.write_buf) > 0 {
timeout := deadline.Sub(time.Now())
if timeout < 0 {
break
}
num_ready, err := selector.Wait(timeout)
if err != nil {
return err
}
if num_ready > 0 && selector.IsReadyToWrite(self.controlling_term.Fd()) {
err = self.write()
if err != nil {
return err
}
}
}
return nil
}

95
tools/tui/select.go Normal file
View File

@ -0,0 +1,95 @@
package tui
import (
"time"
"golang.org/x/sys/unix"
"kitty/tools/utils"
)
type Select struct {
read_set, write_set, err_set unix.FdSet
read_fds, write_fds, err_fds map[int]bool
}
func (self *Select) register(fd int, fdset *map[int]bool) {
(*fdset)[fd] = true
}
func (self *Select) RegisterRead(fd int) {
self.register(fd, &self.read_fds)
}
func (self *Select) RegisterWrite(fd int) {
self.register(fd, &self.write_fds)
}
func (self *Select) RegisterError(fd int) {
self.register(fd, &self.err_fds)
}
func (self *Select) unregister(fd int, fdset *map[int]bool) {
(*fdset)[fd] = false
}
func (self *Select) UnRegisterRead(fd int) {
self.unregister(fd, &self.read_fds)
}
func (self *Select) UnRegisterWrite(fd int) {
self.unregister(fd, &self.write_fds)
}
func (self *Select) UnRegisterError(fd int) {
self.unregister(fd, &self.err_fds)
}
func (self *Select) Wait(timeout time.Duration) (num_ready int, err error) {
self.read_set.Zero()
self.write_set.Zero()
self.err_set.Zero()
max_fd_num := 0
init_set := func(s *unix.FdSet, m *map[int]bool) {
s.Zero()
for fd, enabled := range *m {
if fd > -1 && enabled {
if max_fd_num < fd {
max_fd_num = fd
}
s.Set(fd)
}
}
}
init_set(&self.read_set, &self.read_fds)
init_set(&self.write_set, &self.write_fds)
init_set(&self.err_set, &self.err_fds)
num_ready, err = utils.Select(max_fd_num+1, &self.read_set, &self.write_set, &self.err_set, timeout)
if err == unix.EINTR {
return 0, nil
}
return
}
func (self *Select) WaitForever() (num_ready int, err error) {
return self.Wait(-1)
}
func (self *Select) IsReadyToRead(fd int) bool {
return fd > -1 && self.read_set.IsSet(fd)
}
func (self *Select) IsReadyToWrite(fd int) bool {
return fd > -1 && self.write_set.IsSet(fd)
}
func (self *Select) IsErrored(fd int) bool {
return fd > -1 && self.err_set.IsSet(fd)
}
func (self *Select) UnregisterAll() {
self.read_fds = make(map[int]bool)
self.write_fds = make(map[int]bool)
self.err_fds = make(map[int]bool)
}

96
tools/tui/signal.go Normal file
View File

@ -0,0 +1,96 @@
package tui
import (
"os"
"os/signal"
"syscall"
)
type Signal byte
const (
SIGNULL Signal = 0
SIGINT Signal = 1
SIGTERM Signal = 2
SIGTSTP Signal = 3
SIGHUP Signal = 4
SIGTTIN Signal = 5
SIGTTOU Signal = 6
SIGUSR1 Signal = 7
SIGUSR2 Signal = 8
SIGALRM Signal = 9
)
func as_signal(which os.Signal) Signal {
switch which {
case os.Interrupt:
return SIGINT
case syscall.SIGTERM:
return SIGTERM
case syscall.SIGTSTP:
return SIGTSTP
case syscall.SIGHUP:
return SIGHUP
case syscall.SIGTTIN:
return SIGTTIN
case syscall.SIGTTOU:
return SIGTTOU
case syscall.SIGUSR1:
return SIGUSR1
case syscall.SIGUSR2:
return SIGUSR2
case syscall.SIGALRM:
return SIGALRM
default:
return SIGNULL
}
}
const zero_go_signal = syscall.Signal(0)
func as_go_signal(which Signal) os.Signal {
switch which {
case SIGINT:
return os.Interrupt
case SIGTERM:
return syscall.SIGTERM
case SIGTSTP:
return syscall.SIGTSTP
case SIGHUP:
return syscall.SIGHUP
case SIGTTIN:
return syscall.SIGTTIN
case SIGTTOU:
return syscall.SIGTTOU
case SIGUSR1:
return syscall.SIGUSR1
case SIGUSR2:
return syscall.SIGUSR2
case SIGALRM:
return syscall.SIGALRM
default:
return zero_go_signal
}
}
func write_signal(dest *os.File, which os.Signal) error {
b := make([]byte, 1)
b[0] = byte(as_signal(which))
if b[0] == 0 {
return nil
}
_, err := dest.Write(b)
return err
}
func notify_signals(c chan os.Signal, signals ...Signal) func() {
s := make([]os.Signal, len(signals))
for i, x := range signals {
g := as_go_signal(x)
if g != zero_go_signal {
s[i] = g
}
}
signal.Notify(c, s...)
return func() { signal.Reset(s...) }
}

142
tools/tui/terminal-state.go Normal file
View File

@ -0,0 +1,142 @@
package tui
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 = 4
DECKM = 1 | private
DECSCNM = 5 | private
DECOM = 6 | private
DECAWM = 7 | private
DECARM = 8 | private
DECTCEM = 25 | private
MOUSE_BUTTON_TRACKING = 1000 | private
MOUSE_MOTION_TRACKING = 1002 | private
MOUSE_MOVE_TRACKING = 1003 | private
FOCUS_TRACKING = 1004 | private
MOUSE_UTF8_MODE = 1005 | private
MOUSE_SGR_MODE = 1006 | private
MOUSE_URXVT_MODE = 1015 | private
MOUSE_SGR_PIXEL_MODE = 1016 | private
ALTERNATE_SCREEN = 1049 | private
BRACKETED_PASTE = 2004 | private
PENDING_UPDATE = 2026 | private
HANDLE_TERMIOS_SIGNALS = 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("h")
}
type MouseTracking uint8
const (
NO_MOUSE_TRACKING MouseTracking = iota
BUTTONS_ONLY_MOUSE_TRACKING
BUTTONS_AND_DRAG_MOUSE_TRACKING
FULL_MOUSE_TRACKING
)
type TerminalState struct {
alternate_screen, 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 *TerminalState) 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, MOUSE_BUTTON_TRACKING, MOUSE_MOTION_TRACKING,
MOUSE_MOVE_TRACKING, FOCUS_TRACKING, MOUSE_UTF8_MODE, MOUSE_SGR_MODE, BRACKETED_PASTE)
set_modes(&sb, DECARM, DECAWM, DECTCEM)
if self.alternate_screen {
set_modes(&sb, ALTERNATE_SCREEN)
sb.WriteString(CLEAR_SCREEN)
}
if self.kitty_keyboard_mode {
sb.WriteString("\033[>31u")
} else {
sb.WriteString("\033[>u")
}
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 *TerminalState) ResetStateData() []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())
}

View File

@ -3,27 +3,15 @@
package utils package utils
import ( import (
"os"
"time" "time"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
func Select(nfd int, r *unix.FdSet, w *unix.FdSet, e *unix.FdSet, timeout time.Duration) (n int, err error) { func Select(nfd int, r *unix.FdSet, w *unix.FdSet, e *unix.FdSet, timeout time.Duration) (n int, err error) {
deadline := time.Now().Add(timeout) if timeout < 0 {
for { return unix.Pselect(nfd, r, w, e, nil, nil)
t := deadline.Sub(time.Now())
if t < 0 {
t = 0
}
ts := NsecToTimespec(t)
q, qerr := unix.Pselect(nfd, r, w, w, &ts, nil)
if qerr == unix.EINTR {
if time.Now().After(deadline) {
return 0, os.ErrDeadlineExceeded
}
continue
}
return q, qerr
} }
ts := NsecToTimespec(timeout)
return unix.Pselect(nfd, r, w, e, &ts, nil)
} }

View File

@ -12,20 +12,9 @@ import (
// Go unix does not wrap pselect on darwin // Go unix does not wrap pselect on darwin
func Select(nfd int, r *unix.FdSet, w *unix.FdSet, e *unix.FdSet, timeout time.Duration) (n int, err error) { func Select(nfd int, r *unix.FdSet, w *unix.FdSet, e *unix.FdSet, timeout time.Duration) (n int, err error) {
deadline := time.Now().Add(timeout) if timeout < 0 {
for { return unix.Select(nfd, r, w, e, nil)
t := deadline.Sub(time.Now())
if t < 0 {
t = 0
}
ts := NsecToTimeval(t)
q, qerr := unix.Select(nfd, r, w, w, &ts)
if qerr == unix.EINTR {
if time.Now().After(deadline) {
return 0, os.ErrDeadlineExceeded
}
continue
}
return q, qerr
} }
ts := NsecToTimeval(timeout)
return unix.Select(nfd, r, w, e, &ts)
} }