time-buddy 1.0.1__tar.gz
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.
- time_buddy-1.0.1/LICENSE +9 -0
- time_buddy-1.0.1/PKG-INFO +136 -0
- time_buddy-1.0.1/README.md +116 -0
- time_buddy-1.0.1/pyproject.toml +44 -0
- time_buddy-1.0.1/setup.cfg +4 -0
- time_buddy-1.0.1/time_buddy.egg-info/PKG-INFO +136 -0
- time_buddy-1.0.1/time_buddy.egg-info/SOURCES.txt +10 -0
- time_buddy-1.0.1/time_buddy.egg-info/dependency_links.txt +1 -0
- time_buddy-1.0.1/time_buddy.egg-info/entry_points.txt +2 -0
- time_buddy-1.0.1/time_buddy.egg-info/requires.txt +3 -0
- time_buddy-1.0.1/time_buddy.egg-info/top_level.txt +1 -0
- time_buddy-1.0.1/time_buddy.py +502 -0
time_buddy-1.0.1/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Alen Zubic
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: time-buddy
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: A simple to use time tracking CLI for macOS.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/alen-z/time-buddy
|
|
7
|
+
Project-URL: Repository, https://github.com/alen-z/time-buddy
|
|
8
|
+
Project-URL: Issues, https://github.com/alen-z/time-buddy/issues
|
|
9
|
+
Keywords: time-tracking,macos,cli,screen-time,productivity
|
|
10
|
+
Classifier: Operating System :: MacOS
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Topic :: Utilities
|
|
13
|
+
Requires-Python: >=3.7
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: tzlocal
|
|
17
|
+
Requires-Dist: halo
|
|
18
|
+
Requires-Dist: colorama
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# Time Buddy
|
|
22
|
+
|
|
23
|
+
A simple to use time tracking CLI for **macOS**.
|
|
24
|
+
|
|
25
|
+
Track your screen time based on macOS system logs to calculate how many hours you've worked each day.
|
|
26
|
+
|
|
27
|
+

