diff --git a/kittens/__init__.py b/kittens/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/kittens/loop.py b/kittens/loop.py new file mode 100644 index 000000000..b869f2aed --- /dev/null +++ b/kittens/loop.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2018, Kovid Goyal + +import fcntl +import os +import selectors +import sys +import termios +import tty + +from contextlib import closing, contextmanager + + +@contextmanager +def non_block(fd): + oldfl = fcntl.fcntl(fd, fcntl.F_GETFL) + fcntl.fcntl(fd, fcntl.F_SETFL, oldfl | os.O_NONBLOCK) + yield + fcntl.fcntl(fd, fcntl.F_SETFL, oldfl) + + +@contextmanager +def raw_terminal(fd): + isatty = os.isatty(fd) + if isatty: + old = termios.tcgetattr(fd) + tty.setraw(fd) + yield + if isatty: + termios.tcsetattr(fd, termios.TCSADRAIN, old) + + +class Loop: + + def __init__(self, input_fd=None, output_fd=None): + self.input_fd = input_fd or sys.stdin.fileno() + self.output_fd = output_fd or sys.stdout.fileno() + self.wakeup_read_fd, self.wakeup_write_fd = os.pipe() + self.sel = s = selectors.DefaultSelector() + s.register(self.input_fd, selectors.EVENT_READ, self._read_ready) + s.register(self.wakeup_read_fd, selectors.EVENT_READ, self._wakeup_ready) + s.register(self.output_fd, selectors.EVENT_WRITE, self._write_ready) + self.keep_going = True + self.return_code = 0 + + def _read_ready(self, handler): + pass + + def _write_ready(self, handler): + pass + + def _on_unhandled_exception(self, tb): + pass + + def _wakeup_ready(self, handler): + os.read(self.wakeup_read_fd) + + def wakeup(self): + os.write(self.wakeup_write_fd, b'1') + + def _loop(self, handler): + select = self.sel.select + tb = None + waiting_for_write = True + with closing(self.sel), non_block(self.input_fd), non_block(self.output_fd), raw_terminal(self.input_fd): + handler.write_buf.insert(0, b'\033 F\033?1049h + while self.keep_going: + has_data_to_write = bool(handler.write_buf) + if has_data_to_write != waiting_for_write: + waiting_for_write = has_data_to_write + self.sel.modify(self.output_fd, selectors.EVENT_WRITE if waiting_for_write else 0, self._write_ready) + events = select() + for key, mask in events: + try: + key.data(handler) + except Exception: + import traceback + tb = traceback.format_exc() + self.keep_going = False + self.return_code = 1 + break + if tb is not None: + self._report_error_loop(tb) + + def _report_error_loop(self, tb): + raise NotImplementedError('TODO: Implement') diff --git a/kittens/tui/__init__.py b/kittens/tui/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/kittens/tui/loop.py b/kittens/tui/loop.py new file mode 100644 index 000000000..5c18bcdba --- /dev/null +++ b/kittens/tui/loop.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2018, Kovid Goyal + +import codecs +import fcntl +import io +import os +import selectors +import sys +import termios +import tty +from contextlib import closing, contextmanager + +from .operations import init_state, reset_state + + +@contextmanager +def non_block(fd): + oldfl = fcntl.fcntl(fd, fcntl.F_GETFL) + fcntl.fcntl(fd, fcntl.F_SETFL, oldfl | os.O_NONBLOCK) + yield + fcntl.fcntl(fd, fcntl.F_SETFL, oldfl) + + +@contextmanager +def raw_terminal(fd): + isatty = os.isatty(fd) + if isatty: + old = termios.tcgetattr(fd) + tty.setraw(fd) + yield + if isatty: + termios.tcsetattr(fd, termios.TCSADRAIN, old) + + +def write_all(fd, data): + while data: + n = os.write(fd, data) + if not n: + break + data = data[n:] + + +@contextmanager +def sanitize_term(output_fd): + write_all(output_fd, init_state()) + yield + write_all(output_fd, reset_state()) + + +class Loop: + + def __init__(self, input_fd=None, output_fd=None): + self.input_fd = input_fd or sys.stdin.fileno() + self.output_fd = output_fd or sys.stdout.fileno() + self.wakeup_read_fd, self.wakeup_write_fd = os.pipe() + self.sel = s = selectors.DefaultSelector() + s.register(self.input_fd, selectors.EVENT_READ, self._read_ready) + s.register( + self.wakeup_read_fd, selectors.EVENT_READ, self._wakeup_ready + ) + s.register(self.output_fd, selectors.EVENT_WRITE, self._write_ready) + self.return_code = 0 + self.read_allowed = True + self.read_buf = '' + self.decoder = codecs.IncrementalDecoder(errors='ignore') + try: + self.iov_limit = os.sysconf('SC_IOV_MAX') - 1 + except Exception: + self.iov_limit = 255 + + def _read_ready(self, handler): + if not self.read_allowed: + return + data = os.read(self.input_fd, io.DEFAULT_BUFFER_SIZE) + if not data: + raise EOFError('The input stream is closed') + data = self.decoder.decode(data) + if self.read_buf: + data = self.read_buf + data + self.read_buf = data + + def _write_ready(self, handler): + if len(handler.write_buf) > self.iov_limit: + handler.write_buf[self.iov_limit - 1] = b''.join(handler.write_buf[self.iov_limit - 1:]) + del handler.write_buf[self.iov_limit:] + sizes = tuple(map(len, handler.write_buf)) + written = os.writev(self.output_fd, handler.write_buf) + if not written: + raise EOFError('The output stream is closed') + if written >= sum(sizes): + handler.write_buf = [] + else: + consumed = 0 + for i, buf in enumerate(handler.write_buf): + if not written: + break + if len(buf) <= written: + written -= len(buf) + consumed += 1 + continue + handler.write_buf[i] = buf[written:] + break + del handler.write_buf[:consumed] + + def _on_unhandled_exception(self, tb): + pass + + def _wakeup_ready(self, handler): + os.read(self.wakeup_read_fd) + + def wakeup(self): + os.write(self.wakeup_write_fd, b'1') + + def quit(self, return_code=None): + self.read_allowed = False + if return_code is not None: + self.return_code = return_code + + def _loop(self, handler): + select = self.sel.select + tb = None + waiting_for_write = True + with closing(self.sel), sanitize_term(self.output_fd), non_block(self.input_fd), non_block(self.output_fd), raw_terminal(self.input_fd): + while True: + has_data_to_write = bool(handler.write_buf) + if not has_data_to_write and not self.read_allowed: + break + if has_data_to_write != waiting_for_write: + waiting_for_write = has_data_to_write + self.sel.modify( + self.output_fd, selectors.EVENT_WRITE + if waiting_for_write else 0, self._write_ready + ) + events = select() + for key, mask in events: + try: + key.data(handler) + except Exception: + import traceback + tb = traceback.format_exc() + self.return_code = 1 + break + if tb is not None: + self._report_error_loop(tb) + + def _report_error_loop(self, tb): + raise NotImplementedError('TODO: Implement') diff --git a/kittens/tui/operations.py b/kittens/tui/operations.py new file mode 100644 index 000000000..9015b7483 --- /dev/null +++ b/kittens/tui/operations.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2018, Kovid Goyal + +S7C1T = b'\033 F' +SAVE_CURSOR = b'\0337' +RESTORE_CURSOR = b'\0338' +SAVE_PRIVATE_MODE_VALUES = b'\033[?s' +RESTORE_PRIVATE_MODE_VALUES = b'\033[?r' + +MODES = dict( + LNM=(20, ''), + IRM=(4, ''), + DECKM=(1, '?'), + DECSCNM=(5, '?'), + DECOM=(6, '?'), + DECAWM=(6, '?'), + DECARM=(8, '?'), + DECTCEM=(25, '?'), + MOUSE_BUTTON_TRACKING=(1000, '?'), + MOUSE_MOTION_TRACKING=(1002, '?'), + MOUSE_MOVE_TRACKING=(1003, '?'), + FOCUS_TRACKING=(1004, '?'), + MOUSE_UTF8_MODE=(1005, '?'), + MOUSE_SGR_MODE=(1006, '?'), + MOUSE_URXVT_MODE=(1015, '?'), + ALTERNATE_SCREEN=(1049, '?'), + BRACKETED_PASTE=(2004, '?'), + EXTENDED_KEYBOARD=(2017, '?'), +) + + +def set_mode(which, private=True): + num, private = MODES[which] + return '\033[{}{}h'.format(private, num).encode('ascii') + + +def reset_mode(which): + num, private = MODES[which] + return '\033[{}{}l'.format(private, num).encode('ascii') + + +def init_state(alternate_screen=True): + ans = ( + S7C1T + SAVE_CURSOR + SAVE_PRIVATE_MODE_VALUES + reset_mode('LNM') + + reset_mode('IRM') + reset_mode('DECKM') + reset_mode('DECSCNM') + + set_mode('DECARM') + reset_mode('DECOM') + set_mode('DECAWM') + + set_mode('DECTCEM') + reset_mode('MOUSE_BUTTON_TRACKING') + + reset_mode('MOUSE_MOTION_TRACKING') + reset_mode('MOUSE_MOVE_TRACKING') + + reset_mode('FOCUS_TRACKING') + reset_mode('MOUSE_UTF8_MODE') + + reset_mode('MOUSE_SGR_MODE') + reset_mode('MOUSE_UTF8_MODE') + + set_mode('BRACKETED_PASTE') + set_mode('EXTENDED_KEYBOARD') + ) + if alternate_screen: + ans += set_mode('ALTERNATE_SCREEN') + return ans + + +def reset_state(normal_screen=True): + ans = b'' + if normal_screen: + ans += reset_mode('ALTERNATE_SCREEN') + ans += RESTORE_PRIVATE_MODE_VALUES + ans += RESTORE_CURSOR + return ans