scripts: Adopted -k/--keep-open in treemap.py/codemap.py

It probably makes sense to adopt this in all ascii-art scripts that
operate on files.
This commit is contained in:
Christopher Haster
2025-03-15 03:49:43 -05:00
parent 6c36f76ffd
commit cb0119681d
3 changed files with 412 additions and 45 deletions
+197 -13
View File
@@ -12,10 +12,18 @@ import bisect
import collections as co
import csv
import fnmatch
import io
import itertools as it
import math as mt
import os
import re
import shutil
import time
try:
import inotify_simple
except ModuleNotFoundError:
inotify_simple = None
# we don't actually need that many chars/colors thanks to the
@@ -45,6 +53,107 @@ def openio(path, mode='r', buffering=-1):
else:
return open(path, mode, buffering)
if inotify_simple is None:
Inotify = None
else:
class Inotify(inotify_simple.INotify):
def __init__(self, paths):
super().__init__()
# wait for interesting events
flags = (inotify_simple.flags.ATTRIB
| inotify_simple.flags.CREATE
| inotify_simple.flags.DELETE
| inotify_simple.flags.DELETE_SELF
| inotify_simple.flags.MODIFY
| inotify_simple.flags.MOVED_FROM
| inotify_simple.flags.MOVED_TO
| inotify_simple.flags.MOVE_SELF)
# recurse into directories
for path in paths:
if os.path.isdir(path):
for dir, _, files in os.walk(path):
self.add_watch(dir, flags)
for f in files:
self.add_watch(os.path.join(dir, f), flags)
else:
self.add_watch(path, flags)
class RingIO:
def __init__(self, maxlen=None, head=False):
self.maxlen = maxlen
self.head = head
self.lines = co.deque(maxlen=maxlen)
self.tail = io.StringIO()
# trigger automatic sizing
if maxlen == 0:
self.resize(0)
def __len__(self):
return len(self.lines)
def write(self, s):
# note using split here ensures the trailing string has no newline
lines = s.split('\n')
if len(lines) > 1 and self.tail.getvalue():
self.tail.write(lines[0])
lines[0] = self.tail.getvalue()
self.tail = io.StringIO()
self.lines.extend(lines[:-1])
if lines[-1]:
self.tail.write(lines[-1])
def resize(self, maxlen):
self.maxlen = maxlen
if maxlen == 0:
maxlen = shutil.get_terminal_size((80, 5))[1]
if maxlen != self.lines.maxlen:
self.lines = co.deque(self.lines, maxlen=maxlen)
canvas_lines = 1
def draw(self):
# did terminal size change?
if self.maxlen == 0:
self.resize(0)
# copy lines
lines = self.lines.copy()
# pad to fill any existing canvas, but truncate to terminal size
h = shutil.get_terminal_size((80, 5))[1]
lines.extend('' for _ in range(
len(lines),
min(RingIO.canvas_lines, h)))
while len(lines) > h:
if self.head:
lines.pop()
else:
lines.popleft()
# first thing first, give ourself a canvas
while RingIO.canvas_lines < len(lines):
sys.stdout.write('\n')
RingIO.canvas_lines += 1
# write lines from top to bottom so later lines overwrite earlier
# lines, note [xA/[xB stop at terminal boundaries
for i, line in enumerate(lines):
# move cursor, clear line, disable/reenable line wrapping
sys.stdout.write('\r')
if len(lines)-1-i > 0:
sys.stdout.write('\x1b[%dA' % (len(lines)-1-i))
sys.stdout.write('\x1b[K')
sys.stdout.write('\x1b[?7l')
sys.stdout.write(line)
sys.stdout.write('\x1b[?7h')
if len(lines)-1-i > 0:
sys.stdout.write('\x1b[%dB' % (len(lines)-1-i))
sys.stdout.flush()
# parse different data representations
def dat(x, *args):
try:
@@ -709,7 +818,7 @@ def partition_squarify(children, total, x, y, width, height, *,
i = j
def main(csv_paths, *,
def main_(f, csv_paths, *,
by=None,
fields=None,
defines=[],
@@ -731,6 +840,12 @@ def main(csv_paths, *,
label=False,
no_label=False,
**args):
# give f an writeln function
def writeln(s=''):
f.write(s)
f.write('\n')
f.writeln = writeln
# figure out what color should be
if color == 'auto':
color = sys.stdout.isatty()
@@ -761,20 +876,20 @@ def main(csv_paths, *,
# figure out width/height
if width is None:
width_ = min(80, shutil.get_terminal_size((80, 5))[0])
elif width:
elif width > 0:
width_ = width
else:
width_ = shutil.get_terminal_size((80, 5))[0]
width_ = max(0, shutil.get_terminal_size((80, 5))[0] + width)
if height is None:
height_ = (2
if not no_header
and (title is not None or not no_stats)
else 1)
elif height:
elif height > 0:
height_ = height
else:
height_ = shutil.get_terminal_size((80, 5))[1] - 1
height_ = max(0, shutil.get_terminal_size((80, 5))[1] + height)
# first collect results from CSV files
fields_, results = collect(csv_paths, defines)
@@ -1001,19 +1116,70 @@ def main(csv_paths, *,
stat['mean'], stat['stddev'],
stat['min'], stat['max'])
if title and not no_stats:
print('%s%*s%s' % (
f.writeln('%s%*s%s' % (
title_,
max(width_-len(stat_)-len(title_), 0), ' ',
stat_))
elif title:
print(title_)
f.writeln(title_)
elif not no_stats:
print(stat_)
f.writeln(stat_)
# draw canvas
for row in range(canvas.height//canvas.yscale):
line = canvas.draw(row)
print(line)
f.writeln(line)
def main(csv_paths, *,
height=None,
keep_open=False,
head=False,
cat=False,
sleep=False,
**args):
# keep-open?
if keep_open:
try:
while True:
# register inotify before running the command, this avoids
# modification race conditions
if Inotify:
inotify = Inotify(csv_paths)
if cat:
main_(sys.stdout, csv_paths,
# make space for shell prompt
height=height if height is not False else -1,
**args)
else:
ring = RingIO(head=head)
main_(ring, csv_paths,
height=height if height is not False else 0,
**args)
ring.draw()
# try to inotifywait
if Inotify:
ptime = time.time()
inotify.read()
inotify.close()
# sleep a minimum amount of time to avoid flickering
time.sleep(max(0, (sleep or 0.01) - (time.time()-ptime)))
else:
time.sleep(sleep or 2)
except KeyboardInterrupt:
pass
if not cat:
sys.stdout.write('\n')
# single-pass?
else:
main_(sys.stdout, csv_paths,
# make space for shell prompt
height=height if height is not False else -1,
**args)
if __name__ == "__main__":
@@ -1103,14 +1269,15 @@ if __name__ == "__main__":
nargs='?',
type=lambda x: int(x, 0),
const=0,
help="Width in columns. 0 uses the terminal width. Defaults to "
"min(terminal, 80).")
help="Width in columns. <=0 uses the terminal width. Defaults "
"to min(terminal, 80).")
parser.add_argument(
'-H', '--height',
nargs='?',
type=lambda x: int(x, 0),
const=0,
help="Height in rows. 0 uses the terminal height. Defaults to 1.")
const=False,
help="Height in rows. <=0 uses the terminal height. Defaults "
"to 1.")
parser.add_argument(
'--no-header',
action='store_true',
@@ -1198,6 +1365,23 @@ if __name__ == "__main__":
'--no-label',
action='store_true',
help="Don't render any labels.")
parser.add_argument(
'-k', '--keep-open',
action='store_true',
help="Continue to open and redraw the CSV files in a loop.")
parser.add_argument(
'-^', '--head',
action='store_true',
help="Show the first n lines.")
parser.add_argument(
'-z', '--cat',
action='store_true',
help="Pipe directly to stdout.")
parser.add_argument(
'-s', '--sleep',
type=float,
help="Time in seconds to sleep between redraws when running "
"with -k. Defaults to 2 seconds.")
sys.exit(main(**{k: v
for k, v in vars(parser.parse_intermixed_args()).items()
if v is not None}))