Created
March 25, 2026 22:30
-
-
Save christianromney/59a848851ffa7604960f961fab7073f8 to your computer and use it in GitHub Desktop.
Claude Code status line (Babashka) — Catppuccin Frappe theme
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bb | |
| ;; Claude Code status line — complements Starship / Catppuccin Frappe prompt | |
| ;; Receives Claude Code session JSON on stdin | |
| (require '[cheshire.core :as json] | |
| '[clojure.string :as str] | |
| '[babashka.process :refer [shell]]) | |
| ;; --------------------------------------------------------------------------- | |
| ;; Display helpers | |
| ;; --------------------------------------------------------------------------- | |
| (def palette | |
| "Catppuccin Frappe ANSI escape sequences, keyed by color name." | |
| {:reset "\u001b[0m" | |
| :dim "\u001b[2m" | |
| :yellow "\u001b[38;2;229;200;144m" | |
| :peach "\u001b[38;2;239;159;118m" | |
| :lavender "\u001b[38;2;186;187;241m" | |
| :mauve "\u001b[38;2;202;158;230m" | |
| :overlay1 "\u001b[38;2;131;139;167m" | |
| :green "\u001b[38;2;166;209;137m" | |
| :red "\u001b[38;2;231;130;132m"}) | |
| (def role->color | |
| "Maps semantic display roles to Catppuccin Frappe palette color keys." | |
| {:current-directory :peach | |
| :git-branch :yellow | |
| :model :lavender | |
| :cost :mauve | |
| :muted :overlay1 | |
| :context-ok :green | |
| :context-warn :peach | |
| :context-critical :red}) | |
| (defn display | |
| "Apply one or more styles to text, followed by a reset sequence. Styles are | |
| applied in order; each may be a semantic role key (e.g. :git-branch) that | |
| maps to a palette color, or a direct palette key (e.g. :dim, :yellow). | |
| Unknown role keys or colors are ignored, defaulting to the standard foreground | |
| text color." | |
| [text style & styles] | |
| (let [resolve #(get palette (get role->color % %) "") | |
| codes (apply str (map resolve (cons style styles)))] | |
| (str codes text (:reset palette)))) | |
| (def separator | |
| "Separator rendered between status-line segments: a dimmed dash in the | |
| muted overlay color." | |
| (display "⁃" :dim :muted)) | |
| ;; --------------------------------------------------------------------------- | |
| ;; Input parsing | |
| ;; --------------------------------------------------------------------------- | |
| (def input | |
| "Raw session data parsed from the JSON payload Claude Code writes to stdin." | |
| (json/parse-string (slurp *in*) true)) | |
| ;; --------------------------------------------------------------------------- | |
| ;; Shared extraction helpers | |
| ;; --------------------------------------------------------------------------- | |
| (defn- extract-dir | |
| "Return the current working directory path from a session input map, | |
| preferring the workspace current_dir field over the top-level cwd field." | |
| [input] | |
| (or (get-in input [:workspace :current_dir]) | |
| (:cwd input) | |
| "")) | |
| ;; --------------------------------------------------------------------------- | |
| ;; Status-line segment renderers | |
| ;; --------------------------------------------------------------------------- | |
| (defn current-directory | |
| "Return a colorized string showing the base name (last path segment) of the | |
| current working directory. Returns a colorized \".\" when the path is blank." | |
| [input] | |
| (let [dir (extract-dir input) | |
| base (if (str/blank? dir) "." (last (str/split dir #"/")))] | |
| (display base :current-directory))) | |
| (defn model-info | |
| "Return a colorized string showing the display name of the Claude model in | |
| use (e.g. \"Claude Sonnet 4.6\")." | |
| [input] | |
| (display (or (get-in input [:model :display_name]) "") :model)) | |
| (defn session-name | |
| "Return a dimmed, muted string showing the session name in brackets. | |
| Returns nil when no session name has been assigned." | |
| [input] | |
| (let [name (:session_name input)] | |
| (when-not (str/blank? name) | |
| (display (str "[" name "]") :dim :muted)))) | |
| (defn- parse-branch | |
| "Extract the branch name from porcelain v2 status output lines. Returns a | |
| short SHA prefixed with ':' for detached HEAD, or nil when the branch | |
| header is absent." | |
| [lines] | |
| (let [head (some-> (first (filter #(str/starts-with? % "# branch.head ") lines)) | |
| (subs (count "# branch.head "))) | |
| oid (some-> (first (filter #(str/starts-with? % "# branch.oid ") lines)) | |
| (subs (count "# branch.oid ")) | |
| (subs 0 7))] | |
| (cond-> head | |
| (= head "(detached)") (constantly (str ":" oid))))) | |
| (defn git-info | |
| "Return a colorized string showing the current git branch name and | |
| work-tree status indicators (δ for staged/unstaged changes, ∅ for | |
| untracked files). Returns nil when the current directory is not inside a | |
| git repository. Uses a single git-status call to collect all information." | |
| [input] | |
| (when-let [output (try | |
| (let [result (shell {:out :string :err :string :continue true} | |
| "git" "--no-optional-locks" "-C" (extract-dir input) | |
| "status" "--porcelain=v2" "--branch")] | |
| (when (zero? (:exit result)) | |
| (str/trim (:out result)))) | |
| (catch Exception _ nil))] | |
| (let [lines (str/split-lines output) | |
| dirty? (some #(re-find #"^[12u] " %) lines) | |
| untracked? (some #(str/starts-with? % "? ") lines)] | |
| (when-let [branch (parse-branch lines)] | |
| (display (str branch (when dirty? " δ") (when untracked? " ∅")) | |
| :git-branch))))) | |
| (defn context-usage | |
| "Return a colorized string showing context-window utilization as a | |
| percentage used. Color shifts from :context-ok to :context-warn at 75% | |
| used, and to :context-critical at 90% used. Returns nil when no messages | |
| have been exchanged." | |
| [input] | |
| (when-let [remaining (get-in input [:context_window :remaining_percentage])] | |
| (let [used-int (- 100 (int remaining)) | |
| color-key (cond | |
| (>= used-int 90) :context-critical | |
| (>= used-int 75) :context-warn | |
| :else :context-ok)] | |
| (display (str "Context: " used-int "% Used") color-key)))) | |
| (defn format-token-count | |
| "Format a raw token count as a compact, human-readable string. | |
| Under 1,000: display the exact count (e.g. 42 → \"42\"). | |
| Under 1,000,000: display in k with one decimal place (e.g. 12345 → \"12.3k\"). | |
| 1,000,000 and above: display in M with one decimal place (e.g. 1234567 → \"1.2M\")." | |
| [n] | |
| (cond | |
| (< n 1000) (str n) | |
| (< n 1000000) (format "%.1fk" (/ (double n) 1000)) | |
| :else (format "%.1fM" (/ (double n) 1000000)))) | |
| (defn token-info | |
| "Return a muted string showing cumulative session input and output token | |
| counts. Returns nil when either count is absent." | |
| [input] | |
| (let [total-in (get-in input [:context_window :total_input_tokens]) | |
| total-out (get-in input [:context_window :total_output_tokens])] | |
| (when (and total-in total-out) | |
| (display (str "Tokens: " (format-token-count total-in) "↑ " | |
| (format-token-count total-out) "↓") | |
| :muted)))) | |
| (defn cost-info | |
| "Return a colorized string showing the cumulative session cost in USD. | |
| Returns nil when cost data is absent." | |
| [input] | |
| (when-let [cost (get-in input [:cost :total_cost_usd])] | |
| (display (str "Cost: " (format "$%.4f" (double cost))) :cost))) | |
| ;; --------------------------------------------------------------------------- | |
| ;; Assemble status line parts and print | |
| ;; --------------------------------------------------------------------------- | |
| (print (str/join separator | |
| (filterv some? | |
| [(current-directory input) | |
| (session-name input) | |
| (git-info input) | |
| (model-info input) | |
| (context-usage input) | |
| (token-info input) | |
| (cost-info input)]))) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment