From a41bce136a610f7ef5311ff893496bf2b2af6b71 Mon Sep 17 00:00:00 2001 From: Bruce Hill Date: Sun, 19 Apr 2020 15:46:41 -0700 Subject: [PATCH] Added python bindings. --- Makefile | 8 +- Python/Makefile | 24 +++++ Python/btui.c | 1 + Python/btui.py | 234 ++++++++++++++++++++++++++++++++++++++++++++++++ Python/test.py | 39 ++++++++ 5 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 Python/Makefile create mode 100644 Python/btui.c create mode 100644 Python/btui.py create mode 100644 Python/test.py diff --git a/Makefile b/Makefile index 79401eb..f15eac7 100644 --- a/Makefile +++ b/Makefile @@ -35,4 +35,10 @@ lua: testlua: @cd Lua; make test -.PHONY: all, checksyntax, clean, c, testc, lua, testlua +python: + @cd Python; make + +testpython: + @cd Python; make test + +.PHONY: all, checksyntax, clean, c, testc, lua, testlua, python, testpython diff --git a/Python/Makefile b/Python/Makefile new file mode 100644 index 0000000..e45737c --- /dev/null +++ b/Python/Makefile @@ -0,0 +1,24 @@ +PREFIX= +CC ?= gcc +O ?= -O2 +CFLAGS=-std=c99 -D_XOPEN_SOURCE=500 -D_GNU_SOURCE -D_POSIX_C_SOURCE=200809L +CWARN=-Wall -Wpedantic -Wextra -Wno-unknown-pragmas -Wno-missing-field-initializers\ + -Wno-padded -Wsign-conversion -Wno-missing-noreturn -Wno-cast-qual -Wtype-limits +#CFLAGS += -fsanitize=address -fno-omit-frame-pointer +G= + +all: libbtui.so + +clean: + rm -f btui.o libbtui.so + +libbtui.so: btui.o + $(CC) $(CFLAGS) $(CWARN) $(G) $(O) -shared $< -o $@ + +btui.o: btui.c ../btui.h + $(CC) $(CFLAGS) $(CWARN) $(G) $(O) -fPIC -c $< -o $@ + +test: libbtui.so + python3 test.py + +.PHONY: all, clean, test diff --git a/Python/btui.c b/Python/btui.c new file mode 100644 index 0000000..9cb561f --- /dev/null +++ b/Python/btui.c @@ -0,0 +1 @@ +#include "../btui.h" diff --git a/Python/btui.py b/Python/btui.py new file mode 100644 index 0000000..bec1a4a --- /dev/null +++ b/Python/btui.py @@ -0,0 +1,234 @@ +import ctypes +from contextlib import contextmanager + +__all__ = ['btui'] + +# Load the shared library into c types. +libbtui = ctypes.CDLL("./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_enable.restype = ctypes.POINTER(BTUI_struct) + +attr_t = ctypes.c_longlong +BTUI_ATTRS = { + "normal": attr_t.in_dll(libbtui, 'BTUI_NORMAL'), + "bold": attr_t.in_dll(libbtui, 'BTUI_BOLD'), + "faint": attr_t.in_dll(libbtui, 'BTUI_FAINT'), + "dim": attr_t.in_dll(libbtui, 'BTUI_FAINT'), + "italic": attr_t.in_dll(libbtui, 'BTUI_ITALIC'), + "underline": attr_t.in_dll(libbtui, 'BTUI_UNDERLINE'), + "blink_slow": attr_t.in_dll(libbtui, 'BTUI_BLINK_SLOW'), + "blink_fast": attr_t.in_dll(libbtui, 'BTUI_BLINK_FAST'), + "reverse": attr_t.in_dll(libbtui, 'BTUI_REVERSE'), + "conceal": attr_t.in_dll(libbtui, 'BTUI_CONCEAL'), + "strikethrough": attr_t.in_dll(libbtui, 'BTUI_STRIKETHROUGH'), + "fraktur": attr_t.in_dll(libbtui, 'BTUI_FRAKTUR'), + "double_underline": attr_t.in_dll(libbtui, 'BTUI_DOUBLE_UNDERLINE'), + "no_bold_or_faint": attr_t.in_dll(libbtui, 'BTUI_NO_BOLD_OR_FAINT'), + "no_italic_or_fraktur": attr_t.in_dll(libbtui, 'BTUI_NO_ITALIC_OR_FRAKTUR'), + "no_underline": attr_t.in_dll(libbtui, 'BTUI_NO_UNDERLINE'), + "no_blink": attr_t.in_dll(libbtui, 'BTUI_NO_BLINK'), + "no_reverse": attr_t.in_dll(libbtui, 'BTUI_NO_REVERSE'), + "no_conceal": attr_t.in_dll(libbtui, 'BTUI_NO_CONCEAL'), + "no_strikethrough": attr_t.in_dll(libbtui, 'BTUI_NO_STRIKETHROUGH'), + "fg_black": attr_t.in_dll(libbtui, 'BTUI_FG_BLACK'), + "fg_red": attr_t.in_dll(libbtui, 'BTUI_FG_RED'), + "fg_green": attr_t.in_dll(libbtui, 'BTUI_FG_GREEN'), + "fg_yellow": attr_t.in_dll(libbtui, 'BTUI_FG_YELLOW'), + "fg_blue": attr_t.in_dll(libbtui, 'BTUI_FG_BLUE'), + "fg_magenta": attr_t.in_dll(libbtui, 'BTUI_FG_MAGENTA'), + "fg_cyan": attr_t.in_dll(libbtui, 'BTUI_FG_CYAN'), + "fg_white": attr_t.in_dll(libbtui, 'BTUI_FG_WHITE'), + "fg_normal": attr_t.in_dll(libbtui, 'BTUI_FG_NORMAL'), + "bg_black": attr_t.in_dll(libbtui, 'BTUI_BG_BLACK'), + "bg_red": attr_t.in_dll(libbtui, 'BTUI_BG_RED'), + "bg_green": attr_t.in_dll(libbtui, 'BTUI_BG_GREEN'), + "bg_yellow": attr_t.in_dll(libbtui, 'BTUI_BG_YELLOW'), + "bg_blue": attr_t.in_dll(libbtui, 'BTUI_BG_BLUE'), + "bg_magenta": attr_t.in_dll(libbtui, 'BTUI_BG_MAGENTA'), + "bg_cyan": attr_t.in_dll(libbtui, 'BTUI_BG_CYAN'), + "bg_white": attr_t.in_dll(libbtui, 'BTUI_BG_WHITE'), + "bg_normal": attr_t.in_dll(libbtui, 'BTUI_BG_NORMAL'), + "framed": attr_t.in_dll(libbtui, 'BTUI_FRAMED'), + "encircled": attr_t.in_dll(libbtui, 'BTUI_ENCIRCLED'), + "overlined": attr_t.in_dll(libbtui, 'BTUI_OVERLINED'), +} + +BTUI_INVERSE_ATTRS = { + "normal": attr_t.in_dll(libbtui, 'BTUI_NORMAL'), + "bold": attr_t.in_dll(libbtui, 'BTUI_NO_BOLD_OR_FAINT'), + "faint": attr_t.in_dll(libbtui, 'BTUI_NO_BOLD_OR_FAINT'), + "dim": attr_t.in_dll(libbtui, 'BTUI_NO_BOLD_OR_FAINT'), + "italic": attr_t.in_dll(libbtui, 'BTUI_NO_ITALIC_OR_FRAKTUR'), + "underline": attr_t.in_dll(libbtui, 'BTUI_NO_UNDERLINE'), + "blink_slow": attr_t.in_dll(libbtui, 'BTUI_NO_BLINK'), + "blink_fast": attr_t.in_dll(libbtui, 'BTUI_NO_BLINK'), + "reverse": attr_t.in_dll(libbtui, 'BTUI_NO_REVERSE'), + "conceal": attr_t.in_dll(libbtui, 'BTUI_NO_CONCEAL'), + "strikethrough": attr_t.in_dll(libbtui, 'BTUI_NO_STRIKETHROUGH'), + "fraktur": attr_t.in_dll(libbtui, 'BTUI_NO_ITALIC_OR_FRAKTUR'), + "double_underline": attr_t.in_dll(libbtui, 'BTUI_NO_UNDERLINE'), + "fg_black": attr_t.in_dll(libbtui, 'BTUI_FG_NORMAL'), + "fg_red": attr_t.in_dll(libbtui, 'BTUI_FG_NORMAL'), + "fg_green": attr_t.in_dll(libbtui, 'BTUI_FG_NORMAL'), + "fg_yellow": attr_t.in_dll(libbtui, 'BTUI_FG_NORMAL'), + "fg_blue": attr_t.in_dll(libbtui, 'BTUI_FG_NORMAL'), + "fg_magenta": attr_t.in_dll(libbtui, 'BTUI_FG_NORMAL'), + "fg_cyan": attr_t.in_dll(libbtui, 'BTUI_FG_NORMAL'), + "fg_white": attr_t.in_dll(libbtui, 'BTUI_FG_NORMAL'), + "fg_normal": attr_t.in_dll(libbtui, 'BTUI_FG_NORMAL'), + "bg_black": attr_t.in_dll(libbtui, 'BTUI_BG_NORMAL'), + "bg_red": attr_t.in_dll(libbtui, 'BTUI_BG_NORMAL'), + "bg_green": attr_t.in_dll(libbtui, 'BTUI_BG_NORMAL'), + "bg_yellow": attr_t.in_dll(libbtui, 'BTUI_BG_NORMAL'), + "bg_blue": attr_t.in_dll(libbtui, 'BTUI_BG_NORMAL'), + "bg_magenta": attr_t.in_dll(libbtui, 'BTUI_BG_NORMAL'), + "bg_cyan": attr_t.in_dll(libbtui, 'BTUI_BG_NORMAL'), + "bg_white": attr_t.in_dll(libbtui, 'BTUI_BG_NORMAL'), + "bg_normal": attr_t.in_dll(libbtui, 'BTUI_BG_NORMAL'), +}; + + +class BTUI: + def __enter__(self): + self.enable() + + def __exit__(self, *exc): + self.disable() + + def enable(self): + self._btui = libbtui.btui_enable() + + def disable(self): + libbtui.btui_disable(self._btui) + + @contextmanager + def disabled(self): + self.disable() + try: yield self + finally: self.enable() + + 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 width(self): + assert self._btui + return self._btui.contents.width + + @property + def height(self): + assert self._btui + return self._btui.contents.height + + 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) + + def move(self, x, y): + assert self._btui + libbtui.btui_move_cursor(self._btui, int(x), int(y)) + + def flush(self): + assert self._btui + libbtui.btui_flush(self._btui) + + def suspend(self): + assert self._btui + libbtui.btui_suspend(self._btui) + + def clear(self, mode='screen'): + if mode == 'below': + self.write_bytes(b'\033[J') + elif mode == 'above': + self.write_bytes(b'\033[1J') + elif mode == 'screen': + self.write_bytes(b'\033[2J') + elif mode == 'right': + self.write_bytes(b'\033[K') + elif mode == 'left': + self.write_bytes(b'\033[1K') + elif mode == 'line': + self.write_bytes(b'\033[2K') + libbtui.btui_flush(self._btui) + + def set_attributes(self, *attrs): + assert self._btui + attr_long = ctypes.c_longlong(0) + for a in attrs: + attr_long.value |= BTUI_ATTRS[a].value + libbtui.btui_set_attributes(self._btui, attr_long) + + def unset_attributes(self, *attrs): + assert self._btui + attr_long = ctypes.c_longlong(0) + for a in attrs: + attr_long.value |= BTUI_INVERSE_ATTRS[a].value + libbtui.btui_set_attributes(self._btui, attr_long) + + @contextmanager + def attributes(self, *attrs): + self.set_attributes(*attrs) + try: yield + finally: self.unset_attributes(*attrs) + + 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 set_bg(self, r, g, b): + assert self._btui + libbtui.btui_set_bg(self._btui, int(r*255), int(g*255), int(b*255)) + + @contextmanager + def fg(self, r, g, b): + self.set_fg(r, g, b) + try: yield + finally: self.set_attributes("fg_normal") + + @contextmanager + def bg(self, r, g, b): + self.set_bg(r, g, b) + try: yield + finally: self.set_attributes("bg_normal") + + 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)) + + 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)) + + 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)) + + +btui = BTUI() diff --git a/Python/test.py b/Python/test.py new file mode 100644 index 0000000..d7ca512 --- /dev/null +++ b/Python/test.py @@ -0,0 +1,39 @@ +from btui import btui + +with btui: + key = None + x, y = 1, 1 + while key != 'q' and key != 'Ctrl-c': + if key == '?': + with btui.disabled(): + input("Press enter to continue.") + elif key == 'Ctrl-z': + btui.suspend() + + btui.clear() + + with btui.fg(.8, .95, .2): + btui.outline_box(x, y, 30, 1) + btui.move(x, y) + btui.write(f"Pressed: {key}") + + with btui.attributes("bg_blue", "fg_black"): + title = "Python BTUI Demo" + w = len(title) + center = (btui.width - w) // 2 + btui.fill_box(center - 2, 0, w + 4, 3) + btui.draw_shadow(center - 2, 0, w + 4, 3) + btui.move(center, 1) + btui.write(title) + + with btui.attributes("faint"): + s = f"Size: ({btui.width}x{btui.height})" + btui.move(btui.width-len(s), btui.height-1) + btui.write(s) + + key, mx, my = btui.getkey() + if mx: + x, y = mx, my + + if key == 'Ctrl-c': + raise KeyboardInterrupt