routstrd 0.3.3 → 0.3.4
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.
- package/bun.lock +2 -2
- package/dist/daemon/index.js +4 -0
- package/dist/index.js +73 -74
- package/package.json +2 -2
- package/src/tui/usage/app.ts +84 -68
- package/src/tui/usage/data.ts +0 -12
package/bun.lock
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"name": "routstrd",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@cashu/cashu-ts": "^4.3.0",
|
|
8
|
-
"@routstr/sdk": "^0.3.
|
|
8
|
+
"@routstr/sdk": "^0.3.12",
|
|
9
9
|
"applesauce-core": "^5.1.0",
|
|
10
10
|
"applesauce-relay": "^5.1.0",
|
|
11
11
|
"applesauce-wallet-connect": "^6.0.0",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
|
|
72
72
|
"@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="],
|
|
73
73
|
|
|
74
|
-
"@routstr/sdk": ["@routstr/sdk@0.3.
|
|
74
|
+
"@routstr/sdk": ["@routstr/sdk@0.3.12", "", { "dependencies": { "@cashu/cashu-ts": "^3.1.1", "applesauce-core": "^5.1.0", "applesauce-relay": "^5.1.0", "applesauce-sqlite": "^6.0.0", "rxjs": "^7.8.1", "zustand": "^5.0.5" }, "optionalDependencies": { "better-sqlite3": "^12.10.0" }, "peerDependencies": { "typescript": ">=5.0.0" } }, "sha512-S/z52pPYZRnWOeD/Vgmk4xx4h6KHDcjkk0eb/vUPnpTEN9+pZ2widG9P3Bz37GWvCFsaGmTO3rWX1bhn34YkDA=="],
|
|
75
75
|
|
|
76
76
|
"@scure/base": ["@scure/base@2.2.0", "", {}, "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg=="],
|
|
77
77
|
|
package/dist/daemon/index.js
CHANGED
|
@@ -47047,6 +47047,8 @@ async function createBunSqliteDriver(dbPath, options) {
|
|
|
47047
47047
|
const logger6 = (options?.logger ?? consoleLogger3).child("BunSqliteDriver");
|
|
47048
47048
|
const SQLite = (await import("bun:sqlite")).default;
|
|
47049
47049
|
const db = new SQLite(dbPath);
|
|
47050
|
+
db.run("PRAGMA journal_mode = WAL");
|
|
47051
|
+
db.run("PRAGMA busy_timeout = 5000");
|
|
47050
47052
|
db.run(`
|
|
47051
47053
|
CREATE TABLE IF NOT EXISTS sdk_storage (
|
|
47052
47054
|
key TEXT PRIMARY KEY,
|
|
@@ -47154,6 +47156,8 @@ var createBunSqliteUsageTrackingDriver = (options = {}) => {
|
|
|
47154
47156
|
throw new Error("Bun SQLite Database constructor is required. Pass { sqlite: { Database } } when creating the driver.");
|
|
47155
47157
|
}
|
|
47156
47158
|
const db = new SQLiteDatabase(dbPath);
|
|
47159
|
+
db.run("PRAGMA journal_mode = WAL");
|
|
47160
|
+
db.run("PRAGMA busy_timeout = 5000");
|
|
47157
47161
|
db.run(`
|
|
47158
47162
|
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
47159
47163
|
id TEXT PRIMARY KEY,
|
package/dist/index.js
CHANGED
|
@@ -13876,9 +13876,6 @@ var init_constants = __esm(() => {
|
|
|
13876
13876
|
// src/tui/usage/data.ts
|
|
13877
13877
|
async function fetchStatus() {
|
|
13878
13878
|
try {
|
|
13879
|
-
const running = await isDaemonRunning();
|
|
13880
|
-
if (!running)
|
|
13881
|
-
return null;
|
|
13882
13879
|
const result = await callDaemon("/status");
|
|
13883
13880
|
if (result.error)
|
|
13884
13881
|
return null;
|
|
@@ -13895,9 +13892,6 @@ async function fetchStatus() {
|
|
|
13895
13892
|
}
|
|
13896
13893
|
async function fetchBalance() {
|
|
13897
13894
|
try {
|
|
13898
|
-
const running = await isDaemonRunning();
|
|
13899
|
-
if (!running)
|
|
13900
|
-
return null;
|
|
13901
13895
|
const result = await callDaemon("/keys/balance");
|
|
13902
13896
|
if (result.error)
|
|
13903
13897
|
return null;
|
|
@@ -13914,9 +13908,6 @@ async function fetchBalance() {
|
|
|
13914
13908
|
}
|
|
13915
13909
|
async function fetchUsageSummary() {
|
|
13916
13910
|
try {
|
|
13917
|
-
const running = await isDaemonRunning();
|
|
13918
|
-
if (!running)
|
|
13919
|
-
return null;
|
|
13920
13911
|
const tz = new Date().getTimezoneOffset();
|
|
13921
13912
|
const result = await callDaemon(`/usage/summary?tz=${tz}`);
|
|
13922
13913
|
if (result.error)
|
|
@@ -13950,9 +13941,6 @@ function formatNumber(n) {
|
|
|
13950
13941
|
}
|
|
13951
13942
|
async function fetchClients() {
|
|
13952
13943
|
try {
|
|
13953
|
-
const running = await isDaemonRunning();
|
|
13954
|
-
if (!running)
|
|
13955
|
-
return [];
|
|
13956
13944
|
const result = await callDaemon("/clients");
|
|
13957
13945
|
if (result.error)
|
|
13958
13946
|
return [];
|
|
@@ -14606,10 +14594,9 @@ async function runUsageTui() {
|
|
|
14606
14594
|
let clients = [];
|
|
14607
14595
|
let visibleTabs = getVisibleTabs(false);
|
|
14608
14596
|
let refreshInterval = null;
|
|
14609
|
-
let shouldUpdate = true;
|
|
14610
14597
|
let autoRefresh = true;
|
|
14611
14598
|
let cleanedUp = false;
|
|
14612
|
-
let
|
|
14599
|
+
let fetching = false;
|
|
14613
14600
|
if (isInteractive) {
|
|
14614
14601
|
stdout.write(enterAlternateScreen() + hideCursor());
|
|
14615
14602
|
stdin.setRawMode?.(true);
|
|
@@ -14633,55 +14620,70 @@ async function runUsageTui() {
|
|
|
14633
14620
|
}
|
|
14634
14621
|
process.on("SIGINT", () => cleanup(0));
|
|
14635
14622
|
process.on("SIGTERM", () => cleanup(0));
|
|
14636
|
-
async function
|
|
14637
|
-
if (
|
|
14623
|
+
async function fetchData() {
|
|
14624
|
+
if (fetching)
|
|
14638
14625
|
return;
|
|
14639
|
-
|
|
14626
|
+
fetching = true;
|
|
14640
14627
|
try {
|
|
14641
|
-
const
|
|
14642
|
-
|
|
14643
|
-
|
|
14644
|
-
|
|
14645
|
-
|
|
14646
|
-
|
|
14647
|
-
|
|
14628
|
+
const running2 = await isDaemonRunning();
|
|
14629
|
+
if (!running2) {
|
|
14630
|
+
stats = null;
|
|
14631
|
+
} else {
|
|
14632
|
+
const [newStats, newBalance, newStatus, newClients] = await Promise.all([
|
|
14633
|
+
fetchUsageSummary(),
|
|
14634
|
+
fetchBalance(),
|
|
14635
|
+
fetchStatus(),
|
|
14636
|
+
fetchClients()
|
|
14637
|
+
]);
|
|
14638
|
+
if (newStats)
|
|
14639
|
+
stats = newStats;
|
|
14640
|
+
if (newBalance)
|
|
14641
|
+
balance = newBalance;
|
|
14642
|
+
if (newStatus)
|
|
14643
|
+
status = newStatus;
|
|
14644
|
+
if (newClients && newClients.length > 0)
|
|
14645
|
+
clients = newClients;
|
|
14648
14646
|
const npubsVisible = hasAnyNpubs(clients);
|
|
14649
14647
|
visibleTabs = getVisibleTabs(npubsVisible);
|
|
14650
14648
|
if (currentTab === "npubs" && !npubsVisible) {
|
|
14651
14649
|
currentTab = "clients";
|
|
14652
14650
|
vimState.scrollPos = 0;
|
|
14653
14651
|
}
|
|
14654
|
-
shouldUpdate = false;
|
|
14655
14652
|
}
|
|
14656
|
-
|
|
14657
|
-
|
|
14653
|
+
render2();
|
|
14654
|
+
} finally {
|
|
14655
|
+
fetching = false;
|
|
14656
|
+
}
|
|
14657
|
+
}
|
|
14658
|
+
function render2() {
|
|
14659
|
+
const width = getWidth();
|
|
14660
|
+
const height = getHeight();
|
|
14661
|
+
if (!stats) {
|
|
14662
|
+
stdout.write(moveCursor(1, 1) + eraseDown() + `${COLORS.red}Error: Could not fetch usage data.${COLORS.reset}
|
|
14658
14663
|
` + `Make sure routstrd is running.
|
|
14659
14664
|
` + `
|
|
14660
14665
|
Press Q to quit.`);
|
|
14661
|
-
|
|
14662
|
-
|
|
14663
|
-
|
|
14664
|
-
|
|
14665
|
-
|
|
14666
|
-
|
|
14666
|
+
return;
|
|
14667
|
+
}
|
|
14668
|
+
const content = renderTabContent(currentTab, stats, balance, status, width, clients);
|
|
14669
|
+
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}` : ""}`;
|
|
14670
|
+
const chrome = renderHeader(currentTab, width, visibleTabs) + renderTabs(currentTab, visibleTabs) + renderSeparator(width) + renderSearchBar();
|
|
14671
|
+
const chromeLines = chrome.split(`
|
|
14667
14672
|
`).length - 1;
|
|
14668
|
-
|
|
14669
|
-
|
|
14673
|
+
const footerSeparator = renderSeparator(width);
|
|
14674
|
+
const footerLines = footerSeparator.split(`
|
|
14670
14675
|
`).length - 1;
|
|
14671
|
-
|
|
14672
|
-
|
|
14673
|
-
|
|
14676
|
+
const contentViewportHeight = Math.max(1, height - chromeLines - footerLines - 1);
|
|
14677
|
+
const visibleContent = applyScrollToContent(content, contentViewportHeight);
|
|
14678
|
+
const footerBlock = (visibleContent ? `
|
|
14674
14679
|
` : "") + footerSeparator + footer;
|
|
14675
|
-
|
|
14676
|
-
} finally {
|
|
14677
|
-
rendering = false;
|
|
14678
|
-
}
|
|
14680
|
+
stdout.write(moveCursor(1, 1) + eraseDown() + chrome + visibleContent + footerBlock);
|
|
14679
14681
|
}
|
|
14680
14682
|
const handleKey = (key) => {
|
|
14681
14683
|
if (vimState.isSearching) {
|
|
14682
14684
|
if (key === "\x1B" || key === "\x1B[3~") {
|
|
14683
14685
|
exitSearchMode();
|
|
14684
|
-
render2(
|
|
14686
|
+
render2();
|
|
14685
14687
|
return;
|
|
14686
14688
|
}
|
|
14687
14689
|
if (key === "\r" || key === `
|
|
@@ -14689,117 +14691,115 @@ Press Q to quit.`);
|
|
|
14689
14691
|
if (stats?.entries)
|
|
14690
14692
|
performSearch(vimState.searchQuery, stats.entries);
|
|
14691
14693
|
exitSearchMode();
|
|
14692
|
-
render2(
|
|
14694
|
+
render2();
|
|
14693
14695
|
return;
|
|
14694
14696
|
}
|
|
14695
14697
|
if (key === "\x7F" || key === "\b") {
|
|
14696
14698
|
vimState.searchQuery = vimState.searchQuery.slice(0, -1);
|
|
14697
14699
|
if (stats?.entries)
|
|
14698
14700
|
performSearch(vimState.searchQuery, stats.entries);
|
|
14699
|
-
render2(
|
|
14701
|
+
render2();
|
|
14700
14702
|
return;
|
|
14701
14703
|
}
|
|
14702
14704
|
if (key === "\x03") {
|
|
14703
14705
|
exitSearchMode();
|
|
14704
|
-
render2(
|
|
14706
|
+
render2();
|
|
14705
14707
|
return;
|
|
14706
14708
|
}
|
|
14707
14709
|
if (key.length === 1 && key.charCodeAt(0) >= 32 && key.charCodeAt(0) < 127) {
|
|
14708
14710
|
vimState.searchQuery += key;
|
|
14709
14711
|
if (stats?.entries)
|
|
14710
14712
|
performSearch(vimState.searchQuery, stats.entries);
|
|
14711
|
-
render2(
|
|
14713
|
+
render2();
|
|
14712
14714
|
}
|
|
14713
14715
|
return;
|
|
14714
14716
|
}
|
|
14715
14717
|
if (key === "q" || key === "Q" || key === "\x03")
|
|
14716
14718
|
return cleanup(0);
|
|
14717
14719
|
if (key === "r" || key === "R") {
|
|
14718
|
-
|
|
14719
|
-
render2(true);
|
|
14720
|
+
fetchData();
|
|
14720
14721
|
return;
|
|
14721
14722
|
}
|
|
14722
14723
|
if (key === "a" || key === "A") {
|
|
14723
14724
|
autoRefresh = !autoRefresh;
|
|
14724
|
-
|
|
14725
|
-
render2(false);
|
|
14725
|
+
render2();
|
|
14726
14726
|
return;
|
|
14727
14727
|
}
|
|
14728
14728
|
if (key === "j" || key === "\x1B[B" || key === "\x1BOB") {
|
|
14729
14729
|
scrollDown();
|
|
14730
|
-
render2(
|
|
14730
|
+
render2();
|
|
14731
14731
|
return;
|
|
14732
14732
|
}
|
|
14733
14733
|
if (key === "k" || key === "\x1B[A" || key === "\x1BOA") {
|
|
14734
14734
|
scrollUp();
|
|
14735
|
-
render2(
|
|
14735
|
+
render2();
|
|
14736
14736
|
return;
|
|
14737
14737
|
}
|
|
14738
14738
|
if (key === "l" || key === "\x1B[C" || key === "\x1BOC") {
|
|
14739
14739
|
const currentIdx = visibleTabs.findIndex((t) => t.id === currentTab);
|
|
14740
14740
|
currentTab = visibleTabs[(currentIdx + 1) % visibleTabs.length].id;
|
|
14741
14741
|
vimState.scrollPos = 0;
|
|
14742
|
-
render2(
|
|
14742
|
+
render2();
|
|
14743
14743
|
return;
|
|
14744
14744
|
}
|
|
14745
14745
|
if (key === "h" || key === "\x1B[D" || key === "\x1BOD") {
|
|
14746
14746
|
const currentIdx = visibleTabs.findIndex((t) => t.id === currentTab);
|
|
14747
14747
|
currentTab = visibleTabs[(currentIdx - 1 + visibleTabs.length) % visibleTabs.length].id;
|
|
14748
14748
|
vimState.scrollPos = 0;
|
|
14749
|
-
render2(
|
|
14749
|
+
render2();
|
|
14750
14750
|
return;
|
|
14751
14751
|
}
|
|
14752
14752
|
if (key === "g") {
|
|
14753
14753
|
if (vimState.lastKey === "g" && Date.now() - vimState.lastKeyTime < 300) {
|
|
14754
14754
|
scrollToBottom();
|
|
14755
14755
|
vimState.lastKey = "";
|
|
14756
|
-
render2(
|
|
14756
|
+
render2();
|
|
14757
14757
|
return;
|
|
14758
14758
|
}
|
|
14759
14759
|
vimState.lastKey = "g";
|
|
14760
14760
|
vimState.lastKeyTime = Date.now();
|
|
14761
14761
|
scrollToTop();
|
|
14762
|
-
render2(
|
|
14762
|
+
render2();
|
|
14763
14763
|
return;
|
|
14764
14764
|
}
|
|
14765
14765
|
if (key === "\x02") {
|
|
14766
14766
|
pageUp();
|
|
14767
|
-
render2(
|
|
14767
|
+
render2();
|
|
14768
14768
|
return;
|
|
14769
14769
|
}
|
|
14770
14770
|
if (key === "\x06") {
|
|
14771
14771
|
pageDown();
|
|
14772
|
-
render2(
|
|
14772
|
+
render2();
|
|
14773
14773
|
return;
|
|
14774
14774
|
}
|
|
14775
14775
|
if (key === "\x15") {
|
|
14776
14776
|
scrollUp(10);
|
|
14777
|
-
render2(
|
|
14777
|
+
render2();
|
|
14778
14778
|
return;
|
|
14779
14779
|
}
|
|
14780
14780
|
if (key === "\x04") {
|
|
14781
14781
|
scrollDown(10);
|
|
14782
|
-
render2(
|
|
14782
|
+
render2();
|
|
14783
14783
|
return;
|
|
14784
14784
|
}
|
|
14785
14785
|
if (key === "\x1B[H" || key === "\x1B[1~" || key === "\x1BOH") {
|
|
14786
14786
|
scrollToTop();
|
|
14787
|
-
render2(
|
|
14787
|
+
render2();
|
|
14788
14788
|
return;
|
|
14789
14789
|
}
|
|
14790
14790
|
if (key === "\x1B[F" || key === "\x1B[4~" || key === "\x1BOF") {
|
|
14791
14791
|
scrollToBottom();
|
|
14792
|
-
render2(
|
|
14792
|
+
render2();
|
|
14793
14793
|
return;
|
|
14794
14794
|
}
|
|
14795
14795
|
if (key === "/") {
|
|
14796
14796
|
startSearch(false);
|
|
14797
|
-
render2(
|
|
14797
|
+
render2();
|
|
14798
14798
|
return;
|
|
14799
14799
|
}
|
|
14800
14800
|
if (key === "?") {
|
|
14801
14801
|
startSearch(true);
|
|
14802
|
-
render2(
|
|
14802
|
+
render2();
|
|
14803
14803
|
return;
|
|
14804
14804
|
}
|
|
14805
14805
|
if (key === "n") {
|
|
@@ -14807,7 +14807,7 @@ Press Q to quit.`);
|
|
|
14807
14807
|
prevSearchResult(stats?.entries.length || 0);
|
|
14808
14808
|
else
|
|
14809
14809
|
nextSearchResult(stats?.entries.length || 0);
|
|
14810
|
-
render2(
|
|
14810
|
+
render2();
|
|
14811
14811
|
return;
|
|
14812
14812
|
}
|
|
14813
14813
|
if (key === "N") {
|
|
@@ -14815,28 +14815,27 @@ Press Q to quit.`);
|
|
|
14815
14815
|
nextSearchResult(stats?.entries.length || 0);
|
|
14816
14816
|
else
|
|
14817
14817
|
prevSearchResult(stats?.entries.length || 0);
|
|
14818
|
-
render2(
|
|
14818
|
+
render2();
|
|
14819
14819
|
return;
|
|
14820
14820
|
}
|
|
14821
14821
|
if (key === "\x1B") {
|
|
14822
14822
|
scrollToTop();
|
|
14823
|
-
render2(
|
|
14823
|
+
render2();
|
|
14824
14824
|
return;
|
|
14825
14825
|
}
|
|
14826
14826
|
const tab = visibleTabs.find((t) => t.key === key);
|
|
14827
14827
|
if (tab) {
|
|
14828
14828
|
currentTab = tab.id;
|
|
14829
14829
|
vimState.scrollPos = 0;
|
|
14830
|
-
render2(
|
|
14830
|
+
render2();
|
|
14831
14831
|
}
|
|
14832
14832
|
};
|
|
14833
14833
|
if (isInteractive)
|
|
14834
14834
|
stdin.on("data", handleKey);
|
|
14835
|
-
await
|
|
14835
|
+
await fetchData();
|
|
14836
14836
|
refreshInterval = setInterval(() => {
|
|
14837
14837
|
if (autoRefresh) {
|
|
14838
|
-
|
|
14839
|
-
render2(true);
|
|
14838
|
+
fetchData();
|
|
14840
14839
|
}
|
|
14841
14840
|
}, 2000);
|
|
14842
14841
|
}
|
|
@@ -15588,7 +15587,7 @@ async function isCocodInstalled(cocodPath) {
|
|
|
15588
15587
|
// package.json
|
|
15589
15588
|
var package_default = {
|
|
15590
15589
|
name: "routstrd",
|
|
15591
|
-
version: "0.3.
|
|
15590
|
+
version: "0.3.4",
|
|
15592
15591
|
module: "src/index.ts",
|
|
15593
15592
|
type: "module",
|
|
15594
15593
|
private: false,
|
|
@@ -15612,7 +15611,7 @@ var package_default = {
|
|
|
15612
15611
|
},
|
|
15613
15612
|
dependencies: {
|
|
15614
15613
|
"@cashu/cashu-ts": "^4.3.0",
|
|
15615
|
-
"@routstr/sdk": "^0.3.
|
|
15614
|
+
"@routstr/sdk": "^0.3.12",
|
|
15616
15615
|
"applesauce-core": "^5.1.0",
|
|
15617
15616
|
"applesauce-relay": "^5.1.0",
|
|
15618
15617
|
"applesauce-wallet-connect": "^6.0.0",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "routstrd",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"module": "src/index.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@cashu/cashu-ts": "^4.3.0",
|
|
27
|
-
"@routstr/sdk": "^0.3.
|
|
27
|
+
"@routstr/sdk": "^0.3.12",
|
|
28
28
|
"applesauce-core": "^5.1.0",
|
|
29
29
|
"applesauce-relay": "^5.1.0",
|
|
30
30
|
"applesauce-wallet-connect": "^6.0.0",
|
package/src/tui/usage/app.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
89
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
168
|
+
render();
|
|
150
169
|
return;
|
|
151
170
|
}
|
|
152
171
|
if (key === "\x03") {
|
|
153
172
|
exitSearchMode();
|
|
154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
168
|
-
void render(true);
|
|
186
|
+
void fetchData();
|
|
169
187
|
return;
|
|
170
188
|
}
|
|
171
189
|
if (key === "a" || key === "A") {
|
|
172
190
|
autoRefresh = !autoRefresh;
|
|
173
|
-
|
|
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
|
-
|
|
197
|
+
render();
|
|
181
198
|
return;
|
|
182
199
|
}
|
|
183
200
|
if (key === "k" || key === "\x1b[A" || key === "\x1bOA") {
|
|
184
201
|
scrollUp();
|
|
185
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
224
|
+
render();
|
|
208
225
|
return;
|
|
209
226
|
}
|
|
210
227
|
vimState.lastKey = "g";
|
|
211
228
|
vimState.lastKeyTime = Date.now();
|
|
212
229
|
scrollToTop();
|
|
213
|
-
|
|
230
|
+
render();
|
|
214
231
|
return;
|
|
215
232
|
}
|
|
216
233
|
|
|
217
|
-
if (key === "\x02") { pageUp();
|
|
218
|
-
if (key === "\x06") { pageDown();
|
|
219
|
-
if (key === "\x15") { scrollUp(10);
|
|
220
|
-
if (key === "\x04") { scrollDown(10);
|
|
221
|
-
if (key === "\x1b[H" || key === "\x1b[1~" || key === "\x1bOH") { scrollToTop();
|
|
222
|
-
if (key === "\x1b[F" || key === "\x1b[4~" || key === "\x1bOF") { scrollToBottom();
|
|
223
|
-
if (key === "/") { startSearch(false);
|
|
224
|
-
if (key === "?") { startSearch(true);
|
|
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
|
-
|
|
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
|
-
|
|
251
|
+
render();
|
|
235
252
|
return;
|
|
236
253
|
}
|
|
237
|
-
if (key === "\x1b") { scrollToTop();
|
|
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
|
-
|
|
260
|
+
render();
|
|
244
261
|
}
|
|
245
262
|
};
|
|
246
263
|
|
|
247
264
|
if (isInteractive) stdin.on("data", handleKey);
|
|
248
265
|
|
|
249
|
-
await
|
|
266
|
+
await fetchData();
|
|
250
267
|
|
|
251
268
|
refreshInterval = setInterval(() => {
|
|
252
269
|
if (autoRefresh) {
|
|
253
|
-
|
|
254
|
-
void render(true);
|
|
270
|
+
void fetchData();
|
|
255
271
|
}
|
|
256
272
|
}, 2000);
|
|
257
273
|
}
|
package/src/tui/usage/data.ts
CHANGED
|
@@ -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
|
|