├── .gitignore ├── .meta └── example.gif ├── .ocamlformat ├── CHANGES.md ├── LICENSE ├── Makefile ├── README.md ├── docs ├── fonts │ ├── JetBrainsMono-Regular.ttf │ ├── JetBrainsMono-Regular.woff │ ├── JetBrainsMono-Regular.woff2 │ ├── lato-v17-latin-700.woff2 │ ├── lato-v17-latin-italic.woff2 │ └── lato-v17-latin-regular.woff2 └── odoc.css ├── dune ├── dune-project ├── examples ├── bar_styles.ml ├── bar_styles.mli ├── cargo.ml ├── cargo.mli ├── download.ml ├── download.mli ├── dune ├── interject.ml ├── interject.mli ├── main.ml ├── main.mli ├── readme.ml ├── readme.mli ├── spinners.ml ├── spinners.mli ├── utils.ml ├── utils.mli ├── yarn.ml └── yarn.mli ├── progress.opam ├── src ├── progress │ ├── dune │ ├── engine │ │ ├── config.ml │ │ ├── dune │ │ ├── duration.ml │ │ ├── duration.mli │ │ ├── flow_meter.ml │ │ ├── flow_meter.mli │ │ ├── import.ml │ │ ├── integer.ml │ │ ├── line.ml │ │ ├── line.mli │ │ ├── line_buffer.ml │ │ ├── line_buffer.mli │ │ ├── line_intf.ml │ │ ├── line_primitives.ml │ │ ├── line_primitives.mli │ │ ├── line_primitives_intf.ml │ │ ├── multi.ml │ │ ├── multi.mli │ │ ├── multi_intf.ml │ │ ├── platform.ml │ │ ├── printer.ml │ │ ├── printer.mli │ │ ├── progress_engine.ml │ │ ├── progress_engine.mli │ │ ├── progress_engine_intf.ml │ │ ├── renderer.ml │ │ ├── renderer.mli │ │ ├── renderer_intf.ml │ │ ├── stdlib_ext.ml │ │ ├── units.ml │ │ └── units.mli │ ├── progress.ml │ ├── progress.mli │ ├── terminal_width.ml │ └── tests │ │ ├── common.ml │ │ ├── dune │ │ ├── test.ml │ │ ├── test.mli │ │ ├── test_flow_meter.ml │ │ ├── test_flow_meter.mli │ │ ├── test_printers.ml │ │ ├── test_printers.mli │ │ ├── test_units.ml │ │ └── test_units.mli └── terminal │ ├── ansi │ ├── ansi.ml │ ├── color.ml │ ├── dune │ ├── import.ml │ ├── style.ml │ ├── terminal_ansi.ml │ └── terminal_ansi.mli │ ├── dune │ ├── terminal.ml │ ├── terminal.mli │ ├── terminal_stubs.c │ └── tests │ ├── common.ml │ ├── dune │ ├── test.ml │ ├── test_colours.ml │ ├── test_colours.mli │ ├── test_width.ml │ └── test_width.mli └── terminal.opam /.gitignore: -------------------------------------------------------------------------------- 1 | _build/ 2 | _opam 3 | *~ 4 | \.\#* 5 | \#*# 6 | *.install 7 | .merlin 8 | -------------------------------------------------------------------------------- /.meta/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigfe/progress/d59f6eccd2317ff959a06afc6d53644eeecadf1e/.meta/example.gif -------------------------------------------------------------------------------- /.ocamlformat: -------------------------------------------------------------------------------- 1 | version = 0.27.0 2 | profile = conventional 3 | 4 | parse-docstrings 5 | module-item-spacing = compact 6 | dock-collection-brackets = false 7 | break-infix = fit-or-vertical 8 | break-separators = before 9 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ### 0.4.0 (2024-20-05) 2 | 3 | - Revert the `terminal` API and keep an "happy" path to get size of a tty 4 | and be compatible with MirageOS (@art-w, @msprotz, #42, #43) 5 | - Use a `float` instead of a `int` in `flow_meter per-second` (@mbarbin, #23, #27) 6 | 7 | ### 0.3.0 (2024-04-13) 8 | 9 | - Be compatible with MirageOS and remove `ocaml_terminal_get_sigwinch` (@art-w, #38) 10 | - Clear all lines in `interject_with` (@Gbury, #30) 11 | - Add `Display.remove_line` (@mbarbin, #26) 12 | - Fix compilation for OCaml 5.2 (reported by @Gbury, fixed by @dinosaure, #40) 13 | - Add `Display.{pause,resume}` (@Gbury, #37) 14 | 15 | ### 0.2.2 (2023-05-26) 16 | 17 | - Fix lower bounds on UTF-8 libraries (@craigfe, 42759d5) 18 | - Fix removed functions in `mtime.2.0.0` (@patricoferris, #31) 19 | - Use actual printer width for elapsed segment (@lsdch, #28) 20 | 21 | ### 0.2.1 (2021-06-29) 22 | 23 | - Fix the count segment of `Progress.counter` (when `pp` is passed) to show the 24 | running total rather than the latest reported value. (#19; @CraigFe, report 25 | by @Ngoguey42) 26 | - Fix `Terminal` stubs on MacOS. (#13; @CraigFe, report by @Ngoguey42) 27 | - Fix package tests on Windows. `Progress` does not yet support the Windows and 28 | Cygwin terminals; this is tracked by #16. (#15; @emillon) 29 | 30 | ### 0.2.0 (2021-06-26) 31 | 32 | Major update of the API, including a number of new features: 33 | 34 | - Rename the `Segment` module to `Line`, and improve the set of primitives for 35 | progress bar construction significantly. This includes time-sensitive segments 36 | (e.g `bytes_per_sec`, `eta`) and padding segments (`lpad` and `rpad`). 37 | - Add `Progress.interject_with` for interleaving logging with rendering, and 38 | functions for using `Progress` with `Logs` reporters. 39 | - Add support for adding lines to an ongoing rendering process via `Display`. 40 | - Improve the behaviour of the rendering core: handle terminal width changes / 41 | respond to user input etc. more cleanly. 42 | - Add many more examples and general improvements to the documentation. 43 | - Extract terminal-specific utilities to a new `Terminal` package. 44 | 45 | Also contains a number of smaller fixes: 46 | 47 | - Fix the display of minutes and seconds of `Progress.Units.seconds` and 48 | `Progress_unix.counter`. (#6, @Ngoguey42) 49 | - Raise an exception when attempting to run separate render processes 50 | simultaneously. (#8, @CraigFe) 51 | 52 | ### 0.1.1 (2020-10-13) 53 | 54 | - Rename `Progress.with_display` to `Progress.with_reporters`. (#3, @CraigFe) 55 | - Change the default display mode of progress bars to `ASCII` rather than 56 | `UTF8`. (#2, @CraigFe) 57 | - Change `Segment.box_dynamic` to take a function rather than a reference. (#1, 58 | @CraigFe) 59 | - Fix a bug causing multi-line layouts to occasionally not adapt to terminal 60 | size changes. (#1, @CraigFe) 61 | 62 | ### 0.1.0 (2020-10-12) 63 | 64 | Initial release. 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2020 Craig Ferguson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | screenshot: 2 | asciinema rec -c "dune exec examples/main.exe -- Stack" --overwrite /tmp/screenshot.json 3 | asciicast2gif -h 4 /tmp/screenshot.json .meta/example.gif 4 | 5 | .PHONY: docs 6 | docs: 7 | dune build @doc 8 | cp -fr docs/{odoc.css,fonts} _build/default/_doc/_html 9 | 10 | .PHONY: example 11 | example: 12 | @dune exec examples/main.exe -- $(name) 13 | 14 | .PHONY: examples 15 | examples: 16 | @for example in $(shell dune exec -- examples/main.exe --list); do \ 17 | echo "*** $$example"; \ 18 | dune exec -- examples/main.exe $$example; \ 19 | done 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Progress

2 |

User-definable progress bars for OCaml

3 |
4 |

5 | 6 |

