From 6cf5fa63b0a101795accc5e033f141d1f274f4e5 Mon Sep 17 00:00:00 2001 From: "Howard M. Lewis Ship" Date: Fri, 20 Sep 2024 14:32:17 -0700 Subject: [PATCH] Fix faint, add pout --- CHANGES.md | 15 +++++-- deps.edn | 1 + src/clj_commons/ansi.clj | 74 +++++++++++++++++++------------- test/clj_commons/ansi_test.clj | 15 ++++--- test/clj_commons/binary_test.clj | 14 +++--- 5 files changed, 73 insertions(+), 46 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b20c950..87ba346 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,10 +1,19 @@ ## 3.2.0 - UNRELEASED -Added `clj-commons.format.exceptions/default-frame-rules` with the defaults for `*default-frame-rules*` -which makes it much easier to override the rules. +Added `clj-commons.ansi/pout` to replace the `pcompose` function; they are identical, but the `pout` name makes more +sense, given that `perr` exists. + +Changed how `clj-commons.ansi/compose` creates ANSI SGR strings; this works around an issue in many terminal emulators +where changing boldness from faint to normal, or faint to bold, is not implemented correctly. `compose` now resets fonts +before each font change, which allows such transitions to render correctly. + +Added `clj-commons.format.exceptions/default-frame-rules` to supply defaults for `*default-frame-rules*` +which makes it much easier to override the default rules. Added function `clj-commons.format.exceptions/format-stack-trace-element` which can be used to convert a Java -StackTraceElement into demangled, readable string, using the same logic used by `format-exception.` +StackTraceElement into demangled, readable string, using the same logic as `format-exception.` + +[Closed Issues](https://github.com/clj-commons/pretty/milestone/52?closed=1) ## 3.1.1 - 22 Aug 2024 diff --git a/deps.edn b/deps.edn index 929ed78..cd3a82f 100644 --- a/deps.edn +++ b/deps.edn @@ -8,6 +8,7 @@ :extra-deps {criterium/criterium {:mvn/version "0.4.6"} org.clojure/core.async {:mvn/version "1.6.681"} nubank/matcher-combinators {:mvn/version "3.9.1"} + io.github.tonsky/clj-reload {:mvn/version "0.7.1"} io.github.cognitect-labs/test-runner {:git/tag "v0.5.1" :git/sha "dfb30dd"}} :jvm-opts ["-Dclj-commons.ansi.enabled=true"] diff --git a/src/clj_commons/ansi.clj b/src/clj_commons/ansi.clj index 21edc7f..75cdb10 100644 --- a/src/clj_commons/ansi.clj +++ b/src/clj_commons/ansi.clj @@ -55,6 +55,8 @@ (str csi sgr)) (def ^:private font-terms + ;; Map a keyword to a tuple of characteristic and SGR parameter value. + ;; We track the current value for each characteristic. (reduce merge {:bold [:bold "1"] :plain [:bold "22"] @@ -83,11 +85,15 @@ current-value))) (defn- compose-font - ^String [active current] + "Uses values in current to build a font string that will reset all fonts characteristics then, + as necessary, add back needed font characteristics." + ^String [current] (when-color-enabled - (let [codes (keep #(delta active current %) [:foreground :background :bold :italic :inverse :underlined])] - (when (seq codes) - (str csi (str/join ";" codes) sgr))))) + (let [codes (keep #(get current %) [:foreground :background :bold :italic :inverse :underlined])] + (if (seq codes) + (str csi "0;" (str/join ";" codes) sgr) + ;; there were active characteristics, but current has none, so just reset font characteristics + reset-font)))) (defn- split-font-def* [font-def] @@ -138,7 +144,9 @@ (throw (ex-info "invalid span declaration" {:font-decl value})))) -(defn- blank? [value] +(defn- nil-or-empty-string? + "True if an empty string, or nil; false otherwise, such as for numbers, etc." + [value] (or (nil? value) (= "" value))) @@ -152,7 +160,8 @@ (mod x 2) 0)))) -(defn- apply-padding [terms pad width actual-width] +(defn- apply-padding + [terms pad width actual-width] (let [padding-needed (- width actual-width) left-padding (case pad (:left nil) padding-needed @@ -197,7 +206,7 @@ [coll *width] (let [f (fn reducer [result input] (cond - (blank? input) + (nil-or-empty-string? input) result (vector? input) @@ -225,7 +234,7 @@ (defn- collect-markup [state input] (cond - (blank? input) + (nil-or-empty-string? input) state (vector? input) @@ -236,12 +245,12 @@ ;; Normal (no width tracking) (let [{:keys [current]} state] (-> (reduce collect-markup - (-> state - (update :current update-font-data-from-font-def font) - (update :stack conj current)) + (update state :current update-font-data-from-font-def font) inputs) - (assoc :current current) - (update :stack pop))))) + ;; At the end of the vector, return current (but not the active) + ;; to what it was previously. We leave active alone until we're about + ;; to output. + (assoc :current current))))) ;; Lists, lazy-lists, etc: processed recursively (sequential? input) @@ -251,27 +260,21 @@ (let [{:keys [active current ^StringBuilder buffer]} state state' (if (= active current) state - (let [font-str (compose-font active current)] + (let [font-str (compose-font current)] (when font-str (.append buffer font-str)) (cond-> (assoc state :active current) ;; Signal that a reset is needed at the very end - font-str (assoc :dirty? true))))] + font-str + (assoc :dirty? (not= font-str reset-font)))))] (.append buffer (str input)) state'))) (defn- compose* [inputs] - (let [initial-font {:foreground "39" - :background "49" - :bold "22" - :italic "23" - :inverse "27" - :underlined "24"} - buffer (StringBuilder. 100) - {:keys [dirty?]} (collect-markup {:stack [] - :active initial-font - :current initial-font + (let [buffer (StringBuilder. 100) + {:keys [dirty?]} (collect-markup {:active {} + :current {} :buffer buffer} inputs)] (when dirty? @@ -289,8 +292,8 @@ `map` or `for` to be mixed into the composed string seamlessly. Nested vectors represent _spans_, a sequence of values with a specific visual representation. - The first element in a span vector declares the visual properties of the span: the color (including - other characteristics such as bold or underline), and the width and padding (described later). + The first element in a span vector declares the visual properties of the span: the font color + and other font characteristics, and the width and padding (described later). Spans may be nested. The declaration is usually a keyword, to define just the font. @@ -322,7 +325,7 @@ Font defs apply on top of the font def of the enclosing span, and the outer span's font def is restored at the end of the inner span, e.g. `[:red \" RED \" [:bold \"RED/BOLD\"] \" RED \"]`. - Alternately, a font def may be a vector of individual keyword, e.g., `[[:bold :red] ...]` rather than + Alternately, a font def may be a vector of individual keywords, e.g., `[[:bold :red] ...]` rather than `[:bold.red ...]`. This works better when the exact font characteristics are determined dynamically. @@ -370,9 +373,18 @@ [& inputs] (compose* inputs)) -(defn pcompose +(defn pout "Composes its inputs as with [[compose]] and then prints the results, with a newline." - {:added "2.2"} + {:added "3.2"} + [& inputs] + (println (compose* inputs))) + +(defn pcompose + "Composes its inputs as with [[compose]] and then prints the results, with a newline. + + Deprecated: use [[pout]] instead." + {:added "2.2" + :deprecated "3.2.0"} [& inputs] (println (compose* inputs))) @@ -382,3 +394,5 @@ [& inputs] (binding [*out* *err*] (println (compose* inputs)))) + + diff --git a/test/clj_commons/ansi_test.clj b/test/clj_commons/ansi_test.clj index 0214639..43cee9a 100644 --- a/test/clj_commons/ansi_test.clj +++ b/test/clj_commons/ansi_test.clj @@ -44,6 +44,7 @@ ["Simple"] "Simple" + ["String" \space :keyword \space 'symbol \space 123 \space 44.5] "String :keyword symbol 123 44.5" @@ -75,7 +76,7 @@ " are operating at " [:green "98.7%"] "."] - "Notice: the [CSI]33mshields[CSI]39m are operating at [CSI]32m98.7%[CSI]39m.[CSI]m" + "Notice: the [CSI]0;33mshields[CSI]m are operating at [CSI]0;32m98.7%[CSI]m." ;; nil is allowed (this is used when formatting is optional, such as the fonts in exceptions). @@ -89,13 +90,13 @@ ["NORMAL" [:red "-RED"] [:bright-red "-BR/RED"]] - "NORMAL[CSI]31m-RED[CSI]91m-BR/RED[CSI]m" + "NORMAL[CSI]0;31m-RED[CSI]0;91m-BR/RED[CSI]m" ["NORMAL-" [:inverse "-INVERSE" [:bold "-INV/BOLD"]] [:inverse.bold "-INV/BOLD"] "-NORMAL"] - "NORMAL-[CSI]7m-INVERSE[CSI]1m-INV/BOLD-INV/BOLD[CSI]22;27m-NORMAL[CSI]m" + "NORMAL-[CSI]0;7m-INVERSE[CSI]0;1;7m-INV/BOLD-INV/BOLD[CSI]m-NORMAL" ;; Basic tests for width: @@ -117,8 +118,8 @@ [{:width 10 :font :red} "BBB"] "|") - "START |[CSI]32mAAA [CSI]39m|[CSI]31m BBB[CSI]39m|[CSI]m" - ; 0123456789 0123456789 + "START |[CSI]0;32mAAA [CSI]m|[CSI]0;31m BBB[CSI]m|" + ; 0123456789 0123456789 '("START |" [{:width 10 @@ -128,8 +129,8 @@ [{:width 10 :font :red} "XYZ"] "|") - "START |[CSI]32mAB[CSI]34mC[CSI]32m [CSI]39m|[CSI]31m XYZ[CSI]39m|[CSI]m" - ; 01 2 3456789 0123456789 + "START |[CSI]0;32mAB[CSI]0;34mC[CSI]0;32m [CSI]m|[CSI]0;31m XYZ[CSI]m|" + ; 01 2 3456789 0123456789 ;; Only pads, never truncates diff --git a/test/clj_commons/binary_test.clj b/test/clj_commons/binary_test.clj index cce3af3..ebd2820 100644 --- a/test/clj_commons/binary_test.clj +++ b/test/clj_commons/binary_test.clj @@ -42,7 +42,7 @@ (deftest binary-fonts (let [byte-data (byte-array [0x59 0x65 073 0x20 0x4e 0x00 0x00 0x09 0x80 0xff])] - (is (= ["{90}0000:{39} {36}59{39} {36}65{39} {36}3B{39} {32}20{39} {36}4E{39} {90}00{39} {90}00{39} {32}09{39} {33}80{39} {33}FF{39} |{36}Ye;{32} {36}N{90}••{32}_{33}××{39} |{}"] + (is (= ["{0;90}0000:{} {0;36}59{} {0;36}65{} {0;36}3B{} {0;32}20{} {0;36}4E{} {0;90}00{} {0;90}00{} {0;32}09{} {0;33}80{} {0;33}FF{} |{0;36}Ye;{0;32} {0;36}N{0;90}••{0;32}_{0;33}××{} |"] (-> (b/format-binary byte-data {:ascii true}) fixup-sgr string/split-lines))))) @@ -96,23 +96,25 @@ (deftest deltas-with-fonts (are [expected actual expected-output] - (= expected-output + (match? expected-output (format-binary-delta expected actual)) "123\t" "123\n" + ;; {} is reset font + ;; 0 is reset font (as a prefix) ;; 90 is bright black for offset ;; 36 is cyan for printable ASCII ;; 32 is green for whitespace ;; 102 is bright green backround, ;; 101 is bright red background - ["{90}0000:{39} {36}31{39} {36}32{39} {36}33{39} {32;102}09{39;49} | {36}31{39} {36}32{39} {36}33{39} {32;101}0A{}"] + ["{0;90}0000:{} {0;36}31{} {0;36}32{} {0;36}33{} {0;32;102}09{} | {0;36}31{} {0;36}32{} {0;36}33{} {0;32;101}0A{}"] "1234" "12" - ["{90}0000:{39} {36}31{39} {36}32{39} {36;102}33{39;49} {36;102}34{39;49} | {36}31{39} {36}32{39} {101}--{49} {101}--{}"] + ["{0;90}0000:{} {0;36}31{} {0;36}32{} {0;36;102}33{} {0;36;102}34{} | {0;36}31{} {0;36}32{} {0;101}--{} {0;101}--{}"] ;; 2 is faint for non-printable "\u001B" "\u001Cxyz" - ["{90}0000:{39} {32;102;2}1B{39;49;22} {102}--{49} {102}--{49} {102}--{49} | {32;101;2}1C{39;49;22} {36;101}78{39;49} {36;101}79{39;49} {36;101}7A{}"])) - + ["{0;90}0000:{} {0;32;102;2}1B{} {0;102}--{} {0;102}--{} {0;102}--{} | {0;32;101;2}1C{} {0;36;101}78{} {0;36;101}79{} {0;36;101}7A{}"] + ))