@naarang/glancebar 1.0.2 → 1.0.3
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/README.md +44 -10
- package/package.json +1 -1
- package/src/cli.ts +250 -15
package/README.md
CHANGED
|
@@ -7,12 +7,13 @@ A customizable statusline for [Claude Code](https://claude.com/product/claude-co
|
|
|
7
7
|
|
|
8
8
|
## Features
|
|
9
9
|
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
- Water
|
|
14
|
-
-
|
|
15
|
-
-
|
|
10
|
+
- **Session info** - Project name, git branch, model, cost, lines changed, and context usage
|
|
11
|
+
- **Calendar events** - Upcoming events from multiple Google accounts
|
|
12
|
+
- **Meeting warnings** - Red alert when a meeting is 5 minutes away
|
|
13
|
+
- **Health reminders** - Water, stretch, and eye break reminders
|
|
14
|
+
- **Color-coded** - Everything has distinct colors for quick scanning
|
|
15
|
+
- **Fully configurable** via CLI
|
|
16
|
+
- **Cross-platform** support (Windows, macOS, Linux)
|
|
16
17
|
|
|
17
18
|
## Requirements
|
|
18
19
|
|
|
@@ -138,8 +139,10 @@ glancebar config --max-title 80
|
|
|
138
139
|
# Toggle calendar name display
|
|
139
140
|
glancebar config --show-calendar false
|
|
140
141
|
|
|
141
|
-
# Enable/disable
|
|
142
|
+
# Enable/disable health reminders
|
|
142
143
|
glancebar config --water-reminder true
|
|
144
|
+
glancebar config --stretch-reminder true
|
|
145
|
+
glancebar config --eye-reminder true
|
|
143
146
|
|
|
144
147
|
# Reset to defaults
|
|
145
148
|
glancebar config --reset
|
|
@@ -147,15 +150,44 @@ glancebar config --reset
|
|
|
147
150
|
|
|
148
151
|
## Display Format
|
|
149
152
|
|
|
153
|
+
Example output:
|
|
154
|
+
```
|
|
155
|
+
glancebar | main* | Opus | $0.12 | +156 -23 | 9.7k/200k (5%) | In 15m: Team Standup (work)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Session Info (from Claude Code)
|
|
159
|
+
|
|
160
|
+
| Field | Color | Example |
|
|
161
|
+
|-------|-------|---------|
|
|
162
|
+
| Project name | Blue | `glancebar`, `my-app` |
|
|
163
|
+
| Git branch | Magenta | `main`, `feature-x*` (asterisk = uncommitted changes) |
|
|
164
|
+
| Model name | Yellow | `Opus`, `Sonnet` |
|
|
165
|
+
| Cost | Green | `$0.01`, `$0.1234` |
|
|
166
|
+
| Lines changed | Green/Red | `+156 -23` |
|
|
167
|
+
| Context usage | Green/Yellow/Red | `9.7k/200k (5%)` |
|
|
168
|
+
|
|
169
|
+
Context usage color changes based on percentage:
|
|
170
|
+
- **Green**: < 50%
|
|
171
|
+
- **Yellow**: 50-80%
|
|
172
|
+
- **Red**: > 80%
|
|
173
|
+
|
|
174
|
+
### Calendar Events
|
|
175
|
+
|
|
150
176
|
| State | Format | Example |
|
|
151
177
|
|-------|--------|---------|
|
|
178
|
+
| **Meeting warning** | Red alert when ≤5m away | `Meeting in 3m - wrap up!` |
|
|
152
179
|
| Upcoming (within threshold) | `In Xm: Title (account)` | `In 15m: Team Standup (work)` |
|
|
153
180
|
| Current | `Now: Title (account)` | `Now: Team Standup (work)` |
|
|
154
181
|
| Later | `HH:MM AM/PM: Title (account)` | `2:30 PM: Meeting (work)` |
|
|
155
182
|
| No events | `No upcoming events` | |
|
|
156
|
-
| Water reminder | Random hydration message | `Stay hydrated! Drink some water` |
|
|
157
183
|
|
|
158
|
-
|
|
184
|
+
### Health Reminders (~30% chance)
|
|
185
|
+
|
|
186
|
+
| Type | Color | Example |
|
|
187
|
+
|------|-------|---------|
|
|
188
|
+
| Water | Cyan | `Stay hydrated! Drink some water` |
|
|
189
|
+
| Stretch | Green | `Time to stretch! Stand up and move` |
|
|
190
|
+
| Eye break | Magenta | `Eye break! Look 20ft away for 20s` |
|
|
159
191
|
|
|
160
192
|
## Configuration
|
|
161
193
|
|
|
@@ -177,7 +209,9 @@ All configuration is stored in `~/.glancebar/`:
|
|
|
177
209
|
| `countdownThresholdMinutes` | 60 | Minutes threshold for countdown display |
|
|
178
210
|
| `maxTitleLength` | 120 | Maximum event title length |
|
|
179
211
|
| `showCalendarName` | true | Show account name after event |
|
|
180
|
-
| `waterReminderEnabled` | true | Enable random water break reminders
|
|
212
|
+
| `waterReminderEnabled` | true | Enable random water break reminders |
|
|
213
|
+
| `stretchReminderEnabled` | true | Enable random stretch/posture reminders |
|
|
214
|
+
| `eyeReminderEnabled` | true | Enable random eye break reminders (20-20-20 rule) |
|
|
181
215
|
|
|
182
216
|
## Building from Source
|
|
183
217
|
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -16,6 +16,8 @@ interface Config {
|
|
|
16
16
|
countdownThresholdMinutes: number;
|
|
17
17
|
maxTitleLength: number;
|
|
18
18
|
waterReminderEnabled: boolean;
|
|
19
|
+
stretchReminderEnabled: boolean;
|
|
20
|
+
eyeReminderEnabled: boolean;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
const COLORS: Record<string, string> = {
|
|
@@ -47,6 +49,8 @@ const DEFAULT_CONFIG: Config = {
|
|
|
47
49
|
countdownThresholdMinutes: 60,
|
|
48
50
|
maxTitleLength: 120,
|
|
49
51
|
waterReminderEnabled: true,
|
|
52
|
+
stretchReminderEnabled: true,
|
|
53
|
+
eyeReminderEnabled: true,
|
|
50
54
|
};
|
|
51
55
|
|
|
52
56
|
const WATER_REMINDERS = [
|
|
@@ -60,6 +64,26 @@ const WATER_REMINDERS = [
|
|
|
60
64
|
"Quick reminder: Drink water!",
|
|
61
65
|
];
|
|
62
66
|
|
|
67
|
+
const STRETCH_REMINDERS = [
|
|
68
|
+
"Time to stretch! Stand up and move",
|
|
69
|
+
"Stretch break! Roll your shoulders",
|
|
70
|
+
"Stand up and stretch your legs",
|
|
71
|
+
"Posture check! Sit up straight",
|
|
72
|
+
"Take a quick stretch break",
|
|
73
|
+
"Move your body! Quick stretch",
|
|
74
|
+
"Stretch your neck and shoulders",
|
|
75
|
+
"Stand up! Your body will thank you",
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
const EYE_REMINDERS = [
|
|
79
|
+
"Eye break! Look 20ft away for 20s",
|
|
80
|
+
"Rest your eyes - look at something distant",
|
|
81
|
+
"20-20-20: Look away from screen",
|
|
82
|
+
"Give your eyes a break!",
|
|
83
|
+
"Look away from the screen for a moment",
|
|
84
|
+
"Eye rest time! Focus on something far",
|
|
85
|
+
];
|
|
86
|
+
|
|
63
87
|
function getConfigDir(): string {
|
|
64
88
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
65
89
|
return join(home, ".glancebar");
|
|
@@ -410,6 +434,20 @@ function formatCountdown(minutes: number): string {
|
|
|
410
434
|
return mins === 0 ? `In ${hours}h` : `In ${hours}h${mins}m`;
|
|
411
435
|
}
|
|
412
436
|
|
|
437
|
+
function getMeetingWarning(event: CalendarEvent | null): string | null {
|
|
438
|
+
if (!event) return null;
|
|
439
|
+
|
|
440
|
+
const now = new Date();
|
|
441
|
+
const minutesUntil = Math.round((event.start.getTime() - now.getTime()) / 60000);
|
|
442
|
+
|
|
443
|
+
// Warning when meeting is 5 minutes or less away
|
|
444
|
+
if (minutesUntil > 0 && minutesUntil <= 5) {
|
|
445
|
+
return `${COLORS.brightRed}Meeting in ${minutesUntil}m - wrap up!${COLORS.reset}`;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
|
|
413
451
|
function formatTime(date: Date): string {
|
|
414
452
|
const hours = date.getHours();
|
|
415
453
|
const minutes = date.getMinutes();
|
|
@@ -440,13 +478,15 @@ Usage:
|
|
|
440
478
|
glancebar config --max-title <length> Set max title length (default: 120)
|
|
441
479
|
glancebar config --show-calendar <true|false> Show calendar name (default: true)
|
|
442
480
|
glancebar config --water-reminder <true|false> Enable/disable water reminders (default: true)
|
|
481
|
+
glancebar config --stretch-reminder <true|false> Enable/disable stretch reminders (default: true)
|
|
482
|
+
glancebar config --eye-reminder <true|false> Enable/disable eye break reminders (default: true)
|
|
443
483
|
glancebar config --reset Reset to default configuration
|
|
444
484
|
glancebar setup Show setup instructions
|
|
445
485
|
|
|
446
486
|
Examples:
|
|
447
487
|
glancebar auth --add user@gmail.com
|
|
448
488
|
glancebar config --lookahead 12
|
|
449
|
-
glancebar config --
|
|
489
|
+
glancebar config --stretch-reminder false
|
|
450
490
|
|
|
451
491
|
Config location: ${getConfigDir()}
|
|
452
492
|
`);
|
|
@@ -681,6 +721,34 @@ function handleConfig(args: string[]) {
|
|
|
681
721
|
return;
|
|
682
722
|
}
|
|
683
723
|
|
|
724
|
+
// Handle --stretch-reminder
|
|
725
|
+
const stretchReminderIndex = args.indexOf("--stretch-reminder");
|
|
726
|
+
if (stretchReminderIndex !== -1) {
|
|
727
|
+
const value = args[stretchReminderIndex + 1]?.toLowerCase();
|
|
728
|
+
if (value !== "true" && value !== "false") {
|
|
729
|
+
console.error("Error: --stretch-reminder must be 'true' or 'false'");
|
|
730
|
+
process.exit(1);
|
|
731
|
+
}
|
|
732
|
+
config.stretchReminderEnabled = value === "true";
|
|
733
|
+
saveConfig(config);
|
|
734
|
+
console.log(`Stretch reminder ${value === "true" ? "enabled" : "disabled"}`);
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Handle --eye-reminder
|
|
739
|
+
const eyeReminderIndex = args.indexOf("--eye-reminder");
|
|
740
|
+
if (eyeReminderIndex !== -1) {
|
|
741
|
+
const value = args[eyeReminderIndex + 1]?.toLowerCase();
|
|
742
|
+
if (value !== "true" && value !== "false") {
|
|
743
|
+
console.error("Error: --eye-reminder must be 'true' or 'false'");
|
|
744
|
+
process.exit(1);
|
|
745
|
+
}
|
|
746
|
+
config.eyeReminderEnabled = value === "true";
|
|
747
|
+
saveConfig(config);
|
|
748
|
+
console.log(`Eye break reminder ${value === "true" ? "enabled" : "disabled"}`);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
|
|
684
752
|
// Show current config
|
|
685
753
|
console.log(`
|
|
686
754
|
Glancebar Configuration
|
|
@@ -696,34 +764,195 @@ Calendar Settings:
|
|
|
696
764
|
|
|
697
765
|
Reminders:
|
|
698
766
|
Water reminder: ${config.waterReminderEnabled ? "enabled" : "disabled"}
|
|
767
|
+
Stretch reminder: ${config.stretchReminderEnabled ? "enabled" : "disabled"}
|
|
768
|
+
Eye break reminder: ${config.eyeReminderEnabled ? "enabled" : "disabled"}
|
|
699
769
|
`);
|
|
700
770
|
}
|
|
701
771
|
|
|
702
|
-
function
|
|
703
|
-
|
|
772
|
+
function getRandomReminder(config: Config): string | null {
|
|
773
|
+
const enabledReminders: Array<() => string> = [];
|
|
774
|
+
|
|
775
|
+
if (config.waterReminderEnabled) {
|
|
776
|
+
enabledReminders.push(() => {
|
|
777
|
+
const reminder = WATER_REMINDERS[Math.floor(Math.random() * WATER_REMINDERS.length)];
|
|
778
|
+
return `${COLORS.brightCyan}${reminder}${COLORS.reset}`;
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
if (config.stretchReminderEnabled) {
|
|
783
|
+
enabledReminders.push(() => {
|
|
784
|
+
const reminder = STRETCH_REMINDERS[Math.floor(Math.random() * STRETCH_REMINDERS.length)];
|
|
785
|
+
return `${COLORS.brightGreen}${reminder}${COLORS.reset}`;
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
if (config.eyeReminderEnabled) {
|
|
790
|
+
enabledReminders.push(() => {
|
|
791
|
+
const reminder = EYE_REMINDERS[Math.floor(Math.random() * EYE_REMINDERS.length)];
|
|
792
|
+
return `${COLORS.brightMagenta}${reminder}${COLORS.reset}`;
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (enabledReminders.length === 0) return null;
|
|
797
|
+
|
|
798
|
+
// ~30% chance to show any reminder
|
|
799
|
+
if (Math.random() >= 0.3) return null;
|
|
704
800
|
|
|
705
|
-
//
|
|
706
|
-
|
|
801
|
+
// Pick a random reminder type from enabled ones
|
|
802
|
+
const randomPicker = enabledReminders[Math.floor(Math.random() * enabledReminders.length)];
|
|
803
|
+
return randomPicker();
|
|
707
804
|
}
|
|
708
805
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
806
|
+
interface ClaudeCodeStatus {
|
|
807
|
+
model?: { display_name?: string };
|
|
808
|
+
cost?: {
|
|
809
|
+
total_cost_usd?: number;
|
|
810
|
+
total_lines_added?: number;
|
|
811
|
+
total_lines_removed?: number;
|
|
812
|
+
};
|
|
813
|
+
cwd?: string;
|
|
814
|
+
workspace?: {
|
|
815
|
+
project_dir?: string;
|
|
816
|
+
};
|
|
817
|
+
context_window?: {
|
|
818
|
+
context_window_size?: number;
|
|
819
|
+
current_usage?: {
|
|
820
|
+
input_tokens?: number;
|
|
821
|
+
output_tokens?: number;
|
|
822
|
+
cache_creation_input_tokens?: number;
|
|
823
|
+
cache_read_input_tokens?: number;
|
|
824
|
+
};
|
|
825
|
+
};
|
|
712
826
|
}
|
|
713
827
|
|
|
714
|
-
|
|
715
|
-
// Consume stdin (Claude Code sends JSON)
|
|
828
|
+
function getGitBranch(cwd?: string): string | null {
|
|
716
829
|
try {
|
|
717
|
-
|
|
718
|
-
|
|
830
|
+
const { execSync } = require("child_process");
|
|
831
|
+
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
832
|
+
cwd: cwd || process.cwd(),
|
|
833
|
+
encoding: "utf-8",
|
|
834
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
835
|
+
}).trim();
|
|
836
|
+
|
|
837
|
+
// Check if there are uncommitted changes
|
|
838
|
+
let isDirty = false;
|
|
839
|
+
try {
|
|
840
|
+
const status = execSync("git status --porcelain", {
|
|
841
|
+
cwd: cwd || process.cwd(),
|
|
842
|
+
encoding: "utf-8",
|
|
843
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
844
|
+
}).trim();
|
|
845
|
+
isDirty = status.length > 0;
|
|
846
|
+
} catch {}
|
|
847
|
+
|
|
848
|
+
return isDirty ? `${branch}*` : branch;
|
|
849
|
+
} catch {
|
|
850
|
+
return null;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function getProjectName(projectDir?: string): string | null {
|
|
855
|
+
if (!projectDir) return null;
|
|
856
|
+
// Extract the last part of the path as project name
|
|
857
|
+
const parts = projectDir.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
858
|
+
return parts.length > 0 ? parts[parts.length - 1] : null;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
async function readStdinJson(): Promise<ClaudeCodeStatus | null> {
|
|
862
|
+
try {
|
|
863
|
+
const chunks: Uint8Array[] = [];
|
|
864
|
+
for await (const chunk of Bun.stdin.stream()) {
|
|
865
|
+
chunks.push(chunk);
|
|
866
|
+
}
|
|
867
|
+
if (chunks.length === 0) return null;
|
|
868
|
+
const text = Buffer.concat(chunks).toString("utf-8").trim();
|
|
869
|
+
if (!text) return null;
|
|
870
|
+
return JSON.parse(text);
|
|
871
|
+
} catch {
|
|
872
|
+
return null;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function formatSessionInfo(status: ClaudeCodeStatus): string {
|
|
877
|
+
const parts: string[] = [];
|
|
878
|
+
|
|
879
|
+
// Project name
|
|
880
|
+
const projectName = getProjectName(status.workspace?.project_dir);
|
|
881
|
+
if (projectName) {
|
|
882
|
+
parts.push(`${COLORS.brightBlue}${projectName}${COLORS.reset}`);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Git branch
|
|
886
|
+
const gitBranch = getGitBranch(status.cwd || status.workspace?.project_dir);
|
|
887
|
+
if (gitBranch) {
|
|
888
|
+
parts.push(`${COLORS.magenta}${gitBranch}${COLORS.reset}`);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// Model name
|
|
892
|
+
if (status.model?.display_name) {
|
|
893
|
+
parts.push(`${COLORS.brightYellow}${status.model.display_name}${COLORS.reset}`);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Cost
|
|
897
|
+
if (status.cost?.total_cost_usd !== undefined) {
|
|
898
|
+
const cost = status.cost.total_cost_usd;
|
|
899
|
+
const costStr = cost < 0.01 ? `$${cost.toFixed(4)}` : `$${cost.toFixed(2)}`;
|
|
900
|
+
parts.push(`${COLORS.green}${costStr}${COLORS.reset}`);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// Lines changed
|
|
904
|
+
const linesAdded = status.cost?.total_lines_added || 0;
|
|
905
|
+
const linesRemoved = status.cost?.total_lines_removed || 0;
|
|
906
|
+
if (linesAdded > 0 || linesRemoved > 0) {
|
|
907
|
+
const linesStr = `${COLORS.green}+${linesAdded}${COLORS.reset} ${COLORS.red}-${linesRemoved}${COLORS.reset}`;
|
|
908
|
+
parts.push(linesStr);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// Context usage (using current_usage for accurate context window state)
|
|
912
|
+
if (status.context_window?.current_usage && status.context_window?.context_window_size) {
|
|
913
|
+
const usage = status.context_window.current_usage;
|
|
914
|
+
// Sum all token types for total context usage
|
|
915
|
+
const totalUsed =
|
|
916
|
+
(usage.input_tokens || 0) +
|
|
917
|
+
(usage.output_tokens || 0) +
|
|
918
|
+
(usage.cache_creation_input_tokens || 0) +
|
|
919
|
+
(usage.cache_read_input_tokens || 0);
|
|
920
|
+
const windowSize = status.context_window.context_window_size;
|
|
921
|
+
const percentage = Math.round((totalUsed / windowSize) * 100);
|
|
922
|
+
const usedK = (totalUsed / 1000).toFixed(1);
|
|
923
|
+
const windowK = Math.round(windowSize / 1000);
|
|
924
|
+
|
|
925
|
+
// Color based on usage: green < 50%, yellow 50-80%, red > 80%
|
|
926
|
+
let color = COLORS.green;
|
|
927
|
+
if (percentage >= 80) color = COLORS.red;
|
|
928
|
+
else if (percentage >= 50) color = COLORS.yellow;
|
|
929
|
+
|
|
930
|
+
parts.push(`${color}${usedK}k/${windowK}k (${percentage}%)${COLORS.reset}`);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
return parts.join(" | ");
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
async function outputStatusline() {
|
|
937
|
+
// Read and parse stdin from Claude Code
|
|
938
|
+
const status = await readStdinJson();
|
|
719
939
|
|
|
720
940
|
try {
|
|
721
941
|
const config = loadConfig();
|
|
722
942
|
const parts: string[] = [];
|
|
723
943
|
|
|
724
|
-
//
|
|
725
|
-
if (
|
|
726
|
-
|
|
944
|
+
// Add session info from Claude Code
|
|
945
|
+
if (status) {
|
|
946
|
+
const sessionInfo = formatSessionInfo(status);
|
|
947
|
+
if (sessionInfo) {
|
|
948
|
+
parts.push(sessionInfo);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// Check for health reminder (water, stretch, eye break)
|
|
953
|
+
const reminder = getRandomReminder(config);
|
|
954
|
+
if (reminder) {
|
|
955
|
+
parts.push(reminder);
|
|
727
956
|
}
|
|
728
957
|
|
|
729
958
|
// Get calendar events
|
|
@@ -731,6 +960,12 @@ async function outputStatusline() {
|
|
|
731
960
|
const events = await getUpcomingEvents(config);
|
|
732
961
|
const event = getCurrentOrNextEvent(events);
|
|
733
962
|
|
|
963
|
+
// Check for meeting warning (within 5 minutes)
|
|
964
|
+
const meetingWarning = getMeetingWarning(event);
|
|
965
|
+
if (meetingWarning) {
|
|
966
|
+
parts.push(meetingWarning);
|
|
967
|
+
}
|
|
968
|
+
|
|
734
969
|
if (event) {
|
|
735
970
|
parts.push(formatEvent(event, config));
|
|
736
971
|
} else if (parts.length === 0) {
|