2
0
mirror of https://github.com/xcat2/confluent.git synced 2026-07-31 10:09:46 +00:00

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.
This commit is contained in:
Jarrod Johnson
2025-04-22 10:35:14 -04:00
parent 0cfdfbdfa4
commit 94af42031e
2 changed files with 117 additions and 20 deletions
+112 -17
View File
@@ -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):
+5 -3
View File
@@ -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%')