Coding style.

- Increase maximum line length from 80 to 100.
This commit is contained in:
Andrew Hamilton 2021-11-29 12:51:34 +10:00
parent 75a028272d
commit 71b9da128b
15 changed files with 404 additions and 696 deletions

View file

@ -90,8 +90,7 @@ class Entry:
MAX_WIDTH = 0 MAX_WIDTH = 0
def __init__(self, path, results, change_time, highlighted=None, def __init__(self, path, results, change_time, highlighted=None, set_results=True):
set_results=True):
self.path = path self.path = path
self.change_time = change_time self.change_time = change_time
self.highlighted = highlighted self.highlighted = highlighted
@ -114,8 +113,7 @@ class Entry:
return self.results[index] return self.results[index]
def appearance_min(self): def appearance_min(self):
if self.appearance_cache is None \ if self.appearance_cache is None or self.last_width != Entry.MAX_WIDTH:
or self.last_width != Entry.MAX_WIDTH:
self.last_width = Entry.MAX_WIDTH self.last_width = Entry.MAX_WIDTH
if self.highlighted is not None: if self.highlighted is not None:
self.results[self.highlighted].is_highlighted = True self.results[self.highlighted].is_highlighted = True
@ -147,8 +145,7 @@ def is_path_excluded(path):
def codebase_files(path, skip_hidden_directories=True): def codebase_files(path, skip_hidden_directories=True):
for (dirpath, dirnames, filenames) in os.walk(path): for (dirpath, dirnames, filenames) in os.walk(path):
if skip_hidden_directories: if skip_hidden_directories:
filtered_dirnames = [dirname for dirname in dirnames filtered_dirnames = [dirname for dirname in dirnames if not is_path_excluded(dirname)]
if not is_path_excluded(dirname)]
dirnames[:] = filtered_dirnames dirnames[:] = filtered_dirnames
for filename in filenames: for filename in filenames:
if not is_path_excluded(filename): if not is_path_excluded(filename):
@ -179,9 +176,8 @@ def highlight_str(line, highlight_color, transparency):
else termstr.XTERM_COLORS[style.bg_color]) else termstr.XTERM_COLORS[style.bg_color])
return termstr.CharStyle( return termstr.CharStyle(
blend_color(fg_color, highlight_color, transparency), blend_color(fg_color, highlight_color, transparency),
blend_color(bg_color, highlight_color, transparency), blend_color(bg_color, highlight_color, transparency), is_bold=style.is_bold,
is_bold=style.is_bold, is_italic=style.is_italic, is_italic=style.is_italic, is_underlined=style.is_underlined)
is_underlined=style.is_underlined)
return termstr.TermStr(line).transform_style(blend_style) return termstr.TermStr(line).transform_style(blend_style)
@ -194,14 +190,12 @@ _UP, _DOWN, _LEFT, _RIGHT = (0, -1), (0, 1), (-1, 0), (1, 0)
def directory_sort(entry): def directory_sort(entry):
path = entry.path path = entry.path
return (os.path.dirname(path), tools.splitext(path)[1], return (os.path.dirname(path), tools.splitext(path)[1], os.path.basename(path))
os.path.basename(path))
def type_sort(entry): def type_sort(entry):
path = entry.path path = entry.path
return (tools.splitext(path)[1], os.path.dirname(path), return (tools.splitext(path)[1], os.path.dirname(path), os.path.basename(path))
os.path.basename(path))
class Summary: class Summary:
@ -235,12 +229,10 @@ class Summary:
if y == 0: if y == 0:
entries = [] entries = []
else: else:
entries = itertools.chain( entries = itertools.chain([self._entries[y]], itertools.islice(self._entries, y),
[self._entries[y]], itertools.islice(self._entries, y), itertools.islice(self._entries, y+1, None))
itertools.islice(self._entries, y+1, None)) state["_old_entries"] = paged_list.PagedList(entries, summary_path, 2000, 1,
state["_old_entries"] = paged_list.PagedList( exist_ok=True, open_func=open_compressed)
entries, summary_path, 2000, 1, exist_ok=True,
open_func=open_compressed)
state["_entries"] = None state["_entries"] = None
state["__cursor_position"] = (x, 0) state["__cursor_position"] = (x, 0)
return state return state
@ -261,8 +253,7 @@ class Summary:
def sort_entries(self): def sort_entries(self):
key_func = directory_sort if self.is_directory_sort else type_sort key_func = directory_sort if self.is_directory_sort else type_sort
self._entries = sorted_collection.SortedCollection( self._entries = sorted_collection.SortedCollection(self._entries, key=key_func)
self._entries, key=key_func)
self.closest_placeholder_generator = None self.closest_placeholder_generator = None
def add_entry(self, entry): def add_entry(self, entry):
@ -273,8 +264,7 @@ class Summary:
if result.is_completed: if result.is_completed:
self.completed_total += 1 self.completed_total += 1
Entry.MAX_WIDTH = max(len(entry), Entry.MAX_WIDTH) Entry.MAX_WIDTH = max(len(entry), Entry.MAX_WIDTH)
self._max_path_length = max(len(entry.path) - len("./"), self._max_path_length = max(len(entry.path) - len("./"), self._max_path_length)
self._max_path_length)
entry_index = self._entries.insert(entry) entry_index = self._entries.insert(entry)
x, y = self._cursor_position x, y = self._cursor_position
if entry_index <= y: if entry_index <= y:
@ -313,11 +303,10 @@ class Summary:
del self._entries._keys[index] del self._entries._keys[index]
del self._entries._items[index] del self._entries._items[index]
if len(row) == Entry.MAX_WIDTH: if len(row) == Entry.MAX_WIDTH:
Entry.MAX_WIDTH = max((len(entry) for entry in self._entries), Entry.MAX_WIDTH = max((len(entry) for entry in self._entries), default=0)
default=0)
if (len(path) - 2) == self._max_path_length: if (len(path) - 2) == self._max_path_length:
self._max_path_length = max( self._max_path_length = max(((len(entry.path) - 2) for entry in self._entries),
((len(entry.path) - 2) for entry in self._entries), default=0) default=0)
x, y = self._cursor_position x, y = self._cursor_position
if y == len(self._entries): if y == len(self._entries):
self._cursor_position = x, y - 1 self._cursor_position = x, y - 1
@ -370,8 +359,7 @@ class Summary:
log.log_message("Started sync with filesystem…") log.log_message("Started sync with filesystem…")
start_time = time.time() start_time = time.time()
all_paths = set() all_paths = set()
for path in fix_paths(self._root_path, for path in fix_paths(self._root_path, codebase_files(self._root_path)):
codebase_files(self._root_path)):
await asyncio.sleep(0) await asyncio.sleep(0)
all_paths.add(path) all_paths.add(path)
if path in cache: if path in cache:
@ -388,8 +376,7 @@ class Summary:
await asyncio.sleep(0) await asyncio.sleep(0)
self.on_file_deleted(path) self.on_file_deleted(path)
duration = time.time() - start_time duration = time.time() - start_time
log.log_message(f"Finished sync with filesystem. " log.log_message(f"Finished sync with filesystem. {round(duration, 2)} secs")
f"{round(duration, 2)} secs")
def _sweep_up(self, x, y): def _sweep_up(self, x, y):
yield from reversed(self._entries[y][:x]) yield from reversed(self._entries[y][:x])
@ -404,8 +391,7 @@ class Summary:
yield from self._entries[y] yield from self._entries[y]
def _sweep_combined(self, x, y): def _sweep_combined(self, x, y):
for up_result, down_result in zip(self._sweep_up(x, y), for up_result, down_result in zip(self._sweep_up(x, y), self._sweep_down(x, y)):
self._sweep_down(x, y)):
yield down_result yield down_result
yield up_result yield up_result
@ -439,8 +425,7 @@ class Summary:
return appearance return appearance
def _set_scroll_position(self, cursor_x, cursor_y, summary_height): def _set_scroll_position(self, cursor_x, cursor_y, summary_height):
scroll_x, scroll_y = new_scroll_x, new_scroll_y = \ scroll_x, scroll_y = new_scroll_x, new_scroll_y = self._view_widget.position
self._view_widget.position
if cursor_y < scroll_y: if cursor_y < scroll_y:
new_scroll_y = max(cursor_y - summary_height + 1, 0) new_scroll_y = max(cursor_y - summary_height + 1, 0)
if (scroll_y + summary_height - 1) < cursor_y: if (scroll_y + summary_height - 1) < cursor_y:
@ -450,8 +435,8 @@ class Summary:
def _highlight_cursor_row(self, appearance, cursor_y): def _highlight_cursor_row(self, appearance, cursor_y):
scroll_x, scroll_y = self._view_widget.position scroll_x, scroll_y = self._view_widget.position
highlighted_y = cursor_y - scroll_y highlighted_y = cursor_y - scroll_y
appearance[highlighted_y] = (highlight_str( appearance[highlighted_y] = (highlight_str(appearance[highlighted_y][:-1],
appearance[highlighted_y][:-1], termstr.Color.white, 0.8) termstr.Color.white, 0.8)
+ appearance[highlighted_y][-1]) + appearance[highlighted_y][-1])
return appearance return appearance
@ -462,8 +447,7 @@ class Summary:
cursor_x, cursor_y = self.cursor_position() cursor_x, cursor_y = self.cursor_position()
width, height = width - 1, height - 1 # Minus one for the scrollbars width, height = width - 1, height - 1 # Minus one for the scrollbars
self._set_scroll_position(cursor_x, cursor_y, height) self._set_scroll_position(cursor_x, cursor_y, height)
return self._highlight_cursor_row( return self._highlight_cursor_row(self._view_widget.appearance(dimensions), cursor_y)
self._view_widget.appearance(dimensions), cursor_y)
def scroll(self, dx, dy): def scroll(self, dx, dy):
scroll_x, scroll_y = self._view_widget.position scroll_x, scroll_y = self._view_widget.position
@ -530,8 +514,7 @@ class Summary:
row = self._entries[row_index] row = self._entries[row_index]
for index_x, result in enumerate(row): for index_x, result in enumerate(row):
if (result.status == tools.Status.problem and if (result.status == tools.Status.problem and
not (row_index == y and index_x <= x and not (row_index == y and index_x <= x and index != len(self._entries))):
index != len(self._entries))):
yield result, (index_x, row_index) yield result, (index_x, row_index)
def move_to_next_issue(self): def move_to_next_issue(self):
@ -594,13 +577,11 @@ class Log:
def log_message(self, message, timestamp=None, char_style=None): def log_message(self, message, timestamp=None, char_style=None):
if isinstance(message, list): if isinstance(message, list):
message = [part[1] if isinstance(part, tuple) else part message = [part[1] if isinstance(part, tuple) else part for part in message]
for part in message]
message = fill3.join("", message) message = fill3.join("", message)
if char_style is not None: if char_style is not None:
message = termstr.TermStr(message, char_style) message = termstr.TermStr(message, char_style)
timestamp = (time.strftime("%H:%M:%S", time.localtime()) timestamp = time.strftime("%H:%M:%S", time.localtime()) if timestamp is None else timestamp
if timestamp is None else timestamp)
line = termstr.TermStr(timestamp, Log._GREY_BOLD_STYLE) + " " + message line = termstr.TermStr(timestamp, Log._GREY_BOLD_STYLE) + " " + message
if not sys.stdout.isatty(): if not sys.stdout.isatty():
print(line, flush=True) print(line, flush=True)
@ -613,8 +594,7 @@ class Log:
self.log_message(message, char_style=Log._GREEN_STYLE) self.log_message(message, char_style=Log._GREEN_STYLE)
def appearance(self, dimensions): def appearance(self, dimensions):
if self._appearance is None or \ if self._appearance is None or fill3.appearance_dimensions(self._appearance) != dimensions:
fill3.appearance_dimensions(self._appearance) != dimensions:
width, height = dimensions width, height = dimensions
del self.lines[:-height] del self.lines[:-height]
self._appearance = fill3.appearance_resize(self.lines, dimensions) self._appearance = fill3.appearance_resize(self.lines, dimensions)
@ -629,9 +609,8 @@ def highlight_chars(str_, style, marker="*"):
def get_status_help(): def get_status_help():
return fill3.join("\n", ["Statuses:"] + return fill3.join("\n", ["Statuses:"] + [" " + tools.STATUS_TO_TERMSTR[status] + " " + meaning
[" " + tools.STATUS_TO_TERMSTR[status] + " " + meaning for status, meaning in tools.STATUS_MEANINGS])
for status, meaning in tools.STATUS_MEANINGS])
class Help: class Help:
@ -645,8 +624,7 @@ class Help:
portal = self.view.portal portal = self.view.portal
self.key_map = { self.key_map = {
"h": self._exit_help, terminal.UP_KEY: portal.scroll_up, "h": self._exit_help, terminal.UP_KEY: portal.scroll_up,
terminal.DOWN_KEY: portal.scroll_down, terminal.DOWN_KEY: portal.scroll_down, terminal.LEFT_KEY: portal.scroll_left,
terminal.LEFT_KEY: portal.scroll_left,
terminal.RIGHT_KEY: portal.scroll_right, "q": self._exit_help, terminal.RIGHT_KEY: portal.scroll_right, "q": self._exit_help,
terminal.ESC: self._exit_help} terminal.ESC: self._exit_help}
@ -663,8 +641,7 @@ class Help:
appearance_changed_event.set() appearance_changed_event.set()
def on_keyboard_input(self, term_code, appearance_changed_event): def on_keyboard_input(self, term_code, appearance_changed_event):
action = (self.key_map.get(term_code) or action = self.key_map.get(term_code) or self.key_map.get(term_code.lower())
self.key_map.get(term_code.lower()))
if action is not None: if action is not None:
action() action()
appearance_changed_event.set() appearance_changed_event.set()
@ -740,15 +717,13 @@ class Screen:
result_widget = fill3.Text("Nothing selected") result_widget = fill3.Text("Nothing selected")
self._view = fill3.View.from_widget(result_widget) self._view = fill3.View.from_widget(result_widget)
self._listing = fill3.Border(Listing(self._view)) self._listing = fill3.Border(Listing(self._view))
log = fill3.Border(self._log, title="Log", log = fill3.Border(self._log, title="Log", characters=Screen._DIMMED_BORDER)
characters=Screen._DIMMED_BORDER)
quarter_cut = functools.partial(self._partition, 25) quarter_cut = functools.partial(self._partition, 25)
golden_cut = functools.partial(self._partition, 38.198) golden_cut = functools.partial(self._partition, 38.198)
three_quarter_cut = functools.partial(self._partition, 75) three_quarter_cut = functools.partial(self._partition, 75)
port_log = fill3.Row([fill3.Column([summary, log], three_quarter_cut), port_log = fill3.Row([fill3.Column([summary, log], three_quarter_cut), self._listing],
self._listing], golden_cut) golden_cut)
land_log = fill3.Column([fill3.Row([summary, log]), self._listing], land_log = fill3.Column([fill3.Row([summary, log]), self._listing], quarter_cut)
quarter_cut)
port_no_log = fill3.Row([summary, self._listing], golden_cut) port_no_log = fill3.Row([summary, self._listing], golden_cut)
land_no_log = fill3.Column([summary, self._listing], quarter_cut) land_no_log = fill3.Column([summary, self._listing], quarter_cut)
self._layouts = [[land_no_log, port_no_log], [land_log, port_log]] self._layouts = [[land_no_log, port_no_log], [land_log, port_log]]
@ -769,8 +744,7 @@ class Screen:
x, y = selected_widget.scroll_position x, y = selected_widget.scroll_position
widget_width, widget_height = fill3.appearance_dimensions( widget_width, widget_height = fill3.appearance_dimensions(
selected_widget.result.appearance_min()) selected_widget.result.appearance_min())
listing_width, listing_height = (self._listing.widget. listing_width, listing_height = self._listing.widget.last_dimensions
last_dimensions)
listing_width -= 1 # scrollbars listing_width -= 1 # scrollbars
listing_height -= 1 listing_height -= 1
x = min(x + dx, max(widget_width - listing_width, 0)) x = min(x + dx, max(widget_width - listing_width, 0))
@ -780,42 +754,33 @@ class Screen:
selected_widget.scroll_position = x, y selected_widget.scroll_position = x, y
def cursor_up(self): def cursor_up(self):
(self._summary.cursor_up() if self._is_summary_focused self._summary.cursor_up() if self._is_summary_focused else self._move_listing(_UP)
else self._move_listing(_UP))
def cursor_down(self): def cursor_down(self):
(self._summary.cursor_down() if self._is_summary_focused self._summary.cursor_down() if self._is_summary_focused else self._move_listing(_DOWN)
else self._move_listing(_DOWN))
def cursor_right(self): def cursor_right(self):
(self._summary.cursor_right() if self._is_summary_focused self._summary.cursor_right() if self._is_summary_focused else self._move_listing(_RIGHT)
else self._move_listing(_RIGHT))
def cursor_left(self): def cursor_left(self):
(self._summary.cursor_left() if self._is_summary_focused self._summary.cursor_left() if self._is_summary_focused else self._move_listing(_LEFT)
else self._move_listing(_LEFT))
def cursor_page_up(self): def cursor_page_up(self):
(self._summary.cursor_page_up() if self._is_summary_focused self._summary.cursor_page_up() if self._is_summary_focused else self.listing_page_up()
else self.listing_page_up())
def cursor_page_down(self): def cursor_page_down(self):
(self._summary.cursor_page_down() if self._is_summary_focused self._summary.cursor_page_down() if self._is_summary_focused else self.listing_page_down()
else self.listing_page_down())
def cursor_end(self): def cursor_end(self):
(self._summary.cursor_end() if self._is_summary_focused self._summary.cursor_end() if self._is_summary_focused else self._page_listing(_RIGHT)
else self._page_listing(_RIGHT))
def cursor_home(self): def cursor_home(self):
(self._summary.cursor_home() if self._is_summary_focused self._summary.cursor_home() if self._is_summary_focused else self._page_listing(_LEFT)
else self._page_listing(_LEFT))
def _page_listing(self, vector): def _page_listing(self, vector):
dx, dy = vector dx, dy = vector
listing_width, listing_height = self._listing.widget.last_dimensions listing_width, listing_height = self._listing.widget.last_dimensions
self._move_listing((dx * (listing_width // 2), self._move_listing((dx * (listing_width // 2), dy * (listing_height // 2)))
dy * (listing_height // 2)))
def listing_page_up(self): def listing_page_up(self):
self._page_listing(_UP) self._page_listing(_UP)
@ -836,13 +801,11 @@ class Screen:
else: else:
path = self._summary.get_selection().path path = self._summary.get_selection().path
path_colored = tools.path_colored(path) path_colored = tools.path_colored(path)
line_num = (self._summary.get_selection().entry[0]. line_num = self._summary.get_selection().entry[0].scroll_position[1] + 1
scroll_position[1] + 1)
self._log.log_message([in_green("Editing "), path_colored, self._log.log_message([in_green("Editing "), path_colored,
in_green(f" at line {line_num}")]) in_green(f" at line {line_num}")])
subprocess.Popen(f"{self.editor_command} +{line_num} {path}", subprocess.Popen(f"{self.editor_command} +{line_num} {path}", shell=True,
shell=True, stdout=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stderr=subprocess.PIPE)
def toggle_status_style(self): def toggle_status_style(self):
self._summary.toggle_status_style(self._log) self._summary.toggle_status_style(self._log)
@ -862,16 +825,14 @@ class Screen:
selection = self._summary.get_selection() selection = self._summary.get_selection()
tool_name = tools.tool_name_colored(selection.tool, selection.path) tool_name = tools.tool_name_colored(selection.tool, selection.path)
path_colored = tools.path_colored(selection.path) path_colored = tools.path_colored(selection.path)
self._log.log_message([in_green("Refreshing "), tool_name, self._log.log_message([in_green("Refreshing "), tool_name, in_green(" result of "),
in_green(" result of "), path_colored, path_colored, in_green("")])
in_green("")])
self._summary.refresh_result(selection) self._summary.refresh_result(selection)
def refresh_tool(self): def refresh_tool(self):
selection = self._summary.get_selection() selection = self._summary.get_selection()
tool_name = tools.tool_name_colored(selection.tool, selection.path) tool_name = tools.tool_name_colored(selection.tool, selection.path)
self._log.log_message([in_green("Refreshing all results of "), self._log.log_message([in_green("Refreshing all results of "), tool_name, in_green("")])
tool_name, in_green("")])
self._summary.refresh_tool(selection.tool) self._summary.refresh_tool(selection.tool)
_DIMMED_BORDER = [termstr.TermStr(part).fg_color(termstr.Color.grey_100) _DIMMED_BORDER = [termstr.TermStr(part).fg_color(termstr.Color.grey_100)
@ -879,10 +840,8 @@ class Screen:
def _set_focus(self): def _set_focus(self):
focused, unfocused = fill3.Border.THICK, Screen._DIMMED_BORDER focused, unfocused = fill3.Border.THICK, Screen._DIMMED_BORDER
self._summary_border.set_style(focused if self._is_summary_focused self._summary_border.set_style(focused if self._is_summary_focused else unfocused)
else unfocused) self._listing.set_style(unfocused if self._is_summary_focused else focused)
self._listing.set_style(unfocused if self._is_summary_focused
else focused)
def toggle_focus(self): def toggle_focus(self):
self._is_summary_focused = not self._is_summary_focused self._is_summary_focused = not self._is_summary_focused
@ -894,10 +853,8 @@ class Screen:
def xdg_open(self): def xdg_open(self):
path = self._summary.get_selection().path path = self._summary.get_selection().path
path_colored = tools.path_colored(path) path_colored = tools.path_colored(path)
self._log.log_message([in_green("Opening "), path_colored, self._log.log_message([in_green("Opening "), path_colored, in_green("")])
in_green("")]) subprocess.Popen(["xdg-open", path], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
subprocess.Popen(["xdg-open", path], stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
def save(self): def save(self):
worker.Worker.unsaved_jobs_total = 0 worker.Worker.unsaved_jobs_total = 0
@ -907,8 +864,7 @@ class Screen:
def _select_entry_at_position(self, x, y, view_width, view_height): def _select_entry_at_position(self, x, y, view_width, view_height):
border_width = 1 border_width = 1
if x < border_width or y < border_width or x > view_width or \ if x < border_width or y < border_width or x > view_width or y > view_height:
y > view_height:
return return
view_x, view_y = self._summary._view_widget.portal.position view_x, view_y = self._summary._view_widget.portal.position
column_index = x - border_width + view_x column_index = x - border_width + view_x
@ -923,18 +879,14 @@ class Screen:
def _is_switching_focus(self, x, y, view_width, view_height): def _is_switching_focus(self, x, y, view_width, view_height):
return (not self._is_fullscreen and return (not self._is_fullscreen and
(self._is_listing_portrait and (self._is_listing_portrait and
(x > view_width and (x > view_width and self._is_summary_focused or x <= view_width and
self._is_summary_focused or x <= view_width and not self._is_summary_focused) or not self._is_listing_portrait and
not self._is_summary_focused) or (y > view_height and self._is_summary_focused or y <= view_height and
not self._is_listing_portrait and
(y > view_height and
self._is_summary_focused or y <= view_height and
not self._is_summary_focused))) not self._is_summary_focused)))
def on_mouse_input(self, term_code): def on_mouse_input(self, term_code):
if self._is_help_visible: if self._is_help_visible:
self._help_widget.on_mouse_input( self._help_widget.on_mouse_input(term_code, self._appearance_changed_event)
term_code, self._appearance_changed_event)
return return
event = terminal.decode_mouse_input(term_code) event = terminal.decode_mouse_input(term_code)
if event[0] not in [terminal.PRESS_MOUSE, terminal.DRAG_MOUSE]: if event[0] not in [terminal.PRESS_MOUSE, terminal.DRAG_MOUSE]:
@ -953,8 +905,7 @@ class Screen:
elif event[1] == terminal.WHEEL_DOWN_MOUSE: elif event[1] == terminal.WHEEL_DOWN_MOUSE:
self.listing_page_down() self.listing_page_down()
else: else:
view_width, view_height = \ view_width, view_height = self._summary._view_widget.portal.last_dimensions
self._summary._view_widget.portal.last_dimensions
if self._is_switching_focus(x, y, view_width, view_height): if self._is_switching_focus(x, y, view_width, view_height):
self.toggle_focus() self.toggle_focus()
else: else:
@ -965,11 +916,9 @@ class Screen:
def on_keyboard_input(self, term_code): def on_keyboard_input(self, term_code):
if self._is_help_visible: if self._is_help_visible:
self._help_widget.on_keyboard_input( self._help_widget.on_keyboard_input(term_code, self._appearance_changed_event)
term_code, self._appearance_changed_event)
return return
action = (Screen._KEY_MAP.get(term_code) or action = Screen._KEY_MAP.get(term_code) or Screen._KEY_MAP.get(term_code.lower())
Screen._KEY_MAP.get(term_code.lower()))
if action is not None: if action is not None:
action(self) action(self)
self._appearance_changed_event.set() self._appearance_changed_event.set()
@ -982,21 +931,16 @@ class Screen:
view.widget = widget.result view.widget = widget.result
tool_name = tools.tool_name_colored(widget.tool, widget.path) tool_name = tools.tool_name_colored(widget.tool, widget.path)
divider = " " + self._listing.top * 2 + " " divider = " " + self._listing.top * 2 + " "
self._listing.title = ( self._listing.title = (tools.path_colored(widget.path) + divider + tool_name + " " +
tools.path_colored(widget.path) + divider + tool_name + " " + tools.STATUS_TO_TERMSTR[widget.status] + divider + "line " + str(y+1))
tools.STATUS_TO_TERMSTR[widget.status] + divider +
"line " + str(y+1))
_STATUS_BAR = highlight_chars( _STATUS_BAR = highlight_chars(" *help *quit *t*a*b:focus *turn *log *edit *next *sort"
" *help *quit *t*a*b:focus *turn *log *edit *next *sort" " *refresh *fullscreen *open", Log._GREEN_STYLE)
" *refresh *fullscreen *open", Log._GREEN_STYLE)
@functools.lru_cache() @functools.lru_cache()
def _get_partial_bar_chars(self, bar_transparency): def _get_partial_bar_chars(self, bar_transparency):
bar_color = blend_color(termstr.Color.black, termstr.Color.white, bar_color = blend_color(termstr.Color.black, termstr.Color.white, bar_transparency)
bar_transparency) return [termstr.TermStr(char).fg_color(bar_color).bg_color(termstr.Color.black)
return [termstr.TermStr(char).fg_color(bar_color).
bg_color(termstr.Color.black)
for char in fill3.ScrollBar._PARTIAL_CHARS[1]] for char in fill3.ScrollBar._PARTIAL_CHARS[1]]
@functools.lru_cache(maxsize=2) @functools.lru_cache(maxsize=2)
@ -1007,19 +951,18 @@ class Screen:
whole = int(whole) whole = int(whole)
if whole < len(bar) and bar[whole].data == " ": if whole < len(bar) and bar[whole].data == " ":
left_part = bar[:whole] left_part = bar[:whole]
right_part = (self._get_partial_bar_chars(bar_transparency) right_part = (self._get_partial_bar_chars(bar_transparency)[int(fraction * 8)]
[int(fraction * 8)] + bar[whole+1:]) + bar[whole+1:])
else: else:
progress_bar_size = round(progress_bar_size) progress_bar_size = round(progress_bar_size)
left_part = bar[:progress_bar_size] left_part = bar[:progress_bar_size]
right_part = bar[progress_bar_size:] right_part = bar[progress_bar_size:]
return [highlight_str(left_part, termstr.Color.white, bar_transparency) return [highlight_str(left_part, termstr.Color.white, bar_transparency) + right_part]
+ right_part]
def _get_status_bar(self, width): def _get_status_bar(self, width):
incomplete = self._summary.result_total - self._summary.completed_total incomplete = self._summary.result_total - self._summary.completed_total
progress_bar_size = width if self._summary.result_total == 0 else \ progress_bar_size = (width if self._summary.result_total == 0 else
max(0, width * incomplete / self._summary.result_total) max(0, width * incomplete / self._summary.result_total))
return self._get_status_bar_appearance(width, progress_bar_size) return self._get_status_bar_appearance(width, progress_bar_size)
def appearance(self, dimensions): def appearance(self, dimensions):
@ -1028,44 +971,35 @@ class Screen:
if self._is_help_visible: if self._is_help_visible:
body = self._help_widget body = self._help_widget
elif self._is_fullscreen: elif self._is_fullscreen:
body = (self._summary_border if self._is_summary_focused body = self._summary_border if self._is_summary_focused else self._listing
else self._listing)
else: else:
body = (self._layouts[self._is_log_visible] body = self._layouts[self._is_log_visible][self._is_listing_portrait]
[self._is_listing_portrait])
width, height = max(dimensions[0], 10), max(dimensions[1], 20) width, height = max(dimensions[0], 10), max(dimensions[1], 20)
result = (body.appearance((width, height-1)) + result = body.appearance((width, height-1)) + self._get_status_bar(width)
self._get_status_bar(width))
return (result if (width, height) == dimensions return (result if (width, height) == dimensions
else fill3.appearance_resize(result, dimensions)) else fill3.appearance_resize(result, dimensions))
_KEY_MAP = { _KEY_MAP = {"t": toggle_window_orientation, "l": toggle_log, "h": toggle_help,
"t": toggle_window_orientation, "l": toggle_log, "h": toggle_help, terminal.UP_KEY: cursor_up, terminal.DOWN_KEY: cursor_down,
terminal.UP_KEY: cursor_up, terminal.DOWN_KEY: cursor_down, terminal.LEFT_KEY: cursor_left, terminal.RIGHT_KEY: cursor_right,
terminal.LEFT_KEY: cursor_left, terminal.RIGHT_KEY: cursor_right, terminal.PAGE_DOWN_KEY: cursor_page_down, terminal.PAGE_UP_KEY: cursor_page_up,
terminal.PAGE_DOWN_KEY: cursor_page_down, "s": toggle_order, terminal.HOME_KEY: cursor_home, terminal.END_KEY: cursor_end,
terminal.PAGE_UP_KEY: cursor_page_up, "s": toggle_order, "n": move_to_next_issue, "N": move_to_next_issue_of_tool, "e": edit_file,
terminal.HOME_KEY: cursor_home, terminal.END_KEY: cursor_end, "q": quit_, terminal.ESC: quit_, "r": refresh, "R": refresh_tool,
"n": move_to_next_issue, "N": move_to_next_issue_of_tool, "\t": toggle_focus, "f": toggle_fullscreen, "o": xdg_open}
"e": edit_file, "q": quit_, terminal.ESC: quit_, "r": refresh,
"R": refresh_tool, "\t": toggle_focus, "f": toggle_fullscreen,
"o": xdg_open}
def setup_inotify(root_path, loop, on_filesystem_event, exclude_filter): def setup_inotify(root_path, loop, on_filesystem_event, exclude_filter):
watch_manager = pyinotify.WatchManager() watch_manager = pyinotify.WatchManager()
event_mask = (pyinotify.IN_CREATE | pyinotify.IN_DELETE | event_mask = (pyinotify.IN_CREATE | pyinotify.IN_DELETE | pyinotify.IN_CLOSE_WRITE |
pyinotify.IN_CLOSE_WRITE | pyinotify.IN_ATTRIB | pyinotify.IN_ATTRIB | pyinotify.IN_MOVED_FROM | pyinotify.IN_MOVED_TO)
pyinotify.IN_MOVED_FROM | pyinotify.IN_MOVED_TO)
watch_manager.add_watch(root_path, event_mask, rec=True, auto_add=True, watch_manager.add_watch(root_path, event_mask, rec=True, auto_add=True,
proc_fun=on_filesystem_event, proc_fun=on_filesystem_event, exclude_filter=exclude_filter,
exclude_filter=exclude_filter, quiet=False) quiet=False)
return pyinotify.AsyncioNotifier(watch_manager, loop, return pyinotify.AsyncioNotifier(watch_manager, loop, callback=lambda notifier: None)
callback=lambda notifier: None)
def load_state(pickle_path, jobs_added_event, appearance_changed_event, def load_state(pickle_path, jobs_added_event, appearance_changed_event, root_path, loop):
root_path, loop):
is_first_run = True is_first_run = True
try: try:
with gzip.open(pickle_path, "rb") as file_: with gzip.open(pickle_path, "rb") as file_:
@ -1119,16 +1053,14 @@ def main(root_path, loop, worker_count=None, editor_command=None, theme=None,
pickle_path = os.path.join(tools.CACHE_PATH, "summary.pickle") pickle_path = os.path.join(tools.CACHE_PATH, "summary.pickle")
jobs_added_event = asyncio.Event() jobs_added_event = asyncio.Event()
appearance_changed_event = asyncio.Event() appearance_changed_event = asyncio.Event()
summary, screen, log, is_first_run = load_state( summary, screen, log, is_first_run = load_state(pickle_path, jobs_added_event,
pickle_path, jobs_added_event, appearance_changed_event, root_path, appearance_changed_event, root_path, loop)
loop)
screen.editor_command = editor_command screen.editor_command = editor_command
log.log_message("Program started.") log.log_message("Program started.")
jobs_added_event.set() jobs_added_event.set()
def callback(event): def callback(event):
on_filesystem_event(event, summary, root_path, on_filesystem_event(event, summary, root_path, appearance_changed_event)
appearance_changed_event)
notifier = setup_inotify(root_path, loop, callback, is_path_excluded) notifier = setup_inotify(root_path, loop, callback, is_path_excluded)
try: try:
log.log_message(f"Starting workers ({worker_count}) …") log.log_message(f"Starting workers ({worker_count}) …")
@ -1139,13 +1071,11 @@ def main(root_path, loop, worker_count=None, editor_command=None, theme=None,
time.sleep(0.05) time.sleep(0.05)
screen.stop_workers() screen.stop_workers()
loop.stop() loop.stop()
loop.create_task(summary.sync_with_filesystem( loop.create_task(summary.sync_with_filesystem(appearance_changed_event, log))
appearance_changed_event, log))
for worker_ in screen.workers: for worker_ in screen.workers:
loop.create_task(worker_.future) loop.create_task(worker_.future)
if sys.stdout.isatty(): if sys.stdout.isatty():
with fill3.context(loop, appearance_changed_event, screen, with fill3.context(loop, appearance_changed_event, screen, exit_loop=exit_loop):
exit_loop=exit_loop):
loop.run_forever() loop.run_forever()
log.log_message("Program stopped.") log.log_message("Program stopped.")
else: else:
@ -1192,13 +1122,10 @@ def print_tool_info():
for extensions, tools_ in tools.TOOLS_FOR_EXTENSIONS: for extensions, tools_ in tools.TOOLS_FOR_EXTENSIONS:
for extension in extensions: for extension in extensions:
for tool in tools_: for tool in tools_:
extensions_for_tool.setdefault( extensions_for_tool.setdefault(tool, {extension}).add(extension)
tool, {extension}).add(extension)
for tool in sorted(tools.tools_all(), key=lambda t: t.__name__): for tool in sorted(tools.tools_all(), key=lambda t: t.__name__):
print(termstr.TermStr(tool.__name__).bold() print(termstr.TermStr(tool.__name__).bold() if tools.is_tool_available(tool)
if tools.is_tool_available(tool) else termstr.TermStr(tool.__name__).fg_color(termstr.Color.red) + " (not available)")
else termstr.TermStr(tool.__name__).fg_color(termstr.Color.red)
+ " (not available) ")
print("url:", tool.url) print("url:", tool.url)
extensions = list(extensions_for_tool.get(tool, {"*"})) extensions = list(extensions_for_tool.get(tool, {"*"}))
print("extensions:", ", ".join(extensions)) print("extensions:", ", ".join(extensions))
@ -1245,10 +1172,9 @@ def check_arguments():
if arguments["--compression"] not in compressions: if arguments["--compression"] not in compressions:
print("--compression must be one of:", " ".join(compressions)) print("--compression must be one of:", " ".join(compressions))
sys.exit(1) sys.exit(1)
editor_command = arguments["--editor"] or os.environ.get("EDITOR", None)\ editor_command = (arguments["--editor"] or os.environ.get("EDITOR", None)
or os.environ.get("VISUAL", None) or os.environ.get("VISUAL", None))
return root_path, worker_count, editor_command, arguments["--theme"], \ return root_path, worker_count, editor_command, arguments["--theme"], arguments["--compression"]
arguments["--compression"]
def inotify_watches_exceeded(): def inotify_watches_exceeded():
@ -1259,15 +1185,13 @@ def inotify_watches_exceeded():
def entry_point(): def entry_point():
root_path, worker_count, editor_command, theme, compression = \ root_path, worker_count, editor_command, theme, compression = check_arguments()
check_arguments()
manage_cache(root_path) manage_cache(root_path)
with terminal.terminal_title("eris: " + os.path.basename(root_path)): with terminal.terminal_title("eris: " + os.path.basename(root_path)):
with chdir(root_path): # FIX: Don't change directory if possible. with chdir(root_path): # FIX: Don't change directory if possible.
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
try: try:
main(root_path, loop, worker_count, editor_command, theme, main(root_path, loop, worker_count, editor_command, theme, compression)
compression)
except pyinotify.WatchManagerError: except pyinotify.WatchManagerError:
inotify_watches_exceeded() inotify_watches_exceeded()

View file

@ -8,15 +8,13 @@ import shutil
def batch(iter_, page_size): def batch(iter_, page_size):
for _, batch in itertools.groupby( for _, batch in itertools.groupby(enumerate(iter_), lambda tuple_: tuple_[0] // page_size):
enumerate(iter_), lambda tuple_: tuple_[0] // page_size):
yield [value for index, value in batch] yield [value for index, value in batch]
class PagedList: class PagedList:
def __init__(self, list_, pages_dir, page_size, cache_size, exist_ok=False, def __init__(self, list_, pages_dir, page_size, cache_size, exist_ok=False, open_func=open):
open_func=open):
self.pages_dir = pages_dir # An empty or non-existant directory. self.pages_dir = pages_dir # An empty or non-existant directory.
self.page_size = page_size self.page_size = page_size
self.cache_size = cache_size self.cache_size = cache_size
@ -57,12 +55,10 @@ class PagedList:
stop_page_index -= 1 stop_page_index -= 1
stop_page_offset = self.page_size stop_page_offset = self.page_size
if start_page_index == stop_page_index: if start_page_index == stop_page_index:
return (self._get_page(start_page_index) return self._get_page(start_page_index)[start_page_offset:stop_page_offset]
[start_page_offset:stop_page_offset])
else: else:
return (self._get_page(start_page_index)[start_page_offset:] + return (self._get_page(start_page_index)[start_page_offset:] +
[line for page_index in [line for page_index in range(start_page_index+1, stop_page_index)
range(start_page_index+1, stop_page_index)
for line in self._get_page(page_index)] + for line in self._get_page(page_index)] +
self._get_page(stop_page_index)[:stop_page_offset]) self._get_page(stop_page_index)[:stop_page_offset])
else: else:
@ -70,8 +66,7 @@ class PagedList:
return self._get_page(page_index)[page_offset] return self._get_page(page_index)[page_offset]
def _setup_page_cache(self): def _setup_page_cache(self):
self._get_page = functools.lru_cache(self.cache_size)( self._get_page = functools.lru_cache(self.cache_size)(self._get_page_org)
self._get_page_org)
def __getstate__(self): # Don't pickle the lru_cache. def __getstate__(self): # Don't pickle the lru_cache.
state = self.__dict__.copy() state = self.__dict__.copy()

View file

@ -55,23 +55,16 @@ class Status(enum.IntEnum):
timed_out = 7 timed_out = 7
_STATUS_COLORS = {Status.ok: termstr.Color.green, _STATUS_COLORS = {Status.ok: termstr.Color.green, Status.problem: termstr.Color.dark_green,
Status.problem: termstr.Color.dark_green, Status.not_applicable: termstr.Color.grey_80, Status.running: termstr.Color.lime,
Status.not_applicable: termstr.Color.grey_80, Status.error: termstr.Color.red, Status.timed_out: termstr.Color.purple}
Status.running: termstr.Color.lime, STATUS_MEANINGS = [(Status.ok, "Ok"), (Status.problem, "Problem"),
Status.error: termstr.Color.red, (Status.not_applicable, "Not applicable"), (Status.running, "Running"),
Status.timed_out: termstr.Color.purple} (Status.timed_out, "Timed out"), (Status.pending, "Pending"),
STATUS_MEANINGS = [ (Status.error, "Error")]
(Status.ok, "Ok"), (Status.problem, "Problem"), STATUS_TO_TERMSTR = {status: termstr.TermStr(" ", termstr.CharStyle(bg_color=color))
(Status.not_applicable, "Not applicable"), (Status.running, "Running"), for status, color in _STATUS_COLORS.items()}
(Status.timed_out, "Timed out"), (Status.pending, "Pending"), STATUS_TO_TERMSTR[Status.pending] = termstr.TermStr(".").fg_color(termstr.Color.grey_100)
(Status.error, "Error")
]
STATUS_TO_TERMSTR = {
status: termstr.TermStr(" ", termstr.CharStyle(bg_color=color))
for status, color in _STATUS_COLORS.items()}
STATUS_TO_TERMSTR[Status.pending] = termstr.TermStr(".").fg_color(
termstr.Color.grey_100)
def get_ls_color_codes(): def get_ls_color_codes():
@ -85,8 +78,7 @@ TIMEOUT = 60
def _printable(text): def _printable(text):
return "".join(char if ord(char) > 31 or char in ["\n", "\t"] else "#" return "".join(char if ord(char) > 31 or char in ["\n", "\t"] else "#" for char in text)
for char in text)
def _fix_input(input_): def _fix_input(input_):
@ -94,23 +86,19 @@ def _fix_input(input_):
def _do_command(command, **kwargs): def _do_command(command, **kwargs):
completed_process = subprocess.run(command, stdout=subprocess.PIPE, completed_process = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
stderr=subprocess.PIPE, text=True, text=True, **kwargs)
**kwargs) return (_fix_input(completed_process.stdout), _fix_input(completed_process.stderr),
return (_fix_input(completed_process.stdout), completed_process.returncode)
_fix_input(completed_process.stderr), completed_process.returncode)
def _run_command(command, error_status=None, has_color=False, timeout=None, def _run_command(command, error_status=None, has_color=False, timeout=None, **kwargs):
**kwargs):
error_status = Status.problem if error_status is None else error_status error_status = Status.problem if error_status is None else error_status
if has_color: if has_color:
process = subprocess.run(command, stdout=subprocess.PIPE, process = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
stderr=subprocess.PIPE, text=True, text=True, timeout=timeout, **kwargs)
timeout=timeout, **kwargs) stdout, stderr, returncode = (termstr.TermStr.from_term(process.stdout),
stdout, stderr, returncode = ( termstr.TermStr.from_term(process.stderr), process.returncode)
termstr.TermStr.from_term(process.stdout),
termstr.TermStr.from_term(process.stderr), process.returncode)
else: else:
stdout, stderr, returncode = _do_command(command, timeout=timeout) stdout, stderr, returncode = _do_command(command, timeout=timeout)
result_status = Status.ok if returncode == 0 else error_status result_status = Status.ok if returncode == 0 else error_status
@ -131,8 +119,7 @@ def _syntax_highlight(text, lexer, style):
hex_rgb = hex_rgb[1:] hex_rgb = hex_rgb[1:]
return tuple(eval("0x"+hex_rgb[index:index+2]) for index in [0, 2, 4]) return tuple(eval("0x"+hex_rgb[index:index+2]) for index in [0, 2, 4])
def _char_style_for_token_type(token_type, default_bg_color, def _char_style_for_token_type(token_type, default_bg_color, default_style):
default_style):
try: try:
token_style = style.style_for_token(token_type) token_style = style.style_for_token(token_type)
except KeyError: except KeyError:
@ -141,17 +128,14 @@ def _syntax_highlight(text, lexer, style):
else _parse_rgb(token_style["color"])) else _parse_rgb(token_style["color"]))
bg_color = (default_bg_color if token_style["bgcolor"] is None bg_color = (default_bg_color if token_style["bgcolor"] is None
else _parse_rgb(token_style["bgcolor"])) else _parse_rgb(token_style["bgcolor"]))
return termstr.CharStyle(fg_color, bg_color, token_style["bold"], return termstr.CharStyle(fg_color, bg_color, token_style["bold"], token_style["italic"],
token_style["italic"],
token_style["underline"]) token_style["underline"])
default_bg_color = _parse_rgb(style.background_color) default_bg_color = _parse_rgb(style.background_color)
default_style = termstr.CharStyle(bg_color=default_bg_color) default_style = termstr.CharStyle(bg_color=default_bg_color)
text = fill3.join( text = fill3.join("", [termstr.TermStr(text, _char_style_for_token_type(
"", [termstr.TermStr(text, _char_style_for_token_type( token_type, default_bg_color, default_style))
token_type, default_bg_color, default_style)) for token_type, text in pygments.lex(text, lexer)])
for token_type, text in pygments.lex(text, lexer)]) text_widget = fill3.Text(text, pad_char=termstr.TermStr(" ").bg_color(default_bg_color))
text_widget = fill3.Text(text, pad_char=termstr.TermStr(" ").bg_color(
default_bg_color))
return fill3.join("\n", text_widget.text) return fill3.join("\n", text_widget.text)
@ -171,8 +155,7 @@ def _permissions_in_octal(permissions):
for part_index in range(3): for part_index in range(3):
index = part_index * 3 + 1 index = part_index * 3 + 1
part = permissions[index:index+3] part = permissions[index:index+3]
digit = sum(2 ** (2 - index) for index, element in enumerate(part) digit = sum(2 ** (2 - index) for index, element in enumerate(part) if element != "-")
if element != "-")
result.append(str(digit)) result.append(str(digit))
return "".join(result) return "".join(result)
@ -183,12 +166,11 @@ def _pretty_bytes(bytes):
units = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB") units = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
unit_index = int(math.floor(math.log(bytes, 1024))) unit_index = int(math.floor(math.log(bytes, 1024)))
power = math.pow(1024, unit_index) power = math.pow(1024, unit_index)
conversion = round(bytes/power, 2) conversion = round(bytes / power, 2)
return f"{conversion} {units[unit_index]}" return f"{conversion} {units[unit_index]}"
@deps(deps={"file", "coreutils"}, url="https://github.com/ahamilton/eris", @deps(deps={"file", "coreutils"}, url="https://github.com/ahamilton/eris", executables={"file"})
executables={"file"})
def metadata(path): def metadata(path):
def detail(value, unit): def detail(value, unit):
@ -198,41 +180,31 @@ def metadata(path):
stat_result = os.stat(path) stat_result = os.stat(path)
permissions = stat.filemode(stat_result.st_mode) permissions = stat.filemode(stat_result.st_mode)
hardlinks = str(stat_result.st_nlink) hardlinks = str(stat_result.st_nlink)
group = [pwd.getpwuid(stat_result.st_gid).pw_name, group = [pwd.getpwuid(stat_result.st_gid).pw_name, detail(stat_result.st_gid, "gid")]
detail(stat_result.st_gid, "gid")] owner = [pwd.getpwuid(stat_result.st_uid).pw_name, detail(stat_result.st_uid, "uid")]
owner = [pwd.getpwuid(stat_result.st_uid).pw_name, modified, created, access = [[time.asctime(time.gmtime(seconds)), detail(int(seconds), "secs")]
detail(stat_result.st_uid, "uid")] for seconds in (stat_result.st_mtime, stat_result.st_ctime,
modified, created, access = [ stat_result.st_atime)]
[time.asctime(time.gmtime(seconds)), detail(int(seconds), "secs")] size = [_pretty_bytes(stat_result.st_size), detail(stat_result.st_size, "bytes")]
for seconds in (stat_result.st_mtime, stat_result.st_ctime, stdout, *rest = _do_command(["file", "--dereference", "--brief", "--uncompress", "--mime",
stat_result.st_atime)] path])
size = [_pretty_bytes(stat_result.st_size),
detail(stat_result.st_size, "bytes")]
stdout, *rest = _do_command(
["file", "--dereference", "--brief", "--uncompress", "--mime", path])
mime_type = stdout mime_type = stdout
stdout, *rest = _do_command( stdout, *rest = _do_command(["file", "--dereference", "--brief", "--uncompress", path])
["file", "--dereference", "--brief", "--uncompress", path])
file_type = stdout file_type = stdout
permissions_value = [permissions, permissions_value = [permissions, detail(_permissions_in_octal(permissions), None)]
detail(_permissions_in_octal(permissions), None)]
text = [] text = []
for line in [ for line in [("size", size), ("permissions", permissions_value), None,
("size", size), ("permissions", permissions_value), None, ("modified time", modified), ("creation time", created), ("access time", access),
("modified time", modified), ("creation time", created), None, ("owner", owner), ("group", group), None, ("hardlinks", hardlinks),
("access time", access), None, ("symlink", is_symlink), None, ("mime type", mime_type.strip()),
("owner", owner), ("group", group), None, ("file type", file_type.strip())]:
("hardlinks", hardlinks), ("symlink", is_symlink), None,
("mime type", mime_type.strip()),
("file type", file_type.strip())]:
if line is None: if line is None:
text.append("\n") text.append("\n")
else: else:
name, value = line name, value = line
name = termstr.TermStr(name + ":").fg_color( name = termstr.TermStr(name + ":").fg_color(termstr.Color.blue).ljust(16)
termstr.Color.blue).ljust(16)
text.append(name + fill3.join("", value) + "\n") text.append(name + fill3.join("", value) + "\n")
return (Status.ok, fill3.join("", text)) return Status.ok, fill3.join("", text)
@deps(deps={"python3-pygments"}, url="http://pygments.org/") @deps(deps={"python3-pygments"}, url="http://pygments.org/")
@ -264,8 +236,7 @@ def _is_python_test_file(path):
@deps(url="https://docs.python.org/3/library/unittest.html") @deps(url="https://docs.python.org/3/library/unittest.html")
def python_unittests(path): def python_unittests(path):
if _is_python_test_file(path): if _is_python_test_file(path):
command = ([path] if _has_shebang_line(path) command = [path] if _has_shebang_line(path) else [PYTHON_EXECUTABLE, path]
else [PYTHON_EXECUTABLE, path])
stdout, stderr, returncode = _do_command(command, timeout=TIMEOUT) stdout, stderr, returncode = _do_command(command, timeout=TIMEOUT)
status = Status.ok if returncode == 0 else Status.problem status = Status.ok if returncode == 0 else Status.problem
return status, (stdout + "\n" + stderr) return status, (stdout + "\n" + stderr)
@ -281,12 +252,10 @@ def pytest(path):
with tempfile.TemporaryDirectory() as temp_dir: with tempfile.TemporaryDirectory() as temp_dir:
env = os.environ.copy() env = os.environ.copy()
env["COVERAGE_FILE"] = os.path.join(temp_dir, "coverage") env["COVERAGE_FILE"] = os.path.join(temp_dir, "coverage")
process = subprocess.run(command, stdout=subprocess.PIPE, process = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
stderr=subprocess.PIPE, text=True, text=True, timeout=TIMEOUT, env=env)
timeout=TIMEOUT, env=env) stdout, stderr, returncode = (termstr.TermStr.from_term(process.stdout),
stdout, stderr, returncode = ( termstr.TermStr.from_term(process.stderr), process.returncode)
termstr.TermStr.from_term(process.stdout),
termstr.TermStr.from_term(process.stderr), process.returncode)
if returncode == 5: if returncode == 5:
status = Status.not_applicable status = Status.not_applicable
else: else:
@ -297,17 +266,14 @@ def pytest(path):
@deps(deps={"python3-mypy"}, url="http://mypy-lang.org/") @deps(deps={"python3-mypy"}, url="http://mypy-lang.org/")
def mypy(path): def mypy(path):
stdout, stderr, returncode = _do_command( stdout, stderr, returncode = _do_command(
[PYTHON_EXECUTABLE, "-m", "mypy", "--ignore-missing-imports", path], [PYTHON_EXECUTABLE, "-m", "mypy", "--ignore-missing-imports", path], timeout=TIMEOUT)
timeout=TIMEOUT)
status = Status.ok if returncode == 0 else Status.problem status = Status.ok if returncode == 0 else Status.problem
return status, stdout return status, stdout
def _colorize_coverage_report(lines): def _colorize_coverage_report(lines):
line_color = {"> ": termstr.Color.green, "! ": termstr.Color.grey_150, line_color = {"> ": termstr.Color.green, "! ": termstr.Color.grey_150, " ": None}
" ": None} return fill3.join("", [termstr.TermStr(line).fg_color(line_color[line[:2]]) for line in lines])
return fill3.join("", [termstr.TermStr(line).fg_color(line_color[line[:2]])
for line in lines])
@deps(deps={"python3-coverage"}, url="https://coverage.readthedocs.io/") @deps(deps={"python3-coverage"}, url="https://coverage.readthedocs.io/")
@ -317,13 +283,11 @@ def python_coverage(path):
return Status.not_applicable, f'No "{coverage_path}" file.' return Status.not_applicable, f'No "{coverage_path}" file.'
if os.stat(path).st_mtime > os.stat(coverage_path).st_mtime: if os.stat(path).st_mtime > os.stat(coverage_path).st_mtime:
return (Status.not_applicable, return (Status.not_applicable,
f'File has been modified since "{coverage_path}"' f'File has been modified since "{coverage_path}" file was generated.')
' file was generated.')
path = os.path.normpath(path) path = os.path.normpath(path)
with tempfile.TemporaryDirectory() as temp_dir: with tempfile.TemporaryDirectory() as temp_dir:
stdout, stderr, returncode = _do_command( stdout, stderr, returncode = _do_command([PYTHON_EXECUTABLE, "-m", "coverage", "annotate",
[PYTHON_EXECUTABLE, "-m", "coverage", "--directory", temp_dir, path])
"annotate", "--directory", temp_dir, path])
if returncode != 0: if returncode != 0:
return Status.problem, stdout return Status.problem, stdout
cover_filename = os.listdir(temp_dir)[0] cover_filename = os.listdir(temp_dir)[0]
@ -380,8 +344,7 @@ def python_mccabe(path):
stdout, *rest = _do_command([PYTHON_EXECUTABLE, "-m", "mccabe", path]) stdout, *rest = _do_command([PYTHON_EXECUTABLE, "-m", "mccabe", path])
max_score = 0 max_score = 0
with contextlib.suppress(ValueError): # When there are no lines with contextlib.suppress(ValueError): # When there are no lines
max_score = max(_get_mccabe_line_score(line) max_score = max(_get_mccabe_line_score(line) for line in stdout.splitlines())
for line in stdout.splitlines())
status = Status.problem if max_score > 10 else Status.ok status = Status.problem if max_score > 10 else Status.ok
return status, _colorize_mccabe(stdout) return status, _colorize_mccabe(stdout)
@ -394,8 +357,7 @@ def python_mccabe(path):
# Status.not_applicable) # Status.not_applicable)
@deps(deps={"perltidy"}, url="http://perltidy.sourceforge.net/", @deps(deps={"perltidy"}, url="http://perltidy.sourceforge.net/", executables={"perltidy"})
executables={"perltidy"})
def perltidy(path): def perltidy(path):
stdout, *rest = _do_command(["perltidy", "-st", path]) stdout, *rest = _do_command(["perltidy", "-st", path])
return Status.ok, _syntax_highlight_using_path(stdout, path) return Status.ok, _syntax_highlight_using_path(stdout, path)
@ -414,8 +376,7 @@ def html_syntax(path):
def pandoc(path): def pandoc(path):
with tempfile.TemporaryDirectory() as temp_dir: with tempfile.TemporaryDirectory() as temp_dir:
temp_path = os.path.join(temp_dir, "temp.html") temp_path = os.path.join(temp_dir, "temp.html")
_do_command(["pandoc", "-t", "html", "-o", temp_path, path], _do_command(["pandoc", "-t", "html", "-o", temp_path, path], timeout=TIMEOUT)
timeout=TIMEOUT)
return elinks(temp_path) return elinks(temp_path)
@ -425,23 +386,19 @@ MAX_IMAGE_SIZE = 200
def _resize_image(image, new_width): def _resize_image(image, new_width):
import PIL.Image # Here to avoid 'Segmentation Fault' in install-tools import PIL.Image # Here to avoid 'Segmentation Fault' in install-tools
scale = new_width / image.width scale = new_width / image.width
return image.resize((int(image.width * scale), int(image.height * scale)), return image.resize((int(image.width * scale), int(image.height * scale)), PIL.Image.ANTIALIAS)
PIL.Image.ANTIALIAS)
def _image_to_text(image): def _image_to_text(image):
text = "" * image.width text = "" * image.width
data = list(image.getdata()) data = list(image.getdata())
width = image.width width = image.width
rows = [data[row_index*width:(row_index+1)*width] rows = [data[row_index*width:(row_index+1)*width] for row_index in range(image.height)]
for row_index in range(image.height)]
if image.height % 2 == 1: if image.height % 2 == 1:
rows.append([None] * image.width) rows.append([None] * image.width)
return fill3.join("\n", [ return fill3.join("\n", [
termstr.TermStr(text, tuple(termstr.CharStyle( termstr.TermStr(text, tuple(termstr.CharStyle(fg_color=top_pixel, bg_color=bottom_pixel)
fg_color=top_pixel, bg_color=bottom_pixel) for top_pixel, bottom_pixel in zip(rows[index], rows[index+1])))
for top_pixel, bottom_pixel in zip(rows[index],
rows[index+1])))
for index in range(0, image.height, 2)]) for index in range(0, image.height, 2)])
@ -471,24 +428,18 @@ def godoc(path):
with tempfile.TemporaryDirectory() as temp_dir: with tempfile.TemporaryDirectory() as temp_dir:
symlink_path = os.path.join(temp_dir, "file.go") symlink_path = os.path.join(temp_dir, "file.go")
os.symlink(os.path.abspath(path), symlink_path) os.symlink(os.path.abspath(path), symlink_path)
stdout, stderr, returncode = _do_command(["go", "doc", "."], stdout, stderr, returncode = _do_command(["go", "doc", "."], cwd=temp_dir)
cwd=temp_dir)
os.remove(symlink_path) os.remove(symlink_path)
status = (Status.not_applicable if stdout.strip() == "" or returncode != 0 status = Status.not_applicable if stdout.strip() == "" or returncode != 0 else Status.ok
else Status.ok)
return status, stdout + stderr return status, stdout + stderr
@deps(deps={"git"}, url="https://git-scm.com/docs/git-log", @deps(deps={"git"}, url="https://git-scm.com/docs/git-log",
executables={"git"}) executables={"git"})
def git_log(path): def git_log(path):
status, output = _run_command( status, output = _run_command(["git", "log", "--find-renames", "--follow", "--stat", "--color",
["git", "log", "--find-renames", "--follow", "--stat", "--color", path], error_status=Status.not_applicable, has_color=True)
path], error_status=Status.not_applicable, has_color=True) return (Status.not_applicable, "") if output.data == "" else (status, output)
if output.data == "":
return Status.not_applicable, ""
else:
return status, output
def make_tool_function(dependencies, command, url=None, error_status=None, def make_tool_function(dependencies, command, url=None, error_status=None,
@ -501,8 +452,7 @@ def make_tool_function(dependencies, command, url=None, error_status=None,
@deps(deps=set(dependencies), url=url, executables=executables) @deps(deps=set(dependencies), url=url, executables=executables)
def func(path): def func(path):
return _run_command(command_parts + [path], error_status, has_color, return _run_command(command_parts + [path], error_status, has_color, timeout)
timeout)
func.command = command func.command = command
return func return func
@ -539,8 +489,7 @@ def lru_cache_with_eviction(maxsize=128, typed=False):
def remove_version(*args, **kwds): def remove_version(*args, **kwds):
return user_function(*args[1:], **kwds) return user_function(*args[1:], **kwds)
new_func = functools.lru_cache(maxsize=maxsize, typed=typed)( new_func = functools.lru_cache(maxsize=maxsize, typed=typed)(remove_version)
remove_version)
def add_version(*args, **kwds): def add_version(*args, **kwds):
key = make_key(args, kwds, typed) key = make_key(args, kwds, typed)
@ -552,8 +501,7 @@ def lru_cache_with_eviction(maxsize=128, typed=False):
return decorating_function return decorating_function
def dump_pickle_safe(object_, path, protocol=pickle.HIGHEST_PROTOCOL, def dump_pickle_safe(object_, path, protocol=pickle.HIGHEST_PROTOCOL, open=open):
open=open):
tmp_path = path + ".tmp" tmp_path = path + ".tmp"
try: try:
with open(tmp_path, "wb") as file_: with open(tmp_path, "wb") as file_:
@ -566,15 +514,13 @@ def dump_pickle_safe(object_, path, protocol=pickle.HIGHEST_PROTOCOL,
@functools.lru_cache() @functools.lru_cache()
def compression_open_func(compression): def compression_open_func(compression):
return (open if compression == "none" else return open if compression == "none" else importlib.import_module(compression).open
importlib.import_module(compression).open)
class Result: class Result:
COMPLETED_STATUSES = { COMPLETED_STATUSES = {Status.ok, Status.problem, Status.error,
Status.ok, Status.problem, Status.error, Status.not_applicable, Status.not_applicable, Status.timed_out}
Status.timed_out}
def __init__(self, path, tool): def __init__(self, path, tool):
self.path = path self.path = path
@ -594,8 +540,7 @@ class Result:
if self.status == Status.pending or self.compression is None: if self.status == Status.pending or self.compression is None:
return unknown_label return unknown_label
try: try:
with compression_open_func(self.compression)( with compression_open_func(self.compression)(self.pickle_path(), "rb") as pickle_file:
self.pickle_path(), "rb") as pickle_file:
return pickle.load(pickle_file) return pickle.load(pickle_file)
except FileNotFoundError: except FileNotFoundError:
return unknown_label return unknown_label
@ -603,8 +548,7 @@ class Result:
@result.setter @result.setter
def result(self, value): def result(self, value):
os.makedirs(os.path.dirname(self.pickle_path()), exist_ok=True) os.makedirs(os.path.dirname(self.pickle_path()), exist_ok=True)
dump_pickle_safe(value, self.pickle_path(), dump_pickle_safe(value, self.pickle_path(), open=compression_open_func(self.compression))
open=compression_open_func(self.compression))
Result.result.fget.evict(self) Result.result.fget.evict(self)
def set_status(self, status): def set_status(self, status):
@ -627,10 +571,8 @@ class Result:
end_time = time.time() end_time = time.time()
self.set_status(new_status) self.set_status(new_status)
appearance_changed_event.set() appearance_changed_event.set()
log.log_message( log.log_message(["Finished running ", tool_name, " on ", path, ". ",
["Finished running ", tool_name, " on ", path, ". ", STATUS_TO_TERMSTR[new_status], f" {round(end_time - start_time, 2)} secs"])
STATUS_TO_TERMSTR[new_status],
f" {round(end_time - start_time, 2)} secs"])
def reset(self): def reset(self):
self.set_status(Status.pending) self.set_status(Status.pending)
@ -641,8 +583,7 @@ class Result:
fg_color=termstr.Color.white, bg_color=status_color, is_bold=True)) fg_color=termstr.Color.white, bg_color=status_color, is_bold=True))
def appearance_min(self): def appearance_min(self):
return ([self._get_cursor() if self.is_highlighted else return [self._get_cursor() if self.is_highlighted else STATUS_TO_TERMSTR[self.status]]
STATUS_TO_TERMSTR[self.status]])
def get_pages_dir(self): def get_pages_dir(self):
return self.pickle_path() + ".pages" return self.pickle_path() + ".pages"
@ -657,8 +598,7 @@ class Result:
def as_html(self): def as_html(self):
html, styles = termstr.TermStr( html, styles = termstr.TermStr(
STATUS_TO_TERMSTR[self.status]).as_html() STATUS_TO_TERMSTR[self.status]).as_html()
return (f'<a title="{self.tool.__name__}" ' return (f'<a title="{self.tool.__name__}" href="{self.path}/{self.tool.__name__}" '
f'href="{self.path}/{self.tool.__name__}" '
f'target="listing">{html}</a>', styles) f'target="listing">{html}</a>', styles)
@ -716,8 +656,7 @@ def splitext(path):
@functools.lru_cache() @functools.lru_cache()
def is_tool_available(tool): def is_tool_available(tool):
if (hasattr(tool, "command") and tool.command.startswith( if (hasattr(tool, "command") and tool.command.startswith(f"{PYTHON_EXECUTABLE} -m ")):
f"{PYTHON_EXECUTABLE} -m ")):
return importlib.util.find_spec(tool.command.split()[2]) is not None return importlib.util.find_spec(tool.command.split()[2]) is not None
try: try:
return all(shutil.which(executable) for executable in tool.executables) return all(shutil.which(executable) for executable in tool.executables)
@ -726,8 +665,7 @@ def is_tool_available(tool):
def tools_for_path(path): def tools_for_path(path):
git_tools = ([git_diff, git_blame, git_log] git_tools = [git_diff, git_blame, git_log] if os.path.exists(".git") else []
if os.path.exists(".git") else [])
root, ext = splitext(path) root, ext = splitext(path)
extra_tools = [] if ext == "" else _tools_for_extension().get(ext[1:], []) extra_tools = [] if ext == "" else _tools_for_extension().get(ext[1:], [])
tools = generic_tools() + git_tools + extra_tools tools = generic_tools() + git_tools + extra_tools
@ -764,10 +702,8 @@ def path_colored(path):
else: else:
dirname = dirname + os.path.sep dirname = dirname + os.path.sep
dir_style = _charstyle_of_path(os.path.sep) dir_style = _charstyle_of_path(os.path.sep)
parts = [termstr.TermStr(part, dir_style) parts = [termstr.TermStr(part, dir_style) for part in dirname.split(os.path.sep)]
for part in dirname.split(os.path.sep)] path_sep = termstr.TermStr(os.path.sep).fg_color(termstr.Color.grey_100)
path_sep = termstr.TermStr(os.path.sep).fg_color(
termstr.Color.grey_100)
dir_name = fill3.join(path_sep, parts) dir_name = fill3.join(path_sep, parts)
return dir_name + termstr.TermStr(basename, char_style) return dir_name + termstr.TermStr(basename, char_style)

View file

@ -47,9 +47,8 @@ def make_listing_page(url_path):
result = index[(path, tool_name)] result = index[(path, tool_name)]
tool = getattr(tools, tool_name) tool = getattr(tools, tool_name)
tool_name_colored = tools.tool_name_colored(tool, path) tool_name_colored = tools.tool_name_colored(tool, path)
header = fill3.appearance_as_html( header = fill3.appearance_as_html([tools.path_colored(path) + " - " + tool_name_colored,
[tools.path_colored(path) + " - " + tool_name_colored, termstr.TermStr(" ").underline() * 100])
termstr.TermStr(" ").underline() * 100])
body = fill3.appearance_as_html(result.appearance_min()) body = fill3.appearance_as_html(result.appearance_min())
return make_page(header + body, f"{path} - {tool_name}") return make_page(header + body, f"{path} - {tool_name}")
@ -90,13 +89,11 @@ def make_main_page(project_name):
def make_summary_page(project_name, summary): def make_summary_page(project_name, summary):
summary_html, summary_styles = summary.as_html() summary_html, summary_styles = summary.as_html()
body_html = ("\n".join(style.as_html() for style in summary_styles) body_html = "\n".join(style.as_html() for style in summary_styles) + "\n" + summary_html
+ "\n" + summary_html)
return make_page(body_html, "Summary of " + project_name) return make_page(body_html, "Summary of " + project_name)
def run(server_class=http.server.HTTPServer, handler_class=Webserver, def run(server_class=http.server.HTTPServer, handler_class=Webserver, port=8080):
port=8080):
server_address = ("", port) server_address = ("", port)
httpd = server_class(server_address, handler_class) httpd = server_class(server_address, handler_class)
print("Starting httpd…") print("Starting httpd…")
@ -104,8 +101,7 @@ def run(server_class=http.server.HTTPServer, handler_class=Webserver,
def get_summary(project_path): def get_summary(project_path):
pickle_path = os.path.join(project_path, tools.CACHE_PATH, pickle_path = os.path.join(project_path, tools.CACHE_PATH, "summary.pickle")
"summary.pickle")
with gzip.open(pickle_path, "rb") as file_: with gzip.open(pickle_path, "rb") as file_:
screen = pickle.load(file_) screen = pickle.load(file_)
summary = screen._summary summary = screen._summary

View file

@ -26,9 +26,8 @@ class Worker:
async def create_process(self): async def create_process(self):
create = asyncio.create_subprocess_exec( create = asyncio.create_subprocess_exec(
"eris-worker", stdin=asyncio.subprocess.PIPE, "eris-worker", stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, preexec_fn=os.setsid)
preexec_fn=os.setsid)
self.process = await create self.process = await create
pid_line = await self.process.stdout.readline() pid_line = await self.process.stdout.readline()
self.child_pgid = int(pid_line.strip()) self.child_pgid = int(pid_line.strip())
@ -46,8 +45,7 @@ class Worker:
break break
return tools.Status(int(data)) return tools.Status(int(data))
async def job_runner(self, screen, summary, log, jobs_added_event, async def job_runner(self, screen, summary, log, jobs_added_event, appearance_changed_event):
appearance_changed_event):
await self.create_process() await self.create_process()
while True: while True:
await jobs_added_event.wait() await jobs_added_event.wait()

View file

@ -9,8 +9,7 @@ except ImportError:
setup(name="eris", setup(name="eris",
version="2021.10.18", version="2021.10.18",
description=("Eris maintains an up-to-date set of reports for every" description=("Eris maintains an up-to-date set of reports for every file in a codebase."),
" file in a codebase."),
url="https://github.com/ahamilton/eris", url="https://github.com/ahamilton/eris",
author="Andrew Hamilton", author="Andrew Hamilton",
author_email="and_hamilton@yahoo.com", author_email="and_hamilton@yahoo.com",
@ -18,8 +17,7 @@ setup(name="eris",
packages=["eris"], packages=["eris"],
py_modules=["lscolors", "sorted_collection"], py_modules=["lscolors", "sorted_collection"],
package_data={"eris": ["LS_COLORS.sh", "tools.toml"]}, package_data={"eris": ["LS_COLORS.sh", "tools.toml"]},
entry_points={"console_scripts": entry_points={"console_scripts": ["eris=eris.__main__:entry_point",
["eris=eris.__main__:entry_point", "eris-worker=eris.worker:main",
"eris-worker=eris.worker:main", "eris-webserver=eris.webserver:main",
"eris-webserver=eris.webserver:main", "pydoc_color=eris.pydoc_color:main"]})
"pydoc_color=eris.pydoc_color:main"]})

View file

@ -67,16 +67,12 @@ class ToolsTestCase(unittest.TestCase):
def test_metadata(self): def test_metadata(self):
mock_stat_result = unittest.mock.Mock( mock_stat_result = unittest.mock.Mock(
st_mode=0o755, st_mtime=1454282045, st_ctime=1454282045, st_mode=0o755, st_mtime=1454282045, st_ctime=1454282045, st_atime=1454282047,
st_atime=1454282047, st_size=12, st_uid=1111, st_gid=1111, st_size=12, st_uid=1111, st_gid=1111, st_nlink=2)
st_nlink=2)
mock_pw_entry = unittest.mock.Mock(pw_name="foo") mock_pw_entry = unittest.mock.Mock(pw_name="foo")
with unittest.mock.patch.object(os, "stat", with unittest.mock.patch.object(os, "stat", return_value=mock_stat_result):
return_value=mock_stat_result): with unittest.mock.patch.object(tools.pwd, "getpwuid", return_value=mock_pw_entry):
with unittest.mock.patch.object(tools.pwd, "getpwuid", self._test_tool(tools.metadata, [("hi3.py", tools.Status.ok)])
return_value=mock_pw_entry):
self._test_tool(tools.metadata,
[("hi3.py", tools.Status.ok)])
def test_contents(self): def test_contents(self):
self._test_tool(tools.contents, [("hi3.py", tools.Status.ok)]) self._test_tool(tools.contents, [("hi3.py", tools.Status.ok)])
@ -107,19 +103,16 @@ class ToolsTestCase(unittest.TestCase):
self._test_tool(tools.python_mccabe, self.HI_OK) self._test_tool(tools.python_mccabe, self.HI_OK)
def test_perl_syntax(self): def test_perl_syntax(self):
self._test_tool(tools.perl_syntax, self._test_tool(tools.perl_syntax, [("perl.pl", tools.Status.ok)])
[("perl.pl", tools.Status.ok)])
def test_c_syntax_gcc(self): def test_c_syntax_gcc(self):
self._test_tool(tools.c_syntax_gcc, [("hello.c", tools.Status.ok)]) self._test_tool(tools.c_syntax_gcc, [("hello.c", tools.Status.ok)])
def test_objdump_headers(self): def test_objdump_headers(self):
self._test_tool(tools.objdump_headers, self._test_tool(tools.objdump_headers, [("rotatingtree.o", tools.Status.ok)])
[("rotatingtree.o", tools.Status.ok)])
def test_objdump_disassemble(self): def test_objdump_disassemble(self):
self._test_tool(tools.objdump_disassemble, self._test_tool(tools.objdump_disassemble, [("rotatingtree.o", tools.Status.problem)])
[("rotatingtree.o", tools.Status.problem)])
def test_readelf(self): def test_readelf(self):
self._test_tool(tools.readelf, [("rotatingtree.o", tools.Status.ok)]) self._test_tool(tools.readelf, [("rotatingtree.o", tools.Status.ok)])
@ -148,8 +141,7 @@ class ToolsTestCase(unittest.TestCase):
def test_pil(self): def test_pil(self):
for extension in ["png", "jpg", "gif", "bmp", "ppm", "tiff", "tga"]: for extension in ["png", "jpg", "gif", "bmp", "ppm", "tiff", "tga"]:
self._test_tool(tools.pil, [("circle." + extension, self._test_tool(tools.pil, [("circle." + extension, tools.Status.ok)])
tools.Status.ok)])
class LruCacheWithEvictionTestCase(unittest.TestCase): class LruCacheWithEvictionTestCase(unittest.TestCase):

View file

@ -14,15 +14,13 @@ import termstr
def appearance_is_valid(appearance): def appearance_is_valid(appearance):
return (all(isinstance(line, (str, termstr.TermStr)) and len(line) > 0 return (all(isinstance(line, (str, termstr.TermStr)) and len(line) > 0 for line in appearance)
for line in appearance) and and len(set(len(line) for line in appearance)) < 2)
len(set(len(line) for line in appearance)) < 2)
def appearance_resize(appearance, dimensions, pad_char=" "): def appearance_resize(appearance, dimensions, pad_char=" "):
width, height = dimensions width, height = dimensions
result = [line[:width].ljust(width, pad_char) result = [line[:width].ljust(width, pad_char) for line in appearance[:height]]
for line in appearance[:height]]
if len(result) < height: if len(result) < height:
result.extend([pad_char * width] * (height - len(result))) result.extend([pad_char * width] * (height - len(result)))
return result return result
@ -82,18 +80,14 @@ class Row:
widths = self.widths_func(self.widgets, width) widths = self.widths_func(self.widgets, width)
assert sum(widths) == width, (sum(widths), width) assert sum(widths) == width, (sum(widths), width)
return join_horizontal([column_widget.appearance((item_width, height)) return join_horizontal([column_widget.appearance((item_width, height))
for column_widget, item_width for column_widget, item_width in zip(self.widgets, widths)])
in zip(self.widgets, widths)])
def appearance_min(self): def appearance_min(self):
appearances = [column_widget.appearance_min() appearances = [column_widget.appearance_min() for column_widget in self.widgets]
for column_widget in self.widgets] dimensions = [appearance_dimensions(appearance) for appearance in appearances]
dimensions = [appearance_dimensions(appearance)
for appearance in appearances]
max_height = max(height for width, height in dimensions) max_height = max(height for width, height in dimensions)
return join_horizontal([ return join_horizontal([appearance_resize(appearance, (width, max_height))
appearance_resize(appearance, (width, max_height)) for appearance, (width, height) in zip(appearances, dimensions)])
for appearance, (width, height) in zip(appearances, dimensions)])
def even_partition(row_widgets, height): def even_partition(row_widgets, height):
@ -115,8 +109,7 @@ def join_vertical(appearances):
class Column: class Column:
def __init__(self, widgets, partition_func=even_partition, def __init__(self, widgets, partition_func=even_partition, background_char=" "):
background_char=" "):
self.widgets = widgets self.widgets = widgets
self.partition_func = partition_func self.partition_func = partition_func
self.background_char = background_char self.background_char = background_char
@ -128,19 +121,16 @@ class Column:
heights = self.partition_func(self.widgets, height) heights = self.partition_func(self.widgets, height)
assert sum(heights) == height, (sum(heights), height) assert sum(heights) == height, (sum(heights), height)
return join_vertical([row_widget.appearance((width, item_height)) return join_vertical([row_widget.appearance((width, item_height))
for row_widget, item_height for row_widget, item_height in zip(self.widgets, heights)])
in zip(self.widgets, heights)])
def _appearance_list(self, widgets): def _appearance_list(self, widgets):
if widgets == []: if widgets == []:
return [] return []
appearances = [row_widget.appearance_min() for row_widget in widgets] appearances = [row_widget.appearance_min() for row_widget in widgets]
dimensions = [appearance_dimensions(appearance) dimensions = [appearance_dimensions(appearance) for appearance in appearances]
for appearance in appearances]
max_width = max(width for width, height in dimensions) max_width = max(width for width, height in dimensions)
padded_appearances = [ padded_appearances = [appearance_resize(appearance, (max_width, height))
appearance_resize(appearance, (max_width, height)) for appearance, (width, height) in zip(appearances, dimensions)]
for appearance, (width, height) in zip(appearances, dimensions)]
result = [] result = []
for appearance in padded_appearances: for appearance in padded_appearances:
result.extend(appearance) result.extend(appearance)
@ -170,44 +160,37 @@ class ScrollBar:
DEFAULT_BAR_COLOR = termstr.Color.grey_100 DEFAULT_BAR_COLOR = termstr.Color.grey_100
DEFAULT_BACKGROUND_COLOR = termstr.Color.grey_30 DEFAULT_BACKGROUND_COLOR = termstr.Color.grey_30
def __init__(self, is_horizontal, interval=(0, 0), bar_color=None, def __init__(self, is_horizontal, interval=(0, 0), bar_color=None, background_color=None):
background_color=None):
self._is_horizontal = is_horizontal self._is_horizontal = is_horizontal
self.interval = interval self.interval = interval
bar_color = bar_color or ScrollBar.DEFAULT_BAR_COLOR bar_color = bar_color or ScrollBar.DEFAULT_BAR_COLOR
background_color = (background_color or background_color = background_color or ScrollBar.DEFAULT_BACKGROUND_COLOR
ScrollBar.DEFAULT_BACKGROUND_COLOR)
self._bar_char = termstr.TermStr("").fg_color(bar_color) self._bar_char = termstr.TermStr("").fg_color(bar_color)
self._background_char = termstr.TermStr(" ").bg_color(background_color) self._background_char = termstr.TermStr(" ").bg_color(background_color)
if self._is_horizontal: if self._is_horizontal:
bar_color, background_color = background_color, bar_color bar_color, background_color = background_color, bar_color
self._partial_chars = [(termstr.TermStr(char).fg_color( self._partial_chars = [
bar_color).bg_color(background_color), (termstr.TermStr(char).fg_color(bar_color).bg_color(background_color),
termstr.TermStr(char).fg_color( termstr.TermStr(char).fg_color(background_color).bg_color(bar_color))
background_color).bg_color(bar_color)) for char in self._PARTIAL_CHARS[self._is_horizontal]]
for char in self._PARTIAL_CHARS[self._is_horizontal]]
def appearance(self, dimensions): def appearance(self, dimensions):
width, height = dimensions width, height = dimensions
assert width == 1 or height == 1, (width, height) assert width == 1 or height == 1, (width, height)
length = width if self._is_horizontal else height length = width if self._is_horizontal else height
assert all(0 <= fraction <= 1 for fraction in self.interval), \ assert all(0 <= fraction <= 1 for fraction in self.interval), self.interval
self.interval
(start_index, start_remainder), (end_index, end_remainder) = \ (start_index, start_remainder), (end_index, end_remainder) = \
[divmod(fraction * length * 8, 8) for fraction in self.interval] [divmod(fraction * length * 8, 8) for fraction in self.interval]
start_index, end_index = int(start_index), int(end_index) start_index, end_index = int(start_index), int(end_index)
start_remainder, end_remainder = \ start_remainder, end_remainder = int(start_remainder), int(end_remainder)
int(start_remainder), int(end_remainder)
if start_index == end_index: if start_index == end_index:
end_index, end_remainder = start_index + 1, start_remainder end_index, end_remainder = start_index + 1, start_remainder
elif end_index == start_index + 1: elif end_index == start_index + 1:
end_remainder = max(start_remainder, end_remainder) end_remainder = max(start_remainder, end_remainder)
bar = (self._background_char * start_index + bar = (self._background_char * start_index + self._partial_chars[start_remainder][0] +
self._partial_chars[start_remainder][0] +
self._bar_char * (end_index - start_index - 1) + self._bar_char * (end_index - start_index - 1) +
self._partial_chars[end_remainder][1] + self._partial_chars[end_remainder][1] +
self._background_char * (length - end_index - 1)) self._background_char * (length - end_index - 1))[:length]
bar = bar[:length]
return [bar] if self._is_horizontal else [char for char in bar] return [bar] if self._is_horizontal else [char for char in bar]
@ -222,8 +205,7 @@ class Portal:
def _scroll_half_pages(self, dx, dy): def _scroll_half_pages(self, dx, dy):
x, y = self.position x, y = self.position
width, height = self.last_dimensions width, height = self.last_dimensions
self.position = (max(x + dx * (width // 2), 0), self.position = (max(x + dx * (width // 2), 0), max(y + dy * (height // 2), 0))
max(y + dy * (height // 2), 0))
def scroll_up(self): def scroll_up(self):
self._scroll_half_pages(0, -1) self._scroll_half_pages(0, -1)
@ -251,8 +233,7 @@ class Portal:
class View: class View:
def __init__(self, portal, horizontal_scrollbar, vertical_scrollbar, def __init__(self, portal, horizontal_scrollbar, vertical_scrollbar, hide_scrollbars=True):
hide_scrollbars=True):
self.portal = portal self.portal = portal
self.horizontal_scrollbar = horizontal_scrollbar self.horizontal_scrollbar = horizontal_scrollbar
self.vertical_scrollbar = vertical_scrollbar self.vertical_scrollbar = vertical_scrollbar
@ -260,8 +241,7 @@ class View:
@classmethod @classmethod
def from_widget(cls, widget): def from_widget(cls, widget):
return cls(Portal(widget), ScrollBar(is_horizontal=True), return cls(Portal(widget), ScrollBar(is_horizontal=True), ScrollBar(is_horizontal=False))
ScrollBar(is_horizontal=False))
@property @property
def position(self): def position(self):
@ -282,41 +262,32 @@ class View:
def appearance(self, dimensions): def appearance(self, dimensions):
width, height = dimensions width, height = dimensions
try: try:
full_width, full_height = (self.portal.widget. full_width, full_height = self.portal.widget.appearance_dimensions()
appearance_dimensions())
except AttributeError: except AttributeError:
full_appearance = self.portal.widget.appearance_min() full_appearance = self.portal.widget.appearance_min()
full_width, full_height = appearance_dimensions(full_appearance) full_width, full_height = appearance_dimensions(full_appearance)
if full_width == 0 or full_height == 0: if full_width == 0 or full_height == 0:
return self.portal.appearance(dimensions) return self.portal.appearance(dimensions)
x, y = self.portal.position x, y = self.portal.position
hide_scrollbar_vertical = (self.hide_scrollbars and hide_scrollbar_vertical = self.hide_scrollbars and full_height <= height and y == 0
full_height <= height and y == 0) hide_scrollbar_horizontal = self.hide_scrollbars and full_width <= width and x == 0
hide_scrollbar_horizontal = (self.hide_scrollbars and
full_width <= width and x == 0)
if not hide_scrollbar_horizontal: if not hide_scrollbar_horizontal:
full_width = max(full_width, x + width) full_width = max(full_width, x + width)
self.horizontal_scrollbar.interval = (x / full_width, self.horizontal_scrollbar.interval = (x / full_width, (x + width) / full_width)
(x + width) / full_width)
height -= 1 height -= 1
if not hide_scrollbar_vertical: if not hide_scrollbar_vertical:
full_height = max(full_height, y + height) full_height = max(full_height, y + height)
self.vertical_scrollbar.interval = (y / full_height, self.vertical_scrollbar.interval = (y / full_height, (y + height) / full_height)
(y + height) / full_height)
width -= 1 width -= 1
portal_appearance = self.portal.appearance((width, height)) portal_appearance = self.portal.appearance((width, height))
if hide_scrollbar_vertical: if hide_scrollbar_vertical:
result = portal_appearance result = portal_appearance
else: else:
scrollbar_v_appearance = self.vertical_scrollbar.appearance( scrollbar_v_appearance = self.vertical_scrollbar.appearance((1, height))
(1, height)) result = join_horizontal([portal_appearance, scrollbar_v_appearance])
result = join_horizontal([portal_appearance,
scrollbar_v_appearance])
if not hide_scrollbar_horizontal: if not hide_scrollbar_horizontal:
scrollbar_h_appearance = self.horizontal_scrollbar.appearance( scrollbar_h_appearance = self.horizontal_scrollbar.appearance((width, 1))
(width, 1)) result.append(scrollbar_h_appearance[0] + ("" if hide_scrollbar_vertical else " "))
result.append(scrollbar_h_appearance[0] +
("" if hide_scrollbar_vertical else " "))
return result return result
@ -350,8 +321,7 @@ class Table:
def appearance_min(self): def appearance_min(self):
if self._widgets == []: if self._widgets == []:
return [] return []
appearances = [[cell.appearance_min() for cell in row] appearances = [[cell.appearance_min() for cell in row] for row in self._widgets]
for row in self._widgets]
row_heights = [0] * len(self._widgets) row_heights = [0] * len(self._widgets)
column_widths = [0] * len(self._widgets[0]) column_widths = [0] * len(self._widgets[0])
for y, row in enumerate(appearances): for y, row in enumerate(appearances):
@ -397,8 +367,7 @@ class Border:
title_bar = padded_title.center(content_width, self.top) title_bar = padded_title.center(content_width, self.top)
result = [self.top_left + title_bar + self.top_right] result = [self.top_left + title_bar + self.top_right]
result.extend(self.left + line + self.right for line in body_content) result.extend(self.left + line + self.right for line in body_content)
result.append(self.bottom_left + self.bottom * content_width + result.append(self.bottom_left + self.bottom * content_width + self.bottom_right)
self.bottom_right)
return result return result
def appearance_min(self): def appearance_min(self):
@ -450,12 +419,9 @@ def draw_screen(widget):
def patch_screen(widget): def patch_screen(widget):
global _last_appearance global _last_appearance
appearance = widget.appearance(os.get_terminal_size()) appearance = widget.appearance(os.get_terminal_size())
zip_func = (itertools.zip_longest zip_func = (itertools.zip_longest if len(appearance) > len(_last_appearance) else zip)
if len(appearance) > len(_last_appearance) else zip) changed_lines = (str(terminal.move(0, row_index)) + line for row_index, (line, old_line)
changed_lines = (str(terminal.move(0, row_index)) + line in enumerate(zip_func(appearance, _last_appearance)) if line != old_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) print(*changed_lines, sep="", end="", flush=True)
_last_appearance = appearance _last_appearance = appearance
@ -491,12 +457,10 @@ def context(loop, appearance_changed_event, screen_widget, exit_loop=None):
appearance_changed_event.set() appearance_changed_event.set()
if exit_loop is None: if exit_loop is None:
exit_loop = loop.stop exit_loop = loop.stop
with signal_handler(loop, signal.SIGWINCH, with (signal_handler(loop, signal.SIGWINCH, lambda: draw_screen(screen_widget)),
lambda: draw_screen(screen_widget)), \ signal_handler(loop, signal.SIGINT, exit_loop),
signal_handler(loop, signal.SIGINT, exit_loop), \ signal_handler(loop, signal.SIGTERM, exit_loop), terminal.alternate_buffer(),
signal_handler(loop, signal.SIGTERM, exit_loop), \ terminal.interactive(), terminal.mouse_tracking()):
terminal.alternate_buffer(), terminal.interactive(), \
terminal.mouse_tracking():
update_task = loop.create_task( update_task = loop.create_task(
update_screen(screen_widget, appearance_changed_event)) update_screen(screen_widget, appearance_changed_event))
try: try:

View file

@ -18,8 +18,7 @@ class WidgetTests(unittest.TestCase):
def test_rows_widget(self): def test_rows_widget(self):
rows = fill3.Row([self.TEXT_A, self.TEXT_B]) rows = fill3.Row([self.TEXT_A, self.TEXT_B])
self.assert_string(rows.appearance_min(), "AB") self.assert_string(rows.appearance_min(), "AB")
rows = fill3.Row([fill3.Filler(self.TEXT_A), rows = fill3.Row([fill3.Filler(self.TEXT_A), fill3.Filler(self.TEXT_B)])
fill3.Filler(self.TEXT_B)])
self.assert_string(rows.appearance((4, 1)), "A B ") self.assert_string(rows.appearance((4, 1)), "A B ")
def test_columns_widget(self): def test_columns_widget(self):
@ -46,25 +45,20 @@ class WidgetTests(unittest.TestCase):
"│A│\n" "│A│\n"
"└─┘") "└─┘")
for empty_contents in [fill3.Filler(fill3.Text("")), fill3.Column([])]: for empty_contents in [fill3.Filler(fill3.Text("")), fill3.Column([])]:
self.assert_string(fill3.Border(empty_contents).appearance((2, 2)), self.assert_string(fill3.Border(empty_contents).appearance((2, 2)), "┌┐\n"
"┌┐\n" "└┘")
"└┘") self.assert_string(fill3.Border(fill3.Column([])).appearance_min(), "┌┐\n"
self.assert_string(fill3.Border(fill3.Column([])).appearance_min(), "└┘")
"┌┐\n" self.assert_string(fill3.Border(empty_contents).appearance((3, 3)), "┌─┐\n"
"└┘") "│ │\n"
self.assert_string(fill3.Border(empty_contents).appearance((3, 3)), "└─┘")
"┌─┐\n"
"│ │\n"
"└─┘")
text = fill3.Text("abcdef") text = fill3.Text("abcdef")
self.assert_string(fill3.Border(text, title="AB").appearance((8, 3)), self.assert_string(fill3.Border(text, title="AB").appearance((8, 3)), "┌─ AB ─┐\n"
"┌─ AB ─┐\n" "│abcdef│\n"
"│abcdef│\n" "└──────┘")
"└──────┘") self.assert_string(fill3.Border(text, title="ABC").appearance((6, 3)), "┌ …C ┐\n"
self.assert_string(fill3.Border(text, title="ABC").appearance((6, 3)), "│abcd│\n"
"┌ …C ┐\n" "└────┘")
"│abcd│\n"
"└────┘")
def test_placeholder_widget(self): def test_placeholder_widget(self):
placeholder = fill3.Placeholder(self.TEXT_A) placeholder = fill3.Placeholder(self.TEXT_A)
@ -73,12 +67,9 @@ class WidgetTests(unittest.TestCase):
self.assert_string(placeholder.appearance_min(), "B") self.assert_string(placeholder.appearance_min(), "B")
def assert_string2(self, appearance, expected_string): def assert_string2(self, appearance, expected_string):
self.assertEqual( self.assertEqual(("\n".join(line.data for line in appearance),
("\n".join(line.data for line in appearance), "".join("i" if style.fg_color == fill3.ScrollBar.DEFAULT_BACKGROUND_COLOR else " "
"".join("i" if style.fg_color == for line in appearance for style in line.style)), expected_string)
fill3.ScrollBar.DEFAULT_BACKGROUND_COLOR else " "
for line in appearance for style in line.style)),
expected_string)
def test_scroll_bar(self): def test_scroll_bar(self):
scroll_bar = fill3.ScrollBar(is_horizontal=True) scroll_bar = fill3.ScrollBar(is_horizontal=True)
@ -92,7 +83,6 @@ class WidgetTests(unittest.TestCase):
self.assert_string2(scroll_bar.appearance((4, 1)), ("", " i ")) self.assert_string2(scroll_bar.appearance((4, 1)), ("", " i "))
scroll_bar.interval = (0, 0.75) scroll_bar.interval = (0, 0.75)
self.assert_string2(scroll_bar.appearance((2, 1)), ("", "i ")) self.assert_string2(scroll_bar.appearance((2, 1)), ("", "i "))
scroll_bar = fill3.ScrollBar(is_horizontal=False) scroll_bar = fill3.ScrollBar(is_horizontal=False)
self.assertEqual(scroll_bar.interval, (0, 0)) self.assertEqual(scroll_bar.interval, (0, 0))
self.assert_string2(scroll_bar.appearance((1, 1)), ("", " ")) self.assert_string2(scroll_bar.appearance((1, 1)), ("", " "))
@ -119,7 +109,7 @@ class WidgetTests(unittest.TestCase):
table = fill3.Table([[self.TEXT_A, self.TEXT_B]]) table = fill3.Table([[self.TEXT_A, self.TEXT_B]])
self.assert_string(table.appearance_min(), "AB") self.assert_string(table.appearance_min(), "AB")
table = fill3.Table([[self.TEXT_A, self.TEXT_B], table = fill3.Table([[self.TEXT_A, self.TEXT_B],
[self.TEXT_B, self.TEXT_A]]) [self.TEXT_B, self.TEXT_A]])
self.assert_string(table.appearance_min(), "AB\n" self.assert_string(table.appearance_min(), "AB\n"
"BA") "BA")
label_foo = fill3.Text("FOO") label_foo = fill3.Text("FOO")

View file

@ -41,12 +41,10 @@ def _parse_ls_colors(ls_codes):
_DEFAULT_COLOR_CODES = \ _DEFAULT_COLOR_CODES = \
{BLOCK_DEVICE_KEY: '01;33', SYMLINK_KEY: '01;36', {BLOCK_DEVICE_KEY: '01;33', SYMLINK_KEY: '01;36', STICKY_OTHER_WRITABLE_KEY: '30;42',
STICKY_OTHER_WRITABLE_KEY: '30;42', DIRECTORY_KEY: '01;34', DIRECTORY_KEY: '01;34', SETUID_KEY: '37;41', CHARACTER_DEVICE_KEY: '01;33',
SETUID_KEY: '37;41', CHARACTER_DEVICE_KEY: '01;33', SOCKET_KEY: '01;35', SOCKET_KEY: '01;35', EXECUTABLE_KEY: '01;32', STICKY_KEY: '37;44',
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'}
OTHER_WRITABLE_KEY: '34;42', PIPE_KEY: '33', SETGUID_KEY: '30;43',
ORPHAN_KEY: '40;31;01'}
def get_color_codes(environment): def get_color_codes(environment):
@ -55,8 +53,7 @@ def get_color_codes(environment):
try: try:
return _parse_ls_colors(environment["LS_COLORS"]) return _parse_ls_colors(environment["LS_COLORS"])
except Exception: except Exception:
syslog.syslog("Syntax error in LS_COLORS environment variable. " syslog.syslog("Syntax error in LS_COLORS environment variable. Using default colors.")
"Using default colors.")
return _DEFAULT_COLOR_CODES return _DEFAULT_COLOR_CODES
@ -68,8 +65,7 @@ def color_key_for_path(path, color_codes, is_link_target=True):
elif os.path.islink(path): elif os.path.islink(path):
if is_link_target: if is_link_target:
try: try:
link_path = os.path.join(os.path.dirname(path), link_path = os.path.join(os.path.dirname(path), os.readlink(path))
os.readlink(path))
file_stat = os.stat(link_path) file_stat = os.stat(link_path)
except OSError: except OSError:
return ORPHAN_KEY return ORPHAN_KEY
@ -83,16 +79,15 @@ def color_key_for_path(path, color_codes, is_link_target=True):
return SETUID_KEY return SETUID_KEY
elif mode & stat.S_ISGID and SETGUID_KEY in color_codes: elif mode & stat.S_ISGID and SETGUID_KEY in color_codes:
return SETGUID_KEY return SETGUID_KEY
elif ((mode & stat.S_IXUSR or mode & stat.S_IXGRP or elif ((mode & stat.S_IXUSR or mode & stat.S_IXGRP or mode & stat.S_IXOTH)
mode & stat.S_IXOTH) and EXECUTABLE_KEY in color_codes): and EXECUTABLE_KEY in color_codes):
return EXECUTABLE_KEY return EXECUTABLE_KEY
elif file_stat.st_nlink > 1 and MULTI_HARDLINK_KEY in color_codes: elif file_stat.st_nlink > 1 and MULTI_HARDLINK_KEY in color_codes:
return MULTI_HARDLINK_KEY return MULTI_HARDLINK_KEY
else: else:
return FILE_KEY return FILE_KEY
elif stat.S_ISDIR(mode): elif stat.S_ISDIR(mode):
if (mode & stat.S_ISVTX and mode & stat.S_IWOTH and if mode & stat.S_ISVTX and mode & stat.S_IWOTH and STICKY_OTHER_WRITABLE_KEY in color_codes:
STICKY_OTHER_WRITABLE_KEY in color_codes):
return STICKY_OTHER_WRITABLE_KEY return STICKY_OTHER_WRITABLE_KEY
elif (mode & stat.S_IWOTH) != 0 and OTHER_WRITABLE_KEY in color_codes: elif (mode & stat.S_IWOTH) != 0 and OTHER_WRITABLE_KEY in color_codes:
return OTHER_WRITABLE_KEY return OTHER_WRITABLE_KEY
@ -100,8 +95,7 @@ def color_key_for_path(path, color_codes, is_link_target=True):
return STICKY_KEY return STICKY_KEY
else: else:
return DIRECTORY_KEY return DIRECTORY_KEY
for test_function, color_key in [(stat.S_ISFIFO, PIPE_KEY), for test_function, color_key in [(stat.S_ISFIFO, PIPE_KEY), (stat.S_ISSOCK, SOCKET_KEY),
(stat.S_ISSOCK, SOCKET_KEY),
(stat.S_ISBLK, BLOCK_DEVICE_KEY), (stat.S_ISBLK, BLOCK_DEVICE_KEY),
(stat.S_ISCHR, CHARACTER_DEVICE_KEY)]: (stat.S_ISCHR, CHARACTER_DEVICE_KEY)]:
if test_function(mode): if test_function(mode):
@ -119,12 +113,10 @@ def color_code_for_path(path, color_codes):
return extension return extension
elif len(parts) > 2: elif len(parts) > 2:
for extension in color_codes: for extension in color_codes:
if extension.startswith(".") and \ if extension.startswith(".") and basename.endswith(extension):
basename.endswith(extension):
return extension return extension
target_link = color_codes.get(SYMLINK_KEY, None) target_link = color_codes.get(SYMLINK_KEY, None)
color_key = color_key_for_path(path, color_codes, color_key = color_key_for_path(path, color_codes, target_link == "target")
target_link == "target")
if color_key == FILE_KEY: if color_key == FILE_KEY:
filename = os.path.basename(path) filename = os.path.basename(path)
if "." in filename: if "." in filename:

View file

@ -27,22 +27,18 @@ class ParseLsColorsTestCase(unittest.TestCase):
def test_parse_ls_colors(self): def test_parse_ls_colors(self):
self.assertRaises(AssertionError, lscolors._parse_ls_colors, "") self.assertRaises(AssertionError, lscolors._parse_ls_colors, "")
self.assertRaises(AssertionError, lscolors._parse_ls_colors, "::") self.assertRaises(AssertionError, lscolors._parse_ls_colors, "::")
self.assertEqual(lscolors._parse_ls_colors("*.awk=38;5;148;1"), self.assertEqual(lscolors._parse_ls_colors("*.awk=38;5;148;1"), {".awk": "38;5;148;1"})
{".awk": "38;5;148;1"})
self.assertEqual(lscolors._parse_ls_colors("*.tar.gz=38;5;148;1"), self.assertEqual(lscolors._parse_ls_colors("*.tar.gz=38;5;148;1"),
{".tar.gz": "38;5;148;1"}) {".tar.gz": "38;5;148;1"})
self.assertEqual( self.assertEqual(lscolors._parse_ls_colors("*.awk=38;5;148;1:di=38;5;30"),
lscolors._parse_ls_colors("*.awk=38;5;148;1:di=38;5;30"), {".awk": "38;5;148;1", "di": "38;5;30"})
{".awk": "38;5;148;1", "di": "38;5;30"})
class ColorKeyForFileTestCase(TempDirTestCase): class ColorKeyForFileTestCase(TempDirTestCase):
COLOR_CODES = {lscolors.OTHER_WRITABLE_KEY: "other writable", COLOR_CODES = {lscolors.OTHER_WRITABLE_KEY: "other writable",
lscolors.EXECUTABLE_KEY: "executable", lscolors.EXECUTABLE_KEY: "executable", lscolors.ORPHAN_KEY: "orphan",
lscolors.ORPHAN_KEY: "orphan", lscolors.SETGUID_KEY: "setguid", lscolors.SETUID_KEY: "setuid",
lscolors.SETGUID_KEY: "setguid",
lscolors.SETUID_KEY: "setuid",
lscolors.STICKY_KEY: "sticky", lscolors.STICKY_KEY: "sticky",
lscolors.STICKY_OTHER_WRITABLE_KEY: "sticky other writable", lscolors.STICKY_OTHER_WRITABLE_KEY: "sticky other writable",
lscolors.MULTI_HARDLINK_KEY: "multi hardlink", lscolors.MULTI_HARDLINK_KEY: "multi hardlink",
@ -52,166 +48,143 @@ class ColorKeyForFileTestCase(TempDirTestCase):
def test_color_key_for_path_without_extension(self): def test_color_key_for_path_without_extension(self):
executable_path = os.path.join(self.temp_dir, "foo") executable_path = os.path.join(self.temp_dir, "foo")
open(executable_path, "w").close() open(executable_path, "w").close()
self.assertEqual( self.assertEqual(lscolors.color_key_for_path(executable_path, self.COLOR_CODES),
lscolors.color_key_for_path(executable_path, self.COLOR_CODES), lscolors.FILE_KEY)
lscolors.FILE_KEY)
def test_color_key_for_path_with_extension(self): def test_color_key_for_path_with_extension(self):
awk_path = os.path.join(self.temp_dir, "test.awk") awk_path = os.path.join(self.temp_dir, "test.awk")
open(awk_path, "w").close() open(awk_path, "w").close()
self.assertEqual( self.assertEqual(lscolors.color_key_for_path(awk_path, self.COLOR_CODES),
lscolors.color_key_for_path(awk_path, self.COLOR_CODES), lscolors.FILE_KEY)
lscolors.FILE_KEY)
def test_color_key_for_path_with_double_extension(self): def test_color_key_for_path_with_double_extension(self):
tar_gz_path = os.path.join(self.temp_dir, "test.tar.gz") tar_gz_path = os.path.join(self.temp_dir, "test.tar.gz")
open(tar_gz_path, "w").close() open(tar_gz_path, "w").close()
self.assertEqual( self.assertEqual(lscolors.color_key_for_path(tar_gz_path, self.COLOR_CODES),
lscolors.color_key_for_path(tar_gz_path, self.COLOR_CODES), lscolors.FILE_KEY)
lscolors.FILE_KEY)
def test_color_code_for_directory(self): def test_color_code_for_directory(self):
self.assertEqual( self.assertEqual(lscolors.color_key_for_path(self.temp_dir, self.COLOR_CODES),
lscolors.color_key_for_path(self.temp_dir, self.COLOR_CODES), lscolors.DIRECTORY_KEY)
lscolors.DIRECTORY_KEY)
def test_color_code_for_directory_thats_other_writable(self): def test_color_code_for_directory_thats_other_writable(self):
mode = os.stat(self.temp_dir).st_mode mode = os.stat(self.temp_dir).st_mode
os.chmod(self.temp_dir, mode | stat.S_IWOTH) os.chmod(self.temp_dir, mode | stat.S_IWOTH)
self.assertEqual( self.assertEqual(lscolors.color_key_for_path(self.temp_dir, self.COLOR_CODES),
lscolors.color_key_for_path(self.temp_dir, self.COLOR_CODES), lscolors.OTHER_WRITABLE_KEY)
lscolors.OTHER_WRITABLE_KEY)
def test_color_code_for_executable(self): def test_color_code_for_executable(self):
executable_path = os.path.join(self.temp_dir, "a") executable_path = os.path.join(self.temp_dir, "a")
open(executable_path, "w").close() open(executable_path, "w").close()
os.chmod(executable_path, stat.S_IEXEC) os.chmod(executable_path, stat.S_IEXEC)
self.assertEqual( self.assertEqual(lscolors.color_key_for_path(executable_path, self.COLOR_CODES),
lscolors.color_key_for_path(executable_path, self.COLOR_CODES), lscolors.EXECUTABLE_KEY)
lscolors.EXECUTABLE_KEY)
def test_color_code_for_executable_with_extension(self): def test_color_code_for_executable_with_extension(self):
executable_path = os.path.join(self.temp_dir, "a.awk") executable_path = os.path.join(self.temp_dir, "a.awk")
open(executable_path, "w").close() open(executable_path, "w").close()
os.chmod(executable_path, stat.S_IEXEC) os.chmod(executable_path, stat.S_IEXEC)
self.assertEqual( self.assertEqual(lscolors.color_key_for_path(executable_path, self.COLOR_CODES),
lscolors.color_key_for_path(executable_path, self.COLOR_CODES), lscolors.EXECUTABLE_KEY)
lscolors.EXECUTABLE_KEY)
def test_color_code_for_setguid(self): def test_color_code_for_setguid(self):
setguid_path = os.path.join(self.temp_dir, "a") setguid_path = os.path.join(self.temp_dir, "a")
open(setguid_path, "w").close() open(setguid_path, "w").close()
os.chmod(setguid_path, stat.S_ISGID) os.chmod(setguid_path, stat.S_ISGID)
self.assertEqual( self.assertEqual(lscolors.color_key_for_path(setguid_path, self.COLOR_CODES),
lscolors.color_key_for_path(setguid_path, self.COLOR_CODES), lscolors.SETGUID_KEY)
lscolors.SETGUID_KEY)
def test_color_code_for_setuid(self): def test_color_code_for_setuid(self):
setuid_path = os.path.join(self.temp_dir, "a") setuid_path = os.path.join(self.temp_dir, "a")
open(setuid_path, "w").close() open(setuid_path, "w").close()
os.chmod(setuid_path, stat.S_ISUID) os.chmod(setuid_path, stat.S_ISUID)
self.assertEqual( self.assertEqual(lscolors.color_key_for_path(setuid_path, self.COLOR_CODES),
lscolors.color_key_for_path(setuid_path, self.COLOR_CODES), lscolors.SETUID_KEY)
lscolors.SETUID_KEY)
def test_color_code_for_broken_symlink(self): def test_color_code_for_broken_symlink(self):
symlink_path = os.path.join(self.temp_dir, "b") symlink_path = os.path.join(self.temp_dir, "b")
os.symlink(os.path.join(self.temp_dir, "a"), symlink_path) os.symlink(os.path.join(self.temp_dir, "a"), symlink_path)
self.assertEqual( self.assertEqual(lscolors.color_key_for_path(symlink_path, self.COLOR_CODES),
lscolors.color_key_for_path(symlink_path, self.COLOR_CODES), lscolors.ORPHAN_KEY)
lscolors.ORPHAN_KEY)
def test_color_code_for_good_symlink(self): def test_color_code_for_good_symlink(self):
symlink_path = os.path.join(self.temp_dir, "b") symlink_path = os.path.join(self.temp_dir, "b")
awk_path = os.path.join(self.temp_dir, "test.awk") awk_path = os.path.join(self.temp_dir, "test.awk")
open(awk_path, "w").close() open(awk_path, "w").close()
os.symlink(awk_path, symlink_path) os.symlink(awk_path, symlink_path)
self.assertEqual( self.assertEqual(lscolors.color_key_for_path(symlink_path, self.COLOR_CODES),
lscolors.color_key_for_path(symlink_path, self.COLOR_CODES), lscolors.FILE_KEY)
lscolors.FILE_KEY)
def test_color_code_for_pipe(self): def test_color_code_for_pipe(self):
pipe_path = os.path.join(self.temp_dir, "a") pipe_path = os.path.join(self.temp_dir, "a")
os.mkfifo(pipe_path) os.mkfifo(pipe_path)
self.assertEqual( self.assertEqual(lscolors.color_key_for_path(pipe_path, self.COLOR_CODES),
lscolors.color_key_for_path(pipe_path, self.COLOR_CODES), lscolors.PIPE_KEY)
lscolors.PIPE_KEY)
def test_color_code_for_character_device(self): def test_color_code_for_character_device(self):
character_device_path = "/dev/tty" character_device_path = "/dev/tty"
self.assertEqual( self.assertEqual(lscolors.color_key_for_path(character_device_path, self.COLOR_CODES),
lscolors.color_key_for_path(character_device_path, lscolors.CHARACTER_DEVICE_KEY)
self.COLOR_CODES),
lscolors.CHARACTER_DEVICE_KEY)
def test_color_code_for_sticky_directory(self): def test_color_code_for_sticky_directory(self):
mode = os.stat(self.temp_dir).st_mode mode = os.stat(self.temp_dir).st_mode
os.chmod(self.temp_dir, mode | stat.S_ISVTX) os.chmod(self.temp_dir, mode | stat.S_ISVTX)
self.assertEqual( self.assertEqual(lscolors.color_key_for_path(self.temp_dir, self.COLOR_CODES),
lscolors.color_key_for_path(self.temp_dir, self.COLOR_CODES), lscolors.STICKY_KEY)
lscolors.STICKY_KEY)
def test_color_code_for_sticky_and_other_writable(self): def test_color_code_for_sticky_and_other_writable(self):
mode = os.stat(self.temp_dir).st_mode mode = os.stat(self.temp_dir).st_mode
os.chmod(self.temp_dir, mode | stat.S_ISVTX | stat.S_IWOTH) os.chmod(self.temp_dir, mode | stat.S_ISVTX | stat.S_IWOTH)
self.assertEqual( self.assertEqual(lscolors.color_key_for_path(self.temp_dir, self.COLOR_CODES),
lscolors.color_key_for_path(self.temp_dir, self.COLOR_CODES), lscolors.STICKY_OTHER_WRITABLE_KEY)
lscolors.STICKY_OTHER_WRITABLE_KEY)
def test_color_code_for_socket(self): def test_color_code_for_socket(self):
socket_path = os.path.join(self.temp_dir, "socket") socket_path = os.path.join(self.temp_dir, "socket")
socket_ = socket.socket(socket.AF_UNIX) socket_ = socket.socket(socket.AF_UNIX)
socket_.bind(socket_path) socket_.bind(socket_path)
try: try:
self.assertEqual( self.assertEqual(lscolors.color_key_for_path(socket_path, self.COLOR_CODES),
lscolors.color_key_for_path(socket_path, self.COLOR_CODES), lscolors.SOCKET_KEY)
lscolors.SOCKET_KEY)
finally: finally:
socket_.close() socket_.close()
def test_color_code_for_missing_file(self): def test_color_code_for_missing_file(self):
missing_path = os.path.join(self.temp_dir, "a") missing_path = os.path.join(self.temp_dir, "a")
self.assertEqual( self.assertEqual(lscolors.color_key_for_path(missing_path, self.COLOR_CODES),
lscolors.color_key_for_path(missing_path, self.COLOR_CODES), lscolors.MISSING_KEY)
lscolors.MISSING_KEY)
def test_color_code_for_multi_hardlink(self): def test_color_code_for_multi_hardlink(self):
a_path = os.path.join(self.temp_dir, "a") a_path = os.path.join(self.temp_dir, "a")
open(a_path, "w").close() open(a_path, "w").close()
b_path = os.path.join(self.temp_dir, "b") b_path = os.path.join(self.temp_dir, "b")
os.link(a_path, b_path) os.link(a_path, b_path)
self.assertEqual( self.assertEqual(lscolors.color_key_for_path(a_path, self.COLOR_CODES),
lscolors.color_key_for_path(a_path, self.COLOR_CODES), lscolors.MULTI_HARDLINK_KEY)
lscolors.MULTI_HARDLINK_KEY)
class ColorCodeForFileTestCase(TempDirTestCase): class ColorCodeForFileTestCase(TempDirTestCase):
AWK_COLOR = "awk color" AWK_COLOR = "awk color"
TAR_GZ_COLOR = "tar gz color" TAR_GZ_COLOR = "tar gz color"
COLOR_CODES = { COLOR_CODES = {".awk": AWK_COLOR, ".tar.gz": TAR_GZ_COLOR}
".awk": AWK_COLOR, ".tar.gz": TAR_GZ_COLOR}
def test_color_code_for_path_without_extension(self): def test_color_code_for_path_without_extension(self):
file_path = os.path.join(self.temp_dir, "foo") file_path = os.path.join(self.temp_dir, "foo")
open(file_path, "w").close() open(file_path, "w").close()
self.assertEqual( self.assertEqual(lscolors.color_code_for_path(file_path, {"fi": "file color"}),
lscolors.color_code_for_path(file_path, {"fi": "file color"}), "file color")
"file color")
def test_color_code_for_path_with_extension(self): def test_color_code_for_path_with_extension(self):
awk_path = os.path.join(self.temp_dir, "test.awk") awk_path = os.path.join(self.temp_dir, "test.awk")
open(awk_path, "w").close() open(awk_path, "w").close()
self.assertEqual( self.assertEqual(lscolors.color_code_for_path(awk_path, self.COLOR_CODES),
lscolors.color_code_for_path(awk_path, self.COLOR_CODES), self.AWK_COLOR)
self.AWK_COLOR)
def test_color_code_for_path_with_double_extension(self): def test_color_code_for_path_with_double_extension(self):
tar_gz_path = os.path.join(self.temp_dir, "test.tar.gz") tar_gz_path = os.path.join(self.temp_dir, "test.tar.gz")
open(tar_gz_path, "w").close() open(tar_gz_path, "w").close()
self.assertEqual( self.assertEqual(lscolors.color_code_for_path(tar_gz_path, self.COLOR_CODES),
lscolors.color_code_for_path(tar_gz_path, self.COLOR_CODES), self.TAR_GZ_COLOR)
self.TAR_GZ_COLOR)
def _parse_ls_line(line): def _parse_ls_line(line):
@ -227,15 +200,13 @@ def _parse_ls_line(line):
class ParseLsLineTestCase(unittest.TestCase): class ParseLsLineTestCase(unittest.TestCase):
def test_parse_ls_line(self): def test_parse_ls_line(self):
self.assertEqual(_parse_ls_line( self.assertEqual(_parse_ls_line("\x1b[0m\x1b[38;5;254m\x1b[m\x1b[38;5;30mhello\x1b[0m\n"),
"\x1b[0m\x1b[38;5;254m\x1b[m\x1b[38;5;30mhello\x1b[0m\n"), ("38;5;30", "hello"))
("38;5;30", "hello"))
def test_against_ls(root_path, environment): def test_against_ls(root_path, environment):
process = subprocess.run( process = subprocess.run(["ls", "--color=always", "-R", root_path],
["ls", "--color=always", "-R", root_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=environment)
stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=environment)
color_codes = lscolors.get_color_codes(environment) color_codes = lscolors.get_color_codes(environment)
for line in process.stdout.splitlines(): for line in process.stdout.splitlines():
line = line.strip() line = line.strip()
@ -253,14 +224,12 @@ def test_against_ls(root_path, environment):
RICH_COLOR_CODES = ( RICH_COLOR_CODES = (
"bd=38;5;68:ca=38;5;17:cd=38;5;113;1:di=38;5;30:do=38;5;127:" "bd=38;5;68:ca=38;5;17:cd=38;5;113;1:di=38;5;30:do=38;5;127:ex=38;5;166;1:pi=38;5;126:"
"ex=38;5;166;1:pi=38;5;126:fi=38;5;253:ln=target:mh=38;5;220;1:" "fi=38;5;253:ln=target:mh=38;5;220;1:no=38;5;254:or=48;5;196;38;5;232;1:ow=38;5;33;1:"
"no=38;5;254:or=48;5;196;38;5;232;1:ow=38;5;33;1:sg=38;5;137;1:" "sg=38;5;137;1:su=38;5;137:so=38;5;197:st=48;5;235;38;5;118;1:tw=48;5;235;38;5;139;1:"
"su=38;5;137:so=38;5;197:st=48;5;235;38;5;118;1:tw=48;5;235;38;5;139;1:" "*.BAT=38;5;108:*.PL=38;5;160:*.asm=38;5;240;1:*.awk=38;5;148;1:*.bash=38;5;173:"
"*.BAT=38;5;108:*.PL=38;5;160:*.asm=38;5;240;1:*.awk=38;5;148;1:" "*.bat=38;5;108:*.c=38;5;110:*.cfg=1:*.coffee=38;5;94;1:*.conf=1:*.cpp=38;5;24;1:"
"*.bash=38;5;173:*.bat=38;5;108:*.c=38;5;110:*.cfg=1:*.coffee=38;5;94;1:" "*.cs=38;5;74;1:*.css=38;5;91:*.csv=38;5;78:*.diff=48;5;197;38;5;232:*.enc=38;5;192;3")
"*.conf=1:*.cpp=38;5;24;1:*.cs=38;5;74;1:*.css=38;5;91:*.csv=38;5;78:"
"*.diff=48;5;197;38;5;232:*.enc=38;5;192;3")
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -50,8 +50,7 @@ def run_in_container(container, command):
def build_ubuntu(): def build_ubuntu():
cmd("sudo debootstrap --components=main,restricted,universe,multiverse " cmd("sudo debootstrap --components=main,restricted,universe,multiverse "
"impish ubuntu.part http://au.archive.ubuntu.com/ubuntu/") "impish ubuntu.part http://au.archive.ubuntu.com/ubuntu/")
run_in_container("ubuntu.part", run_in_container("ubuntu.part", "ln -sf /lib/systemd/resolv.conf /etc/resolv.conf")
"ln -sf /lib/systemd/resolv.conf /etc/resolv.conf")
os.rename("ubuntu.part", "ubuntu") os.rename("ubuntu.part", "ubuntu")
@ -106,6 +105,5 @@ def main(work_path):
if __name__ == "__main__": if __name__ == "__main__":
work_path = (tempfile.mkdtemp(prefix="make-appimage-") work_path = tempfile.mkdtemp(prefix="make-appimage-") if len(sys.argv) == 1 else sys.argv[1]
if len(sys.argv) == 1 else sys.argv[1])
main(work_path) main(work_path)

View file

@ -6,9 +6,8 @@ import eris.tools as tools
def main(): def main():
all_tools = ([(["*"], tools.generic_tools() + git_tools = [tools.git_diff, tools.git_blame, tools.git_log]
[tools.git_diff, tools.git_blame, tools.git_log])] + all_tools = [(["*"], tools.generic_tools() + git_tools)] + tools.TOOLS_FOR_EXTENSIONS
tools.TOOLS_FOR_EXTENSIONS)
tool_set = set() tool_set = set()
extension_set = set() extension_set = set()
for extensions, tools_ in all_tools: for extensions, tools_ in all_tools:
@ -51,9 +50,8 @@ Eris maintains an up-to-date set of reports for every file in a codebase.
File types({len(extension_set)-1}) | Tools({len(tool_set)}) File types({len(extension_set)-1}) | Tools({len(tool_set)})
----------:| -----""") ----------:| -----""")
for extensions, tools_ in all_tools: for extensions, tools_ in all_tools:
print("%s | %s" % ( print("%s | %s" % (" ".join("." + extension for extension in extensions),
" ".join("." + extension for extension in extensions), "".join(f"[{tool.__name__}]({tool.url})" for tool in tools_)))
"".join(f"[{tool.__name__}]({tool.url})" for tool in tools_)))
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -50,16 +50,12 @@ class Color:
def _xterm_colors(): def _xterm_colors():
result = [ result = [(0x00, 0x00, 0x00), (0xcd, 0x00, 0x00), (0x00, 0xcd, 0x00), (0xcd, 0xcd, 0x00),
(0x00, 0x00, 0x00), (0xcd, 0x00, 0x00), (0x00, 0xcd, 0x00), (0x00, 0x00, 0xee), (0xcd, 0x00, 0xcd), (0x00, 0xcd, 0xcd), (0xe5, 0xe5, 0xe5),
(0xcd, 0xcd, 0x00), (0x00, 0x00, 0xee), (0xcd, 0x00, 0xcd), (0x7f, 0x7f, 0x7f), (0xff, 0x00, 0x00), (0x00, 0xff, 0x00), (0xff, 0xff, 0x00),
(0x00, 0xcd, 0xcd), (0xe5, 0xe5, 0xe5), (0x7f, 0x7f, 0x7f), (0x5c, 0x5c, 0xff), (0xff, 0x00, 0xff), (0x00, 0xff, 0xff), (0xff, 0xff, 0xff)]
(0xff, 0x00, 0x00), (0x00, 0xff, 0x00), (0xff, 0xff, 0x00), grad = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]
(0x5c, 0x5c, 0xff), (0xff, 0x00, 0xff), (0x00, 0xff, 0xff), result.extend([(grad[(i // 36) % 6], grad[(i // 6) % 6], grad[i % 6]) for i in range(216)])
(0xff, 0xff, 0xff)]
intensities = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]
result.extend([(intensities[(i // 36) % 6], intensities[(i // 6) % 6],
intensities[i % 6]) for i in range(216)])
result.extend([(8 + i * 10, 8 + i * 10, 8 + i * 10) for i in range(24)]) result.extend([(8 + i * 10, 8 + i * 10, 8 + i * 10) for i in range(24)])
return result return result
@ -93,13 +89,11 @@ class CharStyle:
return CharStyle._POOL[key] return CharStyle._POOL[key]
except KeyError: except KeyError:
obj = object.__new__(cls) obj = object.__new__(cls)
obj.fg_color, obj.bg_color, obj.is_bold, obj.is_italic, \ obj.fg_color, obj.bg_color, obj.is_bold, obj.is_italic, obj.is_underlined = key
obj.is_underlined = key
return CharStyle._POOL.setdefault(key, obj) return CharStyle._POOL.setdefault(key, obj)
def __getnewargs__(self): def __getnewargs__(self):
return (self.fg_color, self.bg_color, self.is_bold, self.is_italic, return self.fg_color, self.bg_color, self.is_bold, self.is_italic, self.is_underlined
self.is_underlined)
def __getstate__(self): def __getstate__(self):
state = self.__dict__.copy() state = self.__dict__.copy()
@ -118,8 +112,7 @@ class CharStyle:
attributes.append("i") attributes.append("i")
if self.is_underlined: if self.is_underlined:
attributes.append("u") attributes.append("u")
return (f"<CharStyle: fg:{self.fg_color} bg:{self.bg_color}" return f"<CharStyle: fg:{self.fg_color} bg:{self.bg_color} attr:{','.join(attributes)}>"
f" attr:{','.join(attributes)}>")
def _color_code(self, color_, is_foreground): def _color_code(self, color_, is_foreground):
if isinstance(color_, int): if isinstance(color_, int):
@ -144,12 +137,9 @@ class CharStyle:
def as_html(self): def as_html(self):
bold_code = "font-weight:bold; " if self.is_bold else "" bold_code = "font-weight:bold; " if self.is_bold else ""
italic_code = "font-style:italic; " if self.is_italic else "" italic_code = "font-style:italic; " if self.is_italic else ""
underline_code = ("text-decoration:underline; " underline_code = "text-decoration:underline; " if self.is_underlined else ""
if self.is_underlined else "") fg_color = self.fg_color if type(self.fg_color) == tuple else XTERM_COLORS[self.fg_color]
fg_color = (self.fg_color if type(self.fg_color) == tuple bg_color = self.bg_color if type(self.bg_color) == tuple else XTERM_COLORS[self.bg_color]
else XTERM_COLORS[self.fg_color])
bg_color = (self.bg_color if type(self.bg_color) == tuple
else XTERM_COLORS[self.bg_color])
return (f"<style>.S{id(self)} {{font-size:80%%; color:rgb{fg_color!r};" return (f"<style>.S{id(self)} {{font-size:80%%; color:rgb{fg_color!r};"
f" background-color:rgb{bg_color!r}; " f" background-color:rgb{bg_color!r}; "
f"{bold_code}{italic_code}{underline_code}}}</style>") f"{bold_code}{italic_code}{underline_code}}}</style>")
@ -227,23 +217,19 @@ class TermStr(collections.UserString):
bg_color = int(codes[index+1]) bg_color = int(codes[index+1])
codes[index+1:index+2] = [] codes[index+1:index+2] = []
elif code == "2" and previous_code == "38": # rgb fg color elif code == "2" and previous_code == "38": # rgb fg color
fg_color = tuple(int(component) fg_color = tuple(int(component) for component in codes[index+1:index+4])
for component in codes[index+1:index+4])
codes[index+1:index+4] = [] codes[index+1:index+4] = []
elif code == "2" and previous_code == "48": # rgb bg color elif code == "2" and previous_code == "48": # rgb bg color
bg_color = tuple(int(component) bg_color = tuple(int(component) for component in codes[index+1:index+4])
for component in codes[index+1:index+4])
codes[index+1:index+4] = [] codes[index+1:index+4] = []
previous_code = code previous_code = code
result_parts.append(cls(part[end_index+1:], result_parts.append(cls(part[end_index+1:], CharStyle(fg_color, bg_color, is_bold,
CharStyle(fg_color, bg_color, is_bold, is_italic, is_underlined)))
is_italic, is_underlined)))
return cls("").join(result_parts) return cls("").join(result_parts)
def __eq__(self, other): def __eq__(self, other):
return (self is other or return (self is other or (isinstance(other, self.__class__) and
(isinstance(other, self.__class__) and self.data == other.data and self.style == other.style))
self.data == other.data and self.style == other.style))
def __ne__(self, other): def __ne__(self, other):
return not self.__eq__(other) return not self.__eq__(other)
@ -266,10 +252,8 @@ class TermStr(collections.UserString):
return result return result
def __str__(self): def __str__(self):
return "".join(_join_lists( return "".join(_join_lists([style.code_for_term, self.data[start_index:end_index]]
[style.code_for_term, self.data[start_index:end_index]] for style, start_index, end_index in self._partition_style) + [ESC + NORMAL])
for style, start_index, end_index in self._partition_style) +
[ESC + NORMAL])
def __repr__(self): def __repr__(self):
return f"<TermStr: {self.data!r}>" return f"<TermStr: {self.data!r}>"
@ -302,8 +286,7 @@ class TermStr(collections.UserString):
return self.__class__(result, self.style[index]) return self.__class__(result, self.style[index])
def join(self, parts): def join(self, parts):
parts = [TermStr(part) if isinstance(part, str) else part parts = [TermStr(part) if isinstance(part, str) else part for part in parts]
for part in parts]
joined_style = _join_lists(self.style + part.style 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), return self.__class__(self.data.join(part.data for part in parts),
tuple(joined_style[len(self.style):])) tuple(joined_style[len(self.style):]))
@ -356,50 +339,43 @@ class TermStr(collections.UserString):
if left_width < 1: if left_width < 1:
return self return self
return (self.__class__(fillchar * left_width) + self + return (self.__class__(fillchar * left_width) + self +
self.__class__(fillchar * self.__class__(fillchar * (width - left_width - len(self.data))))
(width - left_width - len(self.data))))
# Below are extra methods useful for termstrs. # Below are extra methods useful for termstrs.
def transform_style(self, transform_func): def transform_style(self, transform_func):
new_style = tuple(_join_lists( new_style = tuple(_join_lists([transform_func(style)] * (end_index - start_index)
[transform_func(style)] * (end_index - start_index) for style, start_index, end_index in self._partition_style))
for style, start_index, end_index in self._partition_style))
return self.__class__(self.data, new_style) return self.__class__(self.data, new_style)
def bold(self): def bold(self):
def make_bold(style): def make_bold(style):
return CharStyle(style.fg_color, style.bg_color, is_bold=True, return CharStyle(style.fg_color, style.bg_color, is_bold=True,
is_italic=style.is_italic, is_italic=style.is_italic, is_underlined=style.is_underlined)
is_underlined=style.is_underlined)
return self.transform_style(make_bold) return self.transform_style(make_bold)
def underline(self): def underline(self):
def make_underlined(style): def make_underlined(style):
return CharStyle(style.fg_color, style.bg_color, return CharStyle(style.fg_color, style.bg_color, is_bold=style.is_bold,
is_bold=style.is_bold, is_italic=style.is_italic, is_italic=style.is_italic, is_underlined=True)
is_underlined=True)
return self.transform_style(make_underlined) return self.transform_style(make_underlined)
def italic(self): def italic(self):
def make_italic(style): def make_italic(style):
return CharStyle(style.fg_color, style.bg_color, return CharStyle(style.fg_color, style.bg_color, is_bold=style.is_bold,
is_bold=style.is_bold, is_italic=True, is_italic=True, is_underlined=style.is_underlined)
is_underlined=style.is_underlined)
return self.transform_style(make_italic) return self.transform_style(make_italic)
def fg_color(self, fg_color): def fg_color(self, fg_color):
def set_fgcolor(style): def set_fgcolor(style):
return CharStyle(fg_color, style.bg_color, is_bold=style.is_bold, return CharStyle(fg_color, style.bg_color, is_bold=style.is_bold,
is_italic=style.is_italic, is_italic=style.is_italic, is_underlined=style.is_underlined)
is_underlined=style.is_underlined)
return self.transform_style(set_fgcolor) return self.transform_style(set_fgcolor)
def bg_color(self, bg_color): def bg_color(self, bg_color):
def set_bgcolor(style): def set_bgcolor(style):
return CharStyle(style.fg_color, bg_color, is_bold=style.is_bold, return CharStyle(style.fg_color, bg_color, is_bold=style.is_bold,
is_italic=style.is_italic, is_italic=style.is_italic, is_underlined=style.is_underlined)
is_underlined=style.is_underlined)
return self.transform_style(set_bgcolor) return self.transform_style(set_bgcolor)
def as_html(self): def as_html(self):

View file

@ -22,12 +22,9 @@ class XtermColorsTests(unittest.TestCase):
self.assertEqual(termstr.XTERM_COLORS[255], (238, 238, 238)) self.assertEqual(termstr.XTERM_COLORS[255], (238, 238, 238))
def test_closest_color_index(self): def test_closest_color_index(self):
self.assertEqual(termstr.closest_color_index( self.assertEqual(termstr.closest_color_index((0, 0, 0), termstr.XTERM_COLORS), 0)
(0, 0, 0), termstr.XTERM_COLORS), 0) self.assertEqual(termstr.closest_color_index((255, 255, 255), termstr.XTERM_COLORS), 15)
self.assertEqual(termstr.closest_color_index( self.assertEqual(termstr.closest_color_index((135, 135, 1), termstr.XTERM_COLORS), 100)
(255, 255, 255), termstr.XTERM_COLORS), 15)
self.assertEqual(termstr.closest_color_index(
(135, 135, 1), termstr.XTERM_COLORS), 100)
class CharStyleTests(unittest.TestCase): class CharStyleTests(unittest.TestCase):
@ -48,12 +45,10 @@ class CharStyleTests(unittest.TestCase):
self.assertTrue(style is loaded_style) self.assertTrue(style is loaded_style)
def test_repr(self): def test_repr(self):
self.assertEqual(repr(self.style), self.assertEqual(repr(self.style), "<CharStyle: fg:(255, 255, 255) bg:(0, 0, 0) attr:>")
"<CharStyle: fg:(255, 255, 255) bg:(0, 0, 0) attr:>")
def test_code_for_term(self): def test_code_for_term(self):
self.assertEqual(self.style.code_for_term, self.assertEqual(self.style.code_for_term, "\x1b[m\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m")
"\x1b[m\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m")
class TermStrTests(unittest.TestCase): class TermStrTests(unittest.TestCase):
@ -65,8 +60,7 @@ class TermStrTests(unittest.TestCase):
foo_bold = termstr.TermStr("foo", bold_style) foo_bold = termstr.TermStr("foo", bold_style)
self.assertEqual(repr(foo_bold), "<TermStr: 'foo'>") self.assertEqual(repr(foo_bold), "<TermStr: 'foo'>")
self.assertEqual(foo + "bar", termstr.TermStr("foobar")) self.assertEqual(foo + "bar", termstr.TermStr("foobar"))
self.assertEqual(foo + termstr.TermStr("bar"), self.assertEqual(foo + termstr.TermStr("bar"), termstr.TermStr("foobar"))
termstr.TermStr("foobar"))
self.assertEqual("bar" + foo, termstr.TermStr("barfoo")) self.assertEqual("bar" + foo, termstr.TermStr("barfoo"))
self.assertFalse(foo == foo_bold) self.assertFalse(foo == foo_bold)
self.assertFalse(foo_bold == foo) self.assertFalse(foo_bold == foo)
@ -88,11 +82,9 @@ class TermStrTests(unittest.TestCase):
self.assertEqual(foo.find("oo"), 1) self.assertEqual(foo.find("oo"), 1)
self.assertEqual(termstr.TermStr("fo") * 2, termstr.TermStr("fofo")) self.assertEqual(termstr.TermStr("fo") * 2, termstr.TermStr("fofo"))
self.assertEqual(2 * termstr.TermStr("fo"), termstr.TermStr("fofo")) self.assertEqual(2 * termstr.TermStr("fo"), termstr.TermStr("fofo"))
self.assertEqual(foobar.split("b"), [termstr.TermStr("foo"), self.assertEqual(foobar.split("b"), [termstr.TermStr("foo"), termstr.TermStr("ar")])
termstr.TermStr("ar")])
self.assertEqual(foo.join(["C", "D"]), termstr.TermStr("CfooD")) self.assertEqual(foo.join(["C", "D"]), termstr.TermStr("CfooD"))
self.assertEqual(foo.join(["C", termstr.TermStr("D")]), self.assertEqual(foo.join(["C", termstr.TermStr("D")]), termstr.TermStr("CfooD"))
termstr.TermStr("CfooD"))
self.assertEqual(foo.join([]), termstr.TermStr("")) self.assertEqual(foo.join([]), termstr.TermStr(""))
self.assertEqual(foo.join(["C"]), termstr.TermStr("C")) self.assertEqual(foo.join(["C"]), termstr.TermStr("C"))
bar = termstr.TermStr("bar", bold_style) bar = termstr.TermStr("bar", bold_style)
@ -107,16 +99,14 @@ class TermStrTests(unittest.TestCase):
self.assertEqual(termstr.TermStr("FOO").lower(), foo) self.assertEqual(termstr.TermStr("FOO").lower(), foo)
self.assertEqual(termstr.TermStr("FOO", bold_style).lower(), foo_bold) self.assertEqual(termstr.TermStr("FOO", bold_style).lower(), foo_bold)
self.assertEqual(termstr.TermStr("FOO").swapcase(), foo) self.assertEqual(termstr.TermStr("FOO").swapcase(), foo)
self.assertEqual(termstr.TermStr("FOO", bold_style).swapcase(), self.assertEqual(termstr.TermStr("FOO", bold_style).swapcase(), foo_bold)
foo_bold)
phrase = termstr.TermStr("foo bar") phrase = termstr.TermStr("foo bar")
self.assertEqual(phrase.title(), termstr.TermStr("Foo Bar")) self.assertEqual(phrase.title(), termstr.TermStr("Foo Bar"))
self.assertEqual(phrase.capitalize(), termstr.TermStr("Foo bar")) self.assertEqual(phrase.capitalize(), termstr.TermStr("Foo bar"))
self.assertEqual(foo.upper(), termstr.TermStr("FOO")) self.assertEqual(foo.upper(), termstr.TermStr("FOO"))
self.assertEqual(foo_bold.center(0), foo_bold) self.assertEqual(foo_bold.center(0), foo_bold)
self.assertEqual(foo_bold.center(7), self.assertEqual(foo_bold.center(7),
termstr.TermStr(" ") + foo_bold + termstr.TermStr(" ") + foo_bold + termstr.TermStr(" "))
termstr.TermStr(" "))
self.assertEqual(foo_bold.ljust(0), foo_bold) self.assertEqual(foo_bold.ljust(0), foo_bold)
self.assertEqual(foo_bold.ljust(5), foo_bold + termstr.TermStr(" ")) self.assertEqual(foo_bold.ljust(5), foo_bold + termstr.TermStr(" "))
self.assertEqual(foo_bold.rjust(0), foo_bold) self.assertEqual(foo_bold.rjust(0), foo_bold)
@ -129,16 +119,15 @@ class TermStrTests(unittest.TestCase):
def test_from_term(self): def test_from_term(self):
def test_round_trip(term_str): def test_round_trip(term_str):
self.assertEqual(termstr.TermStr.from_term(str(term_str)), self.assertEqual(termstr.TermStr.from_term(str(term_str)), term_str)
term_str)
test_round_trip(termstr.TermStr("foo")) test_round_trip(termstr.TermStr("foo"))
test_round_trip(termstr.TermStr("foo").bold()) test_round_trip(termstr.TermStr("foo").bold())
test_round_trip(termstr.TermStr("foo").underline()) test_round_trip(termstr.TermStr("foo").underline())
test_round_trip(termstr.TermStr("foo").italic()) test_round_trip(termstr.TermStr("foo").italic())
test_round_trip(termstr.TermStr("foo").fg_color(termstr.Color.red)) test_round_trip(termstr.TermStr("foo").fg_color(termstr.Color.red))
test_round_trip(termstr.TermStr("foo").fg_color(termstr.Color.red). test_round_trip(
bg_color(termstr.Color.green)) termstr.TermStr("foo").fg_color(termstr.Color.red).bg_color(termstr.Color.green))
test_round_trip(termstr.TermStr("foo").fg_color(1)) test_round_trip(termstr.TermStr("foo").fg_color(1))
test_round_trip(termstr.TermStr("foo").bg_color(10)) test_round_trip(termstr.TermStr("foo").bg_color(10))
self.assertEqual( self.assertEqual(
@ -147,14 +136,10 @@ class TermStrTests(unittest.TestCase):
termstr.TermStr("foo").fg_color(3)) termstr.TermStr("foo").fg_color(3))
self.assertEqual(termstr.TermStr.from_term(ESC + "[45mfoo"), self.assertEqual(termstr.TermStr.from_term(ESC + "[45mfoo"),
termstr.TermStr("foo").bg_color(5)) termstr.TermStr("foo").bg_color(5))
self.assertEqual(termstr.TermStr.from_term(ESC + "[45mfoo" + self.assertEqual(termstr.TermStr.from_term(ESC + "[45mfoo" + ESC + "[mbar"),
ESC + "[mbar"), termstr.TermStr("foo").bg_color(5) + termstr.TermStr("bar"))
termstr.TermStr("foo").bg_color(5) + self.assertEqual(termstr.TermStr.from_term(ESC + "[45mfoo" + ESC + "[0mbar"),
termstr.TermStr("bar")) termstr.TermStr("foo").bg_color(5) + termstr.TermStr("bar"))
self.assertEqual(termstr.TermStr.from_term(ESC + "[45mfoo" +
ESC + "[0mbar"),
termstr.TermStr("foo").bg_color(5) +
termstr.TermStr("bar"))
self.assertEqual(termstr.TermStr.from_term(ESC + "[1;3mfoo"), self.assertEqual(termstr.TermStr.from_term(ESC + "[1;3mfoo"),
termstr.TermStr("foo").bold().italic()) termstr.TermStr("foo").bold().italic())
self.assertEqual(termstr.TermStr.from_term(ESC + "[01mfoo"), self.assertEqual(termstr.TermStr.from_term(ESC + "[01mfoo"),
@ -165,12 +150,9 @@ class TermStrTests(unittest.TestCase):
termstr.TermStr("foo").fg_color(13)) termstr.TermStr("foo").fg_color(13))
self.assertEqual(termstr.TermStr.from_term(ESC + "[105mfoo"), self.assertEqual(termstr.TermStr.from_term(ESC + "[105mfoo"),
termstr.TermStr("foo").bg_color(13)) termstr.TermStr("foo").bg_color(13))
self.assertEqual(termstr.TermStr.from_term(ESC + "(B" + self.assertEqual(termstr.TermStr.from_term(ESC + "(B" + ESC + "[mfoo"),
ESC + "[mfoo"),
termstr.TermStr("foo")) termstr.TermStr("foo"))
self.assertEqual( self.assertEqual(termstr.TermStr.from_term(ESC + "39;49;00mfoo"), termstr.TermStr("foo"))
termstr.TermStr.from_term(ESC + "39;49;00mfoo"),
termstr.TermStr("foo"))
if __name__ == "__main__": if __name__ == "__main__":