tools: Added color to git_log output.

- Added a simple parser for termcodes to termstr.
- Added missing 'italic' conversion in termstr.
- Simplified termcode for 'normal'.
This commit is contained in:
Andrew Hamilton 2019-07-09 12:00:39 +10:00
parent ee064f00af
commit f6cb615c97
16 changed files with 178 additions and 100 deletions

View file

@ -11,33 +11,33 @@ import sys
ESC = "\x1b" ESC = "\x1b"
normal = ESC + "(B\x1b[m" normal = "[m"
bold = ESC + "[1m" bold = "[1m"
italic = ESC + "[3m" italic = "[3m"
standout = ESC + "[7m" standout = "[7m"
underline = ESC + "[4m" underline = "[4m"
enter_fullscreen = ESC + "[?1049h" enter_fullscreen = ESC + "[?1049h"
exit_fullscreen = ESC + "[?1049l" exit_fullscreen = ESC + "[?1049l"
hide_cursor = ESC + "[?25l" hide_cursor = ESC + "[?25l"
normal_cursor = ESC + "[?25l\x1b[?25h" normal_cursor = ESC + "[?25l" + ESC + "[?25h"
clear = ESC + "[H\x1b[2J" clear = ESC + "[H" + ESC + "[2J"
save = ESC + "7" save = ESC + "7"
restore = ESC + "8" restore = ESC + "8"
def color(color_number, is_foreground): def color(color_number, is_foreground):
"""Set the color of text.""" """Set the color of text."""
return f"\x1b[{'38' if is_foreground else '48'};5;{color_number:d}m" return f"[{'38' if is_foreground else '48'};5;{color_number:d}m"
def rgb_color(rgb, is_foreground): def rgb_color(rgb, is_foreground):
"""Set the color of text using an rgb tuple.""" """Set the color of text using an rgb tuple."""
return f"\x1b[{'38' if is_foreground else '48'};2;" + "%i;%i;%im" % rgb return f"[{'38' if is_foreground else '48'};2;" + "%i;%i;%im" % rgb
def move(x, y): def move(x, y):
"""Move the cursor to column x, row y.""" """Move the cursor to column x, row y."""
return f"\x1b[{y + 1:d};{x + 1:d}H" return ESC + f"[{y + 1:d};{x + 1:d}H"
@contextlib.contextmanager @contextlib.contextmanager

View file

