@naarang/glancebar 1.0.5 → 1.0.7

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 +93 -18
  2. package/package.json +1 -1
  3. package/src/cli.ts +744 -80
package/README.md CHANGED
@@ -9,7 +9,8 @@ A customizable statusline for [Claude Code](https://claude.com/product/claude-co
9
9
 
10
10
  - **Session info** - Project name, git branch, model, cost, lines changed, and context usage
11
11
  - **System stats** - CPU and memory usage (optional)
12
- - **Calendar events** - Upcoming events from multiple Google accounts
12
+ - **Calendar events** - Upcoming events from Google Calendar and Zoho Calendar
13
+ - **Zoho Tasks** - Display top pending tasks from Zoho Mail
13
14
  - **Meeting warnings** - Red alert when a meeting is 5 minutes away
14
15
  - **Health reminders** - Water, stretch, and eye break reminders
15
16
  - **Color-coded** - Everything has distinct colors for quick scanning
@@ -19,7 +20,8 @@ A customizable statusline for [Claude Code](https://claude.com/product/claude-co
19
20
  ## Requirements
20
21
 
21
22
  - [Bun](https://bun.sh/) >= 1.0.0
22
- - Google Cloud project with Calendar API enabled
23
+ - Google Cloud project with Calendar API enabled (for Google Calendar)
24
+ - Zoho API Console credentials (for Zoho Calendar)
23
25
 
24
26
  ## Installation
25
27
 
@@ -49,8 +51,9 @@ npm install -g @naarang/glancebar
49
51
  # 1. Run setup guide
50
52
  glancebar setup
51
53
 
52
- # 2. Add your Google account (after setting up credentials)
54
+ # 2. Add your account (after setting up credentials)
53
55
  glancebar auth --add your-email@gmail.com
56
+ # Select Google (1) or Zoho (2) when prompted
54
57
 
55
58
  # 3. Test it
56
59
  glancebar
@@ -58,7 +61,9 @@ glancebar
58
61
 
59
62
  ## Setup
60
63
 
61
- ### 1. Create Google Cloud Project
64
+ ### Google Calendar Setup
65
+
66
+ #### 1. Create Google Cloud Project
62
67
 
63
68
  1. Go to [Google Cloud Console](https://console.cloud.google.com/)
64
69
  2. Create a new project or select an existing one
@@ -66,30 +71,82 @@ glancebar
66
71
  - Go to "APIs & Services" > "Library"
67
72
  - Search for "Google Calendar API" and enable it
68
73
 
69
- ### 2. Create OAuth Credentials
74
+ #### 2. Create OAuth Credentials
70
75
 
71
76
  1. Go to "APIs & Services" > "Credentials"
72
77
  2. Click "Create Credentials" > "OAuth client ID"
73
78
  3. Select "Desktop app" as application type
74
- 4. Download the JSON file
75
- 5. Rename to `credentials.json` and save to `~/.glancebar/credentials.json`
79
+ 4. Give it a name and click "Create"
80
+ 5. Download the JSON file
81
+ 6. Rename to `credentials.json` and save to `~/.glancebar/credentials.json`
76
82
 
77
- ### 3. Add Redirect URI
83
+ #### 3. Add Redirect URI
78
84
 
79
- In Google Cloud Console, edit your OAuth client and add:
85
+ Desktop app credentials don't show a redirect URI field in the console UI. You need to manually edit the downloaded `credentials.json` file:
80
86
 
81
- ```
82
- http://localhost:3000/callback
87
+ 1. Open `~/.glancebar/credentials.json` in a text editor
88
+ 2. Find the `"redirect_uris"` array and ensure it contains:
89
+ ```json
90
+ "redirect_uris": ["http://localhost:3000/callback"]
91
+ ```
92
+ 3. Save the file
93
+
94
+ Alternatively, you can add it via Google Cloud Console:
95
+ 1. Go to "APIs & Services" > "Credentials"
96
+ 2. Click on your OAuth client to edit it
97
+ 3. Under "Authorized redirect URIs", click "Add URI"
98
+ 4. Enter `http://localhost:3000/callback`
99
+ 5. Save and re-download the JSON file
100
+
101
+ ### Zoho Calendar Setup
102
+
103
+ #### 1. Register Application
104
+
105
+ 1. Go to [Zoho API Console](https://api-console.zoho.com/)
106
+ 2. Click "Add Client" > "Server-based Applications"
107
+ 3. Enter application details
108
+
109
+ #### 2. Configure Client
110
+
111
+ 1. Set Authorized Redirect URI: `http://localhost:3000/callback`
112
+ 2. Note your Client ID and Client Secret
113
+
114
+ #### 3. Save Credentials
115
+
116
+ Create `~/.glancebar/zoho_credentials.json`:
117
+
118
+ ```json
119
+ {
120
+ "client_id": "YOUR_CLIENT_ID",
121
+ "client_secret": "YOUR_CLIENT_SECRET"
122
+ }
83
123
  ```
84
124
 
85
- ### 4. Add Accounts
125
+ ### Add Accounts
86
126
 
87
127
  ```bash
128
+ # Add Google Calendar account
88
129
  glancebar auth --add your-email@gmail.com
89
- glancebar auth --add work@company.com
130
+ # Select "1" for Google Calendar
131
+
132
+ # Add Zoho Calendar account
133
+ glancebar auth --add your-email@zoho.com
134
+ # Select "2" for Zoho Calendar
135
+ # Then select your datacenter region (1-7)
90
136
  ```
91
137
 
92
- ### 5. Configure Claude Code
138
+ **Zoho Datacenters:**
139
+ | Choice | Region | Domain |
140
+ |--------|--------|--------|
141
+ | 1 | United States | zoho.com |
142
+ | 2 | Europe | zoho.eu |
143
+ | 3 | India | zoho.in |
144
+ | 4 | Australia | zoho.com.au |
145
+ | 5 | China | zoho.com.cn |
146
+ | 6 | Japan | zoho.jp |
147
+ | 7 | Canada | zohocloud.ca |
148
+
149
+ ### Configure Claude Code
93
150
 
94
151
  Update `~/.claude/settings.json`:
95
152
 
@@ -149,6 +206,10 @@ glancebar config --eye-reminder true
149
206
  glancebar config --cpu-usage true
150
207
  glancebar config --memory-usage true
151
208
 
209
+ # Enable/disable Zoho tasks display
210
+ glancebar config --zoho-tasks true
211
+ glancebar config --max-tasks 3
212
+
152
213
  # Reset to defaults
153
214
  glancebar config --reset
154
215
  ```
@@ -188,6 +249,16 @@ Context usage color changes based on percentage:
188
249
  | Later | `HH:MM AM/PM: Title (account)` | `2:30 PM: Meeting (work)` |
189
250
  | No events | `No upcoming events` | |
190
251
 
252
+ ### Zoho Tasks
253
+
254
+ | State | Color | Example |
255
+ |-------|-------|---------|
256
+ | Overdue task | Red | `Overdue task title` |
257
+ | High priority | Yellow | `High priority task` |
258
+ | Normal task | White | `Normal task title` |
259
+
260
+ Tasks are displayed as: `Tasks: Task 1, Task 2, Task 3`
261
+
191
262
  ### Health Reminders (~5% chance)
192
263
 
193
264
  | Type | Color | Example |
@@ -202,10 +273,12 @@ All configuration is stored in `~/.glancebar/`:
202
273
 
203
274
  ```
204
275
  ~/.glancebar/
205
- ├── config.json # User settings
206
- ├── credentials.json # Google OAuth credentials (you provide this)
207
- └── tokens/ # OAuth tokens per account
208
- └── <email>.json
276
+ ├── config.json # User settings
277
+ ├── credentials.json # Google OAuth credentials (you provide)
278
+ ├── zoho_credentials.json # Zoho OAuth credentials (you provide)
279
+ └── tokens/ # OAuth tokens per account
280
+ ├── google_<email>.json # Google tokens
281
+ └── zoho_<email>.json # Zoho tokens
209
282
  ```
210
283
 
211
284
  ### Default Settings
@@ -221,6 +294,8 @@ All configuration is stored in `~/.glancebar/`:
221
294
  | `eyeReminderEnabled` | true | Enable random eye break reminders (20-20-20 rule) |
222
295
  | `showCpuUsage` | false | Show CPU usage percentage |
223
296
  | `showMemoryUsage` | false | Show memory usage |
297
+ | `showZohoTasks` | true | Show pending Zoho tasks |
298
+ | `maxTasksToShow` | 3 | Maximum number of tasks to display |
224
299
 
225
300
  ## Building from Source
226
301
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naarang/glancebar",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
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
@@ -10,7 +10,9 @@ import { createInterface } from "readline";
10
10
  // ============================================================================
11
11
 
12
12
  interface Config {
13
- accounts: string[];
13
+ accounts: string[]; // Legacy - for backwards compatibility
14
+ gmailAccounts: string[]; // Google accounts
15
+ zohoAccounts: ZohoAccount[]; // Zoho accounts
14
16
  lookaheadHours: number;
15
17
  showCalendarName: boolean;
16
18
  countdownThresholdMinutes: number;
@@ -20,6 +22,13 @@ interface Config {
20
22
  eyeReminderEnabled: boolean;
21
23
  showCpuUsage: boolean;
22
24
  showMemoryUsage: boolean;
25
+ showZohoTasks: boolean;
26
+ maxTasksToShow: number;
27
+ }
28
+
29
+ interface ZohoAccount {
30
+ email: string;
31
+ datacenter: string; // com, eu, in, com.au, com.cn, jp, zohocloud.ca
23
32
  }
24
33
 
25
34
  const COLORS: Record<string, string> = {
@@ -45,7 +54,9 @@ const COLORS: Record<string, string> = {
45
54
  const ACCOUNT_COLORS = ["cyan", "magenta", "brightGreen", "orange", "brightBlue", "pink", "yellow", "purple"];
46
55
 
47
56
  const DEFAULT_CONFIG: Config = {
48
- accounts: [],
57
+ accounts: [], // Legacy
58
+ gmailAccounts: [],
59
+ zohoAccounts: [],
49
60
  lookaheadHours: 8,
50
61
  showCalendarName: true,
51
62
  countdownThresholdMinutes: 60,
@@ -55,6 +66,8 @@ const DEFAULT_CONFIG: Config = {
55
66
  eyeReminderEnabled: true,
56
67
  showCpuUsage: false,
57
68
  showMemoryUsage: false,
69
+ showZohoTasks: true,
70
+ maxTasksToShow: 3,
58
71
  };
59
72
 
60
73
  const WATER_REMINDERS = [
@@ -117,7 +130,14 @@ function loadConfig(): Config {
117
130
  try {
118
131
  const content = readFileSync(configPath, "utf-8");
119
132
  const userConfig = JSON.parse(content);
120
- return { ...DEFAULT_CONFIG, ...userConfig };
133
+ const config = { ...DEFAULT_CONFIG, ...userConfig };
134
+
135
+ // Migrate legacy accounts to gmailAccounts
136
+ if (config.accounts && config.accounts.length > 0 && (!config.gmailAccounts || config.gmailAccounts.length === 0)) {
137
+ config.gmailAccounts = [...config.accounts];
138
+ }
139
+
140
+ return config;
121
141
  } catch {
122
142
  return { ...DEFAULT_CONFIG };
123
143
  }
@@ -129,23 +149,58 @@ function saveConfig(config: Config): void {
129
149
  }
130
150
 
131
151
  // ============================================================================
132
- // OAuth Authentication
152
+ // Google OAuth Authentication
133
153
  // ============================================================================
134
154
 
135
- const SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"];
155
+ const GOOGLE_SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"];
136
156
  const REDIRECT_URI = "http://localhost:3000/callback";
137
157
 
138
- interface Credentials {
158
+ interface GoogleCredentials {
139
159
  installed?: { client_id: string; client_secret: string };
140
160
  web?: { client_id: string; client_secret: string };
141
161
  }
142
162
 
143
- function getCredentialsPath(): string {
163
+ // ============================================================================
164
+ // Zoho OAuth Authentication
165
+ // ============================================================================
166
+
167
+ const ZOHO_SCOPES = [
168
+ "ZohoCalendar.calendar.READ",
169
+ "ZohoCalendar.event.READ",
170
+ "ZohoMail.tasks.READ",
171
+ ];
172
+ const ZOHO_REDIRECT_URI = "http://localhost:3000/callback";
173
+
174
+ // Zoho datacenter mappings
175
+ const ZOHO_DATACENTERS: Record<string, { accounts: string; calendar: string; mail: string }> = {
176
+ "com": { accounts: "https://accounts.zoho.com", calendar: "https://calendar.zoho.com", mail: "https://mail.zoho.com" },
177
+ "eu": { accounts: "https://accounts.zoho.eu", calendar: "https://calendar.zoho.eu", mail: "https://mail.zoho.eu" },
178
+ "in": { accounts: "https://accounts.zoho.in", calendar: "https://calendar.zoho.in", mail: "https://mail.zoho.in" },
179
+ "com.au": { accounts: "https://accounts.zoho.com.au", calendar: "https://calendar.zoho.com.au", mail: "https://mail.zoho.com.au" },
180
+ "com.cn": { accounts: "https://accounts.zoho.com.cn", calendar: "https://calendar.zoho.com.cn", mail: "https://mail.zoho.com.cn" },
181
+ "jp": { accounts: "https://accounts.zoho.jp", calendar: "https://calendar.zoho.jp", mail: "https://mail.zoho.jp" },
182
+ "zohocloud.ca": { accounts: "https://accounts.zohocloud.ca", calendar: "https://calendar.zohocloud.ca", mail: "https://mail.zohocloud.ca" },
183
+ };
184
+
185
+ interface ZohoCredentials {
186
+ client_id: string;
187
+ client_secret: string;
188
+ }
189
+
190
+ interface ZohoToken {
191
+ access_token: string;
192
+ refresh_token: string;
193
+ expires_at: number;
194
+ api_domain?: string;
195
+ }
196
+
197
+ // Google credentials
198
+ function getGoogleCredentialsPath(): string {
144
199
  return join(getConfigDir(), "credentials.json");
145
200
  }
146
201
 
147
- function loadCredentials(): Credentials {
148
- const credPath = getCredentialsPath();
202
+ function loadGoogleCredentials(): GoogleCredentials {
203
+ const credPath = getGoogleCredentialsPath();
149
204
  if (!existsSync(credPath)) {
150
205
  throw new Error(
151
206
  `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.`
@@ -154,23 +209,55 @@ function loadCredentials(): Credentials {
154
209
  return JSON.parse(readFileSync(credPath, "utf-8"));
155
210
  }
156
211
 
157
- function getTokenPath(account: string): string {
212
+ function getGoogleTokenPath(account: string): string {
213
+ const safeAccount = account.replace(/[^a-zA-Z0-9@.-]/g, "_");
214
+ return join(getTokensDir(), `google_${safeAccount}.json`);
215
+ }
216
+
217
+ // Legacy token path (for migration)
218
+ function getLegacyTokenPath(account: string): string {
158
219
  const safeAccount = account.replace(/[^a-zA-Z0-9@.-]/g, "_");
159
220
  return join(getTokensDir(), `${safeAccount}.json`);
160
221
  }
161
222
 
162
- function createOAuth2Client(credentials: Credentials) {
223
+ // Zoho credentials
224
+ function getZohoCredentialsPath(): string {
225
+ return join(getConfigDir(), "zoho_credentials.json");
226
+ }
227
+
228
+ function loadZohoCredentials(): ZohoCredentials {
229
+ const credPath = getZohoCredentialsPath();
230
+ if (!existsSync(credPath)) {
231
+ throw new Error(
232
+ `zoho_credentials.json not found at ${credPath}\n\nPlease create OAuth credentials in Zoho API Console and save to:\n${credPath}\n\nRun 'glancebar setup' for detailed instructions.`
233
+ );
234
+ }
235
+ return JSON.parse(readFileSync(credPath, "utf-8"));
236
+ }
237
+
238
+ function getZohoTokenPath(account: string): string {
239
+ const safeAccount = account.replace(/[^a-zA-Z0-9@.-]/g, "_");
240
+ return join(getTokensDir(), `zoho_${safeAccount}.json`);
241
+ }
242
+
243
+ function createGoogleOAuth2Client(credentials: GoogleCredentials) {
163
244
  const { client_id, client_secret } = credentials.installed || credentials.web!;
164
245
  return new google.auth.OAuth2(client_id, client_secret, REDIRECT_URI);
165
246
  }
166
247
 
167
- function getAuthenticatedClient(account: string) {
168
- const credentials = loadCredentials();
169
- const oauth2Client = createOAuth2Client(credentials);
170
- const tokenPath = getTokenPath(account);
248
+ function getGoogleAuthenticatedClient(account: string) {
249
+ const credentials = loadGoogleCredentials();
250
+ const oauth2Client = createGoogleOAuth2Client(credentials);
171
251
 
252
+ // Try new path first, then legacy path
253
+ let tokenPath = getGoogleTokenPath(account);
172
254
  if (!existsSync(tokenPath)) {
173
- return null;
255
+ const legacyPath = getLegacyTokenPath(account);
256
+ if (existsSync(legacyPath)) {
257
+ tokenPath = legacyPath;
258
+ } else {
259
+ return null;
260
+ }
174
261
  }
175
262
 
176
263
  const token = JSON.parse(readFileSync(tokenPath, "utf-8"));
@@ -185,13 +272,13 @@ function getAuthenticatedClient(account: string) {
185
272
  return oauth2Client;
186
273
  }
187
274
 
188
- async function authenticateAccount(account: string): Promise<void> {
189
- const credentials = loadCredentials();
190
- const oauth2Client = createOAuth2Client(credentials);
275
+ async function authenticateGoogleAccount(account: string): Promise<void> {
276
+ const credentials = loadGoogleCredentials();
277
+ const oauth2Client = createGoogleOAuth2Client(credentials);
191
278
 
192
279
  const authUrl = oauth2Client.generateAuthUrl({
193
280
  access_type: "offline",
194
- scope: SCOPES,
281
+ scope: GOOGLE_SCOPES,
195
282
  prompt: "consent",
196
283
  login_hint: account,
197
284
  });
@@ -211,11 +298,132 @@ async function authenticateAccount(account: string): Promise<void> {
211
298
  mkdirSync(tokensDir, { recursive: true });
212
299
  }
213
300
 
214
- const tokenPath = getTokenPath(account);
301
+ const tokenPath = getGoogleTokenPath(account);
215
302
  writeFileSync(tokenPath, JSON.stringify(tokens, null, 2));
216
303
  console.log(`Token saved for ${account}`);
217
304
  }
218
305
 
306
+ // ============================================================================
307
+ // Zoho OAuth Flow
308
+ // ============================================================================
309
+
310
+ async function authenticateZohoAccount(account: ZohoAccount): Promise<void> {
311
+ const credentials = loadZohoCredentials();
312
+ const dc = ZOHO_DATACENTERS[account.datacenter];
313
+
314
+ if (!dc) {
315
+ throw new Error(`Invalid datacenter: ${account.datacenter}. Valid options: ${Object.keys(ZOHO_DATACENTERS).join(", ")}`);
316
+ }
317
+
318
+ const params = new URLSearchParams({
319
+ response_type: "code",
320
+ client_id: credentials.client_id,
321
+ scope: ZOHO_SCOPES.join(","),
322
+ redirect_uri: ZOHO_REDIRECT_URI,
323
+ access_type: "offline",
324
+ prompt: "consent",
325
+ });
326
+
327
+ const authUrl = `${dc.accounts}/oauth/v2/auth?${params.toString()}`;
328
+
329
+ console.log(`\nAuthenticating Zoho: ${account.email}`);
330
+ console.log(`Datacenter: ${account.datacenter}`);
331
+ console.log(`Opening browser...`);
332
+
333
+ const code = await startServerAndGetCode(authUrl);
334
+
335
+ console.log(`Exchanging code for tokens...`);
336
+
337
+ // Exchange code for tokens
338
+ const tokenParams = new URLSearchParams({
339
+ grant_type: "authorization_code",
340
+ client_id: credentials.client_id,
341
+ client_secret: credentials.client_secret,
342
+ redirect_uri: ZOHO_REDIRECT_URI,
343
+ code: code,
344
+ });
345
+
346
+ const tokenResponse = await fetch(`${dc.accounts}/oauth/v2/token`, {
347
+ method: "POST",
348
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
349
+ body: tokenParams.toString(),
350
+ });
351
+
352
+ if (!tokenResponse.ok) {
353
+ const errorText = await tokenResponse.text();
354
+ throw new Error(`Failed to get Zoho tokens: ${errorText}`);
355
+ }
356
+
357
+ const tokenData = await tokenResponse.json();
358
+
359
+ const token: ZohoToken = {
360
+ access_token: tokenData.access_token,
361
+ refresh_token: tokenData.refresh_token,
362
+ expires_at: Date.now() + (tokenData.expires_in * 1000),
363
+ api_domain: tokenData.api_domain || dc.calendar,
364
+ };
365
+
366
+ const tokensDir = getTokensDir();
367
+ if (!existsSync(tokensDir)) {
368
+ mkdirSync(tokensDir, { recursive: true });
369
+ }
370
+
371
+ const tokenPath = getZohoTokenPath(account.email);
372
+ writeFileSync(tokenPath, JSON.stringify(token, null, 2));
373
+ console.log(`Token saved for ${account.email}`);
374
+ }
375
+
376
+ async function refreshZohoToken(account: ZohoAccount): Promise<ZohoToken | null> {
377
+ const tokenPath = getZohoTokenPath(account.email);
378
+ if (!existsSync(tokenPath)) return null;
379
+
380
+ const token: ZohoToken = JSON.parse(readFileSync(tokenPath, "utf-8"));
381
+
382
+ // Check if token is still valid (with 5 minute buffer)
383
+ if (token.expires_at > Date.now() + 300000) {
384
+ return token;
385
+ }
386
+
387
+ // Refresh the token
388
+ try {
389
+ const credentials = loadZohoCredentials();
390
+ const dc = ZOHO_DATACENTERS[account.datacenter];
391
+
392
+ const params = new URLSearchParams({
393
+ grant_type: "refresh_token",
394
+ client_id: credentials.client_id,
395
+ client_secret: credentials.client_secret,
396
+ refresh_token: token.refresh_token,
397
+ });
398
+
399
+ const response = await fetch(`${dc.accounts}/oauth/v2/token`, {
400
+ method: "POST",
401
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
402
+ body: params.toString(),
403
+ });
404
+
405
+ if (!response.ok) return null;
406
+
407
+ const data = await response.json();
408
+ const updatedToken: ZohoToken = {
409
+ ...token,
410
+ access_token: data.access_token,
411
+ expires_at: Date.now() + (data.expires_in * 1000),
412
+ };
413
+
414
+ writeFileSync(tokenPath, JSON.stringify(updatedToken, null, 2));
415
+ return updatedToken;
416
+ } catch {
417
+ return null;
418
+ }
419
+ }
420
+
421
+ function getZohoAuthenticatedToken(account: ZohoAccount): ZohoToken | null {
422
+ const tokenPath = getZohoTokenPath(account.email);
423
+ if (!existsSync(tokenPath)) return null;
424
+ return JSON.parse(readFileSync(tokenPath, "utf-8"));
425
+ }
426
+
219
427
  function startServerAndGetCode(authUrl: string): Promise<string> {
220
428
  return new Promise((resolve, reject) => {
221
429
  let server: Server;
@@ -313,16 +521,24 @@ interface CalendarEvent {
313
521
  account: string;
314
522
  accountEmail: string;
315
523
  accountIndex: number;
524
+ provider: "google" | "zoho";
316
525
  }
317
526
 
318
- async function getUpcomingEvents(config: Config): Promise<CalendarEvent[]> {
319
- const allEvents: CalendarEvent[] = [];
320
- const now = new Date();
321
- const timeMax = new Date(now.getTime() + config.lookaheadHours * 60 * 60 * 1000);
527
+ // Get all accounts combined for indexing
528
+ function getAllAccounts(config: Config): string[] {
529
+ const gmailAccounts = config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts;
530
+ const zohoEmails = config.zohoAccounts.map(z => z.email);
531
+ return [...gmailAccounts, ...zohoEmails];
532
+ }
322
533
 
323
- const eventPromises = config.accounts.map(async (account, accountIndex) => {
534
+ async function getGoogleEvents(config: Config, now: Date, timeMax: Date): Promise<CalendarEvent[]> {
535
+ const gmailAccounts = config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts;
536
+ const allAccounts = getAllAccounts(config);
537
+
538
+ const eventPromises = gmailAccounts.map(async (account) => {
539
+ const accountIndex = allAccounts.indexOf(account);
324
540
  try {
325
- const auth = getAuthenticatedClient(account);
541
+ const auth = getGoogleAuthenticatedClient(account);
326
542
  if (!auth) return [];
327
543
 
328
544
  const calendar = google.calendar({ version: "v3", auth });
@@ -358,6 +574,7 @@ async function getUpcomingEvents(config: Config): Promise<CalendarEvent[]> {
358
574
  account: extractAccountName(account),
359
575
  accountEmail: account,
360
576
  accountIndex,
577
+ provider: "google" as const,
361
578
  };
362
579
  });
363
580
  } catch {
@@ -366,14 +583,251 @@ async function getUpcomingEvents(config: Config): Promise<CalendarEvent[]> {
366
583
  });
367
584
 
368
585
  const results = await Promise.all(eventPromises);
369
- for (const events of results) {
370
- allEvents.push(...events);
371
- }
586
+ return results.flat();
587
+ }
588
+
589
+ async function getZohoEvents(config: Config, now: Date, timeMax: Date): Promise<CalendarEvent[]> {
590
+ if (!config.zohoAccounts || config.zohoAccounts.length === 0) return [];
591
+
592
+ const allAccounts = getAllAccounts(config);
593
+ const gmailCount = (config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts).length;
594
+
595
+ const eventPromises = config.zohoAccounts.map(async (account, idx) => {
596
+ const accountIndex = gmailCount + idx;
597
+ try {
598
+ const token = await refreshZohoToken(account);
599
+ if (!token) return [];
600
+
601
+ const dc = ZOHO_DATACENTERS[account.datacenter];
602
+ // Always use the calendar-specific API domain, not the generic api_domain
603
+ const apiBase = dc.calendar;
604
+
605
+ // First get list of calendars
606
+ const calendarsResponse = await fetch(`${apiBase}/api/v1/calendars?category=own`, {
607
+ headers: {
608
+ Authorization: `Zoho-oauthtoken ${token.access_token}`,
609
+ },
610
+ });
611
+
612
+ if (!calendarsResponse.ok) return [];
613
+
614
+ const calendarsData = await calendarsResponse.json();
615
+ const calendars = calendarsData.calendars || [];
616
+
617
+ if (calendars.length === 0) return [];
618
+
619
+ // Use the first (primary) calendar
620
+ const primaryCalendar = calendars.find((c: any) => c.isdefault) || calendars[0];
621
+ const calendarUid = primaryCalendar.uid;
622
+
623
+ // Format dates for Zoho API (yyyyMMdd'T'HHmmss'Z')
624
+ const formatZohoDate = (date: Date): string => {
625
+ return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "");
626
+ };
627
+
628
+ const range = JSON.stringify({
629
+ start: formatZohoDate(now),
630
+ end: formatZohoDate(timeMax),
631
+ });
372
632
 
633
+ const eventsResponse = await fetch(
634
+ `${apiBase}/api/v1/calendars/${encodeURIComponent(calendarUid)}/events?range=${encodeURIComponent(range)}`,
635
+ {
636
+ headers: {
637
+ Authorization: `Zoho-oauthtoken ${token.access_token}`,
638
+ },
639
+ }
640
+ );
641
+
642
+ if (!eventsResponse.ok) return [];
643
+
644
+ const eventsData = await eventsResponse.json();
645
+ const events = eventsData.events || [];
646
+
647
+ return events.map((event: any) => {
648
+ const isAllDay = event.isallday === true;
649
+ let start: Date, end: Date;
650
+
651
+ // Parse Zoho date format: "20260109T163000+0530" or "20260109T163000Z"
652
+ const parseZohoDate = (dateStr: string): Date => {
653
+ // Format: YYYYMMDDTHHmmss+HHMM or YYYYMMDDTHHmmssZ
654
+ const match = dateStr.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})([Z]|([+-])(\d{2})(\d{2}))?$/);
655
+ if (match) {
656
+ const [, year, month, day, hour, min, sec, tz, sign, tzHour, tzMin] = match;
657
+ if (tz === "Z") {
658
+ return new Date(Date.UTC(+year, +month - 1, +day, +hour, +min, +sec));
659
+ } else if (sign && tzHour && tzMin) {
660
+ const offsetMinutes = (+tzHour * 60 + +tzMin) * (sign === "+" ? -1 : 1);
661
+ const utc = Date.UTC(+year, +month - 1, +day, +hour, +min, +sec);
662
+ return new Date(utc + offsetMinutes * 60000);
663
+ }
664
+ // No timezone, assume local
665
+ return new Date(+year, +month - 1, +day, +hour, +min, +sec);
666
+ }
667
+ // Fallback to standard parsing
668
+ return new Date(dateStr);
669
+ };
670
+
671
+ if (event.dateandtime) {
672
+ start = parseZohoDate(event.dateandtime.start);
673
+ end = parseZohoDate(event.dateandtime.end);
674
+ } else {
675
+ start = parseZohoDate(event.start);
676
+ end = parseZohoDate(event.end);
677
+ }
678
+
679
+ return {
680
+ id: event.uid || "",
681
+ title: event.title || "(No title)",
682
+ start,
683
+ end,
684
+ isAllDay,
685
+ account: extractAccountName(account.email),
686
+ accountEmail: account.email,
687
+ accountIndex,
688
+ provider: "zoho" as const,
689
+ };
690
+ });
691
+ } catch {
692
+ return [];
693
+ }
694
+ });
695
+
696
+ const results = await Promise.all(eventPromises);
697
+ return results.flat();
698
+ }
699
+
700
+ async function getUpcomingEvents(config: Config): Promise<CalendarEvent[]> {
701
+ const now = new Date();
702
+ const timeMax = new Date(now.getTime() + config.lookaheadHours * 60 * 60 * 1000);
703
+
704
+ // Fetch from both providers in parallel
705
+ const [googleEvents, zohoEvents] = await Promise.all([
706
+ getGoogleEvents(config, now, timeMax),
707
+ getZohoEvents(config, now, timeMax),
708
+ ]);
709
+
710
+ const allEvents = [...googleEvents, ...zohoEvents];
373
711
  allEvents.sort((a, b) => a.start.getTime() - b.start.getTime());
374
712
  return allEvents;
375
713
  }
376
714
 
715
+ // ============================================================================
716
+ // Zoho Tasks
717
+ // ============================================================================
718
+
719
+ interface ZohoTask {
720
+ id: string;
721
+ title: string;
722
+ description: string;
723
+ dueDate: Date | null;
724
+ priority: "High" | "Normal" | "Low";
725
+ status: string;
726
+ isOverdue: boolean;
727
+ }
728
+
729
+ async function getZohoTasks(config: Config): Promise<ZohoTask[]> {
730
+ if (!config.zohoAccounts || config.zohoAccounts.length === 0) return [];
731
+ if (!config.showZohoTasks) return [];
732
+
733
+ const allTasks: ZohoTask[] = [];
734
+
735
+ for (const account of config.zohoAccounts) {
736
+ try {
737
+ const token = await refreshZohoToken(account);
738
+ if (!token) continue;
739
+
740
+ const dc = ZOHO_DATACENTERS[account.datacenter];
741
+ const mailBase = dc.mail;
742
+
743
+ // Fetch tasks assigned to user
744
+ const response = await fetch(
745
+ `${mailBase}/api/tasks/?view=assignedtome&action=view&limit=10&from=0`,
746
+ {
747
+ headers: {
748
+ Authorization: `Zoho-oauthtoken ${token.access_token}`,
749
+ Accept: "application/json",
750
+ },
751
+ }
752
+ );
753
+
754
+ if (!response.ok) continue;
755
+
756
+ const data = await response.json();
757
+ const tasks = data.data?.tasks || [];
758
+
759
+ const now = new Date();
760
+
761
+ for (const task of tasks) {
762
+ // Skip completed tasks
763
+ if (task.status === "Completed" || task.status === "completed") continue;
764
+
765
+ let dueDate: Date | null = null;
766
+ let isOverdue = false;
767
+
768
+ if (task.dueDate) {
769
+ // Parse DD/MM/YYYY format
770
+ const parts = task.dueDate.split("/");
771
+ if (parts.length === 3) {
772
+ dueDate = new Date(+parts[2], +parts[1] - 1, +parts[0]);
773
+ isOverdue = dueDate < now;
774
+ }
775
+ }
776
+
777
+ allTasks.push({
778
+ id: task.id || "",
779
+ title: task.title || "(No title)",
780
+ description: task.description || "",
781
+ dueDate,
782
+ priority: task.priority || "Normal",
783
+ status: task.status || "Open",
784
+ isOverdue,
785
+ });
786
+ }
787
+ } catch {
788
+ // Silently continue on error
789
+ }
790
+ }
791
+
792
+ // Sort: overdue first, then by due date (soonest first), then by priority
793
+ allTasks.sort((a, b) => {
794
+ // Overdue tasks first
795
+ if (a.isOverdue && !b.isOverdue) return -1;
796
+ if (!a.isOverdue && b.isOverdue) return 1;
797
+
798
+ // Then by due date (null dates go last)
799
+ if (a.dueDate && b.dueDate) {
800
+ return a.dueDate.getTime() - b.dueDate.getTime();
801
+ }
802
+ if (a.dueDate && !b.dueDate) return -1;
803
+ if (!a.dueDate && b.dueDate) return 1;
804
+
805
+ // Then by priority
806
+ const priorityOrder = { High: 0, Normal: 1, Low: 2 };
807
+ return (priorityOrder[a.priority] || 1) - (priorityOrder[b.priority] || 1);
808
+ });
809
+
810
+ return allTasks.slice(0, config.maxTasksToShow);
811
+ }
812
+
813
+ function formatTasks(tasks: ZohoTask[]): string | null {
814
+ if (tasks.length === 0) return null;
815
+
816
+ const formatted = tasks.map((task) => {
817
+ const title = task.title.length > 25 ? task.title.slice(0, 24) + "…" : task.title;
818
+
819
+ if (task.isOverdue) {
820
+ return `${COLORS.red}${title}${COLORS.reset}`;
821
+ } else if (task.priority === "High") {
822
+ return `${COLORS.yellow}${title}${COLORS.reset}`;
823
+ } else {
824
+ return `${COLORS.white}${title}${COLORS.reset}`;
825
+ }
826
+ });
827
+
828
+ return `${COLORS.cyan}Tasks:${COLORS.reset} ${formatted.join(", ")}`;
829
+ }
830
+
377
831
  function extractAccountName(email: string): string {
378
832
  const atIndex = email.indexOf("@");
379
833
  if (atIndex === -1) return email;
@@ -528,9 +982,9 @@ Display calendar events, tasks, and more at a glance.
528
982
  Usage:
529
983
  glancebar Output statusline (for Claude Code)
530
984
  glancebar auth Authenticate all configured accounts
531
- glancebar auth --add <email> Add and authenticate a new account
985
+ glancebar auth --add <email> Add and authenticate a new account (prompts for provider)
532
986
  glancebar auth --remove <email> Remove an account
533
- glancebar auth --list List configured accounts
987
+ glancebar auth --list List all configured accounts
534
988
  glancebar config Show current configuration
535
989
  glancebar config --lookahead <hours> Set lookahead hours (default: 8)
536
990
  glancebar config --countdown-threshold <mins> Set countdown threshold in minutes (default: 60)
@@ -541,11 +995,14 @@ Usage:
541
995
  glancebar config --eye-reminder <true|false> Enable/disable eye break reminders (default: true)
542
996
  glancebar config --cpu-usage <true|false> Show CPU usage (default: false)
543
997
  glancebar config --memory-usage <true|false> Show memory usage (default: false)
998
+ glancebar config --zoho-tasks <true|false> Show Zoho tasks (default: true)
999
+ glancebar config --max-tasks <number> Max tasks to show (default: 3)
544
1000
  glancebar config --reset Reset to default configuration
545
1001
  glancebar setup Show setup instructions
546
1002
 
547
1003
  Examples:
548
- glancebar auth --add user@gmail.com
1004
+ glancebar auth --add user@gmail.com # Will prompt for Google or Zoho
1005
+ glancebar auth --add user@zoho.com # Will prompt for Google or Zoho
549
1006
  glancebar config --lookahead 12
550
1007
  glancebar config --stretch-reminder false
551
1008
 
@@ -558,6 +1015,9 @@ function printSetup() {
558
1015
  Glancebar - Setup Instructions
559
1016
  ==============================
560
1017
 
1018
+ GOOGLE CALENDAR SETUP
1019
+ ---------------------
1020
+
561
1021
  Step 1: Create Google Cloud Project
562
1022
  - Go to https://console.cloud.google.com/
563
1023
  - Create a new project or select existing one
@@ -574,17 +1034,41 @@ Step 3: Create OAuth Credentials
574
1034
 
575
1035
  Step 4: Save credentials
576
1036
  - Rename downloaded file to "credentials.json"
577
- - Save it to: ${getCredentialsPath()}
1037
+ - Save it to: ${getGoogleCredentialsPath()}
578
1038
 
579
1039
  Step 5: Add redirect URI
580
- - In Google Cloud Console, edit your OAuth client
581
- - Add redirect URI: http://localhost:3000/callback
1040
+ - Edit credentials.json and ensure redirect_uris contains:
1041
+ "redirect_uris": ["http://localhost:3000/callback"]
1042
+
1043
+ ZOHO CALENDAR SETUP
1044
+ -------------------
1045
+
1046
+ Step 1: Register Application
1047
+ - Go to https://api-console.zoho.com/
1048
+ - Click "Add Client" > "Server-based Applications"
1049
+
1050
+ Step 2: Configure Client
1051
+ - Set Authorized Redirect URI: http://localhost:3000/callback
1052
+ - Note your Client ID and Client Secret
1053
+
1054
+ Step 3: Save credentials
1055
+ - Create file: ${getZohoCredentialsPath()}
1056
+ - Add content:
1057
+ {
1058
+ "client_id": "YOUR_CLIENT_ID",
1059
+ "client_secret": "YOUR_CLIENT_SECRET"
1060
+ }
1061
+
1062
+ ADDING ACCOUNTS
1063
+ ---------------
582
1064
 
583
- Step 6: Add your Google accounts
584
1065
  glancebar auth --add your-email@gmail.com
585
- glancebar auth --add work@company.com
1066
+ # Select "Google" or "Zoho" when prompted
1067
+ # For Zoho, select your datacenter region
1068
+
1069
+ CONFIGURE CLAUDE CODE
1070
+ ---------------------
586
1071
 
587
- Step 7: Configure Claude Code statusline
588
1072
  Update ~/.claude/settings.json:
589
1073
  {
590
1074
  "statusLine": {
@@ -599,19 +1083,40 @@ For more info: https://github.com/vishal-android-freak/glancebar
599
1083
  }
600
1084
 
601
1085
  async function handleAuth(args: string[]) {
1086
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1087
+ const prompt = (q: string): Promise<string> => new Promise((r) => rl.question(q, r));
1088
+
602
1089
  // Handle --list
603
1090
  if (args.includes("--list")) {
604
1091
  const config = loadConfig();
605
- if (config.accounts.length === 0) {
1092
+ const gmailAccounts = config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts;
1093
+ const hasAny = gmailAccounts.length > 0 || config.zohoAccounts.length > 0;
1094
+
1095
+ if (!hasAny) {
606
1096
  console.log("No accounts configured.");
607
1097
  } else {
608
- console.log("Configured accounts:");
609
- config.accounts.forEach((acc, i) => {
610
- const tokenPath = getTokenPath(acc);
611
- const status = existsSync(tokenPath) ? "authenticated" : "not authenticated";
612
- console.log(` ${i + 1}. ${acc} (${status})`);
613
- });
1098
+ if (gmailAccounts.length > 0) {
1099
+ console.log("\nGoogle Calendar accounts:");
1100
+ gmailAccounts.forEach((acc, i) => {
1101
+ let tokenPath = getGoogleTokenPath(acc);
1102
+ if (!existsSync(tokenPath)) {
1103
+ tokenPath = getLegacyTokenPath(acc);
1104
+ }
1105
+ const status = existsSync(tokenPath) ? "authenticated" : "not authenticated";
1106
+ console.log(` ${i + 1}. ${acc} (${status})`);
1107
+ });
1108
+ }
1109
+
1110
+ if (config.zohoAccounts.length > 0) {
1111
+ console.log("\nZoho Calendar accounts:");
1112
+ config.zohoAccounts.forEach((acc, i) => {
1113
+ const tokenPath = getZohoTokenPath(acc.email);
1114
+ const status = existsSync(tokenPath) ? "authenticated" : "not authenticated";
1115
+ console.log(` ${i + 1}. ${acc.email} [${acc.datacenter}] (${status})`);
1116
+ });
1117
+ }
614
1118
  }
1119
+ rl.close();
615
1120
  return;
616
1121
  }
617
1122
 
@@ -621,25 +1126,89 @@ async function handleAuth(args: string[]) {
621
1126
  const email = args[addIndex + 1];
622
1127
  if (!email || email.startsWith("--")) {
623
1128
  console.error("Error: Please provide an email address after --add");
1129
+ rl.close();
624
1130
  process.exit(1);
625
1131
  }
626
1132
 
627
1133
  if (!email.includes("@")) {
628
1134
  console.error("Error: Invalid email address");
1135
+ rl.close();
629
1136
  process.exit(1);
630
1137
  }
631
1138
 
1139
+ // Prompt for provider
1140
+ console.log("\nSelect calendar provider:");
1141
+ console.log(" 1. Google Calendar");
1142
+ console.log(" 2. Zoho Calendar");
1143
+ const providerChoice = await prompt("\nEnter choice (1 or 2): ");
1144
+
632
1145
  const config = loadConfig();
633
- if (config.accounts.includes(email)) {
634
- console.log(`Account ${email} already exists. Re-authenticating...`);
1146
+
1147
+ if (providerChoice === "1") {
1148
+ // Google Calendar
1149
+ const gmailAccounts = config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts;
1150
+ if (gmailAccounts.includes(email)) {
1151
+ console.log(`\nGoogle account ${email} already exists. Re-authenticating...`);
1152
+ } else {
1153
+ if (config.gmailAccounts.length === 0 && config.accounts.length > 0) {
1154
+ config.gmailAccounts = [...config.accounts];
1155
+ }
1156
+ config.gmailAccounts.push(email);
1157
+ saveConfig(config);
1158
+ console.log(`\nAdded ${email} to Google accounts.`);
1159
+ }
1160
+
1161
+ await authenticateGoogleAccount(email);
1162
+ console.log("\nDone!");
1163
+ } else if (providerChoice === "2") {
1164
+ // Zoho Calendar
1165
+ console.log("\nSelect Zoho datacenter:");
1166
+ console.log(" 1. com - United States");
1167
+ console.log(" 2. eu - Europe");
1168
+ console.log(" 3. in - India");
1169
+ console.log(" 4. com.au - Australia");
1170
+ console.log(" 5. com.cn - China");
1171
+ console.log(" 6. jp - Japan");
1172
+ console.log(" 7. zohocloud.ca - Canada");
1173
+
1174
+ const dcChoice = await prompt("\nEnter choice (1-7): ");
1175
+ const dcMap: Record<string, string> = {
1176
+ "1": "com",
1177
+ "2": "eu",
1178
+ "3": "in",
1179
+ "4": "com.au",
1180
+ "5": "com.cn",
1181
+ "6": "jp",
1182
+ "7": "zohocloud.ca",
1183
+ };
1184
+
1185
+ const datacenter = dcMap[dcChoice];
1186
+ if (!datacenter) {
1187
+ console.error("Error: Invalid datacenter choice");
1188
+ rl.close();
1189
+ process.exit(1);
1190
+ }
1191
+
1192
+ const existingZoho = config.zohoAccounts.find((z) => z.email === email);
1193
+ if (existingZoho) {
1194
+ console.log(`\nZoho account ${email} already exists. Re-authenticating...`);
1195
+ existingZoho.datacenter = datacenter;
1196
+ saveConfig(config);
1197
+ } else {
1198
+ config.zohoAccounts.push({ email, datacenter });
1199
+ saveConfig(config);
1200
+ console.log(`\nAdded ${email} to Zoho accounts.`);
1201
+ }
1202
+
1203
+ await authenticateZohoAccount({ email, datacenter });
1204
+ console.log("\nDone!");
635
1205
  } else {
636
- config.accounts.push(email);
637
- saveConfig(config);
638
- console.log(`Added ${email} to accounts.`);
1206
+ console.error("Error: Invalid choice. Please enter 1 or 2.");
1207
+ rl.close();
1208
+ process.exit(1);
639
1209
  }
640
1210
 
641
- await authenticateAccount(email);
642
- console.log("\nDone!");
1211
+ rl.close();
643
1212
  return;
644
1213
  }
645
1214
 
@@ -649,52 +1218,92 @@ async function handleAuth(args: string[]) {
649
1218
  const email = args[removeIndex + 1];
650
1219
  if (!email || email.startsWith("--")) {
651
1220
  console.error("Error: Please provide an email address after --remove");
1221
+ rl.close();
652
1222
  process.exit(1);
653
1223
  }
654
1224
 
655
1225
  const config = loadConfig();
656
- const idx = config.accounts.indexOf(email);
657
- if (idx === -1) {
658
- console.error(`Error: Account ${email} not found.`);
659
- process.exit(1);
1226
+ const gmailAccounts = config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts;
1227
+
1228
+ // Check Google accounts
1229
+ const googleIdx = gmailAccounts.indexOf(email);
1230
+ if (googleIdx !== -1) {
1231
+ if (config.gmailAccounts.length > 0) {
1232
+ config.gmailAccounts.splice(googleIdx, 1);
1233
+ } else {
1234
+ config.accounts.splice(googleIdx, 1);
1235
+ }
1236
+ saveConfig(config);
1237
+
1238
+ // Remove token files
1239
+ const tokenPath = getGoogleTokenPath(email);
1240
+ const legacyPath = getLegacyTokenPath(email);
1241
+ if (existsSync(tokenPath)) unlinkSync(tokenPath);
1242
+ if (existsSync(legacyPath)) unlinkSync(legacyPath);
1243
+
1244
+ console.log(`Removed Google account ${email}.`);
1245
+ rl.close();
1246
+ return;
660
1247
  }
661
1248
 
662
- config.accounts.splice(idx, 1);
663
- saveConfig(config);
1249
+ // Check Zoho accounts
1250
+ const zohoIdx = config.zohoAccounts.findIndex((z) => z.email === email);
1251
+ if (zohoIdx !== -1) {
1252
+ config.zohoAccounts.splice(zohoIdx, 1);
1253
+ saveConfig(config);
664
1254
 
665
- const tokenPath = getTokenPath(email);
666
- if (existsSync(tokenPath)) {
667
- unlinkSync(tokenPath);
1255
+ const tokenPath = getZohoTokenPath(email);
1256
+ if (existsSync(tokenPath)) unlinkSync(tokenPath);
1257
+
1258
+ console.log(`Removed Zoho account ${email}.`);
1259
+ rl.close();
1260
+ return;
668
1261
  }
669
1262
 
670
- console.log(`Removed ${email} from accounts.`);
671
- return;
1263
+ console.error(`Error: Account ${email} not found.`);
1264
+ rl.close();
1265
+ process.exit(1);
672
1266
  }
673
1267
 
674
1268
  // Default: authenticate all accounts
675
1269
  const config = loadConfig();
1270
+ const gmailAccounts = config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts;
1271
+ const hasAny = gmailAccounts.length > 0 || config.zohoAccounts.length > 0;
676
1272
 
677
- if (config.accounts.length === 0) {
1273
+ if (!hasAny) {
678
1274
  console.log("No accounts configured.\n");
679
1275
  console.log("Add an account using:");
680
1276
  console.log(" glancebar auth --add your-email@gmail.com\n");
1277
+ rl.close();
681
1278
  return;
682
1279
  }
683
1280
 
684
- console.log("Glancebar - Google Calendar Authentication");
685
- console.log("==========================================\n");
1281
+ console.log("Glancebar - Calendar Authentication");
1282
+ console.log("====================================\n");
686
1283
 
687
- const rl = createInterface({ input: process.stdin, output: process.stdout });
688
- const prompt = (q: string): Promise<string> => new Promise((r) => rl.question(q, r));
1284
+ // Authenticate Google accounts
1285
+ for (const account of gmailAccounts) {
1286
+ let tokenPath = getGoogleTokenPath(account);
1287
+ if (!existsSync(tokenPath)) {
1288
+ tokenPath = getLegacyTokenPath(account);
1289
+ }
1290
+ if (existsSync(tokenPath)) {
1291
+ console.log(`Google: ${account} - Already authenticated`);
1292
+ const answer = await prompt(`Re-authenticate? (y/N): `);
1293
+ if (answer.toLowerCase() !== "y") continue;
1294
+ }
1295
+ await authenticateGoogleAccount(account);
1296
+ }
689
1297
 
690
- for (const account of config.accounts) {
691
- const tokenPath = getTokenPath(account);
1298
+ // Authenticate Zoho accounts
1299
+ for (const account of config.zohoAccounts) {
1300
+ const tokenPath = getZohoTokenPath(account.email);
692
1301
  if (existsSync(tokenPath)) {
693
- console.log(`${account}: Already authenticated`);
694
- const answer = await prompt(`Re-authenticate ${account}? (y/N): `);
1302
+ console.log(`Zoho: ${account.email} [${account.datacenter}] - Already authenticated`);
1303
+ const answer = await prompt(`Re-authenticate? (y/N): `);
695
1304
  if (answer.toLowerCase() !== "y") continue;
696
1305
  }
697
- await authenticateAccount(account);
1306
+ await authenticateZohoAccount(account);
698
1307
  }
699
1308
 
700
1309
  rl.close();
@@ -706,8 +1315,9 @@ function handleConfig(args: string[]) {
706
1315
 
707
1316
  // Handle --reset
708
1317
  if (args.includes("--reset")) {
709
- const accounts = config.accounts; // Preserve accounts
710
- saveConfig({ ...DEFAULT_CONFIG, accounts });
1318
+ // Preserve all account types
1319
+ const { accounts, gmailAccounts, zohoAccounts } = config;
1320
+ saveConfig({ ...DEFAULT_CONFIG, accounts, gmailAccounts, zohoAccounts });
711
1321
  console.log("Configuration reset to defaults (accounts preserved).");
712
1322
  return;
713
1323
  }
@@ -838,12 +1448,49 @@ function handleConfig(args: string[]) {
838
1448
  return;
839
1449
  }
840
1450
 
1451
+ // Handle --zoho-tasks
1452
+ const zohoTasksIndex = args.indexOf("--zoho-tasks");
1453
+ if (zohoTasksIndex !== -1) {
1454
+ const value = args[zohoTasksIndex + 1]?.toLowerCase();
1455
+ if (value !== "true" && value !== "false") {
1456
+ console.error("Error: --zoho-tasks must be 'true' or 'false'");
1457
+ process.exit(1);
1458
+ }
1459
+ config.showZohoTasks = value === "true";
1460
+ saveConfig(config);
1461
+ console.log(`Zoho tasks display ${value === "true" ? "enabled" : "disabled"}`);
1462
+ return;
1463
+ }
1464
+
1465
+ // Handle --max-tasks
1466
+ const maxTasksIndex = args.indexOf("--max-tasks");
1467
+ if (maxTasksIndex !== -1) {
1468
+ const value = parseInt(args[maxTasksIndex + 1], 10);
1469
+ if (isNaN(value) || value < 1 || value > 10) {
1470
+ console.error("Error: --max-tasks must be between 1 and 10");
1471
+ process.exit(1);
1472
+ }
1473
+ config.maxTasksToShow = value;
1474
+ saveConfig(config);
1475
+ console.log(`Max tasks to show set to ${value}`);
1476
+ return;
1477
+ }
1478
+
841
1479
  // Show current config
1480
+ const gmailAccounts = config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts;
1481
+ const googleStr = gmailAccounts.length > 0 ? gmailAccounts.join(", ") : "(none)";
1482
+ const zohoStr = config.zohoAccounts.length > 0
1483
+ ? config.zohoAccounts.map((z) => `${z.email} [${z.datacenter}]`).join(", ")
1484
+ : "(none)";
1485
+
842
1486
  console.log(`
843
1487
  Glancebar Configuration
844
1488
  =======================
845
1489
  Config directory: ${getConfigDir()}
846
- Accounts: ${config.accounts.length > 0 ? config.accounts.join(", ") : "(none)"}
1490
+
1491
+ Accounts:
1492
+ Google Calendar: ${googleStr}
1493
+ Zoho Calendar: ${zohoStr}
847
1494
 
848
1495
  Calendar Settings:
849
1496
  Lookahead hours: ${config.lookaheadHours}
@@ -859,6 +1506,10 @@ Reminders:
859
1506
  System Stats:
860
1507
  CPU usage: ${config.showCpuUsage ? "enabled" : "disabled"}
861
1508
  Memory usage: ${config.showMemoryUsage ? "enabled" : "disabled"}
1509
+
1510
+ Zoho Tasks:
1511
+ Show tasks: ${config.showZohoTasks ? "enabled" : "disabled"}
1512
+ Max tasks to show: ${config.maxTasksToShow}
862
1513
  `);
863
1514
  }
864
1515
 
@@ -1058,9 +1709,16 @@ async function outputStatusline() {
1058
1709
  parts.push(reminder);
1059
1710
  }
1060
1711
 
1061
- // Get calendar events
1062
- if (config.accounts.length > 0) {
1063
- const events = await getUpcomingEvents(config);
1712
+ // Get calendar events and tasks in parallel
1713
+ const gmailAccounts = config.gmailAccounts.length > 0 ? config.gmailAccounts : config.accounts;
1714
+ const hasAccounts = gmailAccounts.length > 0 || config.zohoAccounts.length > 0;
1715
+
1716
+ if (hasAccounts) {
1717
+ const [events, tasks] = await Promise.all([
1718
+ getUpcomingEvents(config),
1719
+ getZohoTasks(config),
1720
+ ]);
1721
+
1064
1722
  const event = getCurrentOrNextEvent(events);
1065
1723
 
1066
1724
  // Check for meeting warning (within 5 minutes)
@@ -1069,6 +1727,12 @@ async function outputStatusline() {
1069
1727
  parts.push(meetingWarning);
1070
1728
  }
1071
1729
 
1730
+ // Add tasks if available
1731
+ const tasksStr = formatTasks(tasks);
1732
+ if (tasksStr) {
1733
+ parts.push(tasksStr);
1734
+ }
1735
+
1072
1736
  if (event) {
1073
1737
  parts.push(formatEvent(event, config));
1074
1738
  } else if (parts.length === 0) {