123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551(*————————————————————————————————————————————————————————————————————————————
Copyright (c) 2020–2021 Craig Ferguson <me@craigfe.io>
Distributed under the MIT license. See terms at the end of this file.
————————————————————————————————————————————————————————————————————————————*)(** The core of the progress bar rendering logic. Consumes the {!Line} DSL and
emits rendering functions that put {!Ansi} escape codes in the right places. *)includeRenderer_intfopen!ImportmoduleBar_id=Unique_id()(* TODO: this module should probably be inlined with the
[Line_primitives.Compiled.t] types: since those values only ever correspond to
exactly one [Bar_renderer], and both are responsible for state management. *)moduleBar_renderer:sigtype'attypecontents={width:int;data:[`Cleanofstring|`Dirtyofstring]}valcreate:'aLine_primitives.t->'atvalupdate:unconditional:bool->_t->unit->contentsvalreport:'at->'a->contentsvaltick:_t->contentsvalfinalise:_t->contentsvalid:_t->Bar_id.tend=structtype'at={line_buffer:Line_buffer.t;update:unconditional:bool->int;report:'a->int;finalise:unit->int;tick:unit->int;id:Bar_id.t;mutablefinalised:bool}typecontents={width:int;data:[`Cleanofstring|`Dirtyofstring]}(* TODO: get rid of all this boilerplate *)letcreate:typea.aLine_primitives.t->at=funs->lets=Line_primitives.compilesinletline_buffer=Line_buffer.create~size:80inletreport=letreport=Staged.prj(Line_primitives.reports)infun(a:a)->reportline_bufferainletupdate=letupdate=Staged.prj(Line_primitives.updates)infun~unconditional->updateline_buffer~unconditionalinletfinalise=letfinalise=Staged.prj(Line_primitives.finalises)infun()->finaliseline_bufferinlettick=lettick=Staged.prj(Line_primitives.ticks)infun()->tickline_bufferinletid=Bar_id.create()in{line_buffer;report;update;finalise;tick;id;finalised=false}letfinaliset=letwidth=t.finalise()inletdata=Line_buffer.contentst.line_bufferint.finalised<-true;{width;data}lettickt=ift.finalisedthenfinalisetelseletwidth=t.tick()inletdata=Line_buffer.contentst.line_bufferin{width;data}letupdate~unconditionalt()=(* We continue to render even once the bar has been finalised in order to
account for terminal width changes. *)ift.finalisedthenfinalisetelseletwidth=t.update~unconditionalinletdata=Line_buffer.contentst.line_bufferin{width;data}letreporttx=ift.finalisedthenfinalisetelseletwidth=t.reportxinletdata=Line_buffer.contentst.line_bufferin{width;data}letidt=t.idendmoduleBar_list=structincludeMulti.Hlist(Bar_renderer)endmoduleDisplay:sigtypetmoduleUnique_id:Eqvalcreate:config:Config.t->(_,_)Bar_list.t->t(** Initialise a display with a given set of bar renderers. *)(* Immutable state *)valuid:t->Unique_id.tvalconfig:t->Config.t(* Lifecycle management *)valinitial_render:t->unitvaltick:t->unitvalhandle_width_change:t->int->unitvalinterject_with:t->(unit->'a)->'avalcleanup:t->unitvalfinalise:t->unit(* Bar-specific functions *)valadd_line:?above:int->t->_Bar_renderer.t->unitvalrerender_line:t->Bar_id.t->Bar_renderer.contents->unitvalfinalise_line:t->Bar_id.t->unitend=structmoduleUnique_id=Unique_id()typesome_bar=|E:{renderer:_Bar_renderer.t;mutablelatest_width:int;mutableposition:int}->some_bartypet={config:Config.t;uid:Unique_id.t;bars:(Bar_id.t,some_bar)Hashtbl.t;rows:some_baroptionVector.t}letconfigt=t.configletcreate~configbars=letuid=Unique_id.create()inletrows=letfi=function|None->None|Somerenderer->Some(E{renderer;latest_width=0;position=i})inBar_list.mapibars~f:{f}|>Vector.of_list~dummy:Noneinletbar_count=Bar_list.lengthbarsinletbars=Hashtbl.createbar_countinVector.iterrows~f:(Option.iter(fun(E{renderer;_}ast)->Hashtbl.addbars~key:(Bar_renderer.idrenderer)~data:t));{config;uid;bars;rows}letuid{uid;_}=uid(* Terminals generally don't wrap blank suffixes of lines (e.g. [" \n"] is
equivalent to ["\n"]), so we should account for this when estimating the
width at which a given line will be wrapped. *)letget_blank_suffix_length=letrecauxstr=function|-1->-1|i->(matchstr.[i]with' '->auxstr(i-1)|_->i)infunstr->letlast_index=String.lengthstr-1inlast_index-auxstrlast_indexletrerender_line_and_advance{config={ppf;_};_}(Ebar)new_widthdata=letold_width=bar.latest_widthinbar.latest_width<-new_width-get_blank_suffix_lengthdata;Format.pp_print_stringppfdata;ifnew_width<old_widththenFormat.pp_print_stringppfTerminal.Ansi.erase_lineletrerender_all_from_top~stage~starting_at~unconditional({config={ppf;_};rows;_}ast)=lettotal_rows=Vector.lengthrowsinVector.iteri_fromstarting_atrows~f:(funidxslot->letis_last=idx=total_rows-1inlet()=matchslotwith|None->Format.fprintfppf"%s"Terminal.Ansi.erase_line|Some(Ebar)->(let({width;data}:Bar_renderer.contents)=matchstagewith|`update->Bar_renderer.update~unconditionalbar.renderer()|`tick->Bar_renderer.tickbar.renderer|`finalise->Bar_renderer.finalisebar.rendererinmatchdatawith|`Clean_whennotunconditional->()|`Cleancontents|`Dirtycontents->rerender_line_and_advancet(Ebar)widthcontents)inmatchis_lastwith|false->Format.pp_force_newlineppf()|true->Format.fprintfppf"\r%!")letinitial_render=rerender_all_from_top~stage:`update~starting_at:0~unconditional:trueletget_bar_exn~msgbarsuid=letexceptionFinalisedofstringinmatchHashtbl.findbarsuidwith|x->x|exceptionNot_found->raise(Finalisedmsg)letrerender_line({config={ppf;_};bars;rows;_}ast)uid({width;data}:Bar_renderer.contents)=let(Ebar)=get_bar_exn~msg:"Can't render to finalised bar"barsuidinmatchdatawith|`Clean_->()|`Dirtydata->letdistance_from_base=Vector.lengthrows-bar.position-1in(* NOTE: we add an initial carriage return to avoid overflowing the line if
the user has typed into the terminal between renders. *)Format.fprintfppf"\r%a"Terminal.Ansi.move_updistance_from_base;rerender_line_and_advancet(Ebar)widthdata;Format.fprintfppf"%a\r%!"Terminal.Ansi.move_downdistance_from_baseletfinalise_linetuid=let(Ebar)=get_bar_exn~msg:"Bar already finalised"t.barsuidinletcontents=Bar_renderer.finalisebar.rendererinrerender_linetuidcontents;Hashtbl.removet.barsuidletadd_line?(above=0)trenderer=letposition=Vector.lengtht.rows-aboveinletkey=Bar_renderer.idrendererinletbar=E{renderer;latest_width=0;position}inHashtbl.addt.bars~key~data:bar;Vector.insertt.rowsposition(Somebar);Vector.iteri_from(position+1)t.rows~f:(funi->function|None->()|Some(Ebar)->bar.position<-i);(* The cursor is now one line above the bottom. Move to the correct starting
position for a re-render of the affected suffix of the display. *)Format.pp_force_newlinet.config.ppf();Terminal.Ansi.move_upt.config.ppfabove;rerender_all_from_top~stage:`update~starting_at:position~unconditional:truetletceil_divxy=(x+y-1)/yletoverflow_rows~old_width~new_width=max0(ceil_divold_widthnew_width-1)lethandle_width_change({config={ppf;_};rows;_}asdisplay)new_width=letrow_count=Vector.lengthrowsinletlatest_widths=Array.initrow_count~f:(funi->Vector.get_exnrowsi|>Option.fold~none:0~some:(fun(Et)->t.latest_width))inletoverflows=Array.fold_leftlatest_widths~init:0~f:(funaold_width->a+overflow_rows~old_width~new_width)inletbottom_overflow=overflow_rows~old_width:latest_widths.(row_count-1)~new_widthinletmove_up=overflows+row_count-1-bottom_overflowinTerminal.Ansi.move_upppfmove_up;ifoverflows>0thenFormat.pp_print_stringppfTerminal.Ansi.erase_display_suffix;rerender_all_from_top~stage:`update~starting_at:0~unconditional:truedisplaylettick({config={ppf;_};rows;_}ast)=Terminal.Ansi.move_upppf(Vector.lengthrows-1);rerender_all_from_top~stage:`tick~starting_at:0~unconditional:falsetletinterject_with({config={ppf;_};rows;_}ast)f=Format.fprintfppf"%a%s%!"Terminal.Ansi.move_up(Vector.lengthrows-1)Terminal.Ansi.erase_line;Fun.protectf~finally:(fun()->rerender_all_from_top~stage:`update~starting_at:0~unconditional:truet)letcleanup{config;_}=ifconfig.hide_cursorthenFormat.fprintfconfig.ppf"\n%s%!"Terminal.Ansi.show_cursorletfinalise({config={ppf;hide_cursor;persistent;_};rows;_}asdisplay)=Terminal.Ansi.move_upppf(Vector.lengthrows-1);ifpersistentthen(rerender_all_from_top~stage:`finalise~starting_at:0~unconditional:truedisplay;Format.fprintfppf"@,@]")elseFormat.pp_print_stringppfTerminal.Ansi.erase_display_suffix;Format.fprintfppf"%s%!"(ifhide_cursorthenTerminal.Ansi.show_cursorelse"")endmoduleReporter=structtype'at={uid:Bar_id.t;display:Display.Unique_id.t;update:unconditional:bool->unit;report:'a->unit}letreportt=t.reporttype(_,_)list=|[]:('a,'a)list|(::):'a*('b,'c)list->('a->'b,'c)listendmoduleMake(Platform:Platform.S)=structmoduleConfig=ConfigmoduleLine=Line.Make(Platform)moduleGlobal:sigvalactive_display:unit->Display.toptionvalfind_display:Display.Unique_id.t->(Display.t,[`finalised])resultvalset_active_exn:Display.t->unitvalset_inactive:unit->unitend=structtyperuntime={(* Race conditions over these fields are not handled, but protection against
concurrent usage is best-effort anyway. *)mutableactive_display:Display.toption;displaced_handlers:(int,Sys.signal_behavior)Hashtbl.t}letruntime={active_display=None;displaced_handlers=Hashtbl.create0}letis_active()=Option.is_someruntime.active_displayletactive_display()=runtime.active_displayletcleanup()=Option.iterDisplay.cleanupruntime.active_displayletfind_displayuid=matchactive_display()with|None->Error`finalised|Somedisplay->ifnot(Display.Unique_id.equal(Display.uiddisplay)uid)thenError`finalisedelseOkdisplaylethandle_width_changew=match(w,runtime.active_display)with|None,_|_,None->()|Somew,Somedisplay->Display.handle_width_changedisplaywlethandle_signalcode=cleanup();matchHashtbl.find_optruntime.displaced_handlerscodewith|Some(Signal_handlef)->fcode|SomeSignal_default->exit100|SomeSignal_ignore|None->()letinit_handlers=lazy(at_exitcleanup;Platform.Terminal_width.set_changed_callbackhandle_width_change)letsignals=letopenSysin[sigint;sigterm;sigsegv]@ifwin32then[]else[sigquit]letset_active_exndisplay=ifis_active()thenfailwith"Can't run more than one progress bar renderer simultaneously";Lazy.forceinit_handlers;ListLabels.itersignals~f:(funcode->letprev_handler=Sys.signalcode(Signal_handlehandle_signal)in(* Until the previous signal is added to the hashtable, there's a short
period of time in which the {i previous} signal handler might be
ignored. Not much we can do about that, unfortunately. *)Hashtbl.addruntime.displaced_handlers~key:code~data:prev_handler);runtime.active_display<-Somedisplayletset_inactive()=Hashtbl.iterruntime.displaced_handlers~f:(fun~key~data->Sys.set_signalkeydata);Hashtbl.clearruntime.displaced_handlers;runtime.active_display<-Noneendletreporter_of_bar(typea)display(bar:aBar_renderer.t):areporter=funx->Bar_renderer.reportbarx|>Display.rerender_linedisplay(Bar_renderer.idbar)letupdater_of_bardisplaybar~unconditional=Bar_renderer.update~unconditionalbar()|>Display.rerender_linedisplay(Bar_renderer.idbar)moduleBar_list=structincludeBar_listletrecof_multi:typeab.Config.t->(a,b)Multi.t->(a,b)Bar_list.t=leteltcfgx=Bar_renderer.create(Line.to_primitivecfgx)infuncfg->function|Zero->Zero|Onex->One(eltcfgx)|Manyxs->Many(List.mapxs~f:(funx->eltcfgx))|Plus(xs,ys)->Plus(of_multicfgxs,of_multicfgys)endmoduleHlist=struct(* ['a] and ['b] correspond to parameters of [Bar_list.t]. *)type(_,_)t=|[]:('a,'a)t|(::):'a*('b,'c)t->('a->'b,'c)tletrecappend:typeabc.(a,b)t->(b,c)t->(a,c)t=funxsys->matchxswith[]->ys|x::xs->x::appendxsysendletinterject_with:'a.(unit->'a)->'a=funf->matchGlobal.active_display()with|None->f()|Somed->Display.interject_withdfletinstrument_logs_reporter:Logs.reporter->Logs.reporter=letwrap_msgf:'a'b.('a,'b)Logs.msgf->('a,'b)Logs.msgf=funmsgfconstruction->interject_with(fun()->msgfconstruction)infunr->{report=(funsrclevel~overkf->r.reportsrclevel~overk(wrap_msgff))}letlogs_reporter?pp_header?app?dst()=instrument_logs_reporter(Logs_fmt.reporter?pp_header?app?dst())moduleReporter=structincludeReporterletfinalise(t:_t)=matchGlobal.find_displayt.displaywith|Error`finalised->failwith"Display already finalised"|Okdisplay->Display.finalise_linedisplayt.uidendmoduleReporters=structtypenonrec('a,'b)t=('a,'b)Reporter.listletrecapply_all:typeab.a->(a,b)t->b=funf->function[]->f|x::xs->apply_all(fx)xsletrecof_hlist:typeab.(a,b)Hlist.t->(a,b)t=function|[]->[]|x::xs->x::of_hlistxsendmoduleDisplay=structtype('a,'b)t={uid:Display.Unique_id.t;initial_reporters:('a,'b)Reporters.t}letstart:typeab.?config:Config.user_supplied->(a,b)Multi.t->(a,b)t=fun?(config=Config.v())bars->letconfig=Config.apply_defaultsconfiginletbars=Bar_list.of_multiconfigbarsinletppf=config.ppfinletdisplay=Display.create~configbarsinGlobal.set_active_exndisplay;Format.pp_open_boxppf0;ifconfig.hide_cursorthenFormat.pp_print_stringppfTerminal.Ansi.hide_cursor;Display.initial_renderdisplay;letrecinner:typeab.(a,b)Bar_list.t->(a,b)Hlist.t=function|Zero->[]|Onebar->[reporter_of_bardisplaybar]|Manyxs->[ListLabels.mapxs~f:(funbar->reporter_of_bardisplaybar)]|Plus(left,right)->letleft=innerleftinletright=innerrightinHlist.appendleftrightinletbars=innerbarsin{uid=Display.uiddisplay;initial_reporters=Reporters.of_hlistbars}letadd_line?abovetline:_Reporter.t=matchGlobal.find_displayt.uidwith|Error`finalised->failwith"Cannot add a line to a finalised display"|Okd->letbar=Bar_renderer.create(Line.to_primitive(Display.configd)line)inlet()=Display.add_line?abovedbarinletuid=Bar_renderer.idbarinletreport=reporter_of_bardbarinletupdate=updater_of_bardbarin{display=Display.uidd;uid;report;update}letfinaliset=matchGlobal.find_displayt.uidwith|Error`finalised->failwith"Display already finalised"|Okdisplay->Display.finalisedisplay;Global.set_inactive()lettickt=matchGlobal.find_displayt.uidwith|Error`finalised->()|Okd->Display.tickdletreporterst=t.initial_reportersendletwith_reporters?configtf=letdisplay=Display.start?configtinFun.protect(fun()->Reporters.apply_allf(Display.reportersdisplay))~finally:(fun()->Display.finalisedisplay)letwith_reporter?configbf=with_reporters?config(Multi.lineb)fend(*————————————————————————————————————————————————————————————————————————————
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.
————————————————————————————————————————————————————————————————————————————*)