Skip to content

Commit da1e479

Browse files
authored
Feature/configurable statusbar icon and color (#91)
* feat: add configurable status bar icon and color options * Refactor code structure for improved readability and maintainability * feat: apply background style selection to both status bar items * feat: add copilot instructions for coding time tracker * feat: update status bar to use a single item with configurable icon and improved tooltip
1 parent 4de8bff commit da1e479

5 files changed

Lines changed: 258 additions & 34 deletions

File tree

.github/copilot-instructions.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Copilot Instructions for Simple Coding Time Tracker
2+
3+
## Project Overview
4+
VS Code extension that tracks coding time with visualization, health reminders, and git branch/language detection. Version 0.6.7, published to VS Code Marketplace and Open VSX.
5+
6+
## Architecture
7+
8+
### Core Components (all in `src/`)
9+
- **extension.ts**: Main entry point; activates on startup; registers commands, event listeners, and manages component lifecycle
10+
- **timeTracker.ts**: Tracks user activity (cursor, text edits, window focus); monitors git branches; manages inactivity/focus timeouts; coordinates with health notifications
11+
- **database.ts**: Persists time entries via VS Code's globalState; each entry stores: date, project, timeSpent (minutes), branch, language
12+
- **summaryView.ts**: Webview UI showing 4 chart types (project, timeline, heatmap, languages); handles search/filtering; ~2800 lines with HTML/CSS/JS embedded
13+
- **settingsView.ts**: User-friendly settings UI; updates all tracker configuration
14+
- **statusBar.ts**: Real-time timer display at bottom of editor; clickable to show summary
15+
- **healthNotifications.ts**: Manages 3 health reminders (20-20-20 eye rule, stretch, break intervals)
16+
- **logger.ts**: Centralized logging
17+
- **utils.ts**: 50+ language detection from file extensions and VS Code language IDs; time formatting
18+
19+
### Data Flow
20+
1. **Activity Detection** → Activity triggers cursor/text/hover event handlers in timeTracker
21+
2. **Activity Tracking** → Updates lastCursorActivity; resets inactivity timeout
22+
3. **Branch Monitoring** → Git watcher checks branch every 5 seconds; saves session and starts new one on branch change
23+
4. **Time Accumulation** → Save interval (5 sec) calculates elapsed time; validates against 24-hour ceiling
24+
5. **Database Persistence** → addEntry() to globalState; queried by summaryView for charts
25+
26+
### Configuration Management
27+
Settings stored in package.json `contributes.configuration`; read via `vscode.workspace.getConfiguration()`. Key settings:
28+
- `inactivityTimeout` (minutes): Stop tracking after inactivity in focused VS Code
29+
- `focusTimeout` (minutes): Continue tracking N minutes after VS Code loses focus
30+
- `statusBar.showSeconds`, `statusBar.icon`, `statusBar.backgroundStyle`
31+
- `health.enableNotifications`, `health.eyeRestInterval`, `health.stretchInterval`, `health.breakInterval`
32+
33+
**Pattern**: Settings changes trigger `onDidChangeConfiguration` listener in extension.ts; notify components to reload config via updateConfiguration() methods.
34+
35+
## Critical Workflows
36+
37+
### Building & Packaging
38+
```bash
39+
npm install # Install dependencies
40+
npm run compile # TypeScript → JavaScript to dist/
41+
npm run package # Create .vsix file via webpack
42+
```
43+
44+
### Testing (see CONTRIBUTING.md § Testing the Extension)
45+
1. **F5 in VS Code**: Launches Extension Development Host
46+
2. **Enable dev commands**: Search "enableDevCommands" in settings
47+
3. **Generate test data**: Command `SCTT: Generate Test Data (Dev)` creates 90-day dataset
48+
49+
### Release Process (TECHNICAL.md)
50+
- **Beta**: `git tag v<version>-beta.<number> && git push origin v<version>-beta.<number>`
51+
- **Production**: `git tag v<version> && git push origin v<version>`
52+
- GitHub Actions auto-builds, creates release, publishes to Marketplace
53+
54+
### Git Branch Strategy
55+
- `main`: Production releases → Marketplace
56+
- `develop`: Beta testing
57+
- `site`: GitHub Pages documentation (separate branch)
58+
- `stats-data`: Statistics only
59+
60+
## Key Patterns & Conventions
61+
62+
### Event Management
63+
- Use `vscode.* event listeners` for activity (onDidChangeTextEditorSelection, onDidChangeTextDocument, onDidChangeWindowState, onDidChangeActiveTextEditor)
64+
- Always call `this.updateCursorActivity()` on any editor activity
65+
- Store timers in private fields; clear with `clearTimeout()` / `clearInterval()`; clean up on deactivate
66+
67+
### Time Tracking Logic
68+
- **Cursor inactivity**: 2.5 min default (configurable); reset on any editor activity
69+
- **Focus timeout**: 3 min default; continues tracking N minutes after VS Code loses focus
70+
- **Branch changes**: Save current session before starting new one (prevents cross-branch mixing)
71+
- **Validation**: Reject time entries > 24 hours or <= 0
72+
73+
### Language Detection
74+
`detectLanguageFromFile(filePath)` uses file extension map (~50+ languages); fallback to VS Code's languageId via `detectLanguageFromLanguageId(languageId)`. See [utils.ts](src/utils.ts) for full map.
75+
76+
### Database Migrations
77+
If adding new TimeEntry fields, add migration logic in Database.migrateEntries() to backfill old entries (see branch/language migration pattern).
78+
79+
### Webview Communication
80+
Webviews (summaryView, settingsView) use `webview.postMessage()` to send and `onDidReceiveMessage()` to receive. Commands passed as JSON: `{ command: 'name', data: {...} }`.
81+
82+
### Error Handling
83+
- Wrap async operations in try-catch
84+
- Use vscode.window.showErrorMessage() for user-facing errors
85+
- Log via Logger.getInstance().log()
86+
- Never silently fail on persistence or git operations
87+
88+
## Important Files Reference
89+
- [package.json](package.json): Dependencies, extension config, scripts
90+
- [TECHNICAL.md](TECHNICAL.md): Release process, CI/CD pipelines
91+
- [CONTRIBUTING.md](CONTRIBUTING.md): Dev setup, testing commands
92+
- [src/timeTracker.ts](src/timeTracker.ts): Core tracking—understand inactivity/focus logic here first
93+
- [src/database.ts](src/database.ts): Understand TimeEntry schema and query patterns

.vscode/settings.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,8 @@
66
"simpleCodingTimeTracker.statusBar.showSeconds": true,
77
"simpleCodingTimeTracker.health.enableNotifications": false,
88
"simpleCodingTimeTracker.health.modalNotifications": true,
9-
"simpleCodingTimeTracker.health.breakThreshold": 90
9+
"simpleCodingTimeTracker.health.breakThreshold": 90,
10+
"simpleCodingTimeTracker.statusBar.icon": "$(clock)",
11+
"simpleCodingTimeTracker.statusBar.color": "#0bef5b",
12+
"simpleCodingTimeTracker.statusBar.backgroundStyle": "remote"
1013
}