@ -122,13 +122,13 @@ class CharStyle:
@_cache_first_result @_cache_first_result
def code_for_term(self): def code_for_term(self):
fg_termcode = self.termcode_of_color(self.fg_color, True) fg_termcode = terminal.ESC + self.termcode_of_color(self.fg_color, True)
bg_termcode = self.termcode_of_color(self.bg_color, False) bg_termcode = terminal.ESC + self.termcode_of_color(self.bg_color, False)
bold_code = terminal.bold if self.is_bold else "" bold_code = (terminal.ESC + terminal.bold) if self.is_bold else ""
italic_code = terminal.italic if self.is_italic else "" italic_code = (terminal.ESC + terminal.italic) if self.is_italic else ""
underline_code = terminal.underline if self.is_underlined else "" underline_code = (terminal.ESC + terminal.underline) if self.is_underlined else ""
return "".join([terminal.normal, fg_termcode, bg_termcode, bold_code, return "".join([terminal.ESC, terminal.normal, fg_termcode, bg_termcode,
italic_code, underline_code]) bold_code, italic_code, underline_code])
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 ""
@ -158,6 +158,45 @@ class TermStr(collections.UserString):
self.style = (style if isinstance(style, tuple) self.style = (style if isinstance(style, tuple)
else (style,) * len(data)) else (style,) * len(data))
@classmethod
def from_term(cls, data):
parts = data.split(terminal.ESC)
fg_color, bg_color = None, None
is_bold, is_italic, is_underlined = False, False, False
result_parts = []
for part in parts:
try:
end_index = part.index("m")
except ValueError:
end_index = 0
if part[:2] == "[m": # Normal
is_bold, is_italic, is_underlined = False, False, False
fg_color, bg_color = None, None
elif part[:3] == terminal.bold:
is_bold = True
elif part[:3] == terminal.italic:
is_italic = True
elif part[:3] == terminal.underline:
is_underlined = True
elif end_index == 3 and part.startswith("[3"): # 8 foreground color
fg_color = int(part[2])
elif end_index == 3 and part.startswith("[4"): # 8 background color
bg_color = int(part[2])
elif part[:6] == "[38;5;": # simple foreground color
fg_color = int(part[6:end_index])
elif part[:6] == "[48;5;": # simple background color
bg_color = int(part[6:end_index])
elif part[:6] == "[38;2;": # rgb foreground color
fg_color = tuple(int(component)
for component in part[6:end_index].split(";"))
elif part[:6] == "[48;2;": # rgb background color
bg_color = tuple(int(component)
for component in part[6:end_index].split(";"))
result_parts.append(cls(part[end_index+1:],
CharStyle(fg_color, bg_color, is_bold,
is_italic, is_underlined)))
return cls("").join(result_parts)
def __eq__(self, other): def __eq__(self, other):
return (self is other or return (self is other or
(isinstance(other, self.__class__) and (isinstance(other, self.__class__) and
@ -189,7 +228,7 @@ class TermStr(collections.UserString):
return "".join(_join_lists( return "".join(_join_lists(
[style.code_for_term(), str_] [style.code_for_term(), str_]
for style, str_, position in self._partition_style()) + for style, str_, position in self._partition_style()) +
[terminal.normal]) [terminal.ESC + terminal.normal])
def __repr__(self): def __repr__(self):
return f"<TermStr: {self.data!r}>" return f"<TermStr: {self.data!r}>"
@ -282,24 +321,35 @@ class TermStr(collections.UserString):
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_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_underlined=True) is_bold=style.is_bold, is_italic=style.is_italic,
is_underlined=True)
return self.transform_style(make_underlined) return self.transform_style(make_underlined)
def italic(self):
def make_italic(style):
return CharStyle(style.fg_color, style.bg_color,
is_bold=style.is_bold, is_italic=True,
is_underlined=style.is_underlined)
return self.transform_style(make_italic)
def fg_color(self, fg_color): def 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_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_underlined=style.is_underlined) is_underlined=style.is_underlined)
return self.transform_style(set_bgcolor) return self.transform_style(set_bgcolor)

View file

@ -416,6 +416,18 @@ def perltidy(path):
# perl6_syntax.deps={"rakudo"} # perl6_syntax.deps={"rakudo"}
@deps(deps={"git"}, url="https://git-scm.com/docs/git-log",
executables={"git"})
def git_log(path):
process = subprocess.run(["git", "log", "--find-renames", "--follow",
"--stat", "--color", path], text=True,
capture_output=True)
status = (Status.normal if process.returncode == 0
else Status.not_applicable)
return status, fill3.Fixed(termstr.TermStr.from_term(
process.stdout + process.stderr). splitlines())
@deps(deps={"tidy"}, url="tidy", executables={"tidy"}) @deps(deps={"tidy"}, url="tidy", executables={"tidy"})
def html_syntax(path): def html_syntax(path):
# Maybe only show errors # Maybe only show errors

View file

