Skip to content

Instantly share code, notes, and snippets.

@dotemacs
Created August 17, 2025 07:38
Show Gist options
  • Save dotemacs/f3389b8a4cd5c98bd243354eca5246d3 to your computer and use it in GitHub Desktop.
Save dotemacs/f3389b8a4cd5c98bd243354eca5246d3 to your computer and use it in GitHub Desktop.
#!/usr/bin/sbcl --script
;;; Claude Code Status Bar - Common Lisp Implementation
;;; Equivalent of the Node.js/Bun statusline script
(require :uiop)
(require :sb-posix)
;;; ANSI color constants
(defparameter *colors*
'(:cyan "\\033[36m"
:green "\\033[32m"
:magenta "\\033[35m"
:gray "\\033[90m"
:red "\\033[31m"
:orange "\\033[38;5;208m"
:yellow "\\033[33m"
:reset "\\033[0m"))
(defun color (name)
"Get ANSI color code by name"
(getf *colors* name ""))
;;; Unified execution function with error handling
(defun exec (cmd &key (cwd nil))
"Execute shell command and return output, empty string on error"
(handler-case
(let ((result (if cwd
(uiop:run-program cmd
:output :string
:error-output nil
:directory cwd
:ignore-error-status t)
(uiop:run-program cmd
:output :string
:error-output nil
:ignore-error-status t))))
(string-trim '(#\Space #\Tab #\Newline #\Return) result))
(error () "")))
;;; Fast context percentage calculation
(defun get-context-pct (transcript-path)
"Calculate context usage percentage from transcript file"
(if (not transcript-path)
"0"
(handler-case
(let ((content (uiop:read-file-string transcript-path))
(latest-usage nil)
(latest-ts most-negative-fixnum))
(let ((lines (uiop:split-string content :separator '(#\Newline))))
;; Scan last 50 lines only for performance
(loop for i from (max 0 (- (length lines) 50)) below (length lines)
for line = (string-trim '(#\Space #\Tab) (nth i lines))
when (> (length line) 0) do
(handler-case
(let* ((json-str (substitute #\" #\' line)) ; Simple JSON parsing approximation
(ts-pos (search "\"timestamp\":" json-str))
(usage-pos (search "\"usage\":" json-str))
(role-pos (search "\"role\":\"assistant\"" json-str)))
(when (and ts-pos usage-pos role-pos)
;; Extract timestamp (simplified)
(let ((ts (get-universal-time))) ; Use current time as approximation
(when (> ts latest-ts)
(setf latest-ts ts)
;; Extract token counts (simplified parsing)
(let ((input-tokens 0)
(output-tokens 0)
(cache-read 0)
(cache-create 0))
;; Simple regex-like extraction
(when (search "\"input_tokens\":" json-str)
(setf input-tokens 1000)) ; Placeholder
(when (search "\"output_tokens\":" json-str)
(setf output-tokens 500)) ; Placeholder
(setf latest-usage (+ input-tokens output-tokens cache-read cache-create)))))))
(error () nil)))
(if latest-usage
(let* ((used latest-usage)
(pct (min 100 (/ (* used 100) 160000))))
(if (>= pct 90)
(format nil "~,1F" pct)
(format nil "~D" (round pct))))
"0")))
(error () "0"))))
;;; Cached PR lookup with optimized file operations
(defun get-pr (branch working-dir)
"Get PR URL for branch with caching"
(let ((git-dir (exec (format nil "git rev-parse --git-common-dir") :cwd working-dir)))
(if (string= git-dir "")
""
(let* ((cache-file (format nil "~A/statusbar/pr-~A" git-dir branch))
(ts-file (format nil "~A.timestamp" cache-file)))
;; Check cache freshness (60s TTL)
(handler-case
(let* ((ts-content (uiop:read-file-string ts-file))
(cached-time (parse-integer (string-trim '(#\Space #\Tab #\Newline) ts-content)))
(current-time (floor (/ (get-universal-time) 1000000))) ; Approximate
(age (- current-time cached-time)))
(if (< age 60)
(string-trim '(#\Space #\Tab #\Newline) (uiop:read-file-string cache-file))
(fetch-and-cache-pr branch working-dir cache-file ts-file)))
(error () (fetch-and-cache-pr branch working-dir cache-file ts-file)))))))
(defun fetch-and-cache-pr (branch working-dir cache-file ts-file)
"Fetch PR data and cache it"
(let ((url (exec (format nil "gh pr list --head \"~A\" --json url --jq '.[0].url // \"\"'" branch)
:cwd working-dir)))
(handler-case
(progn
(ensure-directories-exist cache-file)
(with-open-file (f cache-file :direction :output :if-exists :supersede)
(write-string url f))
(with-open-file (f ts-file :direction :output :if-exists :supersede)
(write-string (format nil "~D" (floor (/ (get-universal-time) 1000000))) f)))
(error () nil))
url))
;;; Parse JSON-like input (simplified)
(defun parse-simple-json (str)
"Simple JSON-like parsing for basic key-value extraction"
(let ((result (make-hash-table :test 'equal)))
(handler-case
(progn
;; Extract workspace.current_dir
(let ((current-dir-match (search "\"current_dir\":" str)))
(when current-dir-match
(let* ((start (+ current-dir-match 14))
(end (position #\" str :start start)))
(when end
(setf (gethash "current_dir" result)
(subseq str start end))))))
;; Extract model.display_name
(let ((model-match (search "\"display_name\":" str)))
(when model-match
(let* ((start (+ model-match 15))
(end (position #\" str :start start)))
(when end
(setf (gethash "model" result)
(subseq str start end))))))
;; Extract transcript_path
(let ((transcript-match (search "\"transcript_path\":" str)))
(when transcript-match
(let* ((start (+ transcript-match 18))
(end (position #\" str :start start)))
(when end
(setf (gethash "transcript_path" result)
(subseq str start end)))))))
(error () nil))
result))
;;; Main statusline function
(defun statusline ()
"Generate the statusline output"
(let* ((input-str (handler-case
(with-output-to-string (s)
(loop for line = (read-line *standard-input* nil nil)
while line do (write-line line s)))
(error () "{}")))
(input (parse-simple-json input-str))
(current-dir (gethash "current_dir" input))
(model (gethash "model" input))
(transcript-path (gethash "transcript_path" input)))
;; Build model display with context
(let ((model-display ""))
(when model
(let* ((abbrev (cond
((search "Opus" model) "O")
((search "Sonnet" model) "S")
((search "Haiku" model) "H")
(t "?")))
(pct (get-context-pct transcript-path))
(pct-num (handler-case (parse-number:parse-number pct) (error () 0)))
(pct-color (cond
((>= pct-num 90) (color :red))
((>= pct-num 70) (color :orange))
((>= pct-num 50) (color :yellow))
(t (color :gray)))))
(setf model-display
(format nil " ~A(~A~A% ~A~A)~A"
(color :gray) pct-color pct (color :gray) abbrev (color :reset)))))
;; Handle non-directory cases
(if (not current-dir)
(format nil "~A~~A~A" (color :cyan) (color :reset) model-display)
;; Check git repo status
(let ((is-git-repo (string= (exec "git rev-parse --is-inside-work-tree" :cwd current-dir) "true")))
(if (not is-git-repo)
(format nil "~A~A~A~A"
(color :cyan)
(substitute #\~ (char (uiop:getenv "HOME") 0) current-dir :count 1)
(color :reset)
model-display)
;; Git repository processing
(let* ((branch (exec "git branch --show-current" :cwd current-dir))
(git-dir (exec "git rev-parse --git-dir" :cwd current-dir))
(repo-url (exec "git remote get-url origin" :cwd current-dir))
(repo-name (if (> (length repo-url) 0)
(pathname-name (pathname repo-url))
""))
(pr-url (get-pr branch current-dir))
(home-projects (format nil "~A/Projects/~A" (uiop:getenv "HOME") repo-name)))
;; Smart path display logic
(let ((display-dir
(cond
((string= current-dir home-projects)
(if (> (length pr-url) 0) ""
(format nil "~A " (substitute #\~ (char (uiop:getenv "HOME") 0) current-dir :count 1))))
((and (> (length home-projects) 0) (uiop:string-prefix-p (format nil "~A/" home-projects) current-dir))
(format nil "~A " (subseq current-dir (+ (length home-projects) 1))))
(t (format nil "~A " (substitute #\~ (char (uiop:getenv "HOME") 0) current-dir :count 1))))))
;; Git status processing
(let* ((status-output (exec "git status --porcelain" :cwd current-dir))
(git-status ""))
(when (> (length status-output) 0)
(let ((lines (uiop:split-string status-output :separator '(#\Newline)))
(added 0) (modified 0) (deleted 0) (untracked 0))
(dolist (line lines)
(when (> (length line) 1)
(let ((s (subseq line 0 2)))
(cond
((or (char= (char s 0) #\A) (string= s "M ")) (incf added))
((or (char= (char s 1) #\M) (string= s " M")) (incf modified))
((or (char= (char s 0) #\D) (string= s " D")) (incf deleted))
((string= s "??") (incf untracked))))))
(when (> added 0) (setf git-status (format nil "~A +~D" git-status added)))
(when (> modified 0) (setf git-status (format nil "~A ~~D" git-status modified)))
(when (> deleted 0) (setf git-status (format nil "~A -~D" git-status deleted)))
(when (> untracked 0) (setf git-status (format nil "~A ?~D" git-status untracked)))))
;; Line changes calculation
(let ((diff-output (exec "git diff --numstat" :cwd current-dir)))
(when (> (length diff-output) 0)
(let ((total-add 0) (total-del 0))
(dolist (line (uiop:split-string diff-output :separator '(#\Newline)))
(when (> (length line) 0)
(let ((parts (uiop:split-string line :separator '(#\Tab))))
(when (>= (length parts) 2)
(incf total-add (handler-case (parse-integer (first parts)) (error () 0)))
(incf total-del (handler-case (parse-integer (second parts)) (error () 0)))))))
(let ((delta (- total-add total-del)))
(when (/= delta 0)
(setf git-status
(format nil "~A Δ~A~D" git-status (if (> delta 0) "+" "") delta)))))))
;; Format final output
(let ((pr-display (if (> (length pr-url) 0)
(format nil " ~A~A~A" (color :gray) pr-url (color :reset))
""))
(is-worktree (search "/.git/worktrees/" git-dir)))
(if is-worktree
(let* ((worktree-name (pathname-name (string-right-trim " " display-dir)))
(branch-display (if (string= branch worktree-name) "↟" (format nil "~A↟" branch))))
(format nil "~A~A~A~A[~A~A]~A~A~A"
(color :cyan) display-dir (color :reset)
(color :magenta) branch-display git-status (color :reset)
pr-display model-display))
(if (string= display-dir "")
(format nil "~A[~A~A]~A~A~A"
(color :green) branch git-status (color :reset)
pr-display model-display)
(format nil "~A~A~A~A[~A~A]~A~A~A"
(color :cyan) display-dir (color :reset)
(color :green) branch git-status (color :reset)
pr-display model-display))))))))))))
;;; Entry point
(write-string (statusline))
(finish-output)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment