@naarang/glancebar 1.0.0
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/LICENSE +21 -0
- package/README.md +230 -0
- package/package.json +63 -0
- package/src/cli.ts +813 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Vishal Dubey
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# @naarang/glancebar
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@naarang/glancebar)
|
|
4
|
+
[](https://github.com/vishal-android-freak/glancebar/blob/main/LICENSE)
|
|
5
|
+
|
|
6
|
+
A customizable statusline for [Claude Code](https://claude.ai/claude-code) - display calendar events, tasks, and more at a glance.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
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)
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
|
|
19
|
+
- [Bun](https://bun.sh/) >= 1.0.0
|
|
20
|
+
- Google Cloud project with Calendar API enabled
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
### Using bunx (recommended)
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
bunx @naarang/glancebar --help
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Global installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
bun install -g @naarang/glancebar
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Using npm
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx @naarang/glancebar --help
|
|
40
|
+
# or
|
|
41
|
+
npm install -g @naarang/glancebar
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quick Start
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# 1. Run setup guide
|
|
48
|
+
glancebar setup
|
|
49
|
+
|
|
50
|
+
# 2. Add your Google account
|
|
51
|
+
glancebar auth --add your-email@gmail.com
|
|
52
|
+
|
|
53
|
+
# 3. Test it
|
|
54
|
+
glancebar
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Setup
|
|
58
|
+
|
|
59
|
+
### 1. Create Google Cloud Project
|
|
60
|
+
|
|
61
|
+
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
|
62
|
+
2. Create a new project or select an existing one
|
|
63
|
+
3. Enable the **Google Calendar API**:
|
|
64
|
+
- Go to "APIs & Services" > "Library"
|
|
65
|
+
- Search for "Google Calendar API" and enable it
|
|
66
|
+
|
|
67
|
+
### 2. Create OAuth Credentials
|
|
68
|
+
|
|
69
|
+
1. Go to "APIs & Services" > "Credentials"
|
|
70
|
+
2. Click "Create Credentials" > "OAuth client ID"
|
|
71
|
+
3. Select "Desktop app" as application type
|
|
72
|
+
4. Download the JSON file
|
|
73
|
+
5. Rename to `credentials.json` and save to `~/.glancebar/credentials.json`
|
|
74
|
+
|
|
75
|
+
### 3. Add Redirect URI
|
|
76
|
+
|
|
77
|
+
In Google Cloud Console, edit your OAuth client and add:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
http://localhost:3000/callback
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 4. Add Accounts
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
glancebar auth --add your-email@gmail.com
|
|
87
|
+
glancebar auth --add work@company.com
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 5. Configure Claude Code
|
|
91
|
+
|
|
92
|
+
Update `~/.claude/settings.json`:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"statusLine": {
|
|
97
|
+
"type": "command",
|
|
98
|
+
"command": "bunx @naarang/glancebar",
|
|
99
|
+
"padding": 0
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Usage
|
|
105
|
+
|
|
106
|
+
### Statusline Output
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
glancebar
|
|
110
|
+
# Output: In 15m: Team Standup (work)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Commands
|
|
114
|
+
|
|
115
|
+
| Command | Description |
|
|
116
|
+
|---------|-------------|
|
|
117
|
+
| `glancebar` | Display statusline output |
|
|
118
|
+
| `glancebar auth` | Re-authenticate all accounts |
|
|
119
|
+
| `glancebar auth --add <email>` | Add a new account |
|
|
120
|
+
| `glancebar auth --remove <email>` | Remove an account |
|
|
121
|
+
| `glancebar auth --list` | List all accounts |
|
|
122
|
+
| `glancebar config` | Show current configuration |
|
|
123
|
+
| `glancebar setup` | Show setup instructions |
|
|
124
|
+
| `glancebar --help` | Show help |
|
|
125
|
+
|
|
126
|
+
### Configuration Options
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
# Set lookahead hours (how far ahead to look for events)
|
|
130
|
+
glancebar config --lookahead 12
|
|
131
|
+
|
|
132
|
+
# Set countdown threshold (show "In Xm" instead of time)
|
|
133
|
+
glancebar config --countdown-threshold 30
|
|
134
|
+
|
|
135
|
+
# Set max title length
|
|
136
|
+
glancebar config --max-title 80
|
|
137
|
+
|
|
138
|
+
# Toggle calendar name display
|
|
139
|
+
glancebar config --show-calendar false
|
|
140
|
+
|
|
141
|
+
# Enable/disable water reminders
|
|
142
|
+
glancebar config --water-reminder true
|
|
143
|
+
|
|
144
|
+
# Set water reminder interval (in minutes)
|
|
145
|
+
glancebar config --water-interval 45
|
|
146
|
+
|
|
147
|
+
# Reset to defaults
|
|
148
|
+
glancebar config --reset
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Display Format
|
|
152
|
+
|
|
153
|
+
| State | Format | Example |
|
|
154
|
+
|-------|--------|---------|
|
|
155
|
+
| Upcoming (within threshold) | `In Xm: Title (account)` | `In 15m: Team Standup (work)` |
|
|
156
|
+
| Current | `Now: Title (account)` | `Now: Team Standup (work)` |
|
|
157
|
+
| Later | `HH:MM AM/PM: Title (account)` | `2:30 PM: Meeting (work)` |
|
|
158
|
+
| No events | `No upcoming events` | |
|
|
159
|
+
| Water reminder | Random hydration message | `Stay hydrated! Drink some water` |
|
|
160
|
+
|
|
161
|
+
Events are color-coded by account (cyan, magenta, green, orange, blue, pink, yellow, purple).
|
|
162
|
+
|
|
163
|
+
## Configuration
|
|
164
|
+
|
|
165
|
+
All configuration is stored in `~/.glancebar/`:
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
~/.glancebar/
|
|
169
|
+
├── config.json # User settings
|
|
170
|
+
├── credentials.json # Google OAuth credentials (you provide this)
|
|
171
|
+
└── tokens/ # OAuth tokens per account
|
|
172
|
+
└── <email>.json
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Default Settings
|
|
176
|
+
|
|
177
|
+
| Setting | Default | Description |
|
|
178
|
+
|---------|---------|-------------|
|
|
179
|
+
| `lookaheadHours` | 8 | Hours ahead to look for events |
|
|
180
|
+
| `countdownThresholdMinutes` | 60 | Minutes threshold for countdown display |
|
|
181
|
+
| `maxTitleLength` | 120 | Maximum event title length |
|
|
182
|
+
| `showCalendarName` | true | Show account name after event |
|
|
183
|
+
| `waterReminderEnabled` | false | Enable water break reminders |
|
|
184
|
+
| `waterReminderIntervalMinutes` | 45 | Minutes between water reminders |
|
|
185
|
+
|
|
186
|
+
## Building from Source
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
# Clone the repository
|
|
190
|
+
git clone https://github.com/vishal-android-freak/glancebar.git
|
|
191
|
+
cd glancebar
|
|
192
|
+
|
|
193
|
+
# Install dependencies
|
|
194
|
+
bun install
|
|
195
|
+
|
|
196
|
+
# Run locally
|
|
197
|
+
bun run dev
|
|
198
|
+
|
|
199
|
+
# Build binaries for all platforms
|
|
200
|
+
bun run build:all
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Build Targets
|
|
204
|
+
|
|
205
|
+
| Platform | Command |
|
|
206
|
+
|----------|---------|
|
|
207
|
+
| Linux x64 | `bun run build:linux-x64` |
|
|
208
|
+
| Linux ARM64 | `bun run build:linux-arm64` |
|
|
209
|
+
| macOS x64 | `bun run build:darwin-x64` |
|
|
210
|
+
| macOS ARM64 | `bun run build:darwin-arm64` |
|
|
211
|
+
| Windows x64 | `bun run build:win-x64` |
|
|
212
|
+
|
|
213
|
+
## Roadmap
|
|
214
|
+
|
|
215
|
+
- [ ] Task integration (Todoist, Google Tasks)
|
|
216
|
+
- [ ] Weather information
|
|
217
|
+
- [ ] System stats
|
|
218
|
+
- [ ] Custom modules
|
|
219
|
+
|
|
220
|
+
## Contributing
|
|
221
|
+
|
|
222
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
223
|
+
|
|
224
|
+
## Author
|
|
225
|
+
|
|
226
|
+
**Vishal Dubey** ([@vishal-android-freak](https://github.com/vishal-android-freak))
|
|
227
|
+
|
|
228
|
+
## License
|
|
229
|
+
|
|
230
|
+
[MIT](LICENSE)
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@naarang/glancebar",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A customizable statusline for Claude Code - display calendar events, tasks, and more at a glance",
|
|
5
|
+
"author": "Vishal Dubey",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"glancebar": "src/cli.ts"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src/cli.ts",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"dev": "bun run src/cli.ts",
|
|
21
|
+
"build:all": "bun run build:linux-x64 && bun run build:linux-arm64 && bun run build:darwin-x64 && bun run build:darwin-arm64 && bun run build:win-x64",
|
|
22
|
+
"build:linux-x64": "bun build src/cli.ts --compile --target=bun-linux-x64 --outfile dist/glancebar-linux-x64",
|
|
23
|
+
"build:linux-arm64": "bun build src/cli.ts --compile --target=bun-linux-arm64 --outfile dist/glancebar-linux-arm64",
|
|
24
|
+
"build:darwin-x64": "bun build src/cli.ts --compile --target=bun-darwin-x64 --outfile dist/glancebar-darwin-x64",
|
|
25
|
+
"build:darwin-arm64": "bun build src/cli.ts --compile --target=bun-darwin-arm64 --outfile dist/glancebar-darwin-arm64",
|
|
26
|
+
"build:win-x64": "bun build src/cli.ts --compile --target=bun-windows-x64 --outfile dist/glancebar-win-x64.exe"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"claude-code",
|
|
30
|
+
"statusline",
|
|
31
|
+
"glancebar",
|
|
32
|
+
"google-calendar",
|
|
33
|
+
"calendar",
|
|
34
|
+
"productivity",
|
|
35
|
+
"cli",
|
|
36
|
+
"bun"
|
|
37
|
+
],
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/vishal-android-freak/glancebar.git"
|
|
41
|
+
},
|
|
42
|
+
"os": [
|
|
43
|
+
"darwin",
|
|
44
|
+
"linux",
|
|
45
|
+
"win32"
|
|
46
|
+
],
|
|
47
|
+
"cpu": [
|
|
48
|
+
"x64",
|
|
49
|
+
"arm64"
|
|
50
|
+
],
|
|
51
|
+
"engines": {
|
|
52
|
+
"bun": ">=1.0.0"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/bun": "latest"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"typescript": "^5"
|
|
59
|
+
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"googleapis": "^170.0.0"
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,813 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { google } from "googleapis";
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "fs";
|
|
4
|
+
import { join, dirname } from "path";
|
|
5
|
+
import { createServer, Server } from "http";
|
|
6
|
+
import { createInterface } from "readline";
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Configuration
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
interface Config {
|
|
13
|
+
accounts: string[];
|
|
14
|
+
lookaheadHours: number;
|
|
15
|
+
showCalendarName: boolean;
|
|
16
|
+
countdownThresholdMinutes: number;
|
|
17
|
+
maxTitleLength: number;
|
|
18
|
+
waterReminderEnabled: boolean;
|
|
19
|
+
waterReminderIntervalMinutes: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const COLORS: Record<string, string> = {
|
|
23
|
+
reset: "\x1b[0m",
|
|
24
|
+
red: "\x1b[31m",
|
|
25
|
+
green: "\x1b[32m",
|
|
26
|
+
yellow: "\x1b[33m",
|
|
27
|
+
blue: "\x1b[34m",
|
|
28
|
+
magenta: "\x1b[35m",
|
|
29
|
+
cyan: "\x1b[36m",
|
|
30
|
+
white: "\x1b[37m",
|
|
31
|
+
brightRed: "\x1b[91m",
|
|
32
|
+
brightGreen: "\x1b[92m",
|
|
33
|
+
brightYellow: "\x1b[93m",
|
|
34
|
+
brightBlue: "\x1b[94m",
|
|
35
|
+
brightMagenta: "\x1b[95m",
|
|
36
|
+
brightCyan: "\x1b[96m",
|
|
37
|
+
orange: "\x1b[38;5;208m",
|
|
38
|
+
pink: "\x1b[38;5;213m",
|
|
39
|
+
purple: "\x1b[38;5;141m",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const ACCOUNT_COLORS = ["cyan", "magenta", "brightGreen", "orange", "brightBlue", "pink", "yellow", "purple"];
|
|
43
|
+
|
|
44
|
+
const DEFAULT_CONFIG: Config = {
|
|
45
|
+
accounts: [],
|
|
46
|
+
lookaheadHours: 8,
|
|
47
|
+
showCalendarName: true,
|
|
48
|
+
countdownThresholdMinutes: 60,
|
|
49
|
+
maxTitleLength: 120,
|
|
50
|
+
waterReminderEnabled: true,
|
|
51
|
+
waterReminderIntervalMinutes: 30,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const WATER_REMINDERS = [
|
|
55
|
+
"Stay hydrated! Drink some water",
|
|
56
|
+
"Time for a water break!",
|
|
57
|
+
"Hydration check! Grab some water",
|
|
58
|
+
"Your body needs water. Drink up!",
|
|
59
|
+
"Water break! Stay refreshed",
|
|
60
|
+
"Don't forget to drink water!",
|
|
61
|
+
"Hydrate yourself! Take a sip",
|
|
62
|
+
"Quick reminder: Drink water!",
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
function getConfigDir(): string {
|
|
66
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
67
|
+
return join(home, ".glancebar");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getConfigPath(): string {
|
|
71
|
+
return join(getConfigDir(), "config.json");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getTokensDir(): string {
|
|
75
|
+
return join(getConfigDir(), "tokens");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getCredentialsPath(): string {
|
|
79
|
+
return join(getConfigDir(), "credentials.json");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function ensureConfigDir(): void {
|
|
83
|
+
const dir = getConfigDir();
|
|
84
|
+
if (!existsSync(dir)) {
|
|
85
|
+
mkdirSync(dir, { recursive: true });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function loadConfig(): Config {
|
|
90
|
+
const configPath = getConfigPath();
|
|
91
|
+
if (!existsSync(configPath)) {
|
|
92
|
+
return { ...DEFAULT_CONFIG };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const content = readFileSync(configPath, "utf-8");
|
|
97
|
+
const userConfig = JSON.parse(content);
|
|
98
|
+
return { ...DEFAULT_CONFIG, ...userConfig };
|
|
99
|
+
} catch {
|
|
100
|
+
return { ...DEFAULT_CONFIG };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function saveConfig(config: Config): void {
|
|
105
|
+
ensureConfigDir();
|
|
106
|
+
writeFileSync(getConfigPath(), JSON.stringify(config, null, 2));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ============================================================================
|
|
110
|
+
// OAuth Authentication
|
|
111
|
+
// ============================================================================
|
|
112
|
+
|
|
113
|
+
const SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"];
|
|
114
|
+
const REDIRECT_URI = "http://localhost:3000/callback";
|
|
115
|
+
|
|
116
|
+
interface Credentials {
|
|
117
|
+
installed?: { client_id: string; client_secret: string };
|
|
118
|
+
web?: { client_id: string; client_secret: string };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function loadCredentials(): Credentials {
|
|
122
|
+
const credPath = getCredentialsPath();
|
|
123
|
+
if (!existsSync(credPath)) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`credentials.json not found at ${credPath}\n\nPlease download OAuth credentials from Google Cloud Console and save to:\n${credPath}`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
return JSON.parse(readFileSync(credPath, "utf-8"));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getTokenPath(account: string): string {
|
|
132
|
+
const safeAccount = account.replace(/[^a-zA-Z0-9@.-]/g, "_");
|
|
133
|
+
return join(getTokensDir(), `${safeAccount}.json`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function createOAuth2Client(credentials: Credentials) {
|
|
137
|
+
const { client_id, client_secret } = credentials.installed || credentials.web!;
|
|
138
|
+
return new google.auth.OAuth2(client_id, client_secret, REDIRECT_URI);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function getAuthenticatedClient(account: string) {
|
|
142
|
+
const credentials = loadCredentials();
|
|
143
|
+
const oauth2Client = createOAuth2Client(credentials);
|
|
144
|
+
const tokenPath = getTokenPath(account);
|
|
145
|
+
|
|
146
|
+
if (!existsSync(tokenPath)) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const token = JSON.parse(readFileSync(tokenPath, "utf-8"));
|
|
151
|
+
oauth2Client.setCredentials(token);
|
|
152
|
+
|
|
153
|
+
oauth2Client.on("tokens", (tokens) => {
|
|
154
|
+
const currentToken = JSON.parse(readFileSync(tokenPath, "utf-8"));
|
|
155
|
+
const updatedToken = { ...currentToken, ...tokens };
|
|
156
|
+
writeFileSync(tokenPath, JSON.stringify(updatedToken, null, 2));
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return oauth2Client;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function authenticateAccount(account: string): Promise<void> {
|
|
163
|
+
const credentials = loadCredentials();
|
|
164
|
+
const oauth2Client = createOAuth2Client(credentials);
|
|
165
|
+
|
|
166
|
+
const authUrl = oauth2Client.generateAuthUrl({
|
|
167
|
+
access_type: "offline",
|
|
168
|
+
scope: SCOPES,
|
|
169
|
+
prompt: "consent",
|
|
170
|
+
login_hint: account,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
console.log(`\nAuthenticating: ${account}`);
|
|
174
|
+
console.log(`Opening browser...`);
|
|
175
|
+
|
|
176
|
+
const code = await startServerAndGetCode(authUrl);
|
|
177
|
+
|
|
178
|
+
console.log(`Exchanging code for tokens...`);
|
|
179
|
+
|
|
180
|
+
const { tokens } = await oauth2Client.getToken(code);
|
|
181
|
+
oauth2Client.setCredentials(tokens);
|
|
182
|
+
|
|
183
|
+
const tokensDir = getTokensDir();
|
|
184
|
+
if (!existsSync(tokensDir)) {
|
|
185
|
+
mkdirSync(tokensDir, { recursive: true });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const tokenPath = getTokenPath(account);
|
|
189
|
+
writeFileSync(tokenPath, JSON.stringify(tokens, null, 2));
|
|
190
|
+
console.log(`Token saved for ${account}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function startServerAndGetCode(authUrl: string): Promise<string> {
|
|
194
|
+
return new Promise((resolve, reject) => {
|
|
195
|
+
let server: Server;
|
|
196
|
+
|
|
197
|
+
server = createServer(async (req, res) => {
|
|
198
|
+
const url = new URL(req.url!, `http://localhost:3000`);
|
|
199
|
+
|
|
200
|
+
if (!url.pathname.startsWith("/callback")) {
|
|
201
|
+
res.writeHead(404);
|
|
202
|
+
res.end("Not found");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const code = url.searchParams.get("code");
|
|
207
|
+
const error = url.searchParams.get("error");
|
|
208
|
+
|
|
209
|
+
if (error) {
|
|
210
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
211
|
+
res.end(`<html><body><h1>Authentication failed</h1><p>Error: ${error}</p></body></html>`);
|
|
212
|
+
server.close();
|
|
213
|
+
reject(new Error(error));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (code) {
|
|
218
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
219
|
+
res.end(`
|
|
220
|
+
<html>
|
|
221
|
+
<body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #1a1a2e; color: #eee;">
|
|
222
|
+
<div style="text-align: center;">
|
|
223
|
+
<h1 style="color: #4ade80;">Authentication Successful!</h1>
|
|
224
|
+
<p>You can close this window and return to the terminal.</p>
|
|
225
|
+
</div>
|
|
226
|
+
</body>
|
|
227
|
+
</html>
|
|
228
|
+
`);
|
|
229
|
+
|
|
230
|
+
setTimeout(() => {
|
|
231
|
+
server.close(() => resolve(code));
|
|
232
|
+
}, 500);
|
|
233
|
+
} else {
|
|
234
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
235
|
+
res.end("<html><body><h1>Authentication failed</h1><p>No code received.</p></body></html>");
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
server.listen(3000, () => openBrowser(authUrl));
|
|
240
|
+
|
|
241
|
+
server.on("error", (err: NodeJS.ErrnoException) => {
|
|
242
|
+
if (err.code === "EADDRINUSE") {
|
|
243
|
+
reject(new Error("Port 3000 is already in use. Please close any application using it and try again."));
|
|
244
|
+
} else {
|
|
245
|
+
reject(err);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
setTimeout(() => {
|
|
250
|
+
server.close();
|
|
251
|
+
reject(new Error("Authentication timeout (5 minutes)"));
|
|
252
|
+
}, 300000);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function openBrowser(url: string) {
|
|
257
|
+
const { exec } = require("child_process");
|
|
258
|
+
const platform = process.platform;
|
|
259
|
+
|
|
260
|
+
let command: string;
|
|
261
|
+
if (platform === "win32") {
|
|
262
|
+
command = `start "" "${url}"`;
|
|
263
|
+
} else if (platform === "darwin") {
|
|
264
|
+
command = `open "${url}"`;
|
|
265
|
+
} else {
|
|
266
|
+
command = `xdg-open "${url}"`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
exec(command, (err: Error | null) => {
|
|
270
|
+
if (err) {
|
|
271
|
+
console.log(`\nCould not open browser automatically.`);
|
|
272
|
+
console.log(`Please open this URL manually:\n${url}\n`);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ============================================================================
|
|
278
|
+
// Calendar
|
|
279
|
+
// ============================================================================
|
|
280
|
+
|
|
281
|
+
interface CalendarEvent {
|
|
282
|
+
id: string;
|
|
283
|
+
title: string;
|
|
284
|
+
start: Date;
|
|
285
|
+
end: Date;
|
|
286
|
+
isAllDay: boolean;
|
|
287
|
+
account: string;
|
|
288
|
+
accountEmail: string;
|
|
289
|
+
accountIndex: number;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function getUpcomingEvents(config: Config): Promise<CalendarEvent[]> {
|
|
293
|
+
const allEvents: CalendarEvent[] = [];
|
|
294
|
+
const now = new Date();
|
|
295
|
+
const timeMax = new Date(now.getTime() + config.lookaheadHours * 60 * 60 * 1000);
|
|
296
|
+
|
|
297
|
+
const eventPromises = config.accounts.map(async (account, accountIndex) => {
|
|
298
|
+
try {
|
|
299
|
+
const auth = getAuthenticatedClient(account);
|
|
300
|
+
if (!auth) return [];
|
|
301
|
+
|
|
302
|
+
const calendar = google.calendar({ version: "v3", auth });
|
|
303
|
+
|
|
304
|
+
const response = await calendar.events.list({
|
|
305
|
+
calendarId: "primary",
|
|
306
|
+
timeMin: now.toISOString(),
|
|
307
|
+
timeMax: timeMax.toISOString(),
|
|
308
|
+
maxResults: 10,
|
|
309
|
+
singleEvents: true,
|
|
310
|
+
orderBy: "startTime",
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const events = response.data.items || [];
|
|
314
|
+
return events.map((event) => {
|
|
315
|
+
const isAllDay = !event.start?.dateTime;
|
|
316
|
+
let start: Date, end: Date;
|
|
317
|
+
|
|
318
|
+
if (isAllDay) {
|
|
319
|
+
start = new Date(event.start?.date + "T00:00:00");
|
|
320
|
+
end = new Date(event.end?.date + "T00:00:00");
|
|
321
|
+
} else {
|
|
322
|
+
start = new Date(event.start?.dateTime!);
|
|
323
|
+
end = new Date(event.end?.dateTime!);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
id: event.id || "",
|
|
328
|
+
title: event.summary || "(No title)",
|
|
329
|
+
start,
|
|
330
|
+
end,
|
|
331
|
+
isAllDay,
|
|
332
|
+
account: extractAccountName(account),
|
|
333
|
+
accountEmail: account,
|
|
334
|
+
accountIndex,
|
|
335
|
+
};
|
|
336
|
+
});
|
|
337
|
+
} catch {
|
|
338
|
+
return [];
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const results = await Promise.all(eventPromises);
|
|
343
|
+
for (const events of results) {
|
|
344
|
+
allEvents.push(...events);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
allEvents.sort((a, b) => a.start.getTime() - b.start.getTime());
|
|
348
|
+
return allEvents;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function extractAccountName(email: string): string {
|
|
352
|
+
const atIndex = email.indexOf("@");
|
|
353
|
+
if (atIndex === -1) return email;
|
|
354
|
+
|
|
355
|
+
const domain = email.slice(atIndex + 1);
|
|
356
|
+
if (domain === "gmail.com") {
|
|
357
|
+
return email.slice(0, atIndex);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return domain.split(".")[0];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function getCurrentOrNextEvent(events: CalendarEvent[]): CalendarEvent | null {
|
|
364
|
+
const now = new Date();
|
|
365
|
+
|
|
366
|
+
for (const event of events) {
|
|
367
|
+
if (event.start <= now && event.end > now) return event;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
for (const event of events) {
|
|
371
|
+
if (event.start > now) return event;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ============================================================================
|
|
378
|
+
// Formatter
|
|
379
|
+
// ============================================================================
|
|
380
|
+
|
|
381
|
+
function formatEvent(event: CalendarEvent, config: Config): string {
|
|
382
|
+
const now = new Date();
|
|
383
|
+
const isHappening = event.start <= now && event.end > now;
|
|
384
|
+
const minutesUntil = Math.round((event.start.getTime() - now.getTime()) / 60000);
|
|
385
|
+
|
|
386
|
+
let timeStr: string;
|
|
387
|
+
if (isHappening) {
|
|
388
|
+
timeStr = "Now";
|
|
389
|
+
} else if (minutesUntil <= config.countdownThresholdMinutes && minutesUntil > 0) {
|
|
390
|
+
timeStr = formatCountdown(minutesUntil);
|
|
391
|
+
} else {
|
|
392
|
+
timeStr = formatTime(event.start);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const title = event.title.length > config.maxTitleLength
|
|
396
|
+
? event.title.slice(0, config.maxTitleLength - 1) + "…"
|
|
397
|
+
: event.title;
|
|
398
|
+
|
|
399
|
+
const colorName = ACCOUNT_COLORS[event.accountIndex % ACCOUNT_COLORS.length];
|
|
400
|
+
const color = COLORS[colorName] || COLORS.white;
|
|
401
|
+
|
|
402
|
+
if (config.showCalendarName) {
|
|
403
|
+
return `${color}${timeStr}: ${title} (${event.account})${COLORS.reset}`;
|
|
404
|
+
}
|
|
405
|
+
return `${color}${timeStr}: ${title}${COLORS.reset}`;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function formatCountdown(minutes: number): string {
|
|
409
|
+
if (minutes < 60) return `In ${minutes}m`;
|
|
410
|
+
const hours = Math.floor(minutes / 60);
|
|
411
|
+
const mins = minutes % 60;
|
|
412
|
+
return mins === 0 ? `In ${hours}h` : `In ${hours}h${mins}m`;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function formatTime(date: Date): string {
|
|
416
|
+
const hours = date.getHours();
|
|
417
|
+
const minutes = date.getMinutes();
|
|
418
|
+
const isPM = hours >= 12;
|
|
419
|
+
const hour12 = hours % 12 || 12;
|
|
420
|
+
return `${hour12}:${minutes.toString().padStart(2, "0")} ${isPM ? "PM" : "AM"}`;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ============================================================================
|
|
424
|
+
// CLI Commands
|
|
425
|
+
// ============================================================================
|
|
426
|
+
|
|
427
|
+
function printHelp() {
|
|
428
|
+
console.log(`
|
|
429
|
+
glancebar - A customizable statusline for Claude Code
|
|
430
|
+
|
|
431
|
+
Display calendar events, tasks, and more at a glance.
|
|
432
|
+
|
|
433
|
+
Usage:
|
|
434
|
+
glancebar Output statusline (for Claude Code)
|
|
435
|
+
glancebar auth Authenticate all configured accounts
|
|
436
|
+
glancebar auth --add <email> Add and authenticate a new account
|
|
437
|
+
glancebar auth --remove <email> Remove an account
|
|
438
|
+
glancebar auth --list List configured accounts
|
|
439
|
+
glancebar config Show current configuration
|
|
440
|
+
glancebar config --lookahead <hours> Set lookahead hours (default: 8)
|
|
441
|
+
glancebar config --countdown-threshold <mins> Set countdown threshold in minutes (default: 60)
|
|
442
|
+
glancebar config --max-title <length> Set max title length (default: 120)
|
|
443
|
+
glancebar config --show-calendar <true|false> Show calendar name (default: true)
|
|
444
|
+
glancebar config --water-reminder <true|false> Enable/disable water reminders (default: true)
|
|
445
|
+
glancebar config --water-interval <mins> Set water reminder interval (default: 30)
|
|
446
|
+
glancebar config --reset Reset to default configuration
|
|
447
|
+
glancebar setup Show setup instructions
|
|
448
|
+
|
|
449
|
+
Examples:
|
|
450
|
+
glancebar auth --add user@gmail.com
|
|
451
|
+
glancebar config --lookahead 12
|
|
452
|
+
glancebar config --water-interval 45
|
|
453
|
+
|
|
454
|
+
Config location: ${getConfigDir()}
|
|
455
|
+
`);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function printSetup() {
|
|
459
|
+
console.log(`
|
|
460
|
+
Glancebar - Setup Instructions
|
|
461
|
+
==============================
|
|
462
|
+
|
|
463
|
+
Step 1: Create Google Cloud Project
|
|
464
|
+
- Go to https://console.cloud.google.com/
|
|
465
|
+
- Create a new project or select existing one
|
|
466
|
+
|
|
467
|
+
Step 2: Enable Google Calendar API
|
|
468
|
+
- Go to "APIs & Services" > "Library"
|
|
469
|
+
- Search for "Google Calendar API" and enable it
|
|
470
|
+
|
|
471
|
+
Step 3: Create OAuth Credentials
|
|
472
|
+
- Go to "APIs & Services" > "Credentials"
|
|
473
|
+
- Click "Create Credentials" > "OAuth client ID"
|
|
474
|
+
- Select "Desktop app" as application type
|
|
475
|
+
- Download the JSON file
|
|
476
|
+
|
|
477
|
+
Step 4: Save credentials
|
|
478
|
+
- Rename downloaded file to "credentials.json"
|
|
479
|
+
- Save it to: ${getCredentialsPath()}
|
|
480
|
+
|
|
481
|
+
Step 5: Add redirect URI
|
|
482
|
+
- In Google Cloud Console, edit your OAuth client
|
|
483
|
+
- Add redirect URI: http://localhost:3000/callback
|
|
484
|
+
|
|
485
|
+
Step 6: Add your Google accounts
|
|
486
|
+
glancebar auth --add your-email@gmail.com
|
|
487
|
+
glancebar auth --add work@company.com
|
|
488
|
+
|
|
489
|
+
Step 7: Configure Claude Code statusline
|
|
490
|
+
Update ~/.claude/settings.json:
|
|
491
|
+
{
|
|
492
|
+
"statusLine": {
|
|
493
|
+
"type": "command",
|
|
494
|
+
"command": "bunx @naarang/glancebar",
|
|
495
|
+
"padding": 0
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
For more info: https://github.com/vishal-android-freak/glancebar
|
|
500
|
+
`);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function handleAuth(args: string[]) {
|
|
504
|
+
// Handle --list
|
|
505
|
+
if (args.includes("--list")) {
|
|
506
|
+
const config = loadConfig();
|
|
507
|
+
if (config.accounts.length === 0) {
|
|
508
|
+
console.log("No accounts configured.");
|
|
509
|
+
} else {
|
|
510
|
+
console.log("Configured accounts:");
|
|
511
|
+
config.accounts.forEach((acc, i) => {
|
|
512
|
+
const tokenPath = getTokenPath(acc);
|
|
513
|
+
const status = existsSync(tokenPath) ? "authenticated" : "not authenticated";
|
|
514
|
+
console.log(` ${i + 1}. ${acc} (${status})`);
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Handle --add
|
|
521
|
+
const addIndex = args.indexOf("--add");
|
|
522
|
+
if (addIndex !== -1) {
|
|
523
|
+
const email = args[addIndex + 1];
|
|
524
|
+
if (!email || email.startsWith("--")) {
|
|
525
|
+
console.error("Error: Please provide an email address after --add");
|
|
526
|
+
process.exit(1);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (!email.includes("@")) {
|
|
530
|
+
console.error("Error: Invalid email address");
|
|
531
|
+
process.exit(1);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const config = loadConfig();
|
|
535
|
+
if (config.accounts.includes(email)) {
|
|
536
|
+
console.log(`Account ${email} already exists. Re-authenticating...`);
|
|
537
|
+
} else {
|
|
538
|
+
config.accounts.push(email);
|
|
539
|
+
saveConfig(config);
|
|
540
|
+
console.log(`Added ${email} to accounts.`);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
await authenticateAccount(email);
|
|
544
|
+
console.log("\nDone!");
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Handle --remove
|
|
549
|
+
const removeIndex = args.indexOf("--remove");
|
|
550
|
+
if (removeIndex !== -1) {
|
|
551
|
+
const email = args[removeIndex + 1];
|
|
552
|
+
if (!email || email.startsWith("--")) {
|
|
553
|
+
console.error("Error: Please provide an email address after --remove");
|
|
554
|
+
process.exit(1);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const config = loadConfig();
|
|
558
|
+
const idx = config.accounts.indexOf(email);
|
|
559
|
+
if (idx === -1) {
|
|
560
|
+
console.error(`Error: Account ${email} not found.`);
|
|
561
|
+
process.exit(1);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
config.accounts.splice(idx, 1);
|
|
565
|
+
saveConfig(config);
|
|
566
|
+
|
|
567
|
+
const tokenPath = getTokenPath(email);
|
|
568
|
+
if (existsSync(tokenPath)) {
|
|
569
|
+
unlinkSync(tokenPath);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
console.log(`Removed ${email} from accounts.`);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Default: authenticate all accounts
|
|
577
|
+
const config = loadConfig();
|
|
578
|
+
|
|
579
|
+
if (config.accounts.length === 0) {
|
|
580
|
+
console.log("No accounts configured.\n");
|
|
581
|
+
console.log("Add an account using:");
|
|
582
|
+
console.log(" glancebar auth --add your-email@gmail.com\n");
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
console.log("Glancebar - Google Calendar Authentication");
|
|
587
|
+
console.log("==========================================\n");
|
|
588
|
+
|
|
589
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
590
|
+
const prompt = (q: string): Promise<string> => new Promise((r) => rl.question(q, r));
|
|
591
|
+
|
|
592
|
+
for (const account of config.accounts) {
|
|
593
|
+
const tokenPath = getTokenPath(account);
|
|
594
|
+
if (existsSync(tokenPath)) {
|
|
595
|
+
console.log(`${account}: Already authenticated`);
|
|
596
|
+
const answer = await prompt(`Re-authenticate ${account}? (y/N): `);
|
|
597
|
+
if (answer.toLowerCase() !== "y") continue;
|
|
598
|
+
}
|
|
599
|
+
await authenticateAccount(account);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
rl.close();
|
|
603
|
+
console.log("\nAll accounts authenticated!");
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function handleConfig(args: string[]) {
|
|
607
|
+
const config = loadConfig();
|
|
608
|
+
|
|
609
|
+
// Handle --reset
|
|
610
|
+
if (args.includes("--reset")) {
|
|
611
|
+
const accounts = config.accounts; // Preserve accounts
|
|
612
|
+
saveConfig({ ...DEFAULT_CONFIG, accounts });
|
|
613
|
+
console.log("Configuration reset to defaults (accounts preserved).");
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Handle --lookahead
|
|
618
|
+
const lookaheadIndex = args.indexOf("--lookahead");
|
|
619
|
+
if (lookaheadIndex !== -1) {
|
|
620
|
+
const value = parseInt(args[lookaheadIndex + 1], 10);
|
|
621
|
+
if (isNaN(value) || value < 1 || value > 168) {
|
|
622
|
+
console.error("Error: lookahead must be between 1 and 168 hours");
|
|
623
|
+
process.exit(1);
|
|
624
|
+
}
|
|
625
|
+
config.lookaheadHours = value;
|
|
626
|
+
saveConfig(config);
|
|
627
|
+
console.log(`Lookahead hours set to ${value}`);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Handle --countdown-threshold
|
|
632
|
+
const countdownIndex = args.indexOf("--countdown-threshold");
|
|
633
|
+
if (countdownIndex !== -1) {
|
|
634
|
+
const value = parseInt(args[countdownIndex + 1], 10);
|
|
635
|
+
if (isNaN(value) || value < 0 || value > 1440) {
|
|
636
|
+
console.error("Error: countdown-threshold must be between 0 and 1440 minutes");
|
|
637
|
+
process.exit(1);
|
|
638
|
+
}
|
|
639
|
+
config.countdownThresholdMinutes = value;
|
|
640
|
+
saveConfig(config);
|
|
641
|
+
console.log(`Countdown threshold set to ${value} minutes`);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Handle --max-title
|
|
646
|
+
const maxTitleIndex = args.indexOf("--max-title");
|
|
647
|
+
if (maxTitleIndex !== -1) {
|
|
648
|
+
const value = parseInt(args[maxTitleIndex + 1], 10);
|
|
649
|
+
if (isNaN(value) || value < 10 || value > 500) {
|
|
650
|
+
console.error("Error: max-title must be between 10 and 500");
|
|
651
|
+
process.exit(1);
|
|
652
|
+
}
|
|
653
|
+
config.maxTitleLength = value;
|
|
654
|
+
saveConfig(config);
|
|
655
|
+
console.log(`Max title length set to ${value}`);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Handle --show-calendar
|
|
660
|
+
const showCalIndex = args.indexOf("--show-calendar");
|
|
661
|
+
if (showCalIndex !== -1) {
|
|
662
|
+
const value = args[showCalIndex + 1]?.toLowerCase();
|
|
663
|
+
if (value !== "true" && value !== "false") {
|
|
664
|
+
console.error("Error: --show-calendar must be 'true' or 'false'");
|
|
665
|
+
process.exit(1);
|
|
666
|
+
}
|
|
667
|
+
config.showCalendarName = value === "true";
|
|
668
|
+
saveConfig(config);
|
|
669
|
+
console.log(`Show calendar name set to ${value}`);
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Handle --water-reminder
|
|
674
|
+
const waterReminderIndex = args.indexOf("--water-reminder");
|
|
675
|
+
if (waterReminderIndex !== -1) {
|
|
676
|
+
const value = args[waterReminderIndex + 1]?.toLowerCase();
|
|
677
|
+
if (value !== "true" && value !== "false") {
|
|
678
|
+
console.error("Error: --water-reminder must be 'true' or 'false'");
|
|
679
|
+
process.exit(1);
|
|
680
|
+
}
|
|
681
|
+
config.waterReminderEnabled = value === "true";
|
|
682
|
+
saveConfig(config);
|
|
683
|
+
console.log(`Water reminder ${value === "true" ? "enabled" : "disabled"}`);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
|
|
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");
|
|
693
|
+
process.exit(1);
|
|
694
|
+
}
|
|
695
|
+
config.waterReminderIntervalMinutes = value;
|
|
696
|
+
saveConfig(config);
|
|
697
|
+
console.log(`Water reminder interval set to ${value} minutes`);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Show current config
|
|
702
|
+
console.log(`
|
|
703
|
+
Glancebar Configuration
|
|
704
|
+
=======================
|
|
705
|
+
Config directory: ${getConfigDir()}
|
|
706
|
+
Accounts: ${config.accounts.length > 0 ? config.accounts.join(", ") : "(none)"}
|
|
707
|
+
|
|
708
|
+
Calendar Settings:
|
|
709
|
+
Lookahead hours: ${config.lookaheadHours}
|
|
710
|
+
Countdown threshold: ${config.countdownThresholdMinutes} minutes
|
|
711
|
+
Max title length: ${config.maxTitleLength}
|
|
712
|
+
Show calendar name: ${config.showCalendarName}
|
|
713
|
+
|
|
714
|
+
Reminders:
|
|
715
|
+
Water reminder: ${config.waterReminderEnabled ? "enabled" : "disabled"}
|
|
716
|
+
Water interval: ${config.waterReminderIntervalMinutes} minutes
|
|
717
|
+
`);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function shouldShowWaterReminder(config: Config): boolean {
|
|
721
|
+
if (!config.waterReminderEnabled) return false;
|
|
722
|
+
|
|
723
|
+
const now = new Date();
|
|
724
|
+
const minutes = now.getHours() * 60 + now.getMinutes();
|
|
725
|
+
|
|
726
|
+
// Show water reminder if current minute falls on the interval
|
|
727
|
+
return minutes % config.waterReminderIntervalMinutes === 0;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function getWaterReminder(): string {
|
|
731
|
+
const reminder = WATER_REMINDERS[Math.floor(Math.random() * WATER_REMINDERS.length)];
|
|
732
|
+
return `${COLORS.brightCyan}${reminder}${COLORS.reset}`;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
async function outputStatusline() {
|
|
736
|
+
// Consume stdin (Claude Code sends JSON)
|
|
737
|
+
try {
|
|
738
|
+
for await (const _ of Bun.stdin.stream()) break;
|
|
739
|
+
} catch {}
|
|
740
|
+
|
|
741
|
+
try {
|
|
742
|
+
const config = loadConfig();
|
|
743
|
+
const parts: string[] = [];
|
|
744
|
+
|
|
745
|
+
// Check for water reminder first
|
|
746
|
+
if (shouldShowWaterReminder(config)) {
|
|
747
|
+
parts.push(getWaterReminder());
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Get calendar events
|
|
751
|
+
if (config.accounts.length > 0) {
|
|
752
|
+
const events = await getUpcomingEvents(config);
|
|
753
|
+
const event = getCurrentOrNextEvent(events);
|
|
754
|
+
|
|
755
|
+
if (event) {
|
|
756
|
+
parts.push(formatEvent(event, config));
|
|
757
|
+
} else if (parts.length === 0) {
|
|
758
|
+
parts.push("No upcoming events");
|
|
759
|
+
}
|
|
760
|
+
} else if (parts.length === 0) {
|
|
761
|
+
parts.push("No accounts configured");
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
console.log(parts.join(" | "));
|
|
765
|
+
} catch {
|
|
766
|
+
console.log("Calendar unavailable");
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// ============================================================================
|
|
771
|
+
// Main
|
|
772
|
+
// ============================================================================
|
|
773
|
+
|
|
774
|
+
async function main() {
|
|
775
|
+
const args = process.argv.slice(2);
|
|
776
|
+
const command = args[0];
|
|
777
|
+
|
|
778
|
+
if (!command) {
|
|
779
|
+
// Default: output statusline
|
|
780
|
+
await outputStatusline();
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
switch (command) {
|
|
785
|
+
case "help":
|
|
786
|
+
case "--help":
|
|
787
|
+
case "-h":
|
|
788
|
+
printHelp();
|
|
789
|
+
break;
|
|
790
|
+
|
|
791
|
+
case "setup":
|
|
792
|
+
printSetup();
|
|
793
|
+
break;
|
|
794
|
+
|
|
795
|
+
case "auth":
|
|
796
|
+
await handleAuth(args.slice(1));
|
|
797
|
+
break;
|
|
798
|
+
|
|
799
|
+
case "config":
|
|
800
|
+
handleConfig(args.slice(1));
|
|
801
|
+
break;
|
|
802
|
+
|
|
803
|
+
default:
|
|
804
|
+
console.error(`Unknown command: ${command}`);
|
|
805
|
+
console.error("Run 'glancebar --help' for usage.");
|
|
806
|
+
process.exit(1);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
main().catch((err) => {
|
|
811
|
+
console.error("Error:", err.message);
|
|
812
|
+
process.exit(1);
|
|
813
|
+
});
|