"""Functions for input and output with the curses module."""
import curses
import sys
from typing import NamedTuple, Any, Callable
from ..classes import *
from functools import cache
_curses_int_to_standard_key_name = {
1: "ctrl+a",
2: "ctrl+b",
3: "ctrl+c",
4: "ctrl+d",
5: "ctrl+e",
6: "ctrl+f",
7: "ctrl+g",
8: "ctrl+h",
9: "tab", # same as ctrl i
10: "enter", # same as ctrl j
11: "ctrl+k",
12: "ctrl+l",
13: "enter", # same as ctrl m
14: "ctrl+n",
15: "ctrl+o",
16: "ctrl+p",
17: "ctrl+q",
18: "ctrl+r",
19: "ctrl+s",
20: "ctrl+t",
21: "ctrl+u",
22: "ctrl+v",
23: "ctrl+w",
24: "ctrl+x",
25: "ctrl+y",
26: "ctrl+z",
27: "escape",
28: "ctrl+\\",
29: "ctrl+]",
30: "ctrl+^",
31: "ctrl+_",
curses.KEY_BACKSPACE: "backspace",
curses.KEY_ENTER: "enter",
curses.KEY_UP: "up",
curses.KEY_DOWN: "down",
curses.KEY_LEFT: "left",
curses.KEY_RIGHT: "right",
curses.KEY_HOME: "home",
curses.KEY_END: "end",
curses.KEY_NPAGE: "page down",
curses.KEY_PPAGE: "page up",
curses.KEY_DC: "delete",
curses.KEY_IC: "insert",
curses.KEY_F1: "f1",
curses.KEY_F2: "f2",
curses.KEY_F3: "f3",
curses.KEY_F4: "f4",
curses.KEY_F5: "f5",
curses.KEY_F6: "f6",
curses.KEY_F7: "f7",
curses.KEY_F8: "f8",
curses.KEY_F9: "f9",
curses.KEY_F10: "f10",
curses.KEY_F11: "f11",
curses.KEY_F12: "f12",
curses.KEY_F13: "f13",
curses.KEY_F14: "f14",
curses.KEY_F15: "f15",
curses.KEY_F16: "f16",
curses.KEY_F17: "f17",
curses.KEY_F18: "f18",
curses.KEY_F19: "f19",
curses.KEY_F20: "f20",
}
def _key_code_to_str(key: int | str):
if isinstance(key, str):
try:
ret = _curses_int_to_standard_key_name[ord(key)]
return ret
except KeyError:
return key
try:
return _curses_int_to_standard_key_name[key]
except:
return "unknown"
def _mouse_button_to_str(mouse_button: int) -> str:
out = []
if mouse_button & curses.BUTTON_CTRL:
out.append("ctrl")
if mouse_button & curses.BUTTON_ALT:
out.append("alt")
if mouse_button & curses.BUTTON_SHIFT:
out.append("shift")
if mouse_button & curses.BUTTON1_PRESSED:
out.append("left mouse")
elif mouse_button & curses.BUTTON1_RELEASED:
out.append("left mouse released")
elif mouse_button & curses.BUTTON2_PRESSED:
out.append("middle mouse")
elif mouse_button & curses.BUTTON2_RELEASED:
out.append("middle mouse released")
elif mouse_button & curses.BUTTON3_PRESSED:
out.append("right mouse")
elif mouse_button & curses.BUTTON3_RELEASED:
out.append("right mouse released")
elif mouse_button & curses.BUTTON4_PRESSED:
out.append("mouse wheel up")
elif mouse_button & curses.BUTTON5_PRESSED:
out.append("mouse wheel down")
else:
out.append(f"unknown {mouse_button}")
return "+".join(out)
[docs]
def wrapper(func: Callable[[curses.window], Any]):
"""Wrap your applications main function to start execution.
An additional wrapper around :func:`curses.wrapper`
that allows for mouse event detection and using
a terminals default background and foreground colors.
"""
try:
def wrapped_func(stdscr):
curses.curs_set(0)
curses.raw()
curses.mousemask(curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION)
curses.mouseinterval(0)
curses.use_default_colors()
print('\033[?1003h') # xterm enable reporting of all mouse events
func(stdscr)
curses.wrapper(wrapped_func)
finally:
print('\033[?1003l') # xterm disable reporting of all mouse events
@cache
def _char_style_to_attr(style: StyleAttr) -> int:
out = 0
if style & StyleAttr.BOLD:
out |= curses.A_BOLD
if style & StyleAttr.ITALIC:
out |= curses.A_ITALIC
if style & StyleAttr.REVERSE:
out |= curses.A_REVERSE
if style & StyleAttr.STRIKE_THROUGH:
out |= curses.A_CHARTEXT
if style & StyleAttr.UNDERLINE:
out |= curses.A_UNDERLINE
if style & StyleAttr.DIM:
out |= curses.A_DIM
return out
@cache
def _color_to_curses(clr: Color):
if isinstance(clr, int):
return int(clr)
else:
return clr.to_nearest_8bit()
# match clr:
#
# case Color.BLACK:
# return curses.COLOR_BLACK
# case Color.RED:
# return curses.COLOR_RED
# case Color.GREEN:
# return curses.COLOR_GREEN
# case Color.YELLOW:
# return curses.COLOR_YELLOW
# case Color.WHITE:
# return curses.COLOR_WHITE
# case Color.BLACK:
# return curses.COLOR_BLACK
# BLUE = 34
# MAGENTA = 35
# CYAN = 36
# WHITE = 37
# RESET = 39
# if out > 7: # don't do anything with reset
# return 0
return out
_pair_cache: dict[tuple[int, int], int] = {
(-1, -1): 0 #curses alwayst initializes color pair 0 to be (-1 -1)
}
def _init_pair_from_style(i: int, style: ComputedStyle):
curr_fg, curr_bg = curses.pair_content(i)
new_fg = _color_to_curses(style.fg) if style.fg else curr_fg
new_bg = _color_to_curses(style.bg) if style.bg else curr_bg
try:
return _pair_cache[(new_fg, new_bg)]
except KeyError:
new_pair_number = len(_pair_cache) + 1
curses.init_pair(new_pair_number, new_fg, new_bg)
_pair_cache[(new_fg, new_bg)] = new_pair_number
return new_pair_number
return i
[docs]
def draw_result(result: Result, stdscr: curses.window):
"""Display the result in a curses window.
You can get the curses window by wrapping your main function in a
:func:`wrapper`."""
stdscr.erase()
data = result.try_data(ResultCreatedWith)
if data is None:
raise AssertionError("Result must have a ResultCreatedWith data.")
pair_number = 1
for command in result.get_commands():
if isinstance(command, DrawPixel):
pair_number = _init_pair_from_style(pair_number, command.pixel.style)
stdscr.addch(
command.at.y,
command.at.x,
command.pixel.char,
_char_style_to_attr(command.pixel.style.attrs) | curses.color_pair(pair_number)
)
elif isinstance(command, DrawStringLine):
pair_number = _init_pair_from_style(pair_number, command.string[0].style)
stdscr.addstr(
command.at.y,
command.at.x,
"".join([i.char for i in command.string if i.char_type != CharType.WIDE_TAIL]),
_char_style_to_attr(command.string[0].style.attrs) | curses.color_pair(pair_number)
)
elif isinstance(command, DrawBox):
pair_number = _init_pair_from_style(pair_number, command.fill.style)
s = [command.box.width * command.fill.char for _ in range(command.box.height)]
for dy, line in enumerate(s):
stdscr.addstr(
command.box.position.y + dy,
command.box.position.x,
line,
_char_style_to_attr(command.fill.style.attrs) | curses.color_pair(pair_number)
)
# screen = Screen(data.screen_size.width, data.screen_size.height)
# screen.apply_draw_commands(data.measure_text_func, result.get_commands())
# lines = screen.split_by_lines()
#
# curr_style = CharStyle(0)
# curr_fg = Color.RESET
# curr_bg = Color.RESET
# # curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_WHITE)
# for y, line in enumerate(lines):
# line_str = []
# for x, pixel in enumerate(line):
# if pixel.char_type == CharType.WIDE_TAIL:
# continue
# if curr_style != pixel.style.char_style:
# if line_str:
# stdscr.addstr(
# y,
# x - data.measure_text_func("".join(line_str)),
# "".join(line_str),
# char_style_to_attr(curr_style) | pair_number
# )
# line_str.clear()
# curr_style = pixel.style.char_style
# if (curr_fg, curr_bg) != (pixel.style.fg, pixel.style.bg):
# if line_str:
# stdscr.addstr(
# y,
# x - data.measure_text_func("".join(line_str)),
# "".join(line_str),
# char_style_to_attr(curr_style) | pair_number
# )
# line_str.clear()
# pair_number = init_pair_from_style(pair_number, pixel.style)
# curr_fg = pixel.style.fg
# curr_bg = pixel.style.bg
# line_str.append(pixel.char)
# if line_str:
# stdscr.addstr(
# y,
# len(line) - len(line_str),
# "".join(line_str),
# char_style_to_attr(curr_style) | pair_number
# )