@ -85,13 +85,6 @@ tools_for_extensions = [
success_status = "normal" success_status = "normal"
error_status = "not_applicable" error_status = "not_applicable"
[git_log]
dependencies = ["git"]
url = "https://git-scm.com/docs/git-log"
command = "git log --find-renames --follow --stat"
success_status = "normal"
error_status = "not_applicable"
[objdump_headers] [objdump_headers]
dependencies = ["binutils"] dependencies = ["binutils"]
url = "https://en.wikipedia.org/wiki/Objdump" url = "https://en.wikipedia.org/wiki/Objdump"

View file

@ -1,2 +1,2 @@
(Bdef(B (Bhi(B():(B (B def hi(): 
(B (Bprint(B((B"hi"(B)(B  print("hi")

View file

@ -1,15 +1,15 @@
(Bsize:(B 12.0 B(B (12 bytes)(B size: 12.0 B (12 bytes)
(Bpermissions:(B ?rwxr-xr-x(B (755)(B permissions: ?rwxr-xr-x (755)
(Bmodified time:(B Sun Jan 31 23:14:05 2016(B (1454282045 secs)(B modified time: Sun Jan 31 23:14:05 2016 (1454282045 secs)
(Bcreation time:(B Sun Jan 31 23:14:05 2016(B (1454282045 secs)(B creation time: Sun Jan 31 23:14:05 2016 (1454282045 secs)
(Baccess time:(B Sun Jan 31 23:14:07 2016(B (1454282047 secs)(B access time: Sun Jan 31 23:14:07 2016 (1454282047 secs)
(Bowner:(B foo(B (1111 uid)(B owner: foo (1111 uid)
(Bgroup:(B foo(B (1111 gid)(B group: foo (1111 gid)
(Bhardlinks:(B 2 hardlinks: 2
(Bsymlink:(B no symlink: no
(Bmime type:(B text/x-python; charset=us-ascii mime type: text/x-python; charset=us-ascii
(Bfile type:(B Python script, ASCII text executable (B file type: Python script, ASCII text executable 

View file

@ -1,8 +1,8 @@
(B▀▀▀▀▀▀(B▀▀▀▀(B▀▀▀▀▀▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀▀▀(B▀(B▀▀(B▀▀▀▀(B▀▀(B▀(B▀▀▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀▀(B▀(B▀▀▀▀▀▀▀▀▀▀(B▀(B▀(B▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀(B▀▀▀▀▀▀▀▀▀▀▀▀▀▀(B▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀(B▀(B▀▀▀▀▀▀▀▀▀▀▀▀(B▀(B▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀(B▀(B▀(B▀▀▀▀▀▀▀▀▀▀(B▀(B▀(B▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀▀▀(B▀(B▀(B▀▀▀▀▀▀(B▀(B▀(B▀(B▀▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀▀▀▀▀(B▀(B▀(B▀▀▀▀(B▀▀▀▀▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀

View file

@ -1,8 +1,8 @@
(B▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀▀▀▀(B▀(B▀▀▀▀▀▀(B▀▀(B▀▀▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀▀(B▀(B▀▀▀▀▀▀▀▀▀▀(B▀(B▀▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀▀(B▀▀▀▀▀▀▀▀▀▀▀▀(B▀(B▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀▀(B▀▀▀▀▀▀▀▀▀▀▀▀(B▀(B▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀▀(B▀(B▀▀▀▀▀▀▀▀▀▀▀(B▀▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀▀▀(B▀(B▀▀▀▀▀▀▀▀(B▀(B▀▀▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀▀▀▀▀▀▀(B▀▀(B▀▀▀▀▀▀▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀

View file

@ -1,8 +1,8 @@
(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀▀(B▀(B▀(B▀(B▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀(B▀(B▀(B▀▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀▀(B▀(B▀(B▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀

View file

@ -1,8 +1,8 @@
(B▀▀▀▀▀▀(B▀▀▀▀(B▀▀▀▀▀▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀▀▀(B▀(B▀▀(B▀▀▀▀(B▀▀(B▀(B▀▀▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀▀(B▀(B▀▀▀▀▀▀▀▀▀▀(B▀(B▀(B▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀(B▀▀▀▀▀▀▀▀▀▀▀▀▀▀(B▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀(B▀(B▀▀▀▀▀▀▀▀▀▀▀▀(B▀(B▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀(B▀(B▀(B▀▀▀▀▀▀▀▀▀▀(B▀(B▀(B▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀▀▀(B▀(B▀(B▀▀▀▀▀▀(B▀(B▀(B▀(B▀▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀▀▀▀▀(B▀(B▀(B▀▀▀▀(B▀▀▀▀▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀

View file

@ -1,8 +1,8 @@
(B▀▀▀▀▀▀(B▀▀▀▀(B▀▀▀▀▀▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀▀▀(B▀(B▀▀(B▀▀▀▀(B▀▀(B▀(B▀▀▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀▀(B▀(B▀▀▀▀▀▀▀▀▀▀(B▀(B▀(B▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀(B▀▀▀▀▀▀▀▀▀▀▀▀▀▀(B▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀(B▀(B▀▀▀▀▀▀▀▀▀▀▀▀(B▀(B▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀(B▀(B▀(B▀▀▀▀▀▀▀▀▀▀(B▀(B▀(B▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀▀▀(B▀(B▀(B▀▀▀▀▀▀(B▀(B▀(B▀(B▀▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀▀▀▀▀(B▀(B▀(B▀▀▀▀(B▀▀▀▀▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀

View file

@ -1,8 +1,8 @@
(B▀▀▀▀▀▀(B▀▀▀▀(B▀▀▀▀▀▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀▀▀(B▀(B▀▀(B▀▀▀▀(B▀▀(B▀(B▀▀▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀▀(B▀(B▀▀▀▀▀▀▀▀▀▀(B▀(B▀(B▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀(B▀▀▀▀▀▀▀▀▀▀▀▀▀▀(B▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀(B▀(B▀▀▀▀▀▀▀▀▀▀▀▀(B▀(B▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀(B▀(B▀(B▀▀▀▀▀▀▀▀▀▀(B▀(B▀(B▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀▀▀(B▀(B▀(B▀▀▀▀▀▀(B▀(B▀(B▀(B▀▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀▀▀▀▀(B▀(B▀(B▀▀▀▀(B▀▀▀▀▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀

View file

@ -1,8 +1,8 @@
(B▀▀▀▀▀▀(B▀▀▀▀(B▀▀▀▀▀▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀▀▀(B▀(B▀▀(B▀▀▀▀(B▀▀(B▀(B▀▀▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀▀(B▀(B▀▀▀▀▀▀▀▀▀▀(B▀(B▀(B▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀(B▀▀▀▀▀▀▀▀▀▀▀▀▀▀(B▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀(B▀(B▀▀▀▀▀▀▀▀▀▀▀▀(B▀(B▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀(B▀(B▀(B▀▀▀▀▀▀▀▀▀▀(B▀(B▀(B▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀▀▀(B▀(B▀(B▀▀▀▀▀▀(B▀(B▀(B▀(B▀▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
(B▀▀▀▀▀(B▀(B▀(B▀▀▀▀(B▀▀▀▀▀(B ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀

View file

@ -1,4 +1,4 @@
(B 
(B> def hi():(B > def hi():
(B> print("hi")(B > print("hi")

View file

@ -1 +1 @@
(Bdef(B (Bhi(B():(B def hi():

View file

@ -9,6 +9,7 @@ import unittest
os.environ["TERM"] = "xterm-256color" os.environ["TERM"] = "xterm-256color"
import eris.terminal
from eris.termstr import TermStr, CharStyle from eris.termstr import TermStr, CharStyle
import eris.termstr as termstr import eris.termstr as termstr
@ -55,7 +56,7 @@ class CharStyleTests(unittest.TestCase):
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(B\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):
@ -122,6 +123,28 @@ class TermStrTests(unittest.TestCase):
self.assertEqual(foo_bold.rjust(0), foo_bold) self.assertEqual(foo_bold.rjust(0), foo_bold)
self.assertEqual(foo_bold.rjust(5), TermStr(" ") + foo_bold) self.assertEqual(foo_bold.rjust(5), TermStr(" ") + foo_bold)
def test_from_term(self):
def test_round_trip(term_str):
self.assertEqual(TermStr.from_term(str(term_str)), term_str)
test_round_trip(TermStr("foo"))
test_round_trip(TermStr("foo").bold())
test_round_trip(TermStr("foo").underline())
test_round_trip(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).\
bg_color(termstr.Color.green))
test_round_trip(termstr.TermStr("foo").fg_color(1))
test_round_trip(termstr.TermStr("foo").bg_color(10))
self.assertEqual(TermStr.from_term(eris.terminal.ESC + "[33mfoo"),
termstr.TermStr("foo").fg_color(3))
self.assertEqual(TermStr.from_term(eris.terminal.ESC + "[45mfoo"),
termstr.TermStr("foo").bg_color(5))
self.assertEqual(TermStr.from_term(eris.terminal.ESC + "[45mfoo" +
eris.terminal.ESC + "[mbar"),
termstr.TermStr("foo").bg_color(5) +
termstr.TermStr("bar"))
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()