7 | 8 | **Features**: 9 | 10 | - allows user-defined progress bar layouts; 11 | - supports rendering multiple progress bars simultaneously; 12 | - dynamically responds to changes in terminal size; 13 | - supports interleaving logging with progress bar rendering. 14 | 15 | The documentation is available [online][docs]. 16 | 17 | [docs]: https://craigfe.github.io/progress/progress/index.html 18 | 19 |
20 | 21 | ### Installation 22 | 23 | `Progress` can be installed with `opam`: 24 | 25 | ``` 26 | opam install progress 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/fonts/JetBrainsMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigfe/progress/d59f6eccd2317ff959a06afc6d53644eeecadf1e/docs/fonts/JetBrainsMono-Regular.ttf -------------------------------------------------------------------------------- /docs/fonts/JetBrainsMono-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigfe/progress/d59f6eccd2317ff959a06afc6d53644eeecadf1e/docs/fonts/JetBrainsMono-Regular.woff -------------------------------------------------------------------------------- /docs/fonts/JetBrainsMono-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigfe/progress/d59f6eccd2317ff959a06afc6d53644eeecadf1e/docs/fonts/JetBrainsMono-Regular.woff2 -------------------------------------------------------------------------------- /docs/fonts/lato-v17-latin-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigfe/progress/d59f6eccd2317ff959a06afc6d53644eeecadf1e/docs/fonts/lato-v17-latin-700.woff2 -------------------------------------------------------------------------------- /docs/fonts/lato-v17-latin-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigfe/progress/d59f6eccd2317ff959a06afc6d53644eeecadf1e/docs/fonts/lato-v17-latin-italic.woff2 -------------------------------------------------------------------------------- /docs/fonts/lato-v17-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigfe/progress/d59f6eccd2317ff959a06afc6d53644eeecadf1e/docs/fonts/lato-v17-latin-regular.woff2 -------------------------------------------------------------------------------- /docs/odoc.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | /* Copyright (c) 2016 The odoc contributors. All rights reserved. 3 | Distributed under the ISC license, see terms at the end of the file. 4 | %%NAME%% %%VERSION%% */ 5 | 6 | /* Fonts */ 7 | 8 | @font-face { 9 | font-family: "JetBrainsMono"; 10 | src: url("./fonts/JetBrainsMono-Regular.woff2") format("woff2"), 11 | url("./fonts/JetBrainsMono-Regular.woff") format("woff"); 12 | 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | 17 | @import url("https://rsms.me/inter/inter.css"); 18 | 19 | @font-face { 20 | font-family: "Lato"; 21 | font-style: normal; 22 | font-weight: 400; 23 | src: url("./fonts/lato-v17-latin-regular.woff2") format("woff2"); 24 | } 25 | 26 | @font-face { 27 | font-family: "Lato"; 28 | font-style: normal; 29 | font-weight: 700; 30 | src: url("./fonts/lato-v17-latin-700.woff2") format("woff2"); 31 | } 32 | 33 | @font-face { 34 | font-family: "Lato"; 35 | font-style: italic; 36 | font-weight: 400; 37 | src: url("./fonts/lato-v17-latin-italic.woff2") format("woff2"); 38 | } 39 | 40 | @import url("https://fonts.googleapis.com/css?family=Noticia+Text:400,400i,700"); 41 | @import url("https://fonts.googleapis.com/css?family=Fira+Sans:400,400i,500,500i,600,600i,700,700i"); 42 | 43 | :root { 44 | --font-code: JetBrainsMono, courier; 45 | --font: Lato, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 46 | Ubuntu, Cantarell, Open Sans, Helvetica Neue, Helvetica, Arial, sans-serif; 47 | 48 | --font-heading: Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, 49 | Oxygen, Ubuntu, Cantarell, Open Sans, Helvetica Neue, Helvetica, Arial, 50 | sans-serif; 51 | 52 | --link-color: #3d8fd1; 53 | --anchor-hover: #555; 54 | --anchor-color: #d5d5d5; 55 | --xref-shadow: #cc6666; 56 | --header-shadow: #ddd; 57 | --by-name-version-color: #aaa; 58 | --by-name-nav-link-color: #222; 59 | --target-background: rgba(187, 239, 253, 0.3); 60 | --target-shadow: rgba(187, 239, 253, 0.8); 61 | --pre-border-color: #eee; 62 | --code-background: #f6f8fa; 63 | --spec-summary-border-color: #5c9cf5; 64 | --spec-summary-background: var(--code-background); 65 | --spec-summary-hover-background: #ebeff2; 66 | --spec-details-after-background: rgba(0, 4, 15, 0.05); 67 | --spec-details-after-shadow: rgba(204, 204, 204, 0.53); 68 | } 69 | 70 | /* Reset a few things. */ 71 | 72 | html, 73 | body, 74 | div, 75 | span, 76 | applet, 77 | object, 78 | iframe, 79 | h1, 80 | h2, 81 | h3, 82 | h4, 83 | h5, 84 | h6, 85 | p, 86 | blockquote, 87 | pre, 88 | a, 89 | abbr, 90 | acronym, 91 | address, 92 | big, 93 | cite, 94 | code, 95 | del, 96 | dfn, 97 | em, 98 | img, 99 | ins, 100 | kbd, 101 | q, 102 | s, 103 | samp, 104 | small, 105 | strike, 106 | strong, 107 | sub, 108 | sup, 109 | tt, 110 | var, 111 | b, 112 | u, 113 | i, 114 | center, 115 | dl, 116 | dt, 117 | dd, 118 | ol, 119 | ul, 120 | li, 121 | fieldset, 122 | form, 123 | label, 124 | legend, 125 | table, 126 | caption, 127 | tbody, 128 | tfoot, 129 | thead, 130 | tr, 131 | th, 132 | td, 133 | article, 134 | aside, 135 | canvas, 136 | details, 137 | embed, 138 | figure, 139 | figcaption, 140 | footer, 141 | header, 142 | hgroup, 143 | menu, 144 | nav, 145 | output, 146 | ruby, 147 | section, 148 | summary, 149 | time, 150 | mark, 151 | audio, 152 | video { 153 | margin: 0; 154 | padding: 0; 155 | border: 0; 156 | font: inherit; 157 | vertical-align: baseline; 158 | } 159 | 160 | table { 161 | border-collapse: collapse; 162 | border-spacing: 0; 163 | } 164 | 165 | *, 166 | *:before, 167 | *:after { 168 | box-sizing: border-box; 169 | } 170 | 171 | html { 172 | font-size: 15px; 173 | } 174 | 175 | body { 176 | text-align: left; 177 | background: #ffffff; 178 | color: var(--color); 179 | background-color: var(--main-background); 180 | } 181 | 182 | body { 183 | max-width: 90ex; 184 | margin-left: calc(10vw + 20ex); 185 | margin-right: 4ex; 186 | margin-top: 20px; 187 | margin-bottom: 50px; 188 | font-family: var(--font); 189 | line-height: 1.5; 190 | } 191 | 192 | header { 193 | margin-bottom: 30px; 194 | } 195 | 196 | nav { 197 | font-family: var(--font); 198 | } 199 | 200 | /* Basic markup elements */ 201 | 202 | b, 203 | strong { 204 | font-weight: bold; 205 | } 206 | 207 | i { 208 | font-style: italic; 209 | } 210 | 211 | em, 212 | i em.odd { 213 | font-style: italic; 214 | } 215 | 216 | em.odd, 217 | i em { 218 | font-style: normal; 219 | } 220 | 221 | sup { 222 | vertical-align: super; 223 | } 224 | 225 | sub { 226 | vertical-align: sub; 227 | } 228 | 229 | sup, 230 | sub { 231 | font-size: 12px; 232 | line-height: 0; 233 | margin-left: 0.2ex; 234 | } 235 | 236 | pre { 237 | margin-top: 0.8em; 238 | margin-bottom: 1.2em; 239 | } 240 | 241 | p, 242 | ul, 243 | ol { 244 | margin-top: 0.5em; 245 | margin-bottom: 1em; 246 | } 247 | ul, 248 | ol { 249 | list-style-position: outside; 250 | } 251 | 252 | ul > li { 253 | margin-left: 22px; 254 | } 255 | 256 | ol > li { 257 | margin-left: 27.2px; 258 | } 259 | 260 | li > *:first-child { 261 | margin-top: 0; 262 | } 263 | 264 | /* Text alignements, this should be forbidden. */ 265 | 266 | .left { 267 | text-align: left; 268 | } 269 | 270 | .right { 271 | text-align: right; 272 | } 273 | 274 | .center { 275 | text-align: center; 276 | } 277 | 278 | /* Links and anchors */ 279 | 280 | a { 281 | text-decoration: none; 282 | color: #2c5cbd; 283 | color: var(--link-color); 284 | } 285 | 286 | a:hover { 287 | box-shadow: 0 1px 0 0 #2c5cbd; 288 | box-shadow: 0 1px 0 0 var(--link-color); 289 | } 290 | 291 | /* Linked highlight */ 292 | *:target { 293 | background-color: rgba(187, 239, 253, 0.3) !important; 294 | box-shadow: 0 0px 0 1px rgba(187, 239, 253, 0.8) !important; 295 | background-color: var(--target-background) !important; 296 | box-shadow: 0 0px 0 1px var(--target-shadow) !important; 297 | border-radius: 1px; 298 | } 299 | 300 | *:hover > a.anchor { 301 | visibility: visible; 302 | } 303 | 304 | a.anchor:before { 305 | content: "#"; 306 | } 307 | 308 | a.anchor:hover { 309 | box-shadow: none; 310 | text-decoration: none; 311 | color: #555; 312 | color: var(--anchor-hover); 313 | } 314 | 315 | a.anchor { 316 | visibility: hidden; 317 | position: absolute; 318 | /* top: 0px; */ 319 | /* margin-left: -3ex; */ 320 | margin-left: -1.3em; 321 | font-weight: normal; 322 | font-style: normal; 323 | padding-right: 0.4em; 324 | padding-left: 0.4em; 325 | /* To remain selectable */ 326 | color: #d5d5d5; 327 | color: var(--anchor-color); 328 | } 329 | 330 | .spec > a.anchor { 331 | margin-left: -2.3em; 332 | padding-right: 0.9em; 333 | } 334 | 335 | .xref-unresolved { 336 | color: #2c94bd; 337 | } 338 | .xref-unresolved:hover { 339 | box-shadow: 0 1px 0 0 #cc6666; 340 | box-shadow: 0 1px 0 0 var(--xref-shadow); 341 | } 342 | 343 | /* Section and document divisions. 344 | Until at least 4.03 many of the modules of the stdlib start at .h7, 345 | we restart the sequence there like h2 */ 346 | 347 | h1, 348 | h2, 349 | h3, 350 | h4, 351 | h5, 352 | h6, 353 | .h7, 354 | .h8, 355 | .h9, 356 | .h10 { 357 | font-family: var(--font-heading); 358 | font-weight: 500; 359 | margin: 0.5em 0 0.5em 0; 360 | padding-top: 0.1em; 361 | line-height: 1.2; 362 | overflow-wrap: break-word; 363 | } 364 | 365 | h1, 366 | h2, 367 | h3, 368 | h4, 369 | h5, 370 | h6, 371 | .spec { 372 | scroll-margin-top: 50px; 373 | } 374 | 375 | h1 { 376 | font-weight: 500; 377 | font-size: 2.441em; 378 | margin-top: 1.214em; 379 | } 380 | 381 | h1 { 382 | font-weight: 500; 383 | font-size: 1.953em; 384 | box-shadow: 0 1px 0 0 #ddd; 385 | box-shadow: 0 1px 0 0 var(--header-shadow); 386 | } 387 | 388 | h2 { 389 | text-transform: uppercase; 390 | font-weight: 600; 391 | font-size: 1.263em; 392 | margin-top: 1.7em; 393 | margin-left: 0.5em; 394 | margin-bottom: 1em; 395 | } 396 | 397 | h3 { 398 | margin-top: 1.4em; 399 | font-size: 1.25em; 400 | } 401 | 402 | h4 { 403 | margin-top: 1em; 404 | margin-bottom: 0.7em; 405 | } 406 | 407 | small, 408 | .font_small { 409 | font-size: 0.8em; 410 | } 411 | 412 | h1 code, 413 | h1 tt { 414 | font-size: 95%; 415 | font-weight: inherit; 416 | } 417 | 418 | h2 code, 419 | h2 tt { 420 | font-size: 95%; 421 | font-weight: inherit; 422 | } 423 | 424 | h3 code, 425 | h3 tt { 426 | font-size: 95%; 427 | font-weight: inherit; 428 | } 429 | 430 | h4 { 431 | font-size: 1.12em; 432 | } 433 | 434 | .spec-doc { 435 | margin-bottom: 2em; 436 | } 437 | 438 | /* Comment delimiters, hidden but accessible to screen readers and 439 | selected for copy/pasting */ 440 | 441 | /* Taken from bootstrap */ 442 | /* See also https://stackoverflow.com/a/27769435/4220738 */ 443 | .comment-delim { 444 | position: absolute; 445 | width: 1px; 446 | height: 1px; 447 | padding: 0; 448 | margin: -1px; 449 | overflow: hidden; 450 | clip: rect(0, 0, 0, 0); 451 | white-space: nowrap; 452 | border: 0; 453 | } 454 | 455 | /* Preformatted and code */ 456 | 457 | tt, 458 | code, 459 | pre { 460 | font-family: var(--font-code); 461 | font-weight: 400; 462 | } 463 | 464 | pre { 465 | padding: 0.1em; 466 | border: 1px solid #eee; 467 | border: 1px solid var(--pre-border-color); 468 | border-radius: 5px; 469 | overflow-x: auto; 470 | } 471 | 472 | p code, 473 | li code { 474 | background-color: #f6f8fa; 475 | color: #0d2b3e; 476 | background-color: var(--li-code-background); 477 | color: var(--li-code-color); 478 | border-radius: 3px; 479 | padding: 0 0.3ex; 480 | } 481 | 482 | p a > code { 483 | color: #2c5cbd; 484 | color: var(--link-color); 485 | } 486 | 487 | /* Code blocks (e.g. Examples) */ 488 | 489 | code { 490 | font-size: 0.893rem; 491 | } 492 | 493 | :not(pre):not(h1) > code { 494 | background: var(--code-background); 495 | border-radius: 2px; 496 | padding: 2px 4px; 497 | } 498 | 499 | /* Code lexemes */ 500 | 501 | .keyword { 502 | font-weight: 500; 503 | } 504 | 505 | .arrow { 506 | white-space: nowrap; 507 | } 508 | 509 | /* Module member specification */ 510 | 511 | .spec { 512 | background-color: var(--spec-summary-background); 513 | border-radius: 3px; 514 | border-left: 4px solid var(--spec-summary-border-color); 515 | border-right: 5px solid transparent; 516 | padding: 0.35em 0.5em; 517 | } 518 | 519 | div.spec, 520 | .def-doc { 521 | margin-bottom: 15px; 522 | } 523 | 524 | .spec.type .variant { 525 | margin-left: 2ch; 526 | } 527 | .spec.type .variant p { 528 | margin: 0; 529 | font-style: italic; 530 | } 531 | .spec.type .record { 532 | margin-left: 2ch; 533 | } 534 | .spec.type .record p { 535 | margin: 0; 536 | font-style: italic; 537 | } 538 | 539 | div.def { 540 | margin-top: 0; 541 | text-indent: -2ex; 542 | padding-left: 2ex; 543 | } 544 | 545 | div.def + div.def-doc { 546 | margin-left: 1ex; 547 | margin-top: 2.5px; 548 | } 549 | 550 | div.def-doc > *:first-child { 551 | margin-top: 0; 552 | } 553 | 554 | /* Collapsible inlined include and module */ 555 | 556 | .odoc-include details { 557 | position: relative; 558 | } 559 | 560 | .odoc-include details:after { 561 | z-index: -100; 562 | display: block; 563 | content: " "; 564 | position: absolute; 565 | border-radius: 0 1ex 1ex 0; 566 | right: -20px; 567 | top: 1px; 568 | bottom: 1px; 569 | width: 15px; 570 | background: rgba(0, 4, 15, 0.05); 571 | box-shadow: 0 0px 0 1px rgba(204, 204, 204, 0.53); 572 | background: var(--spec-details-after-background); 573 | box-shadow: 0 0px 0 1px var(--spec-details-after-shadow); 574 | } 575 | 576 | .odoc-include summary { 577 | position: relative; 578 | margin-bottom: 20px; 579 | cursor: pointer; 580 | outline: none; 581 | } 582 | 583 | .odoc-include summary:hover { 584 | background-color: var(--spec-summary-hover-background); 585 | } 586 | 587 | /* FIXME: Does not work in Firefox. */ 588 | .odoc-include summary::-webkit-details-marker { 589 | color: #888; 590 | transform: scaleX(-1); 591 | position: absolute; 592 | top: calc(50% - 5px); 593 | height: 11px; 594 | right: -29px; 595 | } 596 | 597 | /* Records and variants FIXME */ 598 | 599 | div.def table { 600 | text-indent: 0em; 601 | padding: 0; 602 | margin-left: -2ex; 603 | } 604 | 605 | td.def { 606 | padding-left: 2ex; 607 | } 608 | 609 | td.def-doc *:first-child { 610 | margin-top: 0em; 611 | } 612 | 613 | /* Lists of @tags */ 614 | 615 | .at-tags { 616 | list-style-type: none; 617 | margin-left: -3ex; 618 | } 619 | .at-tags li { 620 | padding-left: 3ex; 621 | text-indent: -3ex; 622 | } 623 | .at-tags .at-tag { 624 | text-transform: capitalize; 625 | } 626 | 627 | /* Lists of modules */ 628 | 629 | .modules { 630 | list-style-type: none; 631 | margin-left: -3ex; 632 | } 633 | .modules li { 634 | padding-left: 3ex; 635 | text-indent: -3ex; 636 | margin-top: 5px; 637 | } 638 | .modules .synopsis { 639 | padding-left: 1ch; 640 | } 641 | 642 | /* Odig package index */ 643 | 644 | .packages { 645 | list-style-type: none; 646 | margin-left: -3ex; 647 | } 648 | .packages li { 649 | padding-left: 3ex; 650 | text-indent: -3ex; 651 | } 652 | .packages li a.anchor { 653 | padding-right: 0.5ch; 654 | padding-left: 3ch; 655 | } 656 | .packages .version { 657 | font-size: 10px; 658 | color: var(--by-name-version-color); 659 | } 660 | .packages .synopsis { 661 | padding-left: 1ch; 662 | } 663 | 664 | .by-name nav a { 665 | text-transform: uppercase; 666 | font-size: 18px; 667 | margin-right: 1ex; 668 | color: #222; 669 | color: var(--by-name-nav-link-color); 670 | display: inline-block; 671 | } 672 | 673 | .by-tag nav a { 674 | margin-right: 1ex; 675 | color: #222; 676 | color: var(--by-name-nav-link-color); 677 | display: inline-block; 678 | } 679 | 680 | .by-tag ol { 681 | list-style-type: none; 682 | } 683 | .by-tag ol.tags li { 684 | margin-left: 1ch; 685 | display: inline-block; 686 | } 687 | .by-tag td:first-child { 688 | text-transform: uppercase; 689 | } 690 | 691 | /* Odig package page */ 692 | 693 | .package nav { 694 | display: inline; 695 | font-size: 14px; 696 | font-weight: normal; 697 | } 698 | 699 | .package .version { 700 | font-size: 14px; 701 | } 702 | 703 | .package.info { 704 | margin: 0; 705 | } 706 | 707 | .package.info td:first-child { 708 | font-style: italic; 709 | padding-right: 2ex; 710 | } 711 | 712 | .package.info ul { 713 | list-style-type: none; 714 | display: inline; 715 | margin: 0; 716 | } 717 | 718 | .package.info li { 719 | display: inline-block; 720 | margin: 0; 721 | margin-right: 1ex; 722 | } 723 | 724 | #info-authors li, 725 | #info-maintainers li { 726 | display: block; 727 | } 728 | 729 | /* Sidebar and TOC */ 730 | 731 | .odoc-toc:before { 732 | display: block; 733 | content: "Contents"; 734 | text-transform: uppercase; 735 | font-size: 1.1em; 736 | margin: 1.414em 0 0.5em; 737 | font-weight: 800; 738 | color: #777; 739 | color: var(--toc-before-color); 740 | line-height: 1.2; 741 | } 742 | 743 | .odoc-toc { 744 | position: fixed; 745 | top: 0px; 746 | bottom: 0px; 747 | left: 0px; 748 | max-width: 30ex; 749 | min-width: 26ex; 750 | width: 20%; 751 | background: #f6f8fa; 752 | background: var(--toc-background); 753 | overflow: auto; 754 | color: #1f2d3d; 755 | color: var(--toc-color); 756 | padding-left: 2ex; 757 | padding-right: 2ex; 758 | } 759 | 760 | .odoc-toc ul li a { 761 | font-family: "Fira Sans", sans-serif; 762 | font-size: 0.95em; 763 | color: #333; 764 | color: var(--color); 765 | font-weight: 400; 766 | line-height: 1.6em; 767 | display: block; 768 | } 769 | 770 | .odoc-toc ul li a:hover { 771 | box-shadow: none; 772 | text-decoration: underline; 773 | } 774 | 775 | /* First level titles */ 776 | 777 | .odoc-toc > ul > li > a { 778 | font-weight: 500; 779 | } 780 | 781 | .odoc-toc li ul { 782 | margin: 0px; 783 | } 784 | 785 | .odoc-toc ul { 786 | list-style-type: none; 787 | } 788 | 789 | .odoc-toc ul li { 790 | margin: 0; 791 | } 792 | .odoc-toc > ul > li { 793 | margin-bottom: 0.3em; 794 | } 795 | 796 | .odoc-toc ul li li { 797 | border-left: 1px solid #ccc; 798 | border-left: 1px solid var(--toc-list-border); 799 | margin-left: 5px; 800 | padding-left: 12px; 801 | } 802 | 803 | /* Mobile adjustements. */ 804 | 805 | @media only screen and (max-width: 95ex) { 806 | body { 807 | margin-left: 4ex; 808 | } 809 | .odoc-content { 810 | margin: auto; 811 | padding: 2em; 812 | } 813 | .odoc-toc { 814 | position: static; 815 | width: auto; 816 | min-width: unset; 817 | max-width: unset; 818 | border: none; 819 | padding: 0.2em 1em; 820 | border-radius: 5px; 821 | } 822 | } 823 | 824 | /* Print adjustements. */ 825 | 826 | @media print { 827 | body { 828 | color: black; 829 | background: white; 830 | } 831 | body nav:first-child { 832 | visibility: hidden; 833 | } 834 | } 835 | 836 | /* Syntax highlighting (based on github-gist) */ 837 | 838 | .hljs { 839 | display: block; 840 | background: white; 841 | background: var(--code-background); 842 | padding: 0.5em; 843 | color: #333333; 844 | color: var(--color); 845 | overflow-x: auto; 846 | } 847 | 848 | .hljs-comment, 849 | .hljs-meta { 850 | color: #969896; 851 | } 852 | 853 | .hljs-string, 854 | .hljs-variable, 855 | .hljs-template-variable, 856 | .hljs-strong, 857 | .hljs-emphasis, 858 | .hljs-quote { 859 | color: #df5000; 860 | } 861 | 862 | .hljs-keyword, 863 | .hljs-selector-tag { 864 | color: #a71d5d; 865 | } 866 | 867 | .hljs-type, 868 | .hljs-class .hljs-title { 869 | color: #458; 870 | font-weight: 500; 871 | } 872 | 873 | .hljs-literal, 874 | .hljs-symbol, 875 | .hljs-bullet, 876 | .hljs-attribute { 877 | color: #0086b3; 878 | } 879 | 880 | .hljs-section, 881 | .hljs-name { 882 | color: #63a35c; 883 | } 884 | 885 | .hljs-tag { 886 | color: #333333; 887 | } 888 | 889 | .hljs-attr, 890 | .hljs-selector-id, 891 | .hljs-selector-class, 892 | .hljs-selector-attr, 893 | .hljs-selector-pseudo { 894 | color: #795da3; 895 | } 896 | 897 | .hljs-addition { 898 | color: #55a532; 899 | background-color: #eaffea; 900 | } 901 | 902 | .hljs-deletion { 903 | color: #bd2c00; 904 | background-color: #ffecec; 905 | } 906 | 907 | .hljs-link { 908 | text-decoration: underline; 909 | } 910 | 911 | /*--------------------------------------------------------------------------- 912 | Copyright (c) 2016 The odoc contributors 913 | 914 | Permission to use, copy, modify, and/or distribute this software for any 915 | purpose with or without fee is hereby granted, provided that the above 916 | copyright notice and this permission notice appear in all copies. 917 | 918 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 919 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 920 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 921 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 922 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 923 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 924 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 925 | ---------------------------------------------------------------------------*/ 926 | -------------------------------------------------------------------------------- /dune: -------------------------------------------------------------------------------- 1 | (rule 2 | (alias docs) 3 | (deps 4 | (universe) 5 | (alias_rec doc) 6 | (source_tree docs)) 7 | (action 8 | (bash "cp -fr docs/{odoc.css,fonts} _doc/_html"))) 9 | -------------------------------------------------------------------------------- /dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 2.7) 2 | (name progress) 3 | (implicit_transitive_deps false) 4 | 5 | (generate_opam_files true) 6 | (source (github CraigFe/progress)) 7 | (license MIT) 8 | (maintainers "Craig Ferguson ") 9 | (authors "Craig Ferguson ") 10 | 11 | (package 12 | (name progress) 13 | (synopsis "User-definable progress bars") 14 | (description "\ 15 | A progress bar library for OCaml, featuring a DSL for declaratively specifying 16 | progress bar formats. Supports rendering multiple progress bars simultaneously.\ 17 | ") 18 | (documentation https://CraigFe.github.io/progress/) 19 | (depends 20 | (ocaml (>= 4.08.0)) 21 | (terminal (= :version)) 22 | (fmt (>= 0.8.5)) 23 | (logs (>= 0.7.0)) 24 | (mtime (>= 2.0.0)) 25 | (uucp (>= 2.0.0)) 26 | (uutf (>= 1.0.0)) 27 | vector 28 | (optint (>= 0.1.0)) 29 | (alcotest (and :with-test (>= 1.4.0))) 30 | (astring :with-test))) 31 | 32 | (package 33 | (name terminal) 34 | (synopsis "Basic utilities for interacting with terminals") 35 | (description "Basic utilities for interacting with terminals") 36 | (documentation https://CraigFe.github.io/progress/) 37 | (depends 38 | (ocaml (>= 4.03.0)) 39 | (uucp (>= 2.0.0)) 40 | (uutf (>= 1.0.0)) 41 | stdlib-shims 42 | (alcotest (and :with-test (>= 1.4.0))) 43 | (fmt :with-test) 44 | (astring :with-test) 45 | (mtime (and :with-test (>= 2.0.0))))) 46 | -------------------------------------------------------------------------------- /examples/bar_styles.ml: -------------------------------------------------------------------------------- 1 | let[@ocamlformat "disable"] bar_styles = 2 | let open Progress.Line.Bar_style in 3 | let open Progress.Color in 4 | let brackets = ("[", "]") in 5 | let bars = ("│", "│") in 6 | [ ("ASCII" , ascii |> with_color (ansi `cyan) |> with_empty_color (ansi `blue)) 7 | ; ("arrow" , v ~delims:brackets ~color:(ansi `red) [ "="; ">"; " " ]) 8 | ; ("dots" , v ~delims:brackets ~color:(ansi `magenta) [ "." ]) 9 | ; ("digits1" , v ~delims:brackets (List.init 10 @@ fun i -> string_of_int (9 - i))) 10 | ; ("digits2" , v ~delims:brackets (List.concat @@ 11 | List.init 10 @@ fun i -> 12 | List.init 10 @@ fun j -> 13 | Printf.sprintf "«%d%d»" (9 - i) (9 - j))) 14 | ; ("UTF8" , utf8 |> with_color (ansi `green)) 15 | ; ("rough bar", v ~delims:bars ~color:(hex "#DC2F02") [ "█"; " " ]) 16 | ; ("fine bar" , v ~delims:bars ~color:(hex "#E85D04") [ "█"; "▉"; "▊"; "▋"; "▌"; "▍"; "▎"; "▏"; " " ] ) 17 | ; ("vertical" , v ~delims:bars ~color:(hex "#F48C06") [ "█"; "▇"; "▆"; "▅"; "▄"; "▃"; "▂"; "▁"; " " ] ) 18 | ; ("blocky" , v ~delims:bars ~color:(hex "#FAA307") [ "█"; "▛"; "▌"; "▖"; " " ]) 19 | ; ("fade in" , v ~delims:bars ~color:(hex "#FFBA08") [ "█"; "▓"; "▒"; "░"; " " ]) 20 | ] 21 | 22 | let layout = 23 | let pick_colour = Utils.colour_picker () in 24 | let open Progress.Line in 25 | let bars = 26 | ListLabels.map bar_styles ~f:(fun (name, style) -> 27 | lpad 17 (constf "%s : " name) 28 | ++ bar ~style:(`Custom style) ~color:(pick_colour ()) 1000) 29 | in 30 | Progress.Multi.(blank ++ lines bars ++ blank) 31 | 32 | let run () = 33 | Progress.with_reporters ~config:(Progress.Config.v ~max_width:(Some 51) ()) 34 | layout (fun reporters -> 35 | for _ = 0 to 1000 do 36 | List.iter (fun f -> f 1) reporters; 37 | Unix.sleepf 0.006 38 | done) 39 | -------------------------------------------------------------------------------- /examples/bar_styles.mli: -------------------------------------------------------------------------------- 1 | val run : unit -> unit 2 | -------------------------------------------------------------------------------- /examples/cargo.ml: -------------------------------------------------------------------------------- 1 | let packages = 2 | [ ("0install-solver", "2.17") 3 | ; ("afl-persistent", "1.3") 4 | ; ("alcotest", "1.4.0") 5 | ; ("astring", "0.8.5") 6 | ; ("base", "v0.14.0") 7 | ; ("bechamel", "0.1.0") 8 | ; ("bos", "0.2.0") 9 | ; ("cmdliner", "1.0.4") 10 | ; ("cohttp", "4.0.0") 11 | ; ("core", "v0.14.1") 12 | ; ("ctypes", "0.17.1") 13 | ; ("dune", "2.8.4") 14 | ; ("either", "1.0.0") 15 | ; ("fmt", "0.8.9") 16 | ; ("fpath", "0.7.2") 17 | ; ("logs", "0.7.0") 18 | ; ("lru", "0.3.0") 19 | ; ("lwt", "5.4.0") 20 | ; ("memtrace", "0.1.2") 21 | ; ("mirage", "3.10.1") 22 | ; ("mirage-clock", "3.1.0") 23 | ; ("mirage-clock-unix", "3.1.0") 24 | ; ("mirage-crypto", "0.10.1") 25 | ; ("optint", "0.1.0") 26 | ; ("ppx_repr", "0.3.0") 27 | ; ("repr", "0.3.0") 28 | ; ("stdio", "v0.14.0") 29 | ; ("uucp", "13.0.0") 30 | ; ("uutf", "1.0.2") 31 | ; ("yojson", "1.7.0") 32 | ; ("zarith", "1.9.1") 33 | ] 34 | |> Vector.of_list ~dummy:("", "") 35 | 36 | let setup_logs () = 37 | let reporter = Progress.logs_reporter () in 38 | Fmt_tty.setup_std_outputs (); 39 | Logs_threaded.enable (); 40 | Logs.set_reporter reporter 41 | 42 | let bar = 43 | let open Progress.Line in 44 | let total = Vector.length packages in 45 | list 46 | [ constf " %a" Fmt.(styled `Cyan string) "Building" 47 | ; using fst 48 | (brackets 49 | (bar 50 | ~style:(`Custom (Bar_style.v [ "="; ">"; " " ])) 51 | ~width:(`Fixed 40) total)) 52 | ; ticker_to total 53 | ; using snd string 54 | ] 55 | 56 | let rec package_worker (active_packages, reporter) = 57 | match Vector.pop packages with 58 | | exception Vector.Empty -> () 59 | | package, version -> 60 | active_packages := package :: !active_packages; 61 | Logs.app (fun f -> 62 | f " %a %s %s" Fmt.(styled `Green string) "Compiling" package version); 63 | Unix.sleepf (Random.float 1.); 64 | active_packages := List.filter (( <> ) package) !active_packages; 65 | reporter (); 66 | package_worker (active_packages, reporter) 67 | 68 | let run () = 69 | setup_logs (); 70 | Random.self_init (); 71 | let cpus = 4 in 72 | let run_duration = Mtime_clock.counter () in 73 | let active_packages = ref [] in 74 | Progress.with_reporter ~config:(Progress.Config.v ~persistent:false ()) bar 75 | (fun reporter -> 76 | let reporter () = 77 | let package_list = 78 | !active_packages |> List.sort String.compare |> String.concat ", " 79 | in 80 | reporter (1, package_list) 81 | in 82 | let threads = 83 | List.init cpus (fun _ -> 84 | Thread.create package_worker (active_packages, reporter)) 85 | in 86 | List.iter Thread.join threads); 87 | Logs.app (fun f -> 88 | f " %a in %a" 89 | Fmt.(styled `Green string) 90 | "Finished" Mtime.Span.pp 91 | (Mtime_clock.count run_duration)) 92 | -------------------------------------------------------------------------------- /examples/cargo.mli: -------------------------------------------------------------------------------- 1 | val run : unit -> unit 2 | -------------------------------------------------------------------------------- /examples/download.ml: -------------------------------------------------------------------------------- 1 | open Progress 2 | 3 | let pick_colour = Utils.colour_picker () 4 | 5 | let bar ~total = 6 | let open Line in 7 | let spinner = spinner ~color:(Color.ansi `green) () in 8 | let bar = bar ~color:(pick_colour ()) ~style:`ASCII total in 9 | list 10 | [ spinner 11 | ; brackets (elapsed ()) 12 | ; bar 13 | ; bytes 14 | ; parens (const "eta: " ++ eta total) 15 | ] 16 | 17 | (* Simple mock for a worker performing a download action. *) 18 | module Worker = struct 19 | type t = 20 | { mutable todo : int 21 | ; mutable download_rate : int 22 | ; mutable reporter : int Reporter.t option 23 | } 24 | 25 | let empty () = { todo = 0; reporter = None; download_rate = 100_000 } 26 | 27 | let make_progress t = 28 | let progress = min t.todo t.download_rate in 29 | t.download_rate <- max 0 (t.download_rate + (Random.int 50_001 - 25_000)); 30 | t.todo <- t.todo - progress; 31 | Reporter.report (Option.get t.reporter) progress 32 | end 33 | 34 | type t = { mutable active_workers : Worker.t list; mutable files : int list } 35 | 36 | let run () = 37 | let remove_lines = 38 | (* Run with [REMOVE_LINES=true] to see bars being removed from the 39 | display after the download is done. *) 40 | match Sys.getenv_opt "REMOVE_LINES" with 41 | | None | Some "false" -> false 42 | | Some _ -> true 43 | in 44 | let total_files = 18 in 45 | let files = List.init total_files (fun _ -> Random.int 100_000_000) in 46 | 47 | let bottom_line = 48 | Line.( 49 | spacer 4 50 | ++ ticker_to ~sep:(const " / ") total_files 51 | ++ const " files downloaded, elapsed: " 52 | ++ elapsed ()) 53 | in 54 | let display = 55 | Display.start Multi.(blank ++ blank ++ line bottom_line ++ blank) 56 | in 57 | let [ completed ] = Display.reporters display in 58 | let nb_workers = 5 in 59 | let finish_item (worker : Worker.t) = 60 | Reporter.finalise (Option.get worker.reporter); 61 | completed (); 62 | if remove_lines then 63 | Display.remove_line display (Option.get worker.reporter) 64 | in 65 | let pick_item t (worker : Worker.t) = 66 | match t.files with 67 | | [] -> false 68 | | x :: xs -> 69 | let new_reporter = Display.add_line ~above:3 display (bar ~total:x) in 70 | worker.reporter <- Some new_reporter; 71 | worker.todo <- x; 72 | t.files <- xs; 73 | true 74 | in 75 | let t = 76 | { files; active_workers = List.init nb_workers (fun _ -> Worker.empty ()) } 77 | in 78 | 79 | (* Give everyone something to do *) 80 | ListLabels.iter t.active_workers ~f:(fun x -> 81 | let item_available = pick_item t x in 82 | assert item_available); 83 | 84 | (* Keep going until all files are downloaded *) 85 | while List.length t.active_workers > 0 do 86 | let active_workers = 87 | ListLabels.filter t.active_workers ~f:(fun (worker : Worker.t) -> 88 | Worker.make_progress worker; 89 | if worker.todo > 0 then true (* Not done yet; keep going. *) 90 | else ( 91 | finish_item worker; 92 | pick_item t worker)) 93 | in 94 | 95 | t.active_workers <- active_workers; 96 | Unix.sleepf 0.01 97 | done; 98 | Display.finalise display 99 | -------------------------------------------------------------------------------- /examples/download.mli: -------------------------------------------------------------------------------- 1 | val run : unit -> unit 2 | -------------------------------------------------------------------------------- /examples/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name examples) 3 | (modules 4 | (:standard \ main)) 5 | (libraries progress unix logs logs.fmt logs.threaded fmt fmt.tty mtime 6 | mtime.clock.os vector threads.posix)) 7 | 8 | (executable 9 | (name main) 10 | (modules main) 11 | (libraries examples fmt)) 12 | -------------------------------------------------------------------------------- /examples/interject.ml: -------------------------------------------------------------------------------- 1 | let bar ~total = 2 | let open Progress.Line in 3 | list 4 | [ spinner ~color:(Progress.Color.ansi `green) () 5 | ; bar total 6 | ; count_to total 7 | ] 8 | 9 | let run () = 10 | let total = 100 in 11 | Progress.with_reporter (bar ~total) (fun f -> 12 | for i = 1 to total do 13 | f 1; 14 | if i mod 10 = 0 then 15 | Progress.interject_with (fun () -> 16 | print_endline (":: Finished " ^ string_of_int i)); 17 | Unix.sleepf 0.025 18 | done) 19 | -------------------------------------------------------------------------------- /examples/interject.mli: -------------------------------------------------------------------------------- 1 | val run : unit -> unit 2 | -------------------------------------------------------------------------------- /examples/main.ml: -------------------------------------------------------------------------------- 1 | let examples = 2 | Examples. 3 | [ ( "bar_styles" 4 | , "Demo of possible progress bar configurations" 5 | , Bar_styles.run ) 6 | ; ("cargo", "Port of the Cargo install progress bar", Cargo.run) 7 | ; ("download", "Rainbow-coloured download sequence", Download.run) 8 | ; ("interject", "Logging while displaying a progress bar", Interject.run) 9 | ; ("readme", "Demonstration included in the README", Readme.run) 10 | ; ("spinners", "Demo of possible spinner configurations", Spinners.run) 11 | ; ("yarn", "Yarn-like download and install sequence", Yarn.run) 12 | ] 13 | 14 | let available_examples () = 15 | Format.eprintf "Available examples: @."; 16 | ListLabels.iter examples ~f:(fun (name, desc, _) -> 17 | Format.eprintf "- %-12s %a@." name 18 | Fmt.(styled `Faint (parens string)) 19 | desc) 20 | 21 | let usage () = 22 | Format.eprintf "@."; 23 | available_examples (); 24 | Format.eprintf "\n%a: dune exec %s%s%s.exe -- [--help] @." 25 | Fmt.(styled `Green string) 26 | "usage" Filename.current_dir_name Filename.dir_sep 27 | (Filename.chop_extension __FILE__) 28 | 29 | let () = 30 | Random.self_init (); 31 | Fmt.set_style_renderer Fmt.stderr `Ansi_tty; 32 | match Sys.argv with 33 | | [| _ |] | [| _; "-h" | "-help" | "--help" |] -> usage () 34 | | [| _; "--list" |] -> 35 | ListLabels.iter ~f:(fun (name, _, _) -> print_endline name) examples 36 | | [| _; name |] -> ( 37 | match 38 | List.find_opt 39 | (fun (n, _, _) -> n = String.lowercase_ascii name) 40 | examples 41 | with 42 | | None -> 43 | Format.eprintf "%a: unrecognised example name `%a`.@.@." 44 | Fmt.(styled `Bold @@ styled `Red string) 45 | "Error" 46 | Fmt.(styled `Cyan string) 47 | name; 48 | available_examples (); 49 | exit 1 50 | | Some (_, _, f) -> f ()) 51 | | _ -> 52 | usage (); 53 | exit 1 54 | -------------------------------------------------------------------------------- /examples/main.mli: -------------------------------------------------------------------------------- 1 | (* Intentionally empty *) 2 | -------------------------------------------------------------------------------- /examples/readme.ml: -------------------------------------------------------------------------------- 1 | open Progress 2 | 3 | let bar color message = 4 | let total = 1_000_000_000L in 5 | let open Line.Using_int64 in 6 | list 7 | [ rpad 16 (constf " %s" message) 8 | ; bytes 9 | ; bytes_per_sec 10 | ; bar ~color ~style:`UTF8 total 11 | ; percentage_of total ++ const " " 12 | ] 13 | |> Multi.line 14 | 15 | let main () = 16 | let layout = 17 | let open Multi in 18 | bar (Color.hex "#90e0ef") "index.html" 19 | ++ bar (Color.hex "#48cae4") "sitemap.xml" 20 | ++ bar (Color.hex "#00b4d8") "img/kittens.jpg" 21 | ++ bar (Color.hex "#0096c7") "img/puppies.jpg" 22 | in 23 | with_reporters layout @@ fun a b c d -> 24 | let pick_random () = 25 | match Random.int 100 with 26 | | n when n < 19 -> a 27 | | n when n < 58 -> b 28 | | n when n < 74 -> c 29 | | _ -> d 30 | in 31 | let random_progress () = Random.int64 1_000_000L in 32 | for i = 1 to 13_000 do 33 | if i mod 100 = 0 then Logs.info (fun f -> f "Iterations reached: %d" i); 34 | (pick_random ()) (random_progress ()); 35 | Unix.sleepf 0.001 36 | done 37 | 38 | let run () = 39 | let () = 40 | (* Run with [VERBOSE=true] to see log entries being interleaved with 41 | progress bar rendering. *) 42 | match Sys.getenv_opt "VERBOSE" with 43 | | None | Some "false" -> () 44 | | Some _ -> 45 | (* Configure a [Logs] reporter that behaves properly with concurrent 46 | progress bar rendering. *) 47 | Logs.set_reporter (Progress.logs_reporter ()); 48 | Logs.set_level (Some Info) 49 | in 50 | main () 51 | -------------------------------------------------------------------------------- /examples/readme.mli: -------------------------------------------------------------------------------- 1 | val run : unit -> unit 2 | -------------------------------------------------------------------------------- /examples/spinners.ml: -------------------------------------------------------------------------------- 1 | open Progress 2 | module Ansi = Terminal.Style 3 | 4 | let apply_color color s = Ansi.(code color) ^ s ^ Ansi.(code none) 5 | 6 | let pick_colour = 7 | let i = ref 0 in 8 | let colours = [| `magenta; `blue; `cyan; `green; `yellow; `red |] in 9 | fun () -> 10 | i := (!i + 1) mod Array.length colours; 11 | Color.ansi colours.(!i) 12 | 13 | (** Examples taken from: https://github.com/sindresorhus/cli-spinners/ *) 14 | 15 | include struct 16 | let spin frames min_interval = Line.spinner ~color:(pick_colour ()) ~frames ~min_interval () 17 | 18 | let dots1 = spin [ "⠋"; "⠙"; "⠹"; "⠸"; "⠼"; "⠴"; "⠦"; "⠧"; "⠇"; "⠏" ] 19 | let dots2 = spin [ "⣾"; "⣽"; "⣻"; "⢿"; "⡿"; "⣟"; "⣯"; "⣷" ] 20 | let dots3 = spin [ "⠋"; "⠙"; "⠚"; "⠞"; "⠖"; "⠦"; "⠴"; "⠲"; "⠳"; "⠓" ] 21 | let dots4 = spin [ "⠄"; "⠆"; "⠇"; "⠋"; "⠙"; "⠸"; "⠰"; "⠠"; "⠰"; "⠸"; "⠙"; "⠋"; "⠇"; "⠆" ] 22 | let dots5 = spin [ "⠋"; "⠙"; "⠚"; "⠒"; "⠂"; "⠂"; "⠒"; "⠲"; "⠴"; "⠦"; "⠖"; "⠒"; "⠐"; "⠐"; "⠒"; "⠓"; "⠋" ] 23 | let dots6 = spin [ "⠁"; "⠉"; "⠙"; "⠚"; "⠒"; "⠂"; "⠂"; "⠒"; "⠲"; "⠴"; "⠤"; "⠄"; "⠄"; "⠤"; "⠴"; "⠲"; "⠒"; "⠂"; "⠂"; "⠒"; "⠚"; "⠙"; "⠉"; "⠁" ] 24 | let dots7 = spin [ "⠈"; "⠉"; "⠋"; "⠓"; "⠒"; "⠐"; "⠐"; "⠒"; "⠖"; "⠦"; "⠤"; "⠠"; "⠠"; "⠤"; "⠦"; "⠖"; "⠒"; "⠐"; "⠐"; "⠒"; "⠓"; "⠋"; "⠉"; "⠈" ] 25 | let dots8 = spin [ "⢹"; "⢺"; "⢼"; "⣸"; "⣇"; "⡧"; "⡗"; "⡏" ] 26 | let dots9 = spin [ "⠁"; "⠂"; "⠄"; "⡀"; "⢀"; "⠠"; "⠐"; "⠈" ] 27 | let pointer = spin [ "←"; "↖"; "↑"; "↗"; "→"; "↘"; "↓"; "↙" ] 28 | let chevron = spin [ "▹▹▹▹▹"; "▸▹▹▹▹"; "▹▸▹▹▹"; "▹▹▸▹▹"; "▹▹▹▸▹"; "▹▹▹▹▸" ] 29 | let hamburger = spin [ "☱"; "☲"; "☴" ] 30 | let grow_vert = spin [ " "; "▁"; "▂"; "▃"; "▄"; "▅"; "▆"; "▇"; "█"; "▇"; "▆"; "▅"; "▄"; "▃"; "▂"; "▁" ] 31 | let grow_hori = spin [ "▏"; "▎"; "▍"; "▌"; "▋"; "▊"; "▉"; "▊"; "▋"; "▌"; "▍"; "▎" ] 32 | let moon = spin [ "🌑"; "🌒"; "🌓"; "🌔"; "🌕"; "🌖"; "🌗"; "🌘"; "🌑"; "🌒"; "🌓"; "🌔"; "🌕"; "🌖"; "🌗"; "🌘" ] 33 | let earth = spin [ "🌍 "; "🌎 "; "🌏 " ] 34 | let clock = spin [ "🕛"; "🕚"; "🕙"; "🕘"; "🕗"; "🕖"; "🕕"; "🕔"; "🕓"; "🕒"; "🕑"; "🕐"] 35 | let toggle = spin [ "⊶"; "⊷" ] 36 | let triangle = spin [ "◢"; "◣"; "◤"; "◥" ] 37 | 38 | let bouncing_bar = 39 | spin 40 | [ "[ ]" 41 | ; "[= ]" 42 | ; "[== ]" 43 | ; "[=== ]" 44 | ; "[ ===]" 45 | ; "[ ==]" 46 | ; "[ =]" 47 | ; "[ ]" 48 | ; "[ =]" 49 | ; "[ ==]" 50 | ; "[ ===]" 51 | ; "[====]" 52 | ; "[=== ]" 53 | ; "[== ]" 54 | ; "[= ]" 55 | ] 56 | end 57 | [@@ocamlformat "disable"] 58 | 59 | let unlimited_bar min_interval = 60 | let frames = 61 | let width = 6 in 62 | List.init width (fun i -> 63 | String.concat "" 64 | (List.init width (fun x -> 65 | if x = i then apply_color (Ansi.fg @@ Color.ansi `cyan) ">" 66 | else apply_color Ansi.faint "-"))) 67 | in 68 | let spin = Line.spinner ~min_interval ~frames () in 69 | Line.(const "[" ++ spin ++ spin ++ spin ++ spin ++ spin ++ const "]") 70 | 71 | let run () = 72 | let spinners = 73 | [ ("dots1", dots1, 80) 74 | ; ("dots2", dots2, 80) 75 | ; ("dots3", dots3, 80) 76 | ; ("dots4", dots4, 80) 77 | ; ("dots5", dots5, 80) 78 | ; ("dots6", dots6, 80) 79 | ; ("dots7", dots7, 80) 80 | ; ("dots8", dots8, 80) 81 | ; ("dots9", dots9, 80) 82 | ; ("pointer", pointer, 80) 83 | ; ("chevron", chevron, 80) 84 | ; ("hamburger", hamburger, 100) 85 | ; ("grow vertical", grow_vert, 80) 86 | ; ("grow horizontal", grow_hori, 120) 87 | ; ("earth", earth, 180) 88 | ; ("moon", moon, 100) 89 | ; ("clock", clock, 80) 90 | ; ("bouncing bar", bouncing_bar, 80) 91 | ; ("toggle", toggle, 250) 92 | ; ("triangle", triangle, 50) 93 | ; ("unlimited bar", unlimited_bar, 80) 94 | ] 95 | |> List.map (fun (name, elt, interval) -> 96 | let open Line in 97 | lpad 25 (constf "%s : " name) 98 | ++ elt (Some (Duration.of_int_ms interval))) 99 | in 100 | with_reporters 101 | Multi.(blank ++ lines spinners ++ line (Line.noop ())) 102 | (fun reporters _ -> 103 | let timer = Mtime_clock.counter () in 104 | let render_time = Duration.of_sec 20. in 105 | while Duration.(Mtime_clock.count timer < render_time) do 106 | List.iter (fun f -> f ()) reporters 107 | done) 108 | -------------------------------------------------------------------------------- /examples/spinners.mli: -------------------------------------------------------------------------------- 1 | val run : unit -> unit 2 | -------------------------------------------------------------------------------- /examples/utils.ml: -------------------------------------------------------------------------------- 1 | let ( .%() ) v i = Vector.get v i 2 | let ( .%()<- ) v i x = Vector.set v i x 3 | 4 | let shuffle_vector = 5 | let shuffle_subvector rand_int v i j = 6 | for k = j - 1 downto i + 1 do 7 | let l = rand_int (k + 1) in 8 | let tmp = v.%(l) in 9 | v.%(l) <- v.%(k); 10 | v.%(k) <- tmp 11 | done 12 | in 13 | fun v -> shuffle_subvector Random.int v 0 (Vector.length v) 14 | 15 | let colors = 16 | (* import matplotlib.cm 17 | for i in matplotlib.cm.rainbow(numpy.linspace(0.2, 1, 20)): 18 | print(matplotlib.colors.rgb2hex(i)) 19 | *) 20 | Array.map Progress.Color.hex 21 | [| "#1996f3"; "#06aeed"; "#10c6e6"; "#27dade"; "#3dead5" 22 | ; "#52f5cb"; "#66fcc2"; "#7dffb6"; "#92fda9"; "#a8f79c" 23 | ; "#bced8f"; "#d2de81"; "#e8cb72"; "#feb562"; "#ff9b52" 24 | ; "#ff8143"; "#ff6232"; "#ff4121" 25 | |] 26 | [@@ocamlformat "disable"] 27 | 28 | let colour_picker () = 29 | let count = ref (-1) in 30 | fun () -> 31 | count := succ !count mod Array.length colors; 32 | colors.(!count) 33 | -------------------------------------------------------------------------------- /examples/utils.mli: -------------------------------------------------------------------------------- 1 | val shuffle_vector : _ Vector.t -> unit 2 | val colour_picker : unit -> unit -> Progress.Color.t 3 | -------------------------------------------------------------------------------- /examples/yarn.ml: -------------------------------------------------------------------------------- 1 | (* This example ports the following example from Indicatif (Rust progress bar 2 | library) to use Progress instead: 3 | https://github.com/mitsuhiko/indicatif/blob/556db194b5ffeb4596275f1ce1d477f300bb4626/examples/yarnish.rs *) 4 | 5 | let line_prefix ~stages = 6 | let count = ref 0 in 7 | fun ppf -> 8 | incr count; 9 | Fmt.(styled `Bold (styled `Faint string)) 10 | ppf 11 | (Fmt.str "[%d/%d]" !count stages) 12 | 13 | let config = Progress.Config.v ~persistent:false () 14 | 15 | let with_plain_bar ~total f = 16 | Progress.( 17 | with_reporter ~config 18 | Line.(list [ elapsed (); bar total; percentage_of total ]) 19 | f) 20 | 21 | let with_bars f = 22 | let bar_names = [ "alcotest"; "ctypes"; "irmin"; "fmt"; "logs" ] in 23 | let bars = 24 | ListLabels.map bar_names ~f:(fun name -> 25 | let open Progress.Line in 26 | spinner ~color:(Progress.Color.ansi `green) () 27 | ++ constf " %s: " name 28 | ++ string) 29 | |> Progress.Multi.lines 30 | in 31 | let display = Progress.Display.start ~config bars in 32 | let [ reporters ] = Progress.Display.reporters display in 33 | let a = f display reporters in 34 | Progress.Display.finalise display; 35 | a 36 | 37 | let pick_random l = 38 | let len = List.length l in 39 | fun () -> List.nth l (Random.int len) 40 | 41 | let random_action = 42 | pick_random 43 | [ "cmake ." 44 | ; "make" 45 | ; "make clean" 46 | ; "gcc foo.c -o foo" 47 | ; "gcc bar.c -o bar" 48 | ; "./helper.sh rebuild-cache" 49 | ; "make all-clean" 50 | ; "make test" 51 | ] 52 | 53 | let run () = 54 | Fmt.set_style_renderer Fmt.stdout `Ansi_tty; 55 | let line_prefix = line_prefix ~stages:4 in 56 | let deps = 1234 in 57 | let started = Mtime_clock.counter () in 58 | Fmt.pr "%t 🔍 Resolving packages ... @." line_prefix; 59 | Fmt.pr "%t 🚚 Fetching packages ... @." line_prefix; 60 | Fmt.pr "%t 🔗 Linking %d dependencies ... @." line_prefix deps; 61 | with_plain_bar ~total:deps (fun f -> 62 | for _ = 1 to deps do 63 | f 1; 64 | Unix.sleepf 0.001 65 | done); 66 | Fmt.pr "%t 📃 Building fresh packages ... @." line_prefix; 67 | with_bars (fun display reporters -> 68 | (* Give everyone something to do *) 69 | List.iter (fun f -> f (random_action ())) reporters; 70 | 71 | (* Finish a task every so often *) 72 | let random_reporter = pick_random reporters in 73 | for _ = 1 to 50 do 74 | random_reporter () (random_action ()); 75 | 76 | (* Advance the spinners while we wait *) 77 | for _ = 1 to 5 do 78 | Progress.Display.tick display; 79 | Unix.sleepf 0.05 80 | done 81 | done); 82 | Fmt.pr "✨ Done in %a@." Mtime.Span.pp (Mtime_clock.count started) 83 | -------------------------------------------------------------------------------- /examples/yarn.mli: -------------------------------------------------------------------------------- 1 | val run : unit -> unit 2 | -------------------------------------------------------------------------------- /progress.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | synopsis: "User-definable progress bars" 4 | description: """ 5 | A progress bar library for OCaml, featuring a DSL for declaratively specifying 6 | progress bar formats. Supports rendering multiple progress bars simultaneously.""" 7 | maintainer: ["Craig Ferguson "] 8 | authors: ["Craig Ferguson "] 9 | license: "MIT" 10 | homepage: "https://github.com/CraigFe/progress" 11 | doc: "https://CraigFe.github.io/progress/" 12 | bug-reports: "https://github.com/CraigFe/progress/issues" 13 | depends: [ 14 | "dune" {>= "2.7"} 15 | "ocaml" {>= "4.08.0"} 16 | "terminal" {= version} 17 | "fmt" {>= "0.8.5"} 18 | "logs" {>= "0.7.0"} 19 | "mtime" {>= "2.0.0"} 20 | "uucp" {>= "2.0.0"} 21 | "uutf" {>= "1.0.0"} 22 | "vector" 23 | "optint" {>= "0.1.0"} 24 | "alcotest" {with-test & >= "1.4.0"} 25 | "astring" {with-test} 26 | "odoc" {with-doc} 27 | ] 28 | build: [ 29 | ["dune" "subst"] {dev} 30 | [ 31 | "dune" 32 | "build" 33 | "-p" 34 | name 35 | "-j" 36 | jobs 37 | "@install" 38 | "@runtest" {with-test} 39 | "@doc" {with-doc} 40 | ] 41 | ] 42 | dev-repo: "git+https://github.com/CraigFe/progress.git" 43 | -------------------------------------------------------------------------------- /src/progress/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name progress) 3 | (public_name progress) 4 | (libraries 5 | (re_export progress.engine) 6 | (re_export terminal) 7 | mtime.clock.os 8 | fmt 9 | unix)) 10 | -------------------------------------------------------------------------------- /src/progress/engine/config.ml: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | type user_supplied = 7 | { ppf : Format.formatter option 8 | ; hide_cursor : bool option 9 | ; persistent : bool option 10 | ; max_width : int option option 11 | ; min_interval : Duration.t option option 12 | } 13 | 14 | module Default = struct 15 | (* NOTE: changes here should be reflected in the doc-strings. *) 16 | 17 | let ppf = 18 | (* We avoid using [Format.err_formatter] directly since [Fmt] uses 19 | physical equality to share configuration options. *) 20 | let ppf = Format.formatter_of_out_channel stderr in 21 | Fmt.set_style_renderer ppf `Ansi_tty; 22 | Fmt.set_utf_8 ppf true; 23 | ppf 24 | 25 | let hide_cursor = true 26 | let persistent = true 27 | let max_width = None 28 | let min_interval = Some (Duration.of_sec (1. /. 60.)) 29 | end 30 | 31 | (* Boilerplate from here onwards. Someday I'll write a PPX for this... *) 32 | 33 | let v ?ppf ?hide_cursor ?persistent ?max_width ?min_interval () = 34 | { ppf; hide_cursor; persistent; max_width; min_interval } 35 | 36 | (* Merge two ['a option]s with a left [Some] taking priority *) 37 | let merge_on ~f a b = match (f a, f b) with Some a, _ -> Some a | None, b -> b 38 | 39 | let ( || ) a b = 40 | { ppf = merge_on a b ~f:(fun x -> x.ppf) 41 | ; hide_cursor = merge_on a b ~f:(fun x -> x.hide_cursor) 42 | ; persistent = merge_on a b ~f:(fun x -> x.persistent) 43 | ; max_width = merge_on a b ~f:(fun x -> x.max_width) 44 | ; min_interval = merge_on a b ~f:(fun x -> x.min_interval) 45 | } 46 | 47 | type t = 48 | { ppf : Format.formatter 49 | ; hide_cursor : bool 50 | ; persistent : bool 51 | ; max_width : int option 52 | ; min_interval : Duration.t option 53 | } 54 | 55 | let apply_defaults : user_supplied -> t = 56 | fun { ppf; hide_cursor; persistent; max_width; min_interval } -> 57 | { ppf = Option.value ppf ~default:Default.ppf 58 | ; hide_cursor = Option.value hide_cursor ~default:Default.hide_cursor 59 | ; persistent = Option.value persistent ~default:Default.persistent 60 | ; max_width = Option.value max_width ~default:Default.max_width 61 | ; min_interval = Option.value min_interval ~default:Default.min_interval 62 | } 63 | 64 | (*———————————————————————————————————————————————————————————————————————————— 65 | Copyright (c) 2020–2021 Craig Ferguson 66 | 67 | Permission to use, copy, modify, and/or distribute this software for any 68 | purpose with or without fee is hereby granted, provided that the above 69 | copyright notice and this permission notice appear in all copies. 70 | 71 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 72 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 73 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 74 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 75 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 76 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 77 | DEALINGS IN THE SOFTWARE. 78 | ————————————————————————————————————————————————————————————————————————————*) 79 | -------------------------------------------------------------------------------- /src/progress/engine/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name progress_engine) 3 | (public_name progress.engine) 4 | (libraries 5 | (re_export mtime) 6 | (re_export optint) 7 | fmt 8 | logs 9 | logs.fmt 10 | terminal_ansi 11 | vector)) 12 | -------------------------------------------------------------------------------- /src/progress/engine/duration.ml: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | open Mtime.Span 7 | 8 | let ( ** ) = Int64.mul 9 | 10 | type nonrec t = t 11 | 12 | let equal = Mtime.Span.equal 13 | let compare = Mtime.Span.compare 14 | let compare_zero (f : int -> int -> _) a b = f (compare a b) 0 15 | let ( + ) = Mtime.Span.add 16 | let ( < ) = compare_zero ( < ) 17 | let ( <= ) = compare_zero ( <= ) 18 | let ( = ) = compare_zero ( = ) 19 | let ( >= ) = compare_zero ( >= ) 20 | let ( > ) = compare_zero ( > ) 21 | let v = of_uint64_ns 22 | let zero = v 0L 23 | let nanosecond = v 1L 24 | let microsecond = v 1_000L 25 | let millisecond = v 1_000_000L 26 | let second = v 1_000_000_000L 27 | let minute = v (60L ** 1_000_000_000L) 28 | let hour = v (60L ** 60L ** 1_000_000_000_000L) 29 | let day = v (24L ** 60L ** 60L ** 1_000_000_000_000L) 30 | let of_ns x = v (Int64.of_float x) 31 | let of_us x = v (Int64.of_float (x *. 1e3)) 32 | let of_ms x = v (Int64.of_float (x *. 1e6)) 33 | let of_sec x = v (Int64.of_float (x *. 1e9)) 34 | let of_int64_ms x = v (x ** 1_000_000L) 35 | let of_int_ms x = of_int64_ms (Int64.of_int x) 36 | let of_int64_sec x = v (x ** 1_000_000_000L) 37 | let of_int_sec x = of_int64_sec (Int64.of_int x) 38 | 39 | let of_min = 40 | let f = 60. *. 1e9 in 41 | fun x -> v (Int64.of_float (x *. f)) 42 | 43 | let of_int64_min = 44 | let f = 60L ** 1_000_000_000L in 45 | fun x -> v (x ** f) 46 | 47 | let of_int_min x = of_int64_min (Int64.of_int x) 48 | 49 | let of_hour = 50 | let f = 60. *. 60. *. 1e9 in 51 | fun x -> v (Int64.of_float (x *. f)) 52 | 53 | let of_int64_hour = 54 | let f = 60L ** 60L ** 1_000_000_000L in 55 | fun x -> v (x ** f) 56 | 57 | let of_int_hour x = of_int64_hour (Int64.of_int x) 58 | 59 | let of_day = 60 | let f = 24. *. 60. *. 60. *. 1e9 in 61 | fun x -> v (Int64.of_float (x *. f)) 62 | 63 | module Of_int = struct 64 | let ms = of_int_ms 65 | let sec = of_int_sec 66 | let min = of_int_min 67 | let hour = of_int_hour 68 | let ( + ) = ( + ) 69 | end 70 | 71 | (*———————————————————————————————————————————————————————————————————————————— 72 | Copyright (c) 2020–2021 Craig Ferguson 73 | 74 | Permission to use, copy, modify, and/or distribute this software for any 75 | purpose with or without fee is hereby granted, provided that the above 76 | copyright notice and this permission notice appear in all copies. 77 | 78 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 79 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 80 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 81 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 82 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 83 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 84 | DEALINGS IN THE SOFTWARE. 85 | ————————————————————————————————————————————————————————————————————————————*) 86 | -------------------------------------------------------------------------------- /src/progress/engine/duration.mli: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | type t = Mtime.Span.t 7 | (** The type of time durations. This module is just a convenience wrapper around 8 | {!Mtime.Span}.*) 9 | 10 | val equal : t -> t -> bool 11 | val compare : t -> t -> int 12 | 13 | (** {1 Round values} *) 14 | 15 | val zero : t 16 | val nanosecond : t 17 | val microsecond : t 18 | val millisecond : t 19 | val second : t 20 | val minute : t 21 | val hour : t 22 | val day : t 23 | 24 | (** {1 Convertors} *) 25 | 26 | val of_ns : float -> t 27 | val of_us : float -> t 28 | val of_ms : float -> t 29 | val of_sec : float -> t 30 | val of_min : float -> t 31 | val of_hour : float -> t 32 | val of_day : float -> t 33 | 34 | (** {2 From integers} *) 35 | 36 | val of_int_ms : int -> t 37 | val of_int_sec : int -> t 38 | val of_int_min : int -> t 39 | val of_int_hour : int -> t 40 | val of_int64_ms : int64 -> t 41 | val of_int64_sec : int64 -> t 42 | 43 | module Of_int : sig 44 | val ms : int -> t 45 | val sec : int -> t 46 | val min : int -> t 47 | val hour : int -> t 48 | val ( + ) : t -> t -> t 49 | end 50 | 51 | (** {2 Infix operators} *) 52 | 53 | val ( + ) : t -> t -> t 54 | val ( < ) : t -> t -> bool 55 | val ( <= ) : t -> t -> bool 56 | val ( = ) : t -> t -> bool 57 | val ( >= ) : t -> t -> bool 58 | val ( > ) : t -> t -> bool 59 | 60 | (*———————————————————————————————————————————————————————————————————————————— 61 | Copyright (c) 2020–2021 Craig Ferguson 62 | 63 | Permission to use, copy, modify, and/or distribute this software for any 64 | purpose with or without fee is hereby granted, provided that the above 65 | copyright notice and this permission notice appear in all copies. 66 | 67 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 68 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 69 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 70 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 71 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 72 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 73 | DEALINGS IN THE SOFTWARE. 74 | ————————————————————————————————————————————————————————————————————————————*) 75 | -------------------------------------------------------------------------------- /src/progress/engine/flow_meter.ml: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | open! Import 6 | 7 | type 'a t = 8 | { data : 'a array 9 | ; timestamps : Mtime.t array 10 | ; max_length : int 11 | ; mutable most_recently_added : int 12 | ; mutable length : int 13 | ; get_time : unit -> Mtime.t 14 | ; elt : (module Integer.S with type t = 'a) 15 | } 16 | 17 | let create (type a) ~clock:get_time ~size ~elt : a t = 18 | if size <= 0 then 19 | Fmt.invalid_arg "Flow_meter.create: non-positive size %d" size; 20 | (* We need [n + 1] timestamp samples to integrate over [n] values. *) 21 | let max_length = size + 1 in 22 | let start_time = get_time () in 23 | { get_time 24 | ; data = Array.make max_length (Obj.magic None) 25 | ; timestamps = Array.make max_length start_time 26 | ; max_length 27 | ; most_recently_added = -1 28 | ; length = 0 29 | ; elt 30 | } 31 | 32 | let is_empty t = t.most_recently_added = -1 33 | 34 | let push t ~key ~data = 35 | t.data.(key) <- data; 36 | t.timestamps.(key) <- t.get_time () 37 | 38 | let record t data = 39 | if t.length = t.max_length then ( 40 | (* Buffer is full. Overwrite the oldest value. *) 41 | let next = (t.most_recently_added + 1) mod t.length in 42 | push t ~key:next ~data; 43 | t.most_recently_added <- next) 44 | else ( 45 | (* Increase the buffer size *) 46 | push t ~key:t.length ~data; 47 | t.most_recently_added <- t.length; 48 | t.length <- succ t.length) 49 | 50 | let oldest_index t = 51 | if t.length = t.max_length then (t.most_recently_added + 1) mod t.length 52 | else 0 53 | 54 | let fold = 55 | let rec aux data f acc = function 56 | | -1 -> acc 57 | | n -> aux data f (f acc data.(n)) (n - 1) 58 | in 59 | fun t ~f ~init -> aux t.data f init (t.length - 1) 60 | 61 | let per_second : type a. a t -> float = 62 | fun t -> 63 | let (module Integer) = t.elt in 64 | if is_empty t then Float.zero 65 | else 66 | (* Sum all values in the window {i except the first one} and divide by the 67 | time interval. We can think of the first value as representing work done 68 | just {i before} the time interval starts, so using a half-open sample 69 | correctly avoids over-reporting the flow-rate. *) 70 | let oldest_index = oldest_index t in 71 | let sum = 72 | Integer.sub 73 | (fold t ~f:Integer.add ~init:Integer.zero) 74 | t.data.(oldest_index) 75 | in 76 | let interval = 77 | let start_time = t.timestamps.(oldest_index) in 78 | let end_time = t.timestamps.(t.most_recently_added) in 79 | Mtime.span_to_s (Mtime.span start_time end_time) 80 | in 81 | if Float.compare interval Float.epsilon < 0 then Float.zero 82 | else Integer.to_float sum /. interval 83 | 84 | (*———————————————————————————————————————————————————————————————————————————— 85 | Copyright (c) 2020–2021 Craig Ferguson 86 | 87 | Permission to use, copy, modify, and/or distribute this software for any 88 | purpose with or without fee is hereby granted, provided that the above 89 | copyright notice and this permission notice appear in all copies. 90 | 91 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 92 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 93 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 94 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 95 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 96 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 97 | DEALINGS IN THE SOFTWARE. 98 | ————————————————————————————————————————————————————————————————————————————*) 99 | -------------------------------------------------------------------------------- /src/progress/engine/flow_meter.mli: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | type 'a t 7 | (** The type of {i flow meters} for some metric: values that compute an online 8 | windowed integral of a discrete sequence of [(value, time)] samples. This is 9 | useful for e.g. estimating the download rate of a process given a sequence 10 | of progress updates. *) 11 | 12 | val create : 13 | clock:(unit -> Mtime.t) 14 | -> size:int 15 | -> elt:(module Integer.S with type t = 'a) 16 | -> 'a t 17 | (** [create ~clock ~size ~elt] is a flow meter for values of type [elt], using a 18 | window size of [size] and the [clock] function for collecting timestamps for 19 | recorded values. *) 20 | 21 | val record : 'a t -> 'a -> unit 22 | (** Add a value to the ring buffer. *) 23 | 24 | val per_second : 'a t -> float 25 | (** Estimate the rate of change of recorded values per second. *) 26 | 27 | (*———————————————————————————————————————————————————————————————————————————— 28 | Copyright (c) 2020–2021 Craig Ferguson 29 | 30 | Permission to use, copy, modify, and/or distribute this software for any 31 | purpose with or without fee is hereby granted, provided that the above 32 | copyright notice and this permission notice appear in all copies. 33 | 34 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 35 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 36 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 37 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 38 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 39 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 40 | DEALINGS IN THE SOFTWARE. 41 | ————————————————————————————————————————————————————————————————————————————*) 42 | -------------------------------------------------------------------------------- /src/progress/engine/import.ml: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | include Stdlib_ext 7 | module Terminal = Terminal_ansi 8 | 9 | module Mtime = struct 10 | include Mtime 11 | 12 | let span_to_s span = Mtime.Span.to_float_ns span *. 1e-9 13 | end 14 | 15 | module Vector = struct 16 | include Vector 17 | 18 | let iter ~f t = iter f t 19 | 20 | let iteri_from ~f i t = 21 | for i = i to length t - 1 do 22 | f i (unsafe_get t i) 23 | done 24 | 25 | let rec find_map_from i t ~f = 26 | if i >= length t then None 27 | else 28 | let a = unsafe_get t i in 29 | match f a with 30 | | Some _ as some -> some 31 | | None -> find_map_from (i + 1) t ~f 32 | 33 | let find_map t ~f = find_map_from 0 t ~f 34 | 35 | let insert t k v = 36 | Vector.push t v (* Dummy insertion to expand *); 37 | for i = Vector.length t - 1 downto k + 1 do 38 | Vector.set t i (Vector.get t (pred i)) 39 | done; 40 | Vector.set t k v 41 | 42 | let remove (type a) (t : a t) k = 43 | for i = k to Vector.length t - 2 do 44 | Vector.set t i (Vector.get t (succ i)) 45 | done; 46 | ignore (Vector.pop t : a) 47 | 48 | let get_exn = get 49 | let get = `shadowed 50 | end 51 | (*———————————————————————————————————————————————————————————————————————————— 52 | Copyright (c) 2020–2021 Craig Ferguson 53 | 54 | Permission to use, copy, modify, and/or distribute this software for any 55 | purpose with or without fee is hereby granted, provided that the above 56 | copyright notice and this permission notice appear in all copies. 57 | 58 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 59 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 60 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 61 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 62 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 63 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 64 | DEALINGS IN THE SOFTWARE. 65 | ————————————————————————————————————————————————————————————————————————————*) 66 | -------------------------------------------------------------------------------- /src/progress/engine/integer.ml: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | open! Import 7 | 8 | module type S = sig 9 | type t 10 | 11 | val zero : t 12 | val one : t 13 | val add : t -> t -> t 14 | val sub : t -> t -> t 15 | val equal : t -> t -> bool 16 | val to_string : t -> string 17 | val to_float : t -> float 18 | val of_float : float -> t 19 | end 20 | 21 | module Int : S with type t = int = Int 22 | module Int32 : S with type t = int32 = Int32 23 | module Int64 : S with type t = int64 = Int64 24 | module Int63 : S with type t = int63 = Int63 25 | module Float : S with type t = float = Float 26 | 27 | (*———————————————————————————————————————————————————————————————————————————— 28 | Copyright (c) 2020–2021 Craig Ferguson 29 | 30 | Permission to use, copy, modify, and/or distribute this software for any 31 | purpose with or without fee is hereby granted, provided that the above 32 | copyright notice and this permission notice appear in all copies. 33 | 34 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 35 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 36 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 37 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 38 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 39 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 40 | DEALINGS IN THE SOFTWARE. 41 | ————————————————————————————————————————————————————————————————————————————*) 42 | -------------------------------------------------------------------------------- /src/progress/engine/line.mli: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | include Line_intf.Line 7 | (** @inline *) 8 | 9 | (*———————————————————————————————————————————————————————————————————————————— 10 | Copyright (c) 2020–2021 Craig Ferguson 11 | 12 | Permission to use, copy, modify, and/or distribute this software for any 13 | purpose with or without fee is hereby granted, provided that the above 14 | copyright notice and this permission notice appear in all copies. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 19 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 22 | DEALINGS IN THE SOFTWARE. 23 | ————————————————————————————————————————————————————————————————————————————*) 24 | -------------------------------------------------------------------------------- /src/progress/engine/line_buffer.ml: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | open! Import 7 | 8 | external unsafe_blit_string : string -> int -> bytes -> int -> int -> unit 9 | = "caml_blit_string" 10 | [@@noalloc] 11 | (** Polyfill for pre-4.09.0 *) 12 | 13 | type t = 14 | { mutable buffer : bytes 15 | ; mutable position : int 16 | ; mutable length : int 17 | ; mutable last : string 18 | (** Cache latest delivered contents to avoid unnecessary re-rendering *) 19 | ; mutable last_len : int (** Avoids some string comparisons on [last] *) 20 | ; ppf : Format.formatter Lazy.t 21 | } 22 | (** Invariants: 23 | 24 | - [0 <= position <= length] 25 | - [length = Bytes.length buffer] *) 26 | 27 | let resize t more = 28 | let old_pos = t.position and old_len = t.length in 29 | let new_len = 30 | let res = ref old_len in 31 | while old_pos + more > !res do 32 | res := 2 * !res 33 | done; 34 | !res 35 | in 36 | let new_buffer = Bytes.create new_len in 37 | Bytes.blit ~src:t.buffer ~src_pos:0 ~dst:new_buffer ~dst_pos:0 ~len:t.position; 38 | t.buffer <- new_buffer; 39 | t.length <- new_len 40 | 41 | let advance t len = 42 | let new_position = t.position + len in 43 | if new_position > t.length then resize t len; 44 | (* Fmt.pr "[%d -> %d]" t.position new_position; *) 45 | t.position <- new_position 46 | 47 | let lift_write ~len ~write = 48 | Staged.inj (fun t x -> 49 | let position = t.position in 50 | advance t len; 51 | write x ~into:t.buffer ~pos:position) 52 | 53 | let add_char b c = 54 | let pos = b.position in 55 | if pos >= b.length then resize b 1; 56 | Bytes.unsafe_set b.buffer pos c; 57 | b.position <- pos + 1 58 | 59 | let add_substring t s ~off ~len = 60 | if off < 0 || len < 0 || off > String.length s - len then 61 | invalid_arg "Line_buffer.add_substring"; 62 | let position = t.position in 63 | advance t len; 64 | unsafe_blit_string s off t.buffer position len 65 | 66 | let add_string b s = 67 | let len = String.length s in 68 | let new_position = b.position + len in 69 | if new_position > b.length then resize b len; 70 | unsafe_blit_string s 0 b.buffer b.position len; 71 | b.position <- new_position 72 | 73 | let add_line_buffer ~dst ~src = 74 | let position = dst.position in 75 | let len = src.position in 76 | advance dst len; 77 | Bytes.unsafe_blit ~src:src.buffer ~src_pos:0 ~dst:dst.buffer ~dst_pos:position 78 | ~len 79 | 80 | let create ~size = 81 | let buffer = Bytes.create size in 82 | let rec ppf = 83 | lazy 84 | (let ppf = 85 | Format.make_formatter 86 | (fun s off len -> add_substring t s ~off ~len) 87 | (fun () -> ()) 88 | in 89 | Fmt.set_style_renderer ppf `Ansi_tty; 90 | ppf) 91 | and t = 92 | { buffer; position = 0; length = size; ppf; last = ""; last_len = 0 } 93 | in 94 | t 95 | 96 | let with_ppf t f = 97 | let ppf = Lazy.force t.ppf in 98 | let a = f ppf in 99 | Format.pp_print_flush ppf (); 100 | a 101 | 102 | let reset t = t.position <- 0 103 | 104 | let contents t = 105 | let last = t.last in 106 | let last_len = t.last_len in 107 | let current_len = t.position in 108 | (* NOTE: Without an efficient substring equality function, we have no choice 109 | but to copy here even if the buffer is clean... *) 110 | let current = Bytes.sub_string t.buffer ~pos:0 ~len:current_len in 111 | reset t; 112 | 113 | match Int.equal last_len current_len && String.equal last current with 114 | | true -> `Clean t.last 115 | | false -> 116 | t.last <- current; 117 | t.last_len <- current_len; 118 | `Dirty current 119 | 120 | type mark = int 121 | 122 | let current_position t = t.position 123 | 124 | module Span = struct 125 | type t = { pos : int; len : int } 126 | 127 | let pp ppf t = Fmt.pf ppf "{ pos = %d; len = %d }" t.pos t.len 128 | let empty = { pos = 0; len = 0 } 129 | let between_marks a b = { pos = a; len = b - a } 130 | end 131 | 132 | let skip t (span : Span.t) = 133 | (* XXX: this can cause spurious failures when zooming the terminal, so for the 134 | moment we don't validate positions whatsoever. *) 135 | (* if t.position <> span.pos then 136 | * Fmt.failwith "Misaligned span %a inside line buffer at position %d" Span.pp 137 | * span t.position; *) 138 | advance t span.len 139 | 140 | (*———————————————————————————————————————————————————————————————————————————— 141 | Copyright (c) 2020–2021 Craig Ferguson 142 | 143 | Permission to use, copy, modify, and/or distribute this software for any 144 | purpose with or without fee is hereby granted, provided that the above 145 | copyright notice and this permission notice appear in all copies. 146 | 147 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 148 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 149 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 150 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 151 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 152 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 153 | DEALINGS IN THE SOFTWARE. 154 | ————————————————————————————————————————————————————————————————————————————*) 155 | -------------------------------------------------------------------------------- /src/progress/engine/line_buffer.mli: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | open! Import 7 | 8 | type t 9 | (** A line buffer is a variant of [Stdlib.Buffer] that supports {i skipping} 10 | some section of the underlying bytestring when doing a write pass. *) 11 | 12 | val create : size:int -> t 13 | (** Create a line buffer with the given initial size. *) 14 | 15 | val with_ppf : t -> (Format.formatter -> 'a) -> 'a 16 | (** [with_ppf buf f] gives a view of [buf] as a formatter to [f] (and then 17 | flushes the formatter to [buf]). *) 18 | 19 | val add_char : t -> char -> unit 20 | val add_string : t -> string -> unit 21 | val add_substring : t -> string -> off:int -> len:int -> unit 22 | val add_line_buffer : dst:t -> src:t -> unit 23 | 24 | val lift_write : 25 | len:int 26 | -> write:('a -> into:bytes -> pos:int -> unit) 27 | -> (t -> 'a -> unit) Staged.t 28 | 29 | val contents : t -> [ `Clean of string | `Dirty of string ] 30 | (** Reset the write head to the start of the buffer and return a copy of the 31 | intervening contents. *) 32 | 33 | val reset : t -> unit 34 | 35 | type mark 36 | 37 | val current_position : t -> mark 38 | (** Get a mark of the current write head in the buffer. *) 39 | 40 | module Span : sig 41 | type t 42 | 43 | val empty : t 44 | val between_marks : mark -> mark -> t 45 | val pp : Format.formatter -> t -> unit 46 | end 47 | 48 | val skip : t -> Span.t -> unit 49 | (** Advance over a given span in the buffer. *) 50 | 51 | (*———————————————————————————————————————————————————————————————————————————— 52 | Copyright (c) 2020–2021 Craig Ferguson 53 | 54 | Permission to use, copy, modify, and/or distribute this software for any 55 | purpose with or without fee is hereby granted, provided that the above 56 | copyright notice and this permission notice appear in all copies. 57 | 58 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 59 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 60 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 61 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 62 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 63 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 64 | DEALINGS IN THE SOFTWARE. 65 | ————————————————————————————————————————————————————————————————————————————*) 66 | -------------------------------------------------------------------------------- /src/progress/engine/line_intf.ml: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | open! Import 7 | 8 | (** These values are documented as part of {!DSL}.*) 9 | module type Integer_dependent = sig 10 | type integer 11 | type color 12 | type duration 13 | type 'a printer 14 | type bar_style 15 | 16 | (**) 17 | type 'a t 18 | 19 | val sum : ?pp:integer printer -> width:int -> unit -> integer t 20 | val count_to : ?pp:integer printer -> ?sep:unit t -> integer -> integer t 21 | val bytes : integer t 22 | val bytes_per_sec : integer t 23 | val percentage_of : integer -> integer t 24 | val rate : float printer -> integer t 25 | val eta : ?pp:duration printer -> integer -> integer t 26 | 27 | type bar_style := [ `ASCII | `UTF8 | `Custom of bar_style ] 28 | 29 | val bar : 30 | ?style:bar_style 31 | -> ?color:color 32 | -> ?width:[ `Fixed of int | `Expand ] 33 | -> ?data:[ `Sum | `Latest ] 34 | -> integer 35 | -> integer t 36 | end 37 | 38 | (** The 'main' set of combinators, specialised to a particular integer type. *) 39 | module type DSL = sig 40 | type integer 41 | type color 42 | type duration 43 | type 'a printer 44 | 45 | type 'a t 46 | (** The type of progress lines for reported values of type ['a]. This module 47 | provides a selection of {{!basic} individual line segments} that can be 48 | {{!combinators} combined} to produce more interesting layouts. You may 49 | wish to look over the {{!examples} examples} for inspiration. *) 50 | 51 | (** {1:basic Basic line segments} *) 52 | 53 | val const : string -> _ t 54 | (** [const s] is the segment that always displays [s]. *) 55 | 56 | val constf : ('a, Format.formatter, unit, _ t) format4 -> 'a 57 | (** Like {!const}, but takes a format string and corresponding arguments. 58 | [constf "..." a b c ...] is equivalent to 59 | [const (Format.asprintf "..." a b c ...)], except that colours added with 60 | [Fmt.styled] are not discarded. *) 61 | 62 | val string : string t 63 | (** A line segment that displays a dynamically-sized string message. Use 64 | {!lpad} and {!rpad} to pad the message up to a given length. *) 65 | 66 | val lpad : int -> 'a t -> 'a t 67 | (** [lpad n t] left-pads the segment [t] to size [n] by adding blank space at 68 | the start. *) 69 | 70 | val rpad : int -> 'a t -> 'a t 71 | (** [rpad n t] right-pads the segment [t] to size [n] by adding blank space at 72 | the end. *) 73 | 74 | val of_printer : ?init:'a -> 'a printer -> 'a t 75 | (** [of_printer p] is a segment that renders the {i latest} reported value 76 | using printer [p]. See {!sum} for a variant that reports accumulated 77 | values instead. *) 78 | 79 | (** {2:counting Counting segments} 80 | 81 | These segments all consume integer values and display the accumulated 82 | total of reported values in some way. The top-level [Line] segments are 83 | specialised to [int] values; see "{!integers}" for variants supporting 84 | [int32], [int64] etc. *) 85 | 86 | val count_to : ?pp:integer printer -> ?sep:unit t -> integer -> integer t 87 | (** [count_to target] renders both the current running total of reported 88 | values and the fixed value [target], separated by the the given separator, 89 | i.e. [42/100]. [sep] defaults to [const "/"]. *) 90 | 91 | val bytes : integer t 92 | (** Prints the running total as a number of bytes, using ISO/IEC binary 93 | prefixes (e.g. [10.4 MiB]). See also {!bytes_per_sec}. *) 94 | 95 | val percentage_of : integer -> integer t 96 | (** [percentage_of target] renders the running total as a percentage of 97 | [target], i.e. [42%]. Values outside the range [[0, 100]] will be clamped 98 | to either [0] or [100]. *) 99 | 100 | val sum : ?pp:integer printer -> width:int -> unit -> integer t 101 | (** [sum ~width ()] displays a running total of reported values using 102 | [width]-many terminal columns. If passed, [pp] overrides the printer used 103 | for rendering the count. *) 104 | 105 | (** {2:graphical Graphical segments} *) 106 | 107 | module Bar_style : sig 108 | type t 109 | (** The type of progress bar style specifications. *) 110 | 111 | val ascii : t 112 | (** The style used by [bar ~style:`ASCII] (which is the default). Generates 113 | bars of the form [[######---]]. *) 114 | 115 | val utf8 : t 116 | (** {!utf8} is the style used by [bar ~style:`UTF8]. Uses the UTF-8 block 117 | element characters ([U+2588]–[U+258F]) for progress stages, and a 118 | box-drawing character ([U+2502]) for delimiters. *) 119 | 120 | (** {1 Custom styles} *) 121 | 122 | val v : 123 | ?delims:string * string 124 | -> ?color:color 125 | -> ?color_empty:color 126 | -> string list 127 | -> t 128 | 129 | (** [v stages] is a bar that uses the given string {i stages} to render 130 | progress. The first stage is interpreted as a "full" segment, with 131 | subsequent stages denoting progressively {i less}-full segments until a 132 | final "empty" stage (which is implicitly a space if only one stage is 133 | provided). 134 | 135 | The optional parameters are as follows: 136 | 137 | - [?delims]: a pair of left and right delimiters used to wrap the body 138 | of the progress bar; 139 | - [?color]: the color of non-empty segments (including the in-progress 140 | one); 141 | - [?color_empty]: the color of empty segments. 142 | 143 | {2 Examples} 144 | 145 | - [v [ "#" ]] renders like "[####### ]"; 146 | - [v [ "="; ">"; " " ]] renders like "[======> ]"; 147 | - [v [ "4"; "3"; "2"; "1"; "0" ]] renders like "[444444410000]"; 148 | - ... see [examples/bar_styles.ml] in the source repository for more. 149 | 150 | {2 Specifics} 151 | 152 | Each segment of a rendering progress bar is in one of three states: 153 | full, empty or in-progress. At any given time, either the bar is 154 | entirely full or or there is exactly one in-progress segment. Given the 155 | style [v [s1; s2; ... sN]], these states are rendered as follows: 156 | 157 | - {b full}: rendered as [s1]; 158 | - {b empty}: rendered as [sN] if [N >= 1], otherwise [' ']; 159 | - {b in-progress}: if [N <= 1], then equivalent to the empty state. 160 | Otherwise, the intermediate stages [s2], [s3], ... [s{N-1}] denote 161 | decreasing progress. For example, if there are four intermediate 162 | stages ([N = 6]) then [s2] is used for progress in the range 163 | [\[0, 25%)], [s3] for [\[25%, 50%)] etc. 164 | 165 | For the progress bar to render within a fixed size, the user must ensure 166 | that each of the [stages] must have the same rendered width. *) 167 | 168 | (** {1 Setters} *) 169 | 170 | val with_color : color -> t -> t 171 | val with_empty_color : color -> t -> t 172 | val with_delims : (string * string) option -> t -> t 173 | val with_stages : string list -> t -> t 174 | end 175 | 176 | val bar : 177 | ?style:[ `ASCII | `UTF8 | `Custom of Bar_style.t ] 178 | -> ?color:color 179 | -> ?width:[ `Fixed of int | `Expand ] 180 | -> ?data:[ `Sum | `Latest ] 181 | -> integer 182 | -> integer t 183 | (** [bar total] is a progress bar of the form: 184 | [[#################...............]] 185 | 186 | The proportion of the bar that is filled is given by 187 | [ / total]. Optional parameters are as follows: 188 | 189 | - [?style] specifies whether to use a UTF-8 or an ASCII encoding for the 190 | progress bar. The UTF-8 encoding shows a higher resolution of progress, 191 | but may not be supported in all terminals. The default is [`ASCII]. 192 | 193 | - [?color] causes the filled portion of the bar to be rendered with the 194 | given colour. (Equivalent to setting the colour with 195 | {!Bar_style.with_color}.) 196 | 197 | - [?width] is the width of the bar in columns. Defaults to [`Expand], 198 | which causes the bar to occupy the remaining rendering space after 199 | accounting for other line segments on the same line. 200 | 201 | - [?data] changes the metric that is indicated by the progress bar. [`Sum] 202 | (the default) causes the progress bar to correspond to the 203 | {i running total} of values reported so far. [`Latest] causes each 204 | reported value to overwrite the previous one instead. *) 205 | 206 | val spinner : 207 | ?frames:string list 208 | -> ?color:color 209 | -> ?min_interval:duration option 210 | -> unit 211 | -> _ t 212 | (** [spinner ()] is a small segment that cycles over a fixed number of frames 213 | each time a value is reported. e.g. 214 | 215 | {[ 216 | ⠋ → ⠙ → ⠹ → ⠸ → ⠼ → ⠴ → ⠦ → ⠧ → ⠇ → ⠏ → ... 217 | ]} 218 | 219 | Optional prameters are as follows: 220 | 221 | - [?frames] alters the sequence of frames rendered by the spinner; 222 | - [?color] causes each frame to be rendered with the given colour; 223 | - [?min_interval] is the minimum time interval between frame transitions 224 | of the spinner (i.e. a debounce threshold). The default is [Some 80ms]. 225 | *) 226 | 227 | (** {2:time Time-sensitive segments} *) 228 | 229 | val bytes_per_sec : integer t 230 | (** [bytes_per_sec] renders the rate of change of the running total as a 231 | number of bytes per second, using ISO/IEC binary prefixes (e.g. 232 | [10.4 MiB/s]). *) 233 | 234 | val elapsed : ?pp:duration printer -> unit -> _ t 235 | (** Displays the time for which the bar has been rendering in [MM:SS] form. *) 236 | 237 | val eta : ?pp:duration printer -> integer -> integer t 238 | (** Displays an estimate of the remaining time until [total] is accumulated by 239 | the reporters, in [MM:SS] form. *) 240 | 241 | val rate : float printer -> integer t 242 | (** [rate pp] is an integer segment that uses [pp] to print the {i rate} of 243 | reported values per second. (For instance, [bytes_per_sec] is 244 | [rate Units.Bytes.of_float].)*) 245 | 246 | (** {1:combinators Combining segments} *) 247 | 248 | val ( ++ ) : 'a t -> 'a t -> 'a t 249 | (** Horizontally join two segments of the same reported value type. *) 250 | 251 | val list : ?sep:'a t -> 'a t list -> 'a t 252 | (** Horizontally join a list of segments, with a given separator. [sep] 253 | defaults to [const " "]. *) 254 | 255 | val pair : ?sep:unit t -> 'a t -> 'b t -> ('a * 'b) t 256 | (** Horizontally join a pair of segments consuming different reported values 257 | into a single segment that consumes a pair. *) 258 | 259 | val using : ('a -> 'b) -> 'b t -> 'a t 260 | (** [using f s] is a segment that first applies [f] to the reported value and 261 | then behaves as segment [s]. *) 262 | 263 | (** {1 Utilities} 264 | 265 | The following line segments are definable in terms of the others, but 266 | provided for convenience: *) 267 | 268 | val parens : 'a t -> 'a t 269 | (** [parens t] is [const "(" ++ t ++ const ")"]. *) 270 | 271 | val brackets : 'a t -> 'a t 272 | (** [brackets t] is [const "[" ++ t ++ const "]"]. *) 273 | 274 | val braces : 'a t -> 'a t 275 | (** [braces t] is [const "{" ++ t ++ const "}"]. *) 276 | 277 | val noop : unit -> _ t 278 | (** A zero-width line segment that does nothing. This segment will not be 279 | surrounded with separators when used in a {!list}, making it a useful 280 | "off" state for conditionally-enabled segments. *) 281 | 282 | val spacer : int -> _ t 283 | (** [spacer n] is [const (String.make n ' ')]. *) 284 | 285 | val ticker_to : ?sep:unit t -> integer -> _ t 286 | (** [ticker_to total] is [using ~f:(fun _ -> 1) (counter_to total)]. i.e. it 287 | renders the total {i number} of reported values of some arbitrary type. *) 288 | end 289 | 290 | module Assert_subtype (X : DSL) : 291 | Integer_dependent with type bar_style := X.Bar_style.t = 292 | X 293 | 294 | module type S = sig 295 | include DSL with type integer := int 296 | (** @inline *) 297 | 298 | (** {1:integers Alternative integer types} 299 | 300 | Many of the line segments above are specialised to [int] values for 301 | simplicity (and performance), but certain use-cases may require different 302 | types (e.g. for file transfers greater than [2 GiB] on 32-bit platforms). 303 | The following modules re-export the [Line] DSL with different integer 304 | specialisations, and are intended to be opened locally, e.g. 305 | 306 | {[ 307 | let my_line = 308 | let open Progress.Line.Using_int64 in 309 | list [ const "Downloading large file"; bar total; bytes ] 310 | ]} *) 311 | 312 | module Integer_dependent : sig 313 | (** {!S} contains just the line segments that can be specialised to an 314 | underlying integer implementation. *) 315 | module type S = 316 | Integer_dependent 317 | with type 'a t := 'a t 318 | and type color := color 319 | and type duration := duration 320 | and type 'a printer := 'a printer 321 | and type bar_style := Bar_style.t 322 | 323 | module Make (Integer : Integer.S) : S with type integer := Integer.t 324 | 325 | (** {!Ext} is {!S} extended with non-integer-dependent segments as well. *) 326 | module type Ext = 327 | DSL 328 | with type 'a t := 'a t 329 | and type color := color 330 | and type duration := duration 331 | and type 'a printer := 'a printer 332 | and type Bar_style.t := Bar_style.t 333 | end 334 | 335 | module Using_int32 : Integer_dependent.Ext with type integer := int32 336 | module Using_int63 : Integer_dependent.Ext with type integer := int63 337 | module Using_int64 : Integer_dependent.Ext with type integer := int64 338 | module Using_float : Integer_dependent.Ext with type integer := float 339 | 340 | (** {1:examples Examples} 341 | 342 | {[ 343 | (* Renders: "[######---------------------------------------] 14/100" *) 344 | bar 100 ++ const " " ++ count_to 100 345 | ]} 346 | {[ 347 | (* Renders: "⠏ [01:04] [####-----------------] 293.9 MiB (eta: 07:12)" *) 348 | list 349 | [ spinner () 350 | ; brackets (elapsed ()) 351 | ; bar total 352 | ; bytes 353 | ; parens (const "eta: " ++ eta total) 354 | ] 355 | ]} 356 | {[ 357 | (* Renders: " a.txt │██████████████████████████▋ │ 91.4 MiB/s 92%" *) 358 | list 359 | [ lpad 7 (const file_name) 360 | ; bar ~style:`UTF8 total 361 | ; bytes_per_sec 362 | ; percentage_of total 363 | ] 364 | ]} 365 | 366 | See the [examples/] directory of the source repository for more. *) 367 | 368 | (** {1 Library internals} *) 369 | 370 | module Internals : sig 371 | (** Exposes the underlying implementation of line segments for testing. This 372 | API is unstable, unsafe and mostly undocumented; here be dragons etc. *) 373 | 374 | type 'a line 375 | 376 | module Line_buffer = Line_buffer 377 | 378 | include Line_primitives.S with type 'a t = 'a Line_primitives.t 379 | (** @inline *) 380 | 381 | val box_winsize : ?max:int -> ?fallback:int -> 'a t -> 'a t 382 | (** A box that takes on the current size of the terminal (or [fallback] if 383 | stdout is not attached to a terminal). 384 | 385 | @param fallback defaults to [80]. 386 | @param max defaults to no limit. *) 387 | 388 | val to_line : 'a t -> 'a line 389 | end 390 | with type 'a line := 'a t 391 | end 392 | 393 | module type Line = sig 394 | module type S = S 395 | 396 | type 'a t 397 | 398 | module Make (_ : Platform.S) : sig 399 | include 400 | S 401 | with type 'a t := 'a t 402 | and type color := Terminal.Color.t 403 | and type duration := Duration.t 404 | and type 'a printer := 'a Printer.t 405 | 406 | val to_primitive : Config.t -> 'a t -> 'a Internals.t 407 | end 408 | end 409 | 410 | (*———————————————————————————————————————————————————————————————————————————— 411 | Copyright (c) 2020–2021 Craig Ferguson 412 | 413 | Permission to use, copy, modify, and/or distribute this software for any 414 | purpose with or without fee is hereby granted, provided that the above 415 | copyright notice and this permission notice appear in all copies. 416 | 417 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 418 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 419 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 420 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 421 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 422 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 423 | DEALINGS IN THE SOFTWARE. 424 | ————————————————————————————————————————————————————————————————————————————*) 425 | -------------------------------------------------------------------------------- /src/progress/engine/line_primitives.mli: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | include Line_primitives_intf.Line_primitives 7 | (** @inline *) 8 | 9 | (*———————————————————————————————————————————————————————————————————————————— 10 | Copyright (c) 2020–2021 Craig Ferguson 11 | 12 | Permission to use, copy, modify, and/or distribute this software for any 13 | purpose with or without fee is hereby granted, provided that the above 14 | copyright notice and this permission notice appear in all copies. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 19 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 22 | DEALINGS IN THE SOFTWARE. 23 | ————————————————————————————————————————————————————————————————————————————*) 24 | -------------------------------------------------------------------------------- /src/progress/engine/line_primitives_intf.ml: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | open! Import 7 | 8 | module type Counter = sig 9 | type t 10 | 11 | val counter : unit -> t 12 | val count : t -> Mtime.span 13 | end 14 | 15 | module Types = struct 16 | type event = 17 | [ `report (* User has supplied a reported value. *) 18 | | `rerender (* Renderer wants a re-display. *) 19 | | `tick (* User has requested a "tick" (e.g. to update spinners). *) 20 | | `finish (* The bar or display has been finalised. *) ] 21 | end 22 | 23 | (** The DSL of progress bar segments. *) 24 | module type S = sig 25 | type 'a t 26 | (** The type of segments of progress bars that display reported values of type 27 | ['a]. *) 28 | 29 | include module type of Types 30 | 31 | type theta := Line_buffer.t -> event -> unit 32 | type 'a alpha := Line_buffer.t -> event -> 'a -> unit 33 | 34 | val noop : unit -> _ t 35 | val theta : width:int -> theta -> _ t 36 | 37 | val alpha : 38 | width:int 39 | -> initial:[ `Theta of Line_buffer.t -> unit | `Val of 'a ] 40 | -> 'a alpha 41 | -> 'a t 42 | 43 | val alpha_unsized : 44 | initial: 45 | [ `Theta of width:(unit -> int) -> Line_buffer.t -> int | `Val of 'a ] 46 | -> (width:(unit -> int) -> Line_buffer.t -> event -> 'a -> int) 47 | -> 'a t 48 | 49 | val array : 'a t array -> 'a t 50 | val pair : ?sep:unit t -> 'a t -> 'b t -> ('a * 'b) t 51 | val contramap : f:('a -> 'b) -> 'b t -> 'a t 52 | val on_finalise : 'a -> 'a t -> 'a t 53 | 54 | val of_pp : 55 | width:int -> initial:'a -> (Format.formatter -> event -> 'a -> unit) -> 'a t 56 | (** [of_pp ~width pp] is a segment that uses the supplied fixed-width 57 | pretty-printer to render the value. The pretty-printer must never emit 58 | newline characters. *) 59 | 60 | val conditional : ('a -> bool) -> 'a t -> 'a t 61 | (** [conditional pred s] has the same output format as [s], but is only passes 62 | reported values down to [s] when they satisfy [pred]. *) 63 | 64 | (** {2:stateful Stateful segments} *) 65 | 66 | val periodic : int -> 'a t -> 'a t 67 | (** [periodic n s] has the same output format as [s], but only passes reported 68 | values down to [s] on every [n]-th call. This is useful when progress is 69 | being reported from a hot-loop, where the cost of rendering is 70 | non-negligible. *) 71 | 72 | val accumulator : ('a -> 'a -> 'a) -> 'a -> 'a t -> 'a t 73 | (** [accumulator combine zero s] has the same output format [s]. *) 74 | 75 | val stateful : (unit -> 'a t) -> 'a t 76 | (** [stateful f] is a segment that behaves as [f ()] for any given render, 77 | allowing [f] to initialise any display state at the start of the rendering 78 | process. *) 79 | 80 | (** {2:boxes Dynamically-sized segments} *) 81 | 82 | (** Certain segments can have their size determined dynamically by being 83 | wrapped inside one of the following boxes: *) 84 | 85 | val box_dynamic : 86 | ?pad:[ `left | `right | `none ] -> (unit -> int) -> 'a t -> 'a t 87 | (** [box w] is a box that wraps a dynamically-sized segment and sets it to 88 | have size [w ()] on each tick. *) 89 | 90 | val box_fixed : ?pad:[ `left | `right | `none ] -> int -> 'a t -> 'a t 91 | (** [box-fixed n s] fixes the size of the dynamic segment [s] to be [n]. *) 92 | end 93 | 94 | module type Line_primitives = sig 95 | module type S = S 96 | 97 | include S 98 | 99 | module Compiled : sig 100 | type 'a t 101 | 102 | val pp_dump : Format.formatter -> 'a t -> unit 103 | end 104 | 105 | val compile : 'a t -> 'a Compiled.t 106 | 107 | val update : 108 | 'a Compiled.t -> (unconditional:bool -> Line_buffer.t -> int) Staged.t 109 | 110 | val report : 'a Compiled.t -> (Line_buffer.t -> 'a -> int) Staged.t 111 | val tick : 'a Compiled.t -> (Line_buffer.t -> int) Staged.t 112 | val finalise : 'a Compiled.t -> (Line_buffer.t -> int) Staged.t 113 | end 114 | 115 | (*———————————————————————————————————————————————————————————————————————————— 116 | Copyright (c) 2020–2021 Craig Ferguson 117 | 118 | Permission to use, copy, modify, and/or distribute this software for any 119 | purpose with or without fee is hereby granted, provided that the above 120 | copyright notice and this permission notice appear in all copies. 121 | 122 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 123 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 124 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 125 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 126 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 127 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 128 | DEALINGS IN THE SOFTWARE. 129 | ————————————————————————————————————————————————————————————————————————————*) 130 | -------------------------------------------------------------------------------- /src/progress/engine/multi.ml: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | include Multi_intf 7 | open! Import 8 | 9 | (* This module deals with extending the [Line] DSL to multiple-line layouts. 10 | We want to allow such layouts to have ['a Line.t] values with _different_ 11 | choices of ['a], so have to bite the bullet and use heterogeneous lists. *) 12 | 13 | include Hlist (Line) 14 | 15 | let blank = Zero 16 | let line l = One l 17 | let lines ls = Many ls 18 | 19 | (*———————————————————————————————————————————————————————————————————————————— 20 | Copyright (c) 2020–2021 Craig Ferguson 21 | 22 | Permission to use, copy, modify, and/or distribute this software for any 23 | purpose with or without fee is hereby granted, provided that the above 24 | copyright notice and this permission notice appear in all copies. 25 | 26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 29 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 30 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 31 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 32 | DEALINGS IN THE SOFTWARE. 33 | ————————————————————————————————————————————————————————————————————————————*) 34 | -------------------------------------------------------------------------------- /src/progress/engine/multi.mli: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | include Multi_intf.Multi 7 | (** @inline *) 8 | 9 | (*———————————————————————————————————————————————————————————————————————————— 10 | Copyright (c) 2020–2021 Craig Ferguson 11 | 12 | Permission to use, copy, modify, and/or distribute this software for any 13 | purpose with or without fee is hereby granted, provided that the above 14 | copyright notice and this permission notice appear in all copies. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 19 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 22 | DEALINGS IN THE SOFTWARE. 23 | ————————————————————————————————————————————————————————————————————————————*) 24 | -------------------------------------------------------------------------------- /src/progress/engine/multi_intf.ml: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | open! Import 7 | 8 | type 'a reporter = 'a -> unit 9 | 10 | module type S = sig 11 | type 'a line 12 | type 'a reporter 13 | 14 | type ('a, 'b) t 15 | (** The type of vertical {i sequences} of progress bars. The parameter ['a] 16 | stores a list of the reporting functions associated with each bar, 17 | terminating with ['b]. For example: 18 | 19 | {[ 20 | (* Single progress bar, taking a [float] value. *) 21 | (float reporter -> 'b, 'b) t 22 | 23 | (* A two-bar layout, where the top bar takes [int64]s and the bottom one 24 | takes [string * float] pairs. *) 25 | (int64 reporter -> (string * float) reporter -> 'b, 'b) t 26 | ]} 27 | 28 | These reporting functions are supplied when beginning the 29 | {{!rendering} rendering} process. *) 30 | 31 | val line : 'a line -> ('a reporter -> 'b, 'b) t 32 | (** Construct a multiple-line layout from a single progress bar line. *) 33 | 34 | val lines : 'a line list -> ('a reporter list -> 'b, 'b) t 35 | (** Construct a multiple-line layout from a sequence of lines that all have 36 | the same type of reported values. *) 37 | 38 | val ( ++ ) : ('a, 'b) t -> ('b, 'c) t -> ('a, 'c) t 39 | (** Stack progress bars vertically. [a ++ b] is a set with [a] stacked on top 40 | of [b]. The two sections have separate reporting functions, passed 41 | consecutively to the {!with_reporters} continuation when rendering. *) 42 | 43 | val blank : ('a, 'a) t 44 | (** A blank line, for adding spacing between progress lines. *) 45 | end 46 | 47 | module Hlist (Elt : sig 48 | type 'a t 49 | end) = 50 | struct 51 | type (_, _) t = 52 | | Zero : ('a, 'a) t 53 | | One : 'a Elt.t -> ('a reporter -> 'b, 'b) t 54 | | Many : 'a Elt.t list -> ('a reporter list -> 'b, 'b) t 55 | | Plus : (('a, 'b) t * ('b, 'c) t) -> ('a, 'c) t 56 | 57 | type 'b mapper = { f : 'a. int -> 'a Elt.t option -> 'b } 58 | 59 | let mapi = 60 | let rec aux : type a b c. (a, b) t -> int -> f:c mapper -> int * c list = 61 | fun t i ~f -> 62 | match t with 63 | | Zero -> (succ i, [ f.f i None ]) 64 | | One b -> (succ i, [ f.f i (Some b) ]) 65 | | Many bs -> 66 | ( i + List.length bs 67 | , List.mapi bs ~f:(fun i' x -> f.f (i + i') (Some x)) ) 68 | | Plus (xs, ys) -> 69 | let i, xs = aux xs ~f i in 70 | let i, ys = aux ys ~f i in 71 | (i, xs @ ys) 72 | in 73 | fun t ~f -> snd (aux t 0 ~f) 74 | 75 | let rec length : type a b. (a, b) t -> int = function 76 | | Zero -> 1 77 | | One _ -> 1 78 | | Many xs -> List.length xs 79 | | Plus (a, b) -> length a + length b 80 | 81 | let ( ++ ) xs ys = Plus (xs, ys) 82 | end 83 | 84 | module type Multi = sig 85 | module type S = S 86 | 87 | module Hlist = Hlist 88 | 89 | include 90 | S 91 | with type 'a line := 'a Line.t 92 | and type 'a reporter := 'a -> unit 93 | and type ('a, 'b) t = ('a, 'b) Hlist(Line).t 94 | end 95 | (*———————————————————————————————————————————————————————————————————————————— 96 | Copyright (c) 2020–2021 Craig Ferguson 97 | 98 | Permission to use, copy, modify, and/or distribute this software for any 99 | purpose with or without fee is hereby granted, provided that the above 100 | copyright notice and this permission notice appear in all copies. 101 | 102 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 103 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 104 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 105 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 106 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 107 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 108 | DEALINGS IN THE SOFTWARE. 109 | ————————————————————————————————————————————————————————————————————————————*) 110 | -------------------------------------------------------------------------------- /src/progress/engine/platform.ml: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | (** A platform provides a time source and access to the terminal width. *) 7 | module type S = sig 8 | (** Signature of a monotonic clock. See [Mtime] for various implementations on 9 | different plaforms. *) 10 | module Clock : sig 11 | val elapsed : unit -> Mtime.span 12 | val now : unit -> Mtime.t 13 | 14 | type counter 15 | 16 | val counter : unit -> counter 17 | val count : counter -> Mtime.span 18 | end 19 | 20 | (** Functions for polling (and subscribing to) the terminal width. *) 21 | module Terminal_width : sig 22 | val get : unit -> int option 23 | val set_changed_callback : (int option -> unit) -> unit 24 | end 25 | end 26 | 27 | (*———————————————————————————————————————————————————————————————————————————— 28 | Copyright (c) 2020–2021 Craig Ferguson 29 | 30 | Permission to use, copy, modify, and/or distribute this software for any 31 | purpose with or without fee is hereby granted, provided that the above 32 | copyright notice and this permission notice appear in all copies. 33 | 34 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 35 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 36 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 37 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 38 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 39 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 40 | DEALINGS IN THE SOFTWARE. 41 | ————————————————————————————————————————————————————————————————————————————*) 42 | -------------------------------------------------------------------------------- /src/progress/engine/printer.ml: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | open! Import 7 | 8 | external unsafe_blit_string : string -> int -> bytes -> int -> int -> unit 9 | = "caml_blit_string" 10 | [@@noalloc] 11 | (** Polyfill for pre-4.09.0 *) 12 | 13 | type 'a t = 14 | { write : 'a -> into:bytes -> pos:int -> unit 15 | ; write_len : int 16 | ; width : int 17 | ; to_string : 'a -> string 18 | ; pp : 'a pp 19 | } 20 | 21 | let create ?width ?pp ~to_string ~string_len () = 22 | let write x ~into ~pos = 23 | unsafe_blit_string (to_string x) 0 into pos string_len 24 | in 25 | let width = match width with None -> string_len | Some width -> width in 26 | let pp = match pp with None -> Fmt.of_to_string to_string | Some pp -> pp in 27 | { write; write_len = string_len; width; to_string; pp } 28 | 29 | let integer (type a) ~width (module Integer : Integer.S with type t = a) : a t = 30 | let to_string x = 31 | let x = Integer.to_string x in 32 | let x_len = String.length x in 33 | let padding = width - x_len in 34 | if padding < 0 then 35 | Fmt.invalid_arg 36 | "Progress.Printer.int: can't print integer %s within a width of %d" x 37 | width; 38 | if padding = 0 then x 39 | else 40 | let buf = Bytes.make width ' ' in 41 | unsafe_blit_string x 0 buf padding x_len; 42 | Bytes.unsafe_to_string buf 43 | in 44 | create ~string_len:width ~to_string () 45 | 46 | let int ~width = integer ~width (module Integer.Int) 47 | 48 | let string ~width = 49 | if width < 0 then failwith "Printer.string: negative print length"; 50 | let ellipsis_length = min 3 width in 51 | let ellipsis = String.make ellipsis_length '.' in 52 | let to_string s = 53 | let printed_len = Terminal.guess_printed_width s in 54 | let padding = width - printed_len in 55 | if padding = 0 then s 56 | else if padding > 0 then ( 57 | let len = String.length s in 58 | let buf = Bytes.make (len + padding) ' ' in 59 | unsafe_blit_string s 0 buf 0 len; 60 | Bytes.unsafe_to_string buf) 61 | else 62 | let s = Terminal.truncate_to_width (width - ellipsis_length) s in 63 | s ^ ellipsis 64 | in 65 | create ~string_len:width ~to_string () 66 | 67 | let to_pp { pp; _ } = pp 68 | 69 | let using ~f { write; write_len; to_string; pp; width } = 70 | let write x ~into ~pos = write (f x) ~into ~pos in 71 | let pp = Fmt.using f pp in 72 | let to_string x = to_string (f x) in 73 | { write; write_len; to_string; pp; width } 74 | 75 | let to_line_printer { write; write_len; _ } = 76 | Line_buffer.lift_write ~len:write_len ~write 77 | 78 | let to_to_string { to_string; _ } = to_string 79 | let print_width { width; _ } = width 80 | 81 | module Internals = struct 82 | let integer = integer 83 | let to_line_printer = to_line_printer 84 | end 85 | 86 | (*———————————————————————————————————————————————————————————————————————————— 87 | Copyright (c) 2020–2021 Craig Ferguson 88 | 89 | Permission to use, copy, modify, and/or distribute this software for any 90 | purpose with or without fee is hereby granted, provided that the above 91 | copyright notice and this permission notice appear in all copies. 92 | 93 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 94 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 95 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 96 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 97 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 98 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 99 | DEALINGS IN THE SOFTWARE. 100 | ————————————————————————————————————————————————————————————————————————————*) 101 | -------------------------------------------------------------------------------- /src/progress/engine/printer.mli: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | open! Import 7 | 8 | type -'a t 9 | (** The type of {i fixed-width} pretty-printers for values of type ['a]. 10 | Specifically, these pretty-printers aim to emit strings that have the same 11 | {i rendered} length in terminals (i.e. after accounting for UTF-8 encoding 12 | and ANSI escapes). 13 | 14 | {b Most users don't need this module, and can use the pre-provided {!Line} 15 | segments and {!Units} printers directly.} *) 16 | 17 | (** {1 Constructing printers} *) 18 | 19 | val int : width:int -> int t 20 | (** [int ~width] pretty-prints integers using [width] characters, adding left 21 | padding if necessary. The printer will raise [Invalid_argument] if the 22 | integer to be printed can't be displayed in the given width. *) 23 | 24 | val string : width:int -> string t 25 | (** [string ~width] is a pretty-printer for UTF8-encoded strings using [width] 26 | characters, adding right padding or truncating with ellipses as necessary. 27 | 28 | For example, [string ~width:8] processes values as follows: 29 | 30 | {[ 31 | "" ↦ " " 32 | "hello" ↦ "hello " 33 | "hello world" ↦ "hello..." 34 | ]} 35 | 36 | {b Note:} 37 | {i this printer uses a heuristic function ({!Uucp.tty_width_hint}) to guess 38 | the rendered length of supplied strings. This function is not guaranteed 39 | to be correct on all UTF-8 codepoints, and so certain "unusual" string 40 | inputs can cause progress bar rendering to go awry.} *) 41 | 42 | val using : f:('b -> 'a) -> 'a t -> 'b t 43 | (** [using ~f t] prints values [v] by passing [f v] to the printer [t]. *) 44 | 45 | val create : 46 | ?width:int 47 | -> ?pp:'a pp 48 | -> to_string:('a -> string) 49 | -> string_len:int 50 | -> unit 51 | -> 'a t 52 | (** [create ~to_string ~string_len ()] is a printer that uses [to_string] to 53 | render values in [string_len]-many bytes. 54 | 55 | The rendered width of the output (when displayed in a terminal) is assumed 56 | to also be [string_len] (i.e. the output string is assumed to be ASCII), but 57 | this can be assumption can be overridden by passing an explicit [~width] 58 | (e.g. if the printer emits non-ASCII UTF-8 characters or ANSI escape 59 | sequences). *) 60 | 61 | (** {1 Consuming printers} *) 62 | 63 | val to_pp : 'a t -> Format.formatter -> 'a -> unit 64 | (** Convert a pretty-printer to a [Format]-compatible pretty-printer. *) 65 | 66 | val to_to_string : 'a t -> 'a -> string 67 | (** Convert a pretty-printer to a [to_string] function. *) 68 | 69 | val print_width : _ t -> int 70 | (** [print_width t] is the number of terminal columns occupied by the output of 71 | [t]. *) 72 | 73 | (** {1 Internals} *) 74 | 75 | module Internals : sig 76 | val integer : width:int -> (module Integer.S with type t = 'a) -> 'a t 77 | val to_line_printer : 'a t -> (Line_buffer.t -> 'a -> unit) Staged.t 78 | end 79 | 80 | (*———————————————————————————————————————————————————————————————————————————— 81 | Copyright (c) 2020–2021 Craig Ferguson 82 | 83 | Permission to use, copy, modify, and/or distribute this software for any 84 | purpose with or without fee is hereby granted, provided that the above 85 | copyright notice and this permission notice appear in all copies. 86 | 87 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 88 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 89 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 90 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 91 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 93 | DEALINGS IN THE SOFTWARE. 94 | ————————————————————————————————————————————————————————————————————————————*) 95 | -------------------------------------------------------------------------------- /src/progress/engine/progress_engine.ml: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | open! Import 7 | include Progress_engine_intf 8 | 9 | module type Platform = Platform.S 10 | 11 | module Make (Platform : Platform) = struct 12 | module Color = Terminal.Color 13 | module Duration = Duration 14 | module Multi = Multi 15 | module Printer = Printer 16 | module Units = Units 17 | 18 | module Internals = struct 19 | module Ansi = Terminal.Style 20 | end 21 | 22 | module Config = struct 23 | include Config 24 | 25 | type t = user_supplied 26 | end 27 | 28 | module Renderer = struct 29 | include Renderer.Make (Platform) 30 | include Renderer 31 | end 32 | 33 | module Line = struct 34 | include Line.Make (Platform) 35 | include Line 36 | end 37 | 38 | module Display = Renderer.Display 39 | module Reporter = Renderer.Reporter 40 | 41 | let counter ?(style = `ASCII) ?message ?pp total = 42 | let map_option ~f x = Option.fold ~none:(Line.noop ()) ~some:f x in 43 | let open Line.Using_int64 in 44 | list 45 | [ map_option message ~f:const 46 | ; map_option pp ~f:(fun pp -> sum ~pp ~width:(Printer.print_width pp) ()) 47 | ; elapsed () 48 | ; bar ~style total 49 | ; percentage_of total 50 | ] 51 | 52 | let interject_with = Renderer.interject_with 53 | let instrument_logs_reporter = Renderer.instrument_logs_reporter 54 | let logs_reporter = Renderer.logs_reporter 55 | let with_reporters = Renderer.with_reporters 56 | let with_reporter ?config b f = with_reporters ?config (Multi.line b) f 57 | end 58 | 59 | module Integer = Integer 60 | 61 | module Exposed_for_testing = struct 62 | module Flow_meter = Flow_meter 63 | end 64 | 65 | (*———————————————————————————————————————————————————————————————————————————— 66 | Copyright (c) 2020–2021 Craig Ferguson 67 | 68 | Permission to use, copy, modify, and/or distribute this software for any 69 | purpose with or without fee is hereby granted, provided that the above 70 | copyright notice and this permission notice appear in all copies. 71 | 72 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 73 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 74 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 75 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 76 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 77 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 78 | DEALINGS IN THE SOFTWARE. 79 | ————————————————————————————————————————————————————————————————————————————*) 80 | -------------------------------------------------------------------------------- /src/progress/engine/progress_engine.mli: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | (** [Progress_engine] provides a platform-independent progress bar renderer. 7 | Most users should use the {!Progress} module, which instantiates this core 8 | with a Unix platform implementation. *) 9 | 10 | include Progress_engine_intf.Progress_engine 11 | (** @inline *) 12 | 13 | (*———————————————————————————————————————————————————————————————————————————— 14 | Copyright (c) 2020–2021 Craig Ferguson 15 | 16 | Permission to use, copy, modify, and/or distribute this software for any 17 | purpose with or without fee is hereby granted, provided that the above 18 | copyright notice and this permission notice appear in all copies. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 23 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 26 | DEALINGS IN THE SOFTWARE. 27 | ————————————————————————————————————————————————————————————————————————————*) 28 | -------------------------------------------------------------------------------- /src/progress/engine/progress_engine_intf.ml: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | module type S = sig 7 | (* We go to some effort here to avoid having types directly refer to those in 8 | [Progress_engine] rather than going via the aliases. *) 9 | 10 | (** *) 11 | 12 | (** {1 Preliminaries} 13 | 14 | Some basic types used throughout the rest of the library: *) 15 | 16 | module Color = Terminal_ansi.Color 17 | module Duration = Duration 18 | module Printer = Printer 19 | module Units = Units 20 | 21 | (** {1 Description} 22 | 23 | Describing a progress line is done via the {!Line} DSL. Individual lines 24 | can be stacked vertically via {!Multi}. *) 25 | 26 | module Line : sig 27 | (** @inline *) 28 | include 29 | Line.S 30 | with type 'a t = 'a Line.t 31 | and type color := Color.t 32 | and type duration := Duration.t 33 | and type 'a printer := 'a Printer.t 34 | end 35 | 36 | module Multi : sig 37 | type 'a reporter := 'a -> unit 38 | 39 | (** @inline *) 40 | include 41 | Multi.S with type 'a line := 'a Line.t and type 'a reporter := 'a reporter 42 | end 43 | 44 | (** {2 Pre-provided lines} *) 45 | 46 | val counter : 47 | ?style:[ `ASCII | `UTF8 | `Custom of Line.Bar_style.t ] 48 | -> ?message:string 49 | -> ?pp:int64 Printer.t 50 | -> int64 51 | -> int64 Line.t 52 | (** [counter total] is a progress bar of the form: 53 | 54 | {[ 55 | MM:SS [########..............................] XX% 56 | ]} 57 | 58 | where each reported value contributes cumulatively towards an eventual 59 | total of [total]. [?style] specifies the 60 | {{!Line.Bar_style.t} [Bar_style.t]} to use for rendering the bar, and 61 | [?pp] is used to pretty-print the [] segment, if passed. (For 62 | example, {!Units.Bytes.of_int64} can be used for totals measured in 63 | bytes.) *) 64 | 65 | (** {1 Rendering} 66 | 67 | Once you have a {{!description} description} of the progress bar to be 68 | rendered (either a {!Line.t} or a {!Multi.t}), begin rendering it by using 69 | {!with_reporter} or {!with_reporters} respectively. *) 70 | 71 | type 'a reporter := 'a -> unit 72 | (** A {i reporter} for values of type ['a]. In this library, each progress 73 | line has its own reporting function. *) 74 | 75 | (** Configuration for progress bar rendering. *) 76 | module Config : sig 77 | type t 78 | 79 | val v : 80 | ?ppf:Format.formatter 81 | -> ?hide_cursor:bool 82 | -> ?persistent:bool 83 | -> ?max_width:int option 84 | -> ?min_interval:Duration.t option 85 | -> unit 86 | -> t 87 | (** - [ppf]: the formatter to use for rendering. Defaults to 88 | [Format.err_formatter]. 89 | 90 | - [hide_cursor]: whether or not to hide the terminal cursor (using the 91 | {{:https://en.wikipedia.org/wiki/ANSI_escape_code} [DECTCEM]} ANSI 92 | escape codes) during progress bar rendering. Defaults to [true]. 93 | 94 | - [persistent]: whether or not to retain the final progress bar display 95 | in the terminal after rendering has finished. Defaults to [true]. 96 | 97 | - [max_width]: an optional fixed upper bound on the size of a progress 98 | bar (in addition to the one by the terminal width). Defaults to 99 | [None]. 100 | 101 | - [min_interval]: the minimum time interval between re-renders of the 102 | progress bar display (i.e. a debounce threshold). Defaults to 1/60th 103 | of a second. *) 104 | 105 | val ( || ) : t -> t -> t 106 | (** Merge two config values, with settings from the left taking priority. 107 | That is, [a || b] contains the configuration of [a], with unset defaults 108 | taken from [b]. *) 109 | 110 | (** Provides the default values of each of the config parameters. *) 111 | module Default : sig 112 | val ppf : Format.formatter 113 | (** [ppf] is [Format.err_formatter]. *) 114 | 115 | val hide_cursor : bool 116 | (** [hide_cursor] is [true]. *) 117 | 118 | val persistent : bool 119 | (** [persistent] is [true]. *) 120 | 121 | val max_width : int option 122 | (** [max_width] is [None]. *) 123 | 124 | val min_interval : Duration.t option 125 | (** [min_interval] is 1/60th of a second. *) 126 | end 127 | end 128 | 129 | (** @inline *) 130 | include 131 | Renderer.S 132 | with type 'a reporter := 'a reporter 133 | and type 'a line := 'a Line.t 134 | and type ('a, 'b) multi := ('a, 'b) Multi.t 135 | and type config := Config.t 136 | end 137 | 138 | module type Progress_engine = sig 139 | module type S = S 140 | module type Platform = Platform.S 141 | 142 | module Make (_ : Platform) : S 143 | module Integer = Integer 144 | 145 | module Exposed_for_testing : sig 146 | module Flow_meter = Flow_meter 147 | end 148 | end 149 | 150 | (*———————————————————————————————————————————————————————————————————————————— 151 | Copyright (c) 2020–2021 Craig Ferguson 152 | 153 | Permission to use, copy, modify, and/or distribute this software for any 154 | purpose with or without fee is hereby granted, provided that the above 155 | copyright notice and this permission notice appear in all copies. 156 | 157 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 158 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 159 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 160 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 161 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 162 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 163 | DEALINGS IN THE SOFTWARE. 164 | ————————————————————————————————————————————————————————————————————————————*) 165 | -------------------------------------------------------------------------------- /src/progress/engine/renderer.mli: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | include Renderer_intf.Renderer 7 | (** @inline *) 8 | 9 | (*———————————————————————————————————————————————————————————————————————————— 10 | Copyright (c) 2020–2021 Craig Ferguson 11 | 12 | Permission to use, copy, modify, and/or distribute this software for any 13 | purpose with or without fee is hereby granted, provided that the above 14 | copyright notice and this permission notice appear in all copies. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 19 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 22 | DEALINGS IN THE SOFTWARE. 23 | ————————————————————————————————————————————————————————————————————————————*) 24 | -------------------------------------------------------------------------------- /src/progress/engine/renderer_intf.ml: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | open! Import 7 | 8 | type 'a reporter = 'a -> unit 9 | 10 | module type S = sig 11 | type 'a reporter 12 | type 'a line 13 | type ('a, 'b) multi 14 | type config 15 | 16 | val with_reporter : ?config:config -> 'a line -> ('a reporter -> 'b) -> 'b 17 | (** [with_reporters line f] begins rendering [line] and calls [f] with the 18 | reporting function. Once [f] returns, the display is finalised. {b Note:} 19 | attempting to use the reporting function after [f] has returned will raise 20 | a [Finalised] exception. *) 21 | 22 | val with_reporters : ?config:config -> ('a, 'b) multi -> 'a -> 'b 23 | (** [with_reporters bars f] begins rendering [bars] and passes the 24 | corresponding reporting functions to [f]. Once [f] returns, the display is 25 | finalised. *) 26 | 27 | (** {2 Examples} 28 | 29 | - Reading a file into memory and displaying a single progress bar: 30 | 31 | {[ 32 | let read_file path buffer = 33 | let total = file_size path and in_channel = open_in path in 34 | try 35 | with_reporter (counter ~total ()) @@ fun report -> 36 | let rec aux offset = 37 | let bytes_read = really_read buffer offset in 38 | report bytes_read; 39 | aux (offset + bytes_read) 40 | in 41 | aux 0 42 | with End_of_file -> close_in in_channel 43 | ]} 44 | - Sending data to multiple clients, with one progress bar each: 45 | 46 | {[ 47 | let multi_bar_rendering () = 48 | with_reporters 49 | Multi.(line bar_a ++ line bar_b ++ line bar_c) 50 | (fun report_a report_b report_c -> 51 | for i = 1 to 1000 do 52 | report_a (transfer_bytes client_a); 53 | report_b (transfer_bytes client_b); 54 | report_c (transfer_bytes client_c) 55 | done) 56 | ]} *) 57 | 58 | (** {2 Logging during rendering} *) 59 | 60 | val interject_with : (unit -> 'a) -> 'a 61 | (** [interject_with f] executes the function [f] while temporarily suspending 62 | the rendering of any active progress bar display. This can be useful when 63 | printing to [stdout] / [stderr], to avoid any interference from the 64 | rendering of progress bars. If using the [Logs] library, consider using 65 | {!reporter} and {!instrument_reporter} instead. 66 | 67 | {b Note}: 68 | {i the caller must ensure that the terminal cursor is left in an 69 | appropriate position to resume rendering. In practice, this means that 70 | any printing to the terminal should be terminated with a newline 71 | character and flushed.} *) 72 | 73 | (** Extensions to the {{:https://erratique.ch/software/logs} [Logs]} library 74 | designed to cooperate with progress bar rendering: *) 75 | 76 | val logs_reporter : 77 | ?pp_header:(Logs.level * string option) Fmt.t 78 | -> ?app:Format.formatter 79 | -> ?dst:Format.formatter 80 | -> unit 81 | -> Logs.reporter 82 | (** [reporter] is like [Logs_fmt.reporter] but produces a reporter that 83 | {{!Progress.interject_with} suspends} any ongoing progress bar rendering 84 | while displaying log entries, ensuring that log entries in the terminal 85 | are never overwritten by the renderer. *) 86 | 87 | val instrument_logs_reporter : Logs.reporter -> Logs.reporter 88 | (** [instrument_reporter r] wraps the synchronous reporter [r] to ensure that 89 | any progress bar rendering is suspended while messages are being 90 | constructed for [r]. 91 | 92 | {b Note}: 93 | {i to ensure that log entries are not overwritten by the [Progress] 94 | renderer, [r] must flush any log entries to the terminal synchronously: 95 | as soon as they are reported. This is true of the [Logs] reporters 96 | built by {!Logs.format_reporter} and {!Logs_fmt.reporter}. An 97 | asynchronous reporter should use {!interject_with} to delimit its 98 | flushing action instead.} *) 99 | 100 | (** {2 Manual lifecycle management} 101 | 102 | Functions for explicitly starting and stopping the process of rendering a 103 | bar; useful when the code doing the progress reporting cannot be 104 | conveniently delimited inside {!with_reporter}. All {!Display}s must be 105 | properly {{!Display.finalise} finalised}, and it is not possible to 106 | interleave rendering of displays. *) 107 | 108 | module Reporter : sig 109 | type -'a t 110 | (** The (abstract) type of reporter functions used by the manual lifecycle 111 | management functions in {!Display}. An ['a t] is conceptually an 112 | ['a -> unit] function, but can be explicitly {!finalise}d. *) 113 | 114 | val report : 'a t -> 'a -> unit 115 | 116 | val finalise : _ t -> unit 117 | (** [finalise t] terminates rendering of the line associated with reporter 118 | [t]. Attempting to {!report} to a finalised reporter will raise an 119 | exception. *) 120 | 121 | (** A heterogeneous list type, used by {!Display} for returning a list of 122 | reporters corresponding to multi-line progress displays. *) 123 | type (_, _) list = 124 | | [] : ('a, 'a) list 125 | | ( :: ) : 'a * ('b, 'c) list -> ('a -> 'b, 'c) list 126 | end 127 | 128 | module Display : sig 129 | type ('a, 'b) t 130 | (** The type of active progress bar displays. The type parameters ['a] and 131 | ['b] track the types of the reporting functions supplied by {!reporters} 132 | (see {!Multi.t} for details).*) 133 | 134 | val start : ?config:config -> ('a, 'b) multi -> ('a, 'b) t 135 | (** Initiate rendering of a progress bar display. Raises [Failure] if there 136 | is already an active progress bar display. *) 137 | 138 | val reporters : ('a, unit) t -> ('a, unit) Reporter.list 139 | (** [reporters d] is the list of initial reporting functions belonging to 140 | display [d]. 141 | 142 | {b Note} 143 | {i this list does not include any reporters added {i during} progress 144 | bar rendering via {!add_line}.} *) 145 | 146 | val tick : _ t -> unit 147 | (** [tick d] re-renders the contents of display [d] without reporting any 148 | specific values. This function can be used to update spinners, 149 | durations, etc. when there is no actual progress to report. *) 150 | 151 | val add_line : ?above:int -> (_, _) t -> 'a line -> 'a Reporter.t 152 | (** Add a line to an ongoing display, and get its reporting function. By 153 | default, the line is added to the {i bottom} of the display 154 | ([above = 0]); the [~above] argument can be passed to add the line above 155 | some number of existing lines. *) 156 | 157 | val remove_line : (_, _) t -> _ Reporter.t -> unit 158 | (** Remove a line from an ongoing display, identified by the reporting 159 | function that was returned by [add_line]. Lines may be removed either 160 | before they are finalised (for example if some task has been cancelled) 161 | or after being finalised. In both cases, the line will be removed from 162 | the display, thus retrieving some space in the terminal. Attempting to 163 | remove a line that has already been removed from the display will raise 164 | [Failure]. Also raises [Failure] if the display has already been 165 | finalised. *) 166 | 167 | val pause : (_, _) t -> unit 168 | (** Suspends the rendering of any active progress bar display. It can be 169 | useful to compose with the [Logs] library and avoid interference when 170 | printing to [stdout] / [stderr] from the rendering of progress bars. *) 171 | 172 | val resume : (_, _) t -> unit 173 | (** Resume the rendering of progress bar display. *) 174 | 175 | val finalise : (_, _) t -> unit 176 | (** Terminate the given progress bar display. Raises [Failure] if the 177 | display has already been finalised. *) 178 | end 179 | end 180 | 181 | module type Renderer = sig 182 | module type S = S 183 | 184 | module Make (_ : Platform.S) : 185 | S 186 | with type 'a reporter := 'a reporter 187 | and type 'a line := 'a Line.t 188 | and type ('a, 'b) multi := ('a, 'b) Multi.t 189 | and type config := Config.user_supplied 190 | end 191 | 192 | (*———————————————————————————————————————————————————————————————————————————— 193 | Copyright (c) 2020–2021 Craig Ferguson 194 | 195 | Permission to use, copy, modify, and/or distribute this software for any 196 | purpose with or without fee is hereby granted, provided that the above 197 | copyright notice and this permission notice appear in all copies. 198 | 199 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 200 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 201 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 202 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 203 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 204 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 205 | DEALINGS IN THE SOFTWARE. 206 | ————————————————————————————————————————————————————————————————————————————*) 207 | -------------------------------------------------------------------------------- /src/progress/engine/stdlib_ext.ml: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | type 'a pp = Format.formatter -> 'a -> unit 7 | 8 | let ( >> ) f g x = g (f x) 9 | 10 | let tap f x = 11 | f x; 12 | x 13 | 14 | let trace fmt x = 15 | Fmt.epr fmt x; 16 | x 17 | 18 | module type Eq = sig 19 | type t 20 | 21 | val equal : t -> t -> bool 22 | end 23 | 24 | module type Comparable_infix = sig 25 | type t 26 | 27 | val ( = ) : t -> t -> bool 28 | val ( <= ) : t -> t -> bool 29 | val ( >= ) : t -> t -> bool 30 | val ( < ) : t -> t -> bool 31 | val ( > ) : t -> t -> bool 32 | end 33 | 34 | module Poly = struct 35 | let ( = ) = Stdlib.( = ) 36 | let ( <= ) = Stdlib.( <= ) 37 | let ( >= ) = Stdlib.( >= ) 38 | let ( < ) = Stdlib.( < ) 39 | let ( > ) = Stdlib.( > ) 40 | end 41 | 42 | include Stdlib.StdLabels 43 | include Stdlib.MoreLabels 44 | 45 | (** Shadow polymorphic operators in the Stdlib. *) 46 | include struct 47 | let min : int -> int -> int = min 48 | let max : int -> int -> int = max 49 | let compare : int -> int -> int = compare 50 | 51 | include (Poly : Comparable_infix with type t := int) 52 | end 53 | 54 | module Int = struct 55 | include Int 56 | include (Poly : Comparable_infix with type t := t) 57 | 58 | let float_div a b = to_float a /. to_float b 59 | end 60 | 61 | module Int32 = struct 62 | include Int32 63 | include (Poly : Comparable_infix with type t := t) 64 | 65 | let float_div a b = to_float a /. to_float b 66 | end 67 | 68 | module Int63 = struct 69 | include Optint.Int63 70 | include (Poly : Comparable_infix with type t := t) 71 | 72 | let float_div a b = to_float a /. to_float b 73 | end 74 | 75 | type int63 = Int63.t 76 | 77 | module Int64 = struct 78 | include Int64 79 | include (Poly : Comparable_infix with type t := t) 80 | 81 | let float_div a b = to_float a /. to_float b 82 | end 83 | 84 | module Float = struct 85 | include Float 86 | include (Poly : Comparable_infix with type t := t) 87 | 88 | let float_div = ( /. ) 89 | let to_float x = x 90 | let of_float x = x 91 | end 92 | 93 | module Option = struct 94 | include Option 95 | 96 | let ( || ) a b = match a with Some _ -> a | None -> b 97 | end 98 | 99 | module Result = struct 100 | let get_or_invalid_arg = function 101 | | Ok x -> x 102 | | Error (`Msg s) -> invalid_arg s 103 | 104 | let errorf fmt = Format.kasprintf (fun s -> Error (`Msg s)) fmt 105 | end 106 | 107 | module List = struct 108 | include List 109 | 110 | let rec intersperse ~sep = function 111 | | ([] | [ _ ]) as l -> l 112 | | h1 :: (_ :: _ as tl) -> h1 :: sep :: intersperse ~sep tl 113 | end 114 | 115 | module Staged : sig 116 | type 'a t 117 | type ('a, 'b) endo := 'a t -> 'b t 118 | 119 | external map : f:('a -> 'b) -> ('a, 'b) endo = "%identity" 120 | external inj : 'a -> 'a t = "%identity" 121 | external prj : 'a t -> 'a = "%identity" 122 | 123 | module Syntax : sig 124 | val ( let$ ) : 'a t -> ('a -> 'b) -> 'b t 125 | val ( and$ ) : 'a t -> 'b t -> ('a * 'b) t 126 | end 127 | end = struct 128 | type 'a t = 'a 129 | type ('a, 'b) endo = 'a t -> 'b t 130 | 131 | external map : f:('a -> 'b) -> ('a, 'b) endo = "%identity" 132 | external inj : 'a -> 'a t = "%identity" 133 | external prj : 'a t -> 'a = "%identity" 134 | 135 | module Syntax = struct 136 | let ( let$ ) x f = f x 137 | let ( and$ ) a b = (a, b) 138 | end 139 | end 140 | 141 | module Unique_id () : sig 142 | type t 143 | 144 | val create : unit -> t 145 | val equal : t -> t -> bool 146 | val pp : t pp 147 | end = struct 148 | let allocated = ref 0 149 | 150 | type t = int 151 | 152 | let create () = 153 | let v = !allocated in 154 | incr allocated; 155 | v 156 | 157 | let equal = Int.equal 158 | let pp = Fmt.int 159 | end 160 | 161 | module Sta_dyn : sig 162 | type 'a t = Static of 'a | Dynamic of (unit -> 'a) 163 | 164 | val get : 'a t -> 'a 165 | val lift : ('a -> 'a -> 'a) -> 'a t -> 'a t -> 'a t 166 | val pp : 'a pp -> 'a t pp 167 | end = struct 168 | type 'a t = Static of 'a | Dynamic of (unit -> 'a) 169 | 170 | let get = function Static x -> x | Dynamic f -> f () 171 | 172 | let lift add x y = 173 | let ( ++ ) = add in 174 | match (x, y) with 175 | | Static x, Static y -> Static (x ++ y) 176 | | Dynamic f, Static x | Static x, Dynamic f -> Dynamic (fun () -> x ++ f ()) 177 | | Dynamic f, Dynamic g -> Dynamic (fun () -> f () ++ g ()) 178 | 179 | let pp pp_elt ppf = function 180 | | Static x -> Fmt.pf ppf "Static %a" pp_elt x 181 | | Dynamic f -> Fmt.pf ppf "Dynamic %a" pp_elt (f ()) 182 | end 183 | 184 | (*———————————————————————————————————————————————————————————————————————————— 185 | Copyright (c) 2020–2021 Craig Ferguson 186 | 187 | Permission to use, copy, modify, and/or distribute this software for any 188 | purpose with or without fee is hereby granted, provided that the above 189 | copyright notice and this permission notice appear in all copies. 190 | 191 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 192 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 193 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 194 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 195 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 196 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 197 | DEALINGS IN THE SOFTWARE. 198 | ————————————————————————————————————————————————————————————————————————————*) 199 | -------------------------------------------------------------------------------- /src/progress/engine/units.ml: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | open! Import 7 | 8 | module Percentage = struct 9 | let clamp (lower, upper) = min upper >> max lower 10 | 11 | let of_float = 12 | let percentage x = clamp (0, 100) (Float.to_int (x *. 100.)) in 13 | let pp ppf x = Format.fprintf ppf "%3.0d%%" (percentage x) in 14 | let to_string x = Format.asprintf "%3.0d%%" (percentage x) in 15 | Printer.create ~string_len:4 ~to_string ~pp () 16 | end 17 | 18 | module Bytes = struct 19 | let rec power = function 1 -> 1024L | n -> Int64.mul 1024L (power (n - 1)) 20 | let conv exp = Int64.(of_int >> mul (power exp)) 21 | let kib = conv 1 22 | let mib = conv 2 23 | let gib = conv 3 24 | let tib = conv 4 25 | let pib = conv 5 26 | 27 | (** Pretty-printer for byte counts *) 28 | let generic (type a) (module Integer : Integer.S with type t = a) = 29 | let process_components x k = 30 | let mantissa, unit, rpad = 31 | match[@ocamlformat "disable"] Integer.to_float x with 32 | | n when Float.(n < 1024. ) -> (n , "B", " ") 33 | | n when Float.(n < 1024. ** 2.) -> (n /. 1024. , "KiB", "") 34 | | n when Float.(n < 1024. ** 3.) -> (n /. (1024. ** 2.), "MiB", "") 35 | | n when Float.(n < 1024. ** 4.) -> (n /. (1024. ** 3.), "GiB", "") 36 | | n when Float.(n < 1024. ** 5.) -> (n /. (1024. ** 4.), "TiB", "") 37 | | n when Float.(n < 1024. ** 6.) -> (n /. (1024. ** 5.), "PiB", "") 38 | | n -> (n /. (1024. ** 6.), "EiB", "") 39 | in 40 | (* Round down to the nearest 0.1 *) 41 | let mantissa = Float.trunc (mantissa *. 10.) /. 10. in 42 | let lpad = 43 | match mantissa with 44 | | n when Float.(n < 10.) -> " " 45 | | n when Float.(n < 100.) -> " " 46 | | n when Float.(n < 1000.) -> " " 47 | | _ -> "" 48 | in 49 | k ~mantissa ~unit ~rpad ~lpad 50 | in 51 | let pp ppf x = 52 | process_components x (fun ~mantissa ~unit ~rpad:_ ~lpad:_ -> 53 | Fmt.pf ppf "%.1f %s" mantissa unit) 54 | in 55 | let to_string x = 56 | process_components x (fun ~mantissa ~unit ~rpad ~lpad -> 57 | Printf.sprintf "%s%.1f %s%s" lpad mantissa unit rpad) 58 | in 59 | let string_len = 10 in 60 | Printer.create ~to_string ~string_len ~pp () 61 | 62 | let of_int = generic (module Integer.Int) 63 | let of_int63 = generic (module Integer.Int63) 64 | let of_int64 = generic (module Integer.Int64) 65 | let of_float = generic (module Integer.Float) 66 | let pp_int63 = Printer.to_pp of_int63 67 | end 68 | 69 | module Duration = struct 70 | let mm_ss = 71 | let to_string span = 72 | let span_s = Mtime.span_to_s span in 73 | let minutes = Float.div span_s 60. |> Float.floor in 74 | let seconds = Float.rem span_s 60. |> Float.floor in 75 | (* Reasonable values correctly, but Mtime.Span.max_span must 76 | print to "--:--" as {!Line.eta} relies on it. *) 77 | if Float.compare span_s 0. < 0 || Float.compare minutes 1e4 > 0 then 78 | "--:--" 79 | else Printf.sprintf "%02.0f:%02.0f" minutes seconds 80 | in 81 | Printer.create ~string_len:5 ~to_string () 82 | 83 | let hh_mm_ss = 84 | let to_string span = 85 | let seconds = Mtime.span_to_s span in 86 | if Float.compare seconds 0. < 0 then "--:--:--" 87 | else 88 | Printf.sprintf "%02.0f:%02.0f:%02.0f" 89 | (Float.div seconds 3600. |> Float.floor) 90 | (Float.(rem (div seconds 60.) 60.) |> Float.floor) 91 | (Float.rem seconds 60. |> Float.floor) 92 | in 93 | Printer.create ~string_len:8 ~to_string () 94 | end 95 | 96 | (*———————————————————————————————————————————————————————————————————————————— 97 | Copyright (c) 2020–2021 Craig Ferguson 98 | 99 | Permission to use, copy, modify, and/or distribute this software for any 100 | purpose with or without fee is hereby granted, provided that the above 101 | copyright notice and this permission notice appear in all copies. 102 | 103 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 104 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 105 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 106 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 107 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 108 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 109 | DEALINGS IN THE SOFTWARE. 110 | ————————————————————————————————————————————————————————————————————————————*) 111 | -------------------------------------------------------------------------------- /src/progress/engine/units.mli: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | open! Import 7 | 8 | (** Pretty-printing utilities for common units used in progress bars. *) 9 | 10 | (** Printers for time durations (e.g. [MM:SS], [HH:MM:SS]). *) 11 | module Duration : sig 12 | val mm_ss : Duration.t Printer.t 13 | (** Prints a time span in fixed-width [MM:SS] form. *) 14 | 15 | val hh_mm_ss : Duration.t Printer.t 16 | (** Prints a time span in fixed-width [HH:MM:SS] form. *) 17 | end 18 | 19 | (** Prints a proportion as a percentage (e.g. [42%]). 20 | 21 | {[ 22 | 0. ↦ " 0%" 23 | 0.42 ↦ " 42%" 24 | 0.9999 ↦ " 99%" 25 | 1. ↦ "100%" 26 | ]} 27 | 28 | {b Note:} values will be clamped into the range [[0., 1.]]. *) 29 | module Percentage : sig 30 | val of_float : float Printer.t 31 | end 32 | 33 | (** Prints a numeric value as as a byte count. 34 | 35 | {[ 36 | 0 ↦ " 0.0 B " 37 | 999 ↦ " 999.0 B " 38 | 1024 ↦ " 1.0 KiB" 39 | 1024 * 1023 ↦ "1023.0 KiB" 40 | 1024 * 1024 - 1 ↦ "1023.9 KiB" 41 | ]}*) 42 | module Bytes : sig 43 | val generic : (module Integer.S with type t = 't) -> 't Printer.t 44 | val of_int : int Printer.t 45 | val of_float : float Printer.t 46 | val of_int64 : int64 Printer.t 47 | 48 | (** Quick builders for base-2 byte counts *) 49 | 50 | val kib : int -> int64 51 | (** [kib n] is [n] kibibytes. *) 52 | 53 | val mib : int -> int64 54 | (** [mib n] is [n] mebibytes. *) 55 | 56 | val gib : int -> int64 57 | (** [gib n] is [n] gibibytes. *) 58 | 59 | val tib : int -> int64 60 | (** [tib n] is [n] tebibytes. *) 61 | 62 | val pib : int -> int64 63 | (** [pib n] is [n] pebibytes. *) 64 | 65 | val pp_int63 : int63 Fmt.t 66 | end 67 | 68 | (*———————————————————————————————————————————————————————————————————————————— 69 | Copyright (c) 2020–2021 Craig Ferguson 70 | 71 | Permission to use, copy, modify, and/or distribute this software for any 72 | purpose with or without fee is hereby granted, provided that the above 73 | copyright notice and this permission notice appear in all copies. 74 | 75 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 76 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 77 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 78 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 79 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 80 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 81 | DEALINGS IN THE SOFTWARE. 82 | ————————————————————————————————————————————————————————————————————————————*) 83 | -------------------------------------------------------------------------------- /src/progress/progress.ml: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | module Platform = struct 7 | module Clock = Mtime_clock 8 | module Terminal_width = Terminal_width 9 | end 10 | 11 | include Progress_engine.Make (Platform) 12 | 13 | module Config = struct 14 | include Config 15 | 16 | let stderr_if_tty = 17 | if Unix.(isatty stderr) then Default.ppf 18 | else Format.make_formatter (fun _ _ _ -> ()) (fun () -> ()) 19 | 20 | let v ?(ppf = stderr_if_tty) ?hide_cursor ?persistent ?max_width ?min_interval 21 | () : t = 22 | v ~ppf ?hide_cursor ?persistent ?max_width ?min_interval () 23 | end 24 | 25 | (*———————————————————————————————————————————————————————————————————————————— 26 | Copyright (c) 2020–2021 Craig Ferguson 27 | 28 | Permission to use, copy, modify, and/or distribute this software for any 29 | purpose with or without fee is hereby granted, provided that the above 30 | copyright notice and this permission notice appear in all copies. 31 | 32 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 33 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 34 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 35 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 36 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 37 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 38 | DEALINGS IN THE SOFTWARE. 39 | ————————————————————————————————————————————————————————————————————————————*) 40 | -------------------------------------------------------------------------------- /src/progress/progress.mli: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | (** A library for displaying progress bars, including support for rendering 7 | multiple bars at once. Start by {{!description} describing} of a sequence of 8 | progress bars, then begin {{!rendering} rendering} them. 9 | 10 | {[ 11 | (** Description: "⠼️ [###########--------------------] 37/100" *) 12 | let bar ~total = 13 | let open Progress.Line in 14 | list [ spinner (); bar total; count_to total ] 15 | 16 | (** Rendering: get access to a function [f] for reporting progress. *) 17 | let run () = 18 | Progress.with_reporter (bar ~total:100) (fun f -> 19 | for i = 1 to 100 do 20 | (* ... do some work ... *) 21 | f 1 (* report some progress *) 22 | done) 23 | ]} 24 | 25 | See {!Progress_engine} for an equivalent API that is portable to non-Unix 26 | platforms. *) 27 | 28 | include Progress_engine.S 29 | (** @inline *) 30 | 31 | (*———————————————————————————————————————————————————————————————————————————— 32 | Copyright (c) 2020–2021 Craig Ferguson 33 | 34 | Permission to use, copy, modify, and/or distribute this software for any 35 | purpose with or without fee is hereby granted, provided that the above 36 | copyright notice and this permission notice appear in all copies. 37 | 38 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 39 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 40 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 41 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 42 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 43 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 44 | DEALINGS IN THE SOFTWARE. 45 | ————————————————————————————————————————————————————————————————————————————*) 46 | -------------------------------------------------------------------------------- /src/progress/terminal_width.ml: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | let latest_width = ref None 7 | let refresh () = latest_width := Terminal.Size.get_columns () 8 | let initialize = lazy (refresh ()) 9 | 10 | let get () = 11 | Lazy.force initialize; 12 | !latest_width 13 | 14 | let set_changed_callback on_change = 15 | Terminal.Size.set_changed_callback (fun () -> 16 | refresh (); 17 | on_change !latest_width) 18 | 19 | (*———————————————————————————————————————————————————————————————————————————— 20 | Copyright (c) 2020–2021 Craig Ferguson 21 | 22 | Permission to use, copy, modify, and/or distribute this software for any 23 | purpose with or without fee is hereby granted, provided that the above 24 | copyright notice and this permission notice appear in all copies. 25 | 26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 29 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 30 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 31 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 32 | DEALINGS IN THE SOFTWARE. 33 | ————————————————————————————————————————————————————————————————————————————*) 34 | -------------------------------------------------------------------------------- /src/progress/tests/common.ml: -------------------------------------------------------------------------------- 1 | let check_invalid ~__POS__:pos f = 2 | match f () with 3 | | _ -> 4 | Alcotest.fail ~pos 5 | "Expected [Invalid_argument], but no exception was raised." 6 | | exception Invalid_argument _ -> () 7 | -------------------------------------------------------------------------------- /src/progress/tests/dune: -------------------------------------------------------------------------------- 1 | (test 2 | (name test) 3 | (package progress) 4 | (libraries progress alcotest astring fmt mtime)) 5 | -------------------------------------------------------------------------------- /src/progress/tests/test.ml: -------------------------------------------------------------------------------- 1 | open Astring 2 | 3 | let ( -- ), ( // ) = Int64.(sub, div) 4 | let almost f = f -. Float.epsilon 5 | let ( let@ ) f x = f x 6 | 7 | let config = 8 | Progress.Config.v ~ppf:Format.str_formatter ~hide_cursor:false 9 | ~persistent:true ~max_width:(Some 50) ~min_interval:None () 10 | 11 | let read_bar () = 12 | Format.flush_str_formatter () 13 | |> String.trim ~drop:(function '\r' | '\n' -> true | _ -> false) 14 | 15 | let check_bar ~__POS__:pos expected = 16 | Alcotest.check ~pos 17 | Alcotest.(testable Fmt.Dump.string String.equal) 18 | ("Expected state: " ^ expected) 19 | expected (read_bar ()) 20 | 21 | let clear_test_state () = ignore (Format.flush_str_formatter () : string) 22 | 23 | let test_pair () = 24 | let bar = 25 | Progress.( 26 | Line.( 27 | pair ~sep:(const ", ") 28 | (of_printer ~init:0 (Printer.int ~width:1)) 29 | (of_printer ~init:"foo" (Printer.string ~width:3)))) 30 | in 31 | let () = 32 | let@ report = Progress.with_reporter ~config bar in 33 | check_bar ~__POS__ "0, foo"; 34 | report (1, "bar"); 35 | check_bar ~__POS__ "1, bar" 36 | in 37 | check_bar ~__POS__ "1, bar" 38 | 39 | let test_unicode_bar () = 40 | let () = 41 | let@ report = 42 | Progress.Line.Using_float.bar ~data:`Latest ~style:`UTF8 ~width:(`Fixed 3) 43 | 1. 44 | |> Progress.with_reporter ~config 45 | in 46 | let expect ~__POS__:pos s f = 47 | report f; 48 | check_bar ~__POS__:pos s 49 | in 50 | check_bar ~__POS__ "│ │"; 51 | expect ~__POS__ "" 0.; 52 | expect ~__POS__ "" (almost (1. /. 8.)); 53 | expect ~__POS__ "│▏│" (1. /. 8.); 54 | expect ~__POS__ "" (almost (2. /. 8.)); 55 | expect ~__POS__ "│▎│" (2. /. 8.); 56 | expect ~__POS__ "" (almost (3. /. 8.)); 57 | expect ~__POS__ "│▍│" (3. /. 8.); 58 | expect ~__POS__ "" (almost (4. /. 8.)); 59 | expect ~__POS__ "│▌│" (4. /. 8.); 60 | expect ~__POS__ "" (almost (5. /. 8.)); 61 | expect ~__POS__ "│▋│" (5. /. 8.); 62 | expect ~__POS__ "" (almost (6. /. 8.)); 63 | expect ~__POS__ "│▊│" (6. /. 8.); 64 | expect ~__POS__ "" (almost (7. /. 8.)); 65 | expect ~__POS__ "│▉│" (7. /. 8.); 66 | expect ~__POS__ "" (almost 1.); 67 | expect ~__POS__ "│█│" 1.; 68 | expect ~__POS__ "" (1. +. Float.epsilon); 69 | expect ~__POS__ "" (1. +. (1. /. 8.)); 70 | expect ~__POS__ "" (almost 2.) 71 | in 72 | clear_test_state (); 73 | let () = 74 | let@ report = 75 | Progress.Line.Using_float.bar ~data:`Latest ~style:`UTF8 ~width:(`Fixed 5) 76 | 1. 77 | |> Progress.with_reporter ~config 78 | in 79 | let expect s f = 80 | report f; 81 | check_bar s 82 | in 83 | check_bar ~__POS__ "│ │"; 84 | expect ~__POS__ "" 0.; 85 | expect ~__POS__ "│█▌ │" 0.5; 86 | expect ~__POS__ "│██▉│" (almost 1.); 87 | expect ~__POS__ "│███│" 1. 88 | in 89 | () 90 | 91 | let test_progress_bar_lifecycle () = 92 | let open Progress.Units.Bytes in 93 | let@ report = 94 | let open Progress.Line.Using_int64 in 95 | let total = gib 1 in 96 | list 97 | [ const "" 98 | ; bytes 99 | ; bar ~style:`ASCII ~width:(`Fixed 29) total 100 | ++ const " " 101 | ++ percentage_of total 102 | ] 103 | |> Progress.with_reporter ~config 104 | in 105 | check_bar ~__POS__ " 0.0 B [---------------------------] 0%"; 106 | report (kib 1 -- 1L); 107 | check_bar ~__POS__ " 1023.0 B [---------------------------] 0%"; 108 | report 1L; 109 | check_bar ~__POS__ " 1.0 KiB [---------------------------] 0%"; 110 | report (mib 1 -- kib 1 -- 1L); 111 | (* Should always round downwards. *) 112 | check_bar ~__POS__ " 1023.9 KiB [---------------------------] 0%"; 113 | report 1L; 114 | check_bar ~__POS__ " 1.0 MiB [---------------------------] 0%"; 115 | report (mib 49); 116 | check_bar ~__POS__ " 50.0 MiB [#--------------------------] 4%"; 117 | report (mib 450); 118 | check_bar ~__POS__ " 500.0 MiB [#############--------------] 48%"; 119 | report (gib 1 -- mib 500 -- 1L); 120 | (* 1 byte from completion. Should show 99% and not a full 1024 MiB. *) 121 | check_bar ~__POS__ " 1023.9 MiB [##########################-] 99%"; 122 | report 1L; 123 | (* Now exactly complete *) 124 | check_bar ~__POS__ " 1.0 GiB [###########################] 100%"; 125 | (* Subsequent reports don't overflow the bar *) 126 | report (gib 1 // 2L); 127 | check_bar ~__POS__ " 1.5 GiB [###########################] 100%"; 128 | () 129 | 130 | let test_progress_bar_width () = 131 | let check_width width = 132 | clear_test_state (); 133 | let@ _report = 134 | let open Progress.Line.Using_int64 in 135 | Progress.with_reporter ~config 136 | (bar ~style:`ASCII ~width:(`Fixed width) 1L) 137 | in 138 | let s = read_bar () in 139 | String.length s 140 | |> Alcotest.(check int) (Fmt.str "Expected width of %d: `%S`" width s) width 141 | in 142 | check_width 80; 143 | check_width 40; 144 | Alcotest.check_raises "Overly small progress bar" 145 | (Failure "Not enough space for a progress bar") (fun () -> check_width 2) 146 | 147 | let test_preprovided_counter () = 148 | let pp = Progress.Printer.(using ~f:Int64.to_int (int ~width:3)) in 149 | let@ report = Progress.counter ~pp 999L |> Progress.with_reporter ~config in 150 | check_bar ~__POS__ " 0 00:00 [---------------------------------] 0%"; 151 | report 1L; 152 | check_bar ~__POS__ " 1 00:00 [---------------------------------] 0%"; 153 | report 1L; 154 | check_bar ~__POS__ " 2 00:00 [---------------------------------] 0%"; 155 | report 10L; 156 | check_bar ~__POS__ " 12 00:00 [---------------------------------] 1%"; 157 | report 100L; 158 | check_bar ~__POS__ "112 00:00 [###------------------------------] 11%"; 159 | report 886L; 160 | check_bar ~__POS__ "998 00:00 [################################-] 99%"; 161 | report 1L; 162 | check_bar ~__POS__ "999 00:00 [#################################] 100%" 163 | 164 | module Boxes = struct 165 | let unsized = 166 | Progress.Line.Internals.alpha_unsized ~initial:(`Val ()) 167 | (fun ~width:_ _ _ _ -> 0) 168 | 169 | let test_unsized_not_in_box () = 170 | Alcotest.check_raises "Unsized element not contained in a box" 171 | (Invalid_argument 172 | "Encountered an expanding element that is not contained in a box") 173 | @@ fun () -> 174 | Progress.(with_reporter Line.Internals.(to_line unsized) Fun.id ()) 175 | 176 | let test_two_unsized_in_box () = 177 | Alcotest.check_raises "Two unsized elements in a box" 178 | (Invalid_argument 179 | "Multiple expansion points encountered. Cannot pack two unsized \ 180 | segments in a single box.") 181 | @@ fun () -> 182 | Progress.( 183 | with_reporter 184 | Line.Internals.(to_line @@ box_fixed 10 (array [| unsized; unsized |])) 185 | Fun.id ()) 186 | end 187 | 188 | let () = 189 | let open Alcotest in 190 | run __FILE__ 191 | [ ( "main" 192 | , [ test_case "Pair" `Quick test_pair 193 | ; test_case "Unicode bar" `Quick test_unicode_bar 194 | ; test_case "Progress bar lifecycle" `Quick test_progress_bar_lifecycle 195 | ; test_case "Progress bar width" `Quick test_progress_bar_width 196 | ; test_case "Pre-provided counter" `Quick test_preprovided_counter 197 | ] ) 198 | ; ( "boxes" 199 | , [ test_case "Unsized element not in box" `Quick 200 | Boxes.test_unsized_not_in_box 201 | ; test_case "Two unsized elements in box" `Quick 202 | Boxes.test_two_unsized_in_box 203 | ] ) 204 | ; ("units", Test_units.tests) 205 | ; ("printers", Test_printers.tests) 206 | ; ("flow_meter", Test_flow_meter.tests) 207 | ] 208 | -------------------------------------------------------------------------------- /src/progress/tests/test.mli: -------------------------------------------------------------------------------- 1 | (* intentionally empty *) 2 | -------------------------------------------------------------------------------- /src/progress/tests/test_flow_meter.ml: -------------------------------------------------------------------------------- 1 | open struct 2 | module Flow_meter = Progress_engine.Exposed_for_testing.Flow_meter 3 | module Duration = Progress.Duration 4 | end 5 | 6 | let gen_test_state () = 7 | let now = ref (Mtime.of_uint64_ns 0L) in 8 | let clock () = !now in 9 | let flow_meter = Flow_meter.create ~clock ~size:3 ~elt:(module Int) in 10 | let record ~interval x = 11 | now := Option.get (Mtime.add_span !now interval); 12 | Flow_meter.record flow_meter x 13 | in 14 | let check pos v = 15 | Alcotest.(check ~pos int) 16 | "Expected flow rate" v 17 | (Flow_meter.per_second flow_meter |> Int.of_float) 18 | in 19 | (record, check) 20 | 21 | (* Tests of flow rate in which samples are all 1ms apart. *) 22 | let test_linear () = 23 | let record, check = gen_test_state () in 24 | let record = record ~interval:Duration.millisecond in 25 | (* Initial flow is 0 *) 26 | check __POS__ 0; 27 | check __POS__ 0; 28 | (* Flow rate is 0 until we have at least 2 samples *) 29 | record 1; 30 | check __POS__ 0; 31 | record 1; 32 | check __POS__ 1000 (* 1 in 1 ms *); 33 | record 0; 34 | check __POS__ 500 (* (1 + 0) in 2 ms *); 35 | record 1; 36 | check __POS__ 666 (* (1 + 0 + 1) in 3 ms *); 37 | record 1; 38 | check __POS__ 666 (* (0 + 1 + 1) in 3 ms *); 39 | record 1; 40 | check __POS__ 1000 (* (1 + 1 + 1) in 3 ms *); 41 | record (-1); 42 | check __POS__ 333 (* (1 + 1 + -1) in 3 ms *); 43 | record (-1); 44 | check __POS__ (-333) (* (1 + -1 + -1) in 3 ms *); 45 | record 2; 46 | check __POS__ 0 (* (-1 + -1 + 2) in 3 ms *); 47 | () 48 | 49 | let test_non_linear () = 50 | let record, check = gen_test_state () in 51 | (* Initial flow is 0 *) 52 | check __POS__ 0; 53 | check __POS__ 0; 54 | (* Flow rate is 0 until we have at least 2 samples *) 55 | record ~interval:Duration.zero 1; 56 | record ~interval:Duration.zero 1; 57 | record ~interval:Duration.zero 1; 58 | check __POS__ 0 (* 3 in 0 ms *); 59 | record ~interval:Duration.millisecond 0; 60 | check __POS__ 2000 (* (1 + 1 + 0) in 1 ms *); 61 | record ~interval:Duration.zero 0; 62 | check __POS__ 1000 (* (1 + 0 + 0) in 1 ms *); 63 | record ~interval:Duration.zero 0; 64 | check __POS__ 0 (* (0 + 0 + 0) in 1 ms *); 65 | record ~interval:Duration.zero 3; 66 | check __POS__ 0 (* (0 + 0 + 3) in 0 ms *); 67 | () 68 | 69 | let tests = 70 | [ ("linear", `Quick, test_linear); ("non-linear", `Quick, test_non_linear) ] 71 | -------------------------------------------------------------------------------- /src/progress/tests/test_flow_meter.mli: -------------------------------------------------------------------------------- 1 | val tests : unit Alcotest.test_case list 2 | -------------------------------------------------------------------------------- /src/progress/tests/test_printers.ml: -------------------------------------------------------------------------------- 1 | open Common 2 | 3 | open struct 4 | module Printer = Progress.Printer 5 | module Color = Progress.Color 6 | 7 | let check_print print ~__POS__:pos str x = 8 | Alcotest.(check ~pos (testable Fmt.Dump.string String.equal)) 9 | "" str (print x) 10 | end 11 | 12 | let test_int () = 13 | let print = Printer.(to_to_string (int ~width:5)) in 14 | let check = check_print print in 15 | check ~__POS__ "12345" 12345; 16 | check ~__POS__ "-2345" (-2345); 17 | check ~__POS__ " 1" 1; 18 | check ~__POS__ " -1" (-1); 19 | check_invalid ~__POS__ (fun () -> print 123456); 20 | check_invalid ~__POS__ (fun () -> print (-23456)); 21 | () 22 | 23 | let test_string () = 24 | let check ?(width = 8) = check_print Printer.(to_to_string (string ~width)) in 25 | 26 | (* ASCII strings *) 27 | check ~__POS__ " " ""; 28 | check ~__POS__ "hello " "hello"; 29 | check ~__POS__ " hello " " hello"; 30 | check ~__POS__ "hello me" "hello me"; 31 | check ~__POS__ "hello..." "hello world"; 32 | 33 | (* Short ASCII strings (within maximum ellipsis size) *) 34 | check ~__POS__ ~width:3 "abc" "abc"; 35 | check ~__POS__ ~width:3 "..." "abcd"; 36 | check ~__POS__ ~width:2 "ab" "ab"; 37 | check ~__POS__ ~width:2 ".." "abc"; 38 | check ~__POS__ ~width:1 "a" "a"; 39 | check ~__POS__ ~width:1 "." "ab"; 40 | check ~__POS__ ~width:0 "" ""; 41 | check ~__POS__ ~width:0 "" "a"; 42 | 43 | (* Non-ASCII UTF8 strings *) 44 | check ~__POS__ "————————" "————————"; 45 | check ~__POS__ "—————..." "—————————"; 46 | check ~__POS__ ~width:3 "———" "———"; 47 | check ~__POS__ ~width:3 "..." "————"; 48 | check ~__POS__ ~width:2 ".." "———"; 49 | check ~__POS__ ~width:1 "." "——"; 50 | 51 | (* Strings containing ANSI colour escapes *) 52 | let () = 53 | let col c s = 54 | Terminal.Style.(code (fg (Color.ansi c))) ^ s ^ Terminal.Style.(code none) 55 | in 56 | (* Build up a coloured "hello world" string, retaining prefixes *) 57 | let h = col `red "h" in 58 | let he = h ^ col `blue "e" in 59 | let hel = he ^ col `green "l" in 60 | let hell = hel ^ "l" in 61 | let hello = hell ^ col `red "o" in 62 | let hello_world = hello ^ col `magenta " " ^ col `yellow "w" ^ "orld" in 63 | 64 | (* Check that ppadding is applied correctly *) 65 | check ~width:13 ~__POS__ (hello_world ^ " ") hello_world; 66 | check ~width:12 ~__POS__ (hello_world ^ " ") hello_world; 67 | check ~width:11 ~__POS__ hello_world hello_world; 68 | 69 | (* Check that it truncates correctly at each point *) 70 | check ~width:8 ~__POS__ (hello ^ "...") hello_world; 71 | check ~width:7 ~__POS__ (hell ^ "...") hello_world; 72 | check ~width:6 ~__POS__ (hel ^ "...") hello_world; 73 | check ~width:5 ~__POS__ (he ^ "...") hello_world; 74 | check ~width:4 ~__POS__ (h ^ "...") hello_world; 75 | check ~width:3 ~__POS__ "..." hello_world; 76 | check ~width:2 ~__POS__ ".." hello_world; 77 | check ~width:1 ~__POS__ "." hello_world; 78 | check ~width:0 ~__POS__ "" hello_world 79 | in 80 | 81 | () 82 | 83 | let tests = 84 | Alcotest. 85 | [ test_case "int" `Quick test_int; test_case "string" `Quick test_string ] 86 | -------------------------------------------------------------------------------- /src/progress/tests/test_printers.mli: -------------------------------------------------------------------------------- 1 | val tests : unit Alcotest.test_case list 2 | -------------------------------------------------------------------------------- /src/progress/tests/test_units.ml: -------------------------------------------------------------------------------- 1 | let almost f = f -. Float.epsilon 2 | let ( - ) = Int64.sub 3 | 4 | let expect_pp_fixed ?relaxed_length ~pp:pp_fixed s f = 5 | let result = Progress.Printer.to_to_string pp_fixed f in 6 | Alcotest.(check string) (Fmt.str "Expected rendering of %s" s) s result; 7 | match relaxed_length with 8 | | Some () -> () 9 | | None -> 10 | Alcotest.(check int) 11 | "Expected length" 12 | (Progress.Printer.print_width pp_fixed) 13 | (String.length result) 14 | 15 | let test_percentage () = 16 | let expect = expect_pp_fixed ~pp:Progress.Units.Percentage.of_float in 17 | expect " 0%" (-0.1); 18 | expect " 0%" (almost 0.01); 19 | expect " 1%" 0.01; 20 | expect " 1%" (0.01 +. Float.epsilon); 21 | expect " 10%" 0.1; 22 | expect " 50%" 0.5; 23 | expect " 99%" (almost 1.); 24 | expect "100%" 1.; 25 | expect "100%" (1. +. Float.epsilon); 26 | expect "100%" 1.1; 27 | () 28 | 29 | let test_bytes () = 30 | let expect = expect_pp_fixed ~pp:Progress.Units.Bytes.of_int64 in 31 | let open Progress.Units.Bytes in 32 | expect " 0.0 B " 0L; 33 | expect " 999.0 B " 999L; 34 | expect " 1.0 KiB" (kib 1); 35 | expect "1023.0 KiB" (mib 1 - kib 1); 36 | expect "1023.9 KiB" (mib 1 - 1L); 37 | expect " 1.0 MiB" (mib 1); 38 | expect " 1.0 TiB" (tib 1); 39 | expect " 1.0 PiB" (pib 1); 40 | expect " 1.0 EiB" (pib 1024); 41 | () 42 | 43 | let test_duration () = 44 | let module D = Progress.Duration.Of_int in 45 | let () = 46 | let expect = expect_pp_fixed ~pp:Progress.Units.Duration.mm_ss in 47 | expect "00:00" D.(sec 0); 48 | expect "00:29" D.(sec 29); 49 | expect "00:30" D.(sec 30); 50 | expect "00:30" D.(ms 30_400); 51 | expect "00:59" D.(ms 59_600); 52 | expect "01:00" D.(min 1); 53 | expect "01:00" D.(min 1 + ms 400); 54 | expect "99:00" D.(min 99); 55 | expect "99:59" D.(min 99 + sec 59); 56 | (* [eta] relies on this: *) 57 | expect "--:--" Mtime.Span.max_span; 58 | (* Fail gracefully: *) 59 | expect ~relaxed_length:() "100:00" D.(min 100) 60 | in 61 | 62 | let () = 63 | let expect = expect_pp_fixed ~pp:Progress.Units.Duration.hh_mm_ss in 64 | expect "00:00:00" D.(sec 0); 65 | expect "00:00:59" D.(ms 59_600); 66 | expect "00:01:00" D.(min 1); 67 | expect "00:01:00" D.(min 1 + ms 400); 68 | expect "00:59:59" D.(min 59 + sec 59 + ms 999); 69 | expect "01:00:00" D.(hour 1); 70 | expect "01:00:00" D.(hour 1 + ms 1); 71 | expect "99:59:59" D.(hour 99 + min 59 + sec 59 + ms 999); 72 | (* Fail gracefully: *) 73 | expect ~relaxed_length:() "100:00:00" D.(hour 100) 74 | in 75 | 76 | () 77 | 78 | let tests = 79 | Alcotest. 80 | [ test_case "percentage" `Quick test_percentage 81 | ; test_case "bytes" `Quick test_bytes 82 | ; test_case "duration" `Quick test_duration 83 | ] 84 | -------------------------------------------------------------------------------- /src/progress/tests/test_units.mli: -------------------------------------------------------------------------------- 1 | val tests : unit Alcotest.test_case list 2 | -------------------------------------------------------------------------------- /src/terminal/ansi/ansi.ml: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | open! Import 7 | 8 | let malformed_string s = 9 | Format.kasprintf invalid_arg "Terminal.Ansi: malformed UTF-8 string: %S" s 10 | 11 | module Length_counter = struct 12 | (* Counting length of UTF-8 strings while skipping ANSI escape sequences. See 13 | https://en.wikipedia.org/wiki/ANSI_escape_code#Fe_Escape_sequences for 14 | details. *) 15 | type t = 16 | { mutable acc : int 17 | ; mutable state : 18 | [ `Normal 19 | | `Parsing_ansi_sequence (* Read '\x1b', but not the subsequent byte. *) 20 | | `Ansi_parameter_bytes (* Inside a CSI parameter sequence *) ] 21 | } 22 | 23 | let empty () = { acc = 0; state = `Normal } 24 | let is_initial_ansi_byte c = Char.equal c '\x1b' 25 | 26 | let is_final_ansi_byte c = 27 | let c = Char.code c in 28 | c >= 0x40 && c <= 0x7e 29 | 30 | let guess_printed_char_length c = 31 | match Uucp.Break.tty_width_hint c with 32 | | -1 -> 1 (* Assume width of 1 if [Uucp] can't guess *) 33 | | n -> n 34 | 35 | let add t c = 36 | match Uchar.is_char c with 37 | | false -> t.acc <- t.acc + guess_printed_char_length c 38 | | true -> ( 39 | let c = Uchar.to_char c in 40 | match t.state with 41 | | `Normal -> 42 | if is_initial_ansi_byte c then t.state <- `Parsing_ansi_sequence 43 | else t.acc <- t.acc + 1 44 | | `Parsing_ansi_sequence -> 45 | if Char.equal c '[' (* Control sequence introducer *) then 46 | t.state <- `Ansi_parameter_bytes 47 | else t.state <- `Normal 48 | | `Ansi_parameter_bytes -> 49 | if is_final_ansi_byte c then t.state <- `Normal) 50 | 51 | let count t = t.acc 52 | end 53 | 54 | let guess_printed_width s = 55 | let count = Length_counter.empty () in 56 | Uutf.String.fold_utf_8 57 | (fun () _ -> function 58 | | `Malformed _ -> malformed_string s 59 | | `Uchar c -> Length_counter.add count c) 60 | () s; 61 | Length_counter.count count 62 | 63 | let uchar_size u = 64 | match Uchar.to_int u with 65 | | u when u < 0 -> assert false 66 | | u when u <= 0x007F -> 1 67 | | u when u <= 0x07FF -> 2 68 | | u when u <= 0xFFFF -> 3 69 | | u when u <= 0x10FFFF -> 4 70 | | _ -> assert false 71 | 72 | exception Exit of int 73 | 74 | let string_equal : string -> string -> bool = ( = ) 75 | 76 | let truncate_to_width width s = 77 | if width < 0 then 78 | Format.kasprintf invalid_arg 79 | "Terminal.truncate_to_width: negative width %d requested" width; 80 | let count = Length_counter.empty () in 81 | try 82 | Uutf.String.fold_utf_8 83 | (fun () i -> function 84 | | `Malformed _ -> malformed_string s 85 | | `Uchar c -> 86 | if Length_counter.count count = width then 87 | (* Check for display reset, and add it if it's there; truncating 88 | this would cause the open colour to leak. *) 89 | let display_reset = "\027[0m" in 90 | if 91 | i + 4 <= String.length s 92 | && string_equal (String.sub s ~pos:i ~len:4) display_reset 93 | then raise (Exit (i + 4)) 94 | else raise (Exit i) 95 | else ( 96 | Length_counter.add count c; 97 | let count = Length_counter.count count in 98 | if count <= width then () else raise (Exit i))) 99 | () s; 100 | s 101 | with Exit len -> String.sub s ~pos:0 ~len 102 | 103 | let show_cursor = "\x1b[?25h" 104 | let hide_cursor = "\x1b[?25l" 105 | let erase_display_suffix = "\x1b[J" 106 | let erase_line = "\x1b[K" 107 | let move_up ppf = function 0 -> () | n -> Format.fprintf ppf "\x1b[%dA" n 108 | let move_down ppf = function 0 -> () | n -> Format.fprintf ppf "\x1b[%dB" n 109 | 110 | (*———————————————————————————————————————————————————————————————————————————— 111 | Copyright (c) 2020–2021 Craig Ferguson 112 | 113 | Permission to use, copy, modify, and/or distribute this software for any 114 | purpose with or without fee is hereby granted, provided that the above 115 | copyright notice and this permission notice appear in all copies. 116 | 117 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 118 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 119 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 120 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 121 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 122 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 123 | DEALINGS IN THE SOFTWARE. 124 | ————————————————————————————————————————————————————————————————————————————*) 125 | -------------------------------------------------------------------------------- /src/terminal/ansi/color.ml: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | type plain = 7 | [ `black | `blue | `cyan | `green | `magenta | `red | `white | `yellow ] 8 | 9 | type t = Ansi of [ plain | `bright of plain ] | Rgb of int * int * int 10 | 11 | let pp_plain ppf x = 12 | Format.fprintf ppf 13 | (match x with 14 | | `black -> "black" 15 | | `blue -> "blue" 16 | | `cyan -> "cyan" 17 | | `green -> "green" 18 | | `magenta -> "magenta" 19 | | `red -> "red" 20 | | `white -> "white" 21 | | `yellow -> "yellow") 22 | 23 | let pp_dump ppf = function 24 | | Rgb (r, g, b) -> Format.fprintf ppf "RGB (%d, %d, %d)" r g b 25 | | Ansi (#plain as x) -> Format.fprintf ppf "ANSI (%a)" pp_plain x 26 | | Ansi (`bright x) -> Format.fprintf ppf "ANSI (bright %a)" pp_plain x 27 | 28 | let ansi x = Ansi x 29 | 30 | let rgb = 31 | let invalid_component typ n = 32 | Format.kasprintf invalid_arg "Color.rgb: invalid %s component %d" typ n 33 | in 34 | fun r g b -> 35 | if r < 0 || r > 255 then invalid_component "red" r; 36 | if g < 0 || g > 255 then invalid_component "green" g; 37 | if b < 0 || b > 255 then invalid_component "blue" b; 38 | Rgb (r, g, b) 39 | 40 | let hex = 41 | let invalid_length = 42 | Format.kasprintf invalid_arg "Color.hex: invalid hexstring length %d" 43 | in 44 | let hex c = 45 | if c >= '0' && c <= '9' then Char.code c - Char.code '0' 46 | else if c >= 'a' && c <= 'f' then Char.code c - Char.code 'a' + 10 47 | else if c >= 'A' && c <= 'F' then Char.code c - Char.code 'A' + 10 48 | else 49 | Format.kasprintf invalid_arg "Color.hex: invalid hexstring character %c" c 50 | in 51 | fun s -> 52 | let len = String.length s in 53 | if len = 0 then invalid_length len; 54 | if s.[0] <> '#' then invalid_arg "Color.hex: hexstrings must start with '#'"; 55 | let r1, r0, g1, g0, b1, b0 = 56 | match len with 57 | | 7 -> (hex s.[1], hex s.[2], hex s.[3], hex s.[4], hex s.[5], hex s.[6]) 58 | | 4 -> 59 | (* Short hexstrings of the form #ABC alias longer ones of the form #AABBCC *) 60 | let r, g, b = (hex s.[1], hex s.[2], hex s.[3]) in 61 | (r, r, g, g, b, b) 62 | | _ -> invalid_length len 63 | in 64 | rgb ((16 * r1) + r0) ((16 * g1) + g0) ((16 * b1) + b0) 65 | 66 | (*———————————————————————————————————————————————————————————————————————————— 67 | Copyright (c) 2020–2021 Craig Ferguson 68 | 69 | Permission to use, copy, modify, and/or distribute this software for any 70 | purpose with or without fee is hereby granted, provided that the above 71 | copyright notice and this permission notice appear in all copies. 72 | 73 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 74 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 75 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 76 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 77 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 78 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 79 | DEALINGS IN THE SOFTWARE. 80 | ————————————————————————————————————————————————————————————————————————————*) 81 | -------------------------------------------------------------------------------- /src/terminal/ansi/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name terminal_ansi) 3 | (public_name terminal.ansi) 4 | (libraries stdlib-shims uutf uucp)) 5 | -------------------------------------------------------------------------------- /src/terminal/ansi/import.ml: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | include Stdlib.StdLabels 7 | include Stdlib.MoreLabels 8 | 9 | (*———————————————————————————————————————————————————————————————————————————— 10 | Copyright (c) 2020–2021 Craig Ferguson 11 | 12 | Permission to use, copy, modify, and/or distribute this software for any 13 | purpose with or without fee is hereby granted, provided that the above 14 | copyright notice and this permission notice appear in all copies. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 19 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 22 | DEALINGS IN THE SOFTWARE. 23 | ————————————————————————————————————————————————————————————————————————————*) 24 | -------------------------------------------------------------------------------- /src/terminal/ansi/style.ml: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | type t = 7 | | None 8 | | Bold 9 | | Faint 10 | | Italic 11 | | Underline 12 | | Reverse 13 | | Fg of Color.t 14 | | Bg of Color.t 15 | 16 | let none = None 17 | let bold = Bold 18 | let faint = Faint 19 | let italic = Italic 20 | let underline = Underline 21 | let reverse = Reverse 22 | let fg x = Fg x 23 | let bg x = Bg x 24 | 25 | let code : t -> string = function 26 | | None -> "\x1b[0m" 27 | | Bold -> "\x1b[1m" 28 | | Faint -> "\x1b[2m" 29 | | Italic -> "\x1b[3m" 30 | | Underline -> "\x1b[4m" 31 | | Reverse -> "\x1b[7m" 32 | | Fg (Ansi `black) -> "\x1b[30m" 33 | | Fg (Ansi `red) -> "\x1b[31m" 34 | | Fg (Ansi `green) -> "\x1b[32m" 35 | | Fg (Ansi `yellow) -> "\x1b[33m" 36 | | Fg (Ansi `blue) -> "\x1b[34m" 37 | | Fg (Ansi `magenta) -> "\x1b[35m" 38 | | Fg (Ansi `cyan) -> "\x1b[36m" 39 | | Fg (Ansi `white) -> "\x1b[37m" 40 | | Bg (Ansi `black) -> "\x1b[40m" 41 | | Bg (Ansi `red) -> "\x1b[41m" 42 | | Bg (Ansi `green) -> "\x1b[42m" 43 | | Bg (Ansi `yellow) -> "\x1b[43m" 44 | | Bg (Ansi `blue) -> "\x1b[44m" 45 | | Bg (Ansi `magenta) -> "\x1b[45m" 46 | | Bg (Ansi `cyan) -> "\x1b[46m" 47 | | Bg (Ansi `white) -> "\x1b[47m" 48 | | Fg (Ansi (`bright `black)) -> "\x1b[90m" 49 | | Fg (Ansi (`bright `red)) -> "\x1b[91m" 50 | | Fg (Ansi (`bright `green)) -> "\x1b[92m" 51 | | Fg (Ansi (`bright `yellow)) -> "\x1b[93m" 52 | | Fg (Ansi (`bright `blue)) -> "\x1b[94m" 53 | | Fg (Ansi (`bright `magenta)) -> "\x1b[95m" 54 | | Fg (Ansi (`bright `cyan)) -> "\x1b[96m" 55 | | Fg (Ansi (`bright `white)) -> "\x1b[97m" 56 | | Bg (Ansi (`bright `black)) -> "\x1b[100m" 57 | | Bg (Ansi (`bright `red)) -> "\x1b[101m" 58 | | Bg (Ansi (`bright `green)) -> "\x1b[102m" 59 | | Bg (Ansi (`bright `yellow)) -> "\x1b[103m" 60 | | Bg (Ansi (`bright `blue)) -> "\x1b[104m" 61 | | Bg (Ansi (`bright `magenta)) -> "\x1b[105m" 62 | | Bg (Ansi (`bright `cyan)) -> "\x1b[106m" 63 | | Bg (Ansi (`bright `white)) -> "\x1b[107m" 64 | | Fg (Rgb (r, g, b)) -> Printf.sprintf "\x1b[38;2;%d;%d;%dm" r g b 65 | | Bg (Rgb (r, g, b)) -> Printf.sprintf "\x1b[48;2;%d;%d;%dm" r g b 66 | 67 | (*———————————————————————————————————————————————————————————————————————————— 68 | Copyright (c) 2020–2021 Craig Ferguson 69 | 70 | Permission to use, copy, modify, and/or distribute this software for any 71 | purpose with or without fee is hereby granted, provided that the above 72 | copyright notice and this permission notice appear in all copies. 73 | 74 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 75 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 76 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 77 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 78 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 79 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 80 | DEALINGS IN THE SOFTWARE. 81 | ————————————————————————————————————————————————————————————————————————————*) 82 | -------------------------------------------------------------------------------- /src/terminal/ansi/terminal_ansi.ml: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | module Color = Color 7 | module Style = Style 8 | module Ansi = Ansi 9 | 10 | let guess_printed_width, truncate_to_width = 11 | Ansi.(guess_printed_width, truncate_to_width) 12 | 13 | (*———————————————————————————————————————————————————————————————————————————— 14 | Copyright (c) 2020–2021 Craig Ferguson 15 | 16 | Permission to use, copy, modify, and/or distribute this software for any 17 | purpose with or without fee is hereby granted, provided that the above 18 | copyright notice and this permission notice appear in all copies. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 23 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 26 | DEALINGS IN THE SOFTWARE. 27 | ————————————————————————————————————————————————————————————————————————————*) 28 | -------------------------------------------------------------------------------- /src/terminal/ansi/terminal_ansi.mli: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | (** This library provides a small set of standard utility functions for 7 | interacting with terminals. 8 | 9 | Note that this module does not depend on the [unix] library. There is 10 | therefore a way to use [Terminal_ansi] and [progress.engine] in a context 11 | other than a POSIX system (such as a unikernel). To do this, you need to 12 | compose [Terminal_ansi] with a library that looks like [terminal.unix] 13 | (which should allow you to obtain the size of a TTY). It would then be 14 | sufficient to compose the latter (just as the [terminal] library does) to 15 | then be able to use [Progess_engine.Make] with the result of this 16 | composition. *) 17 | 18 | module Color : sig 19 | type t 20 | (** The type of colours that can be rendered to a terminal. *) 21 | 22 | (** {1 4-bit ANSI colours} 23 | 24 | Colours built using {!ansi} will be rendered using the standard 25 | {{:https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit} 4-bit 26 | ANSI escape codes} for terminals. The actual colours displayed to the 27 | user depend on their terminal configuration / theme, ensuring that they 28 | look natural in context. *) 29 | 30 | type plain = 31 | [ `black | `blue | `cyan | `green | `magenta | `red | `white | `yellow ] 32 | 33 | val ansi : [ plain | `bright of plain ] -> t 34 | 35 | (** {1 24-bit RGB colours} 36 | 37 | Most modern terminals offer support for full 24-bit RGB colour (called 38 | "{{:https://en.wikipedia.org/wiki/ANSI_escape_code#24-bit} true colour}") 39 | in addition to the 16 original ANSI colours. These colours are rendered 40 | exactly as requested, offering greater flexibility at the risk of clashing 41 | with the user's theming. *) 42 | 43 | val rgb : int -> int -> int -> t 44 | (** [rgb r g b] is the RGB24 colour with the given red, green and blue colour 45 | components respectively. Raises [Invalid_argument] if any of the 46 | components are outside the range [[0, 255]]. *) 47 | 48 | val hex : string -> t 49 | (** [hex s] is the RGB24 colour given by the 50 | {{:https://en.wikipedia.org/wiki/Web_colors#Hex_triplet} hex triplet} [s], 51 | which must start with [#]. Examples: 52 | 53 | - [hex "#FF8C00"] = [rgb 0xFF 0x8C 0x00] 54 | - [hex "#fa0"] = [rgb 0xFF 0xAA 0x00] 55 | 56 | Raises [Invalid_argument] if the given string is not a [#]-prefixed hex 57 | triplet. *) 58 | 59 | val pp_dump : Format.formatter -> t -> unit 60 | (** Pretty-print a colour with an unspecified format. *) 61 | end 62 | 63 | module Style : sig 64 | type t 65 | (** The type of terminal {i styles}: values that can be printed to a terminal 66 | in order to change the way that it renders text. *) 67 | 68 | val code : t -> string 69 | (** Get the ANSI escape code for the given style. *) 70 | 71 | (** Constructing ANSI styles: *) 72 | 73 | val none : t 74 | val bold : t 75 | val faint : t 76 | val italic : t 77 | val underline : t 78 | val reverse : t 79 | val fg : Color.t -> t 80 | val bg : Color.t -> t 81 | end 82 | 83 | module Ansi : sig 84 | val show_cursor : string 85 | val hide_cursor : string 86 | val move_up : Format.formatter -> int -> unit 87 | val move_down : Format.formatter -> int -> unit 88 | val erase_line : string 89 | val erase_display_suffix : string 90 | end 91 | 92 | val guess_printed_width : string -> int 93 | (** [guess_printed_width s] returns an estimate of the number of terminal 94 | columns that the UTF-8 encoded string [s] would occupy if displayed in a 95 | terminal, after stripping any ANSI escape codes in the string. 96 | 97 | {b Note:} 98 | {i this function uses a heuristic ({!Uucp.tty_width_hint}) to guess the 99 | rendered length of supplied strings. This function is not guaranteed to 100 | be correct on all UTF-8 codepoints. See the [Uucp] documentation for 101 | details.} *) 102 | 103 | val truncate_to_width : int -> string -> string 104 | (** [truncate_to_width n s] is the longest prefix of UTF-8 encoded string [s] 105 | that will fit within [n] columns when displayed in a terminal (without 106 | including unbalanced ANSI control sequences after the [n]-th column). 107 | 108 | As with {!guess_printed_width}, the implementation relies on heuristics and 109 | so may not be accurate for all inputs (or for all terminal implementations).*) 110 | 111 | (*———————————————————————————————————————————————————————————————————————————— 112 | Copyright (c) 2020–2021 Craig Ferguson 113 | 114 | Permission to use, copy, modify, and/or distribute this software for any 115 | purpose with or without fee is hereby granted, provided that the above 116 | copyright notice and this permission notice appear in all copies. 117 | 118 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 119 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 120 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 121 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 122 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 123 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 124 | DEALINGS IN THE SOFTWARE. 125 | ————————————————————————————————————————————————————————————————————————————*) 126 | -------------------------------------------------------------------------------- /src/terminal/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (public_name terminal) 3 | (name terminal) 4 | (foreign_stubs 5 | (language c) 6 | (names terminal_stubs)) 7 | (libraries 8 | (re_export terminal_ansi) 9 | unix)) 10 | -------------------------------------------------------------------------------- /src/terminal/terminal.ml: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | include Terminal_ansi 7 | 8 | module Size = struct 9 | external sigwinch : unit -> int option = "ocaml_terminal_get_sigwinch" 10 | (** The number of the signal used to indicate terminal size changes. [None] on 11 | Windows. *) 12 | 13 | type dimensions = { rows : int; columns : int } 14 | 15 | external get_dimensions : unit -> dimensions option 16 | = "ocaml_terminal_get_terminal_dimensions" 17 | 18 | let get_columns () = 19 | match get_dimensions () with 20 | | Some { columns; _ } -> Some columns 21 | | None -> None 22 | 23 | let get_rows () = 24 | match get_dimensions () with Some { rows; _ } -> Some rows | None -> None 25 | 26 | let on_change = ref (fun _ -> ()) 27 | 28 | let initialise = 29 | let handle_signal _ = !on_change () in 30 | lazy 31 | (match sigwinch () with 32 | | None -> () 33 | | Some n -> Sys.set_signal n (Signal_handle handle_signal)) 34 | 35 | let set_changed_callback f = 36 | Lazy.force initialise; 37 | on_change := f 38 | end 39 | 40 | (*———————————————————————————————————————————————————————————————————————————— 41 | Copyright (c) 2020–2021 Craig Ferguson 42 | 43 | Permission to use, copy, modify, and/or distribute this software for any 44 | purpose with or without fee is hereby granted, provided that the above 45 | copyright notice and this permission notice appear in all copies. 46 | 47 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 48 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 49 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 50 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 51 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 52 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 53 | DEALINGS IN THE SOFTWARE. 54 | ————————————————————————————————————————————————————————————————————————————*) 55 | -------------------------------------------------------------------------------- /src/terminal/terminal.mli: -------------------------------------------------------------------------------- 1 | (*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*) 5 | 6 | (** @inline *) 7 | include 8 | module type of Terminal_ansi 9 | with type Color.t = Terminal_ansi.Color.t 10 | and type Style.t = Terminal_ansi.Style.t 11 | 12 | module Size : sig 13 | (** Functions for getting the size of the terminal to which [stdout] is 14 | attached (provided [stdout] is a TTY). *) 15 | 16 | type dimensions = { rows : int; columns : int } 17 | 18 | val get_dimensions : unit -> dimensions option 19 | val get_columns : unit -> int option 20 | val get_rows : unit -> int option 21 | val set_changed_callback : (unit -> unit) -> unit 22 | end 23 | 24 | (*———————————————————————————————————————————————————————————————————————————— 25 | Copyright (c) 2020–2021 Craig Ferguson 26 | 27 | Permission to use, copy, modify, and/or distribute this software for any 28 | purpose with or without fee is hereby granted, provided that the above 29 | copyright notice and this permission notice appear in all copies. 30 | 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 32 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 33 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 34 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 35 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 36 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 37 | DEALINGS IN THE SOFTWARE. 38 | ————————————————————————————————————————————————————————————————————————————*) 39 | -------------------------------------------------------------------------------- /src/terminal/terminal_stubs.c: -------------------------------------------------------------------------------- 1 | /*———————————————————————————————————————————————————————————————————————————— 2 | Copyright (c) 2020–2021 Craig Ferguson 3 | Distributed under the MIT license. See terms at the end of this file. 4 | ————————————————————————————————————————————————————————————————————————————*/ 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | // Detect platform 12 | #if defined(_WIN32) || defined (_WIN64) 13 | #define OCAML_TERMINAL_WINDOWS 14 | #elif defined(__unix__) || defined(__unix) || (defined(__APPLE__) && defined(__MACH__)) 15 | #include 16 | #if defined(_POSIX_VERSION) 17 | #define OCAML_TERMINAL_POSIX 18 | #endif 19 | #endif 20 | 21 | // Windows support 22 | #if defined(OCAML_TERMINAL_WINDOWS) 23 | #define WIN32_LEAN_AND_MEAN 24 | #define VC_EXTRALEAN 25 | #include 26 | 27 | 28 | CAMLprim value ocaml_terminal_get_sigwinch() 29 | { 30 | return Val_int(0); 31 | } 32 | 33 | CAMLprim value ocaml_terminal_get_terminal_dimensions(value unit) 34 | { 35 | CAMLparam1(unit); 36 | CAMLlocal2(result, pair); 37 | 38 | CONSOLE_SCREEN_BUFFER_INFO csbi; 39 | int success = GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi); 40 | if (success) 41 | { 42 | result = caml_alloc(1, 0); 43 | pair = caml_alloc(2, 0); 44 | Store_field(result, 0, pair); 45 | Store_field(pair, 0, Val_int((int)(csbi.dwSize.Y))); 46 | Store_field(pair, 1, Val_int((int)(csbi.dwSize.X))); 47 | } 48 | else 49 | { 50 | result = Val_int(0); 51 | } 52 | 53 | CAMLreturn(result); 54 | } 55 | 56 | // POSIX support 57 | #elif defined(OCAML_TERMINAL_POSIX) 58 | #include 59 | 60 | CAMLprim value ocaml_terminal_get_sigwinch (value unit) 61 | { 62 | CAMLparam1(unit); 63 | CAMLlocal1(result); 64 | result = caml_alloc(1, 0); 65 | Store_field(result, 0, Val_int (SIGWINCH)); 66 | CAMLreturn(result); 67 | } 68 | 69 | CAMLprim value ocaml_terminal_get_terminal_dimensions(value unit) 70 | { 71 | CAMLparam1(unit); 72 | CAMLlocal2(result, pair); 73 | struct winsize ws; 74 | int z = ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws); 75 | if (z == 0) 76 | { 77 | result = caml_alloc(1, 0); 78 | pair = caml_alloc(2, 0); 79 | Store_field(result, 0, pair); 80 | Store_field(pair, 0, Val_int(ws.ws_row)); 81 | Store_field(pair, 1, Val_int(ws.ws_col)); 82 | } 83 | else 84 | { 85 | result = Val_int(0); 86 | } 87 | 88 | CAMLreturn(result); 89 | } 90 | 91 | // Unsupported platform 92 | #else 93 | 94 | CAMLprim value ocaml_terminal_get_sigwinch() 95 | { 96 | return Val_int(0); 97 | } 98 | 99 | CAMLprim value ocaml_terminal_get_terminal_dimensions(value unit) 100 | { 101 | CAMLparam1(unit); 102 | CAMLlocal2(result, pair); 103 | 104 | result = Val_int(0); 105 | CAMLreturn(result); 106 | } 107 | 108 | #endif 109 | 110 | /*———————————————————————————————————————————————————————————————————————————— 111 | Copyright (c) 2020–2021 Craig Ferguson 112 | 113 | Permission to use, copy, modify, and/or distribute this software for any 114 | purpose with or without fee is hereby granted, provided that the above 115 | copyright notice and this permission notice appear in all copies. 116 | 117 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 118 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 119 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 120 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 121 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 122 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 123 | DEALINGS IN THE SOFTWARE. 124 | ————————————————————————————————————————————————————————————————————————————*/ 125 | -------------------------------------------------------------------------------- /src/terminal/tests/common.ml: -------------------------------------------------------------------------------- 1 | let check_invalid pos f = 2 | match f () with 3 | | _ -> 4 | Alcotest.fail ~pos 5 | "Expected [Invalid_argument], but no exception was raised." 6 | | exception Invalid_argument _ -> () 7 | -------------------------------------------------------------------------------- /src/terminal/tests/dune: -------------------------------------------------------------------------------- 1 | (test 2 | (name test) 3 | (package terminal) 4 | (libraries terminal alcotest fmt)) 5 | -------------------------------------------------------------------------------- /src/terminal/tests/test.ml: -------------------------------------------------------------------------------- 1 | let () = 2 | let open Alcotest in 3 | run __FILE__ [ ("colours", Test_colours.tests); ("width", Test_width.tests) ] 4 | -------------------------------------------------------------------------------- /src/terminal/tests/test_colours.ml: -------------------------------------------------------------------------------- 1 | open Terminal 2 | open Common 3 | 4 | let check_string pos = Alcotest.(check ~pos (testable Fmt.Dump.string ( = ))) "" 5 | 6 | let check_color pos (r, g, b) x = 7 | Alcotest.(check ~pos string) 8 | "" 9 | (Fmt.str "RGB (%d, %d, %d)" r g b) 10 | (Fmt.to_to_string Color.pp_dump x) 11 | 12 | let test_rgb () = 13 | check_invalid __POS__ (fun () -> Color.rgb (-1) 0 0); 14 | check_invalid __POS__ (fun () -> Color.rgb 0 256 0); 15 | check_string __POS__ "\027[38;2;41;42;43m" 16 | (Color.rgb 41 42 43 |> Style.fg |> Style.code); 17 | () 18 | 19 | let test_hex () = 20 | (* Invalid *) 21 | check_invalid __POS__ (fun () -> Color.hex "FFFFFF"); 22 | check_invalid __POS__ (fun () -> Color.hex "#"); 23 | check_invalid __POS__ (fun () -> Color.hex "#F"); 24 | check_invalid __POS__ (fun () -> Color.hex "#00000-"); 25 | check_invalid __POS__ (fun () -> Color.hex "#00-"); 26 | 27 | (* Valid *) 28 | check_color __POS__ (0x12, 0x34, 0x56) (Color.hex "#123456"); 29 | check_color __POS__ (0xab, 0xcd, 0xef) (Color.hex "#aBCdEf"); 30 | 31 | (* Short aliases *) 32 | check_color __POS__ (0, 0, 0) (Color.hex "#000"); 33 | check_color __POS__ (17, 34, 51) (Color.hex "#123"); 34 | check_color __POS__ (255, 255, 255) (Color.hex "#fff"); 35 | 36 | () 37 | 38 | let tests = 39 | Alcotest.[ test_case "rgb" `Quick test_rgb; test_case "hex" `Quick test_hex ] 40 | -------------------------------------------------------------------------------- /src/terminal/tests/test_colours.mli: -------------------------------------------------------------------------------- 1 | val tests : unit Alcotest.test_case list 2 | -------------------------------------------------------------------------------- /src/terminal/tests/test_width.ml: -------------------------------------------------------------------------------- 1 | open Terminal 2 | open Common 3 | 4 | let pp_sty = Fmt.of_to_string Style.code 5 | let col x = Style.fg (Color.ansi x) 6 | 7 | let test_guess_width () = 8 | let check pos ~expected:n fmt = 9 | Fmt.kstr 10 | (fun x -> Alcotest.(check ~pos int) "" n (guess_printed_width x)) 11 | fmt 12 | in 13 | check __POS__ ~expected:0 ""; 14 | check __POS__ ~expected:1 "a"; 15 | check __POS__ ~expected:5 " a "; 16 | check __POS__ ~expected:3 "▷▷▷"; 17 | 18 | check __POS__ ~expected:0 "%a" pp_sty (col `red); 19 | check __POS__ ~expected:3 "%a<->%a" pp_sty (col `red) pp_sty (col `green); 20 | () 21 | 22 | let test_truncate () = 23 | let trunc n fmt = Fmt.kstr (truncate_to_width n) fmt in 24 | let check pos ~expected s = 25 | Alcotest.(check ~pos (testable Fmt.Dump.string ( = ))) "" expected s 26 | in 27 | check __POS__ ~expected:"" (trunc 0 ""); 28 | check __POS__ ~expected:"" (trunc 5 ""); 29 | check __POS__ ~expected:"▷" (trunc 1 "▷▷▷"); 30 | check __POS__ ~expected:"▷▷" (trunc 2 "▷▷▷"); 31 | check __POS__ ~expected:"▷▷▷" (trunc 3 "▷▷▷"); 32 | 33 | check __POS__ ~expected:"" (trunc 0 "%a" pp_sty (col `red)); 34 | check __POS__ ~expected:"\027[31m" (trunc 1 "%a" pp_sty (col `red)); 35 | check __POS__ ~expected:"▷\027[31m" (trunc 2 "▷%a" pp_sty (col `red)); 36 | 37 | (* Trailing colour is trimmed: *) 38 | check __POS__ ~expected:"▷" (trunc 1 "▷%a" pp_sty (col `red)); 39 | 40 | (* Trailing reset is not trimmed: *) 41 | check __POS__ ~expected:"▷\027[0m" (trunc 1 "▷%a" pp_sty Style.none); 42 | 43 | (* Trailing bytes look like a reset, but aren't: *) 44 | check __POS__ ~expected:"▷" (trunc 1 "▷\027[0"); 45 | 46 | check_invalid __POS__ (fun () -> trunc (-1) ""); 47 | () 48 | 49 | let tests = 50 | Alcotest. 51 | [ test_case "guess_width" `Quick test_guess_width 52 | ; test_case "truncate" `Quick test_truncate 53 | ] 54 | -------------------------------------------------------------------------------- /src/terminal/tests/test_width.mli: -------------------------------------------------------------------------------- 1 | val tests : unit Alcotest.test_case list 2 | -------------------------------------------------------------------------------- /terminal.opam: -------------------------------------------------------------------------------- 1 | # This file is generated by dune, edit dune-project instead 2 | opam-version: "2.0" 3 | synopsis: "Basic utilities for interacting with terminals" 4 | description: "Basic utilities for interacting with terminals" 5 | maintainer: ["Craig Ferguson "] 6 | authors: ["Craig Ferguson "] 7 | license: "MIT" 8 | homepage: "https://github.com/CraigFe/progress" 9 | doc: "https://CraigFe.github.io/progress/" 10 | bug-reports: "https://github.com/CraigFe/progress/issues" 11 | depends: [ 12 | "dune" {>= "2.7"} 13 | "ocaml" {>= "4.03.0"} 14 | "uucp" {>= "2.0.0"} 15 | "uutf" {>= "1.0.0"} 16 | "stdlib-shims" 17 | "alcotest" {with-test & >= "1.4.0"} 18 | "fmt" {with-test} 19 | "astring" {with-test} 20 | "mtime" {with-test & >= "2.0.0"} 21 | "odoc" {with-doc} 22 | ] 23 | build: [ 24 | ["dune" "subst"] {dev} 25 | [ 26 | "dune" 27 | "build" 28 | "-p" 29 | name 30 | "-j" 31 | jobs 32 | "@install" 33 | "@runtest" {with-test} 34 | "@doc" {with-doc} 35 | ] 36 | ] 37 | dev-repo: "git+https://github.com/CraigFe/progress.git" 38 | --------------------------------------------------------------------------------