package.json

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "simple-coding-time-tracker",
33
"displayName": "Simple Coding Time Tracker",
44
"description": "Track and visualize your coding time across projects",
5-
"version": "0.6.6",
5+
"version": "0.6.7",
66
"publisher": "noorashuvo",
77
"license": "MIT",
88
"icon": "icon-sctt.png",
@@ -39,6 +39,22 @@
3939
"default": true,
4040
"description": "Show seconds in the status bar time display (disable to reduce distractions)"
4141
},
42+
"simpleCodingTimeTracker.statusBar.icon": {
43+
"type": "string",
44+
"default": "$(code)",
45+
"description": "Icon or Codicon name to display before the timer (e.g., '$(code)', '$(clock)', '$(rocket)', or any emoji)"
46+
},
47+
"simpleCodingTimeTracker.statusBar.backgroundStyle": {
48+
"type": "string",
49+
"enum": ["default", "warning", "error"],
50+
"default": "warning",
51+
"description": "Background style for the timer in the status bar: default (theme), warning (yellow), or error (red)."
52+
},
53+
"simpleCodingTimeTracker.statusBar.color": {
54+
"type": "string",
55+
"default": "",
56+
"description": "Custom color for the timer text in hex format (e.g., '#FFAA00') or a VS Code theme color reference"
57+
},
4258
"simpleCodingTimeTracker.health.enableNotifications": {
4359
"type": "boolean",
4460
"default": false,

src/settingsView.ts

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ export class SettingsViewProvider {
1010

1111
public show() {
1212
if (this.panel) {
13+
// Refresh HTML so newly added settings render for existing panels
14+
this.panel.webview.html = this.getHtmlContent();
15+
void this.sendCurrentSettings();
1316
this.panel.reveal(vscode.ViewColumn.One);
1417
return;
1518
}
@@ -58,6 +61,9 @@ export class SettingsViewProvider {
5861
inactivityTimeout: config.get('inactivityTimeout', 2.5),
5962
focusTimeout: config.get('focusTimeout', 3),
6063
statusBarShowSeconds: config.get('statusBar.showSeconds', true),
64+
statusBarIcon: config.get('statusBar.icon', '💻'),
65+
statusBarBackgroundStyle: config.get('statusBar.backgroundStyle', 'warning'),
66+
statusBarColor: config.get('statusBar.color', ''),
6167
healthEnableNotifications: config.get('health.enableNotifications', true),
6268
healthModalNotifications: config.get('health.modalNotifications', true),
6369
healthEyeRestInterval: config.get('health.eyeRestInterval', 20),
@@ -80,6 +86,9 @@ export class SettingsViewProvider {
8086
await config.update('inactivityTimeout', settings.inactivityTimeout, vscode.ConfigurationTarget.Workspace);
8187
await config.update('focusTimeout', settings.focusTimeout, vscode.ConfigurationTarget.Workspace);
8288
await config.update('statusBar.showSeconds', settings.statusBarShowSeconds, vscode.ConfigurationTarget.Workspace);
89+
await config.update('statusBar.icon', settings.statusBarIcon, vscode.ConfigurationTarget.Workspace);
90+
await config.update('statusBar.backgroundStyle', settings.statusBarBackgroundStyle, vscode.ConfigurationTarget.Workspace);
91+
await config.update('statusBar.color', settings.statusBarColor, vscode.ConfigurationTarget.Workspace);
8392
await config.update('health.enableNotifications', settings.healthEnableNotifications, vscode.ConfigurationTarget.Workspace);
8493
await config.update('health.modalNotifications', settings.healthModalNotifications, vscode.ConfigurationTarget.Workspace);
8594
await config.update('health.eyeRestInterval', settings.healthEyeRestInterval, vscode.ConfigurationTarget.Workspace);
@@ -190,7 +199,7 @@ export class SettingsViewProvider {
190199
line-height: 1.4;
191200
}
192201
193-
input[type="number"] {
202+
input[type="number"], input[type="text"], select {
194203
width: 100%;
195204
max-width: 200px;
196205
padding: 6px 10px;
@@ -201,10 +210,25 @@ export class SettingsViewProvider {
201210
font-size: 13px;
202211
}
203212
204-
input[type="number"]:focus {
213+
input[type="number"]:focus, input[type="text"]:focus, select:focus {
205214
outline: 1px solid var(--vscode-focusBorder);
206215
}
207216
217+
.color-row {
218+
display: flex;
219+
align-items: center;
220+
gap: 10px;
221+
max-width: 320px;
222+
}
223+
224+
input[type="color"] {
225+
width: 42px;
226+
height: 32px;
227+
padding: 0;
228+
border: 1px solid var(--vscode-input-border);
229+
background: transparent;
230+
}
231+
208232
.checkbox-container {
209233
display: flex;
210234
align-items: center;
@@ -300,6 +324,34 @@ export class SettingsViewProvider {
300324
</div>
301325
<div class="description">Display seconds in the status bar time (HH:MM:SS). Disable to reduce distractions and show only hours and minutes.</div>
302326
</div>
327+
328+
<div class="setting-item">
329+
<label for="statusBarIcon">Status Bar Icon</label>
330+
<div class="description">Icon or Codicon name to display before the timer. Examples: '💻' (default), '$(clock)', '$(rocket)', or any emoji.</div>
331+
<input type="text" id="statusBarIcon" placeholder="💻" style="width: 100%; max-width: 300px;" />
332+
<div class="range-info">Supports emojis, text, or VS Code Codicons like $(clock)</div>
333+
</div>
334+
335+
<div class="setting-item">
336+
<label for="statusBarBackgroundStyle">Status Bar Background</label>
337+
<div class="description">Choose the background style for the timer: use the default status bar color, a warning highlight, or an error highlight.</div>
338+
<select id="statusBarBackgroundStyle" style="max-width: 220px;">
339+
<option value="default">Default (theme)</option>
340+
<option value="warning">Warning (yellow)</option>
341+
<option value="error">Error (red)</option>
342+
</select>
343+
<div class="range-info">Matches VS Code theme colors: status bar default, warning, or error background</div>
344+
</div>
345+
346+
<div class="setting-item">
347+
<label for="statusBarColor">Status Bar Font Color</label>
348+
<div class="description">Pick a font color for the timer text, or enter a hex/theme color. Leave empty for default.</div>
349+
<div class="color-row">
350+
<input type="color" id="statusBarColorPicker" aria-label="Pick status bar font color" />
351+
<input type="text" id="statusBarColor" placeholder="#FFAA00 or theme color" style="flex: 1; max-width: 260px;" />
352+
</div>
353+
<div class="range-info">Examples: #FFAA00 (orange), #00FF00 (green), or theme color (e.g., editor.foreground). Leave blank for default.</div>
354+
</div>
303355
</div>
304356
305357
<div class="setting-group">
@@ -384,6 +436,9 @@ export class SettingsViewProvider {
384436
document.getElementById('inactivityTimeout').value = settings.inactivityTimeout;
385437
document.getElementById('focusTimeout').value = settings.focusTimeout;
386438
document.getElementById('statusBarShowSeconds').checked = settings.statusBarShowSeconds;
439+
document.getElementById('statusBarIcon').value = settings.statusBarIcon;
440+
document.getElementById('statusBarBackgroundStyle').value = settings.statusBarBackgroundStyle;
441+
syncTextAndPicker(settings.statusBarColor || '');
387442
document.getElementById('healthEnableNotifications').checked = settings.healthEnableNotifications;
388443
document.getElementById('healthModalNotifications').checked = settings.healthModalNotifications;
389444
document.getElementById('healthEyeRestInterval').value = settings.healthEyeRestInterval;
@@ -397,6 +452,9 @@ export class SettingsViewProvider {
397452
inactivityTimeout: parseFloat(document.getElementById('inactivityTimeout').value),
398453
focusTimeout: parseFloat(document.getElementById('focusTimeout').value),
399454
statusBarShowSeconds: document.getElementById('statusBarShowSeconds').checked,
455+
statusBarIcon: document.getElementById('statusBarIcon').value,
456+
statusBarBackgroundStyle: document.getElementById('statusBarBackgroundStyle').value,
457+
statusBarColor: document.getElementById('statusBarColor').value,
400458
healthEnableNotifications: document.getElementById('healthEnableNotifications').checked,
401459
healthModalNotifications: document.getElementById('healthModalNotifications').checked,
402460
healthEyeRestInterval: parseInt(document.getElementById('healthEyeRestInterval').value),
@@ -406,6 +464,49 @@ export class SettingsViewProvider {
406464
};
407465
}
408466
467+
const hexRegex = /^#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$/;
468+
469+
function normalizeHexToSix(value) {
470+
const trimmed = value.trim();
471+
if (!trimmed) return '';
472+
const withHash = trimmed.startsWith('#') ? trimmed : '#' + trimmed;
473+
if (!hexRegex.test(withHash)) return '';
474+
const hex = withHash.replace('#', '');
475+
if (hex.length === 3) {
476+
return '#' + hex.split('').map(c => c + c).join('').toLowerCase();
477+
}
478+
return '#' + hex.toLowerCase();
479+
}
480+
481+
function syncTextAndPicker(value) {
482+
const textInput = document.getElementById('statusBarColor');
483+
const pickerInput = document.getElementById('statusBarColorPicker');
484+
textInput.value = value;
485+
const normalized = normalizeHexToSix(value);
486+
if (normalized) {
487+
pickerInput.value = normalized;
488+
} else {
489+
pickerInput.value = '#ffaa00'; // fallback visible color
490+
}
491+
}
492+
493+
// Keep text input and picker in sync
494+
document.addEventListener('DOMContentLoaded', () => {
495+
const textInput = document.getElementById('statusBarColor');
496+
const pickerInput = document.getElementById('statusBarColorPicker');
497+
498+
pickerInput.addEventListener('input', () => {
499+
textInput.value = pickerInput.value;
500+
});
501+
502+
textInput.addEventListener('input', () => {
503+
const normalized = normalizeHexToSix(textInput.value);
504+
if (normalized) {
505+
pickerInput.value = normalized;
506+
}
507+
});
508+
});
509+
409510
document.getElementById('saveButton').addEventListener('click', () => {
410511
const settings = getSettings();
411512
vscode.postMessage({

0 commit comments

Comments
 (0)