Framework for kittens
This commit is contained in:
parent
6736fd3835
commit
c2cb43cc07
0
kittens/__init__.py
Normal file
0
kittens/__init__.py
Normal file
87
kittens/loop.py
Normal file
87
kittens/loop.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=utf-8
|
||||||
|
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
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')
|
||||||
0
kittens/tui/__init__.py
Normal file
0
kittens/tui/__init__.py
Normal file
149
kittens/tui/loop.py
Normal file
149
kittens/tui/loop.py
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=utf-8
|
||||||
|
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
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')
|
||||||
65
kittens/tui/operations.py
Normal file
65
kittens/tui/operations.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=utf-8
|
||||||
|
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
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
|
||||||
Loading…
x
Reference in New Issue
Block a user