├── .circleci └── config.yml ├── .gitignore ├── LICENSE ├── README.md ├── bb.edn ├── dev └── pp_grid │ └── repl.clj ├── project.clj ├── src └── pp_grid │ ├── ansi_escape_code.clj │ ├── api.cljc │ ├── core.cljc │ ├── examples.clj │ ├── layout.clj │ └── object.clj ├── test-bb.clj └── test └── pp_grid └── api_test.clj /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Use the latest 2.1 version of CircleCI pipeline process engine. 2 | # See: https://circleci.com/docs/2.0/configuration-reference 3 | # For a detailed guide to building and testing with clojure, read the docs: 4 | # https://circleci.com/docs/2.0/language-clojure/ for more details 5 | version: 2.1 6 | 7 | # Define a job to be invoked later in a workflow. 8 | # See: https://circleci.com/docs/2.0/configuration-reference/#jobs 9 | jobs: 10 | build: 11 | # Specify the execution environment. You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub. 12 | # See: https://circleci.com/docs/2.0/configuration-reference/#docker-machine-macos-windows-executor 13 | docker: 14 | # specify the version you desire here 15 | - image: circleci/clojure:lein-2.9.5 16 | 17 | # Specify service dependencies here if necessary 18 | # CircleCI maintains a library of pre-built images 19 | # documented at https://circleci.com/docs/2.0/circleci-images/ 20 | # - image: circleci/postgres:9.4 21 | 22 | working_directory: ~/repo 23 | 24 | environment: 25 | LEIN_ROOT: "true" 26 | # Customize the JVM maximum heap limit 27 | JVM_OPTS: -Xmx3200m 28 | 29 | # Add steps to the job 30 | # See: https://circleci.com/docs/2.0/configuration-reference/#steps 31 | steps: 32 | - checkout 33 | 34 | # Download and cache dependencies 35 | - restore_cache: 36 | keys: 37 | - v1-dependencies-{{ checksum "project.clj" }} 38 | # fallback to using the latest cache if no exact match is found 39 | - v1-dependencies- 40 | 41 | - run: lein deps 42 | 43 | - save_cache: 44 | paths: 45 | - ~/.m2 46 | key: v1-dependencies-{{ checksum "project.clj" }} 47 | 48 | # run tests! 49 | - run: lein test 50 | 51 | # Invoke jobs via workflows 52 | # See: https://circleci.com/docs/2.0/configuration-reference/#workflows 53 | workflows: 54 | sample: # This is the name of the workflow, feel free to change it to better match your workflow. 55 | # Inside the workflow, you define the jobs you want to run. 56 | jobs: 57 | - build 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | profiles.clj 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | /.prepl-port 12 | .hgignore 13 | .hg/ 14 | .clerk/ 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Amit Shrestha 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Clojars Project](https://img.shields.io/clojars/v/org.clojars.rorokimdim/pp-grid.svg)](https://clojars.org/org.clojars.rorokimdim/pp-grid) 2 | [![cljdoc badge](https://cljdoc.org/badge/org.clojars.rorokimdim/pp-grid)](https://cljdoc.org/d/org.clojars.rorokimdim/pp-grid) 3 | 4 | # pp-grid 5 | 6 | pp-grid is a clojure library to easily construct formatted text. 7 | 8 | It provides a grid data-structure (a `map`) and some primitive functions 9 | to draw text on it. 10 | 11 | Here is a `5 x 5` grid with `25` cells: 12 | 13 | ``` 14 | |------------------------------------▶︎ x 15 | |┌─────┐┌─────┐┌─────┐┌─────┐┌─────┐ 16 | |│(0,0)││(1,0)││(2,0)││(3,0)││(4,0)│ 17 | |└─────┘└─────┘└─────┘└─────┘└─────┘ 18 | |┌─────┐┌─────┐┌─────┐┌─────┐┌─────┐ 19 | |│(0,1)││(1,1)││(2,1)││(3,1)││(4,1)│ 20 | |└─────┘└─────┘└─────┘└─────┘└─────┘ 21 | |┌─────┐┌─────┐┌─────┐┌─────┐┌─────┐ 22 | |│(0,2)││(1,2)││(2,2)││(3,2)││(4,2)│ 23 | |└─────┘└─────┘└─────┘└─────┘└─────┘ 24 | |┌─────┐┌─────┐┌─────┐┌─────┐┌─────┐ 25 | |│(0,3)││(1,3)││(2,3)││(3,3)││(4,3)│ 26 | |└─────┘└─────┘└─────┘└─────┘└─────┘ 27 | |┌─────┐┌─────┐┌─────┐┌─────┐┌─────┐ 28 | |│(0,4)││(1,4)││(2,4)││(3,4)││(4,4)│ 29 | |└─────┘└─────┘└─────┘└─────┘└─────┘ 30 | ▼ 31 | y 32 | ``` 33 | 34 | We can set any cell's value in a grid 35 | with an `assoc`: `(assoc grid [x y] character)`. 36 | 37 | To illustrate the idea, here is one (tedious) way to write "HELLO WORLD" to a grid: 38 | 39 | ```clojure 40 | (defn make-hello-world [] 41 | (-> (g/empty-grid) 42 | (assoc [0 0] \H 43 | [1 0] \E 44 | [2 0] \L 45 | [3 0] \L 46 | [4 0] \O 47 | [5 0] \space 48 | [6 0] \W 49 | [7 0] \O 50 | [8 0] \R 51 | [9 0] \L 52 | [10 0] \D))) 53 | 54 | (make-hello-world) 55 | ``` 56 | 57 | which gives 58 | 59 | ``` 60 | HELLO WORLD 61 | ``` 62 | 63 | There are many functions available to make writing to a grid easier. For example, the following writes "HELLO WORLD" as well. 64 | 65 | ```clojure 66 | (g/text "HELLO WORLD") 67 | ``` 68 | 69 | ## Examples 70 | 71 | All the examples assume we are using the latest version. Some examples 72 | build up on earlier examples. 73 | 74 | To try these examples in a repl, require pp-grid as follows. 75 | 76 | ```clojure 77 | (require '[pp-grid.api :as g]) 78 | ``` 79 | 80 | All the examples are also available in `pp-grid.examples` namespace. 81 | 82 | Depending on our repl, the examples may print the map structure, instead of the string form. 83 | 84 | We can convert any grid to its string form using `(str grid)` and print the string: 85 | 86 | ```clojure 87 | (println (str grid)) 88 | ``` 89 | 90 | ### ABCD 91 | 92 | ```clojure 93 | (defn make-abcd [] 94 | (-> (g/empty-grid) 95 | (assoc [0 0] \A 96 | [10 0] \B 97 | [0 10] \C 98 | [10 10] \D 99 | [5 5] \*))) 100 | 101 | (make-abcd) 102 | ``` 103 | 104 | which gives 105 | 106 | ``` 107 | A B 108 | 109 | 110 | 111 | 112 | * 113 | 114 | 115 | 116 | 117 | C D 118 | ``` 119 | 120 | ### Boxed ABCD 121 | 122 | ```clojure 123 | (defn make-boxed-abcd [] 124 | (-> (make-abcd) 125 | (g/box :left-padding 1 :right-padding 1))) 126 | 127 | (make-boxed-abcd) 128 | ``` 129 | 130 | which gives 131 | 132 | ``` 133 | +-------------+ 134 | | A B | 135 | | | 136 | | | 137 | | | 138 | | | 139 | | * | 140 | | | 141 | | | 142 | | | 143 | | | 144 | | C D | 145 | +-------------+ 146 | ``` 147 | 148 | ### Horizontally aligned boxes 149 | 150 | ```clojure 151 | (defn make-haligned-boxes [] 152 | (let [b (g/box "B") 153 | b0 (g/box0 "B0") 154 | b1 (g/box1 "B1") 155 | b2 (g/box2 "B2") 156 | b3 (g/box3 "B3") 157 | b4 (g/box4 "B4") 158 | b5 (g/box5 "B5" :left-padding 2 :right-padding 2 :top-padding 2 :bottom-padding 2)] 159 | (g/halign [b b0 b1 b2 b3 b4 b5] 1))) 160 | 161 | (make-haligned-boxes) 162 | ``` 163 | 164 | which gives 165 | 166 | ``` 167 | +-+ ┌──┐ ╭──╮ .... ╒══╕ ******** 168 | |B| B0 │B1│ │B2│ :B3: │B4│ * * 169 | +-+ └──┘ ╰──╯ .... ╘══╛ * * 170 | * B5 * 171 | * * 172 | * * 173 | ******** 174 | ``` 175 | 176 | ### Tables 177 | 178 | ```clojure 179 | (defn make-tables [] 180 | (let [data [{:a 1 :b 2 :c 3} 181 | {:a 10 :b 20 :c 30}] 182 | t0 (g/table [:a :b] data) 183 | t1 (g/table1 [:a :b] data) 184 | t2 (g/table2 [:a :b] data) 185 | t3 (g/table3 [:a :b] data) 186 | matrix (g/matrix [:a :b] data) 187 | alphabets (g/table* (map #(g/box1 (char %)) (range 65 91)) 10)] 188 | (g/valign [t0 t1 t2 t3 matrix alphabets]))) 189 | 190 | (make-tables) 191 | ``` 192 | 193 | which gives 194 | 195 | ``` 196 | +----+----+ 197 | | :a | :b | 198 | +----+----+ 199 | | 1 | 2 | 200 | | 10 | 20 | 201 | +----+----+ 202 | ┌────┬────┐ 203 | │ :a │ :b │ 204 | ├────┼────┤ 205 | │ 1 │ 2 │ 206 | │ 10 │ 20 │ 207 | └────┴────┘ 208 | ╭────┬────╮ 209 | │ :a │ :b │ 210 | ├────┼────┤ 211 | │ 1 │ 2 │ 212 | │ 10 │ 20 │ 213 | ╰────┴────╯ 214 | ........... 215 | : :a : :b : 216 | ..........: 217 | : 1 : 2 : 218 | : 10 : 20 : 219 | :....:....: 220 | ╭ ╮ 221 | │ :a :b │ 222 | │ │ 223 | │ 1 2 │ 224 | │ 10 20 │ 225 | ╰ ╯ 226 | 227 | ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ 228 | │A│ │B│ │C│ │D│ │E│ │F│ │G│ │H│ │I│ │J│ 229 | └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ 230 | ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ 231 | │K│ │L│ │M│ │N│ │O│ │P│ │Q│ │R│ │S│ │T│ 232 | └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ 233 | ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ 234 | │U│ │V│ │W│ │X│ │Y│ │Z│ 235 | └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ 236 | ``` 237 | 238 | We can also put another table (or a grid) inside a table. 239 | 240 | ```clojure 241 | (defn make-nested-table [] 242 | (let [data [{:a 1 :b 2 :c 3 :d 4 :e 5} 243 | {:a 10 :b 20 :c 30 :d 40 :e 50}] 244 | t0 (g/table [:a :b :c :d :e] data) 245 | t1 (g/table1 [:a :b] data) 246 | abcd (make-boxed-abcd)] 247 | (g/table3 [:a :b :t0 :c :t1] 248 | [{:a 100 249 | :b 200 250 | :t0 t0 251 | :c abcd 252 | :t1 t1}]))) 253 | 254 | (make-nested-table) 255 | ``` 256 | 257 | which gives 258 | 259 | ``` 260 | .......................................................................... 261 | : :a : :b : :t0 : :c : :t1 : 262 | .........................................................................: 263 | : 100 : 200 : +----+----+----+----+----+ : +-------------+ : ┌────┬────┐ : 264 | : : : | :a | :b | :c | :d | :e | : | A B | : │ :a │ :b │ : 265 | : : : +----+----+----+----+----+ : | | : ├────┼────┤ : 266 | : : : | 1 | 2 | 3 | 4 | 5 | : | | : │ 1 │ 2 │ : 267 | : : : | 10 | 20 | 30 | 40 | 50 | : | | : │ 10 │ 20 │ : 268 | : : : +----+----+----+----+----+ : | | : └────┴────┘ : 269 | : : : : | * | : : 270 | : : : : | | : : 271 | : : : : | | : : 272 | : : : : | | : : 273 | : : : : | | : : 274 | : : : : | C D | : : 275 | : : : : +-------------+ : : 276 | :.....:.....:............................:.................:.............: 277 | ``` 278 | 279 | ### Decorated text 280 | 281 | Any grid can be decorated using ansi-escape-codes: 282 | 283 | ```clojure 284 | (defn make-decorated-text [s] 285 | (-> s 286 | g/text 287 | (g/decorate g/ESCAPE-CODE-BACKGROUND-BLUE))) 288 | 289 | (make-decorated-text "HELLO") 290 | ``` 291 | 292 | which prints "HELLO" in blue, if our terminal supports ansi-escape-codes. Depending 293 | on how a repl is configured, we might need to `println` the output of the function to see the 294 | blue color. 295 | 296 | Tables can also be decorated by passing in a sequence of ansi-escape-codes. For example, 297 | 298 | ```clojure 299 | (defn make-colored-table [] 300 | (let [data [{:a 1 :b 2} 301 | {:a 10 :b 20} 302 | {:a 100 :b 200} 303 | {:a 1000 :b 2000}]] 304 | (g/table2 [:a :b] data true :right [g/ESCAPE-CODE-BACKGROUND-GREEN 305 | g/ESCAPE-CODE-BACKGROUND-BRIGHT-MAGENTA 306 | g/ESCAPE-CODE-BACKGROUND-BRIGHT-BLUE]))) 307 | 308 | (make-colored-table) 309 | ``` 310 | 311 | which will give us a table with header-row colored in green and the rest colored 312 | in magenta and blue alternately. Again, depending on the repl configuration, we might 313 | need to `println` the output to see the colors. 314 | 315 | Boxes can be decorated by passing in `:fill-escape-codes` option. 316 | 317 | ```clojure 318 | (defn make-colored-boxes [] 319 | (let [abcd (make-abcd) 320 | b0 (g/box1 321 | abcd 322 | :fill-escape-codes [g/ESCAPE-CODE-BACKGROUND-BLUE]) 323 | b1 (g/box1 324 | abcd 325 | :fill-escape-codes [g/ESCAPE-CODE-BOLD 326 | g/ESCAPE-CODE-RED]) 327 | b2 (g/box0 328 | abcd 329 | :fill-escape-codes [g/ESCAPE-CODE-BOLD 330 | g/ESCAPE-CODE-BRIGHT-GREEN 331 | g/ESCAPE-CODE-BACKGROUND-BRIGHT-WHITE]) 332 | b3 (g/box2 333 | abcd 334 | :fill-escape-codes [g/ESCAPE-CODE-BOLD 335 | g/ESCAPE-CODE-RED 336 | g/ESCAPE-CODE-UNDERLINE])] 337 | (g/halign [b0 b1 b2 b3]))) 338 | 339 | (make-colored-boxes) 340 | ``` 341 | 342 | ### Trees 343 | 344 | ```clojure 345 | (defn make-tree [] 346 | (g/tree [1 2 [3 4] [:a :b [10 20]]])) 347 | 348 | (make-tree) 349 | ``` 350 | 351 | which gives 352 | 353 | ``` 354 | ┌───┐ 355 | │ 1 │ 356 | └───┘ 357 | │ 358 | ┌───┐ 359 | │ 2 │ 360 | └───┘ 361 | │ 362 | ┌───┐ ┌───┐ 363 | │ 3 │ │ 4 │ 364 | └───┘ └───┘ 365 | │ 366 | ┌────┐ ┌────┐ ┌────┐ 367 | │ :a │ │ :b │ │ 10 │ 368 | └────┘ └────┘ └────┘ 369 | │ 370 | ┌────┐ 371 | │ 20 │ 372 | └────┘ 373 | ``` 374 | 375 | ### XY Chart 376 | 377 | ```clojure 378 | (defn make-chart-xy [] 379 | (g/chart-xy (range) 380 | [0 1 2 3 2 1 0 1 2 3 2 1 0] 381 | :max-height 3)) 382 | 383 | (make-chart-xy) 384 | ``` 385 | 386 | which gives 387 | 388 | ``` 389 | y 390 | ▲ 391 | | 392 | | * * 393 | | * * * * 394 | | * * * * 395 | *-------------------*-------------------*-▶ x 396 | ``` 397 | 398 | ### Bar Chart 399 | 400 | ```clojure 401 | (defn make-chart-bar [] 402 | (g/chart-bar [20 80 100 200 400])) 403 | 404 | (make-chart-bar) 405 | ``` 406 | 407 | which gives 408 | 409 | ``` 410 | ■■ 20 411 | ■■■■■■■■ 80 412 | ■■■■■■■■■■■ 100 413 | ■■■■■■■■■■■■■■■■■■■■■ 200 414 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 400 415 | ``` 416 | 417 | For a vertical bar chart, we can set `:horizontal` to false: 418 | 419 | ```clojure 420 | (defn make-chart-bar-vertical [] 421 | (g/chart-bar [100 200 250 360] :horizontal false)) 422 | 423 | (make-chart-bar-vertical) 424 | ``` 425 | 426 | which gives 427 | 428 | ``` 429 | █ 430 | █ 431 | █ 432 | █ 433 | █ █ 434 | █ █ 435 | █ █ █ 436 | █ █ █ 437 | █ █ █ 438 | █ █ █ 439 | █ █ █ █ 440 | █ █ █ █ 441 | █ █ █ █ 442 | 1 2 2 3 443 | 0 0 5 6 444 | 0 0 0 0 445 | ``` 446 | 447 | ### Transformations 448 | 449 | A grid can be transformed using the `transform` function. 450 | 451 | It accepts a grid and a function that takes in a coordinate vector and 452 | returns a coordinate vector. 453 | 454 | ```clojure 455 | (defn make-transformations [] 456 | (let [abcd (make-boxed-abcd) 457 | width (:width abcd) 458 | height (:height abcd) 459 | hflipped-abcd (g/transform abcd (g/tf-hflip)) 460 | vflipped-abcd (g/transform abcd (g/tf-vflip)) 461 | vflipped-hflipped-abcd (g/transform hflipped-abcd (g/tf-vflip))] 462 | (-> abcd 463 | (assoc [width 0] hflipped-abcd) 464 | (assoc [0 height] vflipped-abcd) 465 | (assoc [width height] vflipped-hflipped-abcd) 466 | (g/transform (g/tf-scale 0.75 0.75))))) 467 | 468 | (make-transformations) 469 | ``` 470 | 471 | which gives 472 | 473 | ``` 474 | +----------+----------+ 475 | | A B | B A | 476 | | | | 477 | | | | 478 | | | | 479 | | * | * | 480 | | | | 481 | | | | 482 | | C D | D C | 483 | +----------+----------+ 484 | +----------+----------+ 485 | | C D | D C | 486 | | | | 487 | | | | 488 | | * | * | 489 | | | | 490 | | | | 491 | | | | 492 | | A B | B A | 493 | +----------+----------+ 494 | ``` 495 | 496 | ### Diagrams 497 | 498 | May be. Easy ones can be composed with some effort. 499 | 500 | Here is an example. Don't read too much into it :) 501 | 502 | ```clojure 503 | (defn make-diagram [] 504 | (let [a (g/box1 "a") 505 | b (g/box1 "b") 506 | c (g/box1 "c") 507 | d (g/box1 "d") 508 | 509 | abcd (g/transform (make-boxed-abcd) (g/tf-scale 0.75 0.75)) 510 | chart (g/chart-bar [10 20 30] :max-length 4) 511 | 512 | ra (g/arrow-right 5) 513 | c0 (-> (interpose ra [a abcd b]) 514 | (g/halign 1 true) 515 | g/box) 516 | c1 (-> (interpose ra [d chart]) 517 | (g/halign 1 true) 518 | g/box) 519 | c2 (-> (g/arrow-ne 3 "/" "*" "e") 520 | (assoc [0 0] (g/arrow-se 3 "\\" "*" "f")))] 521 | (g/halign (interpose ra [c0 c c1 c2]) 1 true))) 522 | 523 | (make-diagram) 524 | ``` 525 | 526 | which gives 527 | 528 | ``` 529 | +--------------------------------+ 530 | | +----------+ | 531 | | | A B | | 532 | | | | | 533 | |┌─┐ | | | +-------------------+ e 534 | |│a│ ────▶ | | ┌─┐| ┌─┐ |┌─┐ ■■ 10 | / 535 | |└─┘ | * | ────▶ │b│| ────▶ │c│ ────▶ |│d│ ────▶ ■■■■ 20 | ────▶ * 536 | | | | └─┘| └─┘ |└─┘ ■■■■■■ 30| \ 537 | | | | | +-------------------+ f 538 | | | C D | | 539 | | +----------+ | 540 | +--------------------------------+ 541 | ``` 542 | 543 | ## Babashka and GraalVM 544 | 545 | Expected to be compatible with both Babashka and GraalVM. Please file an issue otherwise. 546 | 547 | ## Clerk notebooks 548 | 549 | For viewing grids in a [clerk](https://github.com/nextjournal/clerk) notebook, we can 550 | set a custom viewer. 551 | 552 | For example, try 553 | 554 | ```clojure 555 | (clerk/set-viewers! [{:pred g/grid? 556 | :render-fn '(fn [s] (v/html [:pre s])) 557 | :transform-fn str}]) 558 | ``` 559 | 560 | If you learn to do something fancier please submit a pull-request to update 561 | this section :) 562 | 563 | ## Credits 564 | 565 | 0. [Clojure](https://clojure.org/) 566 | 1. Inspiration from [boxes](https://hackage.haskell.org/package/boxes), [cl-spark](https://github.com/tkych/cl-spark) 567 | 2. [Potemkin](https://github.com/clj-commons/potemkin) 568 | 3. All of these [libraries](https://github.com/rorokimdim/pp-grid/blob/master/project.clj) 569 | 570 | 571 | ## License 572 | 573 | Copyright © 2022 Amit Shrestha 574 | 575 | This program and the accompanying materials are made available under [MIT License](https://opensource.org/licenses/MIT) 576 | -------------------------------------------------------------------------------- /bb.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"]} 2 | -------------------------------------------------------------------------------- /dev/pp_grid/repl.clj: -------------------------------------------------------------------------------- 1 | (ns pp-grid.repl 2 | #_{:clj-kondo/ignore [:unused-namespace]} 3 | (:require [clojure.string :as s] 4 | [clojure.java.io :as io] 5 | [clojure.pprint :as pp] 6 | 7 | [clojure.java.shell :as shell] 8 | 9 | [criterium.core :as criterium] 10 | 11 | [pp-grid.api :as g] 12 | [pp-grid.examples :as e])) 13 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject org.clojars.rorokimdim/pp-grid "0.1.18" 2 | :description "A clojure library for constructing formatted text." 3 | :url "https://github.com/rorokimdim/pp-grid" 4 | :license {:name "MIT License" 5 | :url "https://opensource.org/licenses/MIT"} 6 | 7 | :plugins [[cider/cider-nrepl "0.28.1"] 8 | [mx.cider/enrich-classpath "1.5.2"] 9 | [lein-ancient "1.0.0-RC3"] 10 | [lein-shell "0.5.0"]] 11 | 12 | :dependencies [[org.clojure/clojure "1.10.1"] 13 | [potemkin "0.4.5"]] 14 | 15 | 16 | :repl-options {:init-ns pp-grid.repl} 17 | :aliases {"lint" ["shell" "clj-kondo" "--lint" "src" "test" "dev"]} 18 | 19 | 20 | :target-path "target/%s" 21 | :profiles {:uberjar {:aot :all 22 | :jvm-opts ["-Dclojure.compiler.direct-linking=true"]} 23 | :repl {:source-paths ["dev"] 24 | :dependencies [[criterium "0.4.6"]]}}) 25 | -------------------------------------------------------------------------------- /src/pp_grid/ansi_escape_code.clj: -------------------------------------------------------------------------------- 1 | (ns pp-grid.ansi-escape-code) 2 | 3 | (def ^:const ESCAPE-CODE-BOLD "\u001b[1m") 4 | (def ^:const ESCAPE-CODE-UNDERLINE "\u001b[4m") 5 | (def ^:const ESCAPE-CODE-REVERSED "\u001b[7m") 6 | 7 | (def ^:const ESCAPE-CODE-BLACK "\u001b[30m") 8 | (def ^:const ESCAPE-CODE-BLUE "\u001b[34m") 9 | (def ^:const ESCAPE-CODE-CYAN "\u001b[36m") 10 | (def ^:const ESCAPE-CODE-GREEN "\u001b[32m") 11 | (def ^:const ESCAPE-CODE-MAGENTA "\u001b[35m") 12 | (def ^:const ESCAPE-CODE-RED "\u001b[31m") 13 | (def ^:const ESCAPE-CODE-WHITE "\u001b[37m") 14 | (def ^:const ESCAPE-CODE-YELLOW "\u001b[33m") 15 | 16 | (def ^:const ESCAPE-CODE-BRIGHT-BLACK "\u001b[30;1m") 17 | (def ^:const ESCAPE-CODE-BRIGHT-BLUE "\u001b[34;1m") 18 | (def ^:const ESCAPE-CODE-BRIGHT-CYAN "\u001b[36;1m") 19 | (def ^:const ESCAPE-CODE-BRIGHT-GREEN "\u001b[32;1m") 20 | (def ^:const ESCAPE-CODE-BRIGHT-MAGENTA "\u001b[35;1m") 21 | (def ^:const ESCAPE-CODE-BRIGHT-RED "\u001b[31;1m") 22 | (def ^:const ESCAPE-CODE-BRIGHT-WHITE "\u001b[37;1m") 23 | (def ^:const ESCAPE-CODE-BRIGHT-YELLOW "\u001b[33;1m") 24 | 25 | (def ^:const ESCAPE-CODE-BACKGROUND-BLACK "\u001b[40m") 26 | (def ^:const ESCAPE-CODE-BACKGROUND-BLUE "\u001b[44m") 27 | (def ^:const ESCAPE-CODE-BACKGROUND-CYAN "\u001b[46m") 28 | (def ^:const ESCAPE-CODE-BACKGROUND-GREEN "\u001b[42m") 29 | (def ^:const ESCAPE-CODE-BACKGROUND-MAGENTA "\u001b[45m") 30 | (def ^:const ESCAPE-CODE-BACKGROUND-RED "\u001b[41m") 31 | (def ^:const ESCAPE-CODE-BACKGROUND-WHITE "\u001b[47m") 32 | (def ^:const ESCAPE-CODE-BACKGROUND-YELLOW "\u001b[43m") 33 | 34 | (def ^:const ESCAPE-CODE-BACKGROUND-BRIGHT-BLACK "\u001b[40;1m") 35 | (def ^:const ESCAPE-CODE-BACKGROUND-BRIGHT-BLUE "\u001b[44;1m") 36 | (def ^:const ESCAPE-CODE-BACKGROUND-BRIGHT-CYAN "\u001b[46;1m") 37 | (def ^:const ESCAPE-CODE-BACKGROUND-BRIGHT-GREEN "\u001b[42;1m") 38 | (def ^:const ESCAPE-CODE-BACKGROUND-BRIGHT-MAGENTA "\u001b[45;1m") 39 | (def ^:const ESCAPE-CODE-BACKGROUND-BRIGHT-RED "\u001b[41;1m") 40 | (def ^:const ESCAPE-CODE-BACKGROUND-BRIGHT-WHITE "\u001b[47;1m") 41 | (def ^:const ESCAPE-CODE-BACKGROUND-BRIGHT-YELLOW "\u001b[43;1m") 42 | 43 | (def ^:const ESCAPE-CODE-RESET "\u001b[0m") 44 | -------------------------------------------------------------------------------- /src/pp_grid/api.cljc: -------------------------------------------------------------------------------- 1 | (ns pp-grid.api 2 | #?(:bb 3 | (:require [pp-grid.core] 4 | [pp-grid.ansi-escape-code] 5 | [pp-grid.layout] 6 | [pp-grid.object]) 7 | :clj 8 | (:require [potemkin :refer [import-vars]] 9 | 10 | [pp-grid.core] 11 | [pp-grid.ansi-escape-code] 12 | [pp-grid.layout] 13 | [pp-grid.object]))) 14 | 15 | #?(:bb 16 | (do 17 | (defmacro import-public-vars [ns] 18 | `(do ~@(for [[n# v#] (map (partial take 2) (ns-publics ns))] 19 | (let [meta# (meta v#) 20 | doc# (:doc meta#)] 21 | (if (nil? doc#) 22 | `(def ~n# ~(symbol (str ns "/" n#))) 23 | `(def ~n# ~doc# ~(symbol (str ns "/" n#)))))))) 24 | 25 | (import-public-vars pp-grid.core) 26 | (import-public-vars pp-grid.object) 27 | (import-public-vars pp-grid.layout) 28 | (import-public-vars pp-grid.ansi-escape-code)) 29 | 30 | :clj 31 | (import-vars 32 | [pp-grid.core 33 | 34 | add 35 | empty-grid 36 | grid? 37 | height 38 | render 39 | render-grid 40 | subtract 41 | update-ranges 42 | width 43 | 44 | tf-hflip 45 | tf-project 46 | tf-rotate 47 | tf-rotate-90-degrees 48 | tf-scale 49 | tf-shear 50 | tf-translate 51 | tf-transpose 52 | tf-vflip 53 | transform 54 | 55 | ++ 56 | --] 57 | 58 | [pp-grid.layout 59 | 60 | valign 61 | halign 62 | vspacer 63 | hspacer 64 | pull 65 | spacer 66 | 67 | === 68 | ||] 69 | 70 | [pp-grid.object 71 | 72 | arrow-down 73 | arrow-left 74 | arrow-left-right 75 | arrow-ne 76 | arrow-nw 77 | arrow-right 78 | arrow-se 79 | arrow-sw 80 | arrow-up 81 | arrow-up-down 82 | box 83 | box0 84 | box1 85 | box2 86 | box3 87 | box4 88 | box5 89 | chart-bar 90 | chart-xy 91 | decorate 92 | fill 93 | hfill 94 | hline 95 | matrix 96 | paragraph 97 | table 98 | table* 99 | table0 100 | table1 101 | table2 102 | table3 103 | text 104 | tree 105 | vfill 106 | vline] 107 | 108 | [pp-grid.ansi-escape-code 109 | 110 | ESCAPE-CODE-BOLD 111 | ESCAPE-CODE-UNDERLINE 112 | ESCAPE-CODE-REVERSED 113 | 114 | ESCAPE-CODE-BLACK 115 | ESCAPE-CODE-BLUE 116 | ESCAPE-CODE-CYAN 117 | ESCAPE-CODE-GREEN 118 | ESCAPE-CODE-MAGENTA 119 | ESCAPE-CODE-RED 120 | ESCAPE-CODE-WHITE 121 | ESCAPE-CODE-YELLOW 122 | 123 | ESCAPE-CODE-BRIGHT-BLACK 124 | ESCAPE-CODE-BRIGHT-BLUE 125 | ESCAPE-CODE-BRIGHT-CYAN 126 | ESCAPE-CODE-BRIGHT-GREEN 127 | ESCAPE-CODE-BRIGHT-MAGENTA 128 | ESCAPE-CODE-BRIGHT-RED 129 | ESCAPE-CODE-BRIGHT-WHITE 130 | ESCAPE-CODE-BRIGHT-YELLOW 131 | 132 | ESCAPE-CODE-BACKGROUND-BLACK 133 | ESCAPE-CODE-BACKGROUND-BLUE 134 | ESCAPE-CODE-BACKGROUND-CYAN 135 | ESCAPE-CODE-BACKGROUND-GREEN 136 | ESCAPE-CODE-BACKGROUND-MAGENTA 137 | ESCAPE-CODE-BACKGROUND-RED 138 | ESCAPE-CODE-BACKGROUND-WHITE 139 | ESCAPE-CODE-BACKGROUND-YELLOW 140 | 141 | ESCAPE-CODE-BACKGROUND-BRIGHT-BLACK 142 | ESCAPE-CODE-BACKGROUND-BRIGHT-BLUE 143 | ESCAPE-CODE-BACKGROUND-BRIGHT-CYAN 144 | ESCAPE-CODE-BACKGROUND-BRIGHT-GREEN 145 | ESCAPE-CODE-BACKGROUND-BRIGHT-MAGENTA 146 | ESCAPE-CODE-BACKGROUND-BRIGHT-RED 147 | ESCAPE-CODE-BACKGROUND-BRIGHT-WHITE 148 | ESCAPE-CODE-BACKGROUND-BRIGHT-YELLOW 149 | 150 | ESCAPE-CODE-RESET])) 151 | -------------------------------------------------------------------------------- /src/pp_grid/core.cljc: -------------------------------------------------------------------------------- 1 | (ns pp-grid.core 2 | (:require 3 | #?@(:bb 4 | [[clojure.string :as s] 5 | [clojure.pprint] 6 | 7 | [pp-grid.ansi-escape-code :as ecodes]] 8 | :clj 9 | [[clojure.string :as s] 10 | [clojure.pprint] 11 | 12 | [potemkin :as p] 13 | 14 | [pp-grid.ansi-escape-code :as ecodes]]))) 15 | 16 | (declare 17 | add 18 | compute-ranges 19 | empty-grid 20 | empty-metadata 21 | escaped-char? 22 | grid? 23 | height 24 | render 25 | render-grid 26 | tf-translate 27 | transform 28 | update-ranges 29 | valid-key? 30 | valid-value? 31 | validate-key 32 | validate-value 33 | width) 34 | 35 | (defrecord EscapedChar [escape-code value] 36 | Object 37 | (toString [this] (render this))) 38 | 39 | #?(:bb 40 | :skip 41 | :clj 42 | #_:clj-kondo/ignore 43 | (p/def-map-type Grid [m metadata] 44 | (get [this k default-value] 45 | (if (vector? k) 46 | (get m k default-value) 47 | (cond 48 | (= k :min-x) (first (:mins metadata)) 49 | (= k :max-x) (first (:maxs metadata)) 50 | (= k :min-y) (second (:mins metadata)) 51 | (= k :max-y) (second (:maxs metadata)) 52 | (= k :width) (width this) 53 | (= k :height) (height this) 54 | (= k :m) m 55 | :else (get metadata k default-value)))) 56 | (assoc [this k v] 57 | (when (and (validate-key (:dimension metadata) k) (validate-value v)) 58 | (if (grid? v) 59 | (let [ds (map - k (:mins v))] 60 | (add this (transform v (apply tf-translate ds)))) 61 | (Grid. (assoc m k v) (update-ranges metadata k))))) 62 | (dissoc [this k] 63 | (when (validate-key (:dimension metadata) k) 64 | (let [new-g (Grid. (dissoc m k) metadata) 65 | dimension (:dimension metadata)] 66 | (with-meta new-g (apply update-ranges 67 | (empty-metadata dimension) 68 | (keys new-g)))))) 69 | (keys [this] (keys m)) 70 | (meta [this] metadata) 71 | (empty [this] (Grid. (empty m) (empty-metadata (:dimension metadata)))) 72 | (with-meta [this metadata] (Grid. m metadata)) 73 | clojure.lang.IPersistentCollection 74 | (equiv 75 | [this x] 76 | (if (string? x) 77 | (= (render this) x) 78 | (and (or (instance? java.util.Map x) (map? x)) 79 | (= x m)))) 80 | Object 81 | (toString [this] (render this)))) 82 | 83 | #?(:bb 84 | (defn ->Grid 85 | "Adapted from https://blog.wsscode.com/guide-to-custom-map-types/." 86 | [m metadata] 87 | (let [->GridEntry (fn [k v] 88 | (proxy [clojure.lang.AMapEntry] [] 89 | (key [] k) 90 | (val [] v) 91 | (getKey [] k) 92 | (getValue [] v))) 93 | lookup (fn [k default-value] 94 | (if (vector? k) 95 | (get m k default-value) 96 | (cond 97 | (= k :min-x) (first (:mins metadata)) 98 | (= k :max-x) (first (:maxs metadata)) 99 | (= k :min-y) (second (:mins metadata)) 100 | (= k :max-y) (second (:maxs metadata)) 101 | (= k :width) (width (->Grid m metadata)) 102 | (= k :height) (height (->Grid m metadata)) 103 | (= k :m) m 104 | :else (get metadata k default-value))))] 105 | (proxy [clojure.lang.APersistentMap 106 | clojure.lang.IMeta 107 | clojure.lang.IObj] 108 | [] 109 | (valAt 110 | ([k] 111 | (lookup k nil)) 112 | ([k default-value] 113 | (lookup k default-value))) 114 | (iterator [] 115 | ;; does not work 116 | ;; so need to avoid using this. 117 | ;; one work around is to iterate over a seq of grid 118 | (.iterator ^java.lang.Iterable 119 | (eduction 120 | (map #(->GridEntry % (get this %))) 121 | (keys m)))) 122 | (containsKey [k] (or (contains? m k) 123 | (#{:min-x :max-x 124 | :min-y :max-y 125 | :width :height 126 | :m} k))) 127 | (entryAt [k] 128 | (let [default (gensym) 129 | v (lookup k default)] 130 | (when (not= v default) 131 | (->GridEntry k v)))) 132 | (equiv [x] 133 | (if (string? x) 134 | (= (render (->Grid m metadata)) x) 135 | (= m x))) 136 | (empty [] (->Grid (empty m) (empty-metadata (:dimension metadata)))) 137 | (count [] (count m)) 138 | (assoc [k v] 139 | (when (and (validate-key (:dimension metadata) k) (validate-value v)) 140 | (if (grid? v) 141 | (let [ds (map - k (:mins v))] 142 | (add (->Grid m metadata) (transform v (apply tf-translate ds)))) 143 | (->Grid (assoc m k v) (update-ranges metadata k))))) 144 | (without [k] 145 | (when (validate-key (:dimension metadata) k) 146 | (let [dimension (:dimension metadata) 147 | new-g (->Grid (dissoc m k) {})] 148 | (with-meta new-g (apply update-ranges 149 | (empty-metadata dimension) 150 | (keys new-g)))))) 151 | (seq [] (some->> (keys m) 152 | (map #(->GridEntry % (get this %))))) 153 | (meta [] metadata) 154 | (withMeta [meta] (->Grid m meta)) 155 | (toString [] 156 | (render-grid (->Grid m metadata)))))) 157 | :clj 158 | (defn ->Grid [m metadata] 159 | (Grid. m metadata))) 160 | 161 | (defn empty-metadata 162 | ([] (empty-metadata 2)) 163 | ([dimension] {:dimension dimension :grid? true})) 164 | 165 | (defn empty-grid 166 | ([] 167 | (empty-grid 2)) 168 | ([dimension] 169 | {:pre [(integer? dimension) 170 | (pos? dimension)]} 171 | (->Grid {} (empty-metadata dimension)))) 172 | 173 | (defn width [x] 174 | (cond 175 | (string? x) (->> x 176 | (take-while #(not= % \newline)) 177 | count) 178 | (grid? x) (if (empty? x) 179 | 0 180 | (inc (- (:max-x x) (:min-x x)))) 181 | :else (width (str x)))) 182 | 183 | (defn height [x] 184 | (cond 185 | (string? x) (->> x 186 | (filter #(= % \newline)) 187 | count 188 | inc) 189 | (grid? x) (if (empty? x) 0 190 | (inc (- (:max-y x) (:min-y x)))) 191 | :else (height (str x)))) 192 | 193 | (defn escaped-char? [x] 194 | (instance? EscapedChar x)) 195 | 196 | #?(:bb 197 | (defn grid? [x] 198 | (boolean (:grid? (meta x)))) 199 | :clj 200 | (defn grid? [x] 201 | (instance? Grid x))) 202 | 203 | (defn escaped-char [c escape-code] 204 | (->EscapedChar escape-code c)) 205 | 206 | (defn update-ranges 207 | [metadata & ks] 208 | (if (empty? ks) 209 | metadata 210 | (let [mins (:mins metadata nil) 211 | maxs (:maxs metadata nil)] 212 | (assoc metadata 213 | :mins (apply (partial map min) 214 | (if (nil? mins) ks (conj ks mins))) 215 | :maxs (apply (partial map max) 216 | (if (nil? maxs) ks (conj ks maxs))))))) 217 | 218 | (defn add 219 | "Constructs a grid with all given grids added together." 220 | [& gs] 221 | (let [gs (remove empty? gs)] 222 | (cond 223 | (empty? gs) nil 224 | (= 1 (count gs)) (first gs) 225 | :else (let [ga (first gs) 226 | metadata (meta ga) 227 | new-m (apply assoc 228 | (:m ga) 229 | (mapcat identity (apply concat (rest gs))))] 230 | (->Grid new-m (apply update-ranges metadata (keys new-m))))))) 231 | 232 | (defn subtract 233 | "Returns the first grid minus keys in rest of the grids." 234 | [& gs] 235 | (reduce 236 | (fn [ga gb] 237 | (cond 238 | (empty? gb) ga 239 | (empty? ga) (reduced ga) 240 | :else (apply dissoc ga (keys gb)))) 241 | (first gs) 242 | (rest gs))) 243 | 244 | (defn decorate 245 | "Decorates a grid with given ansi-escape-codes." 246 | [g & escape-codes] 247 | (reduce 248 | (fn [acc [k v]] 249 | (let [escape-code (apply str escape-codes) 250 | new-v (if (escaped-char? v) 251 | (->EscapedChar (apply str escape-code (:escape-code v)) 252 | (:value v)) 253 | (->EscapedChar escape-code v))] 254 | (assoc acc k new-v))) 255 | (empty g) 256 | g)) 257 | 258 | (defn ++ 259 | "Convenience wrapper for add to accept grids as args." 260 | [& gs] 261 | (add gs)) 262 | 263 | (defn -- 264 | "Convenience wrapper for subtract to accept grids as args." 265 | [& gs] 266 | (subtract gs)) 267 | 268 | (defn valid-key? [dimension k] 269 | (and 270 | (vector? k) 271 | (every? integer? k) 272 | (= (count k) dimension))) 273 | 274 | (defn valid-value? [v] 275 | (or (grid? v) 276 | (string? v) 277 | (escaped-char? v) 278 | (char? v) 279 | (contains? (methods render) (type v)))) 280 | 281 | (defn validate-key [dimension k] 282 | (when-not (valid-key? dimension k) 283 | (throw (IllegalArgumentException. (str "Key expected to be a vector of " 284 | dimension " integers")))) 285 | k) 286 | 287 | (defn validate-value [v] 288 | (when-not (valid-value? v) 289 | (throw (IllegalArgumentException. (str "Value must be a char, a Grid or a EscapedChar; got " v)))) 290 | true) 291 | 292 | (defn round [n] 293 | (if (integer? n) 294 | n 295 | (Math/round (double n)))) 296 | 297 | (defn transform 298 | "Transforms a grid into another grid using given transformation function. 299 | 300 | A transformation function accepts a key (coordinate vector) and returns another key. 301 | 302 | For example transformation functions, take a look at the tf-* functions." 303 | ([g f] (transform g f (:dimension g))) 304 | ([g f dimension] 305 | (let [transformed (persistent! 306 | (reduce 307 | (fn [acc [k v]] 308 | (let [new-k (into [] (map round (f k)))] 309 | (assoc! acc new-k v))) 310 | (transient {}) 311 | (seq g)))] 312 | (->Grid transformed 313 | (apply update-ranges 314 | (empty-metadata dimension) 315 | (keys transformed)))))) 316 | 317 | (defn tf-translate 318 | "Returns a function that translates a coordinate by given deltas. 319 | 320 | For example, ((tf-translate 10 20) [1 2]) = [11 22]." 321 | [& deltas] 322 | (fn [k] 323 | (map + k (take (count k) (concat deltas (repeat 0)))))) 324 | 325 | (defn tf-scale 326 | "Returns a function that scales a coordinate by given amounts. 327 | 328 | For example, ((tf-scale 10 2) [3 2]) = [30 4]." 329 | [& ns] 330 | (fn [k] 331 | (map * k (take (count k) (concat ns (repeat 1)))))) 332 | 333 | (defn tf-transpose 334 | "Returns a function that transposes a coordinate. 335 | 336 | Returns a function just to keep it consistent with other tf-* 337 | functions. 338 | 339 | For example, ((tf-transpose) [1 2]) = [2 1]." 340 | [] 341 | (fn [k] 342 | (reverse k))) 343 | 344 | (defn tf-hflip 345 | "Returns a function that horizontally flips a coordinate. 346 | 347 | Returns a function just to keep it consistent with other tf-* 348 | functions. 349 | 350 | For example, ((tf-hflip) [1 2]) = [-1 2]." 351 | [] 352 | (fn [k] 353 | (let [[x y] (validate-key 2 k)] 354 | (list (- x) y)))) 355 | 356 | (defn tf-vflip 357 | "Returns a function that vertically flips a coordinate. 358 | 359 | Returns a function just to keep it consistent with other tf-* 360 | functions. 361 | 362 | For example, ((tf-vflip) [1 2]) = [1 -2]." 363 | [] 364 | (fn [k] 365 | (let [[x y] (validate-key 2 k)] 366 | (list x (- y))))) 367 | 368 | (defn tf-project 369 | "Returns a function that projects a coordinate into given dimension. 370 | 371 | For example, ((tf-project 2) [1 2 3 4 5]) = [1 2]. 372 | 373 | A project-fn can be passed in to change how a coordinate is projected. 374 | By default, the identity function is used." 375 | ([target-dimension] 376 | (tf-project target-dimension identity)) 377 | ([target-dimension project-fn] 378 | (fn [k] 379 | (take target-dimension (concat (project-fn k) (repeat 0)))))) 380 | 381 | (defn tf-rotate 382 | "Returns a function that rotates a coordinate by given angle in radians." 383 | [radians] 384 | (fn [k] 385 | (let [[x y] (validate-key 2 k)] 386 | (list 387 | (- (* x (Math/cos radians)) 388 | (* y (Math/sin radians))) 389 | (+ (* x (Math/sin radians)) 390 | (* y (Math/cos radians))))))) 391 | 392 | (defn tf-rotate-90-degrees 393 | "Returns a function that rotates a coordinate by 90 degrees." 394 | [] 395 | (fn [k] 396 | (let [[x y] (validate-key 2 k)] 397 | (list (- y) x)))) 398 | 399 | (defn tf-shear 400 | "Returns a function that shears a coordinate by given factors. 401 | 402 | See https://en.wikipedia.org/wiki/Shear_mapping. 403 | 404 | For example, ((tf-shear 2 3) [10 5]) = [10 + 2 * 5, 5 + 3 * 10] = [20 35]." 405 | [a b] 406 | (fn [[x y & zs]] 407 | (concat [(+ x (* a y)) (+ y (* b x))] zs))) 408 | 409 | (defmulti ^String render-grid :dimension) 410 | 411 | (defmethod render-grid 1 [g] 412 | (if (empty? g) 413 | nil 414 | (let [min-x (:min-x g) 415 | max-x (:max-x g) 416 | xs (range min-x (inc max-x)) 417 | get-rendered (fn [x] (render (get g [x] " ")))] 418 | (apply str (map get-rendered xs))))) 419 | 420 | #_{:clj-kondo/ignore [:unresolved-namespace]} 421 | (defmethod render-grid 2 [g] 422 | (if (empty? g) 423 | nil 424 | (let [min-x (:min-x g) 425 | min-y (:min-y g) 426 | max-x (:max-x g) 427 | max-y (:max-y g) 428 | xs (range min-x (inc max-x)) 429 | ys (range min-y (inc max-y)) 430 | get-rendered (fn [x y] (render (get g [x y] " ")))] 431 | (s/join 432 | \newline 433 | (for [y ys] 434 | (apply str (map get-rendered xs (repeat y)))))))) 435 | 436 | (defmethod render-grid :default [g] 437 | (if (empty? g) 438 | nil 439 | (render-grid (transform g (tf-project 2) 2)))) 440 | 441 | (defmulti ^String render type) 442 | 443 | (defmethod render java.lang.String [x] x) 444 | 445 | (defmethod render :default [x] 446 | (if (grid? x) 447 | (render-grid x) 448 | (str x))) 449 | 450 | #_{:clj-kondo/ignore [:unresolved-namespace]} 451 | (defmethod render EscapedChar [x] 452 | (str (:escape-code x) 453 | (:value x) 454 | ecodes/ESCAPE-CODE-RESET)) 455 | 456 | 457 | ;; 458 | ;; Overwrite how Grid and EscapedChar get printed: Make them print the rendered string 459 | ;; 460 | #?(:bb 461 | :skip 462 | :clj 463 | (do 464 | (defmethod print-method Grid [g ^java.io.Writer w] 465 | (.write w (render g))) 466 | 467 | (defmethod print-method EscapedChar [x ^java.io.Writer w] 468 | (.write w (render x))) 469 | 470 | (defmethod clojure.pprint/simple-dispatch Grid [g] 471 | (println (render g))) 472 | 473 | (defmethod clojure.pprint/simple-dispatch EscapedChar [x] 474 | (println (render x))))) 475 | -------------------------------------------------------------------------------- /src/pp_grid/examples.clj: -------------------------------------------------------------------------------- 1 | (ns pp-grid.examples 2 | (:require [pp-grid.api :as g])) 3 | 4 | (defn make-grid-illustration [max-x max-y] 5 | (let [cells (->> (for [y (range (inc max-y))] 6 | (for [x (range (inc max-x))] 7 | (g/box1 (g/text (str "(" x "," y ")"))))) 8 | (map g/halign) 9 | g/valign) 10 | x-axis (g/=== 11 | 1 12 | (g/arrow-right (+ (:width cells) 2) \-) 13 | (g/text "x")) 14 | y-axis (g/|| 15 | 0 16 | (g/arrow-down (+ (:height cells) 2) \|) 17 | (g/text "y"))] 18 | (g/halign [y-axis (g/valign [x-axis cells])] 0))) 19 | 20 | (defn make-hello-world [] 21 | (-> (g/empty-grid) 22 | (assoc [0 0] \H 23 | [1 0] \E 24 | [2 0] \L 25 | [3 0] \L 26 | [4 0] \O 27 | [5 0] \space 28 | [6 0] \W 29 | [7 0] \O 30 | [8 0] \R 31 | [9 0] \L 32 | [10 0] \D))) 33 | 34 | (defn make-abcd [] 35 | (-> (g/empty-grid) 36 | (assoc [0 0] \A 37 | [10 0] \B 38 | [0 10] \C 39 | [10 10] \D 40 | [5 5] \*))) 41 | 42 | (defn make-boxed-abcd [] 43 | (-> (make-abcd) 44 | (g/box :left-padding 1 :right-padding 1))) 45 | 46 | (defn make-haligned-boxes [] 47 | (let [b (g/box "B") 48 | b0 (g/box0 "B0") 49 | b1 (g/box1 "B1") 50 | b2 (g/box2 "B2") 51 | b3 (g/box3 "B3") 52 | b4 (g/box4 "B4") 53 | b5 (g/box5 "B5" :left-padding 2 :right-padding 2 :top-padding 2 :bottom-padding 2)] 54 | (g/halign [b b0 b1 b2 b3 b4 b5] 1))) 55 | 56 | (defn make-tables [] 57 | (let [data [{:a 1 :b 2 :c 3} 58 | {:a 10 :b 20 :c 30}] 59 | t0 (g/table [:a :b] data) 60 | t1 (g/table1 [:a :b] data) 61 | t2 (g/table2 [:a :b] data) 62 | t3 (g/table3 [:a :b] data) 63 | matrix (g/matrix [:a :b] data) 64 | alphabets (g/table* (map #(g/box1 (char %)) (range 65 91)) 10)] 65 | (g/valign [t0 t1 t2 t3 matrix alphabets]))) 66 | 67 | (defn make-nested-table [] 68 | (let [data [{:a 1 :b 2 :c 3 :d 4 :e 5} 69 | {:a 10 :b 20 :c 30 :d 40 :e 50}] 70 | t0 (g/table [:a :b :c :d :e] data) 71 | t1 (g/table1 [:a :b] data) 72 | abcd (make-boxed-abcd)] 73 | (g/table3 [:a :b :t0 :c :t1] 74 | [{:a 100 75 | :b 200 76 | :t0 t0 77 | :c abcd 78 | :t1 t1}]))) 79 | 80 | (defn make-colored-table [] 81 | (let [data [{:a 1 :b 2} 82 | {:a 10 :b 20} 83 | {:a 100 :b 200} 84 | {:a 1000 :b 2000}]] 85 | (g/table2 [:a :b] data true :right [g/ESCAPE-CODE-BACKGROUND-GREEN 86 | g/ESCAPE-CODE-BACKGROUND-BRIGHT-MAGENTA 87 | g/ESCAPE-CODE-BACKGROUND-BRIGHT-BLUE]))) 88 | 89 | (defn make-colored-boxes [] 90 | (let [abcd (make-abcd) 91 | b0 (g/box1 92 | abcd 93 | :fill-escape-codes [g/ESCAPE-CODE-BACKGROUND-BLUE]) 94 | b1 (g/box1 95 | abcd 96 | :fill-escape-codes [g/ESCAPE-CODE-BOLD 97 | g/ESCAPE-CODE-RED]) 98 | b2 (g/box0 99 | abcd 100 | :fill-escape-codes [g/ESCAPE-CODE-BOLD 101 | g/ESCAPE-CODE-BRIGHT-GREEN 102 | g/ESCAPE-CODE-BACKGROUND-BRIGHT-WHITE]) 103 | b3 (g/box2 104 | abcd 105 | :fill-escape-codes [g/ESCAPE-CODE-BOLD 106 | g/ESCAPE-CODE-RED 107 | g/ESCAPE-CODE-UNDERLINE])] 108 | (g/halign [b0 b1 b2 b3]))) 109 | 110 | (defn make-decorated-text [s] 111 | (-> s 112 | g/text 113 | (g/decorate g/ESCAPE-CODE-BACKGROUND-BLUE))) 114 | 115 | (defn make-tree [] 116 | (g/tree [1 2 [3 4] [:a :b [10 20]]])) 117 | 118 | (defn make-chart-xy [] 119 | (g/chart-xy (range) 120 | [0 1 2 3 2 1 0 1 2 3 2 1 0] 121 | :max-height 3)) 122 | 123 | (defn make-chart-bar [] 124 | (g/chart-bar [20 80 100 200 400])) 125 | 126 | (defn make-chart-bar-vertical [] 127 | (g/chart-bar [100 200 250 360] :horizontal false)) 128 | 129 | (defn make-transformations [] 130 | (let [abcd (make-boxed-abcd) 131 | width (:width abcd) 132 | height (:height abcd) 133 | hflipped-abcd (g/transform abcd (g/tf-hflip)) 134 | vflipped-abcd (g/transform abcd (g/tf-vflip)) 135 | vflipped-hflipped-abcd (g/transform hflipped-abcd (g/tf-vflip))] 136 | (-> abcd 137 | (assoc [width 0] hflipped-abcd) 138 | (assoc [0 height] vflipped-abcd) 139 | (assoc [width height] vflipped-hflipped-abcd) 140 | (g/transform (g/tf-scale 0.75 0.75))))) 141 | 142 | (defn make-diagram [] 143 | (let [a (g/box1 "a") 144 | b (g/box1 "b") 145 | c (g/box1 "c") 146 | d (g/box1 "d") 147 | 148 | abcd (g/transform (make-boxed-abcd) (g/tf-scale 0.75 0.75)) 149 | chart (g/chart-bar [10 20 30] :max-length 4) 150 | 151 | ra (g/arrow-right 5) 152 | c0 (-> (interpose ra [a abcd b]) 153 | (g/halign 1 true) 154 | g/box) 155 | c1 (-> (interpose ra [d chart]) 156 | (g/halign 1 true) 157 | g/box) 158 | c2 (-> (g/arrow-ne 3 "/" "*" "e") 159 | (assoc [0 0] (g/arrow-se 3 "\\" "*" "f")))] 160 | (g/halign (interpose ra [c0 c c1 c2]) 1 true))) 161 | 162 | (defn make-paragraphs [] 163 | (let [p0 (g/paragraph 164 | "Lorem ipsum dolor sit amet. Et dolor minima non expedita 165 | exercitationem nam quibusdam totam. Et voluptatibus sint a 166 | provident harum ut totam reprehenderit.") 167 | p1 (g/paragraph 168 | "In quia voluptas ad voluptates enim ut officia quaerat 169 | quo illum atque. Ut veniam dolorem non nisi quia est facere 170 | iure est dolore eius non tenetur voluptatem. 33 autem 171 | sequi et itaque totam sed reiciendis adipisci est quaerat voluptas. 172 | Qui suscipit corrupti qui natus adipisci est voluptatibus cumque 173 | sed natus rerum et dolores blanditiis et iusto quibusdam?" 0 0 60 1) 174 | p2 (g/paragraph 175 | "Ut iste libero et debitis alias eum possimus maxime. Eum magnam 176 | aspernatur ex accusamus commodi distinctio quia aut nostrum 177 | molestiae non totam reprehenderit quo inventore sequi." 0 0 50 3)] 178 | (g/valign [p0 p1 p2] 1))) 179 | -------------------------------------------------------------------------------- /src/pp_grid/layout.clj: -------------------------------------------------------------------------------- 1 | (ns pp-grid.layout 2 | (:require [pp-grid.core :as c])) 3 | 4 | (defn valign 5 | "Constructs a grid containing given grids aligned vertically. 6 | 7 | For example, (valign [(text \"A\") (text \"B\") (text \"C\")]) is 8 | A 9 | B 10 | C 11 | " 12 | ([grids] 13 | (valign grids 0)) 14 | ([grids y-padding] 15 | (valign grids y-padding false)) 16 | ([grids y-padding center?] 17 | (reduce 18 | (fn [top bottom] 19 | (cond 20 | (empty? top) bottom 21 | (empty? bottom) top 22 | :else (let [top-width (:width top) 23 | bottom-width (:width bottom) 24 | top-center-x (+ (:min-x top) (/ top-width 2)) 25 | bottom-center-x (+ (:min-x bottom) (/ bottom-width 2)) 26 | x-diff (- top-center-x bottom-center-x) 27 | dx (if center? x-diff 0) 28 | dy (+ y-padding (:height top))] 29 | (c/add top (c/transform bottom (c/tf-translate dx dy)))))) 30 | (first grids) 31 | (rest grids)))) 32 | 33 | (defn halign 34 | "Constructs a grid containing given grids aligned horizontally. 35 | 36 | For example, (halign [(text \"A\") (text \"B\") (text \"C\")]) is ABC." 37 | ([grids] 38 | (halign grids 0)) 39 | ([grids x-padding] 40 | (halign grids x-padding false)) 41 | ([grids x-padding center?] 42 | (reduce 43 | (fn [left right] 44 | (cond 45 | (empty? left) right 46 | (empty? right) left 47 | :else (let [dx (+ x-padding (:width left)) 48 | left-height (:height left) 49 | right-height (:height right) 50 | left-center-y (+ (:min-y left) (/ left-height 2)) 51 | right-center-y (+ (:min-y right) (/ right-height 2)) 52 | y-diff (- left-center-y right-center-y) 53 | dy (if center? y-diff 0)] 54 | (c/add left (c/transform right (c/tf-translate dx dy)))))) 55 | (first grids) 56 | (rest grids)))) 57 | 58 | (defn hspacer 59 | "Constructs a horizontal space of given length. 60 | 61 | For example, (hspacer 5) is \" \"." 62 | ([n] 63 | (if (<= n 0) (c/empty-grid) 64 | (-> (c/empty-grid) 65 | (assoc [0 0] \space) 66 | (assoc [(dec n) 0] \space))))) 67 | 68 | (defn vspacer 69 | "Constructs a vertical space of given length. 70 | 71 | For example, (vspacer 5) is 72 | (space) 73 | (space) 74 | (space) 75 | (space) 76 | (space) 77 | " 78 | ([n] 79 | (if (<= n 0) (c/empty-grid) 80 | (-> (c/empty-grid) 81 | (assoc [0 0] \space) 82 | (assoc [0 (dec n)] \space))))) 83 | 84 | (defn spacer 85 | "Constructs a rectangular space of given width and height." 86 | ([width height] 87 | (c/add (hspacer width) (vspacer height)))) 88 | 89 | (defn === 90 | "Horizontally aligns given grids with some defaults. 91 | 92 | Just a convenience wrapper for halign to accept grids as args and 93 | use some default values for padding and centering." 94 | [x-padding & grids] 95 | (halign grids x-padding true)) 96 | 97 | (defn || 98 | "Vertically aligns given grids with some defaults. 99 | 100 | Just a convenience wrapper for valign to accept grids as args and 101 | use some default values for padding and centering." 102 | [y-padding & grids] 103 | (valign grids y-padding true)) 104 | 105 | (defn pull 106 | "Pulls a grid by given amounts horizontally and vertically. 107 | 108 | Just a convenience function for transforming with tf-translate. Useful 109 | for tweaking alignments." 110 | [g dx dy] 111 | (c/transform g (c/tf-translate dx dy))) 112 | -------------------------------------------------------------------------------- /src/pp_grid/object.clj: -------------------------------------------------------------------------------- 1 | (ns pp-grid.object 2 | (:require [clojure.string :as s] 3 | [clojure.pprint :as pp] 4 | 5 | [pp-grid.ansi-escape-code :as ecodes] 6 | [pp-grid.core :as c] 7 | [pp-grid.layout :as l])) 8 | 9 | (defn text 10 | "Constructs a grid containing given string. 11 | 12 | If s is not a string, then (str s) 13 | will be used. 14 | 15 | Each line in the string is put in a new row." 16 | ([s] 17 | (text s 0)) 18 | ([s padding] 19 | (text s padding padding)) 20 | ([s pad-left pad-right] 21 | (text s pad-left pad-right \space)) 22 | ([s pad-left pad-right pad-char] 23 | (if (string? s) 24 | (->> s 25 | s/split-lines 26 | (map vector (range)) 27 | (reduce 28 | (fn [g [y ^String line]] 29 | (reduce 30 | (fn [g [x v]] 31 | (assoc g [x y] v)) 32 | g 33 | (map vector 34 | (range) 35 | (str (apply str (repeat pad-left pad-char)) 36 | (if (empty? line) " " line) 37 | (apply str (repeat pad-right pad-char)))))) 38 | (c/empty-grid))) 39 | (text (str s) pad-left pad-right pad-char)))) 40 | 41 | (defn paragraph 42 | "Constructs a grid containing a paragraph of text. 43 | 44 | By default paragraphs have max-width of 80 chars. If any word 45 | is longer than max-width chars, it will appear alone in a line without 46 | being truncated." 47 | ([s] 48 | (paragraph s 0)) 49 | ([s padding] 50 | (paragraph s padding padding)) 51 | ([s pad-left pad-right] 52 | (paragraph s pad-left pad-right 80)) 53 | ([s pad-left pad-right max-width] 54 | (paragraph s pad-left pad-right max-width 0)) 55 | ([s pad-left pad-right max-width first-line-indent] 56 | (as-> s $ 57 | (s/split $ #"\s+") 58 | (pp/cl-format nil (str "~{~<~%~1," max-width ":;~A~> ~}") $) 59 | (text (str (apply str (repeat first-line-indent \space)) $) 60 | pad-left pad-right)))) 61 | 62 | (defn decorate 63 | "Decorates a grid or a string-convertible value with given ansi-escape-codes." 64 | [x escape-code & escape-codes] 65 | (if (c/grid? x) 66 | (reduce 67 | (fn [acc [k v]] 68 | (let [escape-code (apply str escape-code escape-codes) 69 | new-v (if (c/escaped-char? v) 70 | (c/->EscapedChar (apply str escape-code (:escape-code v)) 71 | (:value v)) 72 | (c/->EscapedChar escape-code v))] 73 | (assoc acc k new-v))) 74 | (empty x) 75 | (seq x)) 76 | (apply decorate (text x) escape-code escape-codes))) 77 | 78 | (defn hline 79 | "Constructs a horizontal line of given length. 80 | 81 | For example, (hline 3) is '---'." 82 | ([n] 83 | (hline n \─ \─ \─)) 84 | ([n body-char] 85 | (hline n body-char body-char body-char)) 86 | ([n body-char start-char end-char] 87 | (cond 88 | (<= n 0) nil 89 | (= n 1) (assoc (c/empty-grid) [0 0] body-char) 90 | (= n 2) (text (str start-char end-char)) 91 | :else (text 92 | (s/join 93 | nil 94 | (list 95 | start-char 96 | (s/join nil (repeat (- n 2) body-char)) 97 | end-char)))))) 98 | 99 | (defn vline 100 | "Constructs a vertical line of given length. 101 | 102 | For example, (vline 3) is 103 | | 104 | | 105 | | 106 | " 107 | ([n] 108 | (vline n \│ \│ \│)) 109 | ([n body-char] 110 | (vline n body-char body-char body-char)) 111 | ([n body-char start-char end-char] 112 | (cond 113 | (<= n 0) nil 114 | (= n 1) (assoc (c/empty-grid) [0 0] body-char) 115 | (= n 2) (text (str start-char \newline end-char)) 116 | :else (text (s/join 117 | \newline 118 | (list 119 | start-char 120 | (s/join \newline (repeat (- n 2) body-char)) 121 | end-char)))))) 122 | 123 | (defn hfill 124 | "Constructs a horizontal-filler of given size. 125 | 126 | For example, (hfill 3 \\*) is '***'." 127 | ([n] 128 | (hfill n \space)) 129 | ([n c] 130 | (hline n c))) 131 | 132 | (defn vfill 133 | "Constructs a vertical-filler of given size. 134 | 135 | For example, (vfill 3 \\*) is 136 | * 137 | * 138 | * 139 | " 140 | ([n] 141 | (vfill n \space)) 142 | ([n c] 143 | (vline n c))) 144 | 145 | (defn fill 146 | "Constructs a filler of given width and height. 147 | 148 | For example, (fill 4 4 \\*) is 149 | **** 150 | **** 151 | **** 152 | **** 153 | " 154 | ([w h] 155 | (fill w h \space)) 156 | ([w h c] 157 | (l/valign (repeat h (hfill w c))))) 158 | 159 | (defn arrow-left 160 | "Constructs a left arrow of given length. 161 | 162 | For example, (arrow-left 4) is '◀───'." 163 | ([n] 164 | (arrow-left n \─ "◀")) 165 | ([n body-char] 166 | (arrow-left n body-char "◀")) 167 | ([n body-char head-char] 168 | (hline n body-char head-char body-char))) 169 | 170 | (defn arrow-right 171 | "Constructs a right arrow of given length. 172 | 173 | For example, (arrow-right 4) is '───▶︎'." 174 | ([n] 175 | (arrow-right n "─" "▶")) 176 | ([n body-char] 177 | (arrow-right n body-char "▶")) 178 | ([n body-char head-char] 179 | (hline n body-char body-char head-char))) 180 | 181 | (defn arrow-left-right 182 | "Constructs a left-right arrow of given length. 183 | 184 | For example, (arrow-left-right 4) is '◀──▶︎'." 185 | ([n] 186 | (arrow-left-right n \─ "◀" "▶︎")) 187 | ([n body-char] 188 | (arrow-left-right n body-char "◀" "▶︎")) 189 | ([n body-char left-head-char right-head-char] 190 | (hline n body-char left-head-char right-head-char))) 191 | 192 | (defn arrow-up 193 | "Constructs an up arrow of given length. 194 | 195 | For example, (arrow-up 4) is 196 | ▲ 197 | │ 198 | │ 199 | │ 200 | " 201 | ([n] 202 | (arrow-up n \│ "▲")) 203 | ([n body-char] 204 | (arrow-up n body-char "▲")) 205 | ([n body-char head-char] 206 | (vline n body-char head-char body-char))) 207 | 208 | (defn arrow-down 209 | "Constructs a down arrow of given length. 210 | 211 | For example, (arrow-down 4) is 212 | │ 213 | │ 214 | │ 215 | ▼ 216 | " 217 | ([n] 218 | (arrow-down n \│ "▼")) 219 | ([n body-char] 220 | (arrow-down n body-char "▼")) 221 | ([n body-char head-char] 222 | (vline n body-char body-char head-char))) 223 | 224 | (defn arrow-up-down 225 | "Constructs a up-down arrow of given length. 226 | 227 | For example, (arrow-up-down 4) is 228 | ▲ 229 | │ 230 | │ 231 | ▼ 232 | " 233 | ([n] 234 | (arrow-up-down n \│ "▲" "▼")) 235 | ([n body-char] 236 | (arrow-up-down n body-char "▲" "▼")) 237 | ([n body-char up-head-char down-head-char] 238 | (vline n body-char up-head-char down-head-char))) 239 | 240 | (defn arrow-se 241 | "Constructs an arrow pointing south-east of given length. 242 | 243 | For example, (arrow-se 4) is -- ignore the double lines. Can't put it here 244 | without escaping it! 245 | \\ 246 | \\ 247 | \\ 248 | * 249 | " 250 | ([n] 251 | (arrow-se n "\\")) 252 | ([n body-char] 253 | (arrow-se n body-char body-char "*")) 254 | ([n body-char start-char end-char] 255 | (apply assoc 256 | (c/empty-grid) 257 | (interleave (for [x (range n)] 258 | [x x]) 259 | (concat (list start-char) 260 | (repeat (- n 2) body-char) 261 | (list end-char)))))) 262 | 263 | (defn arrow-sw 264 | "Constructs an arrow pointing south-west of given length. 265 | 266 | For example, (arrow-sw 4) is 267 | / 268 | / 269 | / 270 | * 271 | " 272 | ([n] 273 | (arrow-sw n "/")) 274 | ([n body-char] 275 | (arrow-sw n body-char body-char "*")) 276 | ([n body-char start-char end-char] 277 | (c/transform 278 | (arrow-se n body-char start-char end-char) 279 | (c/tf-hflip)))) 280 | 281 | (defn arrow-ne 282 | "Constructs an arrow pointing north-east of given length. 283 | 284 | For example, (arrow-ne 4) is 285 | * 286 | / 287 | / 288 | / 289 | " 290 | ([n] 291 | (arrow-ne n "/")) 292 | ([n body-char] 293 | (arrow-ne n body-char body-char "*")) 294 | ([n body-char start-char end-char] 295 | (c/transform 296 | (arrow-se n body-char start-char end-char) 297 | (c/tf-vflip)))) 298 | 299 | (defn arrow-nw 300 | "Constructs an arrow pointing north-west of given length. 301 | 302 | For example, (arrow-nw 4) is -- ignore the double lines. Can't put it here 303 | without escaping it! 304 | * 305 | \\ 306 | \\ 307 | \\ 308 | " 309 | ([n] 310 | (arrow-nw n "\\")) 311 | ([n body-char] 312 | (arrow-nw n body-char body-char "*")) 313 | ([n body-char start-char end-char] 314 | (c/transform 315 | (arrow-ne n body-char start-char end-char) 316 | (c/tf-hflip)))) 317 | 318 | (def default-box-options {:left-padding 0 319 | :right-padding 0 320 | :top-padding 0 321 | :bottom-padding 0 322 | :left-border-char \| 323 | :right-border-char \| 324 | :top-border-char \- 325 | :bottom-border-char \- 326 | :top-left-corner-char \+ 327 | :top-right-corner-char \+ 328 | :bottom-left-corner-char \+ 329 | :bottom-right-corner-char \+}) 330 | 331 | (defn box 332 | "Constructs a grid wrapping given grid (or a string-convertible value) into a box. 333 | 334 | For example, (box (text \"HELLO\")) is 335 | +-----+ 336 | |HELLO| 337 | +-----+ 338 | " 339 | [g & {:keys [left-padding 340 | right-padding 341 | top-padding 342 | bottom-padding 343 | left-border-char 344 | right-border-char 345 | top-border-char 346 | bottom-border-char 347 | top-left-corner-char 348 | top-right-corner-char 349 | bottom-left-corner-char 350 | bottom-right-corner-char 351 | fill-escape-codes] 352 | :or {left-padding 0 353 | right-padding 0 354 | top-padding 0 355 | bottom-padding 0 356 | left-border-char \| 357 | right-border-char \| 358 | top-border-char \- 359 | bottom-border-char \- 360 | top-left-corner-char \+ 361 | top-right-corner-char \+ 362 | bottom-left-corner-char \+ 363 | bottom-right-corner-char \+} 364 | :as provided-opts}] 365 | (let [opts (merge default-box-options provided-opts)] 366 | (if (c/grid? g) 367 | (let [origin-x (:min-x g) 368 | origin-y (:min-y g) 369 | move-to-origin (fn [o] 370 | (if (and (zero? origin-x) (zero? origin-y)) 371 | o 372 | (c/transform o (c/tf-translate origin-x origin-y)))) 373 | width (+ (:width g) left-padding right-padding) 374 | height (+ (:height g) top-padding bottom-padding 2) 375 | left-line (vline height left-border-char top-left-corner-char bottom-left-corner-char) 376 | right-line (vline height 377 | right-border-char 378 | top-right-corner-char 379 | bottom-right-corner-char) 380 | top-line (hline width top-border-char) 381 | bottom-line (hline width bottom-border-char) 382 | content (l/valign [(vfill top-padding \space) 383 | (l/halign [(hfill left-padding) g (hfill right-padding)]) 384 | (vfill bottom-padding \space)]) 385 | filled-content (if (nil? fill-escape-codes) 386 | content 387 | (let [filled-box (fill (+ (:width g) left-padding right-padding) 388 | (+ (:height g) top-padding bottom-padding) 389 | \space)] 390 | (apply decorate (assoc filled-box [0 0] content) 391 | fill-escape-codes)))] 392 | (l/halign 393 | [(move-to-origin left-line) 394 | (l/valign [(move-to-origin top-line) 395 | filled-content 396 | (move-to-origin bottom-line)]) 397 | (move-to-origin right-line)])) 398 | (apply box (text (str g)) (apply concat opts))))) 399 | 400 | (defn box0 401 | "Constructs a grid wrapping given grid into a border-less box. 402 | 403 | For example, (box0 (text \"HELLO\")) is 404 | 405 | HELLO 406 | 407 | " 408 | [g & {:keys [left-padding 409 | right-padding 410 | top-padding 411 | bottom-padding 412 | fill-escape-codes] 413 | :or {left-padding 0 414 | right-padding 0 415 | top-padding 0 416 | bottom-padding 0}}] 417 | (box g 418 | :left-padding left-padding 419 | :right-padding right-padding 420 | :top-padding top-padding 421 | :bottom-padding bottom-padding 422 | :fill-escape-codes fill-escape-codes 423 | :left-border-char "" 424 | :right-border-char "" 425 | :top-border-char "" 426 | :bottom-border-char "" 427 | :top-left-corner-char "" 428 | :top-right-corner-char "" 429 | :bottom-left-corner-char "" 430 | :bottom-right-corner-char "")) 431 | 432 | (defn box1 433 | "Constructs a grid wrapping given grid into a box. 434 | 435 | Similar to box, but uses different border. 436 | 437 | For example, (box1 (text \"HELLO\")) is 438 | ┌─────┐ 439 | │HELLO│ 440 | └─────┘ 441 | " 442 | [g & {:keys [left-padding 443 | right-padding 444 | top-padding 445 | bottom-padding 446 | fill-escape-codes] 447 | :or {left-padding 0 448 | right-padding 0 449 | top-padding 0 450 | bottom-padding 0}}] 451 | (box g 452 | :left-padding left-padding 453 | :right-padding right-padding 454 | :top-padding top-padding 455 | :bottom-padding bottom-padding 456 | :fill-escape-codes fill-escape-codes 457 | :left-border-char "│" 458 | :right-border-char "│" 459 | :top-border-char "─" 460 | :bottom-border-char "─" 461 | :top-left-corner-char "┌" 462 | :top-right-corner-char "┐" 463 | :bottom-left-corner-char "└" 464 | :bottom-right-corner-char "┘")) 465 | 466 | (defn box2 467 | "Constructs a grid wrapping given grid into a box. 468 | 469 | Similar to box, but uses border with rounded corners. 470 | 471 | For example, (box2 (text \"HELLO\")) is 472 | ╭─────╮ 473 | │HELLO│ 474 | ╰─────╯ 475 | " 476 | [g & {:keys [left-padding 477 | right-padding 478 | top-padding 479 | bottom-padding 480 | fill-escape-codes] 481 | :or {left-padding 0 482 | right-padding 0 483 | top-padding 0 484 | bottom-padding 0}}] 485 | (box g 486 | :left-padding left-padding 487 | :right-padding right-padding 488 | :top-padding top-padding 489 | :bottom-padding bottom-padding 490 | :fill-escape-codes fill-escape-codes 491 | :left-border-char "│" 492 | :right-border-char "│" 493 | :top-border-char "─" 494 | :bottom-border-char "─" 495 | :top-left-corner-char "╭" 496 | :top-right-corner-char "╮" 497 | :bottom-left-corner-char "╰" 498 | :bottom-right-corner-char "╯")) 499 | 500 | (defn box3 501 | "Constructs a grid wrapping given grid into a box. 502 | 503 | Similar to box, but uses different border. 504 | 505 | For example, (box3 (text \"HELLO\")) is 506 | ╒═════╕ 507 | │HELLO│ 508 | ╘═════╛ 509 | " 510 | [g & {:keys [left-padding 511 | right-padding 512 | top-padding 513 | bottom-padding 514 | fill-escape-codes] 515 | :or {left-padding 0 516 | right-padding 0 517 | top-padding 0 518 | bottom-padding 0}}] 519 | (box g 520 | :left-padding left-padding 521 | :right-padding right-padding 522 | :top-padding top-padding 523 | :bottom-padding bottom-padding 524 | :fill-escape-codes fill-escape-codes 525 | :left-border-char ":" 526 | :right-border-char ":" 527 | :top-border-char "." 528 | :bottom-border-char "." 529 | :top-left-corner-char "." 530 | :top-right-corner-char "." 531 | :bottom-left-corner-char "." 532 | :bottom-right-corner-char ".")) 533 | 534 | (defn box4 535 | "Constructs a grid wrapping given grid into a box. 536 | 537 | Similar to box, but uses different border. 538 | 539 | For example, (box4 (text \"HELLO\")) is 540 | ╒═════╕ 541 | │HELLO│ 542 | ╘═════╛ 543 | " 544 | [g & {:keys [left-padding 545 | right-padding 546 | top-padding 547 | bottom-padding 548 | fill-escape-codes] 549 | :or {left-padding 0 550 | right-padding 0 551 | top-padding 0 552 | bottom-padding 0}}] 553 | (box g 554 | :left-padding left-padding 555 | :right-padding right-padding 556 | :top-padding top-padding 557 | :bottom-padding bottom-padding 558 | :fill-escape-codes fill-escape-codes 559 | :left-border-char "│" 560 | :right-border-char "│" 561 | :top-border-char "═" 562 | :bottom-border-char "═" 563 | :top-left-corner-char "╒" 564 | :top-right-corner-char "╕" 565 | :bottom-left-corner-char "╘" 566 | :bottom-right-corner-char "╛")) 567 | 568 | (defn box5 569 | "Constructs a grid wrapping given grid into a box. 570 | 571 | Similar to box, but uses '*' for all border characters. 572 | 573 | For example, (box5 (text \"HELLO\")) is 574 | ******* 575 | *HELLO* 576 | ******* 577 | " 578 | [g & {:keys [left-padding 579 | right-padding 580 | top-padding 581 | bottom-padding 582 | fill-escape-codes] 583 | :or {left-padding 0 584 | right-padding 0 585 | top-padding 0 586 | bottom-padding 0}}] 587 | (box g 588 | :left-padding left-padding 589 | :right-padding right-padding 590 | :top-padding top-padding 591 | :bottom-padding bottom-padding 592 | :fill-escape-codes fill-escape-codes 593 | :left-border-char "*" 594 | :right-border-char "*" 595 | :top-border-char "*" 596 | :bottom-border-char "*" 597 | :top-left-corner-char "*" 598 | :top-right-corner-char "*" 599 | :bottom-left-corner-char "*" 600 | :bottom-right-corner-char "*")) 601 | 602 | (defn table 603 | "Constructs a table. 604 | 605 | Produces similar table as clojure.pprint/print-table. 606 | 607 | Adapted from source-code of clojure.pprint/print-table. 608 | 609 | For example, (table [:a :b] [{:a 1 :b 2} {:a 3 :b 4}]) is 610 | +----+----+ 611 | | :a | :b | 612 | +----+----+ 613 | | 1 | 2 | 614 | | 3 | 4 | 615 | +----+----+ 616 | 617 | By default, columns are right aligned. For left pass in ':align :left', for 618 | center pass ':align :center'. 619 | 620 | row-decorations keyword-argument can be provided. It is a sequence 621 | of ansi-escape codes. If header? is true, the header will be decorated with 622 | the first ansi-escape code in row-decorations. And the other rows will be decorated 623 | with the rest -- if there aren't enough, we'll just cycle over the 624 | given ansi-escape-codes. For example, try 625 | (table [:a :b] [{:a 1 :b 2} {:a 3 :b 4}] 626 | :align :left 627 | :row-decorations [ESCAPE-CODE-BACKGROUND-BRIGHT-GREEN 628 | ESCAPE-CODE-BACKGROUND-BRIGHT-MAGENTA 629 | ESCAPE-CODE-BACKGROUND-BLUE]) 630 | " 631 | [ks rows & {:keys [nsew-char 632 | nse-char 633 | nsw-char 634 | ewn-char 635 | ews-char 636 | ns-char 637 | ew-char 638 | nw-char 639 | ne-char 640 | se-char 641 | sw-char 642 | align 643 | header? 644 | row-decorations] 645 | :or {nsew-char \+ 646 | nse-char \+ 647 | nsw-char \+ 648 | ewn-char \+ 649 | ews-char \+ 650 | ns-char \| 651 | ew-char \- 652 | nw-char \+ 653 | ne-char \+ 654 | se-char \+ 655 | sw-char \+ 656 | header? true 657 | align :right}}] 658 | (let [widths (map 659 | (fn [k] 660 | (apply max 661 | (count (str k)) 662 | (map #(c/width (get % k)) rows))) 663 | ks) 664 | spacers (map #(apply str (repeat % ew-char)) widths) 665 | fmts (map (fn [w] 666 | (condp = align 667 | :left (str "~" w ":A") 668 | :right (str "~" w ":@A") 669 | :center (str "~" w ":@<~A~>"))) 670 | widths) 671 | fmt-row (fn [leader divider trailer row] 672 | (let [vs (map #(get row %) ks) 673 | heights (map c/height vs) 674 | max-height (apply max heights) 675 | ->cols (fn [i] 676 | (for [v vs] 677 | (nth (s/split-lines (str v)) i "")))] 678 | (s/join 679 | \newline 680 | (for [i (range max-height) 681 | :let [cols (->cols i)]] 682 | (as-> (interpose 683 | divider 684 | (for [[col fmt] (map vector cols fmts)] 685 | (clojure.pprint/cl-format nil fmt (str col)))) $ 686 | (apply str $) 687 | (str leader $ trailer)))))) 688 | top-border (text (fmt-row 689 | (str se-char ew-char) 690 | (str ew-char ews-char ew-char) 691 | (str ew-char sw-char) 692 | (zipmap ks spacers))) 693 | header-divider (text (fmt-row 694 | (str nse-char ew-char) 695 | (str ew-char nsew-char ew-char) 696 | (str ew-char nsw-char) 697 | (zipmap ks spacers))) 698 | bottom-border (text (fmt-row 699 | (str ne-char ew-char) 700 | (str ew-char ewn-char ew-char) 701 | (str ew-char nw-char) 702 | (zipmap ks spacers))) 703 | color-fn (fn [i] 704 | (cond 705 | (nil? row-decorations) nil 706 | header? (nth (cons (first row-decorations) 707 | (cycle (rest row-decorations))) i nil) 708 | :else (nth (cycle row-decorations) i nil))) 709 | reset (when (seq row-decorations) 710 | ecodes/ESCAPE-CODE-RESET)] 711 | (l/valign [top-border 712 | (when header? 713 | (text (fmt-row 714 | (str ns-char (color-fn 0) \space) 715 | (str \space reset ns-char (color-fn 0) \space) 716 | (str \space reset ns-char) 717 | (zipmap ks ks)))) 718 | (when header? header-divider) 719 | (l/valign (map (fn [row i] 720 | (text (fmt-row (str ns-char (color-fn (inc i)) \space) 721 | (str \space reset ns-char (color-fn (inc i)) \space) 722 | (str \space reset ns-char) 723 | row))) 724 | rows 725 | (range))) 726 | bottom-border]))) 727 | 728 | (defn table0 729 | "Constructs a border-less table. 730 | 731 | For example, (table0 [:a :b] [{:a 1 :b 2} {:a 3 :b 4}]) is 732 | 733 | :a :b 734 | 735 | 1 2 736 | 3 4 737 | 738 | Rows can be decorated with row-decorations argument, which is a sequence of ansi-escape-codes. 739 | Please see docstring for table." 740 | ([ks rows] 741 | (table0 ks rows true)) 742 | ([ks rows header?] 743 | (table0 ks rows header? :right)) 744 | ([ks rows header? align] 745 | (table0 ks rows header? align nil)) 746 | ([ks rows header? align row-decorations] 747 | (table ks rows 748 | :nsew-char "" 749 | :nse-char "" 750 | :nsw-char "" 751 | :ewn-char "" 752 | :ews-char "" 753 | :ns-char "" 754 | :ew-char "" 755 | :se-char "" 756 | :sw-char "" 757 | :ne-char "" 758 | :nw-char "" 759 | :align align 760 | :header? header? 761 | :row-decorations row-decorations))) 762 | 763 | (defn table1 764 | "Constructs a table. 765 | 766 | Similar to table but uses different border. 767 | 768 | For example, (table1 [:a :b] [{:a 1 :b 2} {:a 3 :b 4}]) is 769 | ┌────┬────┐ 770 | │ :a │ :b │ 771 | ├────┼────┤ 772 | │ 1 │ 2 │ 773 | │ 3 │ 4 │ 774 | └────┴────┘ 775 | 776 | Rows can be decorated with row-decorations argument, which is a sequence of ansi-escape-codes. 777 | Please see docstring for table." 778 | ([ks rows] 779 | (table1 ks rows true)) 780 | ([ks rows header?] 781 | (table1 ks rows header? :right)) 782 | ([ks rows header? align] 783 | (table1 ks rows header? align nil)) 784 | ([ks rows header? align row-decorations] 785 | (table ks rows 786 | :nsew-char "┼" 787 | :nse-char "├" 788 | :nsw-char "┤" 789 | :ewn-char "┴" 790 | :ews-char "┬" 791 | :ns-char "│" 792 | :ew-char "─" 793 | :se-char "┌" 794 | :sw-char "┐" 795 | :ne-char "└" 796 | :nw-char "┘" 797 | :header? header? 798 | :align align 799 | :row-decorations row-decorations))) 800 | 801 | (defn table2 802 | "Constructs a table. 803 | 804 | Similar to table but uses different border (rounded corners). 805 | 806 | For example, (table2 [:a :b] [{:a 1 :b 2} {:a 3 :b 4}]) is 807 | ╭────┬────╮ 808 | │ :a │ :b │ 809 | ├────┼────┤ 810 | │ 1 │ 2 │ 811 | │ 3 │ 4 │ 812 | ╰────┴────╯ 813 | 814 | Rows can be decorated with row-decorations argument, which is a sequence of ansi-escape-codes. 815 | Please see docstring for table." 816 | ([ks rows] 817 | (table2 ks rows true)) 818 | ([ks rows header?] 819 | (table2 ks rows header? :right)) 820 | ([ks rows header? align] 821 | (table2 ks rows header? align nil)) 822 | ([ks rows header? align row-decorations] 823 | (table ks rows 824 | :nsew-char "┼" 825 | :nse-char "├" 826 | :nsw-char "┤" 827 | :ewn-char "┴" 828 | :ews-char "┬" 829 | :ns-char "│" 830 | :ew-char "─" 831 | :se-char "╭" 832 | :sw-char "╮" 833 | :ne-char "╰" 834 | :nw-char "╯" 835 | :header? header? 836 | :align align 837 | :row-decorations row-decorations))) 838 | 839 | (defn table3 840 | "Constructs a table. 841 | 842 | Similar to table but uses dotted border. 843 | 844 | For example, (table3 [:a :b] [{:a 1 :b 2} {:a 3 :b 4}]) is 845 | ........... 846 | : :a : :b : 847 | ..........: 848 | : 1 : 2 : 849 | : 3 : 4 : 850 | :....:....: 851 | 852 | Rows can be decorated with row-decorations argument, which is a sequence of ansi-escape-codes. 853 | Please see docstring for table." 854 | ([ks rows] 855 | (table3 ks rows true)) 856 | ([ks rows header?] 857 | (table3 ks rows header? :right)) 858 | ([ks rows header? align] 859 | (table3 ks rows header? align nil)) 860 | ([ks rows header? align row-decorations] 861 | (table ks rows 862 | :nsew-char "." 863 | :nse-char "." 864 | :nsw-char ":" 865 | :ewn-char ":" 866 | :ews-char "." 867 | :ns-char ":" 868 | :ew-char "." 869 | :se-char "." 870 | :sw-char "." 871 | :ne-char ":" 872 | :nw-char ":" 873 | :header? header? 874 | :align align 875 | :row-decorations row-decorations))) 876 | 877 | (defn table* 878 | "Constructs a n-column, border-less, header-less table with given grids. 879 | 880 | Just a convenience wrapper around table0. For more customization, use 881 | table0 or table functions. 882 | " 883 | ([grids n] 884 | (let [ks (range n) 885 | rows (for [r (partition n n nil grids)] 886 | (zipmap ks r))] 887 | (table0 ks rows false)))) 888 | 889 | (defn matrix 890 | "Constructs a matrix. 891 | 892 | Similar to table, but changes the border to make it look more like a matrix. 893 | 894 | For example, (matrix [:a :b] [{:a 1 :b 2} {:a 3 :b 4}]) is 895 | ╭ ╮ 896 | │ :a :b │ 897 | │ │ 898 | │ 1 2 │ 899 | │ 3 4 │ 900 | ╰ ╯ 901 | " 902 | ([ks rows] 903 | (matrix ks rows true)) 904 | ([ks rows header?] 905 | (l/halign [(vline (+ (if header? 4 2) 906 | (count rows)) 907 | "│" "╭" "╰") 908 | (table0 ks rows header?) 909 | (vline (+ (if header? 4 2) 910 | (count rows)) 911 | "│" "╮" "╯")]))) 912 | 913 | (defn tree 914 | "Constructs a tree representation of a sequence. 915 | 916 | An element of the sequence can be another sequence or a grid or 917 | anything that can be converted to a string. 918 | 919 | For example, (tree [1 2 [3 [4 5] [6 7] 8]]) is 920 | ┌───┐ 921 | │ 1 │ 922 | └───┘ 923 | │ 924 | ┌───┐ 925 | │ 2 │ 926 | └───┘ 927 | │ 928 | ┌───┐ ┌───┐ ┌───┐ ┌───┐ 929 | │ 3 │ │ 4 │ │ 6 │ │ 8 │ 930 | └───┘ └───┘ └───┘ └───┘ 931 | │ │ 932 | ┌───┐ ┌───┐ 933 | │ 5 │ │ 7 │ 934 | └───┘ └───┘ 935 | " 936 | ([node] 937 | (tree node 2 0)) 938 | ([node text-wrapper-fn] 939 | (tree node 2 0 text-wrapper-fn)) 940 | ([node x-padding y-padding] 941 | (tree node x-padding y-padding box1)) 942 | ([node x-padding y-padding text-wrapper-fn] 943 | (tree node x-padding y-padding text-wrapper-fn "│")) 944 | ([node x-padding y-padding text-wrapper-fn branch-marker] 945 | (cond 946 | (sequential? node) (let [args (list x-padding 947 | y-padding 948 | text-wrapper-fn 949 | branch-marker) 950 | children (for [n node] 951 | (if (sequential? n) 952 | (l/halign (map #(apply tree % args) n) x-padding) 953 | (apply tree n args)))] 954 | (as-> children $ 955 | (interpose (text (str branch-marker)) $) 956 | (l/valign $ y-padding))) 957 | (c/grid? node) node 958 | :else (text-wrapper-fn (text (str node)) :left-padding 1 :right-padding 1)))) 959 | 960 | (defn chart-xy 961 | "Constructs a xy-chart (scatter plot). 962 | 963 | For example, (chart-xy (range) [0 1 2 1 0 1 2 1 0] :max-height 2 :max-width 10) is 964 | y 965 | ▲ 966 | | 967 | | * * 968 | |* * * * 969 | *----*----*-▶ x 970 | " 971 | [xs ys & {:keys [point-symbol 972 | draw-axis 973 | x-label 974 | y-label 975 | max-width 976 | max-height] 977 | :or {point-symbol \* 978 | draw-axis true 979 | x-label "x" 980 | y-label "y" 981 | max-width 40 982 | max-height 10}}] 983 | (let [n (count (map vector xs ys)) 984 | xs (take n xs) 985 | ys (take n ys) 986 | [min-x max-x] (apply (juxt min max) xs) 987 | [min-y max-y] (apply (juxt min max) ys) 988 | max-delta-x (- max-x min-x) 989 | max-delta-y (- max-y min-y) 990 | unit-x (/ max-width max-delta-x) 991 | unit-y (/ max-height max-delta-y) 992 | scaled-xs (map (fn [x] (c/round (* x unit-x))) xs) 993 | scaled-ys (map (fn [y] (c/round (* y unit-y))) ys) 994 | ks (map vector scaled-xs scaled-ys) 995 | p (as-> (c/empty-grid) $ 996 | (apply assoc $ (interleave ks (repeat point-symbol)))) 997 | p (if draw-axis 998 | (let [x-axis (l/=== 999 | 1 1000 | (arrow-right (+ (:width p) 2) \-) 1001 | (text x-label)) 1002 | y-axis (l/|| 1003 | 0 1004 | (vline (+ (:height p) 2) \| \| "▲") 1005 | (text y-label)) 1006 | with-x-axis (assoc p [0 0] x-axis) 1007 | with-xy-axis (assoc with-x-axis [0 0] y-axis)] 1008 | (c/add with-xy-axis p)) p)] 1009 | (c/transform p (c/tf-vflip)))) 1010 | 1011 | (defn chart-bar 1012 | "Constructs a bar chart. 1013 | 1014 | For example, (chart-bar [10 20 30 40]) is 1015 | ■■■■■■■■■■■■■ 10 1016 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■ 20 1017 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 30 1018 | ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 40 1019 | " 1020 | [ns & {:keys [labels 1021 | max-length 1022 | bar-symbol 1023 | horizontal] 1024 | :or {labels ns 1025 | horizontal true}}] 1026 | {:pre [(or (nil? max-length) 1027 | (pos? max-length))]} 1028 | (let [min-n (apply min ns) 1029 | max-n (apply max ns) 1030 | max-length (or max-length (if horizontal 40 10)) 1031 | max-delta (- max-n min-n) 1032 | unit (/ max-length (if (zero? max-delta) max-length max-delta)) 1033 | scaled-ns (map (fn [v] (c/round (* v unit))) ns) 1034 | bar-symbol (cond 1035 | bar-symbol bar-symbol 1036 | horizontal "■" 1037 | :else "█") 1038 | text-labels (if horizontal 1039 | (take (count ns) (map #(text (str %)) labels)) 1040 | (take (count ns) (map (fn [l] 1041 | (-> (str l) 1042 | text 1043 | (c/transform (c/tf-transpose)))) 1044 | labels)))] 1045 | (if horizontal 1046 | (l/valign (for [[n text-label] (map vector scaled-ns text-labels)] 1047 | (l/=== 1 (hline n bar-symbol) text-label))) 1048 | (let [chart (l/halign (for [n scaled-ns] 1049 | (or (vline n bar-symbol) (text ""))) 2) 1050 | flipped-chart (c/transform chart (c/tf-vflip))] 1051 | (assoc flipped-chart 1052 | [0 0] 1053 | (l/halign text-labels 2)))))) 1054 | -------------------------------------------------------------------------------- /test-bb.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require [pp-grid.api :as g] 3 | [pp-grid.examples :as e])) 4 | 5 | (defn p [x] 6 | (println (str x))) 7 | 8 | (do 9 | (p (e/make-abcd)) 10 | (p (e/make-boxed-abcd)) 11 | (p (e/make-haligned-boxes)) 12 | (p (e/make-tables)) 13 | (p (e/make-nested-table)) 14 | (p (e/make-decorated-text "Hello bb")) 15 | (p (e/make-colored-table)) 16 | (p (e/make-colored-boxes)) 17 | (p (e/make-tree)) 18 | (p (e/make-chart-xy)) 19 | (p (e/make-chart-bar)) 20 | (p (e/make-transformations)) 21 | (p (e/make-diagram))) 22 | -------------------------------------------------------------------------------- /test/pp_grid/api_test.clj: -------------------------------------------------------------------------------- 1 | (ns pp-grid.api-test 2 | (:require [clojure.test :refer [deftest is testing]] 3 | 4 | [pp-grid.api :as g] 5 | [pp-grid.examples :as e])) 6 | 7 | (deftest test-empty-grid 8 | (testing "grid constructors" 9 | (let [grid (g/empty-grid)] 10 | (is (= 2 (:dimension grid))) 11 | (is (= 0 (:width grid))) 12 | (is (= 0 (:height grid))) 13 | (is (nil? (:mins grid))) 14 | (is (nil? (:maxs grid)))) 15 | (let [grid (g/empty-grid 1)] 16 | (is (= 1 (:dimension grid))) 17 | (is (= 0 (:width grid))) 18 | (is (= 0 (:height grid))) 19 | (is (nil? (:mins grid))) 20 | (is (nil? (:maxs grid)))))) 21 | 22 | (deftest test-width 23 | (testing "string-widths" 24 | (is (= 0 (g/width ""))) 25 | (is (= 0 (g/width "\n123"))) 26 | (is (= 1 (g/width "1"))) 27 | (is (= 1 (g/width "1\n1234"))) 28 | (is (= 4 (g/width "abcd\nefgh")))) 29 | (testing "string-convertible-widths" 30 | (is (= 1 (g/width 1))) 31 | (is (= 4 (g/width 1234))) 32 | (is (= 7 (g/width 1234.26))) 33 | (is (= 2 (g/width :a))) 34 | (is (= 5 (g/width :abcd)))) 35 | (testing "grid-widths" 36 | (is (= 0 (g/width (g/empty-grid)))) 37 | (is (= 5 (g/width (g/text "HELLO")))))) 38 | 39 | (deftest test-height 40 | (testing "string-heights" 41 | (is (= 1 (g/height ""))) 42 | (is (= 2 (g/height "\n123"))) 43 | (is (= 1 (g/height "1"))) 44 | (is (= 2 (g/height "1\n1234"))) 45 | (is (= 2 (g/height "abcd\nefgh")))) 46 | (testing "string-convertible-heights" 47 | (is (= 1 (g/height 1))) 48 | (is (= 1 (g/height 1234))) 49 | (is (= 1 (g/height 1234.26))) 50 | (is (= 1 (g/height :a))) 51 | (is (= 1 (g/height :abcd)))) 52 | (testing "grid-heights" 53 | (is (= 0 (g/height (g/empty-grid)))) 54 | (is (= 1 (g/height (g/text "HELLO")))) 55 | (is (= 4 (g/height (g/text "HELLO\nWorld\nof\ntesting")))))) 56 | 57 | (deftest test-grid-2d 58 | (testing "2d grid" 59 | (let [grid (-> (g/empty-grid 2) 60 | (assoc [0 0] \H 61 | [1 0] \E 62 | [2 0] \L 63 | [3 0] \L 64 | [4 0] \O))] 65 | (is (= 5 (:width grid))) 66 | (is (= 1 (:height grid))) 67 | (is (= 0 (:min-x grid))) 68 | (is (= 4 (:max-x grid))) 69 | (is (= 0 (:min-y grid))) 70 | (is (= 0 (:max-y grid))) 71 | (is (= "HELLO" (g/render grid)))))) 72 | 73 | (deftest test-text 74 | (testing "text grids" 75 | (is (= "hello" (g/text "hello"))) 76 | (is (= " hello " (g/text "hello" 2 2))) 77 | (is (= "***hello**" (g/text "hello" 3 2 \*))) 78 | (is (= "1" (g/text 1))) 79 | (is (= ":a" (g/text :a))) 80 | (is (= "(+ 1 2 3)" (g/text '(+ 1 2 3)))))) 81 | 82 | (deftest test-examples 83 | (testing "grid illustration" 84 | (let [grid (e/make-grid-illustration 4 4)] 85 | (is (= (:dimension grid) 2)) 86 | (is (= (:width grid) 40)) 87 | (is (= (:height grid) 18)) 88 | (is (g/grid? grid)))) 89 | (testing "hello world" 90 | (let [grid (e/make-hello-world)] 91 | (is (= (:dimension grid) 2)) 92 | (is (= (:width grid) 11)) 93 | (is (= (:height grid) 1)) 94 | (is (g/grid? grid)))) 95 | (testing "abcd" 96 | (let [grid (e/make-abcd)] 97 | (is (= (:dimension grid) 2)) 98 | (is (= (:width grid) 11)) 99 | (is (= (:height grid) 11)) 100 | (is (g/grid? grid)))) 101 | (testing "boxed abcd" 102 | (let [grid (e/make-boxed-abcd)] 103 | (is (= (:dimension grid) 2)) 104 | (is (= (:width grid) 15)) 105 | (is (= (:height grid) 13)) 106 | (is (g/grid? grid)))) 107 | (testing "haligned boxes" 108 | (let [grid (e/make-haligned-boxes)] 109 | (is (= (:dimension grid) 2)) 110 | (is (= (:width grid) 35)) 111 | (is (= (:height grid) 7)) 112 | (is (g/grid? grid)))) 113 | (testing "tables" 114 | (let [grid (e/make-tables)] 115 | (is (= (:dimension grid) 2)) 116 | (is (= (:width grid) 50)) 117 | (is (= (:height grid) 41)) 118 | (is (g/grid? grid)))) 119 | (testing "nested-table" 120 | (let [grid (e/make-nested-table)] 121 | (is (= (:dimension grid) 2)) 122 | (is (= (:width grid) 74)) 123 | (is (= (:height grid) 17)) 124 | (is (g/grid? grid)))) 125 | (testing "colored-table" 126 | (let [grid (e/make-colored-table)] 127 | (is (= (:dimension grid) 2)) 128 | (is (= (:width grid) 37)) 129 | (is (= (:height grid) 8)) 130 | (is (g/grid? grid)))) 131 | (testing "colored-boxes" 132 | (let [grid (e/make-colored-boxes)] 133 | (is (= (:dimension grid) 2)) 134 | (is (= (:width grid) 50)) 135 | (is (= (:height grid) 13)) 136 | (is (g/grid? grid)))) 137 | (testing "decorated text" 138 | (let [grid (e/make-decorated-text "HELLO")] 139 | (is (= (:dimension grid) 2)) 140 | (is (= (:width grid) 5)) 141 | (is (= (:height grid) 1)) 142 | (is (g/grid? grid)))) 143 | (testing "tree" 144 | (let [grid (e/make-tree)] 145 | (is (= (:dimension grid) 2)) 146 | (is (= (:width grid) 22)) 147 | (is (= (:height grid) 19)) 148 | (is (g/grid? grid)))) 149 | (testing "chart-xy" 150 | (let [grid (e/make-chart-xy)] 151 | (is (= (:dimension grid) 2)) 152 | (is (= (:width grid) 45)) 153 | (is (= (:height grid) 7)) 154 | (is (g/grid? grid)))) 155 | (testing "chart-bar" 156 | (let [grid (e/make-chart-bar)] 157 | (is (= (:dimension grid) 2)) 158 | (is (= (:width grid) 46)) 159 | (is (= (:height grid) 5)) 160 | (is (g/grid? grid)))) 161 | (testing "chart-bar-vertical" 162 | (let [grid (e/make-chart-bar-vertical)] 163 | (is (= (:dimension grid) 2)) 164 | (is (= (:width grid) 10)) 165 | (is (= (:height grid) 16)) 166 | (is (g/grid? grid)))) 167 | (testing "transformations" 168 | (let [grid (e/make-transformations)] 169 | (is (= (:dimension grid) 2)) 170 | (is (= (:width grid) 23)) 171 | (is (= (:height grid) 20)) 172 | (is (g/grid? grid)))) 173 | (testing "diagram" 174 | (let [grid (e/make-diagram)] 175 | (is (= (:dimension grid) 2)) 176 | (is (= (:width grid) 82)) 177 | (is (= (:height grid) 12)) 178 | (is (g/grid? grid)))) 179 | (testing "paragraphs" 180 | (let [grid (e/make-paragraphs)] 181 | (is (= (:dimension grid) 2)) 182 | (is (= (:width grid) 80)) 183 | (is (= (:height grid) 15)) 184 | (is (g/grid? grid))))) 185 | --------------------------------------------------------------------------------