Coding style.
- Break out termstr, fill3, sorted_collection and lscolors into seperate packages and modules.
This commit is contained in:
parent
8746f578da
commit
2335dec922
18 changed files with 92 additions and 89 deletions
128
eris/ColorMap.py
128
eris/ColorMap.py
|
|
@ -1,128 +0,0 @@
|
|||
|
||||
# This is from https://github.com/broadinstitute/xtermcolor
|
||||
|
||||
|
||||
# Copyright (C) 2012 The Broad Institute
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
# this software and associated documentation files (the "Software"), to deal in
|
||||
# the Software without restriction, including without limitation the rights to
|
||||
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
# of the Software, and to permit persons to whom the Software is furnished to do
|
||||
# so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
|
||||
class TerminalColorMapException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _rgb(color):
|
||||
return ((color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff)
|
||||
|
||||
|
||||
def _diff(color1, color2):
|
||||
(r1, g1, b1) = _rgb(color1)
|
||||
(r2, g2, b2) = _rgb(color2)
|
||||
return abs(r1 - r2) + abs(g1 - g2) + abs(b1 - b2)
|
||||
|
||||
|
||||
class TerminalColorMap:
|
||||
|
||||
def getColors(self, order='rgb'):
|
||||
return self.colors
|
||||
|
||||
def convert(self, hexcolor):
|
||||
diffs = {}
|
||||
for xterm, rgb in self.colors.items():
|
||||
diffs[_diff(rgb, hexcolor)] = xterm
|
||||
minDiffAnsi = diffs[min(diffs.keys())]
|
||||
return (minDiffAnsi, self.colors[minDiffAnsi])
|
||||
|
||||
def colorize(self, string, rgb=None, ansi=None, bg=None, ansi_bg=None):
|
||||
'''Returns the colored string'''
|
||||
if not isinstance(string, str):
|
||||
string = str(string)
|
||||
if rgb is None and ansi is None:
|
||||
raise TerminalColorMapException(
|
||||
'colorize: must specify one named parameter: rgb or ansi')
|
||||
if rgb is not None and ansi is not None:
|
||||
raise TerminalColorMapException(
|
||||
'colorize: must specify only one named parameter: rgb or ansi')
|
||||
if bg is not None and ansi_bg is not None:
|
||||
raise TerminalColorMapException(
|
||||
'colorize: must specify only one named parameter: bg or ansi_bg')
|
||||
|
||||
if rgb is not None:
|
||||
(closestAnsi, closestRgb) = self.convert(rgb)
|
||||
elif ansi is not None:
|
||||
(closestAnsi, closestRgb) = (ansi, self.colors[ansi])
|
||||
|
||||
if bg is None and ansi_bg is None:
|
||||
return "\033[38;5;{ansiCode:d}m{string:s}\033[0m".format(ansiCode=closestAnsi, string=string)
|
||||
|
||||
if bg is not None:
|
||||
(closestBgAnsi, unused) = self.convert(bg)
|
||||
elif ansi_bg is not None:
|
||||
(closestBgAnsi, unused) = (ansi_bg, self.colors[ansi_bg])
|
||||
|
||||
return "\033[38;5;{ansiCode:d}m\033[48;5;{bf:d}m{string:s}\033[0m".format(ansiCode=closestAnsi, bf=closestBgAnsi, string=string)
|
||||
|
||||
|
||||
class VT100ColorMap(TerminalColorMap):
|
||||
primary = [
|
||||
0x000000, 0x800000, 0x008000, 0x808000, 0x000080, 0x800080, 0x008080, 0xc0c0c0
|
||||
]
|
||||
|
||||
bright = [
|
||||
0x808080, 0xff0000, 0x00ff00, 0xffff00, 0x0000ff, 0xff00ff, 0x00ffff, 0xffffff
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
self.colors = dict()
|
||||
self._compute()
|
||||
|
||||
def _compute(self):
|
||||
for index, color in enumerate(self.primary + self.bright):
|
||||
self.colors[index] = color
|
||||
|
||||
|
||||
class XTermColorMap(VT100ColorMap):
|
||||
grayscale_start = 0x08
|
||||
grayscale_end = 0xf8
|
||||
grayscale_step = 10
|
||||
intensities = [
|
||||
0x00, 0x5F, 0x87, 0xAF, 0xD7, 0xFF
|
||||
]
|
||||
|
||||
def _compute(self):
|
||||
for index, color in enumerate(self.primary + self.bright):
|
||||
self.colors[index] = color
|
||||
|
||||
c = 16
|
||||
for i in self.intensities:
|
||||
color = i << 16
|
||||
for j in self.intensities:
|
||||
color &= ~(0xff << 8)
|
||||
color |= j << 8
|
||||
for k in self.intensities:
|
||||
color &= ~0xff
|
||||
color |= k
|
||||
self.colors[c] = color
|
||||
c += 1
|
||||
|
||||
c = 232
|
||||
for hex in list(range(self.grayscale_start, self.grayscale_end, self.grayscale_step)):
|
||||
color = (hex << 16) | (hex << 8) | hex
|
||||
self.colors[c] = color
|
||||
c += 1
|
||||
|
|
@ -36,13 +36,13 @@ import pygments.styles
|
|||
import pyinotify
|
||||
|
||||
import eris
|
||||
import eris.fill3 as fill3
|
||||
import eris.sorted_collection as sorted_collection
|
||||
import eris.terminal as terminal
|
||||
import eris.termstr as termstr
|
||||
import eris.tools as tools
|
||||
import eris.worker as worker
|
||||
import eris.paged_list as paged_list
|
||||
import fill3
|
||||
import sorted_collection
|
||||
import termstr.termstr as termstr
|
||||
import termstr.terminal as terminal
|
||||
|
||||
|
||||
USAGE = """
|
||||
|
|
|
|||
546
eris/fill3.py
546
eris/fill3.py
|
|
@ -1,546 +0,0 @@
|
|||
#!/usr/bin/python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import itertools
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
|
||||
import eris.terminal as terminal
|
||||
import eris.termstr as termstr
|
||||
|
||||
|
||||
def appearance_is_valid(appearance):
|
||||
return (all(isinstance(line, (str, termstr.TermStr)) and len(line) > 0
|
||||
for line in appearance) and
|
||||
len(set(len(line) for line in appearance)) < 2)
|
||||
|
||||
|
||||
def appearance_resize(appearance, dimensions, pad_char=" "):
|
||||
width, height = dimensions
|
||||
result = [line[:width].ljust(width, pad_char)
|
||||
for line in appearance[:height]]
|
||||
if len(result) < height:
|
||||
result.extend([pad_char * width] * (height - len(result)))
|
||||
return result
|
||||
|
||||
|
||||
def appearance_dimensions(appearance):
|
||||
try:
|
||||
return len(appearance[0]), len(appearance)
|
||||
except IndexError:
|
||||
return 0, 0
|
||||
|
||||
|
||||
def join(seperator, parts):
|
||||
if parts == []:
|
||||
return ""
|
||||
try:
|
||||
return seperator.join(parts)
|
||||
except TypeError:
|
||||
return termstr.TermStr(seperator).join(parts)
|
||||
|
||||
|
||||
def join_horizontal(appearances):
|
||||
heights = set(len(appearance) for appearance in appearances)
|
||||
assert len(heights) == 1, heights
|
||||
return [join("", parts) for parts in zip(*appearances)]
|
||||
|
||||
|
||||
def even_widths(column_widgets, width):
|
||||
column_count = len(column_widgets)
|
||||
widths = []
|
||||
for index, column_widget in enumerate(column_widgets):
|
||||
start_pos = int(round(float(width) / column_count * index))
|
||||
end_pos = int(round(float(width) / column_count * (index+1)))
|
||||
widths.append(end_pos - start_pos)
|
||||
return widths
|
||||
|
||||
|
||||
def appearance_as_html(appearance):
|
||||
lines = []
|
||||
all_styles = set()
|
||||
for line in appearance:
|
||||
html, styles = termstr.TermStr(line).as_html()
|
||||
all_styles.update(styles)
|
||||
lines.append(html)
|
||||
return ("\n".join(style.as_html() for style in all_styles) +
|
||||
"\n<pre>" + "<br>".join(lines) + "</pre>")
|
||||
|
||||
|
||||
class Row:
|
||||
|
||||
def __init__(self, widgets, widths_func=even_widths):
|
||||
self.widgets = widgets
|
||||
self.widths_func = widths_func
|
||||
|
||||
def appearance(self, dimensions):
|
||||
width, height = dimensions
|
||||
widths = self.widths_func(self.widgets, width)
|
||||
assert sum(widths) == width, (sum(widths), width)
|
||||
return join_horizontal([column_widget.appearance((item_width, height))
|
||||
for column_widget, item_width
|
||||
in zip(self.widgets, widths)])
|
||||
|
||||
def appearance_min(self):
|
||||
appearances = [column_widget.appearance_min()
|
||||
for column_widget in self.widgets]
|
||||
dimensions = [appearance_dimensions(appearance)
|
||||
for appearance in appearances]
|
||||
max_height = max(height for width, height in dimensions)
|
||||
return join_horizontal([
|
||||
appearance_resize(appearance, (width, max_height))
|
||||
for appearance, (width, height) in zip(appearances, dimensions)])
|
||||
|
||||
|
||||
def even_partition(row_widgets, height):
|
||||
row_count = len(row_widgets)
|
||||
heights = []
|
||||
for index, row_widget in enumerate(row_widgets):
|
||||
start_pos = int(round(float(height) / row_count * index))
|
||||
end_pos = int(round(float(height) / row_count * (index+1)))
|
||||
heights.append(end_pos - start_pos)
|
||||
return heights
|
||||
|
||||
|
||||
def join_vertical(appearances):
|
||||
result = []
|
||||
for appearance in appearances:
|
||||
result.extend(appearance)
|
||||
return result
|
||||
|
||||
|
||||
class Column:
|
||||
|
||||
def __init__(self, widgets, partition_func=even_partition,
|
||||
background_char=" "):
|
||||
self.widgets = widgets
|
||||
self.partition_func = partition_func
|
||||
self.background_char = background_char
|
||||
|
||||
def appearance(self, dimensions):
|
||||
width, height = dimensions
|
||||
if len(self.widgets) == 0: # FIX: Really allow zero widgets?
|
||||
return [self.background_char * width] * height
|
||||
heights = self.partition_func(self.widgets, height)
|
||||
assert sum(heights) == height, (sum(heights), height)
|
||||
return join_vertical([row_widget.appearance((width, item_height))
|
||||
for row_widget, item_height
|
||||
in zip(self.widgets, heights)])
|
||||
|
||||
def _appearance_list(self, widgets):
|
||||
if widgets == []:
|
||||
return []
|
||||
appearances = [row_widget.appearance_min() for row_widget in widgets]
|
||||
dimensions = [appearance_dimensions(appearance)
|
||||
for appearance in appearances]
|
||||
max_width = max(width for width, height in dimensions)
|
||||
padded_appearances = [
|
||||
appearance_resize(appearance, (max_width, height))
|
||||
for appearance, (width, height) in zip(appearances, dimensions)]
|
||||
result = []
|
||||
for appearance in padded_appearances:
|
||||
result.extend(appearance)
|
||||
return result
|
||||
|
||||
def appearance_interval(self, interval):
|
||||
start_y, end_y = interval
|
||||
return self._appearance_list(self.widgets[start_y:end_y])
|
||||
|
||||
def appearance_min(self):
|
||||
return self._appearance_list(self.widgets)
|
||||
|
||||
|
||||
class Filler:
|
||||
|
||||
def __init__(self, widget):
|
||||
self.widget = widget
|
||||
|
||||
def appearance(self, dimensions):
|
||||
return appearance_resize(self.widget.appearance_min(), dimensions)
|
||||
|
||||
|
||||
class ScrollBar:
|
||||
|
||||
_PARTIAL_CHARS = (["█", "▇", "▆", "▅", "▄", "▃", "▂", "▁"],
|
||||
[" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉"])
|
||||
DEFAULT_BAR_COLOR = termstr.Color.grey_100
|
||||
DEFAULT_BACKGROUND_COLOR = termstr.Color.grey_30
|
||||
|
||||
def __init__(self, is_horizontal, interval=(0, 0), bar_color=None,
|
||||
background_color=None):
|
||||
self._is_horizontal = is_horizontal
|
||||
self.interval = interval
|
||||
bar_color = bar_color or ScrollBar.DEFAULT_BAR_COLOR
|
||||
background_color = (background_color or
|
||||
ScrollBar.DEFAULT_BACKGROUND_COLOR)
|
||||
self._bar_char = termstr.TermStr("█").fg_color(bar_color)
|
||||
self._background_char = termstr.TermStr(" ").bg_color(background_color)
|
||||
if self._is_horizontal:
|
||||
bar_color, background_color = background_color, bar_color
|
||||
self._partial_chars = [(termstr.TermStr(char).fg_color(
|
||||
bar_color).bg_color(background_color),
|
||||
termstr.TermStr(char).fg_color(
|
||||
background_color).bg_color(bar_color))
|
||||
for char in self._PARTIAL_CHARS[self._is_horizontal]]
|
||||
|
||||
def appearance(self, dimensions):
|
||||
width, height = dimensions
|
||||
assert width == 1 or height == 1, (width, height)
|
||||
length = width if self._is_horizontal else height
|
||||
assert all(0 <= fraction <= 1 for fraction in self.interval), \
|
||||
self.interval
|
||||
(start_index, start_remainder), (end_index, end_remainder) = \
|
||||
[divmod(fraction * length * 8, 8) for fraction in self.interval]
|
||||
start_index, end_index = int(start_index), int(end_index)
|
||||
start_remainder, end_remainder = \
|
||||
int(start_remainder), int(end_remainder)
|
||||
if start_index == end_index:
|
||||
end_index, end_remainder = start_index + 1, start_remainder
|
||||
elif end_index == start_index + 1:
|
||||
end_remainder = max(start_remainder, end_remainder)
|
||||
bar = (self._background_char * start_index +
|
||||
self._partial_chars[start_remainder][0] +
|
||||
self._bar_char * (end_index - start_index - 1) +
|
||||
self._partial_chars[end_remainder][1] +
|
||||
self._background_char * (length - end_index - 1))
|
||||
bar = bar[:length]
|
||||
return [bar] if self._is_horizontal else [char for char in bar]
|
||||
|
||||
|
||||
class Portal:
|
||||
|
||||
def __init__(self, widget, position=(0, 0), background_char=" "):
|
||||
self.widget = widget
|
||||
self.position = position
|
||||
self.background_char = background_char
|
||||
self.last_dimensions = 0, 0
|
||||
|
||||
def _scroll_half_pages(self, dx, dy):
|
||||
x, y = self.position
|
||||
width, height = self.last_dimensions
|
||||
self.position = (max(x + dx * (width // 2), 0),
|
||||
max(y + dy * (height // 2), 0))
|
||||
|
||||
def scroll_up(self):
|
||||
self._scroll_half_pages(0, -1)
|
||||
|
||||
def scroll_down(self):
|
||||
self._scroll_half_pages(0, 1)
|
||||
|
||||
def scroll_left(self):
|
||||
self._scroll_half_pages(-1, 0)
|
||||
|
||||
def scroll_right(self):
|
||||
self._scroll_half_pages(1, 0)
|
||||
|
||||
def appearance(self, dimensions):
|
||||
width, height = dimensions
|
||||
x, y = self.position
|
||||
try:
|
||||
appearance = self.widget.appearance_interval((y, y+height))
|
||||
except AttributeError:
|
||||
appearance = self.widget.appearance_min()[y:y+height]
|
||||
self.last_dimensions = dimensions
|
||||
return appearance_resize([row[x:x+width] for row in appearance],
|
||||
dimensions, self.background_char)
|
||||
|
||||
|
||||
class View:
|
||||
|
||||
def __init__(self, portal, horizontal_scrollbar, vertical_scrollbar,
|
||||
hide_scrollbars=True):
|
||||
self.portal = portal
|
||||
self.horizontal_scrollbar = horizontal_scrollbar
|
||||
self.vertical_scrollbar = vertical_scrollbar
|
||||
self.hide_scrollbars = hide_scrollbars
|
||||
|
||||
@classmethod
|
||||
def from_widget(cls, widget):
|
||||
return cls(Portal(widget), ScrollBar(is_horizontal=True),
|
||||
ScrollBar(is_horizontal=False))
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
return self.portal.position
|
||||
|
||||
@position.setter
|
||||
def position(self, position):
|
||||
self.portal.position = position
|
||||
|
||||
@property
|
||||
def widget(self):
|
||||
return self.portal.widget
|
||||
|
||||
@widget.setter
|
||||
def widget(self, widget):
|
||||
self.portal.widget = widget
|
||||
|
||||
def appearance(self, dimensions):
|
||||
width, height = dimensions
|
||||
try:
|
||||
full_width, full_height = (self.portal.widget.
|
||||
appearance_dimensions())
|
||||
except AttributeError:
|
||||
full_appearance = self.portal.widget.appearance_min()
|
||||
full_width, full_height = appearance_dimensions(full_appearance)
|
||||
if full_width == 0 or full_height == 0:
|
||||
return self.portal.appearance(dimensions)
|
||||
x, y = self.portal.position
|
||||
hide_scrollbar_vertical = (self.hide_scrollbars and
|
||||
full_height <= height and y == 0)
|
||||
hide_scrollbar_horizontal = (self.hide_scrollbars and
|
||||
full_width <= width and x == 0)
|
||||
if not hide_scrollbar_horizontal:
|
||||
full_width = max(full_width, x + width)
|
||||
self.horizontal_scrollbar.interval = (x / full_width,
|
||||
(x + width) / full_width)
|
||||
height -= 1
|
||||
if not hide_scrollbar_vertical:
|
||||
full_height = max(full_height, y + height)
|
||||
self.vertical_scrollbar.interval = (y / full_height,
|
||||
(y + height) / full_height)
|
||||
width -= 1
|
||||
portal_appearance = self.portal.appearance((width, height))
|
||||
if hide_scrollbar_vertical:
|
||||
result = portal_appearance
|
||||
else:
|
||||
scrollbar_v_appearance = self.vertical_scrollbar.appearance(
|
||||
(1, height))
|
||||
result = join_horizontal([portal_appearance,
|
||||
scrollbar_v_appearance])
|
||||
if not hide_scrollbar_horizontal:
|
||||
scrollbar_h_appearance = self.horizontal_scrollbar.appearance(
|
||||
(width, 1))
|
||||
result.append(scrollbar_h_appearance[0] +
|
||||
("" if hide_scrollbar_vertical else " "))
|
||||
return result
|
||||
|
||||
|
||||
def str_to_appearance(text, pad_char=" "):
|
||||
lines = text.splitlines()
|
||||
if len(lines) == 0:
|
||||
return []
|
||||
max_width = max(len(line) for line in lines)
|
||||
height = len(lines)
|
||||
return appearance_resize(lines, (max_width, height), pad_char)
|
||||
|
||||
|
||||
class Text:
|
||||
|
||||
def __init__(self, text, pad_char=" "):
|
||||
self.text = str_to_appearance(text, pad_char)
|
||||
|
||||
def appearance_min(self):
|
||||
return self.text
|
||||
|
||||
def appearance(self, dimensions):
|
||||
return appearance_resize(self.appearance_min(), dimensions)
|
||||
|
||||
|
||||
class Table:
|
||||
|
||||
def __init__(self, table, pad_char=" "):
|
||||
self._widgets = table
|
||||
self._pad_char = pad_char
|
||||
|
||||
def appearance_min(self):
|
||||
if self._widgets == []:
|
||||
return []
|
||||
appearances = [[cell.appearance_min() for cell in row]
|
||||
for row in self._widgets]
|
||||
row_heights = [0] * len(self._widgets)
|
||||
column_widths = [0] * len(self._widgets[0])
|
||||
for y, row in enumerate(appearances):
|
||||
for x, appearance in enumerate(row):
|
||||
width, height = appearance_dimensions(appearance)
|
||||
row_heights[y] = max(row_heights[y], height)
|
||||
column_widths[x] = max(column_widths[x], width)
|
||||
return join_vertical([join_horizontal(
|
||||
[appearance_resize(appearance, (column_widths[x], row_heights[y]),
|
||||
pad_char=self._pad_char)
|
||||
for x, appearance in enumerate(row)])
|
||||
for y, row in enumerate(appearances)])
|
||||
|
||||
|
||||
class Border:
|
||||
|
||||
THIN = ["─", "─", "│", "│", "┌", "└", "┘", "┐"]
|
||||
THICK = ["━", "━", "┃", "┃", "┏", "┗", "┛", "┓"]
|
||||
ROUNDED = ["─", "─", "│", "│", "╭", "╰", "╯", "╮"]
|
||||
DOUBLE = ["═", "═", "║", "║", "╔", "╚", "╝", "╗"]
|
||||
|
||||
def __init__(self, widget, title=None, characters=THIN):
|
||||
self.widget = widget
|
||||
self.title = title
|
||||
self.set_style(characters)
|
||||
|
||||
def set_style(self, characters):
|
||||
(self.top, self.bottom, self.left, self.right, self.top_left,
|
||||
self.bottom_left, self.bottom_right, self.top_right) = characters
|
||||
|
||||
def _add_border(self, body_content):
|
||||
content_width, content_height = appearance_dimensions(body_content)
|
||||
if self.title is None:
|
||||
title_bar = self.top * content_width
|
||||
else:
|
||||
padded_title = " " + ("…" + self.title[-(content_width-3):]
|
||||
if len(self.title) > content_width - 2
|
||||
else self.title) + " "
|
||||
try:
|
||||
title_bar = padded_title.center(content_width, self.top)
|
||||
except TypeError:
|
||||
padded_title = termstr.TermStr(padded_title)
|
||||
title_bar = padded_title.center(content_width, self.top)
|
||||
result = [self.top_left + title_bar + self.top_right]
|
||||
result.extend(self.left + line + self.right for line in body_content)
|
||||
result.append(self.bottom_left + self.bottom * content_width +
|
||||
self.bottom_right)
|
||||
return result
|
||||
|
||||
def appearance_min(self):
|
||||
return self._add_border(self.widget.appearance_min())
|
||||
|
||||
def appearance(self, dimensions):
|
||||
width, height = dimensions
|
||||
return self._add_border(self.widget.appearance((width-2, height-2)))
|
||||
|
||||
|
||||
class Placeholder:
|
||||
|
||||
def __init__(self, widget=None):
|
||||
self.widget = widget
|
||||
|
||||
def appearance_min(self):
|
||||
return self.widget.appearance_min()
|
||||
|
||||
def appearance(self, dimensions):
|
||||
return self.widget.appearance(dimensions)
|
||||
|
||||
|
||||
class Fixed:
|
||||
|
||||
def __init__(self, appearance_min):
|
||||
self.appearance_min_ = appearance_min
|
||||
self.dimensions = appearance_dimensions(appearance_min)
|
||||
|
||||
def appearance_min(self):
|
||||
return self.appearance_min_
|
||||
|
||||
def appearance_dimensions(self):
|
||||
return self.dimensions
|
||||
|
||||
|
||||
##########################
|
||||
|
||||
|
||||
_last_appearance = []
|
||||
|
||||
|
||||
def draw_screen(widget):
|
||||
global _last_appearance
|
||||
appearance = widget.appearance(os.get_terminal_size())
|
||||
print(terminal.move(0, 0), *appearance, sep="", end="", flush=True)
|
||||
_last_appearance = appearance
|
||||
|
||||
|
||||
def patch_screen(widget):
|
||||
global _last_appearance
|
||||
appearance = widget.appearance(os.get_terminal_size())
|
||||
zip_func = (itertools.zip_longest
|
||||
if len(appearance) > len(_last_appearance) else zip)
|
||||
changed_lines = (str(terminal.move(0, row_index)) + line
|
||||
for row_index, (line, old_line)
|
||||
in enumerate(zip_func(appearance, _last_appearance))
|
||||
if line != old_line)
|
||||
print(*changed_lines, sep="", end="", flush=True)
|
||||
_last_appearance = appearance
|
||||
|
||||
|
||||
async def update_screen(screen_widget, appearance_changed_event):
|
||||
while True:
|
||||
await appearance_changed_event.wait()
|
||||
appearance_changed_event.clear()
|
||||
patch_screen(screen_widget)
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
|
||||
def on_terminal_input(screen_widget):
|
||||
term_code = sys.stdin.read()
|
||||
if term_code.startswith(terminal.MOUSE):
|
||||
for part in term_code.split(terminal.MOUSE)[1:]:
|
||||
screen_widget.on_mouse_input(part)
|
||||
else:
|
||||
return screen_widget.on_keyboard_input(term_code)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def signal_handler(loop, signal_, func):
|
||||
loop.add_signal_handler(signal_, func)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
loop.remove_signal_handler(signal_)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def context(loop, appearance_changed_event, screen_widget, exit_loop=None):
|
||||
appearance_changed_event.set()
|
||||
if exit_loop is None:
|
||||
exit_loop = loop.stop
|
||||
with signal_handler(loop, signal.SIGWINCH,
|
||||
lambda: draw_screen(screen_widget)), \
|
||||
signal_handler(loop, signal.SIGINT, exit_loop), \
|
||||
signal_handler(loop, signal.SIGTERM, exit_loop), \
|
||||
terminal.alternate_buffer(), terminal.interactive(), \
|
||||
terminal.mouse_tracking():
|
||||
update_task = loop.create_task(
|
||||
update_screen(screen_widget, appearance_changed_event))
|
||||
try:
|
||||
loop.add_reader(sys.stdin, on_terminal_input, screen_widget)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
loop.remove_reader(sys.stdin)
|
||||
finally:
|
||||
update_task.cancel()
|
||||
|
||||
|
||||
##########################
|
||||
|
||||
|
||||
class _Screen:
|
||||
|
||||
def __init__(self, appearance_changed_event):
|
||||
self._appearance_changed_event = appearance_changed_event
|
||||
self.content = Filler(Text("Hello World"))
|
||||
|
||||
def appearance(self, dimensions):
|
||||
return self.content.appearance(dimensions)
|
||||
|
||||
def on_keyboard_input(self, term_code):
|
||||
if term_code in ["q", terminal.ESC]:
|
||||
asyncio.get_event_loop().stop()
|
||||
else:
|
||||
self.content = Filler(Text(repr(term_code)))
|
||||
self._appearance_changed_event.set()
|
||||
|
||||
def on_mouse_input(self, term_code):
|
||||
mouse_code = terminal.decode_mouse_input(term_code)
|
||||
self.content = Filler(Text(repr(term_code) + " " + repr(mouse_code)))
|
||||
self._appearance_changed_event.set()
|
||||
|
||||
|
||||
def _main():
|
||||
loop = asyncio.get_event_loop()
|
||||
appearance_changed_event = asyncio.Event()
|
||||
screen = _Screen(appearance_changed_event)
|
||||
with context(loop, appearance_changed_event, screen):
|
||||
loop.run_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_main()
|
||||
138
eris/lscolors.py
138
eris/lscolors.py
|
|
@ -1,138 +0,0 @@
|
|||
|
||||
|
||||
|
||||
"""Determine a color for a file based on its file type.
|
||||
|
||||
This is done in the same way as the ls command.
|
||||
"""
|
||||
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import stat
|
||||
import syslog
|
||||
|
||||
|
||||
FILE_KEY = "fi"
|
||||
DIRECTORY_KEY = "di"
|
||||
OTHER_WRITABLE_KEY = "ow"
|
||||
EXECUTABLE_KEY = "ex"
|
||||
SETUID_KEY = "su"
|
||||
SETGUID_KEY = "sg"
|
||||
SYMLINK_KEY = "ln"
|
||||
ORPHAN_KEY = "or"
|
||||
PIPE_KEY = "pi"
|
||||
CHARACTER_DEVICE_KEY = "cd"
|
||||
BLOCK_DEVICE_KEY = "bd"
|
||||
STICKY_KEY = "st"
|
||||
STICKY_OTHER_WRITABLE_KEY = "tw"
|
||||
SOCKET_KEY = "so"
|
||||
MISSING_KEY = "mi"
|
||||
MULTI_HARDLINK_KEY = "mh"
|
||||
|
||||
|
||||
def _parse_ls_colors(ls_codes):
|
||||
color_codes = {}
|
||||
for entry in ls_codes.split(":"):
|
||||
if "=" not in entry:
|
||||
continue
|
||||
entry_key, entry_value = entry.split("=")
|
||||
if entry_key.startswith("*."):
|
||||
entry_key = entry_key[1:]
|
||||
color_codes[entry_key] = entry_value
|
||||
assert color_codes != {}, color_codes
|
||||
return color_codes
|
||||
|
||||
|
||||
_DEFAULT_COLOR_CODES = \
|
||||
{BLOCK_DEVICE_KEY: '01;33', SYMLINK_KEY: '01;36',
|
||||
STICKY_OTHER_WRITABLE_KEY: '30;42', DIRECTORY_KEY: '01;34',
|
||||
SETUID_KEY: '37;41', CHARACTER_DEVICE_KEY: '01;33', SOCKET_KEY: '01;35',
|
||||
EXECUTABLE_KEY: '01;32', STICKY_KEY: '37;44',
|
||||
OTHER_WRITABLE_KEY: '34;42', PIPE_KEY: '33', SETGUID_KEY: '30;43',
|
||||
ORPHAN_KEY: '40;31;01'}
|
||||
|
||||
|
||||
def get_color_codes(environment):
|
||||
"""Get a dictionary of the color of every file type."""
|
||||
if "LS_COLORS" in environment:
|
||||
try:
|
||||
return _parse_ls_colors(environment["LS_COLORS"])
|
||||
except Exception:
|
||||
syslog.syslog("Syntax error in LS_COLORS environment variable. "
|
||||
"Using default colors.")
|
||||
return _DEFAULT_COLOR_CODES
|
||||
|
||||
|
||||
def color_key_for_path(path, color_codes, is_link_target=True):
|
||||
"""Get the high level type (key) of a file."""
|
||||
# see print_color_indicator in the file 'ls.c' in the coreutils codebase
|
||||
if not os.path.lexists(path):
|
||||
return MISSING_KEY
|
||||
elif os.path.islink(path):
|
||||
if is_link_target:
|
||||
try:
|
||||
link_path = os.path.join(os.path.dirname(path),
|
||||
os.readlink(path))
|
||||
file_stat = os.stat(link_path)
|
||||
except OSError:
|
||||
return ORPHAN_KEY
|
||||
else:
|
||||
return SYMLINK_KEY
|
||||
else:
|
||||
file_stat = os.stat(path)
|
||||
mode = file_stat.st_mode
|
||||
if stat.S_ISREG(mode):
|
||||
if mode & stat.S_ISUID and SETUID_KEY in color_codes:
|
||||
return SETUID_KEY
|
||||
elif mode & stat.S_ISGID and SETGUID_KEY in color_codes:
|
||||
return SETGUID_KEY
|
||||
elif ((mode & stat.S_IXUSR or mode & stat.S_IXGRP or
|
||||
mode & stat.S_IXOTH) and EXECUTABLE_KEY in color_codes):
|
||||
return EXECUTABLE_KEY
|
||||
elif file_stat.st_nlink > 1 and MULTI_HARDLINK_KEY in color_codes:
|
||||
return MULTI_HARDLINK_KEY
|
||||
else:
|
||||
return FILE_KEY
|
||||
elif stat.S_ISDIR(mode):
|
||||
if (mode & stat.S_ISVTX and mode & stat.S_IWOTH and
|
||||
STICKY_OTHER_WRITABLE_KEY in color_codes):
|
||||
return STICKY_OTHER_WRITABLE_KEY
|
||||
elif (mode & stat.S_IWOTH) != 0 and OTHER_WRITABLE_KEY in color_codes:
|
||||
return OTHER_WRITABLE_KEY
|
||||
elif (mode & stat.S_ISVTX) != 0 and STICKY_KEY in color_codes:
|
||||
return STICKY_KEY
|
||||
else:
|
||||
return DIRECTORY_KEY
|
||||
for test_function, color_key in [(stat.S_ISFIFO, PIPE_KEY),
|
||||
(stat.S_ISSOCK, SOCKET_KEY),
|
||||
(stat.S_ISBLK, BLOCK_DEVICE_KEY),
|
||||
(stat.S_ISCHR, CHARACTER_DEVICE_KEY)]:
|
||||
if test_function(mode):
|
||||
return color_key
|
||||
return ORPHAN_KEY
|
||||
|
||||
|
||||
def color_code_for_path(path, color_codes):
|
||||
"""Get the color of a file."""
|
||||
def get_extension(basename, color_codes):
|
||||
parts = basename.split(".")
|
||||
if len(parts) == 2:
|
||||
extension = "." + parts[1]
|
||||
if extension in color_codes:
|
||||
return extension
|
||||
elif len(parts) > 2:
|
||||
for extension in color_codes:
|
||||
if extension.startswith(".") and \
|
||||
basename.endswith(extension):
|
||||
return extension
|
||||
target_link = color_codes.get(SYMLINK_KEY, None)
|
||||
color_key = color_key_for_path(path, color_codes,
|
||||
target_link == "target")
|
||||
if color_key == FILE_KEY:
|
||||
filename = os.path.basename(path)
|
||||
if "." in filename:
|
||||
extension = get_extension(filename, color_codes)
|
||||
if extension is not None:
|
||||
color_key = extension
|
||||
return color_codes.get(color_key, None)
|
||||
|
|
@ -4,13 +4,13 @@
|
|||
import pydoc
|
||||
import sys
|
||||
|
||||
import eris.termstr
|
||||
import termstr.termstr as termstr
|
||||
|
||||
|
||||
class TermDoc(pydoc.TextDoc):
|
||||
|
||||
def bold(self, text):
|
||||
return str(eris.termstr.TermStr(text).bold())
|
||||
return str(termstr.TermStr(text).bold())
|
||||
|
||||
|
||||
def main():
|
||||
|
|
|
|||
|
|
@ -1,340 +0,0 @@
|
|||
|
||||
# From https://code.activestate.com/recipes/577197-sortedcollection/
|
||||
|
||||
# Copyright (c) 2010 Raymond Hettinger
|
||||
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
|
||||
from bisect import bisect_left, bisect_right
|
||||
|
||||
class SortedCollection(object):
|
||||
'''Sequence sorted by a key function.
|
||||
|
||||
SortedCollection() is much easier to work with than using bisect() directly.
|
||||
It supports key functions like those use in sorted(), min(), and max().
|
||||
The result of the key function call is saved so that keys can be searched
|
||||
efficiently.
|
||||
|
||||
Instead of returning an insertion-point which can be hard to interpret, the
|
||||
five find-methods return a specific item in the sequence. They can scan for
|
||||
exact matches, the last item less-than-or-equal to a key, or the first item
|
||||
greater-than-or-equal to a key.
|
||||
|
||||
Once found, an item's ordinal position can be located with the index() method.
|
||||
New items can be added with the insert() and insert_right() methods.
|
||||
Old items can be deleted with the remove() method.
|
||||
|
||||
The usual sequence methods are provided to support indexing, slicing,
|
||||
length lookup, clearing, copying, forward and reverse iteration, contains
|
||||
checking, item counts, item removal, and a nice looking repr.
|
||||
|
||||
Finding and indexing are O(log n) operations while iteration and insertion
|
||||
are O(n). The initial sort is O(n log n).
|
||||
|
||||
The key function is stored in the 'key' attibute for easy introspection or
|
||||
so that you can assign a new key function (triggering an automatic re-sort).
|
||||
|
||||
In short, the class was designed to handle all of the common use cases for
|
||||
bisect but with a simpler API and support for key functions.
|
||||
|
||||
>>> from pprint import pprint
|
||||
>>> from operator import itemgetter
|
||||
|
||||
>>> s = SortedCollection(key=itemgetter(2))
|
||||
>>> for record in [
|
||||
... ('roger', 'young', 30),
|
||||
... ('angela', 'jones', 28),
|
||||
... ('bill', 'smith', 22),
|
||||
... ('david', 'thomas', 32)]:
|
||||
... s.insert(record)
|
||||
0
|
||||
0
|
||||
0
|
||||
3
|
||||
>>> pprint(list(s)) # show records sorted by age
|
||||
[('bill', 'smith', 22),
|
||||
('angela', 'jones', 28),
|
||||
('roger', 'young', 30),
|
||||
('david', 'thomas', 32)]
|
||||
|
||||
>>> s.find_le(29) # find oldest person aged 29 or younger
|
||||
('angela', 'jones', 28)
|
||||
>>> s.find_lt(28) # find oldest person under 28
|
||||
('bill', 'smith', 22)
|
||||
>>> s.find_gt(28) # find youngest person over 28
|
||||
('roger', 'young', 30)
|
||||
|
||||
>>> r = s.find_ge(32) # find youngest person aged 32 or older
|
||||
>>> s.index(r) # get the index of their record
|
||||
3
|
||||
>>> s[3] # fetch the record at that index
|
||||
('david', 'thomas', 32)
|
||||
|
||||
>>> s.key = itemgetter(0) # now sort by first name
|
||||
>>> pprint(list(s))
|
||||
[('angela', 'jones', 28),
|
||||
('bill', 'smith', 22),
|
||||
('david', 'thomas', 32),
|
||||
('roger', 'young', 30)]
|
||||
|
||||
'''
|
||||
|
||||
def __init__(self, iterable=(), key=None):
|
||||
self._given_key = key
|
||||
key = (lambda x: x) if key is None else key
|
||||
decorated = sorted((key(item), item) for item in iterable)
|
||||
self._keys = [k for k, item in decorated]
|
||||
self._items = [item for k, item in decorated]
|
||||
self._key = key
|
||||
|
||||
def _getkey(self):
|
||||
return self._key
|
||||
|
||||
def _setkey(self, key):
|
||||
if key is not self._key:
|
||||
self.__init__(self._items, key=key)
|
||||
|
||||
def _delkey(self):
|
||||
self._setkey(None)
|
||||
|
||||
key = property(_getkey, _setkey, _delkey, 'key function')
|
||||
|
||||
def clear(self):
|
||||
self.__init__([], self._key)
|
||||
|
||||
def copy(self):
|
||||
return self.__class__(self, self._key)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._items)
|
||||
|
||||
def __getitem__(self, i):
|
||||
return self._items[i]
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._items)
|
||||
|
||||
def __reversed__(self):
|
||||
return reversed(self._items)
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%r, key=%s)' % (
|
||||
self.__class__.__name__,
|
||||
self._items,
|
||||
getattr(self._given_key, '__name__', repr(self._given_key))
|
||||
)
|
||||
|
||||
def __reduce__(self):
|
||||
return self.__class__, (self._items, self._given_key)
|
||||
|
||||
def __contains__(self, item):
|
||||
k = self._key(item)
|
||||
i = bisect_left(self._keys, k)
|
||||
j = bisect_right(self._keys, k)
|
||||
return item in self._items[i:j]
|
||||
|
||||
def index(self, item):
|
||||
'Find the position of an item. Raise ValueError if not found.'
|
||||
k = self._key(item)
|
||||
i = bisect_left(self._keys, k)
|
||||
j = bisect_right(self._keys, k)
|
||||
return self._items[i:j].index(item) + i
|
||||
|
||||
def count(self, item):
|
||||
'Return number of occurrences of item'
|
||||
k = self._key(item)
|
||||
i = bisect_left(self._keys, k)
|
||||
j = bisect_right(self._keys, k)
|
||||
return self._items[i:j].count(item)
|
||||
|
||||
def insert(self, item):
|
||||
'Insert a new item. If equal keys are found, add to the left'
|
||||
k = self._key(item)
|
||||
i = bisect_left(self._keys, k)
|
||||
self._keys.insert(i, k)
|
||||
self._items.insert(i, item)
|
||||
return i
|
||||
|
||||
def insert_right(self, item):
|
||||
'Insert a new item. If equal keys are found, add to the right'
|
||||
k = self._key(item)
|
||||
i = bisect_right(self._keys, k)
|
||||
self._keys.insert(i, k)
|
||||
self._items.insert(i, item)
|
||||
return i
|
||||
|
||||
def remove(self, item):
|
||||
'Remove first occurence of item. Raise ValueError if not found'
|
||||
i = self.index(item)
|
||||
del self._keys[i]
|
||||
del self._items[i]
|
||||
|
||||
def find(self, k):
|
||||
'Return first item with a key == k. Raise ValueError if not found.'
|
||||
i = bisect_left(self._keys, k)
|
||||
if i != len(self) and self._keys[i] == k:
|
||||
return self._items[i]
|
||||
raise ValueError('No item found with key equal to: %r' % (k,))
|
||||
|
||||
def find_le(self, k):
|
||||
'Return last item with a key <= k. Raise ValueError if not found.'
|
||||
i = bisect_right(self._keys, k)
|
||||
if i:
|
||||
return self._items[i-1]
|
||||
raise ValueError('No item found with key at or below: %r' % (k,))
|
||||
|
||||
def find_lt(self, k):
|
||||
'Return last item with a key < k. Raise ValueError if not found.'
|
||||
i = bisect_left(self._keys, k)
|
||||
if i:
|
||||
return self._items[i-1]
|
||||
raise ValueError('No item found with key below: %r' % (k,))
|
||||
|
||||
def find_ge(self, k):
|
||||
'Return first item with a key >= equal to k. Raise ValueError if not found'
|
||||
i = bisect_left(self._keys, k)
|
||||
if i != len(self):
|
||||
return self._items[i]
|
||||
raise ValueError('No item found with key at or above: %r' % (k,))
|
||||
|
||||
def find_gt(self, k):
|
||||
'Return first item with a key > k. Raise ValueError if not found'
|
||||
i = bisect_right(self._keys, k)
|
||||
if i != len(self):
|
||||
return self._items[i]
|
||||
raise ValueError('No item found with key above: %r' % (k,))
|
||||
|
||||
|
||||
# --------------------------- Simple demo and tests -------------------------
|
||||
if __name__ == '__main__':
|
||||
|
||||
def ve2no(f, *args):
|
||||
'Convert ValueError result to -1'
|
||||
try:
|
||||
return f(*args)
|
||||
except ValueError:
|
||||
return -1
|
||||
|
||||
def slow_index(seq, k):
|
||||
'Location of match or -1 if not found'
|
||||
for i, item in enumerate(seq):
|
||||
if item == k:
|
||||
return i
|
||||
return -1
|
||||
|
||||
def slow_find(seq, k):
|
||||
'First item with a key equal to k. -1 if not found'
|
||||
for item in seq:
|
||||
if item == k:
|
||||
return item
|
||||
return -1
|
||||
|
||||
def slow_find_le(seq, k):
|
||||
'Last item with a key less-than or equal to k.'
|
||||
for item in reversed(seq):
|
||||
if item <= k:
|
||||
return item
|
||||
return -1
|
||||
|
||||
def slow_find_lt(seq, k):
|
||||
'Last item with a key less-than k.'
|
||||
for item in reversed(seq):
|
||||
if item < k:
|
||||
return item
|
||||
return -1
|
||||
|
||||
def slow_find_ge(seq, k):
|
||||
'First item with a key-value greater-than or equal to k.'
|
||||
for item in seq:
|
||||
if item >= k:
|
||||
return item
|
||||
return -1
|
||||
|
||||
def slow_find_gt(seq, k):
|
||||
'First item with a key-value greater-than or equal to k.'
|
||||
for item in seq:
|
||||
if item > k:
|
||||
return item
|
||||
return -1
|
||||
|
||||
from random import choice
|
||||
pool = [1.5, 2, 2.0, 3, 3.0, 3.5, 4, 4.0, 4.5]
|
||||
for i in range(500):
|
||||
for n in range(6):
|
||||
s = [choice(pool) for i in range(n)]
|
||||
sc = SortedCollection(s)
|
||||
s.sort()
|
||||
for probe in pool:
|
||||
assert repr(ve2no(sc.index, probe)) == repr(slow_index(s, probe))
|
||||
assert repr(ve2no(sc.find, probe)) == repr(slow_find(s, probe))
|
||||
assert repr(ve2no(sc.find_le, probe)) == repr(slow_find_le(s, probe))
|
||||
assert repr(ve2no(sc.find_lt, probe)) == repr(slow_find_lt(s, probe))
|
||||
assert repr(ve2no(sc.find_ge, probe)) == repr(slow_find_ge(s, probe))
|
||||
assert repr(ve2no(sc.find_gt, probe)) == repr(slow_find_gt(s, probe))
|
||||
for i, item in enumerate(s):
|
||||
assert repr(item) == repr(sc[i]) # test __getitem__
|
||||
assert item in sc # test __contains__ and __iter__
|
||||
assert s.count(item) == sc.count(item) # test count()
|
||||
assert len(sc) == n # test __len__
|
||||
assert list(map(repr, reversed(sc))) == list(map(repr, reversed(s))) # test __reversed__
|
||||
assert list(sc.copy()) == list(sc) # test copy()
|
||||
sc.clear() # test clear()
|
||||
assert len(sc) == 0
|
||||
|
||||
sd = SortedCollection('The quick Brown Fox jumped'.split(), key=str.lower)
|
||||
assert sd._keys == ['brown', 'fox', 'jumped', 'quick', 'the']
|
||||
assert sd._items == ['Brown', 'Fox', 'jumped', 'quick', 'The']
|
||||
assert sd._key == str.lower
|
||||
assert repr(sd) == "SortedCollection(['Brown', 'Fox', 'jumped', 'quick', 'The'], key=lower)"
|
||||
sd.key = str.upper
|
||||
assert sd._key == str.upper
|
||||
assert len(sd) == 5
|
||||
assert list(reversed(sd)) == ['The', 'quick', 'jumped', 'Fox', 'Brown']
|
||||
for item in sd:
|
||||
assert item in sd
|
||||
for i, item in enumerate(sd):
|
||||
assert item == sd[i]
|
||||
sd.insert('jUmPeD')
|
||||
sd.insert_right('QuIcK')
|
||||
assert sd._keys ==['BROWN', 'FOX', 'JUMPED', 'JUMPED', 'QUICK', 'QUICK', 'THE']
|
||||
assert sd._items == ['Brown', 'Fox', 'jUmPeD', 'jumped', 'quick', 'QuIcK', 'The']
|
||||
assert sd.find_le('JUMPED') == 'jumped', sd.find_le('JUMPED')
|
||||
assert sd.find_ge('JUMPED') == 'jUmPeD'
|
||||
assert sd.find_le('GOAT') == 'Fox'
|
||||
assert sd.find_ge('GOAT') == 'jUmPeD'
|
||||
assert sd.find('FOX') == 'Fox'
|
||||
assert sd[3] == 'jumped'
|
||||
assert sd[3:5] ==['jumped', 'quick']
|
||||
assert sd[-2] == 'QuIcK'
|
||||
assert sd[-4:-2] == ['jumped', 'quick']
|
||||
for i, item in enumerate(sd):
|
||||
assert sd.index(item) == i
|
||||
try:
|
||||
sd.index('xyzpdq')
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
assert 0, 'Oops, failed to notify of missing value'
|
||||
sd.remove('jumped')
|
||||
assert list(sd) == ['Brown', 'Fox', 'jUmPeD', 'quick', 'QuIcK', 'The']
|
||||
|
||||
import doctest
|
||||
from operator import itemgetter
|
||||
print(doctest.testmod())
|
||||
106
eris/terminal.py
106
eris/terminal.py
|
|
@ -1,106 +0,0 @@
|
|||
|
||||
|
||||
import contextlib
|
||||
import sys
|
||||
import termios
|
||||
|
||||
|
||||
ESC = "\x1b"
|
||||
MOUSE = ESC + "[M"
|
||||
|
||||
normal = "[m"
|
||||
bold = "[1m"
|
||||
italic = "[3m"
|
||||
standout = "[7m"
|
||||
underline = "[4m"
|
||||
|
||||
UP_KEY = ESC + "[A"
|
||||
DOWN_KEY = ESC + "[B"
|
||||
RIGHT_KEY = ESC + "[C"
|
||||
LEFT_KEY = ESC + "[D"
|
||||
PAGE_UP_KEY = ESC + "[5~"
|
||||
PAGE_DOWN_KEY = ESC + "[6~"
|
||||
HOME_KEY = ESC + "[H"
|
||||
END_KEY = ESC + "[F"
|
||||
|
||||
|
||||
def color(color_number, is_foreground):
|
||||
return f"[{'38' if is_foreground else '48'};5;{color_number:d}m"
|
||||
|
||||
|
||||
def rgb_color(rgb, is_foreground):
|
||||
return f"[{'38' if is_foreground else '48'};2;" + "%i;%i;%im" % rgb
|
||||
|
||||
|
||||
def move(x, y):
|
||||
return ESC + f"[{y + 1:d};{x + 1:d}H"
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def terminal_title(title):
|
||||
sys.stdout.write(ESC + "7") # save
|
||||
sys.stdout.write(f"\033]0;{title}\007") # set title
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
sys.stdout.write(ESC + "8") # restore
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def mouse_tracking():
|
||||
sys.stdout.write(ESC + "[?1000h" + ESC + "[?1002h") # tracking on
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
sys.stdout.write(ESC + "[?1002l" + ESC + "[?1000l") # tracking off
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def alternate_buffer():
|
||||
sys.stdout.write(ESC + "[?1049h") # switch to alternate buffer
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
sys.stdout.write(ESC + "[?1049l") # restore normal buffer
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def interactive():
|
||||
old_termios_settings = termios.tcgetattr(sys.stdin)
|
||||
new_settings = termios.tcgetattr(sys.stdin)
|
||||
new_settings[3] = new_settings[3] & ~termios.ECHO & ~termios.ICANON
|
||||
new_settings[6][termios.VMIN] = 0
|
||||
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, new_settings)
|
||||
sys.stdout.write(ESC + "[?1l") # Ensure normal cursor key codes
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_termios_settings)
|
||||
|
||||
|
||||
RELEASE_MOUSE = 0
|
||||
DRAG_MOUSE = 1
|
||||
CLICK_MOUSE = 2
|
||||
PRESS_MOUSE = 3
|
||||
|
||||
WHEEL_UP_MOUSE = 4
|
||||
WHEEL_DOWN_MOUSE = 5
|
||||
|
||||
|
||||
def decode_mouse_input(term_code):
|
||||
keys = [ord(byte) for byte in term_code]
|
||||
b = keys[0] - 32
|
||||
x, y = (keys[1] - 33) % 256, (keys[2] - 33) % 256
|
||||
button = ((b & 64) / 64 * 3) + (b & 3) + 1
|
||||
if b & 3 == 3:
|
||||
action = RELEASE_MOUSE
|
||||
button = 0
|
||||
elif b & 2048:
|
||||
action = RELEASE_MOUSE
|
||||
elif b & 32:
|
||||
action = DRAG_MOUSE
|
||||
elif b & 1536:
|
||||
action = CLICK_MOUSE
|
||||
else:
|
||||
action = PRESS_MOUSE
|
||||
return (action, button, x, y)
|
||||
386
eris/termstr.py
386
eris/termstr.py
|
|
@ -1,386 +0,0 @@
|
|||
|
||||
|
||||
import collections
|
||||
import functools
|
||||
import html
|
||||
import itertools
|
||||
import os
|
||||
import weakref
|
||||
|
||||
import pygments.formatters.terminal256
|
||||
import cwcwidth
|
||||
|
||||
import eris.ColorMap
|
||||
import eris.terminal as terminal
|
||||
|
||||
|
||||
xterm_colormap = eris.ColorMap.XTermColorMap()
|
||||
|
||||
|
||||
@functools.lru_cache()
|
||||
def xterm_color_to_rgb(color_index):
|
||||
return eris.ColorMap._rgb(xterm_colormap.colors[color_index])
|
||||
|
||||
|
||||
class Color:
|
||||
|
||||
# https://en.wikipedia.org/wiki/Natural_Color_System
|
||||
black = (0, 0, 0)
|
||||
white = (255, 255, 255)
|
||||
red = (196, 2, 51)
|
||||
green = (0, 159, 107)
|
||||
dark_green = (0, 119, 80)
|
||||
blue = (0, 135, 189)
|
||||
lime = (0, 255, 0)
|
||||
yellow = (255, 211, 0)
|
||||
grey_30 = (30, 30, 30)
|
||||
grey_50 = (50, 50, 50)
|
||||
grey_80 = (80, 80, 80)
|
||||
grey_100 = (100, 100, 100)
|
||||
grey_150 = (150, 150, 150)
|
||||
grey_200 = (200, 200, 200)
|
||||
light_blue = (173, 216, 230)
|
||||
purple = (200, 0, 200)
|
||||
brown = (150, 75, 0)
|
||||
orange = (255, 153, 0)
|
||||
|
||||
|
||||
class CharStyle:
|
||||
|
||||
_POOL = weakref.WeakValueDictionary()
|
||||
_TERMINAL256_FORMATTER = \
|
||||
pygments.formatters.terminal256.Terminal256Formatter()
|
||||
|
||||
def __new__(cls, fg_color=None, bg_color=None, is_bold=False,
|
||||
is_italic=False, is_underlined=False):
|
||||
if fg_color is None:
|
||||
fg_color = Color.white
|
||||
if bg_color is None:
|
||||
bg_color = Color.black
|
||||
key = (fg_color, bg_color, is_bold, is_italic, is_underlined)
|
||||
try:
|
||||
return CharStyle._POOL[key]
|
||||
except KeyError:
|
||||
obj = object.__new__(cls)
|
||||
obj.fg_color, obj.bg_color, obj.is_bold, obj.is_italic, \
|
||||
obj.is_underlined = key
|
||||
return CharStyle._POOL.setdefault(key, obj)
|
||||
|
||||
def __getnewargs__(self):
|
||||
return (self.fg_color, self.bg_color, self.is_bold, self.is_italic,
|
||||
self.is_underlined)
|
||||
|
||||
def __getstate__(self):
|
||||
state = self.__dict__.copy()
|
||||
if "_cache" in state:
|
||||
del state["_cache"]
|
||||
return state
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.__dict__ = state
|
||||
|
||||
def __repr__(self):
|
||||
attributes = []
|
||||
if self.is_bold:
|
||||
attributes.append("b")
|
||||
if self.is_italic:
|
||||
attributes.append("i")
|
||||
if self.is_underlined:
|
||||
attributes.append("u")
|
||||
return (f"<CharStyle: fg:{self.fg_color} bg:{self.bg_color}"
|
||||
f" attr:{','.join(attributes)}>")
|
||||
|
||||
def _color_code(self, color, is_foreground):
|
||||
if isinstance(color, int):
|
||||
return terminal.color(color, is_foreground)
|
||||
else: # true color
|
||||
if os.environ.get("TERM", None) == "xterm":
|
||||
closest_color = self._TERMINAL256_FORMATTER._closest_color(
|
||||
*color)
|
||||
return terminal.color(closest_color, is_foreground)
|
||||
else:
|
||||
return terminal.rgb_color(color, is_foreground)
|
||||
|
||||
@functools.cached_property
|
||||
def code_for_term(self):
|
||||
fg_termcode = terminal.ESC + self._color_code(self.fg_color, True)
|
||||
bg_termcode = terminal.ESC + self._color_code(self.bg_color, False)
|
||||
bold_code = (terminal.ESC + terminal.bold) if self.is_bold else ""
|
||||
italic_code = ((terminal.ESC + terminal.italic)
|
||||
if self.is_italic else "")
|
||||
underline_code = ((terminal.ESC + terminal.underline)
|
||||
if self.is_underlined else "")
|
||||
return "".join([terminal.ESC, terminal.normal, fg_termcode,
|
||||
bg_termcode, bold_code, italic_code, underline_code])
|
||||
|
||||
def as_html(self):
|
||||
bold_code = "font-weight:bold; " if self.is_bold else ""
|
||||
italic_code = "font-style:italic; " if self.is_italic else ""
|
||||
underline_code = ("text-decoration:underline; "
|
||||
if self.is_underlined else "")
|
||||
fg_color = (self.fg_color if type(self.fg_color) == tuple
|
||||
else xterm_color_to_rgb(self.fg_color))
|
||||
bg_color = (self.bg_color if type(self.bg_color) == tuple
|
||||
else xterm_color_to_rgb(self.bg_color))
|
||||
return (f"<style>.S{id(self)} {{font-size:80%%; color:rgb{fg_color!r};"
|
||||
f" background-color:rgb{bg_color!r}; "
|
||||
f"{bold_code}{italic_code}{underline_code}}}</style>")
|
||||
|
||||
|
||||
def _join_lists(lists):
|
||||
return list(itertools.chain.from_iterable(lists))
|
||||
|
||||
|
||||
_ZERO_WIDTH_SPACE = "\u200b"
|
||||
|
||||
|
||||
def _pad_wide_chars(str_):
|
||||
return "".join(f"{char}{_ZERO_WIDTH_SPACE}"
|
||||
if cwcwidth.wcwidth(char) == 2 else char for char in str_)
|
||||
|
||||
|
||||
class TermStr(collections.UserString):
|
||||
|
||||
def __init__(self, data, style=CharStyle()):
|
||||
if isinstance(style, tuple):
|
||||
self.data = data
|
||||
self.style = style
|
||||
else:
|
||||
try:
|
||||
self.data, self.style = data.data, data.style
|
||||
except AttributeError:
|
||||
self.data = _pad_wide_chars(data)
|
||||
self.style = (style,) * len(self.data)
|
||||
|
||||
@classmethod
|
||||
def from_term(cls, data):
|
||||
data = data.expandtabs(tabsize=4)
|
||||
parts = data.split(terminal.ESC)
|
||||
fg_color, bg_color = None, None
|
||||
is_bold, is_italic, is_underlined = False, False, False
|
||||
result_parts = [parts[0]]
|
||||
for part in parts[1:]:
|
||||
if part.startswith("[K"):
|
||||
end_index = part.index("K")
|
||||
codes = []
|
||||
else:
|
||||
try:
|
||||
end_index = part.index("m")
|
||||
except ValueError:
|
||||
continue
|
||||
codes = part[1:end_index].split(";")
|
||||
previous_code = None
|
||||
for index, code in enumerate(codes):
|
||||
try:
|
||||
code_int = int(code)
|
||||
except ValueError:
|
||||
code_int = None
|
||||
if code in ["", "0", "00"]: # Normal
|
||||
is_bold, is_italic, is_underlined = False, False, False
|
||||
fg_color, bg_color = None, None
|
||||
elif code in ["01", "1"]: # bold
|
||||
is_bold = True
|
||||
elif code in ["03", "3"]: # italic
|
||||
is_italic = True
|
||||
elif code in ["04", "4"]: # underline
|
||||
is_underlined = True
|
||||
elif code_int and 30 <= code_int <= 37 : # dim fg color
|
||||
fg_color = int(code[1])
|
||||
elif code_int and 40 <= code_int <= 47: # dim bg color
|
||||
bg_color = int(code[1])
|
||||
elif code_int and 90 <= code_int <= 97: # high fg color
|
||||
fg_color = int(code[1]) + 8
|
||||
elif code_int and 100 <= code_int <= 107: # high bg color
|
||||
bg_color = int(code[2]) + 8
|
||||
elif code == "5" and previous_code == "38": # simple fg color
|
||||
fg_color = int(codes[index+1])
|
||||
codes[index+1:index+2] = []
|
||||
elif code == "5" and previous_code == "48": # simple bg color
|
||||
bg_color = int(codes[index+1])
|
||||
codes[index+1:index+2] = []
|
||||
elif code == "2" and previous_code == "38": # rgb fg color
|
||||
fg_color = tuple(int(component)
|
||||
for component in codes[index+1:index+4])
|
||||
codes[index+1:index+4] = []
|
||||
elif code == "2" and previous_code == "48": # rgb bg color
|
||||
bg_color = tuple(int(component)
|
||||
for component in codes[index+1:index+4])
|
||||
codes[index+1:index+4] = []
|
||||
previous_code = code
|
||||
result_parts.append(cls(part[end_index+1:],
|
||||
CharStyle(fg_color, bg_color, is_bold,
|
||||
is_italic, is_underlined)))
|
||||
return cls("").join(result_parts)
|
||||
|
||||
def __eq__(self, other):
|
||||
return (self is other or
|
||||
(isinstance(other, self.__class__) and
|
||||
self.data == other.data and self.style == other.style))
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.data, self.style))
|
||||
|
||||
@functools.cached_property
|
||||
def _partition_style(self):
|
||||
if self.data == "":
|
||||
return []
|
||||
last_style, last_index = None, 0
|
||||
result = []
|
||||
for index, style in enumerate(self.style):
|
||||
if style != last_style:
|
||||
if last_style is not None:
|
||||
result.append((last_style, last_index, index))
|
||||
last_style, last_index = style, index
|
||||
result.append((last_style, last_index, len(self.style)))
|
||||
return result
|
||||
|
||||
def __str__(self):
|
||||
return "".join(_join_lists(
|
||||
[style.code_for_term, self.data[start_index:end_index]]
|
||||
for style, start_index, end_index in self._partition_style) +
|
||||
[terminal.ESC + terminal.normal])
|
||||
|
||||
def __repr__(self):
|
||||
return f"<TermStr: {self.data!r}>"
|
||||
|
||||
def __add__(self, other):
|
||||
if isinstance(other, str):
|
||||
other = TermStr(other)
|
||||
return self.__class__(self.data + other.data, self.style + other.style)
|
||||
|
||||
def __radd__(self, other):
|
||||
if isinstance(other, str):
|
||||
other = TermStr(other)
|
||||
return self.__class__(other.data + self.data, other.style + self.style)
|
||||
|
||||
def __mul__(self, n):
|
||||
return self.__class__(self.data*n, self.style*n)
|
||||
__rmul__ = __mul__
|
||||
|
||||
def __getitem__(self, index):
|
||||
data = self.data[index]
|
||||
if len(data) == 0:
|
||||
result = ""
|
||||
else:
|
||||
first_char = " " if data[0] == _ZERO_WIDTH_SPACE else data[0]
|
||||
if len(data) == 1:
|
||||
result = first_char
|
||||
else:
|
||||
end_char = " " if cwcwidth.wcwidth(data[-1]) == 2 else data[-1]
|
||||
result = first_char + data[1:-1] + end_char
|
||||
return self.__class__(result, self.style[index])
|
||||
|
||||
def join(self, parts):
|
||||
parts = [TermStr(part) if isinstance(part, str) else part
|
||||
for part in parts]
|
||||
joined_style = _join_lists(self.style + part.style for part in parts)
|
||||
return self.__class__(self.data.join(part.data for part in parts),
|
||||
tuple(joined_style[len(self.style):]))
|
||||
|
||||
def _split_style(self, parts, sep_length):
|
||||
result = []
|
||||
cursor = 0
|
||||
for part in parts:
|
||||
style_part = self.style[cursor:cursor+len(part)]
|
||||
result.append(self.__class__(part, style_part))
|
||||
cursor += (len(part) + sep_length)
|
||||
return result
|
||||
|
||||
def split(self, sep=None, maxsplit=-1):
|
||||
return self._split_style(self.data.split(sep, maxsplit), len(sep))
|
||||
|
||||
def splitlines(self, keepends=0):
|
||||
result = []
|
||||
cursor = 0
|
||||
for line in self.data.splitlines(keepends=True):
|
||||
result_line = line if keepends else line.rstrip("\r\n")
|
||||
style_part = self.style[cursor:cursor+len(result_line)]
|
||||
result.append(self.__class__(result_line, style_part))
|
||||
cursor += len(line)
|
||||
return result
|
||||
|
||||
def capitalize(self):
|
||||
return self.__class__(self.data.capitalize(), self.style)
|
||||
|
||||
def lower(self):
|
||||
return self.__class__(self.data.lower(), self.style)
|
||||
|
||||
def swapcase(self):
|
||||
return self.__class__(self.data.swapcase(), self.style)
|
||||
|
||||
def title(self):
|
||||
return self.__class__(self.data.title(), self.style)
|
||||
|
||||
def upper(self):
|
||||
return self.__class__(self.data.upper(), self.style)
|
||||
|
||||
def ljust(self, width, fillchar=" "):
|
||||
return self + self.__class__(fillchar * (width - len(self.data)))
|
||||
|
||||
def rjust(self, width, fillchar=" "):
|
||||
return self.__class__(fillchar * (width - len(self.data))) + self
|
||||
|
||||
def center(self, width, fillchar=" "):
|
||||
left_width = (width - len(self.data)) // 2
|
||||
if left_width < 1:
|
||||
return self
|
||||
return (self.__class__(fillchar * left_width) + self +
|
||||
self.__class__(fillchar *
|
||||
(width - left_width - len(self.data))))
|
||||
|
||||
# Below are extra methods useful for termstrs.
|
||||
|
||||
def transform_style(self, transform_func):
|
||||
new_style = tuple(_join_lists(
|
||||
[transform_func(style)] * (end_index - start_index)
|
||||
for style, start_index, end_index in self._partition_style))
|
||||
return self.__class__(self.data, new_style)
|
||||
|
||||
def bold(self):
|
||||
def make_bold(style):
|
||||
return CharStyle(style.fg_color, style.bg_color, is_bold=True,
|
||||
is_italic=style.is_italic,
|
||||
is_underlined=style.is_underlined)
|
||||
return self.transform_style(make_bold)
|
||||
|
||||
def underline(self):
|
||||
def make_underlined(style):
|
||||
return CharStyle(style.fg_color, style.bg_color,
|
||||
is_bold=style.is_bold, is_italic=style.is_italic,
|
||||
is_underlined=True)
|
||||
return self.transform_style(make_underlined)
|
||||
|
||||
def italic(self):
|
||||
def make_italic(style):
|
||||
return CharStyle(style.fg_color, style.bg_color,
|
||||
is_bold=style.is_bold, is_italic=True,
|
||||
is_underlined=style.is_underlined)
|
||||
return self.transform_style(make_italic)
|
||||
|
||||
def fg_color(self, fg_color):
|
||||
def set_fgcolor(style):
|
||||
return CharStyle(fg_color, style.bg_color, is_bold=style.is_bold,
|
||||
is_italic=style.is_italic,
|
||||
is_underlined=style.is_underlined)
|
||||
return self.transform_style(set_fgcolor)
|
||||
|
||||
def bg_color(self, bg_color):
|
||||
def set_bgcolor(style):
|
||||
return CharStyle(style.fg_color, bg_color, is_bold=style.is_bold,
|
||||
is_italic=style.is_italic,
|
||||
is_underlined=style.is_underlined)
|
||||
return self.transform_style(set_bgcolor)
|
||||
|
||||
def as_html(self):
|
||||
result = []
|
||||
styles = set()
|
||||
for style, start_index, end_index in self._partition_style:
|
||||
styles.add(style)
|
||||
encoded = str(html.escape(self.data[start_index:end_index]).encode(
|
||||
"ascii", "xmlcharrefreplace"))[2:-1]
|
||||
encoded = encoded.replace("\\\\", "\\")
|
||||
result.append(f'<span class="S{id(style):d}">{encoded}</span>')
|
||||
return "".join(result), styles
|
||||
|
|
@ -29,9 +29,9 @@ import pygments.styles
|
|||
import toml
|
||||
|
||||
import eris
|
||||
import eris.fill3 as fill3
|
||||
import eris.lscolors as lscolors
|
||||
import eris.termstr as termstr
|
||||
import fill3
|
||||
import lscolors
|
||||
import termstr.termstr as termstr
|
||||
|
||||
|
||||
PYTHON_VERSION = "3.9"
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import os
|
|||
import sys
|
||||
import pickle
|
||||
|
||||
import eris.fill3 as fill3
|
||||
import eris.tools as tools
|
||||
import fill3
|
||||
|
||||
|
||||
USAGE = """Usage:
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ import contextlib
|
|||
import os
|
||||
import signal
|
||||
|
||||
import eris.fill3 as fill3
|
||||
import eris.tools as tools
|
||||
import eris.paged_list
|
||||
import fill3
|
||||
|
||||
|
||||
class Worker:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue