123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533(*————————————————————————————————————————————————————————————————————————————
Copyright (c) 2020–2021 Craig Ferguson <me@craigfe.io>
Distributed under the MIT license. See terms at the end of this file.
————————————————————————————————————————————————————————————————————————————*)includeLine_primitives_intfincludeLine_primitives_intf.Typesopen!ImportopenStaged.Syntax(** The core DSL that is used to define individual lines of a progress bar
display. An ['a t] is an immutable specification of a progress bar line that
consumes values of type ['a], and an ['a Compiled.t] is an efficient mutable
instantiation of that specification used for a single rendering lifecycle.
{2 Width tracking}
We track the rendered "widths" of various components for two reasons: to
handle expansive elements / boxes, and to enable the renderer to respond
correctly to terminal size changes. This is done algebraically for
performance: the alternative of measuring the rendered width is inefficient
because it would need to account for UTF-8 encoding and zero-width ANSI
colour codes. *)type'app=Format.formatter->'a->unittype'at=|Noop|Thetaof{pp:Line_buffer.t->event->unit;width:int}|Alphaof{pp:Line_buffer.t->event->'a->unit;initial:[`ThetaofLine_buffer.t->unit|`Valof'a];width:int}|Alpha_unsizedof{pp:width:(unit->int)->Line_buffer.t->event->'a->int;initial:[`Thetaofwidth:(unit->int)->Line_buffer.t->int|`Valof'a]}|Stagedof(unit->'at)|On_finaliseof{final:'a;inner:'at}|Contramap:'at*('b->'a)->'bt|Condof{if_:'a->bool;then_:'at}|Boxof{contents:'at;width:intSta_dyn.t;pad:[`left|`right|`none]}|Groupof'atarray|Pair:{left:'at;sep:unitt;right:'bt}->('a*'b)tlet[@warning"-unused-value-declaration"]recpp_dump:typea.atpp=funppf->function|Noop->Fmt.stringppf"Noop"|Theta{width;_}->Fmt.pfppf"Theta { width = %d }"width|Alpha{width;_}->Fmt.pfppf"Alpha { width = %d }"width|Alpha_unsized_->Fmt.stringppf"Alpha_unsized _"|Cond{then_;_}->Fmt.pfppf"Cond { then_ = %a }"pp_dumpthen_|Contramap(x,_)->Fmt.pfppf"Contramap ( %a )"pp_dumpx|Stagedf->Fmt.pfppf"Staged ( %a )"pp_dump(f())|On_finalise{inner;_}->Fmt.pfppf"On_finalise ( %a )"pp_dumpinner|Box{contents;_}->Fmt.pfppf"Box ( %a )"pp_dumpcontents|Groupxs->Fmt.Dump.arraypp_dumpppfxs|Pair{left;sep;right}->Fmt.pfppf"(%a, %a, %a)"pp_dumpleftpp_dumpseppp_dumprightletnoop()=Noopletarrayts=Grouptsletof_pp~width~initialpp=letppbufx=Line_buffer.with_ppfbuf(funppf->ppppfx)inAlpha{pp;width;initial=`Valinitial}letalpha~width~initialpp=Alpha{pp;initial;width}letalpha_unsized~initialpp=Alpha_unsized{pp;initial}lettheta~widthpp=Theta{pp;width}letconditionalpreds=Cond{if_=pred;then_=s}letcontramap~fx=Contramap(x,f)leton_finalisefinalinner=On_finalise{final;inner}(** [ticker n] is a function [f] that returns [true] on every [n]th call. *)lettickerinterval:unit->bool=letticker=ref0infun()->ticker:=(!ticker+1)modinterval;!ticker=0letstatefulf=Stagedfletperiodicintervalt=matchintervalwith|nwhenn<=0->Format.kasprintfinvalid_arg"Non-positive interval: %d"n|1->t|_->stateful(fun()->letshould_update=tickerintervalinconditional(fun_->should_update())t)letbox_dynamic?(pad=`none)widthcontents=Box{contents;width=Dynamicwidth;pad}letbox_fixed?(pad=`none)widthcontents=Box{contents;width=Staticwidth;pad}letpair?(sep=noop())ab=Pair{left=a;sep;right=b}letaccumulatorcombinezeros=stateful(fun()->letstate=refzeroincontramaps~f:(funa->state:=combine!statea;!state))(** The [compile] step transforms a pure [t] term in to a potentially-impure
[Compiled.t] term to be used for a single display lifecycle. It has three
purposes:
- eliminate [Staged] nodes by executing any side-effects in preparation for
display;
- compute the available widths of any unsized nodes;
- inline nested groupings to make printing more efficient. *)moduleCompiled=structtype'at=|Noop|Alphaof{pp:Line_buffer.t->event->'a->int;mutablelatest:Line_buffer.t->int}|Thetaof{pp:Line_buffer.t->event->int}|Contramap:'at*('b->'a)->'bt|On_finaliseof{final:'a;inner:'at}|Padof{contents:'at;dir:[`leftofLine_buffer.t|`right];width:intSta_dyn.t}|Condof{if_:'a->bool;then_:'at;width:intSta_dyn.t;mutablelatest:'aoption;mutablelatest_span:Line_buffer.Span.t}|Groupof'atarray|Pair:{left:'at;sep:unitt;right:'bt}->('a*'b)tletrecpp_dump:typea.atpp=funppf->function|Noop->Fmt.stringppf"Noop"|Alpha_->Fmt.stringppf"Alpha _"|Theta_->Fmt.stringppf"Theta _"|On_finalise{inner;_}->Fmt.pfppf"On_finalise ( %a )"pp_dumpinner|Cond{then_;latest_span;width;_}->Fmt.pfppf"Cond { if_ = <opaque>; then_ = %a; width = %a; span = %a }"pp_dumpthen_(Sta_dyn.ppFmt.int)widthLine_buffer.Span.pplatest_span|Contramap(x,_)->Fmt.pfppf"Contramap ( %a )"pp_dumpx|Pad{contents;dir;width}->Fmt.pfppf"Pad { contents = %a;@,dir = %s;@,width = %a }"pp_dumpcontents(matchdirwith`left_->"`left"|`right->"`right")(Sta_dyn.ppFmt.int)width|Groupxs->Fmt.Dump.arraypp_dumpppfxs|Pair{left;sep;right}->Fmt.pfppf"(%a, %a, %a)"pp_dumpleftpp_dumpseppp_dumprightendmoduleCompiler_state:sigtype'at(* State monad instance: *)moduleSyntax:sigvalreturn:'a->'atval(let+):'at->('a->'b)->'btval(let*):'at->('a->'bt)->'btend(* Interacting with the state: *)valconsume_space:intSta_dyn.t->unittvalmeasure_consumed:'at->('a*intSta_dyn.t)tvalexpand:(unit->int)tvalwith_expansion_point:intSta_dyn.t->'at->('a*[`used|`not_used]*intSta_dyn.t)t(* Threading state through the compuation: *)valrun:'at->'aend=structtypestate={consumed:intSta_dyn.t;consumed_static:int;expand:[`Okofunit->int|`No_expansion_point|`Already_expanded]}type'at=state->'a*statemoduleSyntax=structletreturnxs=(x,s)let(let*)atfabts=leta,s=atsinletbt=fabtainbtslet(let+)atfabs=leta,s=atsin(faba,s)endletconsume_spacevs=letconsumed_static=(matchvwithSta_dyn.Staticx->x|Dynamic_->0)+s.consumed_staticin((),{swithconsumed_static;consumed=Sta_dyn.lift(+)vs.consumed})letmeasure_consumedats=leta,s'=atsinletwidth=Sta_dyn.lift(-)s'.consumeds.consumedin((a,width),s')letexpands=matchs.expandwith|`No_expansion_point->invalid_arg"Encountered an expanding element that is not contained in a box"|`Already_expanded->invalid_arg"Multiple expansion points encountered. Cannot pack two unsized \
segments in a single box."|`Okf->(f,{swithexpand=`Already_expanded;consumed=Sta_dyn.lift(-)s.consumed(Dynamicf)})letrunat=letinitial_state={consumed=Static0;consumed_static=0;expand=`No_expansion_point}infst(atinitial_state)letwith_expansion_pointouter_widthats=letf=ref(fun()->assertfalse)inletexpand()=!f()inletx,s_inner=at{consumed=Static0;consumed_static=0;expand=`Okexpand}inlettyp=matchs_inner.expandwith|`Ok_->`not_used|`No_expansion_point->assertfalse|`Already_expanded->(f:=fun()->Sta_dyn.getouter_width-s_inner.consumed_static);`usedin((x,typ,s_inner.consumed),s)endletcompiletop=letrecinner:typea.at->aCompiled.tCompiler_state.t=letopenCompiler_state.Syntaxinfunction|Noop->returnCompiled.Noop|Stageds->inner(s())|Theta{pp;width}->letppppfevent=ppppfevent;widthinlet+()=Compiler_state.consume_space(Staticwidth)inCompiled.Theta{pp}|Alpha_unsized{pp;initial}->let+width=Compiler_state.expandinletppppfx=pp~widthppfxinletlatestbuf=matchinitialwith|`Valv->ppbuf`rerenderv|`Thetaf->f~widthbufinCompiled.Alpha{pp;latest}|Alpha{pp;width;initial}->letppabc=ppabc;widthinlet+()=Compiler_state.consume_space(Staticwidth)inletlatestbuf=matchinitialwith|`Valv->ppbuf`rerenderv|`Thetaf->fbuf;widthinCompiled.Alpha{pp;latest}|Contramap(t,f)->let+inner=innertinCompiled.Contramap(inner,f)|On_finaliset->let+inner=innert.innerinCompiled.On_finalise{final=t.final;inner}|Cond{if_;then_}->let+then_,width=Compiler_state.measure_consumed(innerthen_)inCompiled.Cond{if_;then_;width;latest=None;latest_span=Line_buffer.Span.empty}|Box{contents;width;pad}->(let*contents,point,inner_width=Compiler_state.with_expansion_pointwidth(innercontents)inmatch(point,pad)with|`used,`none(* The padding {i should} never happen. TODO: be more defensive *)|`used,(`left|`right)|`not_used,`none->let+()=Compiler_state.consume_spaceinner_widthincontents|`not_used,((`left|`right)asdir)->letdir=matchdirwith|`right->`right|`left->(* Here we access a dynamic value before (strictly) starting
the process, but it's OK since it's just an estimation. *)`left(Line_buffer.create~size:(min64(Sta_dyn.getwidth)))inlet+()=Compiler_state.consume_spacewidthinCompiled.Pad{contents;dir;width})|Groupg->let+g=ArrayLabels.fold_leftg~init:(return[])~f:(funaccelt->let*acc=accinlet+elt=innereltinelt::acc)inletg=List.revg|>Array.of_listinletrecaux:typea.aCompiled.tarray->aCompiled.tlistlist=fung->ArrayLabels.fold_leftg~init:[]~f:(funacc->function|Compiled.Groupg->letacc'=auxginacc'@acc|a->letacc=accin[a]::acc)inletinners=auxginletinners=inners|>List.rev|>List.concat|>Array.of_listinCompiled.Groupinners|Pair{left;sep;right}->let*left=innerleftinlet*sep=innersepinlet+right=innerrightinCompiled.Pair{left;sep;right}inCompiler_state.run(innertop)letapply_paddingdirwidth=matchdirwith|`right->Staged.inj(funinnerbuf->letinner_width=innerbufinletouter_width=Sta_dyn.getwidthinfor_=inner_width+1toouter_widthdoLine_buffer.add_charbuf' 'done;outer_width)|`leftintermediate_buf->Staged.inj(funinnerbuf->letinner_width=innerintermediate_bufinletouter_width=Sta_dyn.getwidthinfor_=inner_width+1toouter_widthdoLine_buffer.add_charbuf' 'done;Line_buffer.add_line_buffer~src:intermediate_buf~dst:buf;Line_buffer.resetintermediate_buf;outer_width)letreportcompiled=letrecaux:typea.[`report|`finish]->aCompiled.t->(Line_buffer.t->a->int)Staged.t=funtyp->function|Noop->Staged.inj(fun__->0)|Theta{pp}->Staged.inj(funbuf(_:a)->ppbuf`report)|Alphapp->Staged.inj(funbufx->pp.latest<-(funbuf->pp.ppbuf`rerenderx);pp.ppbuf(typ:>event)x)|Contramap(t,f)->let$inner=auxtyptinfunbufa->innerbuf(fa)|On_finalise{inner;_}->auxtypinner|Condtas_elt->let$then_=auxtypt.then_infunbufx->t.latest<-Somex;ift.if_xthen(letstart=Line_buffer.current_positionbufinlet_reported_width=then_bufxinletfinish=Line_buffer.current_positionbufint.latest_span<-Line_buffer.Span.between_marksstartfinish;letwidth=Sta_dyn.gett.widthin(* TODO: Since dynamic widths aren't memoized over a single run,
it's possible for this to fail due to changing width in the
middle of a render, which isn't a bug in user code. Should fix
the race condition and then be more defensive here. *)(* if reported_width <> width then
* Fmt.failwith
* "Conditional segment not respecting stated width: expected %a, \
* reported %d. Segment:@,\
* %a"
* (Sta_dyn.pp Fmt.int) t.width reported_width Compiled.pp_dump elt; *)width)else(Line_buffer.skipbuft.latest_span;Sta_dyn.gett.width)|Groupg->letreporters=Array.mapg~f:(auxtyp>>Staged.prj)inStaged.inj(funbufv->ArrayLabels.fold_leftreporters~f:(funaf->a+fbufv)~init:0)|Pair{left;sep;right}->let$left=auxtypleftand$sep=auxtypsepand$right=auxtyprightinfunbuf(v_left,v_right)->letx=leftbufv_leftinlety=sepbuf()inletz=rightbufv_rightinx+y+z|Pad{contents;dir;width}->let$contents=auxtypcontentsand$pad=apply_paddingdirwidthinfunbufx->pad(funbuf->contentsbufx)bufinauxcompiledletupdatetop=letrecaux:typea.aCompiled.t->(bool->[`rerender|`tick|`finish]->Line_buffer.t->int)Staged.t=function|Noop->Staged.inj(fun___->0)|Theta{pp}->Staged.inj(fun_eventbuf->ppbuf(event:>event))|Alphapp->Staged.inj(fun__buf->pp.latestbuf)|Pad{contents;dir;width}->let$contents=auxcontentsand$pad=apply_paddingdirwidthinfunyeventbuf->pad(funbuf->contentsyeventbuf)buf|Condt->(let$then_=auxt.then_inletupdate_withbufunconditionalevent=letstart=Line_buffer.current_positionbufinlet_reported_width=then_unconditionaleventbufinletfinish=Line_buffer.current_positionbufint.latest_span<-Line_buffer.Span.between_marksstartfinish;(* if actual_width <> t.width then
* Fmt.failwith
* "Conditional segment not respecting stated width: expected %d, \
* found %d. Segment:@,\
* %a"
* t.width actual_width Compiled.pp_dump elt; *)Sta_dyn.gett.widthinfununconditionaleventbuf->match(unconditional,t.latest)with|true,Some_->update_withbufunconditionalevent|false,Somevwhent.if_v->update_withbufunconditionalevent|true,None->update_withbufunconditionalevent(* let start = Line_buffer.current_position buf in
* let width = Sta_dyn.get t.width in
* Line_buffer.add_string buf (String.make width ' ');
* let finish = Line_buffer.current_position buf in
* t.latest_span <- Line_buffer.Span.between_marks start finish;
* width *)|false,_->Line_buffer.skipbuft.latest_span;Sta_dyn.gett.width)|Contramap(inner,_)->auxinner|On_finalise{final;inner}->let$inner_report=report`finishinnerand$inner=auxinnerinfununcondeventppf->ifPoly.(event=`finish)theninner_reportppffinalelseinneruncondeventppf|Groupg->letupdaters=Array.mapg~f:(aux>>Staged.prj)inStaged.inj(fununcondeventppf->Array.fold_leftupdaters~init:0~f:(funaf->a+funcondeventppf))|Pair{left;sep;right}->let$left=auxleftand$sep=auxsepand$right=auxrightinfununcondeventppf->letx=leftuncondeventppfinlety=sepuncondeventppfinletz=rightuncondeventppfinx+y+zinlet$f=auxtopinfun~unconditionaleventbuf:int->funconditionaleventbufletfinaliset=Staged.map(updatet)~f:(funf->f~unconditional:true`finish)lettickt=Staged.map(updatet)~f:(funf->f~unconditional:true`tick)letupdatet=Staged.map(updatet)~f:(funf~unconditionalbuf->f~unconditional`rerenderbuf)letreportt=report`reportt(*————————————————————————————————————————————————————————————————————————————
Copyright (c) 2020–2021 Craig Ferguson <me@craigfe.io>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
————————————————————————————————————————————————————————————————————————————*)