diff --git a/gen-rc-go.py b/gen-rc-go.py index a094ec4c9..f8a155ca0 100755 --- a/gen-rc-go.py +++ b/gen-rc-go.py @@ -3,10 +3,10 @@ import os import subprocess -import sys from typing import Dict, List, Tuple import kitty.constants as kc +from kittens.tui.operations import Mode from kitty.cli import OptionDict, OptionSpecSeq, parse_option_spec 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: - 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: dp = ", ".join(map(lambda x: f'"{serialize_as_go_string(x)}"', kc.default_pager_for_help)) f.write(f'''\ @@ -197,13 +194,14 @@ package kitty type VersionType struct {{ Major, Minor, Patch int }} -var VersionString string = "{kc.str_version}" -var WebsiteBaseURL string = "{kc.website_base_url}" +const VersionString string = "{kc.str_version}" +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 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: template = f.read() diff --git a/tools/tui/loop.go b/tools/tui/loop.go new file mode 100644 index 000000000..97b25604f --- /dev/null +++ b/tools/tui/loop.go @@ -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 +} diff --git a/tools/tui/select.go b/tools/tui/select.go new file mode 100644 index 000000000..571ffdb03 --- /dev/null +++ b/tools/tui/select.go @@ -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) +} diff --git a/tools/tui/signal.go b/tools/tui/signal.go new file mode 100644 index 000000000..1c40d1471 --- /dev/null +++ b/tools/tui/signal.go @@ -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...) } +} diff --git a/tools/tui/terminal-state.go b/tools/tui/terminal-state.go new file mode 100644 index 000000000..300525bd2 --- /dev/null +++ b/tools/tui/terminal-state.go @@ -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[