char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
c52d3a7
main
1import type { Cursor , Frame , Run } from "./ghostty.ts" ; 2import { COLORS , readable } from "./palette.ts" ; 3 4export type Metrics = { 5width :number ; 6height :number ; 7ratio :number ; 8cell :{ width :number ; height :number }; 9}; 10const css = ([ r , g , b ] :readonly number []) => `rgb( ${ r } ${ g } ${ b } )` ; 11const graphemes = new Intl . Segmenter ( undefined , { granularity :"grapheme" }); 12 13/** A painted cell grid with real text above it for native selection and copying. */ 14export class Grid { 15readonly #rows:HTMLElement ; 16readonly #canvas:HTMLCanvasElement ; 17readonly #context:CanvasRenderingContext2D | null ; 18readonly #lines:{ 19element :HTMLElement ; 20runs :{ run :Run ; element :HTMLElement ; start :number ; end :number }[]; 21glyphs :{ element :HTMLElement ; start :number ; end :number }[]; 22}[] = []; 23 #cursor:Cursor | null = null ; 24 #focused= false ; 25 #metrics:Metrics = { width :0 , height :0 , ratio :1 , cell :{ width :8 , height :18 } }; 26 27constructor ( rows :HTMLElement , canvas :HTMLCanvasElement ) { 28this . #rows= rows ; 29this . #canvas= canvas ; 30this . #context= canvas . getContext ( "2d" ); 31} 32 33fit ( metrics :Metrics ) { 34const old = this . #metrics; 35if ( 36metrics . width === old . width && metrics . height === old . height && 37metrics . ratio === old . ratio && metrics . cell . width === old . cell . width && 38metrics . cell . height === old . cell . height 39) return ; 40this . #metrics= metrics ; 41this . #canvas. width = Math . max ( 1 , Math . round ( metrics . width * metrics . ratio )); 42this . #canvas. height = Math . max ( 1 , Math . round ( metrics . height * metrics . ratio )); 43this . #canvas. style . width = ` ${ this . #canvas . width / metrics . ratio } px` ; 44this . #canvas. style . height = ` ${ this . #canvas . height / metrics . ratio } px` ; 45for ( let y = 0 ; y < this . #lines. length ; y ++ ) this . #position( y ); 46this . #paint(); 47} 48 49update ( frame :Frame ) { 50while ( this . #lines. length < frame . rows ) { 51this . #lines. push ({ 52element :this . #rows. appendChild ( document . createElement ( "div" )), 53runs :[], 54glyphs :[], 55}); 56} 57while ( this . #lines. length > frame . rows ) this . #lines. pop () ! . element . remove (); 58for ( const [ y , runs ] of frame . changed ) { 59this . #text( y , runs ); 60} 61this . #cursor= frame . cursor ; 62this . #paint(); 63} 64 65focus ( focused :boolean ) { 66this . #focused= focused ; 67this . #paint(); 68} 69 70 #x( column :number ) { 71return Math . round ( column * this . #metrics. cell . width * this . #metrics. ratio ); 72} 73 74 #y( row :number ) { 75return Math . round ( row * this . #metrics. cell . height * this . #metrics. ratio ); 76} 77 78 #text( y :number , runs :Run []) { 79const line = this . #lines[ y ]; 80line . runs = []; 81line . glyphs = []; 82let column = 0 ; 83const spans = runs . map (( run ) => { 84const start = column ; 85const span = document . createElement ( "span" ); 86span . style . color = run . invisible 87 ?"transparent" 88 :css ( readable ( run . fg ?? COLORS . foreground , run . bg ?? COLORS . background )); 89if ( run . bold ) span . style . fontWeight = "bold" ; 90if ( run . italic ) span . style . fontStyle = "italic" ; 91if ( run . faint ) span . style . opacity = "0.6" ; 92span . className = "terminal-run" ; 93for ( const { segment} of graphemes . segment ( run . text )) { 94const end = column + ( run . wide ?2 :1 ); 95if ( run . wide ) { 96const glyph = document . createElement ( "span" ); 97glyph . className = "terminal-glyph" ; 98glyph . textContent = segment ; 99line . glyphs . push ({ element :glyph , start :column , end}); 100span . append ( glyph ); 101} 102column = end ; 103} 104if ( ! run . wide ) span . textContent = run . text ; 105line . runs . push ({ run, element :span , start, end :column }); 106return span ; 107}); 108line . element . replaceChildren ( ...( spans . length ?spans :[ document . createElement ( "br" )])); 109this . #position( y ); 110} 111 112 #position( y :number ) { 113const { element, runs, glyphs} = this . #lines[ y ]; 114const { ratio} = this . #metrics; 115// keep rows in flow for native line breaks, without accumulating snapped-height rounding 116element . style . top = ` ${ this . #y ( y ) / ratio - y * this . #metrics . cell . height } px` ; 117element . style . height = ` ${ this . #metrics . cell . height } px` ; 118element . style . lineHeight = ` ${( this . #y ( y + 1 ) - this . #y ( y )) / ratio } px` ; 119for ( const span of [ ...runs , ...glyphs ]) { 120span . element . style . width = ` ${( this . #x ( span . end ) - this . #x ( span . start )) / ratio } px` ; 121} 122} 123 124 #paint() { 125const ctx = this . #context; 126if ( ! ctx ) return ; 127const { ratio} = this . #metrics; 128ctx . clearRect ( 0 , 0 , this . #canvas. width , this . #canvas. height ); 129for ( let y = 0 ; y < this . #lines. length ; y ++ ) { 130const top = this . #y( y ); 131const bottom = this . #y( y + 1 ); 132for ( const { run, start, end} of this . #lines[ y ]. runs ) { 133const left = this . #x( start ); 134const right = this . #x( end ); 135if ( run . bg ) { 136ctx . fillStyle = css ( run . bg ); 137ctx . fillRect ( left , top , right - left , bottom - top ); 138} 139if ( ! run . invisible && ( run . underline || run . strikethrough )) { 140ctx . save (); 141ctx . globalAlpha = run . faint ?0.6 :1 ; 142ctx . strokeStyle = ctx . fillStyle = css ( 143readable ( run . fg ?? COLORS . foreground , run . bg ?? COLORS . background ), 144); 145const thickness = Math . max ( 1 , Math . round ( ratio )); 146const baseline = bottom - thickness * 2 ; 147if ( run . strikethrough ) { 148ctx . fillRect ( left , Math . round (( top + bottom ) / 2 ), right - left , thickness ); 149} 150if ( run . underline === 3 ) { 151ctx . lineWidth = thickness ; 152ctx . beginPath (); 153for ( let x = left ; x <= right ; x ++ ) { 154const at = baseline + Math . sin (( x - left ) / ( 2 * ratio )) * ratio ; 155if ( x === left ) ctx . moveTo ( x , at ); 156else ctx . lineTo ( x , at ); 157} 158ctx . stroke (); 159} else if ( run . underline ) { 160const step = run . underline === 4 161 ?thickness * 2 162 :run . underline === 5 163 ?thickness * 6 164 :right - left ; 165const length = run . underline === 4 166 ?thickness 167 :run . underline === 5 168 ?thickness * 4 169 :step ; 170for ( let x = left ; x < right ; x += step ) { 171ctx . fillRect ( x , baseline , Math . min ( length , right - x ), thickness ); 172} 173if ( run . underline === 2 ) { 174ctx . fillRect ( left , baseline - thickness * 2 , right - left , thickness ); 175} 176} 177ctx . restore (); 178} 179} 180} 181const at = this . #cursor; 182if ( ! at ) return ; 183const left = this . #x( at . x ); 184const top = this . #y( at . y ); 185const width = this . #x( at . x + 1 ) - left ; 186const height = this . #y( at . y + 1 ) - top ; 187const stroke = Math . max ( 1 , Math . round ( ratio )); 188const thick = Math . max ( 1 , Math . round ( 2 * ratio )); 189ctx . fillStyle = css ( COLORS . cursor ); 190if ( ! this . #focused|| at . style === "hollow" ) { 191ctx . fillRect ( left , top , width , stroke ); 192ctx . fillRect ( left , top + height - stroke , width , stroke ); 193ctx . fillRect ( left , top , stroke , height ); 194ctx . fillRect ( left + width - stroke , top , stroke , height ); 195} else if ( at . style === "bar" ) ctx . fillRect ( left , top , thick , height ); 196else if ( at . style === "underline" ) ctx . fillRect ( left , top + height - thick , width , thick ); 197else { 198ctx . globalAlpha = 0.7 ; 199ctx . fillRect ( left , top , width , height ); 200ctx . globalAlpha = 1 ; 201} 202} 203}