Add Go statusline implementation

Replace shell-based statusline with native Go for ~4x speedup.
Parses JSON natively, detects git status, checks gitea process,
and formats colored output with proper terminal width handling.
This commit is contained in:
2025-12-18 04:59:15 +01:00
parent ffb73d6544
commit 135bbf68f1
4 changed files with 251 additions and 0 deletions

45
.gitignore vendored Normal file
View File

@ -0,0 +1,45 @@
/statusline
/bin
# Created by https://gitignore.kjanat.com/api/go,linux
# Edit at https://gitignore.kjanat.com?templates=go,linux
### Go ###
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
# End of https://gitignore.kjanat.com/api/go,linux

2
go.mod
View File

@ -1,3 +1,5 @@
module github.com/kjanat/claude-statusline
go 1.24.11
require golang.org/x/sys v0.39.0

2
go.sum Normal file
View File

@ -0,0 +1,2 @@
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=

202
main.go Normal file
View File

@ -0,0 +1,202 @@
package main
import (
"bufio"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"golang.org/x/sys/unix"
)
const statuslineWidthOffset = 7
// Input JSON structure from Claude Code
type StatusInput struct {
Model struct {
DisplayName string `json:"display_name"`
} `json:"model"`
Workspace struct {
CurrentDir string `json:"current_dir"`
} `json:"workspace"`
ContextWindow struct {
ContextWindowSize int `json:"context_window_size"`
CurrentUsage *struct {
InputTokens int `json:"input_tokens"`
CacheCreationTokens int `json:"cache_creation_input_tokens"`
CacheReadInputTokens int `json:"cache_read_input_tokens"`
} `json:"current_usage"`
} `json:"context_window"`
}
// ANSI color codes
const (
reset = "\033[0m"
bold = "\033[1m"
red = "\033[31m"
green = "\033[32m"
yellow = "\033[33m"
magenta = "\033[35m"
cyan = "\033[36m"
boldGreen = "\033[1;32m"
)
func main() {
// Read JSON from stdin
reader := bufio.NewReader(os.Stdin)
var input strings.Builder
for {
line, err := reader.ReadString('\n')
input.WriteString(line)
if err != nil {
break
}
}
var data StatusInput
if err := json.Unmarshal([]byte(input.String()), &data); err != nil {
fmt.Fprintf(os.Stderr, "Error parsing JSON: %v\n", err)
os.Exit(1)
}
// Calculate context info
contextInfo := formatContextInfo(data.ContextWindow.ContextWindowSize, data.ContextWindow.CurrentUsage)
// Get directory name
dirName := filepath.Base(data.Workspace.CurrentDir)
// Check gitea status
giteaStatus := getGiteaStatus()
// Get git info
gitInfo := getGitInfo(data.Workspace.CurrentDir)
// Build left part
left := fmt.Sprintf("%s %s%s%s %s➜%s %s%s%s%s",
giteaStatus,
magenta, data.Model.DisplayName, reset,
boldGreen, reset,
cyan, dirName, reset,
gitInfo)
// Build right part
right := fmt.Sprintf("%s%s%s", yellow, contextInfo, reset)
// Calculate visible lengths (strip ANSI)
leftVisible := stripANSI(left)
rightVisible := stripANSI(right)
// Get terminal width
termWidth := getTerminalWidth() - statuslineWidthOffset
// Calculate padding
padding := termWidth - len(leftVisible) - len(rightVisible)
if padding < 1 {
padding = 1
}
// Output with padding
fmt.Printf("%s%s%s", left, strings.Repeat(" ", padding), right)
}
func formatContextInfo(contextSize int, usage *struct {
InputTokens int `json:"input_tokens"`
CacheCreationTokens int `json:"cache_creation_input_tokens"`
CacheReadInputTokens int `json:"cache_read_input_tokens"`
}) string {
totalK := contextSize / 1000
if usage == nil {
return fmt.Sprintf("0/%dk", totalK)
}
currentTokens := usage.InputTokens + usage.CacheCreationTokens + usage.CacheReadInputTokens
currentK := currentTokens / 1000
return fmt.Sprintf("%dk/%dk", currentK, totalK)
}
func getGiteaStatus() string {
// Check if gitea process is running using pgrep
cmd := exec.Command("pgrep", "-x", "gitea")
if err := cmd.Run(); err == nil {
return green + "●" + reset
}
return red + "●" + reset
}
func getGitInfo(cwd string) string {
// Change to the directory
oldDir, _ := os.Getwd()
if err := os.Chdir(cwd); err != nil {
return ""
}
defer os.Chdir(oldDir)
// Check if we're in a git repo
cmd := exec.Command("git", "rev-parse", "--git-dir")
cmd.Stderr = nil
if err := cmd.Run(); err != nil {
return ""
}
// Get branch name
branch := ""
cmd = exec.Command("git", "symbolic-ref", "--short", "HEAD")
cmd.Stderr = nil
if out, err := cmd.Output(); err == nil {
branch = strings.TrimSpace(string(out))
} else {
// Fallback to short SHA
cmd = exec.Command("git", "rev-parse", "--short", "HEAD")
cmd.Stderr = nil
if out, err := cmd.Output(); err == nil {
branch = strings.TrimSpace(string(out))
}
}
if branch == "" {
return ""
}
// Check if working tree is dirty
isDirty := false
// Check unstaged changes
cmd = exec.Command("git", "diff", "--no-ext-diff", "--quiet", "--exit-code", "--no-optional-locks")
cmd.Stderr = nil
if err := cmd.Run(); err != nil {
isDirty = true
}
// Check staged changes
if !isDirty {
cmd = exec.Command("git", "diff-index", "--cached", "--quiet", "HEAD", "--no-optional-locks")
cmd.Stderr = nil
if err := cmd.Run(); err != nil {
isDirty = true
}
}
if isDirty {
return fmt.Sprintf(" git:(%s) ✗", branch)
}
return fmt.Sprintf(" git:(%s)", branch)
}
func getTerminalWidth() int {
ws, err := unix.IoctlGetWinsize(int(os.Stdout.Fd()), unix.TIOCGWINSZ)
if err != nil {
return 80
}
return int(ws.Col)
}
var ansiRegex = regexp.MustCompile(`\x1b\[[0-9;]*m`)
func stripANSI(s string) string {
return ansiRegex.ReplaceAllString(s, "")
}