├── .gitattributes ├── .githooks └── pre-push ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── CHANGES.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── bin ├── dune └── main.re ├── box.opam ├── dune ├── dune-project ├── lib ├── base.ml ├── border.re ├── box.re ├── box.rei ├── dune ├── index.mld ├── margin.re ├── margin.rei ├── padding.re ├── padding.rei ├── space.re └── terminal.re ├── script └── release.sh └── test ├── border_test.re ├── box_test.re ├── dune ├── margin_padding_test.re ├── margin_test.re ├── padding_test.re └── support ├── framework.re └── runner.re /.gitattributes: -------------------------------------------------------------------------------- 1 | # Tell github that .re and .rei files are Reason 2 | *.re linguist-language=Reason 3 | *.rei linguist-language=Reason 4 | 5 | # Disable syntax detection for .spin 6 | .spin linguist-language=Text 7 | 8 | # Declare shell files to have LF endings on checkout 9 | # On Windows, the default git setting for `core.autocrlf` 10 | # means that when checking out code, LF endings get converted 11 | # to CRLF. This causes problems for shell scripts, as bash 12 | # gets choked up on the extra `\r` character. 13 | * text eol=lf 14 | -------------------------------------------------------------------------------- /.githooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | make test 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | schedule: 11 | # Prime the caches every Monday 12 | - cron: 0 1 * * MON 13 | 14 | jobs: 15 | build: 16 | name: Build and test 17 | 18 | runs-on: ${{ matrix.os }} 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | os: 24 | - macos-latest 25 | - ubuntu-latest 26 | # - windows-latest 27 | ocaml-compiler: 28 | - 4.12.0 29 | 30 | steps: 31 | - name: Checkout code 32 | uses: actions/checkout@v2 33 | 34 | - name: Use OCaml ${{ matrix.ocaml-compiler }} 35 | uses: ocaml/setup-ocaml@v2 36 | with: 37 | ocaml-compiler: ${{ matrix.ocaml-compiler }} 38 | dune-cache: ${{ matrix.os != 'macos-latest' }} 39 | 40 | - name: Install dependencies 41 | run: opam install . --deps-only --with-doc --with-test 42 | 43 | - name: Pin reason-native dependencies 44 | run: make pin-reason-native 45 | 46 | - name: Build 47 | run: make build 48 | 49 | - name: Run tests 50 | run: make test 51 | 52 | - name: Build documentation 53 | run: make doc 54 | 55 | - name: Deploying to github pages 56 | uses: peaceiris/actions-gh-pages@v3 57 | with: 58 | github_token: ${{ secrets.GH_TOKEN }} 59 | publish_dir: ./_build/default/_doc/_html/ 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ocamlbuild working directory 2 | _build/ 3 | 4 | # ocamlbuild targets 5 | *.byte 6 | *.native 7 | 8 | # Merlin configuring file for Vim and Emacs 9 | .merlin 10 | 11 | # Dune generated files 12 | *.install 13 | 14 | # Local OPAM switch 15 | _opam/ 16 | 17 | _build/ 18 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/reason-native"] 2 | path = vendor/reason-native 3 | url = https://github.com/facebookexperimental/reason-native 4 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## 1.1.0 2 | - Fix prod build 3 | 4 | ## 1.0.0 5 | - Render text inside boxes 6 | - Modify the border 7 | - Set the padding's box 8 | - Set the margin's box 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Setup your development environment 4 | 5 | You need Opam, you can install it by following [Opam's documentation](https://opam.ocaml.org/doc/Install.html). 6 | 7 | With Opam installed, you can install the dependencies with: 8 | 9 | ```bash 10 | # If it's the first time, `make create-switch` 11 | make dev 12 | ``` 13 | 14 | Then, build the project with: 15 | 16 | ```bash 17 | make build 18 | ``` 19 | 20 | ### Running Binary 21 | 22 | After building the project, you can run the main binary that is produced. 23 | 24 | ```bash 25 | make start 26 | ``` 27 | 28 | ### Running Tests 29 | 30 | You can run the test compiled executable: 31 | 32 | ```bash 33 | make test 34 | ``` 35 | 36 | ### Building documentation 37 | 38 | Documentation for the libraries in the project can be generated with: 39 | 40 | ```bash 41 | make doc 42 | open-cli $(make doc-path) 43 | ``` 44 | 45 | This assumes you have a command like [open-cli](https://github.com/sindresorhus/open-cli) installed on your system. 46 | 47 | > NOTE: On macOS, you can use the system command `open`, for instance `open $(make doc-path)` 48 | 49 | ### Releasing 50 | 51 | To create a release and publish it on Opam, first update the `CHANGES.md` file with the last changes and the version that you want to release. 52 | The, you can run the script `script/release.sh`. The script will perform the following actions: 53 | 54 | - Create a tag with the version found in `ocaml-box.opam`, and push it to your repository. 55 | - Create the distribution archive. 56 | - Publish the distribution archive to a Github Release. 57 | - Submit a PR on Opam's repository. 58 | 59 | When the release is published on Github, the CI/CD will trigger the `Release` workflow which will perform the following actions 60 | 61 | - Compile binaries for all supported platforms. 62 | - Create an NPM release containing the pre-built binaries. 63 | - Publish the NPM release to the registry. 64 | 65 | ### Repository Structure 66 | 67 | The following snippet describes ocaml-box's repository structure. 68 | 69 | ```text 70 | . 71 | ├── .github/ 72 | | Contains Github specific files such as actions definitions and issue templates. 73 | │ 74 | ├── bin/ 75 | | Source for ocaml-box's binary. This links to the library defined in `lib/`. 76 | │ 77 | ├── lib/ 78 | | Source for ocaml-box's library. Contains ocaml-box's core functionnalities. 79 | │ 80 | ├── test/ 81 | | Unit tests and integration tests for ocaml-box. 82 | │ 83 | ├── dune-project 84 | | Dune file used to mark the root of the project and define project-wide parameters. 85 | | For the documentation of the syntax, see https://dune.readthedocs.io/en/stable/dune-files.html#dune-project 86 | │ 87 | ├── LICENSE 88 | │ 89 | ├── Makefile 90 | | Make file containing common development command. 91 | │ 92 | ├── README.md 93 | │ 94 | └── ocaml-box.opam 95 | Opam package definition. 96 | To know more about creating and publishing opam packages, see https://opam.ocaml.org/doc/Packaging.html. 97 | ``` 98 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 David 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 all 13 | 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 THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := all 2 | 3 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 4 | DUNE = opam exec -- dune 5 | 6 | ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS)) 7 | $(eval $(ARGS):;@:) 8 | 9 | .PHONY: help 10 | help: ## Print this help message 11 | @echo "List of available make commands"; 12 | @echo ""; 13 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'; 14 | @echo ""; 15 | 16 | 17 | .PHONY: all 18 | all: 19 | $(DUNE) build --root . @install 20 | 21 | .PHONY: create-switch 22 | create-switch: 23 | opam switch create . 4.12.0 --deps-only --locked 24 | 25 | .PHONY: pin-reason-native 26 | pin-reason-native: 27 | git submodule update --init 28 | 29 | .PHONY: dev 30 | init: pin-reason-native ## Install development dependencies 31 | git config core.hooksPath .githooks 32 | opam pin add -y ocaml-lsp-server https://github.com/ocaml/ocaml-lsp.git 33 | opam install -y dune-release merlin ocaml-lsp-server 34 | opam install --deps-only --with-test --with-doc -y . 35 | 36 | .PHONY: install 37 | install: all ## Install the packages on the system 38 | $(DUNE) install --root . 39 | 40 | .PHONY: start 41 | start: all ## Run the produced executable 42 | $(DUNE) exec --root . bin/main.exe $(ARGS) 43 | 44 | .PHONY: watch 45 | dev: ## Watch for the filesystem and rebuild on every change 46 | $(DUNE) build --root . --watch 47 | 48 | .PHONY: test 49 | test: ## Run the unit tests 50 | $(DUNE) exec --root . test/runner.exe 51 | 52 | .PHONY: build 53 | build: ## Build the project, including non installable libraries and executables 54 | $(DUNE) build --root . 55 | 56 | .PHONY: clean 57 | clean: ## Clean build artifacts and other generated files 58 | $(DUNE) clean --root . 59 | 60 | .PHONY: doc 61 | doc: ## Generate odoc documentation 62 | $(DUNE) build --root . @doc 63 | 64 | .PHONY: servedoc 65 | servedoc: doc ## Open odoc documentation with default web browser 66 | open _build/default/_doc/_html/index.html 67 | 68 | .PHONY: format 69 | format: ## Format the codebase with ocamlformat 70 | $(DUNE) build --root . --auto-promote @fmt 71 | 72 | .PHONY: format-check 73 | format-check: ## Checks if format is correct 74 | $(DUNE) build @fmt 75 | 76 | .PHONY: watch 77 | watch: ## Watch for the filesystem and rebuild on every change 78 | $(DUNE) build --root . --watch 79 | 80 | .PHONY: utop 81 | utop: ## Run a REPL and link with the project's libraries 82 | $(DUNE) utop --root . lib -- -implicit-bindings 83 | 84 | .PHONY: release 85 | release: all ## Run the release script 86 | opam exec -- sh script/release.sh 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ocaml-box 2 | 3 | ```bash 4 | 5 | ╭─────────────────────────────────────────────────╮ 6 | │ │ 7 | │ Render those kinds of boxes in the terminal │ 8 | │ │ 9 | ╰─────────────────────────────────────────────────╯ 10 | 11 | ``` 12 | 13 | Render boxes in the terminal with OCaml or Reason. 14 | Port of [sindresorhus/boxen](https://github.com/sindresorhus/boxen). 15 | 16 | ## Features 17 | 18 | - Available on all major platform (Windows, Linux and Windows) 19 | - Render boxes with margin, padding and different borders 20 | - Alignment 21 | - Floating 22 | - No dependencies 23 | 24 | ### Missing Features 25 | 26 | - ascii support 27 | - Border Color (and dimming) 28 | 29 | ### Install with opam 30 | 31 | ```bash 32 | opam install box 33 | ``` 34 | 35 | ### Install with esy 36 | 37 | ```bash 38 | esy add @opam/box 39 | ``` 40 | 41 | ## Documentation 42 | 43 | [Documentation](https://davesnx.github.io/ocaml-box/box/index.html) 44 | 45 | ## Contributing 46 | 47 | Take a look at our [Contributing Guide](CONTRIBUTING.md). 48 | 49 | ### Status 50 | 51 | [![Actions Status](https://github.com/davesnx/ocaml-box/workflows/CI/badge.svg)](https://github.com/davesnx/ocaml-box/actions) 52 | -------------------------------------------------------------------------------- /bin/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name main) 3 | (public_name box-bin) 4 | (libraries box) 5 | (flags 6 | (:standard -open Box))) 7 | 8 | (include_subdirs unqualified) 9 | -------------------------------------------------------------------------------- /bin/main.re: -------------------------------------------------------------------------------- 1 | /* This file's purpose is to test manually and isn't public, it might serve as examples! */ 2 | 3 | /* let separator = Box.render( 4 | ~float=`Left, 5 | ~align=`Center, 6 | ~border=Custom({ 7 | topLeft: "", 8 | top: "", 9 | topRight: "", 10 | right: "", 11 | bottomRight: "", 12 | bottom: "", 13 | bottomLeft: "", 14 | left: "", 15 | }), 16 | "xxxxxxxxxxxxxxxxxx", 17 | ); 18 | 19 | print_endline( 20 | render( 21 | ~margin=Margin.all(2), 22 | ~align=`Left, 23 | "box", 24 | ) 25 | ); 26 | 27 | print_endline(separator); 28 | */ 29 | 30 | print_endline( 31 | render(~padding=Padding.left(5), "left") 32 | ); 33 | 34 | print_endline( 35 | render(~padding=Padding.right(5), "right") 36 | ); 37 | 38 | print_endline( 39 | render(~margin=Margin.all(2), ~padding=Padding.all(2), "foo") 40 | ); 41 | 42 | print_endline( 43 | render(~padding=Padding.top(2), "foo") 44 | ); 45 | 46 | print_endline( 47 | render(~padding=Padding.bottom(2), "foo") 48 | ); 49 | -------------------------------------------------------------------------------- /box.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | version: "1.2.0" 3 | synopsis: "Render boxes in the terminal" 4 | description: """ 5 | Render boxes in the terminal, can configure the padding, margin and the border of the box. 6 | """ 7 | maintainer: ["davesnx "] 8 | authors: ["davesnx "] 9 | license: "MIT" 10 | homepage: "https://github.com/davesnx/ocaml-box" 11 | doc: "https://davesnx.github.io/ocaml-box/box/index.html" 12 | bug-reports: "https://github.com/davesnx/ocaml-box/issues" 13 | depends: [ 14 | "ocaml" {>= "4.08.0"} 15 | "dune" {>= "2.7"} 16 | "odoc" {with-doc} 17 | "reason" {build} 18 | ] 19 | dev-repo: "git+https://github.com/davesnx/ocaml-box.git" 20 | build: [ 21 | ["dune" "subst"] {dev} 22 | [ 23 | "dune" 24 | "build" 25 | "-p" 26 | name 27 | "-j" 28 | jobs 29 | "@install" 30 | "@runtest" {with-test} 31 | "@doc" {with-doc} 32 | ] 33 | ] 34 | -------------------------------------------------------------------------------- /dune: -------------------------------------------------------------------------------- 1 | (vendored_dirs vendor) 2 | -------------------------------------------------------------------------------- /dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 2.7) 2 | (name box) 3 | (version 1.2.0) 4 | 5 | (generate_opam_files false) 6 | -------------------------------------------------------------------------------- /lib/base.ml: -------------------------------------------------------------------------------- 1 | module String = struct 2 | external length : string -> int = "%string_length" 3 | 4 | let split_lines = 5 | let back_up_at_newline ~t ~pos ~eol = 6 | pos := !pos - if !pos > 0 && Char.equal t.[!pos - 1] '\r' then 2 else 1; 7 | eol := !pos + 1 8 | in 9 | fun t -> 10 | let n = length t in 11 | if n = 0 12 | then [] 13 | else ( 14 | (* Invariant: [-1 <= pos < eol]. *) 15 | let pos = ref (n - 1) in 16 | let eol = ref n in 17 | let ac = ref [] in 18 | (* We treat the end of the string specially, because if the string ends with a 19 | newline, we don't want an extra empty string at the end of the output. *) 20 | if Char.equal t.[!pos] '\n' then back_up_at_newline ~t ~pos ~eol; 21 | while !pos >= 0 do 22 | if t.[!pos] <> '\n' 23 | then decr pos 24 | else ( 25 | (* Because [pos < eol], we know that [start <= eol]. *) 26 | let start = !pos + 1 in 27 | ac := String.sub t start (!eol - start) :: !ac; 28 | back_up_at_newline ~t ~pos ~eol) 29 | done; 30 | String.sub t 0 !eol :: !ac) 31 | ;; 32 | end 33 | -------------------------------------------------------------------------------- /lib/border.re: -------------------------------------------------------------------------------- 1 | type symbols = { 2 | topLeft: string, 3 | top: string, 4 | topRight: string, 5 | right: string, 6 | bottomRight: string, 7 | bottom: string, 8 | bottomLeft: string, 9 | left: string, 10 | }; 11 | 12 | let single = { 13 | topLeft: {|┌|}, 14 | top: {|─|}, 15 | topRight: {|┐|}, 16 | right: {|│|}, 17 | bottomRight: {|┘|}, 18 | bottom: {|─|}, 19 | bottomLeft: {|└|}, 20 | left: {|│|}, 21 | }; 22 | 23 | let double = { 24 | topLeft: {|╔|}, 25 | top: {|═|}, 26 | topRight: {|╗|}, 27 | right: {|║|}, 28 | bottomRight: {|╝|}, 29 | bottom: {|═|}, 30 | bottomLeft: {|╚|}, 31 | left: {|║|}, 32 | }; 33 | 34 | let round = { 35 | topLeft: {|╭|}, 36 | top: {|─|}, 37 | topRight: {|╮|}, 38 | right: {|│|}, 39 | bottomRight: {|╯|}, 40 | bottom: {|─|}, 41 | bottomLeft: {|╰|}, 42 | left: {|│|}, 43 | }; 44 | 45 | let bold = { 46 | topLeft: {|┏|}, 47 | top: {|━|}, 48 | topRight: {|┓|}, 49 | right: {|┃|}, 50 | bottomRight: {|┛|}, 51 | bottom: {|━|}, 52 | bottomLeft: {|┗|}, 53 | left: {|┃|}, 54 | }; 55 | 56 | let singleDouble = { 57 | topLeft: {|╓|}, 58 | top: {|─|}, 59 | topRight: {|╖|}, 60 | right: {|║|}, 61 | bottomRight: {|╜|}, 62 | bottom: {|─|}, 63 | bottomLeft: {|╙|}, 64 | left: {|║|}, 65 | }; 66 | 67 | let doubleSingle = { 68 | topLeft: {|╒|}, 69 | top: {|═|}, 70 | topRight: {|╕|}, 71 | right: {|│|}, 72 | bottomRight: {|╛|}, 73 | bottom: {|═|}, 74 | bottomLeft: {|╘|}, 75 | left: {|│|}, 76 | }; 77 | 78 | let classic = { 79 | topLeft: {|+|}, 80 | top: {|-|}, 81 | topRight: {|+|}, 82 | right: {|||}, 83 | bottomRight: {|+|}, 84 | bottom: {|-|}, 85 | bottomLeft: {|+|}, 86 | left: {|||}, 87 | }; 88 | 89 | let arrow = { 90 | topLeft: {|↘|}, 91 | top: {|↓|}, 92 | topRight: {|↙|}, 93 | right: {|←|}, 94 | bottomRight: {|↖|}, 95 | bottom: {|↑|}, 96 | bottomLeft: {|↗|}, 97 | left: {|→|}, 98 | }; 99 | 100 | type t = 101 | | Single 102 | | Double 103 | | Round 104 | | Bold 105 | | SingleDouble 106 | | DoubleSingle 107 | | Arrow 108 | | Classic 109 | | Custom(symbols); 110 | 111 | let symbols = 112 | fun 113 | | Single => single 114 | | Double => double 115 | | Round => round 116 | | Bold => bold 117 | | SingleDouble => singleDouble 118 | | DoubleSingle => doubleSingle 119 | | Arrow => arrow 120 | | Classic => classic 121 | | Custom(symbols) => symbols; 122 | -------------------------------------------------------------------------------- /lib/box.re: -------------------------------------------------------------------------------- 1 | open Terminal; 2 | 3 | type position = Left | Center | Right; 4 | 5 | let render = (~align=Center, ~float=Left, ~padding=Padding.empty, ~margin=Margin.empty, ~border=Border.Round, text) => { 6 | let symbols = Border.symbols(border); 7 | let columns = Terminal.columns(); 8 | 9 | let paddingLeftValue = padding.left; 10 | let paddingLeft = renderSpace(paddingLeftValue); 11 | 12 | let marginTop = repeat(margin.top, newLine); 13 | let marginBottom = repeat(margin.bottom, newLine); 14 | 15 | let paddingRightValueWithoutText = padding.right; 16 | let contentWidth = calculateWidestLine(text) + paddingLeftValue + paddingRightValueWithoutText; 17 | let horitzontalTop = repeat(contentWidth, symbols.top); 18 | let horitzontalBottom = repeat(contentWidth, symbols.bottom); 19 | 20 | let calculateMarginLeft = (~columns as _, value) => { 21 | switch (float) { 22 | | Left => value * 2 23 | | Center => contentWidth - value * 2 24 | | Right => (contentWidth * 2) - value * 2 25 | } 26 | }; 27 | 28 | let marginLeftValue = calculateMarginLeft(~columns, margin.left); 29 | let marginLeft = renderSpace(marginLeftValue); 30 | 31 | let renderLine = (text) => { 32 | let paddingRightValue = 33 | contentWidth - textLength(text) - padding.left; 34 | let paddingRight = renderSpace(paddingRightValue); 35 | let text = stack([paddingLeft, text, paddingRight]); 36 | stack( 37 | [marginLeft, symbols.left, text, symbols.right], 38 | ); 39 | }; 40 | 41 | let renderContent = (text) => { 42 | let widestLine = calculateWidestLine(text); 43 | let lines = splitLines(text); 44 | switch (align) { 45 | | Left => lines |> List.map(renderLine); 46 | | Right => { 47 | lines |> List.map((line) => { 48 | let padLeft = widestLine - textLength(line); 49 | let left = repeat(padLeft, " "); 50 | stack([left, line]) |> renderLine; 51 | }); 52 | } 53 | | Center => { 54 | lines |> List.map((line) => { 55 | let padRight = (widestLine - textLength(line)) / 2; 56 | let left = repeat(padRight, " "); 57 | stack([left, line]) |> renderLine; 58 | }); 59 | } 60 | } 61 | }; 62 | 63 | let content = renderContent(text) |> row; 64 | let paddingTop = repeat(~between=newLine, padding.top, renderLine("")); 65 | let paddingBottom = repeat(~between=newLine, padding.bottom, renderLine("")); 66 | 67 | let header = 68 | stack( 69 | [ 70 | marginTop, 71 | marginLeft, 72 | symbols.topLeft, 73 | horitzontalTop, 74 | symbols.topRight, 75 | ], 76 | ); 77 | 78 | let body = switch (padding) { 79 | | { top: 0, bottom: 0, _ } => content 80 | | { top: 0, _ } => row([content, paddingBottom]) 81 | | { bottom: 0, _ } => row([paddingTop, content]) 82 | | _ => row([paddingTop, content, paddingBottom]) 83 | }; 84 | 85 | let footer = 86 | stack( 87 | [ 88 | marginLeft, 89 | symbols.bottomLeft, 90 | horitzontalBottom, 91 | symbols.bottomRight, 92 | marginBottom, 93 | ], 94 | ); 95 | 96 | row([header, body, footer]); 97 | }; 98 | 99 | module Border = Border; 100 | module Padding = Padding; 101 | module Margin = Margin; 102 | -------------------------------------------------------------------------------- /lib/box.rei: -------------------------------------------------------------------------------- 1 | type position = Left | Center | Right; 2 | 3 | let render: (~align: position=?, ~float: position=?, ~padding: Padding.t=?, ~margin: Margin.t=?, ~border: Border.t=?, string) => string; 4 | 5 | 6 | module Border = Border; 7 | module Padding = Padding; 8 | module Margin = Margin; 9 | -------------------------------------------------------------------------------- /lib/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name box) 3 | (public_name box)) 4 | 5 | (documentation 6 | (package box) 7 | (mld_files index)) 8 | -------------------------------------------------------------------------------- /lib/index.mld: -------------------------------------------------------------------------------- 1 | {1:intro Intro} 2 | 3 | Render boxes in the terminal with OCaml or Reason. Port of {{: https://github.com/sindresorhus/boxen } sindresorhus/boxen } 4 | 5 | {[ 6 | ╭─────────────────────────────────────────────────────╮ 7 | │ │ 8 | │ "Render those kinds of boxes in the terminal" │ 9 | │ │ 10 | ╰─────────────────────────────────────────────────────╯ 11 | ]} 12 | 13 | [render text] creates a box around your [text] to print it to the terminal. The text can be aligned to any position and the box can float to any position. Can control the space between the box and the outside with padding Padding.t or the space inside with margin Margin.t. Lastly, can configure the border to be one of the Border.t kinds. 14 | 15 | Check the {!Box} page for more details 16 | 17 | {3:examples Examples} 18 | 19 | 20 | {[ 21 | utop # print_endline (Box.render ~padding:(Padding.all 2) "I love unicorns");; 22 | 23 | ┌─────────────────────┐ 24 | │ │ 25 | │ I love unicorns │ 26 | │ │ 27 | └─────────────────────┘ 28 | 29 | ]} 30 | 31 | 32 | {[ 33 | utop # print_endline (Box.render ~padding:(Padding.all 0) "I love unicorns");; 34 | 35 | ┌──────────────────┐ 36 | │ I love unicorns │ 37 | └──────────────────┘ 38 | ]} 39 | 40 | {[ 41 | utop # print_endline (Box.render ~border:Border.Classic "I love unicorns") 42 | 43 | +-------------------+ 44 | | I love unicorns | 45 | +-------------------+ 46 | ]} 47 | -------------------------------------------------------------------------------- /lib/margin.re: -------------------------------------------------------------------------------- 1 | include Space; 2 | -------------------------------------------------------------------------------- /lib/margin.rei: -------------------------------------------------------------------------------- 1 | type t = { 2 | top: int, 3 | right: int, 4 | bottom: int, 5 | left: int, 6 | }; 7 | 8 | let make: (~top: int=?, ~right: int=?, ~bottom: int=?, ~left: int=?, unit) => t; 9 | 10 | let empty: t; 11 | 12 | let all: int => t; 13 | 14 | let top: int => t; 15 | let right: int => t; 16 | let bottom: int => t; 17 | let left: int => t; 18 | 19 | let horitzontal: int => t; 20 | let vertical: int => t; 21 | 22 | let topLeft: (int, int) => t; 23 | let topRight: (int, int) => t; 24 | let topBottom: (int, int) => t; 25 | 26 | let bottomLeft: (int, int) => t; 27 | let bottomRight: (int, int) => t; 28 | let bottomTop: (int, int) => t; 29 | 30 | let rightBottom: (int, int) => t; 31 | let rightLeft: (int, int) => t; 32 | 33 | let leftBottom: (int, int) => t; 34 | let leftTop: (int, int) => t; 35 | let leftRight: (int, int) => t; 36 | -------------------------------------------------------------------------------- /lib/padding.re: -------------------------------------------------------------------------------- 1 | include Space; 2 | -------------------------------------------------------------------------------- /lib/padding.rei: -------------------------------------------------------------------------------- 1 | type t = { 2 | top: int, 3 | right: int, 4 | bottom: int, 5 | left: int, 6 | }; 7 | 8 | let make: (~top: int=?, ~right: int=?, ~bottom: int=?, ~left: int=?, unit) => t; 9 | 10 | let empty: t; 11 | 12 | let all: int => t; 13 | 14 | let top: int => t; 15 | let right: int => t; 16 | let bottom: int => t; 17 | let left: int => t; 18 | 19 | let horitzontal: int => t; 20 | let vertical: int => t; 21 | 22 | let topLeft: (int, int) => t; 23 | let topRight: (int, int) => t; 24 | let topBottom: (int, int) => t; 25 | 26 | let bottomLeft: (int, int) => t; 27 | let bottomRight: (int, int) => t; 28 | let bottomTop: (int, int) => t; 29 | 30 | let rightBottom: (int, int) => t; 31 | let rightLeft: (int, int) => t; 32 | 33 | let leftBottom: (int, int) => t; 34 | let leftTop: (int, int) => t; 35 | let leftRight: (int, int) => t; 36 | -------------------------------------------------------------------------------- /lib/space.re: -------------------------------------------------------------------------------- 1 | type t = { 2 | top: int, 3 | right: int, 4 | bottom: int, 5 | left: int, 6 | }; 7 | 8 | let make = (~top=0, ~right=0, ~bottom=0, ~left=0, _) => { 9 | { top, right, bottom, left } 10 | }; 11 | 12 | let empty = { top: 0, right: 0, bottom: 0, left: 0 }; 13 | 14 | let all = value => make(~top=value, ~right=value, ~bottom=value, ~left=value, ()); 15 | 16 | let top = top => make(~top, ()); 17 | let right = right => make(~right, ()); 18 | let bottom = bottom => make(~bottom, ()); 19 | let left = left => make(~left, ()); 20 | 21 | let horitzontal = value => make(~left=value, ~right=value, ()); 22 | let vertical = value => make(~top=value, ~bottom=value, ()); 23 | 24 | let topLeft = (top, left) => make(~top, ~left, ()); 25 | let topRight = (top, right) => make(~top, ~right, ()); 26 | let topBottom = (top, bottom) => make(~top, ~bottom, ()); 27 | 28 | let bottomLeft = (bottom, left) => make(~bottom, ~left, ()); 29 | let bottomRight = (bottom, right) => make(~bottom, ~right, ()); 30 | let bottomTop = (bottom, top) => make(~bottom, ~top, ()); 31 | 32 | let rightBottom = (right, bottom) => make(~right, ~bottom, ()); 33 | let rightLeft = (right, left) => make(~right, ~left, ()); 34 | 35 | let leftBottom = (left, bottom) => make(~left, ~bottom, ()); 36 | let leftTop = (left, top) => make(~left, ~top, ()); 37 | let leftRight = (left, right) => make(~left, ~right, ()); 38 | -------------------------------------------------------------------------------- /lib/terminal.re: -------------------------------------------------------------------------------- 1 | let none = ""; 2 | let space = ' '; 3 | let newLine = "\n"; 4 | 5 | let textLength = Base.String.length; 6 | 7 | let splitLines = (text) => text |> Base.String.split_lines |> List.map(String.trim); 8 | 9 | let repeat = (~between="", times, str) => { 10 | times > 0 11 | ? Array.init(times, _ => str) |> Array.to_list |> String.concat(between) 12 | : none 13 | }; 14 | 15 | let calculateWidestLine = text => { 16 | text 17 | |> splitLines 18 | |> List.fold_left( 19 | (current, acc) => max(current, textLength(acc)), 20 | 0, 21 | ); 22 | }; 23 | 24 | let renderSpace = value => { 25 | value > 0 26 | ? String.make(value, space) 27 | : none 28 | }; 29 | 30 | let row = String.concat(newLine); 31 | let stack = String.concat(""); 32 | 33 | 34 | let columns = () => { 35 | switch (Sys.getenv_opt("COLUMNS")) { 36 | | Some(value) => int_of_string(value) 37 | | None => 80 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /script/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if [ -d ".git" ]; then 6 | changes=$(git status --porcelain) 7 | branch=$(git rev-parse --abbrev-ref HEAD) 8 | 9 | if [ -n "${changes}" ]; then 10 | echo "Please commit staged files prior to bumping" 11 | exit 1 12 | elif [ "${branch}" != "main" ]; then 13 | echo "Please run the release script on main" 14 | exit 1 15 | else 16 | dune-release tag 17 | dune-release distrib 18 | dune-release publish -y 19 | dune-release opam pkg 20 | dune-release opam submit --no-auto-open -y 21 | fi 22 | else 23 | echo "This project is not a git repository. Run \`git init\` first to be able to release." 24 | exit 1 25 | fi 26 | -------------------------------------------------------------------------------- /test/border_test.re: -------------------------------------------------------------------------------- 1 | open Framework; 2 | 3 | describe("box.render(~border)", ({test, _}) => { 4 | test("Border.Classic should render a box with classic's border", ({expect, _}) => 5 | expect |> equal( 6 | Box.render(~border=Box.Border.Classic, "foo"), 7 | {| 8 | +---+ 9 | |foo| 10 | +---+ 11 | |}, 12 | ) 13 | ); 14 | 15 | test("Border.Custom should renders a box with custom border", ({expect, _}) => 16 | expect |> equal( 17 | Box.render( 18 | ~border= 19 | Box.Border.Custom({ 20 | topLeft: {|1|}, 21 | top: {|.|}, 22 | topRight: {|2|}, 23 | right: {|.|}, 24 | bottomRight: {|3|}, 25 | bottom: {|.|}, 26 | bottomLeft: {|4|}, 27 | left: {|.|}, 28 | }), 29 | "foo", 30 | ), 31 | {| 32 | 1...2 33 | .foo. 34 | 4...3 35 | |}, 36 | ) 37 | ); 38 | }); 39 | -------------------------------------------------------------------------------- /test/box_test.re: -------------------------------------------------------------------------------- 1 | open Framework; 2 | 3 | describe("box.render()", ({test, _}) => { 4 | test("should render a box", ({expect, _}) => 5 | expect |> equal( 6 | Box.render("foo"), 7 | {| 8 | ╭───╮ 9 | │foo│ 10 | ╰───╯ 11 | |}, 12 | ) 13 | ); 14 | }); 15 | -------------------------------------------------------------------------------- /test/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name ocaml_box_test) 3 | (libraries rely.lib box) 4 | (modules 5 | (:standard \ runner)) 6 | (flags 7 | (:standard -linkall -g -open StdLabels -w +A-48-42-40-5-70))) 8 | 9 | (executable 10 | (name runner) 11 | (libraries rely.lib ocaml_box_test) 12 | (modules runner) 13 | (flags 14 | (:standard -open StdLabels -w +A-48-42-40-70))) 15 | 16 | (rule 17 | (alias runtest) 18 | (action 19 | (run ./runner.exe -q --color=always))) 20 | 21 | (include_subdirs unqualified) 22 | -------------------------------------------------------------------------------- /test/margin_padding_test.re: -------------------------------------------------------------------------------- 1 | open Framework; 2 | 3 | describe("box.render(~margin, ~padding)", ({test, _}) => { 4 | test("should render a box with both combined", ({expect, _}) => 5 | expect |> equal( 6 | Box.render(~margin=Box.Margin.all(2), ~padding=Box.Padding.all(2), "foo"), 7 | {| 8 | 9 | 10 | ╭───────╮ 11 | │ │ 12 | │ │ 13 | │ foo │ 14 | │ │ 15 | │ │ 16 | ╰───────╯ 17 | 18 | |}, 19 | ) 20 | ); 21 | }); 22 | -------------------------------------------------------------------------------- /test/margin_test.re: -------------------------------------------------------------------------------- 1 | open Framework; 2 | 3 | describe("box.render(~margin)", ({test, _}) => { 4 | test("should render a box with space arround", ({expect, _}) => 5 | expect |> equal( 6 | Box.render(~margin=Box.Margin.all(2), "foo"), 7 | {| 8 | 9 | 10 | ╭───╮ 11 | │foo│ 12 | ╰───╯ 13 | 14 | |}, 15 | ) 16 | ); 17 | }); 18 | -------------------------------------------------------------------------------- /test/padding_test.re: -------------------------------------------------------------------------------- 1 | open Framework; 2 | 3 | describe("box.render(~padding)", ({test, _}) => { 4 | test("renders a box with padding", ({expect, _}) => 5 | expect |> equal( 6 | Box.render(~padding=Box.Padding.all(2), "foo"), 7 | {| 8 | ╭───────╮ 9 | │ │ 10 | │ │ 11 | │ foo │ 12 | │ │ 13 | │ │ 14 | ╰───────╯ 15 | |}, 16 | ) 17 | ); 18 | 19 | test("renders a box with padding bottom", ({expect, _}) => 20 | expect |> equal( 21 | Box.render(~padding=Box.Padding.bottom(2), "foo"), 22 | {| 23 | ╭───╮ 24 | │foo│ 25 | │ │ 26 | │ │ 27 | ╰───╯ 28 | |}, 29 | ) 30 | ); 31 | 32 | test("renders a box with padding top", ({expect, _}) => 33 | expect |> equal( 34 | Box.render(~padding=Box.Padding.top(2), "foo"), 35 | {| 36 | ╭───╮ 37 | │ │ 38 | │ │ 39 | │foo│ 40 | ╰───╯ 41 | |}, 42 | ) 43 | ); 44 | 45 | test("renders a box with padding left", ({expect, _}) => 46 | expect |> equal( 47 | Box.render(~padding=Box.Padding.left(2), "foo"), 48 | {| 49 | ╭─────╮ 50 | │ foo│ 51 | ╰─────╯ 52 | |}, 53 | ) 54 | ); 55 | 56 | test("renders a box with padding right", ({expect, _}) => 57 | expect |> equal( 58 | Box.render(~padding=Box.Padding.right(2), "foo"), 59 | {| 60 | ╭─────╮ 61 | │foo │ 62 | ╰─────╯ 63 | |}, 64 | ) 65 | ); 66 | 67 | }); 68 | -------------------------------------------------------------------------------- /test/support/framework.re: -------------------------------------------------------------------------------- 1 | include Rely.Make({ 2 | let config = 3 | Rely.TestFrameworkConfig.initialize({ 4 | snapshotDir: "test/_snapshots", 5 | projectDir: "", 6 | }); 7 | }); 8 | 9 | let equal = (s1, s2, expect) => { 10 | expect.string(String.trim(s1)).toEqual(String.trim(s2)); 11 | }; 12 | -------------------------------------------------------------------------------- /test/support/runner.re: -------------------------------------------------------------------------------- 1 | let () = Ocaml_box_test.Framework.cli(); 2 | --------------------------------------------------------------------------------