123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683(*————————————————————————————————————————————————————————————————————————————
Copyright (c) 2020–2021 Craig Ferguson <me@craigfe.io>
Distributed under the MIT license. See terms at the end of this file.
————————————————————————————————————————————————————————————————————————————*)includeLine_intfmodulePrimitives=Line_primitivesopen!Import(** [Line] is a higher-level wrapper around [Segment] that makes some
simplifying assumptions about progress bar rendering:
- the reported value has a monoid instance, used for initialisation and
accumulation.
- the line is wrapped inside a single box.
It contains its own notion of "accumulated" line segments, and ensures that
this works with conditional rendering of segments (e.g. when rendering with
a minimum interval). *)moduleAcc=structtype'at={mutablelatest:'a;mutableaccumulator:'a;mutablepending:'a;render_start:Mtime.t;flow_meter:'aFlow_meter.t}letwrap:typea.elt:(moduleInteger.Swithtypet=a)->clock:(unit->Mtime.t)->should_update:(unit->bool)->atPrimitives.t->aPrimitives.t=fun~elt:(moduleInteger)~clock~should_updateinner->Primitives.stateful(fun()->letflow_meter=Flow_meter.create~clock~size:32~elt:(moduleInteger)inletrender_start=clock()inletstate={latest=Integer.zero;accumulator=Integer.zero;pending=Integer.zero;render_start;flow_meter}inPrimitives.contramap~f:(funa->Flow_meter.recordstate.flow_metera;state.pending<-Integer.addastate.pending)@@Primitives.conditional(fun_->should_update())(* On finalisation, we must flush the [pending] accumulator to get the
true final value. *)@@Primitives.on_finalise()@@Primitives.contramap~f:(fun()->letto_record=state.pendinginstate.accumulator<-Integer.addto_recordstate.accumulator;state.latest<-to_record;state.pending<-Integer.zero;state)@@inner)letaccumulatort=t.accumulatorletflow_metert=t.flow_meterendmoduleTimer=structtype'at={mutablerender_latest:Mtime.t}letshould_update~interval~clockt=matchintervalwith|None->Staged.inj(fun()->true)|Someinterval->Staged.inj(fun()->letnow=clock()inmatchMtime.Span.compare(Mtime.spant.render_latestnow)interval>=0with|false->false|true->t.render_latest<-now;true)endtype'at=|Noop|Primitiveof'aPrimitives.t|Basicof'aPrimitives.t|Mapof('aPrimitives.t->'aPrimitives.t)*'at|Listof'atlist|Contramap:('bt*('a->'b))->'at|Pair:'at*unitt*'bt->('a*'b)t|Accof{segment:'aAcc.tPrimitives.t;elt:(moduleInteger.Swithtypet='a)}moduleInteger_independent(Platform:Platform.S)=structopenstructmoduleClock=Platform.Clockendletnoop()=Noopletconsts=letlen=String.lengthsandwidth=Terminal.guess_printed_widthsinletsegment=Primitives.theta~width(funbuf_->Line_buffer.add_substringbufs~off:0~len)inBasicsegmentletspacern=const(String.maken' ')(* Like [Format.str_formatter], but with [Fmt] set to use [`Ansi_tty] style rendering. *)letstr_formatter_buf,str_formatter=letbuf=Buffer.create0inletppf=Format.formatter_of_bufferbufinFmt.set_style_rendererppf`Ansi_tty;(buf,ppf)letconstffmt=Fmt.kpf(funppf->Format.pp_print_flushppf();letstr=Buffer.contentsstr_formatter_bufinBuffer.clearstr_formatter_buf;conststr)str_formatterfmtletpair?(sep=noop())ab=Pair(a,sep,b)letlist?(sep=const" ")xs=letxs=ListLabels.filter_mapxs~f:(functionNoop->None|x->Somex)inList(List.intersperse~sepxs)let(++)ab=List[a;b]letparenst=const"("++t++const")"letbracketst=const"["++t++const"]"letbracest=const"{"++t++const"}"letusingfx=Contramap(x,f)letstring=letsegment=Primitives.alpha_unsized~initial:(`Val"")(fun~widthbuf_s->letoutput_len=width()-1(* XXX: why is -1 necessary? *)inifoutput_len<=0then0elseletpp=Staged.prj@@Printer.Internals.to_line_printer(Printer.string~width:output_len)inppbufs;output_len)inBasicsegmentletlpadszt=Map(Primitives.box_fixed~pad:`leftsz,t)letrpadszt=Map(Primitives.box_fixed~pad:`rightsz,t)(* Spinners *)letwith_color_optcolorbuff=matchcolorwith|None->f()|Somes->Line_buffer.add_stringbufTerminal.Style.(code(fgs));leta=f()inLine_buffer.add_stringbufTerminal.Style.(codenone);amoduleModulo_counter:sigtypetvalcreate:int->tvallatest:t->intvaltick:t->intend=structtypet={modulus:int;mutablelatest:int}letcreatemodulus={modulus;latest=0}letlatestt=t.latestlettickt=t.latest<-succt.latestmodt.modulus;t.latestendletdebounceintervals=Primitives.stateful(fun()->letlatest=ref(Clock.now())inletshould_update()=letnow=Clock.now()inmatchMtime.Span.compare(Mtime.span!latestnow)interval>=0with|false->false|true->latest:=now;trueinPrimitives.conditional(fun_->should_update())s)moduleSpinner=structtypet={frames:stringarray;final_frame:stringoption;width:int}letv~frames~final_frame~width={frames;final_frame;width}letdefault=v~final_frame:(Some"✔️")~width:1~frames:[|"⠋";"⠙";"⠹";"⠸";"⠼";"⠴";"⠦";"⠧";"⠇";"⠏"|]letstage_countt=Array.lengtht.framesendletspinner?frames?color?(min_interval=Some(Duration.of_int_ms80))()=letspinner=matchframeswith|None->Spinner.default|Some[]->Fmt.invalid_arg"spinner must have at least one stage"|Some(x::xsasframes)->letwidth=Terminal.guess_printed_widthxinListLabels.iterixs~f:(funix->letwidth'=Terminal.guess_printed_widthxinifwidth<>width'thenFmt.invalid_arg"Spinner frames must have the same UTF-8 length. found %d \
(at index 0) and %d (at index %d)"(i+1)widthwidth');Spinner.v~frames:(Array.of_listframes)~final_frame:None~widthinletapply_debounce=Option.foldmin_interval~none:Fun.id~some:debounceinBasic(Primitives.stateful(fun()->letcounter=Modulo_counter.create(Spinner.stage_countspinner)inapply_debounce@@Primitives.theta~width:spinner.Spinner.width(funbuf->function|`finish->letfinal_frame=matchspinner.final_framewith|None->spinner.frames.(Modulo_counter.tickcounter)|Somex->xinwith_color_optcolorbuf(fun()->Line_buffer.add_stringbuffinal_frame)|(`report|`tick|`rerender)ase->lettick=matchewith|`report|`tick->Modulo_counter.tickcounter|`rerender->Modulo_counter.latestcounterinletframe=spinner.Spinner.frames.(tick)inwith_color_optcolorbuf(fun()->Line_buffer.add_stringbufframe))))endmoduleBar_style=structtypet={delimiters:(string*string)option;blank_space:string;full_space:string;in_progress_stages:stringarray;color:Terminal.Color.toption;color_empty:Terminal.Color.toption;total_delimiter_width:int;segment_width:int}letascii={delimiters=Some("[","]");blank_space="-";full_space="#";in_progress_stages=[||];color=None;color_empty=None;total_delimiter_width=2;segment_width=1}letutf8={delimiters=Some("│","│");blank_space=" ";full_space="█";in_progress_stages=[|" ";"▏";"▎";"▍";"▌";"▋";"▊";"▉"|];color=None;color_empty=None;total_delimiter_width=2;segment_width=1}letparse_stagesctx=function|[]->Fmt.invalid_arg"%s: empty list of bar stages supplied"ctx|full_space::xs->letsegment_width=Terminal.guess_printed_widthfull_spaceinifsegment_width=0thenFmt.invalid_arg"%s: supplied stage '%s' has estimated printed width of 0"ctxfull_space;letin_progress_stages,blank_space=matchList.revxswith|[]->([||],String.makesegment_width' ')|blank_space::xs->(Array.of_listxs,blank_space)in(full_space,in_progress_stages,blank_space,segment_width)letguess_delims_width=function|None->0|Some(l,r)->Terminal.(guess_printed_widthl+guess_printed_widthr)letv?delims?color?color_emptystages=letfull_space,in_progress_stages,blank_space,segment_width=parse_stages"Bar_styles.v"stagesin{delimiters=delims;blank_space;full_space;in_progress_stages;color;color_empty;segment_width;total_delimiter_width=guess_delims_widthdelims}letwith_colorcolort={twithcolor=Somecolor}letwith_empty_colorcolor_emptyt={twithcolor_empty=Somecolor_empty}letwith_delimsdelimiterst={twithdelimiters;total_delimiter_width=guess_delims_widthdelimiters}letwith_stagesstagest=letfull_space,in_progress_stages,blank_space,segment_width=parse_stages"Bar_styles.with_stages"stagesin{twithfull_space;blank_space;in_progress_stages;segment_width}endmoduleMake(Platform:Platform.S)=structopenstructmoduleClock=Platform.ClockendmoduleInteger_independent=Integer_independent(Platform)includeInteger_independentmoduleInternals=structmoduleLine_buffer=Line_bufferincludePrimitivesletbox_winsize?max?(fallback=80)s=letget_width()=letreal_width=Option.value~default:fallback(Platform.Terminal_width.get())inmatchmaxwithNone->real_width|Somem->minmreal_widthinbox_dynamicget_widthsletto_linet=Primitivetendletto_primitive:typea.Config.t->at->aPrimitives.t=letrecinner:typea.at->(unit->bool)->aPrimitives.t=function|Noop->fun_->Primitives.noop()|Primitivex->fun_->x|Pair(a,sep,b)->leta=innerainletsep=innersepinletb=innerbinfunshould_update->Primitives.pair~sep:(sepshould_update)(ashould_update)(bshould_update)|Contramap(x,f)->letx=innerxinfuny->Primitives.contramap~f(xy)|Map(f,x)->funa->f(innerxa)|Listxs->letxs=List.mapxs~f:innerinfunshould_update->Primitives.array(List.mapxs~f:(funf->fshould_update)|>Array.of_list)|Basicsegment->funshould_update->Primitives.conditional(fun_->should_update())@@segment|Acc{segment;elt=(moduleInteger)}->funshould_update->Acc.wrap~elt:(moduleInteger)~clock:Clock.now~should_update@@segmentinfun(config:Config.t)->function|Primitivex->x|t->letinner=innertinletsegment=Primitives.stateful(fun()->letshould_update=letstate={Timer.render_latest=Clock.now()}inStaged.prj(Timer.should_update~clock:Clock.now~interval:config.min_intervalstate)inletx=reftrueinPrimitives.contramap~f:(funa->x:=should_update();a)@@Internals.box_winsize?max:config.max_width@@inner(fun()->!x))insegment(* Basic utilities for combining segments *)moduleInteger_dependent=structmoduletypeS=Integer_dependentwithtype'at:='atandtypecolor:=Terminal.Color.tandtypeduration:=Duration.tandtype'aprinter:='aPrinter.tandtypebar_style:=Bar_style.tmoduletypeExt=DSLwithtype'at:='atandtypecolor:=Terminal.Color.tandtypeduration:=Duration.tandtype'aprinter:='aPrinter.tandtypeBar_style.t:=Bar_style.tmoduleMake_ext(Integer:Integer.S)=structletaccsegment=Acc{segment;elt=(moduleInteger)}letof_printer?initprinter=letpp=Staged.prj@@Printer.Internals.to_line_printerprinterinletwidth=Printer.print_widthprinterinletinitial=matchinitwith|Somev->`Valv|None->`Theta(funbuf->for_=1towidthdoLine_buffer.add_charbuf' 'done)inBasic(Primitives.alpha~width~initial(funbuf_x->ppbufx))letcount_ppprinter=letpp=Staged.prj@@Printer.Internals.to_line_printerprinterinacc@@Primitives.contramap~f:Acc.accumulator@@Primitives.alpha~width:(Printer.print_widthprinter)~initial:(`ValInteger.zero)(funbuf_x->ppbufx)letbytes=count_pp(Units.Bytes.generic(moduleInteger))letpercentage_ofaccumulator=letprinter=Printer.usingUnits.Percentage.of_float~f:(funx->Integer.to_floatx/.Integer.to_floataccumulator)incount_ppprinterletsum?pp~width()=letpp=matchppwith|None->Printer.Internals.integer~width(moduleInteger)|Somex->xinletpp=Staged.prj(Printer.Internals.to_line_printerpp)inacc@@Primitives.contramap~f:Acc.accumulator@@Primitives.alpha~initial:(`ValInteger.zero)~width(funbuf_x->ppbufx)letcount_to?pp?(sep=const"/")total=lettotal=Integer.to_stringtotalinletwidth=matchppwith|Somepp->Printer.print_widthpp|None->String.lengthtotalinList[sum~width();using(fun_->())sep;consttotal]letticker_to?(sep=const"/")total=lettotal=Integer.to_stringtotalinletwidth=String.lengthtotalinletpp=Staged.prj@@Printer.Internals.to_line_printer@@Printer.Internals.integer~width(moduleInteger)inletsegment=Primitives.alpha~width~initial:(`ValInteger.zero)(funbuf_x->ppbufx)inList[Contramap(Acc{segment=Primitives.contramap~f:Acc.accumulatorsegment;elt=(moduleInteger)},fun_->Integer.one);using(fun_->())sep;consttotal](* Progress bars *)moduleBar_style=Bar_styleletbar(spec:Bar_style.t)widthproportionbuf=letfinal_stage=Array.lengthspec.in_progress_stagesinletwidth=width()inletbar_segments=(width-spec.total_delimiter_width)/spec.segment_widthinletsquaresf=Float.of_intbar_segments*.proportioninletsquares=Float.to_intsquaresfinletfilled=minsquaresbar_segmentsinletnot_filled=bar_segments-filled-iffinal_stage=0then0else1inOption.iter(fun(x,_)->Line_buffer.add_stringbufx)spec.delimiters;with_color_optspec.colorbuf(fun()->for_=1tofilleddoLine_buffer.add_stringbufspec.full_spacedone);let()=iffilled<>bar_segmentsthen(letchunks=Float.to_int(squaresf*.Float.of_intfinal_stage)inletindex=chunks-(filled*final_stage)inifindex>=0&&index<final_stagethenwith_color_optspec.colorbuf(fun()->Line_buffer.add_stringbufspec.in_progress_stages.(index));with_color_optspec.color_emptybuf(fun()->for_=1tonot_filleddoLine_buffer.add_stringbufspec.blank_spacedone))inOption.iter(fun(_,x)->Line_buffer.add_stringbufx)spec.delimiters;widthletwith_propfvt=matchvwithNone->t|Somev->fvtletbar~style~color=letstyle=matchstylewith|`ASCII->Bar_style.ascii|`UTF8->Bar_style.utf8|`Customstyle->styleinbar(style|>with_propBar_style.with_colorcolor)letbar?(style=`ASCII)?color?(width=`Expand)?(data=`Sum)total=letproportionx=Integer.to_floatx/.Integer.to_floattotalinletproportion_segment=matchwidthwith|`Fixedwidth->ifwidth<3thenfailwith"Not enough space for a progress bar";Primitives.alpha~width~initial:(`Val0.)(funbuf_x->ignore(bar~style~color(fun_->width)xbuf:int))|`Expand->Primitives.alpha_unsized~initial:(`Val0.)(fun~widthppf_x->bar~style~colorwidthxppf)inmatchdatawith|`Latest->Basic(Primitives.contramapproportion_segment~f:proportion)|`Sum->acc(Primitives.contramapproportion_segment~f:(Acc.accumulator>>proportion))letratepp_val=letpp_rate=letpp_val=Staged.prj(Printer.Internals.to_line_printerpp_val)infunbuf_x->pp_valbufx;Line_buffer.add_stringbuf"/s"inletwidth=Printer.print_widthpp_val+2inacc@@Primitives.contramap~f:(Acc.flow_meter>>Flow_meter.per_second>>Integer.to_float)@@Primitives.alpha~width~initial:(`Val0.)pp_rateletbytes_per_sec=rateUnits.Bytes.of_floatleteta?(pp=Units.Duration.mm_ss)total=letspan_segment=letprinter=letpp=Staged.prj(Printer.Internals.to_line_printerpp)infunppfeventx->matcheventwith|`finish->ppppfMtime.Span.max_span(* renders as [--:--] *)|`report|`rerender|`tick(* TODO: tick should cause the estimate to be re-evaluated. *)->ppppfxinletwidth=Printer.print_widthUnits.Duration.mm_ssinletinitial=`ValMtime.Span.max_spaninPrimitives.alpha~width~initialprinterinacc@@Primitives.contramap~f:(funacc->letper_second=Flow_meter.per_second(Acc.flow_meteracc)inletacc=Acc.accumulatoraccinifInteger.(equalzero)per_secondthenMtime.Span.max_spanelselettodo=Integer.(to_float(subtotalacc))inifFloat.(todo<=0.)thenMtime.Span.zeroelseMtime.Span.of_uint64_ns(Int64.of_float(todo/.Integer.to_floatper_second*.1_000_000_000.)))@@span_segmentletelapsed?(pp=Units.Duration.mm_ss)()=letprint_time=Staged.prj(Printer.Internals.to_line_printerpp)inletsegment=Primitives.stateful(fun()->letelapsed=Clock.counter()inletlatest=refMtime.Span.zeroinletfinished=reffalseinletppbufe=(matchewith|`tick|`report->latest:=Clock.countelapsed|`finishwhennot!finished->latest:=Clock.countelapsed;finished:=true|`rerender|`finish->());print_timebuf!latestinPrimitives.theta~width:5pp)inBasicsegmentincludeInteger_independentendmoduleMake=Make_extendincludeInteger_dependent.Make(Integer.Int)moduleUsing_int32=Integer_dependent.Make_ext(Integer.Int32)moduleUsing_int63=Integer_dependent.Make_ext(Integer.Int63)moduleUsing_int64=Integer_dependent.Make_ext(Integer.Int64)moduleUsing_float=Integer_dependent.Make_ext(Integer.Float)end(*————————————————————————————————————————————————————————————————————————————
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.
————————————————————————————————————————————————————————————————————————————*)