|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
- 📊 Track screen time based on macOS unlock/lock events
|
|
32
|
+
- 📅 Calculate time spent per day with hourly breakdown visualization
|
|
33
|
+
- 💾 Smart caching for faster subsequent runs
|
|
34
|
+
- 🎨 Colorful terminal output with progress indicators
|
|
35
|
+
|
|
36
|
+
## Requirements
|
|
37
|
+
|
|
38
|
+
- macOS (uses macOS system logs)
|
|
39
|
+
- Python 3.7+ (for development)
|
|
40
|
+
|
|
41
|
+
## Setup (Required)
|
|
42
|
+
|
|
43
|
+
Before using Time Buddy, you must enable persistent logging for screen lock/unlock events:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
sudo log config --subsystem com.apple.loginwindow --mode "persist:info"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
This command configures macOS to persistently store login window events. Without this, the system may not retain the screen lock/unlock logs needed for accurate time tracking.
|
|
50
|
+
|
|
51
|
+
> **Note**: You only need to run this once. The setting persists across reboots.
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
### pip (Recommended)
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install time-buddy
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Standalone Binary
|
|
62
|
+
|
|
63
|
+
If you don't have Python installed:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
curl -fsSL https://raw.githubusercontent.com/alen-z/time-buddy/main/install.sh | bash
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Or build from source:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
git clone https://github.com/alen-z/time-buddy.git
|
|
73
|
+
cd time-buddy
|
|
74
|
+
make install
|
|
75
|
+
make install-binary
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Usage
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Show version
|
|
82
|
+
time-buddy --version
|
|
83
|
+
|
|
84
|
+
# Show screen time for the last 7 days (default)
|
|
85
|
+
time-buddy
|
|
86
|
+
|
|
87
|
+
# Show screen time for the last 30 days
|
|
88
|
+
time-buddy --days 30
|
|
89
|
+
|
|
90
|
+
# Show detailed session information
|
|
91
|
+
time-buddy --verbose
|
|
92
|
+
|
|
93
|
+
# Force refresh (ignore cache)
|
|
94
|
+
time-buddy --no-cache
|
|
95
|
+
|
|
96
|
+
# Include weekends in expected hours calculation
|
|
97
|
+
time-buddy --include-weekends
|
|
98
|
+
|
|
99
|
+
# Clear the cache
|
|
100
|
+
time-buddy --clear-cache
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Development
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# Install dependencies
|
|
107
|
+
make install
|
|
108
|
+
|
|
109
|
+
# Install locally for testing (editable mode)
|
|
110
|
+
pip install -e .
|
|
111
|
+
|
|
112
|
+
# Run the application
|
|
113
|
+
make run
|
|
114
|
+
|
|
115
|
+
# Create standalone binary
|
|
116
|
+
make create-binary
|
|
117
|
+
|
|
118
|
+
# Test the binary
|
|
119
|
+
make test-binary
|
|
120
|
+
|
|
121
|
+
# Clean build artifacts
|
|
122
|
+
make clean
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## How It Works
|
|
126
|
+
|
|
127
|
+
Time Buddy reads macOS system logs for screen lock/unlock events and calculates:
|
|
128
|
+
|
|
129
|
+
- **Raw time**: Actual screen-on time between unlock and lock events
|
|
130
|
+
- **Block time**: Time span from first to last activity (your "work block")
|
|
131
|
+
|
|
132
|
+
The output shows a 24-hour visualization where each block represents an hour, colored by activity intensity.
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT License
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Time Buddy
|
|
2
|
+
|
|
3
|
+
A simple to use time tracking CLI for **macOS**.
|
|
4
|
+
|
|
5
|
+
Track your screen time based on macOS system logs to calculate how many hours you've worked each day.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- 📊 Track screen time based on macOS unlock/lock events
|
|
12
|
+
- 📅 Calculate time spent per day with hourly breakdown visualization
|
|
13
|
+
- 💾 Smart caching for faster subsequent runs
|
|
14
|
+
- 🎨 Colorful terminal output with progress indicators
|
|
15
|
+
|
|
16
|
+
## Requirements
|
|
17
|
+
|
|
18
|
+
- macOS (uses macOS system logs)
|
|
19
|
+
- Python 3.7+ (for development)
|
|
20
|
+
|
|
21
|
+
## Setup (Required)
|
|
22
|
+
|
|
23
|
+
Before using Time Buddy, you must enable persistent logging for screen lock/unlock events:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
sudo log config --subsystem com.apple.loginwindow --mode "persist:info"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
This command configures macOS to persistently store login window events. Without this, the system may not retain the screen lock/unlock logs needed for accurate time tracking.
|
|
30
|
+
|
|
31
|
+
> **Note**: You only need to run this once. The setting persists across reboots.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
### pip (Recommended)
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install time-buddy
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Standalone Binary
|
|
42
|
+
|
|
43
|
+
If you don't have Python installed:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
curl -fsSL https://raw.githubusercontent.com/alen-z/time-buddy/main/install.sh | bash
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or build from source:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
git clone https://github.com/alen-z/time-buddy.git
|
|
53
|
+
cd time-buddy
|
|
54
|
+
make install
|
|
55
|
+
make install-binary
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Show version
|
|
62
|
+
time-buddy --version
|
|
63
|
+
|
|
64
|
+
# Show screen time for the last 7 days (default)
|
|
65
|
+
time-buddy
|
|
66
|
+
|
|
67
|
+
# Show screen time for the last 30 days
|
|
68
|
+
time-buddy --days 30
|
|
69
|
+
|
|
70
|
+
# Show detailed session information
|
|
71
|
+
time-buddy --verbose
|
|
72
|
+
|
|
73
|
+
# Force refresh (ignore cache)
|
|
74
|
+
time-buddy --no-cache
|
|
75
|
+
|
|
76
|
+
# Include weekends in expected hours calculation
|
|
77
|
+
time-buddy --include-weekends
|
|
78
|
+
|
|
79
|
+
# Clear the cache
|
|
80
|
+
time-buddy --clear-cache
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Development
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Install dependencies
|
|
87
|
+
make install
|
|
88
|
+
|
|
89
|
+
# Install locally for testing (editable mode)
|
|
90
|
+
pip install -e .
|
|
91
|
+
|
|
92
|
+
# Run the application
|
|
93
|
+
make run
|
|
94
|
+
|
|
95
|
+
# Create standalone binary
|
|
96
|
+
make create-binary
|
|
97
|
+
|
|
98
|
+
# Test the binary
|
|
99
|
+
make test-binary
|
|
100
|
+
|
|
101
|
+
# Clean build artifacts
|
|
102
|
+
make clean
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## How It Works
|
|
106
|
+
|
|
107
|
+
Time Buddy reads macOS system logs for screen lock/unlock events and calculates:
|
|
108
|
+
|
|
109
|
+
- **Raw time**: Actual screen-on time between unlock and lock events
|
|
110
|
+
- **Block time**: Time span from first to last activity (your "work block")
|
|
111
|
+
|
|
112
|
+
The output shows a 24-hour visualization where each block represents an hour, colored by activity intensity.
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT License
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "time-buddy"
|
|
7
|
+
version = "1.0.1"
|
|
8
|
+
description = "A simple to use time tracking CLI for macOS."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.7"
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Operating System :: MacOS",
|
|
14
|
+
"Environment :: Console",
|
|
15
|
+
"Topic :: Utilities",
|
|
16
|
+
]
|
|
17
|
+
keywords = ["time-tracking", "macos", "cli", "screen-time", "productivity"]
|
|
18
|
+
dependencies = [
|
|
19
|
+
"tzlocal",
|
|
20
|
+
"halo",
|
|
21
|
+
"colorama",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
time-buddy = "time_buddy:main"
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/alen-z/time-buddy"
|
|
29
|
+
Repository = "https://github.com/alen-z/time-buddy"
|
|
30
|
+
Issues = "https://github.com/alen-z/time-buddy/issues"
|
|
31
|
+
|
|
32
|
+
[tool.setuptools]
|
|
33
|
+
py-modules = ["time_buddy"]
|
|
34
|
+
|
|
35
|
+
[tool.semantic_release]
|
|
36
|
+
version_toml = ["pyproject.toml:project.version"]
|
|
37
|
+
version_variables = ["time_buddy.py:__version__"]
|
|
38
|
+
commit_message = "chore(release): v{version}"
|
|
39
|
+
|
|
40
|
+
[tool.semantic_release.changelog]
|
|
41
|
+
changelog_file = "CHANGELOG.md"
|
|
42
|
+
|
|
43
|
+
[tool.semantic_release.remote]
|
|
44
|
+
type = "github"
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: time-buddy
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: A simple to use time tracking CLI for macOS.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/alen-z/time-buddy
|
|
7
|
+
Project-URL: Repository, https://github.com/alen-z/time-buddy
|
|
8
|
+
Project-URL: Issues, https://github.com/alen-z/time-buddy/issues
|
|
9
|
+
Keywords: time-tracking,macos,cli,screen-time,productivity
|
|
10
|
+
Classifier: Operating System :: MacOS
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Topic :: Utilities
|
|
13
|
+
Requires-Python: >=3.7
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: tzlocal
|
|
17
|
+
Requires-Dist: halo
|
|
18
|
+
Requires-Dist: colorama
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# Time Buddy
|
|
22
|
+
|
|
23
|
+
A simple to use time tracking CLI for **macOS**.
|
|
24
|
+
|
|
25
|
+
Track your screen time based on macOS system logs to calculate how many hours you've worked each day.
|
|
26
|
+
|
|
27
|
+

|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
- 📊 Track screen time based on macOS unlock/lock events
|
|
32
|
+
- 📅 Calculate time spent per day with hourly breakdown visualization
|
|
33
|
+
- 💾 Smart caching for faster subsequent runs
|
|
34
|
+
- 🎨 Colorful terminal output with progress indicators
|
|
35
|
+
|
|
36
|
+
## Requirements
|
|
37
|
+
|
|
38
|
+
- macOS (uses macOS system logs)
|
|
39
|
+
- Python 3.7+ (for development)
|
|
40
|
+
|
|
41
|
+
## Setup (Required)
|
|
42
|
+
|
|
43
|
+
Before using Time Buddy, you must enable persistent logging for screen lock/unlock events:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
sudo log config --subsystem com.apple.loginwindow --mode "persist:info"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
This command configures macOS to persistently store login window events. Without this, the system may not retain the screen lock/unlock logs needed for accurate time tracking.
|
|
50
|
+
|
|
51
|
+
> **Note**: You only need to run this once. The setting persists across reboots.
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
### pip (Recommended)
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install time-buddy
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Standalone Binary
|
|
62
|
+
|
|
63
|
+
If you don't have Python installed:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
curl -fsSL https://raw.githubusercontent.com/alen-z/time-buddy/main/install.sh | bash
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Or build from source:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
git clone https://github.com/alen-z/time-buddy.git
|
|
73
|
+
cd time-buddy
|
|
74
|
+
make install
|
|
75
|
+
make install-binary
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Usage
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Show version
|
|
82
|
+
time-buddy --version
|
|
83
|
+
|
|
84
|
+
# Show screen time for the last 7 days (default)
|
|
85
|
+
time-buddy
|
|
86
|
+
|
|
87
|
+
# Show screen time for the last 30 days
|
|
88
|
+
time-buddy --days 30
|
|
89
|
+
|
|
90
|
+
# Show detailed session information
|
|
91
|
+
time-buddy --verbose
|
|
92
|
+
|
|
93
|
+
# Force refresh (ignore cache)
|
|
94
|
+
time-buddy --no-cache
|
|
95
|
+
|
|
96
|
+
# Include weekends in expected hours calculation
|
|
97
|
+
time-buddy --include-weekends
|
|
98
|
+
|
|
99
|
+
# Clear the cache
|
|
100
|
+
time-buddy --clear-cache
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Development
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# Install dependencies
|
|
107
|
+
make install
|
|
108
|
+
|
|
109
|
+
# Install locally for testing (editable mode)
|
|
110
|
+
pip install -e .
|
|
111
|
+
|
|
112
|
+
# Run the application
|
|
113
|
+
make run
|
|
114
|
+
|
|
115
|
+
# Create standalone binary
|
|
116
|
+
make create-binary
|
|
117
|
+
|
|
118
|
+
# Test the binary
|
|
119
|
+
make test-binary
|
|
120
|
+
|
|
121
|
+
# Clean build artifacts
|
|
122
|
+
make clean
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## How It Works
|
|
126
|
+
|
|
127
|
+
Time Buddy reads macOS system logs for screen lock/unlock events and calculates:
|
|
128
|
+
|
|
129
|
+
- **Raw time**: Actual screen-on time between unlock and lock events
|
|
130
|
+
- **Block time**: Time span from first to last activity (your "work block")
|
|
131
|
+
|
|
132
|
+
The output shows a 24-hour visualization where each block represents an hour, colored by activity intensity.
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT License
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
time_buddy.py
|
|
5
|
+
time_buddy.egg-info/PKG-INFO
|
|
6
|
+
time_buddy.egg-info/SOURCES.txt
|
|
7
|
+
time_buddy.egg-info/dependency_links.txt
|
|
8
|
+
time_buddy.egg-info/entry_points.txt
|
|
9
|
+
time_buddy.egg-info/requires.txt
|
|
10
|
+
time_buddy.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
time_buddy
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sqlite3
|
|
6
|
+
import subprocess
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
from datetime import date, datetime, time, timedelta
|
|
9
|
+
|
|
10
|
+
__version__ = "1.0.1"
|
|
11
|
+
|
|
12
|
+
def get_version():
|
|
13
|
+
"""Returns the package version."""
|
|
14
|
+
try:
|
|
15
|
+
from importlib.metadata import version
|
|
16
|
+
return version("time-buddy")
|
|
17
|
+
except Exception:
|
|
18
|
+
return __version__
|
|
19
|
+
|
|
20
|
+
# --- Configuration ---
|
|
21
|
+
EXPECTED_HOURS_PER_DAY = 7.5
|
|
22
|
+
|
|
23
|
+
def get_db_path():
|
|
24
|
+
"""Returns the platform-specific path to the database file."""
|
|
25
|
+
app_name = "TimeBuddy"
|
|
26
|
+
# For macOS, use the Application Support directory
|
|
27
|
+
home = os.path.expanduser("~")
|
|
28
|
+
app_support_dir = os.path.join(home, "Library", "Application Support", app_name)
|
|
29
|
+
|
|
30
|
+
if not os.path.exists(app_support_dir):
|
|
31
|
+
os.makedirs(app_support_dir)
|
|
32
|
+
|
|
33
|
+
return os.path.join(app_support_dir, 'time_buddy.db')
|
|
34
|
+
|
|
35
|
+
DB_FILE = get_db_path()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# --- Database Functions ---
|
|
39
|
+
def db_connect():
|
|
40
|
+
"""Connects to the SQLite database."""
|
|
41
|
+
return sqlite3.connect(DB_FILE)
|
|
42
|
+
|
|
43
|
+
def db_init(conn):
|
|
44
|
+
"""Initializes the database schema."""
|
|
45
|
+
with conn:
|
|
46
|
+
conn.execute("""
|
|
47
|
+
CREATE TABLE IF NOT EXISTS raw_logs (
|
|
48
|
+
day TEXT,
|
|
49
|
+
timestamp TEXT,
|
|
50
|
+
data TEXT,
|
|
51
|
+
UNIQUE(day, timestamp, data)
|
|
52
|
+
)
|
|
53
|
+
""")
|
|
54
|
+
conn.execute("""
|
|
55
|
+
CREATE TABLE IF NOT EXISTS fetched_days (
|
|
56
|
+
day TEXT PRIMARY KEY
|
|
57
|
+
)
|
|
58
|
+
""")
|
|
59
|
+
|
|
60
|
+
def db_is_day_cached(conn, day):
|
|
61
|
+
"""Checks if a past day has been fully cached."""
|
|
62
|
+
cursor = conn.cursor()
|
|
63
|
+
cursor.execute("SELECT 1 FROM fetched_days WHERE day = ?", (day.isoformat(),))
|
|
64
|
+
return cursor.fetchone() is not None
|
|
65
|
+
|
|
66
|
+
def db_get_logs_for_day(conn, day):
|
|
67
|
+
"""Retrieves all log entries for a given day from the cache."""
|
|
68
|
+
cursor = conn.cursor()
|
|
69
|
+
cursor.execute("SELECT data FROM raw_logs WHERE day = ?", (day.isoformat(),))
|
|
70
|
+
return [json.loads(row[0]) for row in cursor.fetchall()]
|
|
71
|
+
|
|
72
|
+
def db_cache_logs(conn, day, logs):
|
|
73
|
+
"""Caches a list of log entries for a given day."""
|
|
74
|
+
log_data = [(day.isoformat(), entry.get("timestamp"), json.dumps(entry)) for entry in logs]
|
|
75
|
+
with conn:
|
|
76
|
+
conn.executemany("INSERT OR IGNORE INTO raw_logs (day, timestamp, data) VALUES (?, ?, ?)", log_data)
|
|
77
|
+
|
|
78
|
+
def db_mark_day_as_cached(conn, day):
|
|
79
|
+
"""Marks a day as fully fetched in the database."""
|
|
80
|
+
with conn:
|
|
81
|
+
conn.execute("INSERT OR IGNORE INTO fetched_days (day) VALUES (?)", (day.isoformat(),))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def print_hourly_breakdown(day: date, hourly_durations: defaultdict, block_duration: timedelta):
|
|
85
|
+
"""Prints a single line of 24 colored blocks representing a day's screen time."""
|
|
86
|
+
# --- Color gradient (10 steps from red to green in ANSI 256-color) ---
|
|
87
|
+
gradient_colors = [196, 202, 208, 214, 220, 226, 190, 154, 118, 46]
|
|
88
|
+
|
|
89
|
+
total_duration = sum(hourly_durations.values(), timedelta())
|
|
90
|
+
total_hours = total_duration.total_seconds() / 3600
|
|
91
|
+
total_block_hours = block_duration.total_seconds() / 3600
|
|
92
|
+
raw_percentage = (total_hours / EXPECTED_HOURS_PER_DAY) * 100
|
|
93
|
+
block_percentage = (total_block_hours / EXPECTED_HOURS_PER_DAY) * 100
|
|
94
|
+
|
|
95
|
+
# Determine day of week and apply appropriate color
|
|
96
|
+
day_of_week = day.weekday() # Monday=0, Sunday=6
|
|
97
|
+
day_names = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
|
98
|
+
day_name = day_names[day_of_week]
|
|
99
|
+
|
|
100
|
+
# Use cyan for weekends, default color for weekdays
|
|
101
|
+
if day_of_week >= 5: # Saturday or Sunday
|
|
102
|
+
day_label = f" \033[38;5;51m({day_name})\033[0m"
|
|
103
|
+
else:
|
|
104
|
+
day_label = f" ({day_name})"
|
|
105
|
+
|
|
106
|
+
output_line = f"{day.isoformat()}{day_label}: "
|
|
107
|
+
|
|
108
|
+
for hour in range(24):
|
|
109
|
+
minutes = hourly_durations.get(hour, timedelta()).total_seconds() / 60
|
|
110
|
+
|
|
111
|
+
color_code = ""
|
|
112
|
+
if minutes > 0:
|
|
113
|
+
# Map minutes (1-60) to a gradient index (0-9)
|
|
114
|
+
gradient_index = min(int((minutes - 1) / 6), len(gradient_colors) - 1)
|
|
115
|
+
ansi_color = gradient_colors[gradient_index]
|
|
116
|
+
color_code = f'\033[38;5;{ansi_color}m'
|
|
117
|
+
else:
|
|
118
|
+
# Use a faint grey for hours with no activity
|
|
119
|
+
color_code = '\033[38;5;240m'
|
|
120
|
+
|
|
121
|
+
output_line += f"{color_code}â–ˆ\033[0m"
|
|
122
|
+
|
|
123
|
+
raw_str = f"Raw: {total_hours:.1f} h ({raw_percentage:.0f}%)"
|
|
124
|
+
block_str = f"Block: {total_block_hours:.1f} h ({block_percentage:.0f}%)"
|
|
125
|
+
output_line += f" {raw_str:<22}{block_str}"
|
|
126
|
+
print(output_line)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def process_day_logs(logs, current_day, verbose=False):
|
|
130
|
+
"""Processes log entries for a single day and returns hourly durations and block duration."""
|
|
131
|
+
events = []
|
|
132
|
+
for entry in logs:
|
|
133
|
+
timestamp_str = entry.get("timestamp")
|
|
134
|
+
if not timestamp_str:
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
timestamp = datetime.fromisoformat(timestamp_str)
|
|
139
|
+
except ValueError:
|
|
140
|
+
try:
|
|
141
|
+
timestamp = datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S.%f")
|
|
142
|
+
except ValueError:
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
# Ensure the event belongs to the current day being processed
|
|
146
|
+
if timestamp.date() != current_day:
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
message = entry.get("eventMessage", "")
|
|
150
|
+
|
|
151
|
+
if "screenIsUnlocked" in message:
|
|
152
|
+
events.append({'timestamp': timestamp, 'type': 'unlocked'})
|
|
153
|
+
elif "screenIsLocked" in message:
|
|
154
|
+
events.append({'timestamp': timestamp, 'type': 'locked'})
|
|
155
|
+
|
|
156
|
+
events.sort(key=lambda x: x['timestamp'])
|
|
157
|
+
|
|
158
|
+
# --- Calculate precise screen time (sum of unlock-to-lock sessions) ---
|
|
159
|
+
hourly_durations = defaultdict(timedelta)
|
|
160
|
+
unlock_time = None
|
|
161
|
+
if verbose:
|
|
162
|
+
print(f"Processing sessions for {current_day.isoformat()}:")
|
|
163
|
+
|
|
164
|
+
for event in events:
|
|
165
|
+
if event['type'] == 'unlocked':
|
|
166
|
+
if unlock_time is None:
|
|
167
|
+
unlock_time = event['timestamp']
|
|
168
|
+
elif event['type'] == 'locked':
|
|
169
|
+
if unlock_time is not None:
|
|
170
|
+
lock_time = event['timestamp']
|
|
171
|
+
duration = lock_time - unlock_time
|
|
172
|
+
if verbose:
|
|
173
|
+
print(f" - Session from {unlock_time.strftime('%Y-%m-%d %H:%M:%S')} to {lock_time.strftime('%Y-%m-%d %H:%M:%S')} (Duration: {duration})")
|
|
174
|
+
|
|
175
|
+
current_time = unlock_time
|
|
176
|
+
while current_time < lock_time:
|
|
177
|
+
current_hour_start = current_time.replace(minute=0, second=0, microsecond=0)
|
|
178
|
+
next_hour_start = current_hour_start + timedelta(hours=1)
|
|
179
|
+
|
|
180
|
+
segment_end = min(lock_time, next_hour_start)
|
|
181
|
+
duration_in_hour = segment_end - current_time
|
|
182
|
+
|
|
183
|
+
hourly_durations[current_time.hour] += duration_in_hour
|
|
184
|
+
|
|
185
|
+
current_time = next_hour_start
|
|
186
|
+
|
|
187
|
+
unlock_time = None
|
|
188
|
+
|
|
189
|
+
# --- Calculate Block Time (total span of continuous activity) ---
|
|
190
|
+
total_block_duration = timedelta()
|
|
191
|
+
active_hours = sorted([h for h, d in hourly_durations.items() if d.total_seconds() > 0])
|
|
192
|
+
|
|
193
|
+
if active_hours:
|
|
194
|
+
current_block_start_hour = active_hours[0]
|
|
195
|
+
for i in range(1, len(active_hours)):
|
|
196
|
+
# If there's a gap of more than an hour, the block is broken
|
|
197
|
+
if active_hours[i] > active_hours[i-1] + 1:
|
|
198
|
+
# Process the completed block
|
|
199
|
+
block_end_hour = active_hours[i-1]
|
|
200
|
+
|
|
201
|
+
first_event_in_block = min([e['timestamp'] for e in events if e['timestamp'].hour == current_block_start_hour])
|
|
202
|
+
last_event_in_block = max([e['timestamp'] for e in events if e['timestamp'].hour == block_end_hour])
|
|
203
|
+
|
|
204
|
+
total_block_duration += last_event_in_block - first_event_in_block
|
|
205
|
+
|
|
206
|
+
# Start a new block
|
|
207
|
+
current_block_start_hour = active_hours[i]
|
|
208
|
+
|
|
209
|
+
# Process the final block
|
|
210
|
+
last_block_end_hour = active_hours[-1]
|
|
211
|
+
first_event_in_block = min([e['timestamp'] for e in events if e['timestamp'].hour == current_block_start_hour])
|
|
212
|
+
last_event_in_block = max([e['timestamp'] for e in events if e['timestamp'].hour == last_block_end_hour])
|
|
213
|
+
total_block_duration += last_event_in_block - first_event_in_block
|
|
214
|
+
|
|
215
|
+
return hourly_durations, total_block_duration, unlock_time
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def get_screen_time(days_back, verbose=False, no_cache=False, include_weekends=False):
|
|
219
|
+
"""
|
|
220
|
+
Calculates screen time for the last N days, fetching logs day by day.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
days_back: Number of days to look back
|
|
224
|
+
verbose: Print detailed session information
|
|
225
|
+
no_cache: Force refetching of all logs
|
|
226
|
+
include_weekends: Count weekends toward expected work hours (default: False)
|
|
227
|
+
"""
|
|
228
|
+
# Lazy imports for faster --version response (third-party libs are slow to import)
|
|
229
|
+
from tzlocal import get_localzone
|
|
230
|
+
from halo import Halo
|
|
231
|
+
import colorama
|
|
232
|
+
|
|
233
|
+
conn = db_connect()
|
|
234
|
+
db_init(conn)
|
|
235
|
+
|
|
236
|
+
daily_hourly_durations = {}
|
|
237
|
+
daily_block_durations = {}
|
|
238
|
+
today = datetime.now().date()
|
|
239
|
+
total_actual_hours = 0
|
|
240
|
+
days_with_activity = set()
|
|
241
|
+
local_tz = get_localzone()
|
|
242
|
+
|
|
243
|
+
spinner = None
|
|
244
|
+
if not verbose:
|
|
245
|
+
spinner = Halo(text='Initializing...', spinner='dots')
|
|
246
|
+
spinner.start()
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
# We must process chronologically to handle sessions crossing midnight
|
|
250
|
+
dates_to_process = [today - timedelta(days=i) for i in range(days_back - 1, -1, -1)]
|
|
251
|
+
|
|
252
|
+
# Track whether the previous processed day ended unlocked (carry-over)
|
|
253
|
+
carry_over_unlocked = False
|
|
254
|
+
carry_over_tzinfo = None
|
|
255
|
+
|
|
256
|
+
for current_day in dates_to_process:
|
|
257
|
+
if spinner:
|
|
258
|
+
spinner.text = f"Processing {current_day.isoformat()}..."
|
|
259
|
+
|
|
260
|
+
logs = []
|
|
261
|
+
is_cached = not no_cache and db_is_day_cached(conn, current_day)
|
|
262
|
+
|
|
263
|
+
# Past days can be loaded from cache. Today is always fetched fresh.
|
|
264
|
+
if is_cached and current_day != today:
|
|
265
|
+
if spinner:
|
|
266
|
+
spinner.text = f"Loading logs from cache for {current_day.isoformat()}..."
|
|
267
|
+
logs = db_get_logs_for_day(conn, current_day)
|
|
268
|
+
if verbose:
|
|
269
|
+
print(f"\nLoaded {len(logs)} log entries from cache for {current_day.isoformat()}.")
|
|
270
|
+
else:
|
|
271
|
+
if spinner:
|
|
272
|
+
spinner.text = f"Fetching logs for {current_day.isoformat()}..."
|
|
273
|
+
|
|
274
|
+
start_of_day = datetime.combine(current_day, datetime.min.time())
|
|
275
|
+
end_of_day = datetime.combine(current_day, datetime.max.time())
|
|
276
|
+
start_of_day_aware = start_of_day.replace(tzinfo=local_tz)
|
|
277
|
+
end_of_day_aware = end_of_day.replace(tzinfo=local_tz)
|
|
278
|
+
|
|
279
|
+
if verbose:
|
|
280
|
+
print(f"\nFetching logs for {current_day.isoformat()}...")
|
|
281
|
+
|
|
282
|
+
predicate = 'process == "loginwindow" and eventMessage contains "com.apple.sessionagent.screenIs"'
|
|
283
|
+
command = [
|
|
284
|
+
'log', 'show', '--style', 'json',
|
|
285
|
+
'--predicate', predicate,
|
|
286
|
+
'--start', start_of_day_aware.strftime('%Y-%m-%d %H:%M:%S%z'),
|
|
287
|
+
'--end', end_of_day_aware.strftime('%Y-%m-%d %H:%M:%S%z')
|
|
288
|
+
]
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
result = subprocess.run(command, capture_output=True, text=True, check=True)
|
|
292
|
+
fetched_logs = json.loads(result.stdout)
|
|
293
|
+
logs.extend(fetched_logs)
|
|
294
|
+
|
|
295
|
+
if verbose:
|
|
296
|
+
print(f"Found {len(logs)} log entries.")
|
|
297
|
+
|
|
298
|
+
# Cache the newly fetched logs
|
|
299
|
+
db_cache_logs(conn, current_day, logs)
|
|
300
|
+
if current_day != today:
|
|
301
|
+
db_mark_day_as_cached(conn, current_day)
|
|
302
|
+
|
|
303
|
+
except subprocess.CalledProcessError as e:
|
|
304
|
+
if e.returncode == 1 and not e.stdout and not e.stderr:
|
|
305
|
+
pass # No logs found
|
|
306
|
+
else:
|
|
307
|
+
if spinner:
|
|
308
|
+
spinner.fail(f"Error executing log command for {current_day.isoformat()}")
|
|
309
|
+
print(f"Error executing log command for {current_day.isoformat()}: {e}")
|
|
310
|
+
except json.JSONDecodeError:
|
|
311
|
+
if spinner:
|
|
312
|
+
spinner.fail(f"Error decoding JSON from log output for {current_day.isoformat()}")
|
|
313
|
+
print(f"Error decoding JSON from log output for {current_day.isoformat()}.")
|
|
314
|
+
|
|
315
|
+
if not logs:
|
|
316
|
+
carry_over_unlocked = False
|
|
317
|
+
continue
|
|
318
|
+
|
|
319
|
+
# Inject a synthetic unlocked event at 00:00 only if previous day carried over unlocked
|
|
320
|
+
if carry_over_unlocked:
|
|
321
|
+
start_of_day = datetime.combine(current_day, datetime.min.time())
|
|
322
|
+
tzinfo = carry_over_tzinfo or local_tz
|
|
323
|
+
start_of_day = start_of_day.replace(tzinfo=tzinfo)
|
|
324
|
+
logs.append({
|
|
325
|
+
"timestamp": start_of_day.isoformat(),
|
|
326
|
+
"eventMessage": "screenIsUnlocked (synthetic carryover)"
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
hourly_durations, block_duration, last_unlock_time = process_day_logs(logs, current_day, verbose)
|
|
330
|
+
|
|
331
|
+
# If the last event of the day was an unlock, it's an open session
|
|
332
|
+
if last_unlock_time is not None:
|
|
333
|
+
# If it's today, calculate up to now
|
|
334
|
+
if current_day == today:
|
|
335
|
+
now = datetime.now(local_tz)
|
|
336
|
+
if last_unlock_time.date() == today:
|
|
337
|
+
duration = now - last_unlock_time
|
|
338
|
+
if verbose:
|
|
339
|
+
print(f" - Active session: from {last_unlock_time.strftime('%H:%M:%S')} to now (Duration: {duration})")
|
|
340
|
+
|
|
341
|
+
current_time = last_unlock_time
|
|
342
|
+
while current_time < now:
|
|
343
|
+
current_hour_start = current_time.replace(minute=0, second=0, microsecond=0)
|
|
344
|
+
next_hour_start = current_hour_start + timedelta(hours=1)
|
|
345
|
+
|
|
346
|
+
segment_end = min(now, next_hour_start)
|
|
347
|
+
duration_in_hour = segment_end - current_time
|
|
348
|
+
|
|
349
|
+
hourly_durations[current_time.hour] += duration_in_hour
|
|
350
|
+
|
|
351
|
+
current_time = next_hour_start
|
|
352
|
+
|
|
353
|
+
block_duration += now - last_unlock_time
|
|
354
|
+
# If it's a past day, calculate up to midnight
|
|
355
|
+
else:
|
|
356
|
+
end_of_day = datetime.combine(current_day, time(23, 59, 59, 999999), tzinfo=last_unlock_time.tzinfo)
|
|
357
|
+
duration = end_of_day - last_unlock_time
|
|
358
|
+
if verbose:
|
|
359
|
+
print(f" - Session carried over to next day: from {last_unlock_time.strftime('%H:%M:%S')} to 23:59:59 (Duration: {duration})")
|
|
360
|
+
|
|
361
|
+
current_time = last_unlock_time
|
|
362
|
+
while current_time < end_of_day:
|
|
363
|
+
current_hour_start = current_time.replace(minute=0, second=0, microsecond=0)
|
|
364
|
+
next_hour_start = current_hour_start + timedelta(hours=1)
|
|
365
|
+
|
|
366
|
+
segment_end = min(end_of_day, next_hour_start)
|
|
367
|
+
duration_in_hour = segment_end - current_time
|
|
368
|
+
|
|
369
|
+
hourly_durations[current_time.hour] += duration_in_hour
|
|
370
|
+
|
|
371
|
+
current_time = next_hour_start
|
|
372
|
+
|
|
373
|
+
block_duration += end_of_day - last_unlock_time
|
|
374
|
+
|
|
375
|
+
# Update carry-over state for the next day
|
|
376
|
+
if last_unlock_time is not None:
|
|
377
|
+
carry_over_unlocked = True
|
|
378
|
+
carry_over_tzinfo = last_unlock_time.tzinfo
|
|
379
|
+
else:
|
|
380
|
+
carry_over_unlocked = False
|
|
381
|
+
carry_over_tzinfo = None
|
|
382
|
+
|
|
383
|
+
if any(duration.total_seconds() > 0 for duration in hourly_durations.values()):
|
|
384
|
+
daily_hourly_durations[current_day] = hourly_durations
|
|
385
|
+
daily_block_durations[current_day] = block_duration
|
|
386
|
+
days_with_activity.add(current_day)
|
|
387
|
+
if verbose:
|
|
388
|
+
total_day_hours = sum(hourly_durations.values(), timedelta()).total_seconds() / 3600
|
|
389
|
+
print(f"Calculated {total_day_hours:.1f} hours of screen time.")
|
|
390
|
+
|
|
391
|
+
except KeyboardInterrupt:
|
|
392
|
+
if spinner:
|
|
393
|
+
spinner.warn("Process interrupted by user.")
|
|
394
|
+
print("\n\nProcess interrupted by user. Displaying summary for data collected so far...")
|
|
395
|
+
|
|
396
|
+
finally:
|
|
397
|
+
conn.close()
|
|
398
|
+
|
|
399
|
+
if spinner:
|
|
400
|
+
spinner.succeed("Log processing complete.")
|
|
401
|
+
spinner.stop()
|
|
402
|
+
colorama.reinit()
|
|
403
|
+
|
|
404
|
+
# --- Print Summaries ---
|
|
405
|
+
print("\n--- Daily Screen Time Summary ---")
|
|
406
|
+
if not daily_hourly_durations:
|
|
407
|
+
print("No screen time data found for the selected period.")
|
|
408
|
+
return
|
|
409
|
+
|
|
410
|
+
sorted_days = sorted(daily_hourly_durations.keys())
|
|
411
|
+
for day in sorted_days:
|
|
412
|
+
print_hourly_breakdown(day, daily_hourly_durations[day], daily_block_durations[day])
|
|
413
|
+
|
|
414
|
+
# --- Monthly Summary ---
|
|
415
|
+
print("\n--- Monthly Summary ---")
|
|
416
|
+
for day_data in daily_hourly_durations.values():
|
|
417
|
+
total_actual_hours += sum(day_data.values(), timedelta()).total_seconds() / 3600
|
|
418
|
+
|
|
419
|
+
total_block_hours = sum([d.total_seconds() for d in daily_block_durations.values()]) / 3600
|
|
420
|
+
|
|
421
|
+
# Calculate expected hours based on include_weekends flag
|
|
422
|
+
if include_weekends:
|
|
423
|
+
# All active days count toward expected hours
|
|
424
|
+
total_expected_hours = len(days_with_activity) * EXPECTED_HOURS_PER_DAY
|
|
425
|
+
else:
|
|
426
|
+
# Only count weekdays (Mon-Fri) towards expected hours
|
|
427
|
+
weekday_count = sum(1 for day in days_with_activity if day.weekday() < 5)
|
|
428
|
+
total_expected_hours = weekday_count * EXPECTED_HOURS_PER_DAY
|
|
429
|
+
|
|
430
|
+
if total_expected_hours > 0:
|
|
431
|
+
monthly_raw_percentage = (total_actual_hours / total_expected_hours) * 100
|
|
432
|
+
monthly_block_percentage = (total_block_hours / total_expected_hours) * 100
|
|
433
|
+
|
|
434
|
+
raw_str = f"Raw: {total_actual_hours:.1f} h ({monthly_raw_percentage:.0f}%)"
|
|
435
|
+
block_str = f"Block: {total_block_hours:.1f} h ({monthly_block_percentage:.0f}%)"
|
|
436
|
+
|
|
437
|
+
total_days = len(days_with_activity)
|
|
438
|
+
if not include_weekends:
|
|
439
|
+
weekday_count = sum(1 for day in days_with_activity if day.weekday() < 5)
|
|
440
|
+
weekend_count = total_days - weekday_count
|
|
441
|
+
day_breakdown = f"{total_days} active day(s)"
|
|
442
|
+
if weekend_count > 0:
|
|
443
|
+
day_breakdown += f" ({weekday_count} weekdays, {weekend_count} weekend days)"
|
|
444
|
+
else:
|
|
445
|
+
day_breakdown = f"{total_days} active day(s)"
|
|
446
|
+
|
|
447
|
+
print(f"Total for {day_breakdown}: {raw_str:<22}{block_str}")
|
|
448
|
+
else:
|
|
449
|
+
print("No activity to summarize.")
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def main():
|
|
453
|
+
"""
|
|
454
|
+
Main function to run the CLI.
|
|
455
|
+
"""
|
|
456
|
+
parser = argparse.ArgumentParser(description="A simple to use time tracking CLI for macOS.")
|
|
457
|
+
parser.add_argument(
|
|
458
|
+
'-V', '--version',
|
|
459
|
+
action='version',
|
|
460
|
+
version=f'time-buddy {get_version()}'
|
|
461
|
+
)
|
|
462
|
+
parser.add_argument(
|
|
463
|
+
'--days',
|
|
464
|
+
type=int,
|
|
465
|
+
default=7,
|
|
466
|
+
help='Number of days back to calculate screen time for. (default: 7)'
|
|
467
|
+
)
|
|
468
|
+
parser.add_argument(
|
|
469
|
+
'-v', '--verbose',
|
|
470
|
+
action='store_true',
|
|
471
|
+
help='Print detailed session information for validation.'
|
|
472
|
+
)
|
|
473
|
+
parser.add_argument(
|
|
474
|
+
'--no-cache',
|
|
475
|
+
action='store_true',
|
|
476
|
+
help='Force refetching of all logs, ignoring the cache.'
|
|
477
|
+
)
|
|
478
|
+
parser.add_argument(
|
|
479
|
+
'--clear-cache',
|
|
480
|
+
action='store_true',
|
|
481
|
+
help='Delete the cache file and exit.'
|
|
482
|
+
)
|
|
483
|
+
parser.add_argument(
|
|
484
|
+
'--include-weekends',
|
|
485
|
+
action='store_true',
|
|
486
|
+
help='Include weekends in expected work hours calculation. (default: False)'
|
|
487
|
+
)
|
|
488
|
+
args = parser.parse_args()
|
|
489
|
+
|
|
490
|
+
if args.clear_cache:
|
|
491
|
+
if os.path.exists(DB_FILE):
|
|
492
|
+
os.remove(DB_FILE)
|
|
493
|
+
print(f"Cache file '{DB_FILE}' has been deleted.")
|
|
494
|
+
else:
|
|
495
|
+
print("No cache file to delete.")
|
|
496
|
+
return
|
|
497
|
+
|
|
498
|
+
get_screen_time(args.days, args.verbose, args.no_cache, args.include_weekends)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
if __name__ == "__main__":
|
|
502
|
+
main()
|