Skip to content

Instantly share code, notes, and snippets.

@christianromney
Created March 25, 2026 22:30
Show Gist options
  • Select an option

  • Save christianromney/59a848851ffa7604960f961fab7073f8 to your computer and use it in GitHub Desktop.

Select an option

Save christianromney/59a848851ffa7604960f961fab7073f8 to your computer and use it in GitHub Desktop.
Claude Code status line (Babashka) — Catppuccin Frappe theme
#!/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