Created
August 17, 2025 07:38
-
-
Save dotemacs/f3389b8a4cd5c98bd243354eca5246d3 to your computer and use it in GitHub Desktop.
Common Lisp implementation of https://gist.github.com/steipete/8396e512171d31e934f0013e5651691e
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/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