Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source
Source file framebuffer_widget.ml
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580(******************************************************************************)(* *)(* SPDX-License-Identifier: MIT *)(* Copyright (c) 2026 Mathias Bourgoin <mathias.bourgoin@atacama.tech> *)(* *)(******************************************************************************)(* Pixel-level framebuffer widget.
Dispatches rendering to the best available sub-pixel mode via Terminal_caps. *)letansi_reset="\027[0m"letrgb_to_ansi_256rgb=ifr=g&&g=bthenifr<8then16elseifr>248then231else232+((r-8)/10)elseletr'=r*6/256inletg'=g*6/256inletb'=b*6/256in16+(36*r')+(6*g')+b'typet={mutablepixels:bytes;(* flat RGB, stride = width_px * 3 *)mutablewidth_px:int;mutableheight_px:int;mutabledirty:bool;mutablerender_cache:stringoption;mutablelast_cols:int;mutablelast_rows:int;}letcreate()={pixels=Bytes.empty;width_px=0;height_px=0;dirty=true;render_cache=None;last_cols=0;last_rows=0;}letresize_pixelst~width~height=letnew_size=width*height*3inifwidth<>t.width_px||height<>t.height_pxthenbeginletnew_buf=Bytes.makenew_size'\000'in(* Copy overlapping region *)letcopy_w=minwidtht.width_pxinletcopy_h=minheightt.height_pxinfory=0tocopy_h-1doBytes.blitt.pixels(y*t.width_px*3)new_buf(y*width*3)(copy_w*3)done;t.pixels<-new_buf;t.width_px<-width;t.height_px<-height;t.dirty<-trueendletset_pixelt~x~y~r~g~b=ifx>=0&&x<t.width_px&&y>=0&&y<t.height_pxthenbeginletoffset=((y*t.width_px)+x)*3inBytes.sett.pixelsoffset(Char.chr(rland0xFF));Bytes.sett.pixels(offset+1)(Char.chr(gland0xFF));Bytes.sett.pixels(offset+2)(Char.chr(bland0xFF));t.dirty<-trueendletblitt~src~width~height=letnew_size=width*height*3inletsrc_size=Bytes.lengthsrcinletbuf=Bytes.createnew_sizeinBytes.blitsrc0buf0(minnew_sizesrc_size);t.pixels<-buf;t.width_px<-width;t.height_px<-height;t.dirty<-true;t.render_cache<-Noneletcleart~r~g~b=letn=t.width_px*t.height_pxinfori=0ton-1doBytes.sett.pixels(i*3)(Char.chr(rland0xFF));Bytes.sett.pixels((i*3)+1)(Char.chr(gland0xFF));Bytes.sett.pixels((i*3)+2)(Char.chr(bland0xFF))done;t.dirty<-trueletfill_rectt~x~y~w~h~r~g~b=letx1=max0xandy1=max0yinletx2=mint.width_px(x+w)inlety2=mint.height_px(y+h)inforpy=y1toy2-1doforpx=x1tox2-1doletoffset=((py*t.width_px)+px)*3inBytes.sett.pixelsoffset(Char.chr(rland0xFF));Bytes.sett.pixels(offset+1)(Char.chr(gland0xFF));Bytes.sett.pixels(offset+2)(Char.chr(bland0xFF))donedone;t.dirty<-true(* Inline pixel reader *)letget_rgbtpxpy=ifpx<0||px>=t.width_px||py<0||py>=t.height_pxthen(0,0,0)elseletoffset=((py*t.width_px)+px)*3in(Char.code(Bytes.gett.pixelsoffset),Char.code(Bytes.gett.pixels(offset+1)),Char.code(Bytes.gett.pixels(offset+2)))(* ── Render: Half_block ──────────────────────────────────────────────────── *)letrender_half_blocktcolsrows=(* 1×2 pixels per cell: top pixel = fg ("▀"), bottom pixel = bg.
Pure-black cells (0,0,0) are treated as transparent → space. *)letbuf=Buffer.create(rows*((cols*25)+1))inforcy=0torows-1doifcy>0thenBuffer.add_charbuf'\n';forcx=0tocols-1doletr_top,g_top,b_top=get_rgbtcx(cy*2)inletr_bot,g_bot,b_bot=get_rgbtcx((cy*2)+1)inifr_top=0&&g_top=0&&b_top=0&&r_bot=0&&g_bot=0&&b_bot=0thenBuffer.add_charbuf' 'elsebegin(* Use fg=top (▀), bg=bottom — both as truecolor bg fills for solid rendering.
When colors match, one bg+space suffices. Otherwise ▀ with fg+bg. *)ifr_top=r_bot&&g_top=g_bot&&b_top=b_botthenBuffer.add_stringbuf(Printf.sprintf"\027[48;2;%d;%d;%dm %s"r_topg_topb_topansi_reset)elseBuffer.add_stringbuf(Printf.sprintf"\027[38;2;%d;%d;%dm\027[48;2;%d;%d;%dm\xE2\x96\x80%s"r_topg_topb_topr_botg_botb_botansi_reset)enddonedone;Buffer.contentsbuf(* ── Render: Braille ─────────────────────────────────────────────────────── *)letrender_brailletcolsrows=letcanvas=Braille_canvas.create~width:cols~height:rowsinforcy=0torows-1doforcx=0tocols-1dofordy=0to3dofordx=0to1doletpx=(cx*2)+dxandpy=(cy*4)+dyinifpx<t.width_px&&py<t.height_pxthenbeginletr,g,b=get_rgbtpxpyinletluma=((r*299)+(g*587)+(b*114))/1000inifluma>128thenBraille_canvas.set_dotcanvas~x:px~y:pyenddonedonedonedone;Braille_canvas.rendercanvas(* ── Render: Octant ──────────────────────────────────────────────────────── *)letrender_octanttcolsrows=(* 2×4 pixels per cell with two-color quantization per cell.
For each 2×4 block: compute average luma, classify each pixel as
fg (above avg) or bg (below avg), build the octant pattern accordingly. *)letbuf=Buffer.create(rows*((cols*25)+1))inforcy=0torows-1doifcy>0thenBuffer.add_charbuf'\n';forcx=0tocols-1do(* Collect 8 pixels for this cell (row-major bit order) *)letrs=Array.make80inletgs=Array.make80inletbs=Array.make80inletlumas=Array.make80inletvalid=ref0infordy=0to3dofordx=0to1doleti=(dy*2)+dxinletpx=(cx*2)+dxandpy=(cy*4)+dyinletr,g,b=get_rgbtpxpyinrs.(i)<-r;gs.(i)<-g;bs.(i)<-b;lumas.(i)<-((r*299)+(g*587)+(b*114))/1000;incrvaliddonedone;if!valid=0thenBuffer.add_charbuf' 'elsebegin(* Average luma for threshold *)letsum_luma=Array.fold_left(+)0lumasinletavg_luma=sum_luma/8in(* Classify and accumulate fg/bg averages *)letpattern=ref0inletfg_r=ref0andfg_g=ref0andfg_b=ref0andfg_n=ref0inletbg_r=ref0andbg_g=ref0andbg_b=ref0andbg_n=ref0infori=0to7doiflumas.(i)>=avg_lumathenbeginpattern:=!patternlor(1lsli);fg_r:=!fg_r+rs.(i);fg_g:=!fg_g+gs.(i);fg_b:=!fg_b+bs.(i);incrfg_nendelsebeginbg_r:=!bg_r+rs.(i);bg_g:=!bg_g+gs.(i);bg_b:=!bg_b+bs.(i);incrbg_nenddone;letglyph=Octant_canvas.glyph_of_pattern!patterninmatch!patternwith|0->(* All dark: use bg color as a space *)if!bg_n>0thenbeginletr=!bg_r/!bg_nandg=!bg_g/!bg_nandb=!bg_b/!bg_ninletidx=rgb_to_ansi_256rgbinBuffer.add_stringbuf(Printf.sprintf"\027[48;5;%dm %s"idxansi_reset)endelseBuffer.add_charbuf' '|0xFF->(* All bright: use fg glyph for solid fill *)if!fg_n>0thenbeginletr=!fg_r/!fg_nandg=!fg_g/!fg_nandb=!fg_b/!fg_ninifr=0&&g=0&&b=0thenBuffer.add_charbuf' 'elseletidx=rgb_to_ansi_256rgbinBuffer.add_stringbuf(Printf.sprintf"\027[38;5;%dm%s%s"idxglyphansi_reset)endelseBuffer.add_charbuf' '|_->(* Mixed: fg for set bits, bg for unset bits *)letfg_code=if!fg_n>0thenletr=!fg_r/!fg_nandg=!fg_g/!fg_nandb=!fg_b/!fg_ninPrintf.sprintf"\027[38;5;%dm"(rgb_to_ansi_256rgb)else""inletbg_code=if!bg_n>0thenletr=!bg_r/!bg_nandg=!bg_g/!bg_nandb=!bg_b/!bg_ninPrintf.sprintf"\027[48;5;%dm"(rgb_to_ansi_256rgb)else""inBuffer.add_stringbuffg_code;Buffer.add_stringbufbg_code;Buffer.add_stringbufglyph;Buffer.add_stringbufansi_resetenddonedone;Buffer.contentsbuf(* ── Render: Sextant (U+1FB00 range, 2×3 per cell) ─────────────────────── *)(* Sextant bit layout (Unicode 13, U+1FB00 range, reading order):
bit 0: (row 0, col 0) bit 1: (row 0, col 1)
bit 2: (row 1, col 0) bit 3: (row 1, col 1)
bit 4: (row 2, col 0) bit 5: (row 2, col 1)
U+1FB00 = pattern 0b000001, ..., U+1FB3B = pattern 0b111110
(patterns 0 and 63 use space / full block respectively) *)letsextant_base=0x1FB00(* Encode a codepoint in the supplementary plane to UTF-8 *)letencode_cp4cp=String.init4(funi->matchiwith|0->Char.chr(0xF0lor(cplsr18))|1->Char.chr(0x80lor((cplsr12)land0x3F))|2->Char.chr(0x80lor((cplsr6)land0x3F))|_->Char.chr(0x80lor(cpland0x3F)))(* Precompute sextant glyphs for all 64 patterns (6-bit) *)letsextant_glyphs:stringarray=Array.init64(funpattern->matchpatternwith|0->" "|0x3F->"\xE2\x96\x88"(* U+2588 FULL BLOCK *)|p->encode_cp4(sextant_base+p-1))letrender_sextanttcolsrows=letbuf=Buffer.create(rows*((cols*25)+1))inforcy=0torows-1doifcy>0thenBuffer.add_charbuf'\n';forcx=0tocols-1doletrs=Array.make60inletgs=Array.make60inletbs=Array.make60inletlumas=Array.make60infordy=0to2dofordx=0to1doleti=(dy*2)+dxinletpx=(cx*2)+dxandpy=(cy*3)+dyinletr,g,b=get_rgbtpxpyinrs.(i)<-r;gs.(i)<-g;bs.(i)<-b;lumas.(i)<-((r*299)+(g*587)+(b*114))/1000donedone;letsum_luma=Array.fold_left(+)0lumasinletavg_luma=sum_luma/6inletpattern=ref0inletfg_r=ref0andfg_g=ref0andfg_b=ref0andfg_n=ref0inletbg_r=ref0andbg_g=ref0andbg_b=ref0andbg_n=ref0infori=0to5doiflumas.(i)>=avg_lumathenbeginpattern:=!patternlor(1lsli);fg_r:=!fg_r+rs.(i);fg_g:=!fg_g+gs.(i);fg_b:=!fg_b+bs.(i);incrfg_nendelsebeginbg_r:=!bg_r+rs.(i);bg_g:=!bg_g+gs.(i);bg_b:=!bg_b+bs.(i);incrbg_nenddone;letglyph=sextant_glyphs.(!pattern)inmatch!patternwith|0->if!bg_n>0thenbeginletr=!bg_r/!bg_nandg=!bg_g/!bg_nandb=!bg_b/!bg_ninBuffer.add_stringbuf(Printf.sprintf"\027[48;2;%d;%d;%dm %s"rgbansi_reset)endelseBuffer.add_charbuf' '|0x3F->(* All bright: use bg+space for pixel-exact solid fill (fg glyphs can
have font-dependent gaps showing the terminal background through) *)if!fg_n>0thenbeginletr=!fg_r/!fg_nandg=!fg_g/!fg_nandb=!fg_b/!fg_ninifr=0&&g=0&&b=0thenBuffer.add_charbuf' 'elseBuffer.add_stringbuf(Printf.sprintf"\027[48;2;%d;%d;%dm %s"rgbansi_reset)endelseBuffer.add_charbuf' '|_->letfg_code=if!fg_n>0thenletr=!fg_r/!fg_nandg=!fg_g/!fg_nandb=!fg_b/!fg_ninPrintf.sprintf"\027[38;2;%d;%d;%dm"rgbelse""inletbg_code=if!bg_n>0thenletr=!bg_r/!bg_nandg=!bg_g/!bg_nandb=!bg_b/!bg_ninPrintf.sprintf"\027[48;2;%d;%d;%dm"rgbelse""inBuffer.add_stringbuffg_code;Buffer.add_stringbufbg_code;Buffer.add_stringbufglyph;Buffer.add_stringbufansi_resetdonedone;Buffer.contentsbuf(* ── Render: Sixel ───────────────────────────────────────────────────────── *)(* DCS Sixel encoding.
Each sixel character covers a 1-pixel-wide × 6-pixel-tall column.
Bit pattern: bit 0 = top row, bit 5 = bottom row. ASCII: pattern + 63.
Pb=1 → unset pixels show terminal background (transparent). *)letrender_sixeltcolsrows=letw=t.width_pxandh=t.height_pxinletbuf=Buffer.create(max1024(w*h/4))inBuffer.add_stringbuf"\027P0;1;0q";(* Pass 1: build palette and color index map in one pass. *)letpalette_tbl=Hashtbl.create64inletpalette_rgb=Array.make256(0,0,0)inletn_colors=ref0inlettransparent=-1inletidx_map=Array.make(w*h)transparentinforpy=0toh-1doletrow_off=py*winforpx=0tow-1doletoff=(row_off+px)*3inletr=Char.code(Bytes.unsafe_gett.pixelsoff)inletg=Char.code(Bytes.unsafe_gett.pixels(off+1))inletb=Char.code(Bytes.unsafe_gett.pixels(off+2))inifr<>0||g<>0||b<>0thenbeginletkey=(rlsl16)lor(glsl8)lorbinletci=matchHashtbl.find_optpalette_tblkeywith|Somei->i|None->if!n_colors>=256thenbeginletbest_i=ref0andbest_d=refmax_intinfori=0to!n_colors-1doletpr,pg,pb=palette_rgb.(i)inletd=((r-pr)*(r-pr))+((g-pg)*(g-pg))+((b-pb)*(b-pb))inifd<!best_dthen(best_d:=d;best_i:=i)done;!best_iendelsebeginleti=!n_colorsinHashtbl.addpalette_tblkeyi;palette_rgb.(i)<-(r,g,b);incrn_colors;iendinidx_map.(row_off+px)<-cienddonedone;letnc=!n_colorsin(* Emit palette. *)fori=0tonc-1doletr,g,b=palette_rgb.(i)inBuffer.add_stringbuf(Printf.sprintf"#%d;2;%d;%d;%d"i(r*100/255)(g*100/255)(b*100/255))done;(* Pass 2: single-pass per band — build all color patterns simultaneously,
then emit only colors that appeared. O(w × 6 × bands) instead of
O(nc × w × 6 × bands). *)letn_bands=(h+5)/6in(* Per-color pattern array: band_pat.(ci).(px) = sixel bit pattern *)letband_pat=Array.init(max1nc)(fun_->Array.makew0)inletcolor_present=Array.make(max1nc)falseinletemit_rlepatcount=letc=Char.chr(pat+63)inifcount<=3thenfor_=1tocountdoBuffer.add_charbufcdoneelsebeginBuffer.add_charbuf'!';Buffer.add_stringbuf(string_of_intcount);Buffer.add_charbufcendinforband=0ton_bands-1doletpy0=band*6in(* Clear presence flags *)Array.fillcolor_present0ncfalse;(* Single pass: scatter each pixel's bit into its color's pattern row *)forrow=0to5doletpy=py0+rowinifpy<hthenbeginletrow_off=py*winletbit=1lslrowinforpx=0tow-1doletci=idx_map.(row_off+px)inifci>=0thenbeginband_pat.(ci).(px)<-band_pat.(ci).(px)lorbit;color_present.(ci)<-trueenddoneenddone;(* Emit each color that appeared in this band *)forci=0tonc-1doifcolor_present.(ci)thenbeginletpats=band_pat.(ci)inBuffer.add_stringbuf(Printf.sprintf"#%d"ci);letrun_pat=refpats.(0)inletrun_len=ref1inforpx=1tow-1doifpats.(px)=!run_patthenincrrun_lenelsebeginemit_rle!run_pat!run_len;run_pat:=pats.(px);run_len:=1enddone;emit_rle!run_pat!run_len;Buffer.add_charbuf'$';(* Clear for next band *)Array.fillpats0w0enddone;ifband<n_bands-1thenBuffer.add_charbuf'-'done;Buffer.add_stringbuf"\027\\";ignore(cols,rows);Buffer.contentsbuf(* ── Main render dispatcher ──────────────────────────────────────────────── *)(* Sub-pixel dimensions for each mode: (px_per_cell_x, px_per_cell_y) *)letpx_per_cellmode=matchmodewith|Terminal_caps.Sixel->(8,16)(* approximate; real size via cell_pixel_size *)|Terminal_caps.Octant->(2,4)|Terminal_caps.Sextant->(2,3)|Terminal_caps.Half_block->(1,2)|Terminal_caps.Braille->(2,4)letrender_with_modet~mode~cols~rows=letpx_x,px_y=px_per_cellmodeinletneed_w=cols*px_xandneed_h=rows*px_yinifcols<>t.last_cols||rows<>t.last_rowsthenbeginresize_pixelst~width:need_w~height:need_h;t.last_cols<-cols;t.last_rows<-rows;t.render_cache<-Noneend;if(nott.dirty)&&t.render_cache<>NonethenOption.gett.render_cacheelsebeginift.width_px=0||t.height_px=0thenresize_pixelst~width:need_w~height:need_h;letoutput=matchmodewith|Terminal_caps.Sixel->render_sixeltcolsrows|Terminal_caps.Octant->render_octanttcolsrows|Terminal_caps.Sextant->render_sextanttcolsrows|Terminal_caps.Half_block->render_half_blocktcolsrows|Terminal_caps.Braille->render_brailletcolsrowsint.dirty<-false;t.render_cache<-Someoutput;outputendletrendert~cols~rows=render_with_modet~mode:(Terminal_caps.detect())~cols~rowslet()=Miaou_registry.register~name:"framebuffer"~mli:[%blob"framebuffer_widget.mli"]()