diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..6948f59 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,7 @@ +{ + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + } +} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index bca47d1..ae4139d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,112 +1,211 @@ name: Publish Extension to Visual Studio Marketplace +run-name: Publish VS Code Extension on: push: - branches: - - main - paths-ignore: - - '.github/workflows/**' # Ignore changes to workflow files - workflow_dispatch: # Allow manual trigger + branches: [main] + paths-ignore: ['.github/workflows/**'] + workflow_dispatch: jobs: - verify-changes: + validate: + name: Validate Changes runs-on: ubuntu-latest outputs: - should_publish: ${{ steps.check-changes.outputs.should_publish }} - + should_publish: ${{ steps.changes.outputs.should_publish }} + version: ${{ steps.version.outputs.current }} + skip_reason: ${{ steps.changes.outputs.skip_reason }} + has_code_changes: ${{ steps.changes.outputs.has_code_changes }} + steps: - name: Checkout code uses: actions/checkout@v4 with: - fetch-depth: 0 # Fetch all history for all tags and branches - - - name: Get changed files - id: changed-files + fetch-depth: 2 + - name: Checked out code + run: | + echo "βœ… Code checked out. Current directory: $(pwd)" + echo "Files:" + ls -la + + - name: Get version info + id: version run: | - echo "files=$(git diff --name-only ${{ github.event.before }} ${{ github.event.after }} | tr '\n' ' ')" >> $GITHUB_OUTPUT - - - name: Check version change and file types - id: check-changes + echo "πŸ” Reading package.json version..." + CURRENT=$(node -p "require('./package.json').version") + echo "current=$CURRENT" >> $GITHUB_OUTPUT + echo "✨ Current version: $CURRENT" + echo "πŸ“¦ Package.json contents:" + cat package.json + - name: Version info + run: | + echo "Version info step complete." + + - name: Analyze changes + id: changes run: | - # Check if package.json version changed - VERSION_CHANGED=$(git diff ${{ github.event.before }} ${{ github.event.after }} package.json | grep '+\s*"version"') - - # Get list of changed files - CHANGED_FILES="${{ steps.changed-files.outputs.files }}" - - # Check if only documentation files were changed - DOCS_ONLY=true - for file in $CHANGED_FILES; do - if [[ ! $file =~ \.(md|txt|doc|docx)$ ]] && [[ ! $file =~ ^docs/ ]]; then - DOCS_ONLY=false - break + echo "πŸ”Ž Analyzing changes between commits..." + VERSION_UPDATED=$(git diff HEAD^ HEAD -- package.json | grep -q '"version":' && echo "true" || echo "false") + CODE_CHANGES=$(git diff --name-only HEAD^ HEAD | \ + grep -vE '\\.(md|txt)$|^docs/|^\\.github/|^\\.vscode/|package\\.json$' || echo "") + DOC_CHANGES=$(git diff --name-only HEAD^ HEAD | \ + grep -E '\\.(md|txt)$|^docs/' || echo "") + echo "has_code_changes=${CODE_CHANGES:+true}" >> $GITHUB_OUTPUT + echo "has_doc_changes=${DOC_CHANGES:+true}" >> $GITHUB_OUTPUT + echo "Code changes: $CODE_CHANGES" + echo "Doc changes: $DOC_CHANGES" + if [[ "$VERSION_UPDATED" == "true" ]]; then + if [[ -n "$CODE_CHANGES" ]]; then + echo "should_publish=true" >> $GITHUB_OUTPUT + echo "skip_reason=" >> $GITHUB_OUTPUT + echo "βœ… Publishing needed: Version update detected with code changes" + else + echo "should_publish=false" >> $GITHUB_OUTPUT + echo "skip_reason=Version update without code changes" >> $GITHUB_OUTPUT + echo "⚠️ Version update detected but no code changes found" fi - done - - # Initialize should_publish as false - echo "should_publish=false" >> $GITHUB_OUTPUT - - if [[ "$DOCS_ONLY" == "true" ]]; then - echo "ℹ️ Only documentation files were changed" - echo "should_publish=false" >> $GITHUB_OUTPUT - elif [[ -z "$VERSION_CHANGED" ]]; then - echo "❌ Version in package.json was not updated" - echo "should_publish=false" >> $GITHUB_OUTPUT else - echo "βœ… Version changed and non-documentation files modified" - echo "should_publish=true" >> $GITHUB_OUTPUT + if [[ -n "$CODE_CHANGES" ]]; then + echo "should_publish=false" >> $GITHUB_OUTPUT + echo "skip_reason=Code changes without version update" >> $GITHUB_OUTPUT + echo "❌ Code changes detected without version update" + exit 1 + else + echo "should_publish=false" >> $GITHUB_OUTPUT + if [[ -n "$DOC_CHANGES" ]]; then + echo "skip_reason=Documentation only changes" >> $GITHUB_OUTPUT + echo "ℹ️ Documentation only changes detected" + else + echo "skip_reason=No significant changes" >> $GITHUB_OUTPUT + echo "ℹ️ No significant changes detected" + fi + fi fi + - name: Analyze changes + run: echo "Analyze changes step complete." - publish: - needs: verify-changes - if: needs.verify-changes.outputs.should_publish == 'true' + build-and-package: + name: Build and Package + needs: validate + if: needs.validate.outputs.should_publish == 'true' runs-on: ubuntu-latest - environment: VSC EXT - + outputs: + vsix_path: ${{ steps.package.outputs.path }} steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: '18' - - - name: Install dependencies - run: npm ci - - - name: Compile - run: npm run compile - - - name: Install vsce - run: npm install -g @vscode/vsce - - - name: Clean existing VSIX files - run: rm -f *.vsix - - - name: Package Extension - run: vsce package - - - name: Get Package Info + - name: Install and build + run: | + npm ci + npm run compile + npm install -g @vscode/vsce + - name: Package extension id: package run: | - VERSION=$(node -p "require('./package.json').version") - VSIX_PATH="simple-coding-time-tracker-${VERSION}.vsix" - if [ ! -f "$VSIX_PATH" ]; then - echo "Error: Expected VSIX file $VSIX_PATH not found" + VERSION="${{ needs.validate.outputs.version }}" + VSIX_NAME="simple-coding-time-tracker-${VERSION}.vsix" + rm -f *.vsix + vsce package + if [ ! -f "$VSIX_NAME" ]; then + echo "::error::Failed to create VSIX package" exit 1 fi - echo "Package created: $VSIX_PATH" - echo "vsix_path=$VSIX_PATH" >> $GITHUB_OUTPUT - echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT - echo "version=$VERSION" >> $GITHUB_OUTPUT - - - name: Upload VSIX as artifact + echo "path=$VSIX_NAME" >> $GITHUB_OUTPUT + echo "βœ… Package created: $VSIX_NAME" + - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: extension-release-v${{ steps.package.outputs.version }}-${{ steps.package.outputs.date }} - path: "*.vsix" - retention-days: 99 # Maximum retention period allowed by GitHub - - - name: Publish to Visual Studio Marketplace - run: vsce publish -p "${{ secrets.VSC_PAT }}" \ No newline at end of file + name: extension-v${{ needs.validate.outputs.version }} + path: ${{ steps.package.outputs.path }} + retention-days: 90 + + publish: + name: Publish to Marketplace + needs: [validate, build-and-package] + runs-on: ubuntu-latest + # environment: vscode-marketplace + outputs: + status: ${{ steps.publish.outputs.status }} + error_detail: ${{ steps.publish.outputs.error_detail }} + + steps: + - name: Download package + uses: actions/download-artifact@v4 + with: + name: extension-v${{ needs.validate.outputs.version }} + + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Install vsce + run: | + npm install -g @vscode/vsce + vsce --version + + - name: Diagnostic check + run: | + echo "πŸ” Environment Information" + echo "Node version: $(node -v)" + echo "NPM version: $(npm -v)" + echo "VSCE version: $(vsce --version)" + echo "Package.json version: $(node -p 'require(\'./package.json\').version' 2>/dev/null || echo 'Not found')" + echo "Current directory: $(pwd)" + ls -la + echo "VSIX files:" + ls -lh *.vsix || echo "No VSIX file found" + + - name: Publish to marketplace + id: publish + continue-on-error: true + env: + VSC_PAT: ${{ secrets.VSC_PAT }} + run: | + VERSION="${{ needs.validate.outputs.version }}" + echo "πŸ” Publish v${VERSION}" + echo "VSC_PAT is ${VSC_PAT:+set}" + + echo "πŸš€ Running vsce publish..." + echo "VSIX files before publish:" + ls -lh *.vsix || echo "No VSIX file found" + echo "Package.json present: $(test -f package.json && echo yes || echo no)" + + OUTPUT=$(vsce publish -p "$VSC_PAT" 2>&1) + EXIT_CODE=$? + + echo "πŸ“ vsce publish output:" + echo "$OUTPUT" + echo "Exit code: $EXIT_CODE" + + if [ $EXIT_CODE -eq 0 ]; then + echo "status=success" >> $GITHUB_OUTPUT + echo "βœ… Publish succeeded." + elif [[ "$OUTPUT" == *"already exists"* ]]; then + echo "status=exists" >> $GITHUB_OUTPUT + echo "ℹ️ Version already exists." + else + echo "status=failed" >> $GITHUB_OUTPUT + ERROR_DETAIL=$(echo "$OUTPUT" | grep -v "error" | tail -n 1) + echo "error_detail=$ERROR_DETAIL" >> $GITHUB_OUTPUT + echo "❌ Publish failed: $ERROR_DETAIL" + fi + + - name: Publish step complete + run: echo "Publish step complete." + + - name: Create and push Git tag + if: steps.publish.outputs.status == 'success' + run: | + VERSION="${{ needs.validate.outputs.version }}" + echo "🏷️ Creating git tag v${VERSION}..." + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "GitHub Actions" + git tag -a "v${VERSION}" -m "Release v${VERSION}" + git push origin "v${VERSION}" + echo "βœ… Git tag pushed." + + - name: Tag step complete + if: steps.publish.outputs.status == 'success' + run: echo "Tag step complete." + diff --git a/.github/workflows/repo-stats.yml b/.github/workflows/repo-stats.yml new file mode 100644 index 0000000..145d020 --- /dev/null +++ b/.github/workflows/repo-stats.yml @@ -0,0 +1,29 @@ +name: Repository Statistics +run-name: πŸ“Š Collect Repository Metrics + +on: + schedule: + # Runs every 2nd day at 02:00 UTC (less busy time) + - cron: '0 2 */2 * *' + workflow_dispatch: # Allow manual trigger for testing + +permissions: + contents: write # Needed to write stats data + pull-requests: read + issues: read + +jobs: + repo-stats: + name: Generate Repository Statistics + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Generate repository statistics + uses: jgehrcke/github-repo-stats@RELEASE + with: + repository: ${{ github.repository }} + ghtoken: ${{ secrets.REPO_STATS_TOKEN }} # Use custom PAT instead + databranch: stats-data \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9209ef5..196b679 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ node_modules out +**/*.code-workspace +dist \ No newline at end of file diff --git a/.vscodeignore b/.vscodeignore index f8d4b3e..b0891b5 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -1,20 +1,38 @@ +# Development directories .vscode/** .vscode-test/** src/** out/test/** test/** scripts/** -images/** + +# Source control +.git/** +.github/** .gitignore + +# Configuration files .yarnrc tsconfig.json .eslintrc.json +webpack.config.js + +# Build artifacts **/*.map **/*.ts +**/*.vsix + +# Documentation (except README) CONTRIBUTING.md TECHNICAL.md -.github/** -**/*.vsix -webpack.config.js + +# Dependencies (except type definitions) node_modules/** -!node_modules/@types/ \ No newline at end of file +!node_modules/@types/ + +# Keep these files +!LICENSE +!README.md +!package.json +!icon-sctt.png +!out/src/**/*.js diff --git a/README.md b/README.md index 307b759..700294a 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,9 @@ Simple Coding Time Tracker is a powerful extension for Visual Studio Code that h ## Features - **Automatic Time Tracking**: Seamlessly tracks your coding time in the background. -- **Project-based Tracking**: Organizes time data by project for easy analysis. +- **Project and Branch Tracking**: Organizes time data by project and Git branches for comprehensive analysis. - **Smart Activity Detection**: Automatically pauses tracking during periods of inactivity. -- **Status Bar Display**: Shows your today's total coding time duration in real-time. -- **Tooltip on Status Bar**: Shows the total coding time weekly, monthly and all time basis. +- **Focused Work Detection**: Intelligently tracks time even when VS Code isn't focused. - **Interactive Data Visualization**: - Project Summary Chart: Visual breakdown of time spent on each project - Daily Activity Timeline: Interactive line chart showing your coding patterns @@ -22,10 +21,6 @@ Simple Coding Time Tracker is a powerful extension for Visual Studio Code that h - Project Filtering: Focus on specific projects - Quick Reset: One-click reset for search filters - **Data Persistence**: Safely stores your time data for long-term analysis. -- **Configurable Settings**: - - Save Interval: Customize how often your coding time data is saved (default: 5 seconds) - - Inactivity Timeout: Set how long to wait before stopping the timer when no activity is detected (default: 5 minutes) - - Focus Timeout: Set how long to continue tracking after VS Code loses focus (default: 60 seconds) ## Installation @@ -42,10 +37,15 @@ Once installed, the extension will automatically start tracking your coding time 1. In the summary view, locate the search form 2. Select a date range using the date pickers -3. Optionally choose a specific project from the dropdown +3. Filter by project and/or branch: + - Choose a specific project to see all its branches + - Select a branch to see time data for that specific branch + - The branch dropdown automatically updates to show only branches from the selected project 4. Click "Search" to apply filters 5. Use "Reset" to clear all filters and refresh the view +The charts and visualizations will automatically update to reflect your selected project and branch filters. + ### Configuration Options You can customize the extension's behavior through VS Code settings: @@ -56,24 +56,19 @@ You can customize the extension's behavior through VS Code settings: - **Save Interval**: How often to save your coding time data (in seconds) - Default: 5 seconds - Lower values provide more frequent updates but may impact performance - - Higher values are more efficient but update less frequently - - **Inactivity Timeout**: How long to wait before stopping the timer when no activity is detected (in minutes) - - Default: 5 minutes + - Higher values are more efficient but update less frequently + - **Inactivity Timeout**: How long to wait before stopping the timer when no activity is detected but you are focused on VS Code (in seconds) + - Default: 150 seconds (2.5 minutes) - Lower values will stop tracking sooner when you're not actively coding - Higher values will continue tracking for longer during breaks + - **Focus Timeout**: How long to continue tracking after VS Code loses focus (in seconds) + - Default: 180 seconds (3 minutes) + - Determines how long to keep tracking when you switch to other applications + - Useful for when you're referencing documentation or testing your application + - **Week Start Day**: The first day of the week + - Default: Sunday + - Determines the first day of the week -### Available Commands - -The extension provides the following commands through the Command Palette: - -- **Show Summary** (`SCTT: Show Coding Time Summary`): - Displays a comprehensive summary of your coding activity with interactive charts and visualizations. - -- **Reset Timer for Today** (`SCTT: Reset Coding Timer for Today`): - Resets the coding time tracker for the current day, allowing you to start anew. - -- **Reset All Timers** (`SCTT: Reset All Coding Timers`): - Resets all coding time trackers with a confirmation prompt to prevent unintended resets. ## Screenshots @@ -85,37 +80,27 @@ The summary page provides a detailed report of your coding activity with interac - Theme-aware visualizations that adapt to your VS Code theme - Advanced search and filtering capabilities -![Coding Summary](./images/coding_summary.png) +![Coding Summary](https://raw.githubusercontent.com/twentyTwo/static-file-hosting/main/vsc-ext-coding-time-tracker-files/sctt-light.png) #### Dark theme -![Coding Summary Dark Theme](./images/summry_blck.png) +![Coding Summary Dark Theme](https://raw.githubusercontent.com/twentyTwo/static-file-hosting/main/vsc-ext-coding-time-tracker-files/sctt-dark.png)) -#### Filtering options -![Filter](./images/filter_summry.png) #### Status Bar Status bar resets to zero at midnight each day and hence shows the coding time for the current day. -![Status Bar](./images/statusbar.png) +![Status Bar](https://raw.githubusercontent.com/twentyTwo/static-file-hosting/main/vsc-ext-coding-time-tracker-files/statusbar.png) #### Tooltip Tooltip shows the total coding time weekly, monthly and all time basis. -![Tooltip](./images/tooltip.png) +![Tooltip](https://raw.githubusercontent.com/twentyTwo/static-file-hosting/main/vsc-ext-coding-time-tracker-files/tooltip.png) #### Automatic Pause/Resume When the user is inactive for a period of time, the timer automatically pauses and resumes when the user starts typing again coding again. -![Pause/Resume icon](./images/paused_time.png) - -It is configurable from the settings. Default value is 5 minutes. -![Settings](./images/settings.png) +![Pause/Resume icon](https://raw.githubusercontent.com/twentyTwo/static-file-hosting/main/vsc-ext-coding-time-tracker-files/paused_time.png) -### All Command Palette Commands -There are total 3 commands in the command palette available for this extension. +#### Settings +![Settings](https://raw.githubusercontent.com/twentyTwo/static-file-hosting/main/vsc-ext-coding-time-tracker-files/settings.png) -1. SCTT: Show Coding Time Summary -2. SCTT: Reset Coding Timer for Today -3. SCTT: Reset All Coding Timers - -![All Command Palette Commands](./images/commands.png) ## Technical Documentation @@ -123,6 +108,19 @@ For technical details about development, release process, and internal architect ## Changelog +### [0.4.0] - 2025-06-06 +- Added Git branch tracking to monitor time spent on different branches +- Enhanced project view with branch-specific time tracking +- Implemented dynamic branch filtering based on selected project +- Improved charts to show time distribution across branches +- Added branch-specific data in search results and visualizations + +### [0.3.9] - 2025-05-25 +- Added Focus Timeout setting to intelligently track time when VS Code loses focus +- Fixed version tracking in GitHub Actions workflow to prevent publishing issues +- Updated documentation to clarify timeout settings and their purposes +- Enhanced error handling in the publishing workflow + ### [0.3.4] - 2025-04-19 - Handle multi-root workspaces, external files, and virtual files more effectively. - Added a verify-changes job to check if a version update is required and ensure non-documentation files are modified before publishing. This prevents unnecessary releases. diff --git a/TIME_TRACKING.md b/TIME_TRACKING.md new file mode 100644 index 0000000..d8deb9a --- /dev/null +++ b/TIME_TRACKING.md @@ -0,0 +1,183 @@ +# Simple Coding Time Tracker - How It Works + +This document explains how the time tracking mechanism works in Simple Coding Time Tracker (SCTT). It's written for both developers who want to understand the code and users who want to know what's happening behind the scenes. + +## Core Concepts + +### Time Units and Storage +- Time is tracked in **minutes** +- Data is stored as `TimeEntry` objects with: + - Date: When the coding session occurred + - Project: Which project was being worked on + - Branch: Which git branch was active + - TimeSpent: Duration in minutes + +### Key Variables That Control Tracking + +```typescript +// These are the main timing control variables +saveIntervalSeconds = 5 // How often to save time entries +inactivityTimeoutSeconds = 300 // Stop tracking after 5 mins of inactivity +focusTimeoutSeconds = 60 // Continue tracking for 1 min after losing focus +``` + +## How Time Tracking Works + +### 1. Starting a Tracking Session + +A tracking session starts when: +- You start typing/moving cursor +- VS Code window gains focus +- You switch to a different file + +The tracker records: +- Start time (`startTime`) +- Current project (`currentProject`) +- Current git branch (`currentBranch`) + +### 2. During Active Tracking + +The tracker maintains three important intervals: + +1. **Update Interval (1 second)** + - Runs every second + - Updates internal state + - Used for real-time UI updates + +2. **Save Interval (5 seconds by default)** + - Saves your current session to database + - Creates a new time entry + - Resets the start time + +3. **Activity Monitoring** + - Tracks cursor movements, typing, file changes + - Updates `lastCursorActivity` timestamp + - Used to detect inactivity + +### 3. Project & Branch Tracking + +The tracker automatically handles: + +- **Project Changes** + - Detects when you switch between projects + - Saves time separately for each project + - Multi-root workspace aware + +- **Branch Changes** + - Monitors git branch changes + - Creates separate time entries per branch + - Perfect for tracking time across feature branches + +### 4. When Tracking Stops + +Tracking stops in these scenarios: + +1. **Inactivity** (after 5 minutes by default) + - No cursor movement + - No typing + - No file changes + +2. **Lost Focus** (after 1 minute by default) + - Switched to another application + - Can be configured to wait longer + +3. **Manual Stop** + - VS Code is closed + - Extension is disabled + +### Database Structure + +Time entries are stored with this structure: +```typescript +interface TimeEntry { + date: string; // ISO date string (YYYY-MM-DD) + project: string; // Project/workspace name + timeSpent: number; // Duration in minutes + branch: string; // Git branch name +} +``` + +## Configuration Options + +You can customize tracking behavior: + +1. **Save Interval** (`simpleCodingTimeTracker.saveInterval`) + - How often to save time entries + - Default: 5 seconds + - Lower = More accurate but more writes + +2. **Inactivity Timeout** (`simpleCodingTimeTracker.inactivityTimeout`) + - How long to wait before stopping due to inactivity + - Default: 300 seconds (5 minutes) + - Higher = More lenient with breaks + +3. **Focus Timeout** (`simpleCodingTimeTracker.focusTimeout`) + - How long to continue tracking after losing window focus + - Default: 60 seconds (1 minute) + - Higher = Better for quick app switches + +## Tips for Accurate Tracking + +1. **Keep VS Code Focused** + - Tracking is most accurate when VS Code remains your active window + - Use the focus timeout setting if you frequently switch apps + +2. **Branch Awareness** + - Create branches for different features + - Time is tracked separately per branch + - Great for client billing + +3. **Project Organization** + - Use separate VS Code windows for different projects + - Or use multi-root workspaces for clean separation + +## Common Scenarios + +1. **Multiple Projects Open** + ``` + Project A (main) β†’ 30 mins + Project B (feature) β†’ 45 mins + ``` + Each gets tracked separately + +2. **Branch Switching** + ``` + main β†’ 1 hour + feature/xyz β†’ 30 mins + ``` + Time is split accurately per branch + +3. **Taking Breaks** + ``` + 10:00 - Start coding + 10:30 - Take break (no activity) + 10:35 - Tracking auto-stops + 10:40 - Resume coding (auto-starts) + ``` + +## Behind-the-Scenes Logic + +1. **Activity Detection** + - Monitors VS Code events + - Cursor movements + - Text changes + - File switches + - Git operations + +2. **Time Calculation** + ```typescript + duration = (currentTime - startTime) / 60000 // Convert ms to minutes + ``` + +3. **Session Management** + - Small sessions are combined + - Inactive periods are excluded + - Branch/project switches create new sessions + +## Developer Notes + +Key files and their roles: +- `timeTracker.ts`: Core tracking logic +- `database.ts`: Time entry storage +- `statusBar.ts`: Real-time UI updates +- `summaryView.ts`: Time statistics and reports diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..ed017d0 --- /dev/null +++ b/TODO.md @@ -0,0 +1,33 @@ +# TODO List + +Quick capture for ideas and tasks while coding. +## Guidelines +- Use checkboxes `- [ ]` for actionable items +- Use bullet points `-` for ideas and thoughts +- Add date in parentheses (YYYY-MM-DD) when the task is done +- Keep it simple and informal - this is your working todo list + +## Immediate Tasks +- [X] Listen for branch changes in real-time , save immediately when branch changes , start new session with new branch +- [X] Add branch name to time entries +- [ ] Update UI to show current branch in status bar +- [ ] Branch dropdown in summary view +- [ ] Add branch filter in time tracking summary +- [ ] Handle missing branch data in older time entries +- [ ] Fix race condition in branch detection during quick switches +- [ ] Add proper handling for detached HEAD state +- [ ] Implement data migration strategy for existing entries + +## Ideas +- Maybe add a quick pause button in status bar? +- A variable maybe for tracking the active coding session duration, and it will notify when it exceeds a certain threshold. + +## Bug Fixes + + +## Ideas +- Add branch history view in time tracking summary +- Show branch switching patterns in analytics +- Add branch-specific coding time statistics + + diff --git a/images/coding_summary.png b/images/coding_summary.png deleted file mode 100644 index a26664c..0000000 Binary files a/images/coding_summary.png and /dev/null differ diff --git a/images/commands.png b/images/commands.png deleted file mode 100644 index 4ca885a..0000000 Binary files a/images/commands.png and /dev/null differ diff --git a/images/filter_summry.png b/images/filter_summry.png deleted file mode 100644 index cef0e83..0000000 Binary files a/images/filter_summry.png and /dev/null differ diff --git a/images/paused_time.png b/images/paused_time.png deleted file mode 100644 index 826222e..0000000 Binary files a/images/paused_time.png and /dev/null differ diff --git a/images/settings.png b/images/settings.png deleted file mode 100644 index f715e69..0000000 Binary files a/images/settings.png and /dev/null differ diff --git a/images/statusbar.png b/images/statusbar.png deleted file mode 100644 index cdc2772..0000000 Binary files a/images/statusbar.png and /dev/null differ diff --git a/images/statusbar_click.png b/images/statusbar_click.png deleted file mode 100644 index c5bb9ab..0000000 Binary files a/images/statusbar_click.png and /dev/null differ diff --git a/images/summry_blck.png b/images/summry_blck.png deleted file mode 100644 index 334ce31..0000000 Binary files a/images/summry_blck.png and /dev/null differ diff --git a/images/tooltip.png b/images/tooltip.png deleted file mode 100644 index 2f52813..0000000 Binary files a/images/tooltip.png and /dev/null differ diff --git a/package-lock.json b/package-lock.json index 38bc4a4..22fb9cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,16 @@ { "name": "simple-coding-time-tracker", - "version": "0.3.6", + "version": "0.3.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "simple-coding-time-tracker", - "version": "0.3.6", + "version": "0.3.9", "license": "MIT", + "dependencies": { + "simple-git": "^3.27.0" + }, "devDependencies": { "@types/node": "^14.14.37", "@types/vscode": "^1.60.0", @@ -252,6 +255,21 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1047,7 +1065,6 @@ "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, "dependencies": { "ms": "^2.1.3" }, @@ -2035,8 +2052,7 @@ "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/natural-compare": { "version": "1.4.0", @@ -2538,6 +2554,21 @@ "node": ">=8" } }, + "node_modules/simple-git": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.27.0.tgz", + "integrity": "sha512-ivHoFS9Yi9GY49ogc6/YAi3Fl9ROnF4VyubNylgCkA+RVqLaKWnDSzXOVzya8csELIaWaYNutsEuAhZrtOjozA==", + "license": "MIT", + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.3.5" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", diff --git a/package.json b/package.json index a349c99..60426b7 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "simple-coding-time-tracker", "displayName": "Simple Coding Time Tracker", "description": "Track and visualize your coding time across projects", - "version": "0.3.9", + "version": "0.4.4", "publisher": "noorashuvo", "license": "MIT", "icon": "icon-sctt.png", @@ -22,7 +22,7 @@ "properties": { "simpleCodingTimeTracker.saveInterval": { "type": "number", - "default": 5, + "default": 30, "description": "How often to save coding time data (in seconds)" }, "simpleCodingTimeTracker.inactivityTimeout": { @@ -34,15 +34,39 @@ "type": "number", "default": 180, "description": "If you switch away from VS Code, continue counting as coding time for up to this many seconds before pausing." + }, + "simpleCodingTimeTracker.weekStartDay": { + "type": "string", + "default": "Sunday", + "enum": [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday" + ], + "description": "The first day of a week." } } - }, "commands": [ + }, + "commands": [ { "command": "simpleCodingTimeTracker.showSummary", "title": "SCTT: Show Coding Time Summary" + }, + { + "command": "simpleCodingTimeTracker.viewStorageData", + "title": "SCTT: View Time Tracking Data" + }, + { + "command": "coding-time-tracker.clearAllData", + "title": "Clear All Time Tracking Data" } ] - }, "scripts": { + }, + "scripts": { "vscode:prepublish": "npm run package", "compile": "webpack", "watch": "webpack --watch", @@ -64,8 +88,11 @@ "webpack": "^5.99.8", "webpack-cli": "^6.0.1" }, + "dependencies": { + "simple-git": "^3.27.0" + }, "repository": { "type": "git", "url": "https://github.com/twentyTwo/vsc-ext-coding-time-tracker.git" } -} +} \ No newline at end of file diff --git a/simple-coding-time-tracker-0.3.6.vsix b/simple-coding-time-tracker-0.3.6.vsix deleted file mode 100644 index adaf35d..0000000 Binary files a/simple-coding-time-tracker-0.3.6.vsix and /dev/null differ diff --git a/src/database.ts b/src/database.ts index e2bf5ed..e12cf84 100644 --- a/src/database.ts +++ b/src/database.ts @@ -4,11 +4,13 @@ export interface TimeEntry { date: string; project: string; timeSpent: number; + branch: string; } export interface SummaryData { dailySummary: { [date: string]: number }; projectSummary: { [project: string]: number }; + branchSummary: { [branch: string]: number }; totalTime: number; } @@ -24,6 +26,22 @@ export class Database { } // Load entries into memory this.entries = this.context.globalState.get('timeEntries', []); + + // Migrate existing entries to include branch if needed + this.migrateEntries(); + } + + private async migrateEntries() { + if (this.entries && this.entries.length > 0) { + const needsMigration = this.entries.some(entry => !('branch' in entry)); + if (needsMigration) { + const migratedEntries = this.entries.map(entry => ({ + ...entry, + branch: 'branch' in entry ? entry.branch : 'unknown' + })); + await this.updateEntries(migratedEntries); + } + } } private getLocalDateString(date: Date): string { @@ -32,16 +50,20 @@ export class Database { .split('T')[0]; } - async addEntry(date: Date, project: string, timeSpent: number) { + async addEntry(date: Date, project: string, timeSpent: number, branch: string) { const dateString = this.getLocalDateString(date); const entries = this.getEntries(); - const existingEntryIndex = entries.findIndex(entry => entry.date === dateString && entry.project === project); + const existingEntryIndex = entries.findIndex(entry => + entry.date === dateString && + entry.project === project && + entry.branch === branch + ); if (existingEntryIndex !== -1) { entries[existingEntryIndex].timeSpent += timeSpent; } else { - entries.push({ date: dateString, project, timeSpent }); + entries.push({ date: dateString, project, timeSpent, branch }); } try { @@ -68,25 +90,63 @@ export class Database { const entries = this.getEntries(); const dailySummary: { [date: string]: number } = {}; const projectSummary: { [project: string]: number } = {}; + const branchSummary: { [branch: string]: number } = {}; let totalTime = 0; for (const entry of entries) { dailySummary[entry.date] = (dailySummary[entry.date] || 0) + entry.timeSpent; projectSummary[entry.project] = (projectSummary[entry.project] || 0) + entry.timeSpent; + branchSummary[entry.branch] = (branchSummary[entry.branch] || 0) + entry.timeSpent; totalTime += entry.timeSpent; } return { dailySummary, projectSummary, + branchSummary, totalTime }; - } async searchEntries(startDate?: string, endDate?: string, project?: string): Promise { + } + + async searchEntries(startDate?: string, endDate?: string, project?: string, branch?: string): Promise { const entries = this.getEntries(); return entries.filter(entry => { const dateMatch = (!startDate || entry.date >= startDate) && (!endDate || entry.date <= endDate); const projectMatch = !project || entry.project.toLowerCase().includes(project.toLowerCase()); - return dateMatch && projectMatch; + const branchMatch = !branch || entry.branch.toLowerCase().includes(branch.toLowerCase()); + return dateMatch && projectMatch && branchMatch; }); } + + async getBranchesByProject(project: string): Promise { + const entries = await this.getEntries(); + const branchSet = new Set( + entries + .filter(entry => entry.project === project) + .map(entry => entry.branch) + ); + return Array.from(branchSet).sort(); + } + + async clearAllData(): Promise { + // Ask for explicit confirmation with a specific phrase to prevent accidental deletion + const response = await vscode.window.showInputBox({ + prompt: 'This will permanently delete all time tracking data. Type "DELETE ALL DATA" to confirm.', + placeHolder: 'DELETE ALL DATA' + }); + + if (response !== 'DELETE ALL DATA') { + vscode.window.showInformationMessage('Data deletion cancelled.'); + return; + } + + try { + this.entries = []; + await this.context.globalState.update('timeEntries', []); + vscode.window.showInformationMessage('All time tracking data has been cleared.'); + } catch (error) { + console.error('Error clearing data:', error); + vscode.window.showErrorMessage('Failed to clear time tracking data'); + } + } } \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index 33b4caf..f341b7c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -24,12 +24,50 @@ export function activate(context: vscode.ExtensionContext) { timeTracker.updateConfiguration(); } }) - ); // Register the show summary command + ); + + // Register the show summary command let disposable = vscode.commands.registerCommand('simpleCodingTimeTracker.showSummary', () => { summaryView.show(); }); + // Register view storage command + let viewStorageDisposable = vscode.commands.registerCommand('simpleCodingTimeTracker.viewStorageData', async () => { + try { + const entries = await database.getEntries(); + const processedData = { + totalEntries: entries.length, + exportDate: new Date().toISOString(), + entries: entries.map(entry => ({ + ...entry, + timeSpentFormatted: `${Math.round(entry.timeSpent)} minutes` + })) + }; + + // Create a temporary untitled document + const document = await vscode.workspace.openTextDocument({ + content: JSON.stringify(processedData, null, 2), + language: 'json' + }); + + await vscode.window.showTextDocument(document, { + preview: false, + viewColumn: vscode.ViewColumn.One + }); + + vscode.window.showInformationMessage('Time tracking data loaded successfully'); } catch (error: any) { + vscode.window.showErrorMessage(`Failed to view data: ${error?.message || 'Unknown error'}`); + } + }); + + // Register data management command (hidden from command palette) + let clearDataCommand = vscode.commands.registerCommand('coding-time-tracker.clearAllData', () => { + database.clearAllData(); + }); + context.subscriptions.push(clearDataCommand); + context.subscriptions.push(disposable); + context.subscriptions.push(viewStorageDisposable); context.subscriptions.push(timeTracker); context.subscriptions.push(statusBar); diff --git a/src/statusBar.ts b/src/statusBar.ts index d09d506..db19611 100644 --- a/src/statusBar.ts +++ b/src/statusBar.ts @@ -12,17 +12,14 @@ export class StatusBar implements vscode.Disposable { this.timeTracker = timeTracker; this.statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100); this.statusBarItem.command = 'simpleCodingTimeTracker.showSummary'; - this.statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); - this.statusBarItem.show(); - this.updateStatusBar(); - this.updateInterval = setInterval(() => this.updateStatusBar(), 1000); // Update every second - } - - private updateStatusBar() { - const todayTotal = this.timeTracker.getTodayTotal(); + this.statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); this.statusBarItem.show(); + void this.updateStatusBar(); + this.updateInterval = setInterval(() => void this.updateStatusBar(), 1000); // Update every second + } private async updateStatusBar() { + const todayTotal = (await this.timeTracker.getAllPeriodTotals()).today; const isActive = this.timeTracker.isActive(); this.statusBarItem.text = `${isActive ? 'πŸ’»' : '⏸️'} ${this.formatTime(todayTotal)}`; - this.statusBarItem.tooltip = this.getTooltipText(isActive); + this.statusBarItem.tooltip = await this.getTooltipText(isActive); } private formatTime(minutes: number): string { @@ -30,19 +27,22 @@ export class StatusBar implements vscode.Disposable { const mins = Math.floor(minutes % 60); const secs = Math.floor((minutes * 60) % 60); return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; - } - - private getTooltipText(isActive: boolean): string { - const weeklyTotal = this.timeTracker.getWeeklyTotal(); - const monthlyTotal = this.timeTracker.getMonthlyTotal(); - const allTimeTotal = this.timeTracker.getAllTimeTotal(); - - return `${isActive ? 'Active' : 'Paused'} - Total Coding Time: -This week: ${formatTime(weeklyTotal)} -This month: ${formatTime(monthlyTotal)} -All Time: ${formatTime(allTimeTotal)} -Click to show summary`; - } + } + + private async getTooltipText(isActive: boolean): Promise { + // const weeklyTotal = await this.timeTracker.getWeeklyTotal(); + // const monthlyTotal = await this.timeTracker.getMonthlyTotal(); + // const allTimeTotal = await this.timeTracker.getAllTimeTotal(); + const allPeriodsTotal = await this.timeTracker.getAllPeriodTotals(); + const currentBranch = this.timeTracker.getCurrentBranch(); + + return `${isActive ? 'Active' : 'Paused'} - Total Coding Time + Branch: ${currentBranch} + This week: ${formatTime(allPeriodsTotal.thisWeek)} + This month: ${formatTime(allPeriodsTotal.thisMonth)} + All Time: ${formatTime(allPeriodsTotal.allTime)} + Click to show summary`; + } onDidClick(listener: () => void): vscode.Disposable { return this.onDidClickEmitter.event(listener); diff --git a/src/summaryView.ts b/src/summaryView.ts index f4b05ab..0fbbfad 100644 --- a/src/summaryView.ts +++ b/src/summaryView.ts @@ -24,15 +24,24 @@ export class SummaryViewProvider implements vscode.WebviewViewProvider { webviewView.webview.options = { enableScripts: true, localResourceRoots: [this.context.extensionUri] - }; - - webviewView.webview.onDidReceiveMessage( + }; webviewView.webview.onDidReceiveMessage( async message => { if (message.command === 'refresh') { await this.show(webviewView.webview); } else if (message.command === 'search') { - const searchResults = await this.database.searchEntries(message.startDate, message.endDate, message.project); + const searchResults = await this.database.searchEntries( + message.startDate, + message.endDate, + message.project, + message.branch + ); webviewView.webview.postMessage({ command: 'searchResult', data: searchResults }); + } else if (message.command === 'projectChanged') { + // If project is empty, show all branches, otherwise show only branches for selected project + const branches = message.project + ? await this.database.getBranchesByProject(message.project) + : await this.getUniqueBranches(); + webviewView.webview.postMessage({ command: 'updateBranches', branches }); } }, undefined, @@ -42,24 +51,53 @@ export class SummaryViewProvider implements vscode.WebviewViewProvider { this.show(webviewView.webview); } + // Add this method to get branches for a specific project + private async getBranchesByProject(project: string): Promise { + return await this.database.getBranchesByProject(project); + } + async show(webview?: vscode.Webview) { const summaryData = await this.database.getSummaryData(); const projects = await this.getUniqueProjects(); + const branches = await this.getUniqueBranches(); + // const totalTime = { + // today: formatTime(await this.timeTracker.getTodayTotal()), + // weekly: formatTime(await this.timeTracker.getWeeklyTotal()), + // monthly: formatTime(await this.timeTracker.getMonthlyTotal()), + // yearly: formatTime(await this.timeTracker.getYearlyTotal()), + // allTime: formatTime(await this.timeTracker.getAllTimeTotal()) + // }; + const periodTotals = await this.timeTracker.getAllPeriodTotals(); const totalTime = { - today: formatTime(this.timeTracker.getTodayTotal()), - weekly: formatTime(this.timeTracker.getWeeklyTotal()), - monthly: formatTime(this.timeTracker.getMonthlyTotal()), - yearly: formatTime(this.timeTracker.getYearlyTotal()), // Add this line - allTime: formatTime(this.timeTracker.getAllTimeTotal()) + today: formatTime(periodTotals.today), + yesterday: formatTime(periodTotals.yesterday), + thisWeek: formatTime(periodTotals.thisWeek), + lastWeek: formatTime(periodTotals.lastWeek), + thisMonth: formatTime(periodTotals.thisMonth), + lastMonth: formatTime(periodTotals.lastMonth), + thisYear: formatTime(periodTotals.thisYear), + lastYear: formatTime(periodTotals.lastYear), + allTime: formatTime(periodTotals.allTime) }; + const config = vscode.workspace.getConfiguration('simpleCodingTimeTracker'); + const weekStartDay = config.get('weekStartDay', 'Sunday'); + const configData = { weekStartDay }; + if (webview) { webview.html = this.getHtmlForWebview(projects); - webview.postMessage({ command: 'update', data: summaryData, projects: projects, totalTime: totalTime }); + webview.postMessage({ + command: 'update', + data: summaryData, + projects, + branches, + totalTime, + configData // Pass the config data to the webview as well + }); } else if (this.panel) { this.panel.reveal(); this.panel.webview.html = this.getHtmlForWebview(projects); - this.panel.webview.postMessage({ command: 'update', data: summaryData, projects: projects, totalTime: totalTime }); + this.panel.webview.postMessage({ command: 'update', data: summaryData, projects, totalTime, configData }); } else { this.panel = vscode.window.createWebviewPanel( 'codingTimeSummary', @@ -80,17 +118,21 @@ export class SummaryViewProvider implements vscode.WebviewViewProvider { } else if (message.command === 'search') { const searchResults = await this.database.searchEntries(message.startDate, message.endDate, message.project); this.panel?.webview.postMessage({ command: 'searchResult', data: searchResults }); + } else if (message.command === 'projectChanged') { + // Update branches for selected project + const branches = message.project + ? await this.database.getBranchesByProject(message.project) + : await this.getUniqueBranches(); + this.panel?.webview.postMessage({ command: 'updateBranches', branches }); } }, undefined, this.context.subscriptions - ); - - this.panel.onDidDispose(() => { + ); this.panel.onDidDispose(() => { this.panel = undefined; }); - this.panel.webview.postMessage({ command: 'update', data: summaryData, projects: projects, totalTime: totalTime }); + this.panel.webview.postMessage({ command: 'update', data: summaryData, projects, branches, totalTime, configData }); } } @@ -98,7 +140,7 @@ export class SummaryViewProvider implements vscode.WebviewViewProvider { private async updateContent(webview?: vscode.Webview) { const summaryData = await this.database.getSummaryData(); const projects = await this.getUniqueProjects(); - + if (webview) { webview.html = this.getHtmlForWebview(projects); webview.postMessage({ command: 'update', data: summaryData, projects: projects }); @@ -114,9 +156,15 @@ export class SummaryViewProvider implements vscode.WebviewViewProvider { return Array.from(projectSet).sort(); } + private async getUniqueBranches(): Promise { + const entries = await this.database.getEntries(); + const branchSet = new Set(entries.map(entry => entry.branch)); + return Array.from(branchSet).sort(); + } + private getHtmlForWebview(projects: string[]): string { const projectOptions = projects.map(project => ``).join(''); - + return ` @@ -258,10 +306,19 @@ export class SummaryViewProvider implements vscode.WebviewViewProvider { } .total-time-grid { display: grid; - grid-template-columns: repeat(5, 1fr); /* Change to 5 columns */ + grid-template-columns: repeat(5, 1fr); + grid-template-rows: repeat(2, auto); gap: 20px; margin-bottom: 30px; } + .total-time-item.all-time-cell { + grid-column: 5 / 6; /* Place in the last column */ + grid-row: 1 / 3; /* Span both rows */ + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + } .total-time-item { background-color: var(--vscode-editor-background); border: 1px solid var(--vscode-panel-border); @@ -390,37 +447,53 @@ export class SummaryViewProvider implements vscode.WebviewViewProvider {

