hackerbase/ansi.scm

159 lines
4.5 KiB
Scheme

;;
;; ansi.scm
;;
;; ANSI terminal support.
;;
;; ISC License
;;
;; Copyright 2023 Brmlab, z.s.
;; Dominik Pantůček <dominik.pantucek@trustica.cz>
;;
;; Permission to use, copy, modify, and/or distribute this software
;; for any purpose with or without fee is hereby granted, provided
;; that the above copyright notice and this permission notice appear
;; in all copies.
;;
;; THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
;; WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
;; WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
;; AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
;; CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
;; OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
;; NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
;; CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
;;
(declare (unit ansi))
(module
ansi
(
ansi
a:error
a:warning
a:success
a:neutral
a:default
a:muted
a:highlight
ansi-string-length
ansi-paragraph-format
ansi-tests!
)
(import scheme
(chicken base)
(chicken string)
(chicken irregex)
testing
utils)
;; Only basic ANSI colors and bold attribute support.
(define colors
'((#:black . 30)
(#:red . 31)
(#:green . 32)
(#:yellow . 33)
(#:blue . 34)
(#:magenta . 35)
(#:cyan . 36)
(#:white . 37)
(#:default . 0)
(#:bold . 1)))
;; Returns ANSI sequence changing color and/or bold attribute.
(define (ansi . args)
(let ((argsl
(map
(lambda (key-color)
(number->string (cdr key-color)))
(filter
identity
(map (lambda (arg) (assq arg colors)) args)))))
(if (null? argsl)
""
(string-append "\x1b["
(string-intersperse argsl ";")
"m"))))
;; Nice styles to be used everywhere for consistency
(define a:error (ansi #:red #:bold))
(define a:warning (ansi #:default #:yellow))
(define a:success (ansi #:green #:bold))
(define a:neutral (ansi #:default #:white))
(define a:default (ansi #:default))
(define a:muted (ansi #:black #:bold))
(define a:highlight (ansi #:blue #:bold))
;; Returns visual string length in characters skipping any ANSI CSI
;; SGR sequences.
;;
;; Internal states:
;; 0 - regular string
;; 1 - seen escape
;; 2 - CSI started
(define (ansi-string-length str)
(let loop ((lst (irregex-extract (irregex "." 'u) str))
(state 0)
(len 0))
(if (null? lst)
len
(let ((ch (car lst)))
(case state
((0) (if (equal? ch "\x1b")
(loop (cdr lst) 1 len)
(loop (cdr lst) 0 (add1 len))))
((1) (if (equal? ch "[")
(loop (cdr lst) 2 len)
(loop (cdr lst) 0 len)))
((2) (if (equal? ch "m")
(loop (cdr lst) 0 len)
(loop (cdr lst) 2 len))))))))
;; Removes all ANSI CSI SGR sequences from the string.
(define (ansi-remove str)
(irregex-replace/all (irregex "\x1b\\[[0-9;]*[^0-9;]" 'u) str ""))
;; Formats string as paragraph of maximum given width while removing
;; all ANSI CSI SGR from it.
(define (ansi-paragraph-format str width)
(let loop ((words (string-split
(ansi-remove str)))
(res '("")))
(if (null? words)
(string-intersperse (reverse res) "\n")
(let* ((word (car words))
(wlen (ansi-string-length word))
(llen (ansi-string-length (car res))))
(loop (cdr words)
(if (> (+ llen wlen 1) width)
(cons word res)
(cons (string-append (car res)
(if (eq? (string-length (car res)) 0)
""
" ")
word)
(cdr res))))))))
;; Performs ANSI module self-tests.
(define (ansi-tests!)
(run-tests
ansi
(test-equal? ansi (ansi #:red) "\x1b[31m")
(test-equal? ansi (ansi #:nonsense) "")
(test-equal? ansi (ansi #:default) "\x1b[0m")
(test-eq? ansi-string-length (ansi-string-length "test") 4)
(test-eq? ansi-string-length (ansi-string-length "\x1b[1mtest") 4)
(test-eq? ansi-string-length (ansi-string-length "\x1b[30mtest\x1b[0m") 4)
(test-eq? ansi-string-length (ansi-string-length "\x1b[30mščřž\x1b[0m") 4)
(test-equal? ansi-remove (ansi-remove "\x1b[1mtest") "test")
(test-equal? ansi-remove (ansi-remove "\x1b[30mščřž\x1b[0m") "ščřž")
(test-equal? ansi-paragraph-format
(ansi-paragraph-format "Formats string as paragraph of maximum given width" 80)
"Formats string as paragraph of maximum given width")
(test-equal? ansi-paragraph-format
(ansi-paragraph-format "Formats string as paragraph of maximum given width" 20)
"Formats string as\nparagraph of maximum\ngiven width")
))
)