From 94af42031e326c00608436033d92f14971cf5a09 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 22 Apr 2025 10:35:14 -0400 Subject: [PATCH] Provide screenshot tiling with interval support Only for kitty graphics protocol. Also, attempt to use pillow to convert, if available. Kitty itself needs this, Konsole can work either way. It currently does not preserve aspect ratio, to do that we pretty much need to do some work with pillow. If we specify just the height, then ratio is preserved, but it won't honor the designed bounding box on wide screenshots. Also Konsole won't even honor just one scaling factor. So the better thing would be to determine the aspect ratio, which needs pillow. --- confluent_client/bin/nodeconsole | 129 +++++++++++++++++--- confluent_client/confluent/screensqueeze.py | 8 +- 2 files changed, 117 insertions(+), 20 deletions(-) diff --git a/confluent_client/bin/nodeconsole b/confluent_client/bin/nodeconsole index 3ee218ee..fbcaabc9 100755 --- a/confluent_client/bin/nodeconsole +++ b/confluent_client/bin/nodeconsole @@ -35,6 +35,11 @@ import re import tty import termios import fcntl +import confluent.screensqueeze as sq +try: + from PIL import Image +except ImportError: + Image = None try: # sixel is optional, attempt to import but stub out if unavailable @@ -119,6 +124,29 @@ def indirect_console(): fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, oldfl & ~os.O_NONBLOCK) termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, oldtcattr) +def determine_tile_size(numnodes): + cheight, cwidth, pixwidth, pixheight = sq.get_screengeom() + ratio = (pixwidth / 16) / (pixheight / 10) + bestdeviation = None + bestdims = [] + for i in range(1, numnodes + 1): + number = numnodes + while number % i != 0: + number += 1 + columns = i + rows = number // i + deviation = abs(ratio - (columns / rows)) + if bestdeviation is None: + bestdeviation = deviation + bestdims = [columns, rows] + elif deviation < bestdeviation: + bestdeviation = deviation + bestdims = [columns, rows] + cellswide = cwidth // bestdims[0] + cellshigh = cheight // bestdims[1] + bestdims = bestdims + [cellswide, cellshigh] + return bestdims + cursor_saved = False def sticky_cursor(): global cursor_saved @@ -138,14 +166,14 @@ def sticky_cursor(): indirect_console() -def draw_image(data): +def draw_image(data, width, height): imageformat = os.environ.get('CONFLUENT_IMAGE_PROTOCOL', 'kitty') if imageformat == 'sixel': sixel_draw(data) elif imageformat == 'iterm': iterm_draw(data) else: - kitty_draw(data) + kitty_draw(data, width, height) def sixel_draw(data): @@ -165,11 +193,30 @@ def iterm_draw(data): sys.stdout.write('\n') sys.stdout.flush() -def kitty_draw(data): +def kitty_draw(data, width, height): + if Image: + bindata = base64.b64decode(data) + binfile = io.BytesIO() + binfile.write(bindata) + binfile.seek(0) + img = Image.open(binfile) + outfile = io.BytesIO() + img.save(outfile, format='PNG') + data = base64.b64encode(outfile.getbuffer()) + preamble = '\x1b_Ga=T,f=100' + if height: + preamble += f',r={height - 2},c={width}' + #sys.stdout.write(repr(preamble)) + #sys.stdout.write('\xb[{}D'.format(len(repr(preamble)))) + #return + first = True while data: chunk, data = data[:4096], data[4096:] m = 1 if data else 0 - sys.stdout.write('\x1b_Ga=T,f=100,m={};'.format(m)) + if first: + sys.stdout.write('{},m={};'.format(preamble, m)) + else: + sys.stdout.write('\x1b_Gm={};'.format(m)) sys.stdout.write(chunk.decode('utf8')) sys.stdout.write('\x1b\\') sys.stdout.flush() @@ -212,8 +259,47 @@ if options.Timestamp: logreader.dump_to_console(logname) sys.exit(0) +def prep_node_tile(node): + currcolcell, currrowcell = nodepositions[node] + if currcolcell: + sys.stdout.write(f'\x1b[{currcolcell}C') + if currrowcell: + sys.stdout.write(f'\x1b[{currrowcell}B') + sys.stdout.write(node) + sys.stdout.write('\x1b[{}D'.format(len(node))) + sys.stdout.write(f'\x1b[1B') + +def reset_cursor(node): + currcolcell, currrowcell = nodepositions[node] + if currcolcell: + sys.stdout.write(f'\x1b[{currcolcell}D') + sys.stdout.write(f'\x1b[{currrowcell + 1}A') + + +nodepositions = {} if options.screenshot: + cwidth = None + cheight = None sess = client.Command() + if options.tile: + allnodes = [] + numnodes = 0 + for res in sess.read('/noderange/{}/nodes/'.format(args[0])): + allnodes.append(res['item']['href'].replace('/', '')) + numnodes += 1 + cols, rows, cwidth, cheight = determine_tile_size(numnodes) + currcol = 1 + currcolcell = 0 + currrowcell = 0 + for node in allnodes: + nodepositions[node] = currcolcell, currrowcell + if currcol < cols: + currcol += 1 + currcolcell += cwidth + else: + currcol = 1 + currcolcell = 0 + currrowcell += cheight firstnodename = None dorefresh = True if options.interval is not None: @@ -228,19 +314,28 @@ if options.screenshot: if len(imgdata) < 32: # We were subjected to error sys.stderr.write(f'{node}: Unable to get screenshot\n') continue - if options.interval is not None: - if node != firstnodename: - sys.stderr.write('Multiple nodes not supported for interval') - sys.exit(1) - sticky_cursor() - sys.stdout.write('{}: '.format(node)) - draw_image(imgdata.encode()) - sys.stdout.write('\n') - if options.interval is None: - dorefresh = False - else: - dorefresh = True - time.sleep(options.interval) + if node in nodepositions: + prep_node_tile(node) + else: + if options.interval is not None: + if node != firstnodename: + sys.stderr.write('Multiple nodes not supported for interval') + sys.exit(1) + sticky_cursor() + sys.stdout.write('{}: '.format(node)) + draw_image(imgdata.encode(), cwidth, cheight - 1) + sys.stdout.write(f'\x1b[{cwidth}D') + sys.stdout.write(f'\x1b[{cheight - 1}A') + if node in nodepositions: + reset_cursor(node) + else: + sys.stdout.write('\n') + sys.stdout.flush() + if options.interval is None: + dorefresh = False + else: + dorefresh = True + time.sleep(options.interval) sys.exit(0) def kill(noderange): diff --git a/confluent_client/confluent/screensqueeze.py b/confluent_client/confluent/screensqueeze.py index 2fbeddc6..60aecc21 100644 --- a/confluent_client/confluent/screensqueeze.py +++ b/confluent_client/confluent/screensqueeze.py @@ -18,8 +18,9 @@ import struct import termios def get_screengeom(): - return struct.unpack('hh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, - b'....')) + # returns height in cells, width in cells, width in pixels, height in pixels + return struct.unpack('hhhh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, + b'........')) class ScreenPrinter(object): def __init__(self, noderange, client, textlen=4): @@ -58,7 +59,7 @@ class ScreenPrinter(object): def drawscreen(self, node=None): if self.squeeze: - currheight, currwidth = get_screengeom() + currheight, currwidth, _, _ = get_screengeom() currheight -= 2 if currheight < 1: currheight = 1 @@ -120,6 +121,7 @@ if __name__ == '__main__': c = client.Command() p = ScreenPrinter('d1-d12', c) p.set_output('d3', 'Upload: 67%') + p.set_output('d7', 'Upload: 67%')