okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
4ad84fd
main
1"""Source ranges and diffs as syntax-highlighted images, for reviewing code by 2its shape. 3 4Rendering is albedo-render (native/render), run as a supervised `run` job 5like the files plugin's searches; its pages attach to the running cell. 6""" 7 8from __future__import annotations 9 10import os 11import shlex 12import tempfile 13from collections .abc import Callable 14from pathlib import Path 15 16from albedo_api import PythonApi ,Text 17from albedo_plugins .files import Search ,_missing ,_run ,_which 18 19# Built by native/render/install.sh next to the kernel's own files. 20RENDERER = Path (__file__ ).resolve ().parents [2 ]/ "bin" / "albedo-render" 21ROWS = 80 # display rows per image: legible after providers scale a page down 22IMAGES = 4 # the kernel's per-cell image limit 23 24 25def setup (api :PythonApi )-> dict [str ,object ]: 26attach = api .attach_image 27 28def view_code ( 29path :str , 30start_line :int = 1 , 31end_line :int | None = None , 32* , 33columns :int = 79 , 34 )-> Search [Text ]: 35"""See lines start_line through end_line (default: as far as fits) as 36syntax-highlighted images, returned to you with this cell's result. 37Lines wrap at `columns`. At most 4 images of 80 display rows each; a 38wrapped line takes several rows, so wrap-heavy code fits fewer source 39lines per image. The text names the lines each image shows and the 40call that continues. Await it: `await view_code(path, 40, 120)`.""" 41_check_range (start_line ,end_line ,columns ) 42 43async def run ()-> Text : 44renderer ,target = _renderer (attach ),_existing (path ) 45assert attach is not None 46if target .is_dir (): 47raise IsADirectoryError ( 48f" { path } is a directory; view_code shows one file" 49 ) 50return await _render ( 51renderer , 52attach , 53target , 54path , 55start_line , 56end_line , 57columns , 58 [], 59lambda line :f"view_code( { path !r } , { line } )" , 60 ) 61 62return Search ("view_code" ,run ,"images" ) 63 64def view_diff ( 65path :str = "." ,start_line :int = 1 ,* ,staged :bool = False ,columns :int = 80 66 )-> Search [Text ]: 67"""See your uncommitted changes under `path` (git diff; staged=True for 68the index) as highlighted images: each hunk with its surrounding lines, 69+ for added and - for removed. `columns` is 80 so a 79-column line and 70its mark fit. Files git does not track yet are not included; view them 71with view_code. start_line counts lines of the diff, to continue where 72a call stopped. Await it: `await view_diff()`.""" 73_check_range (start_line ,None ,columns ) 74 75def resume (line :int )-> str : 76return f"view_diff( { path !r } , { line } { ', staged=True' if staged else '' } )" 77 78async def run ()-> Text : 79renderer ,target = _renderer (attach ),_existing (path ) 80assert attach is not None 81where = target if target .is_dir ()else target .parent 82with tempfile .TemporaryDirectory (prefix = "albedo-diff-" )as scratch : 83diff = Path (scratch )/ "changes.diff" 84git = ["git" ,"-C" ,str (where )] 85changes = [ 86* git , 87"diff" , 88"--no-color" , 89"--no-ext-diff" , 90* (["--staged" ]if staged else []), 91"--" , 92str (target .resolve ()), 93 ] 94# Outside a repository git diff compares paths instead; ask first. 95command = ( 96" " .join (map (shlex .quote , [* git ,"rev-parse" ,"--git-dir" ])) 97+ " > /dev/null && " 98+ " " .join (map (shlex .quote ,changes )) 99+ " > " 100+ shlex .quote (str (diff )) 101 ) 102status ,output = await _run (command ) 103if status != 0 : 104raise RuntimeError ( 105f"git diff failed: { output . strip ()[ - 400 :] or f'exit { status } ' } " 106 ) 107if diff .stat ().st_size == 0 : 108return Text ( 109f"no { 'staged' if staged else 'uncommitted' } changes under { path } " 110 ) 111label = f"diff of { path } " + (" (staged)" if staged else "" ) 112return await _render ( 113renderer , 114attach , 115diff , 116label , 117start_line , 118None , 119columns , 120 ["--language" ,"diff" ,"--no-line-numbers" ], 121resume , 122 ) 123 124return Search ("view_diff" ,run ,"images" ) 125 126return {"view_code" :view_code ,"view_diff" :view_diff } 127 128 129def _check_range (start_line :int ,end_line :int | None ,columns :int )-> None : 130if start_line < 1 or (end_line is not None and end_line < start_line ): 131raise ValueError ("start_line >= 1 and end_line >= start_line" ) 132if not 20 <= columns <= 400 : 133raise ValueError ("20 <= columns <= 400" ) 134 135 136def _renderer (attach :Callable [[bytes ],str ]| None )-> str : 137"""The renderer to run, checked before any work that needs it.""" 138if attach is None : 139raise RuntimeError ("this kernel cannot return images" ) 140renderer = ( 141str (RENDERER )if os .access (RENDERER ,os .X_OK )else _which ("albedo-render" ) 142 ) 143if renderer is None : 144raise RuntimeError ( 145"albedo-render is not built; run native/render/install.sh, " 146"or use files.read for the text" 147 ) 148return renderer 149 150 151def _existing (path :str )-> Path : 152target = Path (path ).expanduser () 153if not target .exists (): 154raise FileNotFoundError (_missing (target ,path )) 155return target 156 157 158async def _render ( 159renderer :str , 160attach :Callable [[bytes ],str ], 161file :Path , 162label :str , 163start_line :int , 164end_line :int | None , 165columns :int , 166options :list [str ], 167resume :Callable [[int ],str ], 168)-> Text : 169"""Render `file` and attach its pages to the cell; `resume` names the call 170that continues from a line.""" 171with tempfile .TemporaryDirectory (prefix = "albedo-view-" )as out : 172command = " " .join ( 173shlex .quote (part ) 174for part in [ 175renderer , 176str (file .resolve ()), 177"--out" , 178out , 179"--start" , 180str (start_line ), 181"--end" , 182str (end_line or 2 ** 32 ), 183"--columns" , 184str (columns ), 185"--max-rows" , 186str (ROWS ), 187"--max-images" , 188str (IMAGES ), 189* options , 190 ] 191 ) 192status ,output = await _run (command ) 193if status != 0 : 194failure = [ 195line 196for line in output .splitlines () 197if line .startswith ("albedo-render:" ) 198 ] 199raise RuntimeError ( 200failure [- 1 ].removeprefix ("albedo-render: " ) 201if failure 202else f"albedo-render did not finish: { output . strip ()[ - 400 :] } " 203 ) 204return _attach_pages (attach ,label ,output ,resume ) 205 206 207def _attach_pages ( 208attach :Callable [[bytes ],str ], 209label :str , 210report :str , 211resume :Callable [[int ],str ], 212)-> Text : 213"""Attach each page the renderer wrote, in order, and say what was shown.""" 214language ,shown ,rest = "plain" , [],None 215for line in report .splitlines (): 216kind ,_ ,fields = line .partition (" " ) 217if kind == "language" : 218language = fields 219elif kind == "image" : 220file ,first ,last ,size = fields .rsplit (" " ,3 ) 221shown .append ((file ,int (first ),int (last ),size )) 222elif kind == "remaining" : 223rest = int (fields .split ()[0 ]) 224notes = [f" { label } ( { language } )" ] 225for number , (file ,first ,last ,size )in enumerate (shown ,1 ): 226try : 227_ = attach (Path (file ).read_bytes ()) 228except ValueError as full : 229notes .append (f"image { number } not attached: { full } " ) 230rest = first 231break 232notes .append (f"image { number } : lines { first } - { last } , { size } " ) 233if rest is not None : 234notes .append (f"not shown from line { rest } : { resume ( rest ) } " ) 235return Text ("\n" .join (notes ))