@naarang/glancebar 1.0.1 → 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.
Files changed (3) hide show
  1. package/README.md +46 -16
  2. package/package.json +1 -1
  3. package/src/cli.ts +248 -34
package/README.md CHANGED
@@ -3,16 +3,17 @@
3
3
  [![npm version](https://img.shields.io/npm/v/%40naarang%2Fglancebar)](https://www.npmjs.com/package/@naarang/glancebar)
4
4
  [![license](https://img.shields.io/github/license/vishal-android-freak/glancebar)](https://github.com/vishal-android-freak/glancebar/blob/main/LICENSE)
5
5
 
6
- A customizable statusline for [Claude Code](https://claude.ai/claude-code) - display calendar events, tasks, and more at a glance.
6
+ A customizable statusline for [Claude Code](https://claude.com/product/claude-code) - display calendar events, tasks, and more at a glance.
7
7
 
8
8
  ## Features
9
9
 
10
- - Display upcoming calendar events from multiple Google accounts
11
- - Color-coded events per account
12
- - Countdown display for imminent events
13
- - Water break reminders to stay hydrated
14
- - Fully configurable via CLI
15
- - Cross-platform support (Windows, macOS, Linux)
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
 
@@ -47,7 +48,7 @@ npm install -g @naarang/glancebar
47
48
  # 1. Run setup guide
48
49
  glancebar setup
49
50
 
50
- # 2. Add your Google account
51
+ # 2. Add your Google account (after setting up credentials)
51
52
  glancebar auth --add your-email@gmail.com
52
53
 
53
54
  # 3. Test it
@@ -138,11 +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 water reminders
142
+ # Enable/disable health reminders
142
143
  glancebar config --water-reminder true
143
-
144
- # Set water reminder interval (in minutes)
145
- glancebar config --water-interval 45
144
+ glancebar config --stretch-reminder true
145
+ glancebar config --eye-reminder true
146
146
 
147
147
  # Reset to defaults
148
148
  glancebar config --reset
@@ -150,15 +150,44 @@ glancebar config --reset
150
150
 
151
151
  ## Display Format
152
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
+
153
176
  | State | Format | Example |
154
177
  |-------|--------|---------|
178
+ | **Meeting warning** | Red alert when ≤5m away | `Meeting in 3m - wrap up!` |
155
179
  | Upcoming (within threshold) | `In Xm: Title (account)` | `In 15m: Team Standup (work)` |
156
180
  | Current | `Now: Title (account)` | `Now: Team Standup (work)` |
157
181
  | Later | `HH:MM AM/PM: Title (account)` | `2:30 PM: Meeting (work)` |
158
182
  | No events | `No upcoming events` | |
159
- | Water reminder | Random hydration message | `Stay hydrated! Drink some water` |
160
183
 
161
- Events are color-coded by account (cyan, magenta, green, orange, blue, pink, yellow, purple).
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` |
162
191
 
163
192
  ## Configuration
164
193
 
@@ -180,8 +209,9 @@ All configuration is stored in `~/.glancebar/`:
180
209
  | `countdownThresholdMinutes` | 60 | Minutes threshold for countdown display |
181
210
  | `maxTitleLength` | 120 | Maximum event title length |
182
211
  | `showCalendarName` | true | Show account name after event |
183
- | `waterReminderEnabled` | false | Enable water break reminders |
184
- | `waterReminderIntervalMinutes` | 45 | Minutes between water 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) |
185
215
 
186
216
  ## Building from Source
187
217
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naarang/glancebar",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "A customizable statusline for Claude Code - display calendar events, tasks, and more at a glance",
5
5
  "author": "Vishal Dubey",
6
6
  "license": "MIT",
package/src/cli.ts CHANGED
@@ -16,7 +16,8 @@ interface Config {
16
16
  countdownThresholdMinutes: number;
17
17
  maxTitleLength: number;
18
18
  waterReminderEnabled: boolean;
19
- waterReminderIntervalMinutes: number;
19
+ stretchReminderEnabled: boolean;
20
+ eyeReminderEnabled: boolean;
20
21
  }
21
22
 
22
23
  const COLORS: Record<string, string> = {
@@ -48,7 +49,8 @@ const DEFAULT_CONFIG: Config = {
48
49
  countdownThresholdMinutes: 60,
49
50
  maxTitleLength: 120,
50
51
  waterReminderEnabled: true,
51
- waterReminderIntervalMinutes: 30,
52
+ stretchReminderEnabled: true,
53
+ eyeReminderEnabled: true,
52
54
  };
53
55
 
54
56
  const WATER_REMINDERS = [
@@ -62,6 +64,26 @@ const WATER_REMINDERS = [
62
64
  "Quick reminder: Drink water!",
63
65
  ];
64
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
+
65
87
  function getConfigDir(): string {
66
88
  const home = process.env.HOME || process.env.USERPROFILE || "";
67
89
  return join(home, ".glancebar");
@@ -75,10 +97,6 @@ function getTokensDir(): string {
75
97
  return join(getConfigDir(), "tokens");
76
98
  }
77
99
 
78
- function getCredentialsPath(): string {
79
- return join(getConfigDir(), "credentials.json");
80
- }
81
-
82
100
  function ensureConfigDir(): void {
83
101
  const dir = getConfigDir();
84
102
  if (!existsSync(dir)) {
@@ -118,11 +136,15 @@ interface Credentials {
118
136
  web?: { client_id: string; client_secret: string };
119
137
  }
120
138
 
139
+ function getCredentialsPath(): string {
140
+ return join(getConfigDir(), "credentials.json");
141
+ }
142
+
121
143
  function loadCredentials(): Credentials {
122
144
  const credPath = getCredentialsPath();
123
145
  if (!existsSync(credPath)) {
124
146
  throw new Error(
125
- `credentials.json not found at ${credPath}\n\nPlease download OAuth credentials from Google Cloud Console and save to:\n${credPath}`
147
+ `credentials.json not found at ${credPath}\n\nPlease download OAuth credentials from Google Cloud Console and save to:\n${credPath}\n\nRun 'glancebar setup' for detailed instructions.`
126
148
  );
127
149
  }
128
150
  return JSON.parse(readFileSync(credPath, "utf-8"));
@@ -412,6 +434,20 @@ function formatCountdown(minutes: number): string {
412
434
  return mins === 0 ? `In ${hours}h` : `In ${hours}h${mins}m`;
413
435
  }
414
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
+
415
451
  function formatTime(date: Date): string {
416
452
  const hours = date.getHours();
417
453
  const minutes = date.getMinutes();
@@ -442,14 +478,15 @@ Usage:
442
478
  glancebar config --max-title <length> Set max title length (default: 120)
443
479
  glancebar config --show-calendar <true|false> Show calendar name (default: true)
444
480
  glancebar config --water-reminder <true|false> Enable/disable water reminders (default: true)
445
- glancebar config --water-interval <mins> Set water reminder interval (default: 30)
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)
446
483
  glancebar config --reset Reset to default configuration
447
484
  glancebar setup Show setup instructions
448
485
 
449
486
  Examples:
450
487
  glancebar auth --add user@gmail.com
451
488
  glancebar config --lookahead 12
452
- glancebar config --water-interval 45
489
+ glancebar config --stretch-reminder false
453
490
 
454
491
  Config location: ${getConfigDir()}
455
492
  `);
@@ -684,17 +721,31 @@ function handleConfig(args: string[]) {
684
721
  return;
685
722
  }
686
723
 
687
- // Handle --water-interval
688
- const waterIntervalIndex = args.indexOf("--water-interval");
689
- if (waterIntervalIndex !== -1) {
690
- const value = parseInt(args[waterIntervalIndex + 1], 10);
691
- if (isNaN(value) || value < 5 || value > 120) {
692
- console.error("Error: water-interval must be between 5 and 120 minutes");
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'");
693
744
  process.exit(1);
694
745
  }
695
- config.waterReminderIntervalMinutes = value;
746
+ config.eyeReminderEnabled = value === "true";
696
747
  saveConfig(config);
697
- console.log(`Water reminder interval set to ${value} minutes`);
748
+ console.log(`Eye break reminder ${value === "true" ? "enabled" : "disabled"}`);
698
749
  return;
699
750
  }
700
751
 
@@ -713,38 +764,195 @@ Calendar Settings:
713
764
 
714
765
  Reminders:
715
766
  Water reminder: ${config.waterReminderEnabled ? "enabled" : "disabled"}
716
- Water interval: ${config.waterReminderIntervalMinutes} minutes
767
+ Stretch reminder: ${config.stretchReminderEnabled ? "enabled" : "disabled"}
768
+ Eye break reminder: ${config.eyeReminderEnabled ? "enabled" : "disabled"}
717
769
  `);
718
770
  }
719
771
 
720
- function shouldShowWaterReminder(config: Config): boolean {
721
- if (!config.waterReminderEnabled) return false;
772
+ function getRandomReminder(config: Config): string | null {
773
+ const enabledReminders: Array<() => string> = [];
722
774
 
723
- const now = new Date();
724
- const minutes = now.getHours() * 60 + now.getMinutes();
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;
725
797
 
726
- // Show water reminder if current minute falls on the interval
727
- return minutes % config.waterReminderIntervalMinutes === 0;
798
+ // ~30% chance to show any reminder
799
+ if (Math.random() >= 0.3) return null;
800
+
801
+ // Pick a random reminder type from enabled ones
802
+ const randomPicker = enabledReminders[Math.floor(Math.random() * enabledReminders.length)];
803
+ return randomPicker();
728
804
  }
729
805
 
730
- function getWaterReminder(): string {
731
- const reminder = WATER_REMINDERS[Math.floor(Math.random() * WATER_REMINDERS.length)];
732
- return `${COLORS.brightCyan}${reminder}${COLORS.reset}`;
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
+ };
733
826
  }
734
827
 
735
- async function outputStatusline() {
736
- // Consume stdin (Claude Code sends JSON)
828
+ function getGitBranch(cwd?: string): string | null {
737
829
  try {
738
- for await (const _ of Bun.stdin.stream()) break;
739
- } catch {}
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();
740
939
 
741
940
  try {
742
941
  const config = loadConfig();
743
942
  const parts: string[] = [];
744
943
 
745
- // Check for water reminder first
746
- if (shouldShowWaterReminder(config)) {
747
- parts.push(getWaterReminder());
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);
748
956
  }
749
957
 
750
958
  // Get calendar events
@@ -752,6 +960,12 @@ async function outputStatusline() {
752
960
  const events = await getUpcomingEvents(config);
753
961
  const event = getCurrentOrNextEvent(events);
754
962
 
963
+ // Check for meeting warning (within 5 minutes)
964
+ const meetingWarning = getMeetingWarning(event);
965
+ if (meetingWarning) {
966
+ parts.push(meetingWarning);
967
+ }
968
+
755
969
  if (event) {
756
970
  parts.push(formatEvent(event, config));
757
971
  } else if (parts.length === 0) {