routstrd 0.3.3 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -49,10 +49,9 @@ export async function runUsageTui(): Promise<void> {
49
49
  let clients: ClientInfo[] = [];
50
50
  let visibleTabs: Tab[] = getVisibleTabs(false);
51
51
  let refreshInterval: ReturnType<typeof setInterval> | null = null;
52
- let shouldUpdate = true;
53
52
  let autoRefresh = true;
54
53
  let cleanedUp = false;
55
- let rendering = false;
54
+ let fetching = false;
56
55
 
57
56
  if (isInteractive) {
58
57
  stdout.write(enterAlternateScreen() + hideCursor());
@@ -80,123 +79,141 @@ export async function runUsageTui(): Promise<void> {
80
79
  process.on("SIGINT", () => cleanup(0));
81
80
  process.on("SIGTERM", () => cleanup(0));
82
81
 
83
- async function render(forceFetch = false) {
84
- if (rendering) return;
85
- rendering = true;
86
-
82
+ /**
83
+ * Background fetch — async, never blocks rendering. Updates the state
84
+ * variables only on success, then triggers a repaint. Overlapping calls
85
+ * are skipped via the `fetching` guard so we don't race stale results.
86
+ */
87
+ async function fetchData(): Promise<void> {
88
+ if (fetching) return;
89
+ fetching = true;
87
90
  try {
88
- const width = getWidth();
89
- const height = getHeight();
91
+ const running = await isDaemonRunning();
92
+ if (!running) {
93
+ stats = null;
94
+ } else {
95
+ // Fire all 4 fetches concurrently — cuts the blocked window significantly.
96
+ const [newStats, newBalance, newStatus, newClients] = await Promise.all([
97
+ fetchUsageSummary(),
98
+ fetchBalance(),
99
+ fetchStatus(),
100
+ fetchClients(),
101
+ ]);
102
+ if (newStats) stats = newStats;
103
+ if (newBalance) balance = newBalance;
104
+ if (newStatus) status = newStatus;
105
+ if (newClients && newClients.length > 0) clients = newClients;
90
106
 
91
- if (forceFetch || shouldUpdate) {
92
- stats = await fetchUsageSummary();
93
- balance = await fetchBalance();
94
- status = await fetchStatus();
95
- clients = await fetchClients();
96
107
  const npubsVisible = hasAnyNpubs(clients);
97
108
  visibleTabs = getVisibleTabs(npubsVisible);
98
- // If current tab is npubs but it's no longer visible, fall back to clients
99
109
  if (currentTab === "npubs" && !npubsVisible) {
100
110
  currentTab = "clients";
101
111
  vimState.scrollPos = 0;
102
112
  }
103
- shouldUpdate = false;
104
- }
105
-
106
- if (!stats) {
107
- stdout.write(
108
- moveCursor(1, 1) +
109
- eraseDown() +
110
- `${COLORS.red}Error: Could not fetch usage data.${COLORS.reset}\n` +
111
- `Make sure routstrd is running.\n` +
112
- `\nPress Q to quit.`
113
- );
114
- return;
115
113
  }
114
+ render();
115
+ } finally {
116
+ fetching = false;
117
+ }
118
+ }
116
119
 
117
- const content = renderTabContent(currentTab, stats, balance, status, width, clients);
118
- const footer = `${COLORS.dim}Press [Q] to quit, [R] to refresh, [A] to toggle auto-refresh${autoRefresh ? " (on)" : " (off)"} scroll:${vimState.scrollPos}${COLORS.reset}${vimState.mode === "normal" ? ` ${COLORS.yellow}vim: hjkl/arrows, / search, g top, gg bottom${COLORS.reset}` : ""}`;
119
- const chrome = renderHeader(currentTab, width, visibleTabs) + renderTabs(currentTab, visibleTabs) + renderSeparator(width) + renderSearchBar();
120
- const chromeLines = chrome.split("\n").length - 1;
121
- const footerSeparator = renderSeparator(width);
122
- const footerLines = footerSeparator.split("\n").length - 1;
123
- const contentViewportHeight = Math.max(1, height - chromeLines - footerLines - 1);
124
- const visibleContent = applyScrollToContent(content, contentViewportHeight);
125
- const footerBlock = (visibleContent ? "\n" : "") + footerSeparator + footer;
120
+ /**
121
+ * Synchronous paint — never awaits I/O. Reads the current state variables
122
+ * and writes to stdout. Safe to call from key handlers without blocking.
123
+ */
124
+ function render(): void {
125
+ const width = getWidth();
126
+ const height = getHeight();
126
127
 
127
- stdout.write(moveCursor(1, 1) + eraseDown() + chrome + visibleContent + footerBlock);
128
- } finally {
129
- rendering = false;
128
+ if (!stats) {
129
+ stdout.write(
130
+ moveCursor(1, 1) +
131
+ eraseDown() +
132
+ `${COLORS.red}Error: Could not fetch usage data.${COLORS.reset}\n` +
133
+ `Make sure routstrd is running.\n` +
134
+ `\nPress Q to quit.`
135
+ );
136
+ return;
130
137
  }
138
+
139
+ const content = renderTabContent(currentTab, stats, balance, status, width, clients);
140
+ const footer = `${COLORS.dim}Press [Q] to quit, [R] to refresh, [A] to toggle auto-refresh${autoRefresh ? " (on)" : " (off)"} scroll:${vimState.scrollPos}${COLORS.reset}${vimState.mode === "normal" ? ` ${COLORS.yellow}vim: hjkl/arrows, / search, g top, gg bottom${COLORS.reset}` : ""}`;
141
+ const chrome = renderHeader(currentTab, width, visibleTabs) + renderTabs(currentTab, visibleTabs) + renderSeparator(width) + renderSearchBar();
142
+ const chromeLines = chrome.split("\n").length - 1;
143
+ const footerSeparator = renderSeparator(width);
144
+ const footerLines = footerSeparator.split("\n").length - 1;
145
+ const contentViewportHeight = Math.max(1, height - chromeLines - footerLines - 1);
146
+ const visibleContent = applyScrollToContent(content, contentViewportHeight);
147
+ const footerBlock = (visibleContent ? "\n" : "") + footerSeparator + footer;
148
+
149
+ stdout.write(moveCursor(1, 1) + eraseDown() + chrome + visibleContent + footerBlock);
131
150
  }
132
151
 
133
152
  const handleKey = (key: string) => {
134
153
  if (vimState.isSearching) {
135
154
  if (key === "\x1b" || key === "\x1b[3~") {
136
155
  exitSearchMode();
137
- void render(false);
156
+ render();
138
157
  return;
139
158
  }
140
159
  if (key === "\r" || key === "\n") {
141
160
  if (stats?.entries) performSearch(vimState.searchQuery, stats.entries);
142
161
  exitSearchMode();
143
- void render(false);
162
+ render();
144
163
  return;
145
164
  }
146
165
  if (key === "\x7f" || key === "\x08") {
147
166
  vimState.searchQuery = vimState.searchQuery.slice(0, -1);
148
167
  if (stats?.entries) performSearch(vimState.searchQuery, stats.entries);
149
- void render(false);
168
+ render();
150
169
  return;
151
170
  }
152
171
  if (key === "\x03") {
153
172
  exitSearchMode();
154
- void render(false);
173
+ render();
155
174
  return;
156
175
  }
157
176
  if (key.length === 1 && key.charCodeAt(0) >= 32 && key.charCodeAt(0) < 127) {
158
177
  vimState.searchQuery += key;
159
178
  if (stats?.entries) performSearch(vimState.searchQuery, stats.entries);
160
- void render(false);
179
+ render();
161
180
  }
162
181
  return;
163
182
  }
164
183
 
165
184
  if (key === "q" || key === "Q" || key === "\u0003") return cleanup(0);
166
185
  if (key === "r" || key === "R") {
167
- shouldUpdate = true;
168
- void render(true);
186
+ void fetchData();
169
187
  return;
170
188
  }
171
189
  if (key === "a" || key === "A") {
172
190
  autoRefresh = !autoRefresh;
173
- shouldUpdate = true;
174
- void render(false);
191
+ render();
175
192
  return;
176
193
  }
177
194
 
178
195
  if (key === "j" || key === "\x1b[B" || key === "\x1bOB") {
179
196
  scrollDown();
180
- void render(false);
197
+ render();
181
198
  return;
182
199
  }
183
200
  if (key === "k" || key === "\x1b[A" || key === "\x1bOA") {
184
201
  scrollUp();
185
- void render(false);
202
+ render();
186
203
  return;
187
204
  }
188
205
  if (key === "l" || key === "\x1b[C" || key === "\x1bOC") {
189
206
  const currentIdx = visibleTabs.findIndex((t) => t.id === currentTab);
190
207
  currentTab = visibleTabs[(currentIdx + 1) % visibleTabs.length]!.id;
191
208
  vimState.scrollPos = 0;
192
- void render(false);
209
+ render();
193
210
  return;
194
211
  }
195
212
  if (key === "h" || key === "\x1b[D" || key === "\x1bOD") {
196
213
  const currentIdx = visibleTabs.findIndex((t) => t.id === currentTab);
197
214
  currentTab = visibleTabs[(currentIdx - 1 + visibleTabs.length) % visibleTabs.length]!.id;
198
215
  vimState.scrollPos = 0;
199
- void render(false);
216
+ render();
200
217
  return;
201
218
  }
202
219
 
@@ -204,54 +221,53 @@ export async function runUsageTui(): Promise<void> {
204
221
  if (vimState.lastKey === "g" && Date.now() - vimState.lastKeyTime < 300) {
205
222
  scrollToBottom();
206
223
  vimState.lastKey = "";
207
- void render(false);
224
+ render();
208
225
  return;
209
226
  }
210
227
  vimState.lastKey = "g";
211
228
  vimState.lastKeyTime = Date.now();
212
229
  scrollToTop();
213
- void render(false);
230
+ render();
214
231
  return;
215
232
  }
216
233
 
217
- if (key === "\x02") { pageUp(); void render(false); return; }
218
- if (key === "\x06") { pageDown(); void render(false); return; }
219
- if (key === "\x15") { scrollUp(10); void render(false); return; }
220
- if (key === "\x04") { scrollDown(10); void render(false); return; }
221
- if (key === "\x1b[H" || key === "\x1b[1~" || key === "\x1bOH") { scrollToTop(); void render(false); return; }
222
- if (key === "\x1b[F" || key === "\x1b[4~" || key === "\x1bOF") { scrollToBottom(); void render(false); return; }
223
- if (key === "/") { startSearch(false); void render(false); return; }
224
- if (key === "?") { startSearch(true); void render(false); return; }
234
+ if (key === "\x02") { pageUp(); render(); return; }
235
+ if (key === "\x06") { pageDown(); render(); return; }
236
+ if (key === "\x15") { scrollUp(10); render(); return; }
237
+ if (key === "\x04") { scrollDown(10); render(); return; }
238
+ if (key === "\x1b[H" || key === "\x1b[1~" || key === "\x1bOH") { scrollToTop(); render(); return; }
239
+ if (key === "\x1b[F" || key === "\x1b[4~" || key === "\x1bOF") { scrollToBottom(); render(); return; }
240
+ if (key === "/") { startSearch(false); render(); return; }
241
+ if (key === "?") { startSearch(true); render(); return; }
225
242
  if (key === "n") {
226
243
  if (vimState.searchReverse) prevSearchResult(stats?.entries.length || 0);
227
244
  else nextSearchResult(stats?.entries.length || 0);
228
- void render(false);
245
+ render();
229
246
  return;
230
247
  }
231
248
  if (key === "N") {
232
249
  if (vimState.searchReverse) nextSearchResult(stats?.entries.length || 0);
233
250
  else prevSearchResult(stats?.entries.length || 0);
234
- void render(false);
251
+ render();
235
252
  return;
236
253
  }
237
- if (key === "\x1b") { scrollToTop(); void render(false); return; }
254
+ if (key === "\x1b") { scrollToTop(); render(); return; }
238
255
 
239
256
  const tab = visibleTabs.find((t) => t.key === key);
240
257
  if (tab) {
241
258
  currentTab = tab.id;
242
259
  vimState.scrollPos = 0;
243
- void render(false);
260
+ render();
244
261
  }
245
262
  };
246
263
 
247
264
  if (isInteractive) stdin.on("data", handleKey);
248
265
 
249
- await render(true);
266
+ await fetchData();
250
267
 
251
268
  refreshInterval = setInterval(() => {
252
269
  if (autoRefresh) {
253
- shouldUpdate = true;
254
- void render(true);
270
+ void fetchData();
255
271
  }
256
272
  }, 2000);
257
273
  }
@@ -26,9 +26,6 @@ export interface StatusInfo {
26
26
 
27
27
  export async function fetchStatus(): Promise<StatusInfo | null> {
28
28
  try {
29
- const running = await isDaemonRunning();
30
- if (!running) return null;
31
-
32
29
  const result = await callDaemon("/status");
33
30
  if (result.error) return null;
34
31
 
@@ -52,9 +49,6 @@ export async function fetchStatus(): Promise<StatusInfo | null> {
52
49
 
53
50
  export async function fetchBalance(): Promise<BalanceInfo | null> {
54
51
  try {
55
- const running = await isDaemonRunning();
56
- if (!running) return null;
57
-
58
52
  const result = await callDaemon("/keys/balance");
59
53
  if (result.error) return null;
60
54
 
@@ -78,9 +72,6 @@ export async function fetchBalance(): Promise<BalanceInfo | null> {
78
72
 
79
73
  export async function fetchUsageSummary(): Promise<UsageStats | null> {
80
74
  try {
81
- const running = await isDaemonRunning();
82
- if (!running) return null;
83
-
84
75
  const tz = new Date().getTimezoneOffset();
85
76
  const result = await callDaemon(`/usage/summary?tz=${tz}`);
86
77
  if (result.error) return null;
@@ -123,9 +114,6 @@ export interface ClientInfo {
123
114
 
124
115
  export async function fetchClients(): Promise<ClientInfo[]> {
125
116
  try {
126
- const running = await isDaemonRunning();
127
- if (!running) return [];
128
-
129
117
  const result = await callDaemon("/clients");
130
118
  if (result.error) return [];
131
119
 
@@ -278,18 +278,25 @@ export function renderToday(stats: UsageStats, width: number): string {
278
278
  const _now = new Date();
279
279
  const todayDateStr = `${_now.getFullYear()}-${String(_now.getMonth() + 1).padStart(2, "0")}-${String(_now.getDate()).padStart(2, "0")}`;
280
280
 
281
- let todayStats: { date: string; requests: number; satsCost: number; promptTokens: number; completionTokens: number; totalTokens: number };
282
- let recentDays: Array<{ date: string; requests: number; satsCost: number; totalTokens: number; promptTokens: number; completionTokens: number }>;
283
- let hourlyMap: Map<number, { requests: number; satsCost: number }>;
281
+ let todayStats: { date: string; requests: number; satsCost: number; promptTokens: number; completionTokens: number; totalTokens: number } = {
282
+ date: todayDateStr,
283
+ requests: 0,
284
+ satsCost: 0,
285
+ promptTokens: 0,
286
+ completionTokens: 0,
287
+ totalTokens: 0,
288
+ };
289
+ let recentDays: Array<{ date: string; requests: number; satsCost: number; totalTokens: number; promptTokens: number; completionTokens: number }> = [];
290
+ let hourlyMap: Map<number, { requests: number; satsCost: number }> = new Map();
284
291
 
285
292
  if (stats.summary) {
286
293
  // Use server-aggregated data
287
294
  const { days, hoursToday } = stats.summary;
288
295
  // days[0] is most-recent-first; check if it's today
289
296
  const todayDayStat = days[0]?.date === todayDateStr ? days[0] : undefined;
290
- todayStats = todayDayStat
291
- ? { date: todayDayStat.date, requests: todayDayStat.requests, satsCost: todayDayStat.satsCost, promptTokens: todayDayStat.promptTokens, completionTokens: todayDayStat.completionTokens, totalTokens: todayDayStat.totalTokens }
292
- : { date: todayDateStr, requests: 0, satsCost: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0 };
297
+ if (todayDayStat) {
298
+ todayStats = { date: todayDayStat.date, requests: todayDayStat.requests, satsCost: todayDayStat.satsCost, promptTokens: todayDayStat.promptTokens, completionTokens: todayDayStat.completionTokens, totalTokens: todayDayStat.totalTokens };
299
+ }
293
300
  // Exclude today by date (it has its own box) rather than by position —
294
301
  // days[0] is only today when there was activity today.
295
302
  recentDays = days.filter((d) => d.date !== todayDateStr).slice(0, 6);
package/say_hi.sh DELETED
@@ -1,32 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- LANGUAGES=(
5
- "Mandarin Chinese"
6
- "Spanish"
7
- "English"
8
- "Hindi"
9
- "Arabic"
10
- "Portuguese"
11
- "Bengali"
12
- "Russian"
13
- "Japanese"
14
- "German"
15
- "French"
16
- "Italian"
17
- "Korean"
18
- "Turkish"
19
- "Vietnamese"
20
- "Tamil"
21
- "Polish"
22
- "Dutch"
23
- "Swahili"
24
- "Thai"
25
- "Greek"
26
- )
27
-
28
- for lang in "${LANGUAGES[@]}"; do
29
- echo "=== $lang ==="
30
- pi -p -c "in $lang"
31
- echo
32
- done