clockin-tracker 0.1.0__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.
@@ -0,0 +1,172 @@
1
+ Metadata-Version: 2.3
2
+ Name: clockin-tracker
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Author: Markos Narinian
6
+ Author-email: Markos Narinian <manarinian@gmail.com>
7
+ Requires-Dist: platformdirs>=4.10.0
8
+ Requires-Dist: rich>=15.0.0
9
+ Requires-Dist: typer>=0.26.8
10
+ Requires-Python: >=3.13
11
+ Description-Content-Type: text/markdown
12
+
13
+ # clockin
14
+
15
+ [![Upload Python Package](https://github.com/markosnarinian/clockin/actions/workflows/python-publish.yml/badge.svg)](https://github.com/markosnarinian/clockin/actions/workflows/python-publish.yml)
16
+
17
+ **Time to (c)lock in!**
18
+
19
+ Welcome to a micro-project of mine!
20
+ I've wanted to build a time tracker for a while. It annoys me that every time tracker I've tried is either built for professionals/freelancers, or, in the case of TUI time trackers, built for devs tracking tasks (who cares!). I do a loooot of things and have backlogs full of hundreds of ideas. This is why I want to track time spent working on my projects vs working out/relaxing/reading vs sleeping vs wasting time, along with % time allocation by activity type and by project. And who doesn't like an activity heatmap?
21
+ It took me this long to build because I wanted to create proper apps for all platforms, but I decided I need this now — so I put it under micro-projects on my priorities list and built it as a terminal application (full TUI coming, relatively soon!).
22
+
23
+ ## Installation
24
+
25
+ clockin is published on PyPI as `clockin-tracker`.
26
+
27
+ **uv (recommended)**
28
+
29
+ ```sh
30
+ # Install it as a standalone CLI tool
31
+ uv tool install clockin-tracker
32
+
33
+ # ...or run it without installing anything
34
+ uvx clockin-tracker clockin --help
35
+ ```
36
+
37
+ **pipx**
38
+
39
+ ```sh
40
+ pipx install clockin-tracker
41
+ ```
42
+
43
+ **From source (for development/testing)**
44
+
45
+ ```sh
46
+ git clone https://github.com/markosnarinian/clockin
47
+ cd clockin
48
+ uv sync
49
+ uv run clockin --help
50
+ ```
51
+
52
+ ## Quick start
53
+
54
+ ```sh
55
+ $ clockin start clockin
56
+ ✓ Started session 3f9a1c2e-...
57
+
58
+ $ clockin status
59
+ ╭─ ▶ Active session ─────────────╮
60
+ │ Session 3f9a1c2e-... │
61
+ │ Project clockin │
62
+ │ Started 2026-07-08 09:00:00 │
63
+ │ Elapsed 00:12:34 │
64
+ ╰────────────────────────────────╯
65
+
66
+ $ clockin stop
67
+ ■ Stopped session 3f9a1c2e-...
68
+ ```
69
+
70
+ Prefer emoji over checkmarks? Switch to the built-in `quirky` style — see [Configuration](#configuration).
71
+
72
+ ## Commands
73
+
74
+ | Command | Description |
75
+ | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
76
+ | `clockin start [PROJECT]` | Start a new session, optionally tagged with a project name. Fails if a session is already running. |
77
+ | `clockin stop` | Stop the active session. |
78
+ | `clockin status` | Show the active session (with elapsed time) or a pending canceled session. |
79
+ | `clockin resume [--stop-now]` | Resume the most recently stopped session. With `--stop-now`, immediately re-stops it (e.g. to backdate a correction). |
80
+ | `clockin cancel` | Cancel the active session instead of stopping it. |
81
+ | `clockin restore [--stopped]` | Restore the pending canceled session. With `--stopped`, restore it as stopped instead of active. |
82
+ | `clockin import FILE` | Bulk-import sessions from a JSON file. See [Importing sessions](#importing-sessions). |
83
+ | `clockin config show [--full] [--default]` | Print your configuration, the full merged configuration, or the built-in defaults. |
84
+ | `clockin config edit` | Open the config file in `$VISUAL`/`$EDITOR`. |
85
+ | `clockin config reset` | Reset configuration to defaults. |
86
+
87
+ ## Session lifecycle
88
+
89
+ At most one session can be active at a time, and at most one canceled session can be pending restoration at a time:
90
+
91
+ ```
92
+ start ──▶ active ──stop──▶ stopped ──resume──▶ active
93
+
94
+ cancel
95
+
96
+
97
+ canceled ──restore──▶ active
98
+
99
+ └──restore --stopped──▶ stopped
100
+ ```
101
+
102
+ Starting a new session discards any pending canceled session, so restore it first if you want to keep it.
103
+
104
+ ## Importing sessions
105
+
106
+ `clockin import FILE` reads a JSON array of sessions, or an object with a `sessions` array (see [sample.json](sample.json)):
107
+
108
+ ```json
109
+ {
110
+ "sessions": [
111
+ {
112
+ "id": "work-session-001",
113
+ "project": "clockin",
114
+ "started_at": "2026-01-01T09:00:00Z",
115
+ "stopped_at": "2026-01-01T10:30:00Z",
116
+ "canceled_at": null
117
+ }
118
+ ]
119
+ }
120
+ ```
121
+
122
+ - `started_at` is required; `stopped_at`/`canceled_at` are optional but mutually exclusive.
123
+ - Timestamps accept a Unix timestamp or an ISO-8601 datetime string.
124
+ - `id` is optional (a UUID is generated if omitted); `project` is optional.
125
+ - At most one imported session may be active, and at most one may be canceled — and only if you don't already have one of that kind.
126
+ - Sessions with an `id` that already exists are skipped, so imports are safe to re-run.
127
+
128
+ ### Importing from other software
129
+
130
+ When moving history from another time tracker, calendar, spreadsheet, or database, convert the source export into clockin's import JSON first. Use stable IDs from the original software when possible, optionally prefixed with the source name (for example, `toggl-123456`), so you can safely re-run the same import without duplicating sessions.
131
+
132
+ Map each source entry to one clockin session state:
133
+
134
+ - completed entries become stopped sessions with `started_at` and `stopped_at` set;
135
+ - one open/running entry may become an active session with both `stopped_at` and `canceled_at` set to `null`;
136
+ - deleted or voided entries should usually be skipped unless you intentionally want one represented as the single pending canceled session.
137
+
138
+ Prefer ISO-8601 timestamps with explicit timezones (`2026-01-01T09:00:00-05:00` or `2026-01-01T14:00:00Z`) when converting from software that exports local times. If the source only has a duration, compute `stopped_at` from `started_at + duration`.
139
+
140
+ Agents helping with imports should use the project skill in [`skills/importing-sessions/SKILL.md`](skills/importing-sessions/SKILL.md) for the full conversion workflow, validation checklist, and safety guidance before running `clockin import`.
141
+
142
+ ## Configuration
143
+
144
+ clockin stores its config as JSON in a per-OS config directory. Run `clockin config edit` to open it in `$VISUAL`/`$EDITOR`, or `clockin config reset` to restore the defaults.
145
+
146
+ | Key | Default | Description |
147
+ | ----------------- | ----------- | ---------------------------------------------------------------------------------------------- |
148
+ | `style` | `"default"` | Which output style to use: `"default"` (plain glyphs) or `"quirky"` (emoji, more personality). |
149
+ | `default_project` | `null` | Reserved for a future default project on `clockin start`. Not enforced yet. |
150
+ | `require_project` | `true` | Reserved for making `clockin start` require a project. Not enforced yet. |
151
+
152
+ Run `clockin config show --default` to see the full built-in configuration, including the `styles` definitions themselves (colors, glyphs, and messages per state) if you want to customize them.
153
+
154
+ ## Data storage
155
+
156
+ Sessions are stored in a SQLite database in a per-OS data directory, versioned by schema (currently `clockin.v1.0.db`). Back it up like any other file — there's no built-in export command yet, but `clockin import` can be used in reverse by reading the database directly with any SQLite client.
157
+
158
+ ## Development
159
+
160
+ ```sh
161
+ git clone https://github.com/markosnarinian/clockin
162
+ cd clockin
163
+ uv sync
164
+ uv run pytest
165
+ uv run ruff check .
166
+ uv run ruff format .
167
+ uv run clockin --help
168
+ ```
169
+
170
+ ## License
171
+
172
+ [AGPL-3.0](LICENSE)
@@ -0,0 +1,160 @@
1
+ # clockin
2
+
3
+ [![Upload Python Package](https://github.com/markosnarinian/clockin/actions/workflows/python-publish.yml/badge.svg)](https://github.com/markosnarinian/clockin/actions/workflows/python-publish.yml)
4
+
5
+ **Time to (c)lock in!**
6
+
7
+ Welcome to a micro-project of mine!
8
+ I've wanted to build a time tracker for a while. It annoys me that every time tracker I've tried is either built for professionals/freelancers, or, in the case of TUI time trackers, built for devs tracking tasks (who cares!). I do a loooot of things and have backlogs full of hundreds of ideas. This is why I want to track time spent working on my projects vs working out/relaxing/reading vs sleeping vs wasting time, along with % time allocation by activity type and by project. And who doesn't like an activity heatmap?
9
+ It took me this long to build because I wanted to create proper apps for all platforms, but I decided I need this now — so I put it under micro-projects on my priorities list and built it as a terminal application (full TUI coming, relatively soon!).
10
+
11
+ ## Installation
12
+
13
+ clockin is published on PyPI as `clockin-tracker`.
14
+
15
+ **uv (recommended)**
16
+
17
+ ```sh
18
+ # Install it as a standalone CLI tool
19
+ uv tool install clockin-tracker
20
+
21
+ # ...or run it without installing anything
22
+ uvx clockin-tracker clockin --help
23
+ ```
24
+
25
+ **pipx**
26
+
27
+ ```sh
28
+ pipx install clockin-tracker
29
+ ```
30
+
31
+ **From source (for development/testing)**
32
+
33
+ ```sh
34
+ git clone https://github.com/markosnarinian/clockin
35
+ cd clockin
36
+ uv sync
37
+ uv run clockin --help
38
+ ```
39
+
40
+ ## Quick start
41
+
42
+ ```sh
43
+ $ clockin start clockin
44
+ ✓ Started session 3f9a1c2e-...
45
+
46
+ $ clockin status
47
+ ╭─ ▶ Active session ─────────────╮
48
+ │ Session 3f9a1c2e-... │
49
+ │ Project clockin │
50
+ │ Started 2026-07-08 09:00:00 │
51
+ │ Elapsed 00:12:34 │
52
+ ╰────────────────────────────────╯
53
+
54
+ $ clockin stop
55
+ ■ Stopped session 3f9a1c2e-...
56
+ ```
57
+
58
+ Prefer emoji over checkmarks? Switch to the built-in `quirky` style — see [Configuration](#configuration).
59
+
60
+ ## Commands
61
+
62
+ | Command | Description |
63
+ | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
64
+ | `clockin start [PROJECT]` | Start a new session, optionally tagged with a project name. Fails if a session is already running. |
65
+ | `clockin stop` | Stop the active session. |
66
+ | `clockin status` | Show the active session (with elapsed time) or a pending canceled session. |
67
+ | `clockin resume [--stop-now]` | Resume the most recently stopped session. With `--stop-now`, immediately re-stops it (e.g. to backdate a correction). |
68
+ | `clockin cancel` | Cancel the active session instead of stopping it. |
69
+ | `clockin restore [--stopped]` | Restore the pending canceled session. With `--stopped`, restore it as stopped instead of active. |
70
+ | `clockin import FILE` | Bulk-import sessions from a JSON file. See [Importing sessions](#importing-sessions). |
71
+ | `clockin config show [--full] [--default]` | Print your configuration, the full merged configuration, or the built-in defaults. |
72
+ | `clockin config edit` | Open the config file in `$VISUAL`/`$EDITOR`. |
73
+ | `clockin config reset` | Reset configuration to defaults. |
74
+
75
+ ## Session lifecycle
76
+
77
+ At most one session can be active at a time, and at most one canceled session can be pending restoration at a time:
78
+
79
+ ```
80
+ start ──▶ active ──stop──▶ stopped ──resume──▶ active
81
+
82
+ cancel
83
+
84
+
85
+ canceled ──restore──▶ active
86
+
87
+ └──restore --stopped──▶ stopped
88
+ ```
89
+
90
+ Starting a new session discards any pending canceled session, so restore it first if you want to keep it.
91
+
92
+ ## Importing sessions
93
+
94
+ `clockin import FILE` reads a JSON array of sessions, or an object with a `sessions` array (see [sample.json](sample.json)):
95
+
96
+ ```json
97
+ {
98
+ "sessions": [
99
+ {
100
+ "id": "work-session-001",
101
+ "project": "clockin",
102
+ "started_at": "2026-01-01T09:00:00Z",
103
+ "stopped_at": "2026-01-01T10:30:00Z",
104
+ "canceled_at": null
105
+ }
106
+ ]
107
+ }
108
+ ```
109
+
110
+ - `started_at` is required; `stopped_at`/`canceled_at` are optional but mutually exclusive.
111
+ - Timestamps accept a Unix timestamp or an ISO-8601 datetime string.
112
+ - `id` is optional (a UUID is generated if omitted); `project` is optional.
113
+ - At most one imported session may be active, and at most one may be canceled — and only if you don't already have one of that kind.
114
+ - Sessions with an `id` that already exists are skipped, so imports are safe to re-run.
115
+
116
+ ### Importing from other software
117
+
118
+ When moving history from another time tracker, calendar, spreadsheet, or database, convert the source export into clockin's import JSON first. Use stable IDs from the original software when possible, optionally prefixed with the source name (for example, `toggl-123456`), so you can safely re-run the same import without duplicating sessions.
119
+
120
+ Map each source entry to one clockin session state:
121
+
122
+ - completed entries become stopped sessions with `started_at` and `stopped_at` set;
123
+ - one open/running entry may become an active session with both `stopped_at` and `canceled_at` set to `null`;
124
+ - deleted or voided entries should usually be skipped unless you intentionally want one represented as the single pending canceled session.
125
+
126
+ Prefer ISO-8601 timestamps with explicit timezones (`2026-01-01T09:00:00-05:00` or `2026-01-01T14:00:00Z`) when converting from software that exports local times. If the source only has a duration, compute `stopped_at` from `started_at + duration`.
127
+
128
+ Agents helping with imports should use the project skill in [`skills/importing-sessions/SKILL.md`](skills/importing-sessions/SKILL.md) for the full conversion workflow, validation checklist, and safety guidance before running `clockin import`.
129
+
130
+ ## Configuration
131
+
132
+ clockin stores its config as JSON in a per-OS config directory. Run `clockin config edit` to open it in `$VISUAL`/`$EDITOR`, or `clockin config reset` to restore the defaults.
133
+
134
+ | Key | Default | Description |
135
+ | ----------------- | ----------- | ---------------------------------------------------------------------------------------------- |
136
+ | `style` | `"default"` | Which output style to use: `"default"` (plain glyphs) or `"quirky"` (emoji, more personality). |
137
+ | `default_project` | `null` | Reserved for a future default project on `clockin start`. Not enforced yet. |
138
+ | `require_project` | `true` | Reserved for making `clockin start` require a project. Not enforced yet. |
139
+
140
+ Run `clockin config show --default` to see the full built-in configuration, including the `styles` definitions themselves (colors, glyphs, and messages per state) if you want to customize them.
141
+
142
+ ## Data storage
143
+
144
+ Sessions are stored in a SQLite database in a per-OS data directory, versioned by schema (currently `clockin.v1.0.db`). Back it up like any other file — there's no built-in export command yet, but `clockin import` can be used in reverse by reading the database directly with any SQLite client.
145
+
146
+ ## Development
147
+
148
+ ```sh
149
+ git clone https://github.com/markosnarinian/clockin
150
+ cd clockin
151
+ uv sync
152
+ uv run pytest
153
+ uv run ruff check .
154
+ uv run ruff format .
155
+ uv run clockin --help
156
+ ```
157
+
158
+ ## License
159
+
160
+ [AGPL-3.0](LICENSE)
@@ -0,0 +1,29 @@
1
+ [project]
2
+ name = "clockin-tracker"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Markos Narinian", email = "manarinian@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.13"
10
+ dependencies = [
11
+ "platformdirs>=4.10.0",
12
+ "rich>=15.0.0",
13
+ "typer>=0.26.8",
14
+ ]
15
+
16
+ [project.scripts]
17
+ clockin = "clockin:main"
18
+
19
+ [build-system]
20
+ requires = ["uv_build>=0.11.3,<0.12.0"]
21
+ build-backend = "uv_build"
22
+
23
+ [tool.uv.build-backend]
24
+ module-name = "clockin"
25
+
26
+ [dependency-groups]
27
+ dev = [
28
+ "ruff>=0.15.20",
29
+ ]
@@ -0,0 +1,8 @@
1
+ AUTHOR = "markosnarinian"
2
+ APP_NAME = "clockin"
3
+
4
+
5
+ def main() -> None:
6
+ from clockin.main import app
7
+
8
+ app()
@@ -0,0 +1,104 @@
1
+ import json
2
+ from collections.abc import Mapping
3
+ from typing import Any
4
+
5
+ import platformdirs
6
+
7
+ from clockin import APP_NAME, AUTHOR
8
+
9
+ CONFIGURATION = "config.json"
10
+ DEFAULT_CONFIGURATION: dict[str, Any] = {
11
+ "styles": {
12
+ "default": {
13
+ "started": ("green", "✓", "Started session"),
14
+ "stopped": ("red", "■", "Stopped session"),
15
+ "resumed": ("green", "▶", "Resumed session"),
16
+ "stopped_updated": ("red", "■", "Updated stopped session"),
17
+ "canceled": ("red", "×", "Canceled session"),
18
+ "restored": ("green", "↺", "Restored session"),
19
+ "restored_stopped": ("red", "↺", "Restored session"),
20
+ "restored_stopped_note": (
21
+ "red",
22
+ "↳",
23
+ "stopped_at set to the cancellation time",
24
+ ),
25
+ "warning": ("yellow", "!"),
26
+ "active_title": ("green", "▶", "Active session"),
27
+ "canceled_title": ("red", "×", "Canceled session"),
28
+ "no_active": ("yellow", "!", "No active session"),
29
+ "report_title": ("yellow", "▤", "Report"),
30
+ "no_sessions": ("yellow", "!", "No sessions to report"),
31
+ },
32
+ "quirky": {
33
+ "started": ("green", "🚀", "Blast off! Session started"),
34
+ "stopped": ("red", "🛑", "Whoa there! Session stopped"),
35
+ "resumed": ("green", "🎬", "And we're back! Session resumed"),
36
+ "stopped_updated": ("red", "📝", "Tweaked that stopped session"),
37
+ "canceled": ("red", "🙅", "Nope! Session canceled"),
38
+ "restored": ("green", "🪄", "Ta-da! Session restored"),
39
+ "restored_stopped": ("red", "🪄", "Poof! Stopped session restored"),
40
+ "restored_stopped_note": (
41
+ "red",
42
+ "🕰️",
43
+ "stopped_at set to the cancellation time",
44
+ ),
45
+ "warning": ("yellow", "🤔"),
46
+ "active_title": ("green", "⏱️", "Clock's ticking!"),
47
+ "canceled_title": ("red", "🚫", "Canceled session"),
48
+ "no_active": ("yellow", "😴", "Nothing running... take a break?"),
49
+ "report_title": ("yellow", "📊", "Here's the tea"),
50
+ "no_sessions": ("yellow", "🤷", "Nothing to report yet"),
51
+ },
52
+ },
53
+ "default_project": None,
54
+ "style": "default",
55
+ "require_project": True,
56
+ }
57
+ INITIAL_CONFIGURATION: dict[str, Any] = {
58
+ "default_project": None,
59
+ "style": "default",
60
+ "require_project": True,
61
+ }
62
+
63
+ configuration_path = (
64
+ platformdirs.user_config_path(
65
+ appauthor=AUTHOR,
66
+ appname=APP_NAME,
67
+ ensure_exists=True,
68
+ )
69
+ / CONFIGURATION
70
+ )
71
+
72
+
73
+ def _write_configuration(configuration: dict[str, Any]) -> None:
74
+ configuration_path.write_text(
75
+ json.dumps(configuration, indent=2) + "\n",
76
+ encoding="utf-8",
77
+ )
78
+
79
+
80
+ def _initialize_configuration() -> None:
81
+ _write_configuration(INITIAL_CONFIGURATION)
82
+
83
+
84
+ def load_user_configuration() -> dict[str, Any]:
85
+ return json.loads(configuration_path.read_text(encoding="utf-8"))
86
+
87
+
88
+ def load_configuration() -> dict[str, Any]:
89
+ if not configuration_path.exists() or not configuration_path.is_file():
90
+ _initialize_configuration()
91
+ configuration = DEFAULT_CONFIGURATION.copy()
92
+ configuration.update(load_user_configuration())
93
+ return configuration
94
+
95
+
96
+ def update_configuration(configuration: Mapping[str, Any]) -> dict[str, Any]:
97
+ updated_configuration = load_configuration()
98
+ updated_configuration.update(configuration)
99
+ _write_configuration(updated_configuration)
100
+ return updated_configuration
101
+
102
+
103
+ def reset_configuration() -> None:
104
+ _initialize_configuration()