;; ;; sgr-state.scm ;; ;; ECMA-48 Set Graphics Rendition state management. ;; ;; ISC License ;; ;; Copyright 2023 Dominik Pantůček ;; ;; 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 sgr-state)) (module sgr-state ( sgr-color-valid? sgr-truecolor? make-sgr-truecolor split-sgr-truecolor make-sgr-state empty-sgr-state sgr-state? sgr-state-intensity set-sgr-state-intensity sgr-state-italic set-sgr-state-italic sgr-state-underline set-sgr-state-underline sgr-state-blink set-sgr-state-blink sgr-state-reverse set-sgr-state-reverse sgr-state-crossed set-sgr-state-crossed sgr-state-foreground sgr-state-background set-sgr-state-foreground set-sgr-state-background sgr-state-change->string parse-csi-sgr-sequence+pos parse-csi-sgr-sequence update-sgr-state-from-list update-sgr-state-from-string sgr-state-tests! ) (import scheme (chicken base) (chicken bitwise) (chicken string) testing racket-kwargs) ;; Checks whether this is indexed color or truecolor representation ;; validating the bit mask as well (define (sgr-color-valid? c) (or (< c 256) (and (>= c #x1000000) (<= c #x1ffffff)))) ;; Returns true if given color is truecolor representation (assumes ;; #x1000000 bit set) (define (sgr-truecolor? c) (> c 255)) ;; Creates valid truecolor representation (define (make-sgr-truecolor r g b) (bitwise-ior #x1000000 (arithmetic-shift (bitwise-and r 255) 16) (arithmetic-shift (bitwise-and g 255) 8) (bitwise-and b 255))) ;; Returns the separated RGB alues from SGR truecolor representation (define (split-sgr-truecolor c) (values (bitwise-and (arithmetic-shift c -16) 255) (bitwise-and (arithmetic-shift c -8) 255) (bitwise-and c 255))) ;; Creates empty SGR state (define (make-sgr-state) 0) ;; Constant representing empty (default) SGR state (define empty-sgr-state 0) ;; Returns #t if this is valid SGR state representation (define (sgr-state? v) (or (and (fixnum? v) (= (bitwise-and v (bitwise-not sgr-state-bit-mask)) 0)) (and (pair? v) (fixnum? (car v)) (= (bitwise-and (car v) (bitwise-not sgr-state-bit-mask)) 0) (pair? (cdr v)) (or (not (cadr v)) (and (fixnum? (cadr v)) (= (bitwise-and (cadr v) (bitwise-not #x1ffffff)) 0))) (or (not (cddr v)) (and (fixnum? (cddr v)) (= (bitwise-and (cddr v) (bitwise-not #x1ffffff)) 0)))))) ;; Basic SGR state is just a number, extended state is a pair of ;; number and pair of fg/bg truecolors. (define (sgr-state-bits s) (if (pair? s) (car s) s)) ;; Sets the bits part of given SGR state, if it is not a pair, just ;; returns the new bits as state value (define (set-sgr-state-bits s b) (if (pair? s) (cons b (cdr s)) b)) ;; Computes positional mask, creates getter and setter for given ;; numeric attribute at given bit-offset (define-syntax define-sgr-num-attribute (syntax-rules () ((_ bit-offset bit-size mask getter setter) (begin (define mask (arithmetic-shift (sub1 (arithmetic-shift 1 bit-size)) bit-offset)) (define (getter s) (let ((v (arithmetic-shift (bitwise-and (sgr-state-bits s) mask) (- bit-offset)))) (if (> v 0) v #f))) (define (setter s vf) (let ((v (case vf ((#f) 0) ((#t) 1) (else vf))) (b (sgr-state-bits s))) (set-sgr-state-bits s (bitwise-ior (bitwise-and (arithmetic-shift v bit-offset) mask) (bitwise-and (bitwise-not mask) b))))))))) ;; Wrapper for defining all attributes iteratively, incrementing ;; bit-offset as needed (define-syntax define-sgr-num-attribute+ (syntax-rules () ((_ bit-offset total bit-size mask getter setter remaining ...) (begin (define-sgr-num-attribute bit-offset bit-size mask getter setter) (define-sgr-num-attribute+ (+ bit-offset bit-size) total remaining ...))) ((_ bit-offset total) (define total bit-offset)))) ;; Wrapper for definer starting at bit offset 0 (define-syntax define-sgr-num-attributes (syntax-rules () ((_ total all ...) (begin (define-sgr-num-attribute+ 0 total all ...))))) ;; Define simple (numeric) attributes (define-sgr-num-attributes sgr-state-num-bits ;; 1: bold, 2: half-bright 2 sgr-state-intensity-mask sgr-state-intensity set-sgr-state-intensity 1 sgr-state-italic-mask sgr-state-italic set-sgr-state-italic ;; 1: single, 2: double 2 sgr-state-underline-mask sgr-state-underline set-sgr-state-underline 1 sgr-state-blink-mask sgr-state-blink set-sgr-state-blink 1 sgr-state-reverse-mask sgr-state-reverse set-sgr-state-reverse 1 sgr-state-crossed-mask sgr-state-crossed set-sgr-state-crossed 9 sgr-state-fg256-mask sgr-state-fg256 set-sgr-state-fg256 9 sgr-state-bg256-mask sgr-state-bg256 set-sgr-state-bg256) ;; Used in sgr-state? predicate (define sgr-state-bit-mask (sub1 (arithmetic-shift 1 sgr-state-num-bits))) ;; Returns true, color 0-255 or RGB triplet with bit 17 set to 1 if ;; truecolor (define (sgr-state-foreground s) (if (pair? s) (cadr s) (let ((c+ (sgr-state-fg256 s))) (if c+ (bitwise-and c+ 255) #f)))) ;; Returns true, color 0-255 or RGB triplet with bit 17 set to 1 if ;; truecolor (define (sgr-state-background s) (if (pair? s) (cddr s) (let ((c+ (sgr-state-bg256 s))) (if c+ (bitwise-and c+ 255) #f)))) ;; Sets foreground color, possibly compacting the SGR state, #f is ;; valid value representing default color (define (set-sgr-state-foreground s c) (if (pair? s) ;; Initially it is a pair (if c ;; Setting some foreground (if (< c 256) ;; Foreground does not need pair (if (and (cddr s) (>= (cddr s) 256)) ;; Background needs a pair, set foreground to bits ;; and cadr (cons (set-sgr-state-fg256 (car s) (bitwise-ior c 256)) (cons c (cddr s))) ;; Neither background nor foreground need pair, ;; switch to bit-only representation (set-sgr-state-fg256 (car s) (bitwise-ior c 256))) ;; Foreground needs pair, just update cadr and reset ;; bits (cons (set-sgr-state-fg256 (car s) 0) (cons (bitwise-ior c #x1000000) (cddr s)))) ;; Removing foreground (if (and (cddr s) (>= (cddr s) 256)) ;; Still pair needed for background, store both in bits ;; and cadr (cons (set-sgr-state-fg256 (car s) 0) (cons #f (cddr s))) ;; Just reset, no pair for background needed (set-sgr-state-fg256 s 0))) ;; Initially it is a bit representation (if c ;; Setting foreground (if (< c 256) ;; Just set the bits (set-sgr-state-fg256 s (bitwise-ior c 256)) ;; Create pair representation, store exclusively in cadr (cons (set-sgr-state-fg256 s 0) (cons (bitwise-ior c #x1000000) #f))) ;; Just clear the bits (set-sgr-state-fg256 s 0)))) ;; Sets background color, possibly compacting the SGR state, #f is ;; valid value representing default color (define (set-sgr-state-background s c) (if (pair? s) ;; Initially it is a pair (if c ;; Setting some background (if (< c 256) ;; Background does not need pair (if (and (cadr s) (>= (cadr s) 256)) ;; Foreground needs a pair, set background to bits ;; and cddr (cons (set-sgr-state-bg256 (car s) (bitwise-ior c 256)) (cons (cadr s) c)) ;; Neither background nor foreground need pair, ;; switch to bit-only representation (set-sgr-state-bg256 (car s) (bitwise-ior c 256))) ;; Background needs pair, just update cddr and reset ;; bits (cons (set-sgr-state-bg256 (car s) 0) (cons (cadr s) (bitwise-ior c #x1000000)))) ;; Removing background (if (and (cadr s) (>= (cadr s) 256)) ;; Still pair needed for foreground, store both in bits ;; and cddr (cons (set-sgr-state-bg256 (car s) 0) (cons (cadr s) #f)) ;; Just reset, no pair for foreground needed (set-sgr-state-bg256 s 0))) ;; Initially it is a bit representation (if c ;; Setting background (if (< c 256) ;; Just set the bits (set-sgr-state-bg256 s (bitwise-ior c 256)) ;; Create pair representation, store exclusively in cddr (cons (set-sgr-state-bg256 s 0) (cons #f (bitwise-ior c #x1000000)))) ;; Just clear the bits (set-sgr-state-bg256 s 0)))) ;; Prepends required CSI SGR sequence for given color change (define (sgr-prepend-color-change lst color background?) (let ((off (if background? 10 0))) (cond ((eq? color #f) (cons (+ off 39) lst)) ((< color 8) (cons (+ off 30 color) lst)) ((< color 16) (cons (+ off 82 color) lst)) ((< color 256) (cons color (cons 5 (cons (+ 38 off) lst)))) (else (let-values (((r g b) (split-sgr-truecolor color))) (cons b (cons g (cons r (cons 2 (cons (+ 38 off) lst)))))))))) ;; Produces an CSI SGR sequence to change from the orig state to the ;; next state. (define (sgr-state-change->string orig next) (cond ((equal? orig next) "") ((equal? next empty-sgr-state) "\x1b[0m") (else (let* ((cs0 '()) (cs1 (if (eq? (sgr-state-intensity orig) (sgr-state-intensity next)) cs0 (cons (case (sgr-state-intensity next) ((1) 1) ((2) 2) (else 22)) cs0))) (cs2 (if (eq? (sgr-state-italic orig) (sgr-state-italic next)) cs1 (cons (if (sgr-state-italic next) 3 23) cs1))) (cs3 (if (eq? (sgr-state-underline orig) (sgr-state-underline next)) cs2 (cons (case (sgr-state-underline next) ((1) 4) ((2) 21) (else 24)) cs2))) (cs4 (if (eq? (sgr-state-blink orig) (sgr-state-blink next)) cs3 (cons (if (sgr-state-blink next) 5 25) cs3))) (cs5 (if (eq? (sgr-state-reverse orig) (sgr-state-reverse next)) cs4 (cons (if (sgr-state-reverse next) 7 27) cs4))) (cs6 (if (eq? (sgr-state-crossed orig) (sgr-state-crossed next)) cs5 (cons (if (sgr-state-crossed next) 9 29) cs5))) (cs7 (if (eq? (sgr-state-foreground orig) (sgr-state-foreground next)) cs6 (sgr-prepend-color-change cs6 (sgr-state-foreground next) #f))) (cs8 (if (eq? (sgr-state-background orig) (sgr-state-background next)) cs7 (sgr-prepend-color-change cs7 (sgr-state-background next) #t))) (cs cs8)) (string-append "\x1b[" (string-intersperse (map number->string (reverse cs)) ";") "m"))))) ;; Parses a CSI SGR sequence. Returns a list of numeric arguments in ;; the same order as they are present in the sequence, prepends the ;; terminating character (sequence type). Returns position just after ;; parsed sequence as second value. (define (parse-csi-sgr-sequence+pos str . ps) (let ((pos (if (null? ps) 0 (car ps))) (len (string-length str))) (if (or (>= pos len) (not (eq? (string-ref str pos) #\x1b))) (values '() 0) (if (or (>= (add1 pos) len) (not (eq? (string-ref str (add1 pos)) #\[))) (values '() 1) (let loop ((pos (+ pos 2)) (res '()) (pending #f)) (if (>= pos len) (values '() pos) (let ((ch (string-ref str pos))) (cond ((and (char>=? ch #\0) (char<=? ch #\9)) (let ((digit (- (char->integer ch) (char->integer #\0)))) (loop (add1 pos) res (if pending (+ (* pending 10) digit) digit)))) ((eq? ch #\;) (loop (add1 pos) (cons pending res) #f)) (else (values (cons ch (reverse (if pending (cons pending res) res))) (add1 pos))))))))))) ;; Parses CSI SGR sequence and returns the sequence list only. (define (parse-csi-sgr-sequence str . ps) (let-values (((lst pos) (parse-csi-sgr-sequence+pos str (if (null? ps) 0 (car ps))))) lst)) ;; Parses 256 and 16M color sequences (define (parse-extended-sgr-color lst state background?) (if (null? lst) (list lst state) (case (car lst) ((5) (if (null? (cdr lst)) (list (cdr lst) state) (list (cddr lst) (if background? (set-sgr-state-background state (cadr lst)) (set-sgr-state-foreground state (cadr lst)))))) ((2) (if (null? (cdr lst)) ; R? (list (cdr lst) state) (if (null? (cddr lst)) ; G? (list (cddr lst) state) (if (null? (cdddr lst)) ; B? (list (cdddr lst) state) (list (cddddr lst) (let ((c (make-sgr-truecolor (cadr lst) (caddr lst) (cadddr lst)))) (if background? (set-sgr-state-background state c) (set-sgr-state-foreground state c)))))))) (else (list (cdr lst) state))))) ;; Update given state by parsed SGR state list (define* (update-sgr-state-from-list state lst #:default (default empty-sgr-state)) (if (or (null? lst) (not (eq? (car lst) #\m))) state (let loop ((lst (cdr lst)) (state state)) (if (null? lst) state (case (car lst) ((0) (loop (cdr lst) default)) ((1) (loop (cdr lst) (set-sgr-state-intensity state 1))) ((2) (loop (cdr lst) (set-sgr-state-intensity state 2))) ((3) (loop (cdr lst) (set-sgr-state-intensity state #t))) ((4) (loop (cdr lst) (set-sgr-state-underline state 1))) ((5) (loop (cdr lst) (set-sgr-state-blink state #t))) ((7) (loop (cdr lst) (set-sgr-state-reverse state #t))) ((9) (loop (cdr lst) (set-sgr-state-crossed state #t))) ((21) (loop (cdr lst) (set-sgr-state-underline state 2))) ((22) (loop (cdr lst) (set-sgr-state-intensity state #f))) ((23) (loop (cdr lst) (set-sgr-state-italic state #f))) ((24) (loop (cdr lst) (set-sgr-state-underline state #f))) ((25) (loop (cdr lst) (set-sgr-state-blink state #f))) ((27) (loop (cdr lst) (set-sgr-state-reverse state #f))) ((29) (loop (cdr lst) (set-sgr-state-crossed state #f))) ((30 31 32 33 34 35 36 37) (loop (cdr lst) (set-sgr-state-foreground state (- (car lst) 30)))) ((38) (apply loop (parse-extended-sgr-color (cdr lst) state #f))) ((39) (loop (cdr lst) (set-sgr-state-foreground state #f))) ((40 41 42 43 44 45 46 47) (loop (cdr lst) (set-sgr-state-background state (- (car lst) 40)))) ((48) (apply loop (parse-extended-sgr-color (cdr lst) state #f))) ((49) (loop (cdr lst) (set-sgr-state-background state #f))) ((90 91 92 93 94 95 96 97) (loop (cdr lst) (set-sgr-state-foreground state (- (car lst) 82)))) ((100 101 102 103 104 105 106 107) (loop (cdr lst) (set-sgr-state-background state (- (car lst) 92)))) (else (loop (cdr lst) state))))))) ;; Updates given SGR state based on ESC sequence(s) given in the ;; string. Optional starting position can be given. Processes only ;; one sequence. (define* (update-sgr-state-from-string state str (pos 0) #:default (default empty-sgr-state)) (let ((lst (parse-csi-sgr-sequence str pos))) (update-sgr-state-from-list state lst #:default default))) ;; Module self-tests (define (sgr-state-tests!) (run-tests sgr-state (test-true sgr-color-valid? (sgr-color-valid? 1)) (test-true sgr-color-valid? (sgr-color-valid? #x1234567)) (test-false sgr-color-valid? (sgr-color-valid? 1000)) (test-false sgr-color-valid? (sgr-color-valid? #x2000000)) (test-false sgr-truecolor? (sgr-truecolor? 123)) (test-true sgr-truecolor? (sgr-truecolor? #x1234567)) (test-equal? make-sgr-truecolor (make-sgr-truecolor 1 2 3) #x1010203) (test-equal? split-sgr-truecolor (call-with-values (lambda () (split-sgr-truecolor #x1112233)) list) '(17 34 51)) (test-equal? sgr-state-intensity-mask sgr-state-intensity-mask 3) (test-equal? sgr-state-italic-mask sgr-state-italic-mask 4) (test-equal? sgr-state-italic (sgr-state-italic 4) 1) (test-equal? set-sgr-state-italic (set-sgr-state-italic 0 1) 4) (test-equal? set-sgr-state-italic (set-sgr-state-italic 15 0) 11) (test-equal? set-sgr-state-italic (set-sgr-state-italic 15 #f) 11) (test-equal? set-sgr-state-foreground (set-sgr-state-foreground 0 255) (set-sgr-state-fg256 0 511)) (test-equal? set-sgr-state-foreground (set-sgr-state-foreground 0 256) (cons 0 (cons #x1000100 #f))) (test-equal? set-sgr-state-foreground (set-sgr-state-foreground (set-sgr-state-foreground 0 256) 255) (set-sgr-state-fg256 0 511)) (test-equal? sgr-state-change->string (sgr-state-change->string empty-sgr-state (set-sgr-state-intensity empty-sgr-state 1)) "\x1b[1m") (test-equal? sgr-state-change->string (sgr-state-change->string (set-sgr-state-intensity (set-sgr-state-italic empty-sgr-state 1) 1) empty-sgr-state) "\x1b[0m") (test-equal? sgr-state-change->string (sgr-state-change->string (set-sgr-state-intensity (set-sgr-state-italic empty-sgr-state #t) 2) (set-sgr-state-italic empty-sgr-state #t)) "\x1b[22m") (test-equal? sgr-state-change->string (sgr-state-change->string (set-sgr-state-intensity (set-sgr-state-italic empty-sgr-state #t) 2) (set-sgr-state-intensity empty-sgr-state 2)) "\x1b[23m") (test-equal? sgr-state-change->string (sgr-state-change->string (set-sgr-state-intensity empty-sgr-state 2) (set-sgr-state-intensity (set-sgr-state-italic empty-sgr-state #t) 2)) "\x1b[3m") (test-equal? sgr-state-change->string (sgr-state-change->string (set-sgr-state-intensity empty-sgr-state 2) (set-sgr-state-intensity (set-sgr-state-underline empty-sgr-state 2) 2)) "\x1b[21m") (test-equal? sgr-state-change->string (sgr-state-change->string (set-sgr-state-intensity empty-sgr-state 2) (set-sgr-state-intensity (set-sgr-state-blink empty-sgr-state #t) 2)) "\x1b[5m") (test-equal? sgr-state-change->string (sgr-state-change->string (set-sgr-state-intensity empty-sgr-state 2) (set-sgr-state-intensity (set-sgr-state-reverse empty-sgr-state #t) 2)) "\x1b[7m") (test-equal? sgr-state-change->string (sgr-state-change->string (set-sgr-state-intensity empty-sgr-state 2) (set-sgr-state-intensity (set-sgr-state-crossed empty-sgr-state #t) 2)) "\x1b[9m") (test-equal? sgr-state-change->string (sgr-state-change->string empty-sgr-state (set-sgr-state-foreground empty-sgr-state 5)) "\x1b[35m") (test-equal? sgr-state-change->string (sgr-state-change->string empty-sgr-state (set-sgr-state-foreground empty-sgr-state 15)) "\x1b[97m") (test-equal? sgr-state-change->string (sgr-state-change->string empty-sgr-state (set-sgr-state-foreground empty-sgr-state 115)) "\x1b[38;5;115m") (test-equal? sgr-state-change->string (sgr-state-change->string empty-sgr-state (set-sgr-state-foreground empty-sgr-state #x1112233)) "\x1b[38;2;17;34;51m") (test-equal? sgr-state-change->string (sgr-state-change->string empty-sgr-state (set-sgr-state-background empty-sgr-state 5)) "\x1b[45m") (test-equal? sgr-state-change->string (sgr-state-change->string empty-sgr-state (set-sgr-state-background empty-sgr-state 15)) "\x1b[107m") (test-equal? sgr-state-change->string (sgr-state-change->string empty-sgr-state (set-sgr-state-background empty-sgr-state 115)) "\x1b[48;5;115m") (test-equal? sgr-state-change->string (sgr-state-change->string empty-sgr-state (set-sgr-state-background empty-sgr-state #x1112233)) "\x1b[48;2;17;34;51m") (test-equal? parse-csi-sgr-sequence (parse-csi-sgr-sequence "\x1b[38;2;1;2;3m") '(#\m 38 2 1 2 3)) (test-equal? update-sgr-state-from-string (update-sgr-state-from-string empty-sgr-state "\x1b[1m") 1) (test-equal? update-sgr-state-from-string (update-sgr-state-from-string empty-sgr-state "\x1b[31m") (set-sgr-state-foreground 0 1)) (test-equal? update-sgr-state-from-string (update-sgr-state-from-string empty-sgr-state "\x1b[91m") (set-sgr-state-foreground 0 9)) (test-equal? update-sgr-state-from-string (update-sgr-state-from-string empty-sgr-state "\x1b[38;2;17;34;51m") (set-sgr-state-foreground empty-sgr-state #x1112233)) )) )