include ratuil in asciifarm project
This commit is contained in:
parent
5f78982da9
commit
2084d6466b
|
@ -0,0 +1,128 @@
|
|||
|
||||
|
||||
from enum import Enum
|
||||
|
||||
def clamp(x, lower, upper):
|
||||
return min(max(x, lower), upper)
|
||||
|
||||
class Relativity(Enum):
|
||||
ABSOLUTE = 0
|
||||
RELATIVE = 1
|
||||
VERY_RELATIVE = 2
|
||||
|
||||
class Value:
|
||||
|
||||
|
||||
def __init__(self, val=0, relative=Relativity.ABSOLUTE):
|
||||
self.val = val
|
||||
self.relative = relative
|
||||
|
||||
def to_actual_value(self, available_size, remaining_size=None):
|
||||
if remaining_size is None:
|
||||
remaining_size = available_size
|
||||
value = self.val
|
||||
if self.relative == Relativity.VERY_RELATIVE:
|
||||
value *= remaining_size
|
||||
elif self.relative == Relativity.RELATIVE:
|
||||
value *= available_size
|
||||
return int(value)
|
||||
|
||||
@classmethod
|
||||
def parse(self, text):
|
||||
if text is None:
|
||||
return None
|
||||
text = str(text) # in case someone would enter a number
|
||||
text = "".join(text.split()) # remove whitespace
|
||||
if not text:
|
||||
return None
|
||||
relative = Relativity.ABSOLUTE
|
||||
modifier = 1
|
||||
if text[-1] == "/":
|
||||
relative = Relativity.RELATIVE
|
||||
text = text[:-1]
|
||||
if text[-1] == "/":
|
||||
relative = Relativity.VERY_RELATIVE
|
||||
text = text[:-1]
|
||||
elif text[-1] == "%":
|
||||
relative = Relativity.RELATIVE
|
||||
text = text[:-1]
|
||||
modifier = 0.01
|
||||
if text[-1] == "%":
|
||||
relative = Relativity.VERY_RELATIVE
|
||||
text = text[:-1]
|
||||
if not text:
|
||||
return None
|
||||
if '.' in text:
|
||||
val = float(text)
|
||||
else:
|
||||
val = int(text)
|
||||
return Value(val * modifier, relative)
|
||||
|
||||
|
||||
class BoxStyle():
|
||||
|
||||
LEFT = "left"
|
||||
RIGHT = "right"
|
||||
TOP = "top"
|
||||
BOTTOM = "bottom"
|
||||
|
||||
def __init__(self, width=None, height=None, offset_x=None, offset_y=None, align_right=False, align_bottom=False, granularity=1, min_width=None, min_height=None, max_width=None, max_height=None):
|
||||
self.width = width or Value(1, Relativity.RELATIVE)
|
||||
self.height = height or Value(1, Relativity.RELATIVE)
|
||||
self.min_width = min_width or Value(0)
|
||||
self.min_height = min_height or Value(0)
|
||||
self.max_width = max_width or Value(1, Relativity.RELATIVE)
|
||||
self.max_height = max_height or Value(1, Relativity.RELATIVE)
|
||||
self.offset_x = offset_x or Value(0)
|
||||
self.offset_y = offset_y or Value(0)
|
||||
self.granularity = granularity
|
||||
self.align_right = align_right
|
||||
self.align_bottom = align_bottom
|
||||
|
||||
def get_width(self, available, remaining=None):
|
||||
return clamp(
|
||||
self.width.to_actual_value(available, remaining),
|
||||
self.min_width.to_actual_value(available, remaining),
|
||||
self.max_width.to_actual_value(available, remaining)
|
||||
)
|
||||
|
||||
def get_height(self, available, remaining=None):
|
||||
return clamp(
|
||||
self.height.to_actual_value(available, remaining),
|
||||
self.min_height.to_actual_value(available, remaining),
|
||||
self.max_height.to_actual_value(available, remaining)
|
||||
)
|
||||
|
||||
def get_offset_x(self, available, remaining=None):
|
||||
return self.offset_x.to_actual_value(available, remaining)
|
||||
|
||||
def get_offset_y(self, available, remaining=None):
|
||||
return self.offset_y.to_actual_value(available, remaining)
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_attrs(cls, attrs):
|
||||
width = Value.parse(attrs.get("width"))
|
||||
height = Value.parse(attrs.get("height"))
|
||||
min_width = Value.parse(attrs.get("min-width"))
|
||||
min_height = Value.parse(attrs.get("min-height"))
|
||||
max_width = Value.parse(attrs.get("max-width"))
|
||||
max_height = Value.parse(attrs.get("max-height"))
|
||||
offset_x = Value.parse(attrs.get("offset-x"))
|
||||
offset_y = Value.parse(attrs.get("offset-y"))
|
||||
granularity = int(attrs.get("granularity", "1"))
|
||||
align_right = ("right" in attrs.get("align", "").casefold() or "right" in attrs.get("hor-align", "").casefold())
|
||||
align_bottom = ("bottom" in attrs.get("align", "").casefold() or "bottom" in attrs.get("vert-align", "").casefold())
|
||||
return cls(
|
||||
width = width,
|
||||
height = height,
|
||||
offset_x = offset_x,
|
||||
offset_y = offset_y,
|
||||
align_right = align_right,
|
||||
align_bottom = align_bottom,
|
||||
granularity = granularity,
|
||||
min_width = min_width,
|
||||
min_height = min_height,
|
||||
max_width = max_width,
|
||||
max_height = max_height
|
||||
)
|
|
@ -0,0 +1,207 @@
|
|||
|
||||
|
||||
import sys
|
||||
import io
|
||||
|
||||
from .constants import INT_INFINITY
|
||||
from .screen import Screen
|
||||
from .pad import Pad
|
||||
from .drawtarget import DrawTarget
|
||||
from .strwidth import charwidth
|
||||
|
||||
|
||||
|
||||
class BufferedScreen(DrawTarget):
|
||||
|
||||
def __init__(self, out=sys.stdout, *args, **kwargs):
|
||||
self.screen = RememberingScreen(out, *args, **kwargs)
|
||||
self.buff = Pad(self.screen.width, self.screen.height)
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return self.screen.width
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
return self.screen.height
|
||||
|
||||
def clear(self):
|
||||
self.screen.clear()
|
||||
self.buff = Pad(self.screen.width, self.screen.height)
|
||||
|
||||
def reset(self):
|
||||
self.screen.reset()
|
||||
self.clear()
|
||||
|
||||
def update(self):
|
||||
self.screen.draw_pad(self.buff)
|
||||
self.screen.update()
|
||||
|
||||
def write(self, x, y, text, style=None):
|
||||
self.buff.write(x, y, text, style)
|
||||
|
||||
def draw_pad(self, pad, dest_x=0, dest_y=0, width=INT_INFINITY, height=INT_INFINITY, src_x=0, src_y=0):
|
||||
self.buff.draw_pad(pad, dest_x, dest_y, width, height, src_x, src_y)
|
||||
|
||||
|
||||
class RememberingScreen(DrawTarget):
|
||||
|
||||
def __init__(self, out=sys.stdout, *args, **kwargs):
|
||||
self.out = out
|
||||
self.screen = Screen(io.StringIO(), *args, **kwargs)
|
||||
self.on_screen = Pad(self.screen.width, self.screen.height)
|
||||
self.style = None
|
||||
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return self.screen.width
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
return self.screen.height
|
||||
|
||||
def clear(self):
|
||||
self.on_screen = Pad(self.screen.width, self.screen.height)
|
||||
self.screen.clear()
|
||||
|
||||
def reset(self):
|
||||
self.screen.update_size()
|
||||
self.clear()
|
||||
|
||||
def update(self):
|
||||
self.out.write(self.screen.out.getvalue())
|
||||
self.screen.out = io.StringIO()
|
||||
self.out.flush()
|
||||
|
||||
def write(self, x, y, text, style=None):
|
||||
text = crop(text, self.screen.width-x)
|
||||
self.on_screen.write(x, y, text, style)
|
||||
#self.screen.write(x, y, text, style)
|
||||
self.screen.move(x, y)
|
||||
self.screen.style(style, self.style)
|
||||
self.style = style
|
||||
self.screen.addstr(text)
|
||||
|
||||
def draw_pad_direct(self, *args, **kwargs):
|
||||
self.on_screen.draw_pad(*args, **kwargs)
|
||||
self.screen.draw_pad(*args, **kwargs)
|
||||
|
||||
|
||||
def draw_pad(self, pad, dest_x=0, dest_y=0, width=INT_INFINITY, height=INT_INFINITY, src_x=0, src_y=0):
|
||||
# Optimizes on the amount of characters to write to the terminal, which is more crucial in applications running over a network connection (like ssh)
|
||||
# This will only draw the changed characters
|
||||
width = min(width, self.screen.width - dest_x, pad.width - src_x)
|
||||
height = min(height, self.screen.height - dest_y, pad.height - src_y)
|
||||
|
||||
|
||||
BEGIN = "BEGIN" # before anything on the line has been done
|
||||
RUNNING = "RUNNING" # while changing current characters
|
||||
POSTRUN = "POSTRUN" # after changing some characters. Unsure whether to jump to next place or just continue
|
||||
POSTPOSTRUN = "POSTPOSTRUN" # same, but now the style has been changed
|
||||
BETWEEN = "BETWEEN" # run finished, but not worth to continue. Looking for the next changes
|
||||
for y in range(height):
|
||||
#runs = []
|
||||
#current_run = None
|
||||
running = False
|
||||
last_run = None
|
||||
post_run = ""
|
||||
postpost_run = ""
|
||||
post_style = None
|
||||
extra = 0
|
||||
skip = 0
|
||||
|
||||
state = BEGIN
|
||||
#self.style = None
|
||||
#cursor_x = None
|
||||
for x, (buff_cell) in enumerate(pad.get_line(src_x, src_y + y, width)):#zip(
|
||||
#self.on_screen.get_line(dest_x, dest_y + y, width),
|
||||
#pad.get_line(src_x, src_y + y, width))):
|
||||
scr_cell = self.on_screen.get(dest_x + x, dest_y + y)
|
||||
if scr_cell is None:
|
||||
scr_cell = (None, None)
|
||||
scr_style, scr_char = scr_cell
|
||||
if buff_cell is None:
|
||||
if state == BEGIN:
|
||||
continue
|
||||
if state == RUNNING:
|
||||
cursor_x = x
|
||||
skip += 1
|
||||
state = BETWEEN
|
||||
continue
|
||||
buff_style, buff_char = buff_cell
|
||||
while True:
|
||||
|
||||
if state == BEGIN or state == BETWEEN:
|
||||
if scr_cell == buff_cell:
|
||||
skip += 1
|
||||
break
|
||||
# start the first run
|
||||
if state == BEGIN:
|
||||
skip = 0
|
||||
self.screen.move(dest_x + x, dest_y + y)
|
||||
#else:
|
||||
#self.screen.skip(skip)#x-cursor_x)
|
||||
#skip = 0
|
||||
state = RUNNING
|
||||
|
||||
if state == RUNNING:
|
||||
if scr_cell != buff_cell:
|
||||
self.screen.skip(skip)
|
||||
skip = 0
|
||||
# continuing the same run
|
||||
self.screen.style(buff_style, self.style)
|
||||
self.style = buff_style
|
||||
self.screen.addstr(buff_char)
|
||||
self.on_screen.set_char(x, y, buff_char, buff_style)
|
||||
w = charwidth(buff_char)
|
||||
if w == 2:
|
||||
skip -= 1
|
||||
#self.on_screen.set_char(x + 2, y, None)
|
||||
self.on_screen.set_char(x + 1, y, None)
|
||||
#self.on_screen.set_char(x, y, None)
|
||||
break
|
||||
#cursor_x = x #+ char_width(buf_char) - 1
|
||||
state = BETWEEN#POSTRUN
|
||||
extra = 0
|
||||
post_run = ""
|
||||
postpost_run = ""
|
||||
|
||||
#if state == POSTRUN:
|
||||
#if buff_cell != scr_cell:
|
||||
#self.screen.skip(skip)
|
||||
#skip = 0
|
||||
#self.screen.addstr(post_run)
|
||||
#state = RUNNING
|
||||
#continue
|
||||
#elif extra >= 4:
|
||||
#skip += extra
|
||||
#state = BETWEEN
|
||||
#break
|
||||
#elif buff_style == self.style:
|
||||
#extra += 1
|
||||
#post_run += buff_char
|
||||
#break
|
||||
#else:
|
||||
#new_style = buff_style
|
||||
#state = POSTPOSTRUN
|
||||
|
||||
#if state == POSTPOSTRUN:
|
||||
#if buff_style != new_style:
|
||||
#state = BETWEEN
|
||||
#break
|
||||
#if buff_cell != scr_cell:
|
||||
#self.screen.addstr(post_run)
|
||||
#self.screen.style(new_style, self.style)
|
||||
#self.screen.addstr(postpost_run)
|
||||
#self.style = new_style
|
||||
#state = RUNNING
|
||||
#elif extra >= 4:
|
||||
#skip += extra
|
||||
#state = BETWEEN
|
||||
#break
|
||||
#else:
|
||||
#extra += 1
|
||||
#postpost_run += buff_char
|
||||
#break
|
||||
#self.on_screen.draw_pad(pad, dest_x, dest_y, width, height, src_x, src_y)
|
|
@ -0,0 +1,5 @@
|
|||
|
||||
|
||||
INT_INFINITY = 2**64
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
|
||||
from .constants import INT_INFINITY
|
||||
|
||||
class DrawTarget:
|
||||
|
||||
# is actually more of an interface / trait than a useful class
|
||||
width = None
|
||||
height = None
|
||||
|
||||
def clear(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def write(self, x, y, text, style=None):
|
||||
raise NotImplementedError()
|
||||
|
||||
def write_styled(self, x, y, styledtext, extrastyle=None):
|
||||
if isinstance(styledtext, str):
|
||||
self.write(x, y, styledtext, extrastyle)
|
||||
return
|
||||
for text, style in styledtext:
|
||||
self.write(x, y, text, style.add(extrastyle))
|
||||
x += len(text)
|
||||
|
||||
|
||||
def draw_pad(self, src, dest_x=0, dest_y=0, width=INT_INFINITY, height=INT_INFINITY, src_x=0, src_y=0):
|
||||
raise NotImplementedError()
|
|
@ -0,0 +1,73 @@
|
|||
|
||||
import sys
|
||||
|
||||
BACKSPACE = "backspace"
|
||||
ENTER = "enter"
|
||||
|
||||
|
||||
DELETE = "delete"
|
||||
ESCAPE = "escape"
|
||||
UP = "up"
|
||||
DOWN = "down"
|
||||
LEFT = "left"
|
||||
RIGHT = "right"
|
||||
PAGEUP = "pageup"
|
||||
PAGEDOWN = "pagedown"
|
||||
HOME = "home"
|
||||
END = "end"
|
||||
|
||||
BEFORE_LETTERS = ord('A') - 1
|
||||
|
||||
def name_char(char):
|
||||
n = ord(char)
|
||||
if n > 31 and n != 127:
|
||||
return char
|
||||
if n == 8 or n == 127:
|
||||
return BACKSPACE
|
||||
if n == 10 or n == 13:
|
||||
return ENTER
|
||||
if n > 0 and n <= 26:
|
||||
return "^" + chr(n + BEFORE_LETTERS)
|
||||
return "chr({})".format(n)
|
||||
|
||||
def get_key(stream=sys.stdin, combine_escape=True, do_interrupt=False):
|
||||
char = stream.read(1)
|
||||
if do_interrupt and ord(char) == 3:
|
||||
raise KeyboardInterrupt
|
||||
if ord(char) == 27:
|
||||
if not combine_escape:
|
||||
return ESCAPE
|
||||
nextchar = stream.read(1)
|
||||
while ord(nextchar) == 27: # avoid deep recursion
|
||||
nextchar = stream.read(1)
|
||||
if nextchar != "[":
|
||||
return "\\e" + name_char(nextchar)
|
||||
last = stream.read(1)
|
||||
rest = last
|
||||
while last in "1234567890;=?":
|
||||
last = stream.read(1)
|
||||
rest += last
|
||||
if rest == "A":
|
||||
return UP
|
||||
elif rest == "B":
|
||||
return DOWN
|
||||
elif rest == "C":
|
||||
return RIGHT
|
||||
elif rest == "D":
|
||||
return LEFT
|
||||
elif rest == "H":
|
||||
return HOME
|
||||
elif rest == "F":
|
||||
return END
|
||||
elif rest == "3~":
|
||||
return DELETE
|
||||
elif rest == "5~":
|
||||
return PAGEUP
|
||||
elif rest == "6~":
|
||||
return PAGEDOWN
|
||||
else:
|
||||
return "\\e[" + rest
|
||||
else:
|
||||
return name_char(char)
|
||||
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
|
||||
import os.path
|
||||
|
||||
from .screenelement import ScreenElement
|
||||
|
||||
from .widgets.textbox import TextBox
|
||||
from .widgets.hbox import HBox
|
||||
from .widgets.vbox import VBox
|
||||
from .widgets.listing import Listing
|
||||
from .widgets.border import Border
|
||||
from .widgets.log import Log
|
||||
from .widgets.textinput import TextInput
|
||||
from .widgets.field import Field
|
||||
from .widgets.bar import Bar
|
||||
from .widgets.switchbox import SwitchBox
|
||||
from .widgets.box import Box
|
||||
from .widgets.fill import Fill
|
||||
from .widgets.empty import Empty
|
||||
from .widgets.overlay import Overlay
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
widgets = {
|
||||
"textbox": TextBox,
|
||||
"hbox": HBox,
|
||||
"vbox": VBox,
|
||||
"listing": Listing,
|
||||
"border": Border,
|
||||
"log": Log,
|
||||
"textinput": TextInput,
|
||||
"field": Field,
|
||||
"bar": Bar,
|
||||
"switchbox": SwitchBox,
|
||||
"box": Box,
|
||||
"fill": Fill,
|
||||
"empty": Empty,
|
||||
"overlay": Overlay
|
||||
}
|
||||
|
||||
class Layout:
|
||||
|
||||
def __init__(self, tree, basepath=""):
|
||||
|
||||
self.tree = tree
|
||||
self.id_elements = {}
|
||||
self.changed = True
|
||||
self.target = None
|
||||
self.layout = self.build_layout(self.tree)
|
||||
self._target_size = None
|
||||
|
||||
def build_layout(self, etree):
|
||||
children = [self.build_layout(child) for child in etree]
|
||||
widget = widgets[etree.tag].from_xml(children, etree.attrib, etree.text)
|
||||
se = ScreenElement(widget, etree.attrib)
|
||||
if se.id is not None:
|
||||
self.id_elements[se.id] = se
|
||||
return se
|
||||
|
||||
def set_target(self, target):
|
||||
self.target = target
|
||||
self.resize()
|
||||
|
||||
def resize(self):
|
||||
self.layout.resize(self.target)
|
||||
self._target_size = (self.target.width, self.target.height)
|
||||
self.changed = True
|
||||
|
||||
def update(self, force=False):
|
||||
if self._target_size != (self.target.width, self.target.height):
|
||||
self.resize()
|
||||
if self.changed:
|
||||
force = True
|
||||
self.changed = False
|
||||
self.layout.update(force)
|
||||
|
||||
def get(self, id):
|
||||
return self.id_elements.get(id).widget
|
||||
|
||||
@classmethod
|
||||
def from_xml_str(cls, string, basepath=""):
|
||||
return cls(ET.fromstring(string), basepath)
|
||||
|
||||
@classmethod
|
||||
def from_xml_file(cls, fname, basepath=None):
|
||||
if basepath is None:
|
||||
basepath = os.path.dirname(fname)
|
||||
return cls(ET.parse(fname).getroot(), basepath)
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
|
||||
from .constants import INT_INFINITY
|
||||
from .drawtarget import DrawTarget
|
||||
|
||||
from .strwidth import charwidth
|
||||
|
||||
|
||||
class Pad(DrawTarget):
|
||||
|
||||
def __init__(self, width, height):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.clear()
|
||||
|
||||
def resize(self, width, height):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.clear()
|
||||
|
||||
def fill(self, value):
|
||||
self.data = [value for i in range(self.width*self.height)]
|
||||
|
||||
def clear(self):
|
||||
self.fill(None)
|
||||
|
||||
def write(self, x, y, text, style=None):
|
||||
if y >= self.height:
|
||||
return
|
||||
for char in text:
|
||||
w = charwidth(char)
|
||||
if x + w > self.width:
|
||||
break
|
||||
self.set_char(x, y, char, style)
|
||||
if w == 2:
|
||||
self.delete(x + 1, y)
|
||||
x += w
|
||||
|
||||
|
||||
def set_char(self, x, y, char, style=None):
|
||||
self.data[x + y * self.width] = (style, char)
|
||||
|
||||
def delete(self, x, y):
|
||||
self.data[x + y * self.width] = None
|
||||
|
||||
def get(self, x, y):
|
||||
if y >= self.height or x >= self.width:
|
||||
return None
|
||||
return self.data[x + y * self.width]
|
||||
|
||||
def get_line(self, x, y, length=None):
|
||||
if length is None:
|
||||
length = self.width - x
|
||||
if x >= self.width:
|
||||
return []
|
||||
start = x + y * self.width
|
||||
return self.data[start:start+length]
|
||||
|
||||
def draw_pad(self, src, dest_x=0, dest_y=0, width=INT_INFINITY, height=INT_INFINITY, src_x=0, src_y=0):
|
||||
dest = self
|
||||
width = min(width, dest.width - dest_x, src.width - src_x)
|
||||
height = min(height, dest.height - dest_y, src.height - src_y)
|
||||
for y in range(height):
|
||||
for x, cell in enumerate(src.get_line(src_x, src_y + y, width)):
|
||||
if cell is not None:
|
||||
style, char = cell
|
||||
self.write(dest_x + x, dest_y + y, char, style)
|
|
@ -0,0 +1,141 @@
|
|||
|
||||
import sys
|
||||
import shutil
|
||||
|
||||
from .constants import INT_INFINITY
|
||||
from .drawtarget import DrawTarget
|
||||
from .textstyle import TextStyle
|
||||
from .strwidth import charwidth
|
||||
|
||||
|
||||
class Attr:
|
||||
|
||||
RESET = "0"
|
||||
BOLD = "1"
|
||||
UNDERSCORE = "4"
|
||||
BLINK = "5"
|
||||
REVERSE = "7"
|
||||
CONCEALED = "8"
|
||||
|
||||
|
||||
FG_DEFAULT = "39"
|
||||
BG_DEFAULT = "49"
|
||||
|
||||
FG_COLORS = [str(i) for i in list(range(30, 38)) + list(range(90, 98))]
|
||||
BG_COLORS = [str(i) for i in list(range(40, 48)) + list(range(100, 108))]
|
||||
|
||||
ATTRS = {
|
||||
TextStyle.BOLD: BOLD,
|
||||
TextStyle.REVERSE: REVERSE,
|
||||
TextStyle.UNDERSCORE: UNDERSCORE,
|
||||
TextStyle.BLINK: BLINK
|
||||
}
|
||||
|
||||
class Screen(DrawTarget):
|
||||
|
||||
|
||||
def __init__(self, out=sys.stdout, always_reset=False, blink_bright_background=False):
|
||||
self.out = out
|
||||
self.width = 0
|
||||
self.height = 0
|
||||
self.blink_bright_background = blink_bright_background # use the blink attribute for bright backgrounds
|
||||
self.always_reset = always_reset or blink_bright_background # always reset if the style is different than the previous one
|
||||
self.update_size()
|
||||
|
||||
def update_size(self):
|
||||
size = shutil.get_terminal_size()
|
||||
self.width = size.columns
|
||||
self.height = size.lines
|
||||
|
||||
def move(self, x, y):
|
||||
self.out.write("\033[{};{}f".format(y+1, x+1))
|
||||
|
||||
def addstr(self, text):
|
||||
self.out.write(text)
|
||||
|
||||
def style(self, style, previous=None):
|
||||
if style is None:
|
||||
style = TextStyle.default
|
||||
if style == previous:
|
||||
return
|
||||
parts = []
|
||||
reset = False
|
||||
if style.fg is None or style.bg is None or previous is None or previous != style and self.always_reset:
|
||||
parts.append(Attr.RESET)
|
||||
reset = True
|
||||
else :
|
||||
for attr, enabled in style.attr.items():
|
||||
if not enabled and previous.attr[attr]:
|
||||
parts.append(Attr.RESET)
|
||||
reset = True
|
||||
if style.fg is not None and (reset or style.fg != previous.fg):
|
||||
parts.append(Attr.FG_COLORS[style.fg])
|
||||
if style.bg is not None and (reset or style.bg != previous.bg):
|
||||
parts.append(Attr.BG_COLORS[style.bg])
|
||||
if style.bg > 7 and self.blink_bright_background:
|
||||
parts.append(Attr.BLINK)
|
||||
for attr, enabled in style.attr.items():
|
||||
if enabled and (reset or not previous.attr[attr]):
|
||||
parts.append(Attr.ATTRS[attr])
|
||||
ansistyle = "\033[" + ";".join(parts) + "m"
|
||||
self.out.write(ansistyle)
|
||||
|
||||
def write(self, x, y, text, style=None):
|
||||
self.move(x, y)
|
||||
self.style(style)
|
||||
self.addstr(text)
|
||||
|
||||
def clear(self):
|
||||
self.out.write("\033[0m\033[2J")
|
||||
|
||||
def clear_line(self):
|
||||
self.out.write("\033[K")
|
||||
|
||||
def skip(self, amount=1):
|
||||
if amount == 0:
|
||||
return
|
||||
if amount == 1:
|
||||
stramount = ""
|
||||
else:
|
||||
stramount = str(abs(amount))
|
||||
self.out.write("\033[{}{}".format(stramount, ("C" if amount >= 0 else "D")))
|
||||
|
||||
def draw_pad(self, pad, scr_x=0, scr_y=0, width=INT_INFINITY, height=INT_INFINITY, pad_x=0, pad_y=0):
|
||||
screen = self
|
||||
width = min(width, screen.width - scr_x, pad.width - pad_x)
|
||||
height = min(height, screen.height - scr_y, pad.height - pad_y)
|
||||
last_style = None
|
||||
for y in range(height):
|
||||
screen.move(scr_x, scr_y+y)
|
||||
skip = 0
|
||||
line_y = pad_y + y
|
||||
for cell in pad.get_line(pad_x, line_y, width):
|
||||
if cell is None:
|
||||
skip += 1
|
||||
continue
|
||||
if skip != 0:
|
||||
screen.skip(skip)
|
||||
skip = 0
|
||||
style, char = cell
|
||||
screen.style(style, last_style)
|
||||
last_style = style
|
||||
screen.addstr(char)
|
||||
skip += 1 - charwidth(char)
|
||||
|
||||
def hide_cursor(self):
|
||||
self.out.write("\033[?25l")
|
||||
|
||||
def show_cursor(self):
|
||||
self.out.write("\033[?25h")
|
||||
|
||||
def finalize(self):
|
||||
self.style(None)
|
||||
self.move(0, self.height - 1)
|
||||
self.show_cursor()
|
||||
self.out.flush()
|
||||
|
||||
def update(self):
|
||||
self.out.flush()
|
||||
|
||||
|
||||
Screen.default = Screen()
|
|
@ -0,0 +1,28 @@
|
|||
|
||||
from .boxstyle import BoxStyle
|
||||
|
||||
class ScreenElement:
|
||||
|
||||
def __init__(self, widget, attr):
|
||||
self.widget = widget
|
||||
self.style = BoxStyle.from_attrs(attr)
|
||||
self.id = attr.get("id")
|
||||
self.key = attr.get("key")
|
||||
self.hidden = bool(attr.get("hidden", False))
|
||||
|
||||
def hide(self):
|
||||
self.hidden = True
|
||||
|
||||
def show(self):
|
||||
self.hidden = False
|
||||
|
||||
def resize(self, target):
|
||||
if target is not None and (target.width <= 0 or target.height <= 0):
|
||||
target = None
|
||||
self.target = target
|
||||
self.widget.resize(target)
|
||||
|
||||
def update(self, force):
|
||||
if self.target and not self.hidden:
|
||||
return self.widget.update(self.target, force)
|
||||
return False
|
|
@ -0,0 +1,68 @@
|
|||
|
||||
|
||||
import unicodedata
|
||||
|
||||
# taken from textwrap
|
||||
_whitespace = '\t\n\x0b\x0c\r '
|
||||
|
||||
|
||||
|
||||
def charwidth(char):
|
||||
""" The width of a single character. Ambiguous width is considered 1"""
|
||||
cat = unicodedata.category(char)
|
||||
if cat == "Mn":
|
||||
return 0
|
||||
eaw = unicodedata.east_asian_width(char)
|
||||
if eaw == "Na" or eaw == "H":
|
||||
return 1
|
||||
if eaw == "F" or eaw == "W":
|
||||
return 2
|
||||
if eaw == "A":
|
||||
return 1
|
||||
if eaw == "N":
|
||||
return 1
|
||||
raise Exception("unknown east easian width for character {}: {}".format(ord(char), char))
|
||||
|
||||
def strwidth(text):
|
||||
""" The total width of a string """
|
||||
return sum(charwidth(ch) for ch in text)
|
||||
|
||||
|
||||
def width(text):
|
||||
return stringwidth(text)
|
||||
|
||||
def width_index(text, width):
|
||||
""" The largest index i for which the strwidth(text[:i]) <= width """
|
||||
l = 0
|
||||
for i, char in enumerate(text):
|
||||
w = charwidth(char)
|
||||
if l + w > width:
|
||||
return i
|
||||
l += w
|
||||
return len(text)
|
||||
|
||||
def crop(text, width):
|
||||
return text[:width_index(text, width)]
|
||||
|
||||
def wrap(text, width, separators=None):
|
||||
lines = []
|
||||
for line in text.splitlines():
|
||||
while True:
|
||||
cutoff = width_index(line, width)
|
||||
if cutoff >= len(line):
|
||||
lines.append(line)
|
||||
break
|
||||
if separators is not None:
|
||||
last_sep = max(line.rfind(c, 0, cutoff+1) for c in separators)
|
||||
if last_sep > 0:
|
||||
cutoff = last_sep
|
||||
lines.append(line[:cutoff])
|
||||
if separators is not None:
|
||||
while line[cutoff] in separators:
|
||||
cutoff += 1
|
||||
line = line[cutoff:]
|
||||
return lines
|
||||
|
||||
def wrap_words(text, width):
|
||||
return wrap(text, width, separators=_whitespace)
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
class Attr:
|
||||
|
||||
RESET = "0"
|
||||
BOLD = "1"
|
||||
UNDERSCORE = "4"
|
||||
BLINK = "5"
|
||||
REVERSE = "7"
|
||||
CONCEALED = "8"
|
||||
|
||||
#FG_BLACK = "30"
|
||||
#FG_RED = "31"
|
||||
#FG_GREEN = "32"
|
||||
#FG_YELLOW = "33"
|
||||
#FG_BLUE = "34"
|
||||
#FG_MAGENTA = "35"
|
||||
#FG_CYAN = "36"
|
||||
#FG_WHITE = "37"
|
||||
|
||||
#BG_BLACK = "40"
|
||||
#BG_RED = "41"
|
||||
#BG_GREEN = "42"
|
||||
#BG_YELLOW = "43"
|
||||
#BG_BLUE = "44"
|
||||
#BG_MAGENTA = "45"
|
||||
#BG_CYAN = "46"
|
||||
#BG_WHITE = "47"
|
||||
|
||||
#FG_BRIGHT_BLACK = "90"
|
||||
#FG_BRIGHT_RED = "91"
|
||||
#FG_BRIGHT_GREEN = "92"
|
||||
#FG_BRIGHT_YELLOW = "93"
|
||||
#FG_BRIGHT_BLUE = "94"
|
||||
#FG_BRIGHT_MAGENTA = "95"
|
||||
#FG_BRIGHT_CYAN = "96"
|
||||
#FG_BRIGHT_WHITE = "97"
|
||||
|
||||
#BG_BRIGHT_BLACK = "100"
|
||||
#BG_BRIGHT_RED = "101"
|
||||
#BG_BRIGHT_GREEN = "102"
|
||||
#BG_BRIGHT_YELLOW = "103"
|
||||
#BG_BRIGHT_BLUE = "104"
|
||||
#BG_BRIGHT_MAGENTA = "105"
|
||||
#BG_BRIGHT_CYAN = "106"
|
||||
#BG_BRIGHT_WHITE = "107"
|
||||
|
||||
FG_DEFAULT = "39"
|
||||
BG_DEFAULT = "49"
|
||||
|
||||
FG_COLORS = [str(i) for i in list(range(30, 38)) + list(range(90, 98))]
|
||||
BG_COLORS = [str(i) for i in list(range(40, 48)) + list(range(100, 108))]
|
||||
|
||||
class TextStyle:
|
||||
|
||||
BLACK = 0
|
||||
RED = 1
|
||||
GREEN = 2
|
||||
YELLOW = 3
|
||||
BLUE = 4
|
||||
MAGENTA = 5
|
||||
CYAN = 6
|
||||
WHITE = 7
|
||||
|
||||
BRIGHT_BLACK = 8
|
||||
BRIGHT_RED = 9
|
||||
BRIGHT_GREEN = 10
|
||||
BRIGHT_YELLOW = 11
|
||||
BRIGHT_BLUE = 12
|
||||
BRIGHT_MAGENTA = 13
|
||||
BRIGHT_CYAN = 14
|
||||
BRIGHT_WHITE = 15
|
||||
|
||||
COLORS = list(range(16))
|
||||
|
||||
BOLD = "bold"
|
||||
REVERSE = "reverse"
|
||||
UNDERSCORE = "underscore"
|
||||
BLINK = "blink"
|
||||
|
||||
ATTRIBUTES = [BOLD, REVERSE, UNDERSCORE]
|
||||
|
||||
def __init__(self, fg=None, bg=None, bold=False, reverse=False, underscore=False):
|
||||
self.fg = fg
|
||||
self.bg = bg
|
||||
self.attr = {
|
||||
self.BOLD: bold,
|
||||
self.REVERSE: reverse,
|
||||
self.UNDERSCORE: underscore
|
||||
}
|
||||
self.attr_set = frozenset(key for key, value in self.attr.items() if value)
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, TextStyle) and other.fg == self.fg and other.bg == self.bg and self.attr_set == other.attr_set
|
||||
|
||||
def __repr__(self):
|
||||
if self == self.default:
|
||||
return "TextStyle()"
|
||||
return "TextStyle({}, {}, {})".format(self.fg, self.bg, ", ".join(self.attr.values()))
|
||||
|
||||
def add(self, other):
|
||||
if other is None:
|
||||
other = TextStyle()
|
||||
fg = self.fg
|
||||
if other.fg is not None:
|
||||
fg = other.fg
|
||||
bg = self.bg
|
||||
if other.bg is not None:
|
||||
bg = other.bg
|
||||
attrs = dict(self.attr)
|
||||
for key, val in other.attr.items():
|
||||
if val:
|
||||
attrs[key] = val
|
||||
return TextStyle(fg, bg, **attrs)
|
||||
|
||||
@property
|
||||
def bold(self):
|
||||
return self.attr[self.BOLD]
|
||||
|
||||
@property
|
||||
def underscore(self):
|
||||
return self.attr[self.UNDERSCORE]
|
||||
|
||||
@property
|
||||
def reverse(self):
|
||||
return self.attr[self.REVERSE]
|
||||
|
||||
@classmethod
|
||||
def from_str(cls, text):
|
||||
if text is None:
|
||||
return TextStyle.default
|
||||
fg = None
|
||||
bg = None
|
||||
attrs = {}
|
||||
parts = text.split(";")
|
||||
for part in parts:
|
||||
attr, _sep, value = part.partition(":")
|
||||
attr = attr.strip().casefold()
|
||||
value = value.strip()
|
||||
if attr == "fg" and int(value) in TextStyle.COLORS:
|
||||
fg = int(value)
|
||||
if attr == "bg" and int(value) in TextStyle.COLORS:
|
||||
bg = int(value)
|
||||
if attr in TextStyle.ATTRIBUTES:
|
||||
attrs[attr] = True
|
||||
return cls(fg, bg, **attrs)
|
||||
|
||||
|
||||
TextStyle.default = TextStyle()
|
|
@ -0,0 +1,34 @@
|
|||
|
||||
|
||||
|
||||
|
||||
class Widget:
|
||||
|
||||
_changed = True
|
||||
|
||||
def change(self):
|
||||
self._changed = True
|
||||
|
||||
def is_changed(self):
|
||||
return self._changed
|
||||
|
||||
def unchange(self):
|
||||
self._changed = False
|
||||
|
||||
def resize(self, screen):
|
||||
self.change()
|
||||
|
||||
def update(self, target, force=False):
|
||||
""" draw the widget onto target.
|
||||
if force is false and the widget did not change since the previous update, don't do anything
|
||||
return whether anything was drawn
|
||||
"""
|
||||
if self.is_changed() or force:
|
||||
self.draw(target)
|
||||
self.unchange()
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def from_xml(cls, children, attr, text):
|
||||
raise NotImplementedError
|
|
@ -0,0 +1,40 @@
|
|||
|
||||
from . import Widget
|
||||
from .. textstyle import TextStyle
|
||||
from ..strwidth import strwidth
|
||||
|
||||
class Bar(Widget):
|
||||
""" A bar (healthbar/mana bar/nutrition bar etc) that can be filled, empty, or something in between)"""
|
||||
|
||||
def __init__(self, attr):
|
||||
self.full_char = attr.get("full-char", "#")
|
||||
self.empty_char = attr.get("empty-char", " ")
|
||||
assert strwidth(self.full_char) == strwidth(self.empty_char) == 1
|
||||
self.full_style = TextStyle.from_str(attr.get("full-style", ""))
|
||||
self.empty_style = TextStyle.from_str(attr.get("empty-style", ""))
|
||||
self.total = int(attr.get("total", "-1"))
|
||||
self.filled = int(attr.get("filled", "0"))
|
||||
|
||||
|
||||
def set_total(self, total):
|
||||
self.total = total
|
||||
self.change()
|
||||
|
||||
def set_filled(self, filled):
|
||||
self.filled = filled
|
||||
self.change()
|
||||
|
||||
def draw(self, target):
|
||||
target.clear()
|
||||
width = target.width
|
||||
height = target.height
|
||||
|
||||
bar_end = round(self.filled / self.total * width) if self.total > 0 else 0
|
||||
for y in range(target.height):
|
||||
target.write(0, y, self.full_char * bar_end , self.full_style)
|
||||
target.write(bar_end, y, self.empty_char[0]*(width - bar_end), self.empty_style)
|
||||
|
||||
@classmethod
|
||||
def from_xml(cls, children, attr, text):
|
||||
return cls(attr)
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
|
||||
from . import Widget
|
||||
from ..window import Window
|
||||
from ..textstyle import TextStyle
|
||||
from ..strwidth import strwidth
|
||||
|
||||
class Border(Widget):
|
||||
|
||||
|
||||
def __init__(self, child, attr):
|
||||
self.child = child
|
||||
self.vertchar = "|"
|
||||
self.horchar = "-"
|
||||
self.cornerchar = "+"
|
||||
self.style = TextStyle.from_str(attr.get("style"))
|
||||
char = attr.get("char")
|
||||
if char is not None:
|
||||
self.vertchar = char
|
||||
self.horchar = char
|
||||
self.cornerchar = char
|
||||
self.vertchar = attr.get("vertchar", self.vertchar)
|
||||
self.horchar = attr.get("horchar", self.horchar)
|
||||
self.cornerchar = attr.get("cornerchar", self.cornerchar)
|
||||
assert strwidth(self.horchar) == 1
|
||||
assert strwidth(self.vertchar) == 1
|
||||
assert strwidth(self.cornerchar) == 1
|
||||
|
||||
def resize(self, target):
|
||||
if target is None:
|
||||
self.child.resize(None)
|
||||
else:
|
||||
win = Window(target, 1, 1, target.width - 2, target.height - 2)
|
||||
self.child.resize(win)
|
||||
self.change()
|
||||
|
||||
def update(self, target, force=False):
|
||||
if self.is_changed() or force:
|
||||
self.draw(target)
|
||||
force = True
|
||||
self.unchange()
|
||||
return self.child.update(force) or force
|
||||
|
||||
def draw(self, target):
|
||||
target.write(0, 0, self.cornerchar + self.horchar * (target.width - 2) + self.cornerchar, self.style)
|
||||
target.write(0, target.height - 1, self.cornerchar + self.horchar * (target.width - 2) + self.cornerchar, self.style)
|
||||
for y in range(1, target.height - 1):
|
||||
target.write(0, y, self.vertchar, self.style)
|
||||
target.write(target.width-1, y, self.vertchar, self.style)
|
||||
|
||||
@classmethod
|
||||
def from_xml(cls, children, attr, text):
|
||||
assert len(children) == 1
|
||||
return cls(children[0], attr)
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
|
||||
from . import Widget
|
||||
|
||||
class Box(Widget):
|
||||
|
||||
# doesn't do anything
|
||||
# This is useful when you want to separate attributes
|
||||
|
||||
def __init__(self, child):
|
||||
self.child = child
|
||||
|
||||
def resize(self, target):
|
||||
self.child.resize(target)
|
||||
|
||||
def update(self, target, force=False):
|
||||
return self.child.update(force)
|
||||
|
||||
@classmethod
|
||||
def from_xml(cls, children, attr, text):
|
||||
assert len(children) == 1
|
||||
return cls(children[0])
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
from . import Widget
|
||||
|
||||
class Empty(Widget):
|
||||
|
||||
# just some empty transparent space
|
||||
|
||||
def resize(self, target):
|
||||
pass
|
||||
|
||||
def update(self, target, force=False):
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def from_xml(cls, children, attr, text):
|
||||
return Empty()
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
|
||||
from . import Widget
|
||||
from ..pad import Pad
|
||||
|
||||
class Field(Widget):
|
||||
|
||||
|
||||
def __init__(self, width=0, height=0, char_size=1):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.char_size = char_size
|
||||
self.pad = Pad(self.width * self.char_size, self.height)
|
||||
self.center = (0, 0)
|
||||
self.redraw = False
|
||||
|
||||
def set_char_size(self, char_size):
|
||||
self.char_size = char_size
|
||||
self.pad = Pad(self.width * self.char_size, self.height)
|
||||
|
||||
def set_size(self, width, height):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.pad.resize(width * self.char_size, height)
|
||||
self.redraw = True
|
||||
self.change()
|
||||
|
||||
def change_cell(self, x, y, char, style=None):
|
||||
if x < 0 or y < 0 or x >= self.width or y >= self.height:
|
||||
return
|
||||
self.pad.write(x * self.char_size, y, char, style)
|
||||
self.change()
|
||||
|
||||
def set_center(self, x, y):
|
||||
self.center = (x, y)
|
||||
self.change()
|
||||
|
||||
|
||||
def _round_width(self, x):
|
||||
return x // self.char_size * self.char_size
|
||||
|
||||
def draw(self, target):
|
||||
center_x, center_y = self.center
|
||||
target.draw_pad(
|
||||
self.pad,
|
||||
src_x = max(0, min(
|
||||
self._round_width(self.pad.width - target.width),
|
||||
self._round_width(center_x * self.char_size - target.width // 2)
|
||||
)),
|
||||
src_y = max(0, min(self.pad.height - target.height, center_y - target.height // 2)),
|
||||
width = self._round_width(target.width),
|
||||
dest_x = max(0, (target.width - self.pad.width) // 2),
|
||||
dest_y = max(0, (target.height - self.pad.height) // 2)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_xml(cls, children, attr, text):
|
||||
return cls(char_size=int(attr.get("char-size", 1)))
|
|
@ -0,0 +1,29 @@
|
|||
|
||||
from . import Widget
|
||||
from ..textstyle import TextStyle
|
||||
from ..strwidth import crop, strwidth
|
||||
import math
|
||||
|
||||
class Fill(Widget):
|
||||
|
||||
def __init__(self, char, style=None):
|
||||
self.set_filling(char, style)
|
||||
|
||||
def set_filling(self, char, style=None):
|
||||
assert strwidth(char) > 0
|
||||
self.char = char
|
||||
self.style = style
|
||||
self.change()
|
||||
|
||||
def draw(self, target):
|
||||
target.clear()
|
||||
line = crop(self.char * math.ceil(target.width / strwidth(self.char)), target.width)
|
||||
for y in range(target.height):
|
||||
target.write(0, y, line, self.style)
|
||||
|
||||
@classmethod
|
||||
def from_xml(cls, children, attr, text):
|
||||
return cls(text.strip() if text is not None else attr.get("char", "#"), TextStyle.from_str(attr.get("style")))
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
|
||||
from .splitbox import SplitBox
|
||||
from ..window import Window
|
||||
|
||||
class HBox(SplitBox):
|
||||
|
||||
def resize(self, target):
|
||||
if target is None:
|
||||
for child in self.children:
|
||||
child.resize(None)
|
||||
return
|
||||
start = 0
|
||||
end = target.width
|
||||
for child in self.children:
|
||||
if start >= end:
|
||||
child.resize(None)
|
||||
continue
|
||||
width = end - start
|
||||
width = min(width, child.style.get_width(target.width, width))
|
||||
if child.style.align_right:
|
||||
win = Window(target, end - width, 0, width, target.height)
|
||||
end -= width
|
||||
else:
|
||||
win = Window(target, start, 0, width, target.height)
|
||||
start += width
|
||||
child.resize(win)
|
|
@ -0,0 +1,51 @@
|
|||
|
||||
from . import Widget
|
||||
from ..strwidth import strwidth, crop
|
||||
|
||||
class Listing(Widget):
|
||||
|
||||
def __init__(self, selected=0, selector_char="*", items=None):
|
||||
if items is not None:
|
||||
self.items = list(items)#[line.strip() for line in etree.text.splitlines() if line.strip()]
|
||||
else:
|
||||
self.items = []
|
||||
self.selector = selected
|
||||
self.selector_char = selector_char
|
||||
|
||||
def set_items(self, items):
|
||||
self.items = items
|
||||
self.change()
|
||||
|
||||
def select(self, index):
|
||||
self.selector = index
|
||||
self.change()
|
||||
|
||||
def draw(self, target):
|
||||
target.clear()
|
||||
width = target.width
|
||||
height = target.height
|
||||
|
||||
start = min(self.selector - height//2, len(self.items) - height)
|
||||
start = max(start, 0)
|
||||
end = start + height
|
||||
for i, item in enumerate(self.items[start:end]):
|
||||
if i + start == self.selector:
|
||||
target.write(0, i, self.selector_char)
|
||||
target.write(strwidth(self.selector_char), i, item)
|
||||
if end < len(self.items):
|
||||
target.write(width-1, height-1, "+")
|
||||
if start > 0:
|
||||
target.write(width-1, 0, "-")
|
||||
|
||||
@classmethod
|
||||
def from_xml(cls, children, attr, text):
|
||||
kwargs = {}
|
||||
if text is not None:
|
||||
kwargs["items"] = [line.strip() for line in text.splitlines() if line.strip()]
|
||||
if "select" in attr:
|
||||
kwargs["selected"] = int(attr["select"])
|
||||
if "selector" in attr:
|
||||
kwargs["selector_char"] = attr["selector"]
|
||||
return cls(**kwargs)
|
||||
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
|
||||
from . import Widget
|
||||
from ..strwidth import wrap
|
||||
|
||||
class Log(Widget):
|
||||
|
||||
def __init__(self, messages=None):
|
||||
self.messages = list(messages or [])
|
||||
self.scrolled_back = 0
|
||||
|
||||
def add_message(self, message, style=None):
|
||||
self.messages.append((message, style))
|
||||
if self.scrolled_back:
|
||||
self.scrolled_back += 1
|
||||
self.change()
|
||||
|
||||
def scroll(self, amount, relative=True):
|
||||
if relative:
|
||||
self.scrolled_back += amount
|
||||
else:
|
||||
self.scrolled_back = amount
|
||||
self.scrolled_back = max(self.scrolled_back, 0)
|
||||
self.change()
|
||||
|
||||
|
||||
|
||||
def draw(self, target):
|
||||
width = target.width
|
||||
height = target.height
|
||||
lines = []
|
||||
messages = self.messages
|
||||
for message, style in messages:
|
||||
for line in wrap(message, width):
|
||||
lines.append((line, style))
|
||||
self.scrolled_back = max(min(self.scrolled_back, len(lines)-height), 0)
|
||||
moreDown = False
|
||||
if self.scrolled_back > 0:
|
||||
lines = lines[:-self.scrolled_back]
|
||||
moreDown = True
|
||||
moreUp = False
|
||||
if len(lines) > height:
|
||||
moreUp = True
|
||||
lines = lines[len(lines)-height:]
|
||||
elif len(lines) < height:
|
||||
lines = (height-len(lines)) * [("",)] + lines
|
||||
target.clear()
|
||||
for i, line in enumerate(lines):
|
||||
target.write(0, i, *line)
|
||||
if moreUp:
|
||||
target.write(width-1, 0, '-')
|
||||
if moreDown:
|
||||
target.write(width-1, height-1, '+')
|
||||
|
||||
@classmethod
|
||||
def from_xml(cls, children, attr, text):
|
||||
if text is not None:
|
||||
messages = [(line.strip(), None) for line in text.splitlines() if line.strip()]
|
||||
return cls(messages)
|
||||
else:
|
||||
return cls()
|
|
@ -0,0 +1,57 @@
|
|||
|
||||
from . import Widget
|
||||
from ..window import Window
|
||||
|
||||
class Overlay(Widget):
|
||||
|
||||
def __init__(self, children):
|
||||
self.children = children
|
||||
|
||||
def _get_child(self, index):
|
||||
if isinstance(index, int):
|
||||
return self.children(index)
|
||||
if isinstance(index, str):
|
||||
key = index.casefold()
|
||||
for i, child in enumerate(self.children):
|
||||
if child.key == key:
|
||||
return child
|
||||
|
||||
def hide(self, index):
|
||||
self._get_child(index).hide()
|
||||
self.change()
|
||||
|
||||
def show(self, index):
|
||||
self._get_child(index).show()
|
||||
self.change()
|
||||
|
||||
def resize(self, target):
|
||||
for child in self.children:
|
||||
x = child.style.get_offset_x(target.width)
|
||||
y = child.style.get_offset_y(target.height)
|
||||
width = min(child.style.get_width(target.width), target.width - x)
|
||||
height = min(child.style.get_height(target.height), target.height - y)
|
||||
if child.style.align_right:
|
||||
x = target.width - x - width
|
||||
if child.style.align_bottom:
|
||||
y = target.height - y - height
|
||||
win = Window(target, x, y, width, height)
|
||||
child.resize(win)
|
||||
|
||||
def update(self, target, force):
|
||||
if self.is_changed():
|
||||
force = True
|
||||
self.unchange()
|
||||
children = [child for child in self.children if not child.hidden]
|
||||
if not children:
|
||||
return False
|
||||
#child[0].update(force)
|
||||
for child in self.children:
|
||||
# if any child is changed, all next children get forced updates
|
||||
force = child.update(force) or force
|
||||
#self.children[self.selected].update(force)
|
||||
return force
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_xml(cls, children, attr, text):
|
||||
return cls(children)
|
|
@ -0,0 +1,20 @@
|
|||
|
||||
from . import Widget
|
||||
|
||||
class SplitBox(Widget):
|
||||
|
||||
def __init__(self, children):
|
||||
self.children = children
|
||||
|
||||
def resize(self, target):
|
||||
raise NotImplementedError
|
||||
|
||||
def update(self, target, force=False):
|
||||
changed = False
|
||||
for child in self.children:
|
||||
changed = child.update(force) or changed
|
||||
return changed
|
||||
|
||||
@classmethod
|
||||
def from_xml(cls, children, attr, text):
|
||||
return cls(children)
|
|
@ -0,0 +1,33 @@
|
|||
|
||||
from . import Widget
|
||||
|
||||
class SwitchBox(Widget):
|
||||
|
||||
def __init__(self, children, selected=0):
|
||||
self.children = children
|
||||
self.select(selected)
|
||||
|
||||
def select(self, selected):
|
||||
if isinstance(selected, str):
|
||||
key = selected.casefold()
|
||||
for i, child in enumerate(self.children):
|
||||
if child.key == key:
|
||||
selected = i
|
||||
break
|
||||
self.selected = selected
|
||||
self.change()
|
||||
|
||||
def resize(self, target):
|
||||
for child in self.children:
|
||||
child.resize(target)
|
||||
|
||||
def update(self, target, force):
|
||||
if self.is_changed():
|
||||
force = True
|
||||
self.unchange()
|
||||
return self.children[self.selected].update(force) or force
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_xml(cls, children, attr, text):
|
||||
return cls(children, attr.get("selected", int(attr.get("selected-val", 0))))
|
|
@ -0,0 +1,50 @@
|
|||
|
||||
from . import Widget
|
||||
from ..strwidth import crop, wrap, wrap_words
|
||||
from ..textstyle import TextStyle
|
||||
from collections import defaultdict
|
||||
|
||||
class TextBox(Widget):
|
||||
|
||||
def __init__(self, text="", wrap=None, use_format=False, style=None):
|
||||
self.lines = []
|
||||
if wrap is None or wrap == "":
|
||||
wrap = "crop"
|
||||
assert wrap in {"crop", "words", "chars"}
|
||||
self.wrap = wrap
|
||||
self.use_format = use_format
|
||||
self.set_text(text, None)
|
||||
|
||||
def set_text(self, text, style=None):
|
||||
self.text = text
|
||||
self.style = style
|
||||
if self.use_format:
|
||||
self.format(defaultdict(str))
|
||||
else:
|
||||
self.lines = text.splitlines()
|
||||
self.change()
|
||||
|
||||
def format(self, values):
|
||||
self.lines = self.text.format_map(values).splitlines()
|
||||
self.change()
|
||||
|
||||
def draw(self, target):
|
||||
target.clear()
|
||||
lines = []
|
||||
if self.wrap == "crop":
|
||||
lines = [crop(line, target.width) for line in self.lines][:target.height]
|
||||
elif self.wrap == "chars":
|
||||
for line in self.lines:
|
||||
lines.extend(wrap(line, target.width))
|
||||
elif self.wrap == "words":
|
||||
for line in self.lines:
|
||||
lines.extend(wrap_words(line, target.width))
|
||||
|
||||
for y, line in enumerate(lines[:target.height]):
|
||||
target.write(0, y, line, self.style)
|
||||
|
||||
@classmethod
|
||||
def from_xml(cls, children, attr, text):
|
||||
wrap = attr.get("wrap")
|
||||
use_format = bool(attr.get("format"))
|
||||
return cls((text or "").strip(), wrap, use_format, TextStyle.from_str(attr.get("style", "")))
|
|
@ -0,0 +1,42 @@
|
|||
|
||||
from . import Widget
|
||||
from ..textstyle import TextStyle
|
||||
from ..strwidth import strwidth, width_index
|
||||
|
||||
class TextInput(Widget):
|
||||
|
||||
def __init__(self):
|
||||
self.text = ""
|
||||
self.cursor = None
|
||||
|
||||
def set_text(self, text, cursor=None):
|
||||
self.text = text
|
||||
if cursor is not None:
|
||||
assert cursor >= 0
|
||||
self.cursor = cursor
|
||||
self.change()
|
||||
|
||||
def draw(self, target):
|
||||
target.clear()
|
||||
if self.cursor is None:
|
||||
target.write(0, 0, self.text)
|
||||
else:
|
||||
text = self.text
|
||||
cursor_pos = strwidth(self.text[:self.cursor])
|
||||
textwidth = strwidth(self.text)
|
||||
|
||||
offset = max(0, cursor_pos - target.width * 0.9)
|
||||
|
||||
chars_offset = width_index(text, offset)
|
||||
offset_text = self.text[chars_offset:]
|
||||
target.write(0, 0, offset_text)
|
||||
|
||||
if self.cursor < len(self.text):
|
||||
c = self.text[self.cursor]
|
||||
else:
|
||||
c = ' '
|
||||
target.write(cursor_pos - strwidth(self.text[:chars_offset]), 0, c, TextStyle(reverse=True))
|
||||
|
||||
@classmethod
|
||||
def from_xml(cls, children, attr, text):
|
||||
return cls()
|
|
@ -0,0 +1,29 @@
|
|||
|
||||
from .splitbox import SplitBox
|
||||
from ..window import Window
|
||||
|
||||
class VBox(SplitBox):
|
||||
|
||||
def resize(self, target):
|
||||
if target is None:
|
||||
for child in self.children:
|
||||
child.resize(None)
|
||||
return
|
||||
start = 0
|
||||
end = target.height
|
||||
for child in self.children:
|
||||
if start >= end:
|
||||
child.resize(None)
|
||||
continue
|
||||
height = end - start
|
||||
height = min(height, child.style.get_height(target.height, height))
|
||||
if height <= 0:
|
||||
child.resize(None)
|
||||
continue
|
||||
if child.style.align_bottom:
|
||||
win = Window(target, 0, end - height, target.width, height)
|
||||
end -= height
|
||||
else:
|
||||
win = Window(target, 0, start, target.width, height)
|
||||
start += height
|
||||
child.resize(win)
|
|
@ -0,0 +1,31 @@
|
|||
|
||||
from .constants import INT_INFINITY
|
||||
from .drawtarget import DrawTarget
|
||||
from .strwidth import crop
|
||||
|
||||
class Window(DrawTarget):
|
||||
|
||||
def __init__(self, target, x=0, y=0, width=None, height=None):
|
||||
self.target = target
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.width = width if width is not None else target.width - x
|
||||
self.height = height if height is not None else target.height - y
|
||||
|
||||
def write(self, x, y, text, style=None):
|
||||
if x < 0 or y < 0 or y >= self.height:
|
||||
raise IndexError("Trying to write outside window")
|
||||
text = crop(text, self.width - x)
|
||||
self.target.write(self.x + x, self.y + y, text, style)
|
||||
|
||||
def clear(self):
|
||||
for y in range(self.height):
|
||||
self.write(0, y, " " * self.width)
|
||||
|
||||
def draw_pad(self, pad, dest_x=0, dest_y=0, width=INT_INFINITY, height=INT_INFINITY, src_x=0, src_y=0):
|
||||
if dest_x < 0 or dest_y < 0:
|
||||
raise IndexError("Trying to draw pad outside window")
|
||||
width = min(width, self.width - dest_x)
|
||||
height = min(height, self.height - dest_y)
|
||||
self.target.draw_pad(pad, self.x + dest_x, self.y + dest_y, width, height, src_x, src_y)
|
||||
|
Loading…
Reference in New Issue