@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.
Files changed (3) hide show
  1. package/README.md +44 -10
  2. package/package.json +1 -1
  3. 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
- - 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
 
@@ -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 water reminders
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
- 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` |
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 (~30% chance) |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naarang/glancebar",
3
- "version": "1.0.2",
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,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 --water-reminder true
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 shouldShowWaterReminder(config: Config): boolean {
703
- if (!config.waterReminderEnabled) return false;
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
- // ~30% chance to show water reminder
706
- return Math.random() < 0.3;
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
- function getWaterReminder(): string {
710
- const reminder = WATER_REMINDERS[Math.floor(Math.random() * WATER_REMINDERS.length)];
711
- 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
+ };
712
826
  }
713
827
 
714
- async function outputStatusline() {
715
- // Consume stdin (Claude Code sends JSON)
828
+ function getGitBranch(cwd?: string): string | null {
716
829
  try {
717
- for await (const _ of Bun.stdin.stream()) break;
718
- } 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();
719
939
 
720
940
  try {
721
941
  const config = loadConfig();
722
942
  const parts: string[] = [];
723
943
 
724
- // Check for water reminder first
725
- if (shouldShowWaterReminder(config)) {
726
- 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);
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) {