1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
|
package main
import (
"encoding/json"
"fmt"
"net"
"os"
"strings"
"time"
)
// claudeUsage is what the status line needs to know about Claude plan usage.
type claudeUsage struct {
// NeedsLogin means nobody is logged in to claude.ai, so no number can be
// shown: any cached one is frozen at whatever it was when the session died.
NeedsLogin bool
// Percent is the current 5-hour session utilization.
Percent int
// ResetIn is how long until that window resets.
ResetIn time.Duration
// Reachable is false when the service's last background refresh failed.
Reachable bool
// Age is how old the cached numbers are; only meaningful when !Reachable.
// Negative means unknown.
Age time.Duration
}
// varlinkReply is the subset of a varlink reply we care about.
type varlinkReply struct {
Error string `json:"error"`
Parameters struct {
FiveHour *struct {
Utilization float64 `json:"utilization"`
ResetsAt string `json:"resets_at"`
} `json:"five_hour"`
FetchedAt string `json:"fetched_at"`
// Pointers so a missing field is distinguishable from false: older
// services do not send these, and absence must not read as "broken".
Reachable *bool `json:"reachable"`
NeedsLogin *bool `json:"needs_login"`
} `json:"parameters"`
}
// claudeSocket is the claude-usage varlink service's unix socket.
func claudeSocket() string {
return fmt.Sprintf("/run/user/%d/de.profpatsch.ClaudeUsage", os.Getuid())
}
// fetchClaudeUsage queries the claude-usage varlink service.
//
// The service answers from a cached snapshot it refreshes in the background,
// so this never blocks on the network — but it may not be running at all, and
// a status line must not fail because of that. Every error therefore yields
// (nil, nil): the segment is simply omitted.
func fetchClaudeUsage() (*claudeUsage, error) {
conn, err := net.DialTimeout("unix", claudeSocket(), 2*time.Second)
if err != nil {
return nil, nil
}
defer conn.Close()
// Bound the whole exchange, so a wedged service cannot hang the status bar.
deadline := time.Now().Add(2 * time.Second)
if err := conn.SetDeadline(deadline); err != nil {
return nil, nil
}
req := `{"method":"de.profpatsch.ClaudeUsage.GetUsage"}` + "\x00"
if _, err := conn.Write([]byte(req)); err != nil {
return nil, nil
}
// Varlink frames messages with a trailing NUL.
var buf strings.Builder
chunk := make([]byte, 65536)
for {
n, err := conn.Read(chunk)
if n > 0 {
buf.Write(chunk[:n])
if strings.HasSuffix(buf.String(), "\x00") {
break
}
}
if err != nil {
break
}
}
payload := strings.TrimSuffix(buf.String(), "\x00")
if payload == "" {
return nil, nil
}
var reply varlinkReply
if err := json.Unmarshal([]byte(payload), &reply); err != nil {
return nil, nil
}
// NotLoggedIn means the service has no cached data *and* nobody is logged
// in. There is nothing to show but the call to action.
if reply.Error != "" {
if strings.HasSuffix(reply.Error, ".NotLoggedIn") {
return &claudeUsage{NeedsLogin: true, Age: -1}, nil
}
return nil, nil
}
five := reply.Parameters.FiveHour
if five == nil {
return nil, nil
}
resetsAt, err := time.Parse(time.RFC3339, five.ResetsAt)
if err != nil {
return nil, nil
}
resetIn := time.Until(resetsAt)
if resetIn < 0 {
resetIn = 0
}
// fetched_at is the last *successful* fetch, so this grows without bound
// while the service cannot reach claude.ai.
age := time.Duration(-1)
if reply.Parameters.FetchedAt != "" {
if t, err := time.Parse(time.RFC3339, reply.Parameters.FetchedAt); err == nil {
age = time.Since(t)
}
}
// Default reachable to true and needs_login to false, for forward-compat
// with older services that do not send them.
reachable := true
if reply.Parameters.Reachable != nil {
reachable = *reply.Parameters.Reachable
}
needsLogin := false
if reply.Parameters.NeedsLogin != nil {
needsLogin = *reply.Parameters.NeedsLogin
}
return &claudeUsage{
NeedsLogin: needsLogin,
Percent: int(five.Utilization + 0.5),
ResetIn: resetIn,
Reachable: reachable,
Age: age,
}, nil
}
// humanizeAge renders a coarse age for the staleness marker: minutes, then
// hours, then days. A status line has no room for more precision, and the
// point is only to distinguish a blip from a fortnight of silence.
func humanizeAge(d time.Duration) string {
switch {
case d < time.Hour:
return fmt.Sprintf("%dm", int(d.Minutes()))
case d < 24*time.Hour:
return fmt.Sprintf("%dh", int(d.Hours()))
default:
return fmt.Sprintf("%dd", int(d.Hours()/24))
}
}
|