btui/Python/btui.py

343 lines
11 KiB
Python

import ctypes
import enum
import functools
import time
import os.path
from contextlib import contextmanager
__all__ = ['open', 'TextAttr', 'ClearType', 'CursorType', 'BTUIMode']
# Load the shared library into c types.
libbtui = ctypes.CDLL(os.path.dirname(os.path.abspath(__file__)) + os.path.sep + "libbtui.so")
class FILE(ctypes.Structure):
pass
class BTUI_struct(ctypes.Structure):
_fields_ = [
('in', ctypes.POINTER(FILE)),
('out', ctypes.POINTER(FILE)),
('width', ctypes.c_int),
('height', ctypes.c_int),
('size_changed', ctypes.c_int),
]
libbtui.btui_create.restype = ctypes.POINTER(BTUI_struct)
attr = lambda name: ctypes.c_longlong.in_dll(libbtui, name).value
attr_t = ctypes.c_longlong
class TextAttr(enum.IntEnum):
NORMAL = attr('BTUI_NORMAL')
BOLD = attr('BTUI_BOLD')
FAINT = attr('BTUI_FAINT')
DIM = attr('BTUI_FAINT')
ITALIC = attr('BTUI_ITALIC')
UNDERLINE = attr('BTUI_UNDERLINE')
BLINK_SLOW = attr('BTUI_BLINK_SLOW')
BLINK_FAST = attr('BTUI_BLINK_FAST')
REVERSE = attr('BTUI_REVERSE')
CONCEAL = attr('BTUI_CONCEAL')
STRIKETHROUGH = attr('BTUI_STRIKETHROUGH')
FRAKTUR = attr('BTUI_FRAKTUR')
DOUBLE_UNDERLINE = attr('BTUI_DOUBLE_UNDERLINE')
NO_BOLD_OR_FAINT = attr('BTUI_NO_BOLD_OR_FAINT')
NO_ITALIC_OR_FRAKTUR = attr('BTUI_NO_ITALIC_OR_FRAKTUR')
NO_UNDERLINE = attr('BTUI_NO_UNDERLINE')
NO_BLINK = attr('BTUI_NO_BLINK')
NO_REVERSE = attr('BTUI_NO_REVERSE')
NO_CONCEAL = attr('BTUI_NO_CONCEAL')
NO_STRIKETHROUGH = attr('BTUI_NO_STRIKETHROUGH')
FG_BLACK = attr('BTUI_FG_BLACK')
FG_RED = attr('BTUI_FG_RED')
FG_GREEN = attr('BTUI_FG_GREEN')
FG_YELLOW = attr('BTUI_FG_YELLOW')
FG_BLUE = attr('BTUI_FG_BLUE')
FG_MAGENTA = attr('BTUI_FG_MAGENTA')
FG_CYAN = attr('BTUI_FG_CYAN')
FG_WHITE = attr('BTUI_FG_WHITE')
FG_NORMAL = attr('BTUI_FG_NORMAL')
BG_BLACK = attr('BTUI_BG_BLACK')
BG_RED = attr('BTUI_BG_RED')
BG_GREEN = attr('BTUI_BG_GREEN')
BG_YELLOW = attr('BTUI_BG_YELLOW')
BG_BLUE = attr('BTUI_BG_BLUE')
BG_MAGENTA = attr('BTUI_BG_MAGENTA')
BG_CYAN = attr('BTUI_BG_CYAN')
BG_WHITE = attr('BTUI_BG_WHITE')
BG_NORMAL = attr('BTUI_BG_NORMAL')
FRAMED = attr('BTUI_FRAMED')
ENCIRCLED = attr('BTUI_ENCIRCLED')
OVERLINED = attr('BTUI_OVERLINED')
NO_FRAMED_OR_ENCIRCLED = attr('BTUI_NO_FRAMED_OR_ENCIRCLED')
NO_OVERLINED = attr('BTUI_NO_OVERLINED')
BTUI_INVERSE_ATTRS = {
TextAttr.NORMAL : TextAttr.NORMAL,
TextAttr.BOLD : TextAttr.NO_BOLD_OR_FAINT,
TextAttr.FAINT : TextAttr.NO_BOLD_OR_FAINT,
TextAttr.DIM : TextAttr.NO_BOLD_OR_FAINT,
TextAttr.ITALIC : TextAttr.NO_ITALIC_OR_FRAKTUR,
TextAttr.UNDERLINE : TextAttr.NO_UNDERLINE,
TextAttr.BLINK_SLOW : TextAttr.NO_BLINK,
TextAttr.BLINK_FAST : TextAttr.NO_BLINK,
TextAttr.REVERSE : TextAttr.NO_REVERSE,
TextAttr.CONCEAL : TextAttr.NO_CONCEAL,
TextAttr.STRIKETHROUGH : TextAttr.NO_STRIKETHROUGH,
TextAttr.FRAKTUR : TextAttr.NO_ITALIC_OR_FRAKTUR,
TextAttr.DOUBLE_UNDERLINE: TextAttr.NO_UNDERLINE,
TextAttr.FG_BLACK : TextAttr.FG_NORMAL,
TextAttr.FG_RED : TextAttr.FG_NORMAL,
TextAttr.FG_GREEN : TextAttr.FG_NORMAL,
TextAttr.FG_YELLOW : TextAttr.FG_NORMAL,
TextAttr.FG_BLUE : TextAttr.FG_NORMAL,
TextAttr.FG_MAGENTA : TextAttr.FG_NORMAL,
TextAttr.FG_CYAN : TextAttr.FG_NORMAL,
TextAttr.FG_WHITE : TextAttr.FG_NORMAL,
TextAttr.FG_NORMAL : TextAttr.FG_NORMAL,
TextAttr.BG_BLACK : TextAttr.BG_NORMAL,
TextAttr.BG_RED : TextAttr.BG_NORMAL,
TextAttr.BG_GREEN : TextAttr.BG_NORMAL,
TextAttr.BG_YELLOW : TextAttr.BG_NORMAL,
TextAttr.BG_BLUE : TextAttr.BG_NORMAL,
TextAttr.BG_MAGENTA : TextAttr.BG_NORMAL,
TextAttr.BG_CYAN : TextAttr.BG_NORMAL,
TextAttr.BG_WHITE : TextAttr.BG_NORMAL,
TextAttr.BG_NORMAL : TextAttr.BG_NORMAL,
TextAttr.FRAMED : TextAttr.NO_FRAMED_OR_ENCIRCLED,
TextAttr.ENCIRCLED : TextAttr.NO_FRAMED_OR_ENCIRCLED,
TextAttr.OVERLINED : TextAttr.NO_OVERLINED,
}
class BTUIMode(enum.IntEnum):
UNINITIALIZED = 0
NORMAL = 1
TUI = 2
class CursorType(enum.IntEnum):
DEFAULT = 0
BLINKING_BLOCK = 1
BLOCK = 2
BLINKING_UNDERLINE = 3
UNDERLINE = 4
BLINKING_BAR = 5
BAR = 6
class ClearType(enum.IntEnum):
SCREEN = 0
ABOVE = 1
BELOW = 2
LINE = 3
LEFT = 4
RIGHT = 5
class BTUI:
_autoflush = True
@contextmanager
def attributes(self, *attrs):
self.set_attributes(*attrs)
try: yield
finally: self.unset_attributes(*attrs)
@contextmanager
def bg(self, r, g, b):
self.set_bg(r, g, b)
try: yield
finally: self.set_attributes("bg_normal")
def clear(self, clear_type=ClearType.SCREEN):
assert self._btui
if isinstance(clear_type, str):
clear_type = ClearType[clear_type.upper()]
libbtui.btui_clear(self._btui, clear_type)
if self._autoflush:
libbtui.btui_flush(self._btui)
def disable(self):
libbtui.btui_disable(self._btui)
@contextmanager
def disabled(self):
self.disable()
try: yield self
finally: self.enable()
def draw_shadow(self, x, y, w, h):
assert self._btui
libbtui.btui_draw_shadow(self._btui, int(x), int(y), int(w), int(h))
libbtui.btui_flush(self._btui)
def enable(self, mode=BTUIMode.TUI):
if isinstance(mode, str):
mode = BTUIMode[mode.upper()]
self._btui = libbtui.btui_create(mode)
def set_mode(self, mode):
if isinstance(mode, str):
mode = BTUIMode[mode.upper()]
self._btui = libbtui.set_mode(self._btui, mode)
@contextmanager
def fg(self, r, g, b):
self.set_fg(r, g, b)
try: yield
finally: self.set_attributes("fg_normal")
def fill_box(self, x, y, w, h):
assert self._btui
libbtui.btui_fill_box(self._btui, int(x), int(y), int(w), int(h))
if self._autoflush:
libbtui.btui_flush(self._btui)
def flush(self):
assert self._btui
libbtui.btui_flush(self._btui)
@contextmanager
def buffered(self):
assert self._btui
prev_autoflush = self._autoflush
self._autoflush = False
yield
self._autoflush = prev_autoflush
self.flush()
def getkey(self, timeout=None):
assert self._btui
timeout = -1 if timeout is None else int(timeout)
mouse_x, mouse_y = ctypes.c_int(-1), ctypes.c_int(-1)
key = libbtui.btui_getkey(self._btui, timeout,
ctypes.byref(mouse_x), ctypes.byref(mouse_y))
buf = ctypes.create_string_buffer(64)
libbtui.btui_keyname(key, buf)
if mouse_x.value == -1:
return buf.value.decode('utf8'), None, None
else:
return buf.value.decode('utf8'), mouse_x.value, mouse_y.value
@property
def height(self):
assert self._btui
return self._btui.contents.height
def move(self, x, y):
assert self._btui
libbtui.btui_move_cursor(self._btui, int(x), int(y))
if self._autoflush:
libbtui.btui_flush(self._btui)
def set_cursor(self, cursor_type=CursorType.DEFAULT):
assert self._btui
if isinstance(cursor_type, str):
cursor_type = CursorType[cursor_type.upper()]
libbtui.btui_set_cursor(self._btui, cursor_type)
if self._autoflush:
libbtui.btui_flush(self._btui)
def hide_cursor(self):
assert self._btui
libbtui.btui_hide_cursor(self._btui)
if self._autoflush:
libbtui.btui_flush(self._btui)
def show_cursor(self):
assert self._btui
libbtui.btui_show_cursor(self._btui)
if self._autoflush:
libbtui.btui_flush(self._btui)
def outline_box(self, x, y, w, h):
assert self._btui
libbtui.btui_draw_linebox(self._btui, int(x), int(y), int(w), int(h))
if self._autoflush:
libbtui.btui_flush(self._btui)
def scroll(self, firstline, lastline=None, amount=None):
assert self._btui
if amount is None:
amount = firstline
firstline, lastline = 0, self.height-1
libbtui.btui_scroll(self._btui, firstline, lastline, amount)
if self._autoflush:
libbtui.btui_flush(self._btui)
def set_attributes(self, *attrs):
assert self._btui
attr_long = ctypes.c_longlong(0)
for a in attrs:
if isinstance(a, str):
a = TextAttr[a.upper()]
attr_long.value |= a
libbtui.btui_set_attributes(self._btui, attr_long)
def set_bg(self, r, g, b):
assert self._btui
libbtui.btui_set_bg(self._btui, int(r*255), int(g*255), int(b*255))
def set_fg(self, r, g, b):
assert self._btui
libbtui.btui_set_fg(self._btui, int(r*255), int(g*255), int(b*255))
def suspend(self):
assert self._btui
libbtui.btui_suspend(self._btui)
def unset_attributes(self, *attrs):
assert self._btui
attr_long = ctypes.c_longlong(0)
for a in attrs:
if isinstance(a, str):
a = TextAttr[a.upper()]
attr_long.value |= BTUI_INVERSE_ATTRS[a]
libbtui.btui_set_attributes(self._btui, attr_long)
@property
def width(self):
assert self._btui
return self._btui.contents.width
def write(self, *args, sep=''):
assert self._btui
s = sep.join(args)
self.write_bytes(bytes(s, 'utf8'))
def write_bytes(self, b):
assert self._btui
libbtui.btui_puts(self._btui, b)
if self._autoflush:
libbtui.btui_flush(self._btui)
def delay(fn):
@functools.wraps(fn)
def wrapped(self, *a, **k):
assert self._btui
ret = fn(self, *a, **k)
time.sleep(self.delay)
libbtui.btui_show_cursor(self._btui)
return ret
return wrapped
class DebugBTUI(BTUI):
delay = 0.05
for fn_name in ('clear', 'draw_shadow', 'fill_box', 'move', 'set_cursor', 'hide_cursor', 'show_cursor', 'outline_box',
'scroll', 'set_attributes', 'set_bg', 'set_fg', 'unset_attributes', 'write_bytes'):
setattr(DebugBTUI, fn_name, delay(getattr(BTUI, fn_name)))
_btui = None
@contextmanager
def open(*, debug=False, delay=0.05, mode=BTUIMode.TUI):
global _btui
if not _btui:
if debug:
_btui = DebugBTUI()
_btui.delay = delay
else:
_btui = BTUI()
_btui.enable(mode=mode)
_btui.move(0, 0)
try: yield _btui
finally: _btui.disable()