This Week

-

Loading...

- Sunday - today +

Loading...

This Month

-

Loading...

- - today +

Loading...

This Year

-

Loading...

- January 1st - today +

Loading...

+
+
+

Yesterday

+

Loading...

+
+
+

Last Week

+

Loading...

+
+
+

Last Month

+

Loading...

+

Last Year

+

Loading...

+
+

All Time

Loading...

+

Coding Activity

-
-
+
+
@@ -543,17 +616,42 @@ export class SummaryViewProvider implements vscode.WebviewViewProvider { if (message.command === 'update') { updateContent(message.data); updateProjectDropdown(message.projects); - updateTotalTimeSection(message.totalTime); + if (message.branches) { + updateBranchDropdown(message.branches); + } + updateTotalTimeSection(message.totalTime, message.configData); } else if (message.command === 'searchResult') { displaySearchResult(message.data); + } else if (message.command === 'updateBranches') { + updateBranchDropdown(message.branches); } + }); // Add event listener for project change + document.getElementById('project-search').addEventListener('change', (e) => { + const project = e.target.value; + vscode.postMessage({ command: 'projectChanged', project }); + + // Also trigger search to update charts + const startDate = document.getElementById('start-date-search').value; + const endDate = document.getElementById('end-date-search').value; + const branch = document.getElementById('branch-search').value; + vscode.postMessage({ command: 'search', startDate, endDate, project, branch }); + }); + + // Add event listener for branch change + document.getElementById('branch-search').addEventListener('change', (e) => { + const branch = e.target.value; + const project = document.getElementById('project-search').value; + const startDate = document.getElementById('start-date-search').value; + const endDate = document.getElementById('end-date-search').value; + vscode.postMessage({ command: 'search', startDate, endDate, project, branch }); }); document.getElementById('search-button').addEventListener('click', () => { const startDate = document.getElementById('start-date-search').value; const endDate = document.getElementById('end-date-search').value; const project = document.getElementById('project-search').value; - vscode.postMessage({ command: 'search', startDate, endDate, project }); + const branch = document.getElementById('branch-search').value; + vscode.postMessage({ command: 'search', startDate, endDate, project, branch }); }); // Add event listener for the reload button @@ -563,6 +661,8 @@ export class SummaryViewProvider implements vscode.WebviewViewProvider { document.getElementById('end-date-search').value = ''; // Reset project dropdown document.getElementById('project-search').value = ''; + // Reset branch dropdown + document.getElementById('branch-search').value = ''; // Send refresh command vscode.postMessage({ command: 'refresh' }); }); @@ -573,18 +673,24 @@ export class SummaryViewProvider implements vscode.WebviewViewProvider { projects.map(project => \`\`).join(''); } - function updateTotalTimeSection(totalTime) { + function updateBranchDropdown(branches) { + const dropdown = document.getElementById('branch-search'); + if (dropdown) { + dropdown.innerHTML = '' + + branches.map(branch => '').join(''); + } + } + + function updateTotalTimeSection(totalTime, configData) { document.getElementById('today-total').textContent = totalTime.today; - document.getElementById('weekly-total').textContent = totalTime.weekly; - document.getElementById('monthly-total').textContent = totalTime.monthly; - document.getElementById('yearly-total').textContent = totalTime.yearly; + document.getElementById('yesterday-total').textContent = totalTime.yesterday; + document.getElementById('this-week-total').textContent = totalTime.thisWeek; + document.getElementById('last-week-total').textContent = totalTime.lastWeek; + document.getElementById('this-month-total').textContent = totalTime.thisMonth; + document.getElementById('last-month-total').textContent = totalTime.lastMonth; + document.getElementById('this-year-total').textContent = totalTime.thisYear; + document.getElementById('last-year-total').textContent = totalTime.lastYear; document.getElementById('all-time-total').textContent = totalTime.allTime; - - // Set the start of the current month - const now = new Date(); - const monthNames = ["January", "February", "March", "April", "May", "June", - "July", "August", "September", "October", "November", "December"]; - document.getElementById('month-start').textContent = \`\${monthNames[now.getMonth()]} 1st\`; } function updateContent(data) { @@ -785,10 +891,19 @@ export class SummaryViewProvider implements vscode.WebviewViewProvider { // Calculate data for all charts let totalTime = 0; const projectData = {}; - const dailyData = {}; - - // Process entries for both project and daily summaries - entries.forEach(entry => { + const dailyData = {}; // Process entries for both project and daily summaries + const selectedProject = document.getElementById('project-search').value; + const selectedBranch = document.getElementById('branch-search').value; + + // Filter entries based on selected project and branch + const filteredEntries = entries.filter(entry => { + const projectMatch = !selectedProject || entry.project === selectedProject; + const branchMatch = !selectedBranch || entry.branch === selectedBranch; + return projectMatch && branchMatch; + }); + + // Process filtered entries + filteredEntries.forEach(entry => { totalTime += entry.timeSpent; // Update project data diff --git a/src/timeTracker.ts b/src/timeTracker.ts index e76ae37..80cf781 100644 --- a/src/timeTracker.ts +++ b/src/timeTracker.ts @@ -1,18 +1,39 @@ import * as vscode from 'vscode'; import { Database, TimeEntry } from './database'; +import { simpleGit, SimpleGit } from 'simple-git'; + +type GitWatcher = { + git: SimpleGit; + lastKnownBranch: string; +}; export class TimeTracker implements vscode.Disposable { private isTracking: boolean = false; private startTime: number = 0; private currentProject: string = ''; + private currentBranch: string = 'unknown'; private database: Database; private updateInterval: NodeJS.Timeout | null = null; - private saveInterval: NodeJS.Timeout | null = null; private saveIntervalSeconds: number = 5; + private saveInterval: NodeJS.Timeout | null = null; + private saveIntervalSeconds: number = 5; private lastCursorActivity: number = Date.now(); private cursorInactivityTimeout: NodeJS.Timeout | null = null; private inactivityTimeoutSeconds: number = 300; private focusTimeoutHandle: NodeJS.Timeout | null = null; private focusTimeoutSeconds: number = 60; + private gitWatcher: GitWatcher | null = null; + private branchCheckInterval: NodeJS.Timeout | null = null; + private weekStartDay: number = 0; // 0 = Sunday by default + + private static readonly WEEKDAY_MAP: Record = { + Sunday: 0, + Monday: 1, + Tuesday: 2, + Wednesday: 3, + Thursday: 4, + Friday: 5, + Saturday: 6 + }; constructor(database: Database) { this.database = database; @@ -26,11 +47,17 @@ export class TimeTracker implements vscode.Disposable { // Track text changes vscode.workspace.onDidChangeTextDocument(() => { this.updateCursorActivity(); + }); // Track active editor changes + vscode.window.onDidChangeActiveTextEditor((editor) => { + if (editor) { + this.currentProject = this.getCurrentProject(); + } + this.updateCursorActivity(); }); - // Track active editor changes - vscode.window.onDidChangeActiveTextEditor(() => { - this.updateCursorActivity(); + // Track git branch changes + vscode.workspace.onDidChangeWorkspaceFolders(() => { + this.updateCurrentBranch(); }); // Track hover events @@ -41,7 +68,7 @@ export class TimeTracker implements vscode.Disposable { } }); - // Track type definition requests (triggered by mouse movement over symbols) + // Track type definition requests vscode.languages.registerTypeDefinitionProvider({ scheme: '*' }, { provideTypeDefinition: () => { this.updateCursorActivity(); @@ -49,7 +76,7 @@ export class TimeTracker implements vscode.Disposable { } }); - // Track signature help requests (triggered by hovering over function calls) + // Track signature help requests vscode.languages.registerSignatureHelpProvider({ scheme: '*' }, { provideSignatureHelp: () => { this.updateCursorActivity(); @@ -60,14 +87,12 @@ export class TimeTracker implements vscode.Disposable { // Track when VS Code window gains focus vscode.window.onDidChangeWindowState((e) => { if (e.focused) { - // Clear any pending timeout when focus returns if (this.focusTimeoutHandle) { clearTimeout(this.focusTimeoutHandle); this.focusTimeoutHandle = null; } this.startTracking(); } else { - // Set timeout when focus is lost if (this.focusTimeoutHandle) { clearTimeout(this.focusTimeoutHandle); } @@ -83,8 +108,32 @@ export class TimeTracker implements vscode.Disposable { public updateConfiguration() { const config = vscode.workspace.getConfiguration('simpleCodingTimeTracker'); this.saveIntervalSeconds = config.get('saveInterval', 5); - this.inactivityTimeoutSeconds = config.get('inactivityTimeout', 300); // Default 5 minutes in seconds + this.inactivityTimeoutSeconds = config.get('inactivityTimeout', 300); this.focusTimeoutSeconds = config.get('focusTimeout', 60); + + // Map string to number for weekStartDay + const weekStartDayStr = config.get('weekStartDay', 'Sunday'); + this.weekStartDay = TimeTracker.WEEKDAY_MAP[weekStartDayStr] ?? 0; + } + + private async updateCurrentBranch() { + try { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + if (!workspaceFolder) { + this.currentBranch = 'unknown'; + return; + } + + const git = simpleGit(workspaceFolder.uri.fsPath); + try { + const branchInfo = await git.branch(); + this.currentBranch = branchInfo.current || 'unknown'; + } catch (error) { + this.currentBranch = 'unknown'; + } + } catch (error) { + this.currentBranch = 'unknown'; + } } private setupCursorTracking() { @@ -95,44 +144,40 @@ export class TimeTracker implements vscode.Disposable { const currentTime = Date.now(); const timeSinceLastActivity = currentTime - this.lastCursorActivity; - // Only setup new timeout if we haven't exceeded the inactivity threshold if (timeSinceLastActivity < this.inactivityTimeoutSeconds * 1000) { - // Set up cursor activity tracking this.cursorInactivityTimeout = setTimeout(() => { const now = Date.now(); const inactivityDuration = now - this.lastCursorActivity; - - // Only stop tracking if we've truly been inactive + if (this.isTracking && inactivityDuration >= this.inactivityTimeoutSeconds * 1000) { this.stopTracking(); this.saveCurrentSession(); } - }, this.inactivityTimeoutSeconds * 1000); // Convert seconds to milliseconds + }, this.inactivityTimeoutSeconds * 1000); } this.lastCursorActivity = currentTime; } - public updateCursorActivity() { + public async updateCursorActivity() { if (!this.isTracking) { - this.startTracking(); + await this.startTracking(); } - // Update the last activity timestamp this.lastCursorActivity = Date.now(); - - // Reset and setup the inactivity timer this.setupCursorTracking(); } - startTracking() { + async startTracking() { if (!this.isTracking) { + await this.updateCurrentBranch(); this.isTracking = true; this.startTime = Date.now(); this.currentProject = this.getCurrentProject(); this.updateInterval = setInterval(() => this.updateCurrentSession(), 1000); - this.saveInterval = setInterval(() => this.saveCurrentSession(), this.saveIntervalSeconds * 1000); // Convert seconds to milliseconds + this.saveInterval = setInterval(() => this.saveCurrentSession(), this.saveIntervalSeconds * 1000); this.setupCursorTracking(); + await this.setupGitWatcher(); // Set up real-time branch monitoring } } @@ -152,6 +197,7 @@ export class TimeTracker implements vscode.Disposable { this.cursorInactivityTimeout = null; } this.saveCurrentSession(); + this.stopGitWatcher(); // Clean up branch monitoring } } @@ -162,58 +208,57 @@ export class TimeTracker implements vscode.Disposable { private async saveCurrentSession() { if (this.isTracking) { - const duration = (Date.now() - this.startTime) / 60000; // Convert to minutes - await this.database.addEntry(new Date(), this.currentProject, duration); - this.startTime = Date.now(); // Reset the start time for the next interval + const duration = (Date.now() - this.startTime) / 60000; + await this.database.addEntry(new Date(), this.currentProject, duration, this.currentBranch); + this.startTime = Date.now(); } } private getCurrentProject(): string { + // If we have a current project name, keep using it + if (this.currentProject && this.currentProject !== 'Unknown Project') { + return this.currentProject; + } + const workspaceFolders = vscode.workspace.workspaceFolders; - if (!workspaceFolders) { + + // No workspace folders open + if (!workspaceFolders || workspaceFolders.length === 0) { return 'Unknown Project'; } - // Get the active text editor - const activeEditor = vscode.window.activeTextEditor; - if (!activeEditor) { - return 'No Active File'; + // Single workspace + if (workspaceFolders.length === 1) { + return workspaceFolders[0].name; } - // Get workspace information + // Multi-root workspace const workspaceName = vscode.workspace.name || 'Default Workspace'; - const workspaceFolder = vscode.workspace.getWorkspaceFolder(activeEditor.document.uri); - - if (!workspaceFolder) { - // File is outside any workspace folder - return `External/${this.getExternalProjectName(activeEditor.document.uri)}`; - } - - // If we're in a multi-root workspace, prefix with workspace name - if (workspaceFolders.length > 1) { - return `${workspaceName}/${workspaceFolder.name}`; + const activeEditor = vscode.window.activeTextEditor; + if (activeEditor) { + const workspaceFolder = vscode.workspace.getWorkspaceFolder(activeEditor.document.uri); + if (workspaceFolder) { + return `${workspaceName}/${workspaceFolder.name}`; + } } - return workspaceFolder.name; + // Default to first workspace if no active editor + return workspaceFolders[0].name; } private getExternalProjectName(uri: vscode.Uri): string { - // Handle different scenarios for external files if (uri.scheme !== 'file') { return 'Virtual Files'; } - // Get the parent folder name for external files const path = uri.fsPath; const parentFolder = path.split(/[\\/]/); - - // Remove empty segments and file name + const folders = parentFolder.filter(Boolean); if (folders.length >= 2) { - // Return "ParentFolder/CurrentFolder" return `${folders[folders.length - 2]}/${folders[folders.length - 1]}`; } - + return 'Other'; } @@ -223,35 +268,117 @@ export class TimeTracker implements vscode.Disposable { .split('T')[0]; } - getTodayTotal(): number { - const today = this.getLocalDateString(new Date()); - const entries = this.database.getEntries(); - const todayTotal = entries - .filter((entry: TimeEntry) => entry.date === today) - .reduce((sum: number, entry: TimeEntry) => sum + entry.timeSpent, 0); - - // Add the current session time if tracking is active and we haven't exceeded inactivity threshold + async getAllPeriodTotals(): Promise<{ + today: number; + yesterday: number; + thisWeek: number; + lastWeek: number; + thisMonth: number; + lastMonth: number; + thisYear: number; + lastYear: number; + allTime: number; + }> { + const now = new Date(); + const todayStr = this.getLocalDateString(now); + const yesterdayStr = this.getLocalDateString( new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1)); + + // Week calculations + let diff = now.getDay() - this.weekStartDay; + if (diff < 0) diff += 7; + const startOfThisWeek = new Date(now.getFullYear(), now.getMonth(), now.getDate() - diff); + const startOfLastWeek = new Date(startOfThisWeek); + startOfLastWeek.setDate(startOfThisWeek.getDate() - 7); + const endOfLastWeek = new Date(startOfThisWeek); + endOfLastWeek.setDate(startOfThisWeek.getDate() - 1); + + // Month calculations + const startOfThisMonth = new Date(now.getFullYear(), now.getMonth(), 1); + const startOfLastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1); + const endOfLastMonth = new Date(now.getFullYear(), now.getMonth(), 0); + + // Year calculations + const startOfThisYear = new Date(now.getFullYear(), 0, 1); + const startOfLastYear = new Date(now.getFullYear() - 1, 0, 1); + const endOfLastYear = new Date(now.getFullYear() - 1, 11, 31); + + // Date strings for range comparisons + const startOfThisWeekStr = this.getLocalDateString(startOfThisWeek); + const startOfLastWeekStr = this.getLocalDateString(startOfLastWeek); + const endOfLastWeekStr = this.getLocalDateString(endOfLastWeek); + const startOfThisMonthStr = this.getLocalDateString(startOfThisMonth); + const startOfLastMonthStr = this.getLocalDateString(startOfLastMonth); + const endOfLastMonthStr = this.getLocalDateString(endOfLastMonth); + const startOfThisYearStr = this.getLocalDateString(startOfThisYear); + const startOfLastYearStr = this.getLocalDateString(startOfLastYear); + const endOfLastYearStr = this.getLocalDateString(endOfLastYear); + + const entries = await this.database.getEntries(); + + let today = 0, yesterday = 0, thisWeek = 0, lastWeek = 0, thisMonth = 0, lastMonth = 0, thisYear = 0, lastYear = 0, allTime = 0; + + for (const entry of entries) { + allTime += entry.timeSpent; + + if (entry.date === todayStr) today += entry.timeSpent; + if (entry.date === yesterdayStr) yesterday += entry.timeSpent; + + if (entry.date >= startOfThisWeekStr && entry.date <= todayStr) thisWeek += entry.timeSpent; + if (entry.date >= startOfLastWeekStr && entry.date <= endOfLastWeekStr) lastWeek += entry.timeSpent; + + if (entry.date >= startOfThisMonthStr && entry.date <= todayStr) thisMonth += entry.timeSpent; + if (entry.date >= startOfLastMonthStr && entry.date <= endOfLastMonthStr) lastMonth += entry.timeSpent; + + if (entry.date >= startOfThisYearStr && entry.date <= todayStr) thisYear += entry.timeSpent; + if (entry.date >= startOfLastYearStr && entry.date <= endOfLastYearStr) lastYear += entry.timeSpent; + } + + // Add current session time to today, weekly, monthly, yearly, allTime if tracking if (this.isTracking) { const timeSinceLastActivity = Date.now() - this.lastCursorActivity; - // Only include current session if we're still within the activity window if (timeSinceLastActivity < this.inactivityTimeoutSeconds * 1000) { const currentSessionTime = (Date.now() - this.startTime) / 60000; - return todayTotal + currentSessionTime; + today += currentSessionTime; + thisWeek += currentSessionTime; + thisMonth += currentSessionTime; + thisYear += currentSessionTime; + allTime += currentSessionTime; } } - - return todayTotal; + + return { today, yesterday, thisWeek, lastWeek, thisMonth, lastMonth, thisYear, lastYear, allTime }; } - getCurrentProjectTime(): number { + // async getTodayTotal(): Promise { + // const today = this.getLocalDateString(new Date()); + // const entries = await this.database.getEntries(); + // const todayTotal = entries + // .filter((entry: TimeEntry) => entry.date === today) + // .reduce((sum: number, entry: TimeEntry) => sum + entry.timeSpent, 0); + + // if (this.isTracking) { + // const timeSinceLastActivity = Date.now() - this.lastCursorActivity; + // if (timeSinceLastActivity < this.inactivityTimeoutSeconds * 1000) { + // const currentSessionTime = (Date.now() - this.startTime) / 60000; + // return todayTotal + currentSessionTime; + // } + // } + + // return todayTotal; + // } + + async getCurrentProjectTime(): Promise { const today = this.getLocalDateString(new Date()); const currentProject = this.getCurrentProject(); - const entries = this.database.getEntries(); + const entries = await this.database.getEntries(); const currentProjectTime = entries - .filter((entry: TimeEntry) => entry.date === today && entry.project === currentProject) + .filter((entry: TimeEntry) => + entry.date === today && + entry.project === currentProject && + entry.branch === this.currentBranch + ) .reduce((sum: number, entry: TimeEntry) => sum + entry.timeSpent, 0); - - // Add the current session time if tracking is active and within activity window + if (this.isTracking && this.currentProject === currentProject) { const timeSinceLastActivity = Date.now() - this.lastCursorActivity; if (timeSinceLastActivity < this.inactivityTimeoutSeconds * 1000) { @@ -259,70 +386,141 @@ export class TimeTracker implements vscode.Disposable { return currentProjectTime + currentSessionTime; } } - + return currentProjectTime; } - getWeeklyTotal(): number { - const now = new Date(); - const startOfWeek = new Date(now.getFullYear(), now.getMonth(), now.getDate() - now.getDay()); - return this.getTotalSince(startOfWeek); + // async getWeeklyTotal(): Promise { + // const now = new Date(); + // // Calculate difference between current day and weekStartDay + // let diff = now.getDay() - this.weekStartDay; + // if (diff < 0) diff += 7; + // const startOfWeek = new Date(now.getFullYear(), now.getMonth(), now.getDate() - diff); + // return this.getTotalSince(startOfWeek); + // } + + // async getMonthlyTotal(): Promise { + // const now = new Date(); + // const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + // return this.getTotalSince(startOfMonth); + // } + + // async getAllTimeTotal(): Promise { + // const entries = await this.database.getEntries(); + // const total = entries.reduce((sum: number, entry: TimeEntry) => sum + entry.timeSpent, 0); + + // if (this.isTracking) { + // const timeSinceLastActivity = Date.now() - this.lastCursorActivity; + // if (timeSinceLastActivity < this.inactivityTimeoutSeconds * 1000) { + // const currentSessionTime = (Date.now() - this.startTime) / 60000; + // return total + currentSessionTime; + // } + // } + + // return total; + // } + + // private async getTotalSince(startDate: Date): Promise { + // const entries = await this.database.getEntries(); + // const startDateString = this.getLocalDateString(startDate); + // const now = this.getLocalDateString(new Date()); + + // const filteredEntries = entries.filter(entry => + // entry.date >= startDateString && entry.date <= now + // ); + + // const total = filteredEntries.reduce((sum, entry) => sum + entry.timeSpent, 0); + + // if (this.isTracking) { + // const timeSinceLastActivity = Date.now() - this.lastCursorActivity; + // if (timeSinceLastActivity < this.inactivityTimeoutSeconds * 1000) { + // const currentSessionTime = (Date.now() - this.startTime) / 60000; + // return total + currentSessionTime; + // } + // } + + // return total; + // } + + // async getYearlyTotal(): Promise { + // const now = new Date(); + // const startOfYear = new Date(now.getFullYear(), 0, 1); + // return this.getTotalSince(startOfYear); + // } + + dispose() { + this.stopTracking(); } - getMonthlyTotal(): number { - const now = new Date(); - const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); - return this.getTotalSince(startOfMonth); + isActive(): boolean { + return this.isTracking; } - getAllTimeTotal(): number { - const total = this.database.getEntries() - .reduce((sum: number, entry: TimeEntry) => sum + entry.timeSpent, 0); + getCurrentBranch(): string { + return this.currentBranch; + } - // Add current session if tracking is active and within activity window - if (this.isTracking) { - const timeSinceLastActivity = Date.now() - this.lastCursorActivity; - if (timeSinceLastActivity < this.inactivityTimeoutSeconds * 1000) { - const currentSessionTime = (Date.now() - this.startTime) / 60000; - return total + currentSessionTime; + private async setupGitWatcher() { + try { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + if (!workspaceFolder) { + return; } - } - return total; - } + const git = simpleGit(workspaceFolder.uri.fsPath); + const isGitRepo = await git.checkIsRepo(); - private getTotalSince(startDate: Date): number { - const entries = this.database.getEntries(); - const startDateString = this.getLocalDateString(startDate); - const now = this.getLocalDateString(new Date()); - - const filteredEntries = entries.filter(entry => - entry.date >= startDateString && entry.date <= now - ); + if (!isGitRepo) { + return; + } - const total = filteredEntries.reduce((sum, entry) => sum + entry.timeSpent, 0); + const branchInfo = await git.branch(); + this.gitWatcher = { + git, + lastKnownBranch: branchInfo.current || 'unknown' + }; - // Add current session if tracking is active and within activity window - if (this.isTracking) { - const timeSinceLastActivity = Date.now() - this.lastCursorActivity; - if (timeSinceLastActivity < this.inactivityTimeoutSeconds * 1000) { - const currentSessionTime = (Date.now() - this.startTime) / 60000; - return total + currentSessionTime; - } - } + // Check for branch changes every second + this.branchCheckInterval = setInterval(async () => { + await this.checkBranchChanges(); + }, 1000); - return total; + } catch (error) { + console.error('Error setting up git watcher:', error); + } } - getYearlyTotal(): number { - const now = new Date(); - const startOfYear = new Date(now.getFullYear(), 0, 1); // January 1st of current year - return this.getTotalSince(startOfYear); - } dispose() { - this.stopTracking(); + private async checkBranchChanges() { + if (!this.gitWatcher || !this.isTracking) { + return; + } + + try { + const branchInfo = await this.gitWatcher.git.branch(); + const currentBranch = branchInfo.current || 'unknown'; + + // If branch has changed + if (currentBranch !== this.gitWatcher.lastKnownBranch) { + // Save the current session with the old branch + await this.saveCurrentSession(); + + // Update branch tracking + this.gitWatcher.lastKnownBranch = currentBranch; + this.currentBranch = currentBranch; + + // Start a new session + this.startTime = Date.now(); + } + } catch (error) { + console.error('Error checking branch changes:', error); + } } - isActive(): boolean { - return this.isTracking; + private stopGitWatcher() { + if (this.branchCheckInterval) { + clearInterval(this.branchCheckInterval); + this.branchCheckInterval = null; + } + this.gitWatcher = null; } } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 8b5d90e..0e14d0a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "module": "commonjs", "target": "es6", "outDir": "out", - "lib": ["es6"], + "lib": ["es6", "dom", "dom.iterable"], "sourceMap": true, "rootDir": "src", "strict": true,