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:
45
.gitignore
vendored
Normal file
45
.gitignore
vendored
Normal 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
2
go.mod
@ -1,3 +1,5 @@
|
|||||||
module github.com/kjanat/claude-statusline
|
module github.com/kjanat/claude-statusline
|
||||||
|
|
||||||
go 1.24.11
|
go 1.24.11
|
||||||
|
|
||||||
|
require golang.org/x/sys v0.39.0
|
||||||
|
|||||||
2
go.sum
Normal file
2
go.sum
Normal 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
202
main.go
Normal 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, "")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user