tm-timer 1.0.1__tar.gz → 2.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.
- tm_timer-2.0/LICENSE +22 -0
- tm_timer-2.0/PKG-INFO +67 -0
- tm_timer-2.0/README.md +53 -0
- tm_timer-2.0/pyproject.toml +16 -0
- {tm_timer-1.0.1 → tm_timer-2.0}/setup.py +1 -1
- tm_timer-2.0/tm_timer/cli.py +387 -0
- tm_timer-2.0/tm_timer.egg-info/PKG-INFO +67 -0
- {tm_timer-1.0.1 → tm_timer-2.0}/tm_timer.egg-info/SOURCES.txt +2 -0
- tm_timer-2.0/tm_timer.egg-info/entry_points.txt +2 -0
- tm_timer-1.0.1/PKG-INFO +0 -13
- tm_timer-1.0.1/README.md +0 -24
- tm_timer-1.0.1/tm_timer/cli.py +0 -102
- tm_timer-1.0.1/tm_timer.egg-info/PKG-INFO +0 -13
- tm_timer-1.0.1/tm_timer.egg-info/entry_points.txt +0 -2
- {tm_timer-1.0.1 → tm_timer-2.0}/setup.cfg +0 -0
- {tm_timer-1.0.1 → tm_timer-2.0}/tm_timer/__init__.py +0 -0
- {tm_timer-1.0.1 → tm_timer-2.0}/tm_timer.egg-info/dependency_links.txt +0 -0
- {tm_timer-1.0.1 → tm_timer-2.0}/tm_timer.egg-info/top_level.txt +0 -0
tm_timer-2.0/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Ph4nt01
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
20
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
21
|
+
DEALINGS IN THE SOFTWARE.
|
|
22
|
+
|
tm_timer-2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tm-timer
|
|
3
|
+
Version: 2.0
|
|
4
|
+
Summary: Minimalistic terminal-based timer with Study/Train tracking
|
|
5
|
+
Author: ph4nt01
|
|
6
|
+
Author-email: Ph4nt01 <ph4nt0.84@email.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Requires-Python: >=3.6
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Dynamic: author
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
Dynamic: requires-python
|
|
14
|
+
|
|
15
|
+
# In The Name of God
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## ⏱ tm : Minimalistic Terminal-based timer with Study/Train tracking
|
|
20
|
+
|
|
21
|
+
### Features:
|
|
22
|
+
|
|
23
|
+
- Run from terminal:
|
|
24
|
+
- Countdown timer: `tm 10s`, `tm 25m`, `tm 1h30m`
|
|
25
|
+
- Study session: `tm --study` (tracks across sessions, resets daily)
|
|
26
|
+
- Train session: `tm --train` (tracks across sessions, resets daily)
|
|
27
|
+
- Daily study/train totals saved in `~/.tm_timer/data.json`
|
|
28
|
+
- Tables and statistics:
|
|
29
|
+
- `tm --studystatus` / `tm --trainstatus` → show daily table and Standard Deviation
|
|
30
|
+
- `tm --tables` → export `tables.md`
|
|
31
|
+
- Statistics:
|
|
32
|
+
- Average daily hours
|
|
33
|
+
- Standard Deviation of daily hours
|
|
34
|
+
- Status report (Very Good / Good / Not Good)
|
|
35
|
+
- Controls:
|
|
36
|
+
- Press `p` to pause/resume
|
|
37
|
+
- Press `q` to quit early
|
|
38
|
+
- Supports human durations like `2h45m30s`
|
|
39
|
+
- Simple, no graphical BLOAT.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
### Installation:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
sudo apt install pipx # if not installed
|
|
47
|
+
pipx install tm-timer # to install tm-timer
|
|
48
|
+
pipx ensurepath # to add tm to PATH
|
|
49
|
+
exec $SHELL # reload shell
|
|
50
|
+
|
|
51
|
+
#or run locally with using cli.py
|
|
52
|
+
python3 cli.py 3h40m
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Example Usage:
|
|
56
|
+
bash
|
|
57
|
+
Copy code
|
|
58
|
+
tm --study
|
|
59
|
+
# Timer starts, press 'q' to stop, progress saved daily
|
|
60
|
+
|
|
61
|
+
tm --studystatus
|
|
62
|
+
# Shows study table and stats
|
|
63
|
+
|
|
64
|
+
tm --tables
|
|
65
|
+
# Exports study/train tables as tables.md
|
|
66
|
+
⏱ tm preview:
|
|
67
|
+
|
tm_timer-2.0/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# In The Name of God
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## ⏱ tm : Minimalistic Terminal-based timer with Study/Train tracking
|
|
6
|
+
|
|
7
|
+
### Features:
|
|
8
|
+
|
|
9
|
+
- Run from terminal:
|
|
10
|
+
- Countdown timer: `tm 10s`, `tm 25m`, `tm 1h30m`
|
|
11
|
+
- Study session: `tm --study` (tracks across sessions, resets daily)
|
|
12
|
+
- Train session: `tm --train` (tracks across sessions, resets daily)
|
|
13
|
+
- Daily study/train totals saved in `~/.tm_timer/data.json`
|
|
14
|
+
- Tables and statistics:
|
|
15
|
+
- `tm --studystatus` / `tm --trainstatus` → show daily table and Standard Deviation
|
|
16
|
+
- `tm --tables` → export `tables.md`
|
|
17
|
+
- Statistics:
|
|
18
|
+
- Average daily hours
|
|
19
|
+
- Standard Deviation of daily hours
|
|
20
|
+
- Status report (Very Good / Good / Not Good)
|
|
21
|
+
- Controls:
|
|
22
|
+
- Press `p` to pause/resume
|
|
23
|
+
- Press `q` to quit early
|
|
24
|
+
- Supports human durations like `2h45m30s`
|
|
25
|
+
- Simple, no graphical BLOAT.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
### Installation:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
sudo apt install pipx # if not installed
|
|
33
|
+
pipx install tm-timer # to install tm-timer
|
|
34
|
+
pipx ensurepath # to add tm to PATH
|
|
35
|
+
exec $SHELL # reload shell
|
|
36
|
+
|
|
37
|
+
#or run locally with using cli.py
|
|
38
|
+
python3 cli.py 3h40m
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Example Usage:
|
|
42
|
+
bash
|
|
43
|
+
Copy code
|
|
44
|
+
tm --study
|
|
45
|
+
# Timer starts, press 'q' to stop, progress saved daily
|
|
46
|
+
|
|
47
|
+
tm --studystatus
|
|
48
|
+
# Shows study table and stats
|
|
49
|
+
|
|
50
|
+
tm --tables
|
|
51
|
+
# Exports study/train tables as tables.md
|
|
52
|
+
⏱ tm preview:
|
|
53
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tm-timer"
|
|
7
|
+
version = "2.0"
|
|
8
|
+
description = "Minimalistic terminal-based timer with Study/Train tracking"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{ name="Ph4nt01", email="ph4nt0.84@email.com" }]
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
requires-python = ">=3.7"
|
|
13
|
+
dependencies = []
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
tm = "tm_timer.cli:main"
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
# File: cli.py
|
|
2
|
+
import argparse
|
|
3
|
+
import atexit
|
|
4
|
+
import datetime
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import sys
|
|
10
|
+
import termios
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
import tty
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
# --- Terminal helpers (kept minimal) ---
|
|
17
|
+
try:
|
|
18
|
+
original_term_settings = termios.tcgetattr(sys.stdin.fileno())
|
|
19
|
+
except Exception:
|
|
20
|
+
original_term_settings = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def restore_terminal():
|
|
24
|
+
if original_term_settings is None:
|
|
25
|
+
return
|
|
26
|
+
try:
|
|
27
|
+
termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, original_term_settings)
|
|
28
|
+
except Exception:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
atexit.register(restore_terminal)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def with_raw_mode():
|
|
36
|
+
fd = sys.stdin.fileno()
|
|
37
|
+
old_settings = termios.tcgetattr(fd)
|
|
38
|
+
tty.setraw(fd)
|
|
39
|
+
return old_settings
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_char():
|
|
43
|
+
fd = sys.stdin.fileno()
|
|
44
|
+
old_settings = with_raw_mode()
|
|
45
|
+
try:
|
|
46
|
+
ch = sys.stdin.read(1)
|
|
47
|
+
finally:
|
|
48
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
49
|
+
return ch
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# --- Storage setup ---
|
|
53
|
+
DATA_DIR = Path(os.path.expanduser("~")) / ".tm_timer"
|
|
54
|
+
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
DATA_FILE = DATA_DIR / "data.json"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def load_data():
|
|
59
|
+
if not DATA_FILE.exists():
|
|
60
|
+
return {"study": {}, "train": {}}
|
|
61
|
+
try:
|
|
62
|
+
with DATA_FILE.open("r", encoding="utf-8") as f:
|
|
63
|
+
data = json.load(f)
|
|
64
|
+
except Exception:
|
|
65
|
+
return {"study": {}, "train": {}}
|
|
66
|
+
# Ensure keys
|
|
67
|
+
if "study" not in data:
|
|
68
|
+
data["study"] = {}
|
|
69
|
+
if "train" not in data:
|
|
70
|
+
data["train"] = {}
|
|
71
|
+
return data
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def save_data(data):
|
|
75
|
+
tmp = DATA_FILE.with_suffix(".tmp")
|
|
76
|
+
with tmp.open("w", encoding="utf-8") as f:
|
|
77
|
+
json.dump(data, f, indent=2)
|
|
78
|
+
tmp.replace(DATA_FILE)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_day_seconds(activity, date_str):
|
|
82
|
+
data = load_data()
|
|
83
|
+
return int(data.get(activity, {}).get(date_str, 0))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def update_day_seconds(activity, date_str, seconds):
|
|
87
|
+
data = load_data()
|
|
88
|
+
data.setdefault(activity, {})
|
|
89
|
+
data[activity][date_str] = int(seconds)
|
|
90
|
+
save_data(data)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# --- Utility formatters and stats ---
|
|
94
|
+
def format_hms(total_seconds):
|
|
95
|
+
total_seconds = int(round(total_seconds))
|
|
96
|
+
h = total_seconds // 3600
|
|
97
|
+
m = (total_seconds % 3600) // 60
|
|
98
|
+
s = total_seconds % 60
|
|
99
|
+
return f"{h:02d}:{m:02d}:{s:02d}"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def seconds_to_hours(seconds):
|
|
103
|
+
return seconds / 3600.0
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def compute_stats_for_activity(activity):
|
|
107
|
+
data = load_data().get(activity, {})
|
|
108
|
+
# Use values sorted by date
|
|
109
|
+
seconds_list = [int(v) for k, v in sorted(data.items())]
|
|
110
|
+
n = len(seconds_list)
|
|
111
|
+
if n == 0:
|
|
112
|
+
return {"n": 0, "avg": 0.0, "sd": 0.0, "hours_list": []}
|
|
113
|
+
hours = [seconds_to_hours(s) for s in seconds_list]
|
|
114
|
+
avg = sum(hours) / n
|
|
115
|
+
if n < 2:
|
|
116
|
+
sd = 0.0
|
|
117
|
+
else:
|
|
118
|
+
# population SD (divide by n)
|
|
119
|
+
var = sum((x - avg) ** 2 for x in hours) / n
|
|
120
|
+
sd = math.sqrt(var)
|
|
121
|
+
return {"n": n, "avg": avg, "sd": sd, "hours_list": hours, "seconds_list": seconds_list}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def status_from_avg_sd(avg, sd):
|
|
125
|
+
half = avg / 2.0
|
|
126
|
+
if math.isclose(sd, half, rel_tol=1e-9, abs_tol=1e-9):
|
|
127
|
+
return "Good"
|
|
128
|
+
if sd > half:
|
|
129
|
+
return f"Not Good - should be lower than {half:.2f}h"
|
|
130
|
+
return "Very Good"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# --- Printing helpers ---
|
|
134
|
+
def print_activity_table(activity):
|
|
135
|
+
data = load_data().get(activity, {})
|
|
136
|
+
if not data:
|
|
137
|
+
print(f"No entries for {activity.upper()}.")
|
|
138
|
+
return
|
|
139
|
+
# Sort by date descending (most recent first)
|
|
140
|
+
items = sorted(data.items(), key=lambda kv: kv[0], reverse=True)
|
|
141
|
+
print(f"\n| Today's Name | {activity.upper()} Hour |")
|
|
142
|
+
print("| ------------ | ----------------- |")
|
|
143
|
+
for date_str, secs in items:
|
|
144
|
+
try:
|
|
145
|
+
dt = datetime.datetime.strptime(date_str, "%Y-%m-%d").date()
|
|
146
|
+
day_name = dt.strftime("%a").upper()
|
|
147
|
+
except Exception:
|
|
148
|
+
day_name = date_str
|
|
149
|
+
print(f"| {day_name} | {format_hms(secs)} |")
|
|
150
|
+
stats = compute_stats_for_activity(activity)
|
|
151
|
+
if stats["n"] < 2:
|
|
152
|
+
print("\nNot enough recording for statistics (need 2+ days).")
|
|
153
|
+
else:
|
|
154
|
+
print(
|
|
155
|
+
f"\n[AVG] = {stats['avg']:.2f}h [Standard Deviation of Time] = {stats['sd']:.2f}h Status = {status_from_avg_sd(stats['avg'], stats['sd'])}"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def write_tables_md(path="tables.md"):
|
|
160
|
+
data = load_data()
|
|
161
|
+
lines = []
|
|
162
|
+
for activity in ("study", "train"):
|
|
163
|
+
lines.append(f"## {activity.upper()} Table\n")
|
|
164
|
+
lines.append("| Date | Day | Time |\n")
|
|
165
|
+
lines.append("| ---- | --- | ---- |\n")
|
|
166
|
+
items = sorted(data.get(activity, {}).items(), key=lambda kv: kv[0], reverse=True)
|
|
167
|
+
for date_str, secs in items:
|
|
168
|
+
try:
|
|
169
|
+
dt = datetime.datetime.strptime(date_str, "%Y-%m-%d").date()
|
|
170
|
+
day_name = dt.strftime("%a").upper()
|
|
171
|
+
except Exception:
|
|
172
|
+
day_name = ""
|
|
173
|
+
lines.append(f"| {date_str} | {day_name} | {format_hms(secs)} |\n")
|
|
174
|
+
stats = compute_stats_for_activity(activity)
|
|
175
|
+
if stats["n"] < 2:
|
|
176
|
+
lines.append("\nNot enough recording for statistics (need 2+ days).\n\n")
|
|
177
|
+
else:
|
|
178
|
+
lines.append(
|
|
179
|
+
f"\n[AVG] = {stats['avg']:.2f}h [Standard Deviation of Time] = {stats['sd']:.2f}h Status = {status_from_avg_sd(stats['avg'], stats['sd'])}\n\n"
|
|
180
|
+
)
|
|
181
|
+
out_path = Path.cwd() / path
|
|
182
|
+
out_path.write_text("".join(lines), encoding="utf-8")
|
|
183
|
+
return str(out_path.resolve())
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# --- Time parsing for countdown mode (compat) ---
|
|
187
|
+
import re
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def parse_time_input(user_input):
|
|
191
|
+
pattern = r"^\s*(?:(\d+)h)?\s*(?:(\d+)m)?\s*(?:(\d+)s)?\s*$"
|
|
192
|
+
match = re.fullmatch(pattern, user_input.strip().lower())
|
|
193
|
+
if not match or not any(match.groups()):
|
|
194
|
+
raise ValueError("Invalid time format. Use format like '1h30m45s' or '25m' or '45s'.")
|
|
195
|
+
hours = int(match.group(1)) if match.group(1) else 0
|
|
196
|
+
minutes = int(match.group(2)) if match.group(2) else 0
|
|
197
|
+
seconds = int(match.group(3)) if match.group(3) else 0
|
|
198
|
+
return hours * 3600 + minutes * 60 + seconds
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# --- Main run logic for activity timers (study/train) ---
|
|
202
|
+
def run_activity(activity):
|
|
203
|
+
global running, paused
|
|
204
|
+
running = True
|
|
205
|
+
paused = False
|
|
206
|
+
|
|
207
|
+
today = datetime.date.today()
|
|
208
|
+
today_str = today.isoformat()
|
|
209
|
+
stored_today = get_day_seconds(activity, today_str)
|
|
210
|
+
|
|
211
|
+
# seconds_float holds the current day's accumulated seconds (float for delta accumulation)
|
|
212
|
+
seconds_float = float(stored_today)
|
|
213
|
+
|
|
214
|
+
# Show initial stats (before starting)
|
|
215
|
+
stats_before = compute_stats_for_activity(activity)
|
|
216
|
+
if stats_before["n"] >= 2:
|
|
217
|
+
print(
|
|
218
|
+
f"[Before] [AVG] = {stats_before['avg']:.2f}h [Standard Deviation of Time] = {stats_before['sd']:.2f}h Status = {status_from_avg_sd(stats_before['avg'], stats_before['sd'])}"
|
|
219
|
+
)
|
|
220
|
+
else:
|
|
221
|
+
print("[Before] Not enough recordings to compute AVG/SD (need 2+ days).")
|
|
222
|
+
|
|
223
|
+
# input listener thread
|
|
224
|
+
def input_listener():
|
|
225
|
+
global running, paused
|
|
226
|
+
while running:
|
|
227
|
+
key = get_char()
|
|
228
|
+
if key.lower() == "p":
|
|
229
|
+
paused = not paused
|
|
230
|
+
elif key.lower() == "q":
|
|
231
|
+
running = False
|
|
232
|
+
break
|
|
233
|
+
|
|
234
|
+
input_thread = threading.Thread(target=input_listener, daemon=True)
|
|
235
|
+
input_thread.start()
|
|
236
|
+
|
|
237
|
+
last_ts = time.time()
|
|
238
|
+
current_day = today
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
cols = shutil.get_terminal_size((80, 20)).columns
|
|
242
|
+
except Exception:
|
|
243
|
+
cols = 80
|
|
244
|
+
|
|
245
|
+
while running:
|
|
246
|
+
now_ts = time.time()
|
|
247
|
+
prev_ts = last_ts
|
|
248
|
+
last_ts = now_ts
|
|
249
|
+
if not paused:
|
|
250
|
+
prev_date = current_day
|
|
251
|
+
now_date = datetime.date.fromtimestamp(now_ts)
|
|
252
|
+
if now_date == prev_date:
|
|
253
|
+
seconds_float += now_ts - prev_ts
|
|
254
|
+
else:
|
|
255
|
+
# crossed midnight. compute split: seconds before midnight go to prev_date, remainder to now_date
|
|
256
|
+
midnight_ts = datetime.datetime.combine(now_date, datetime.time.min).timestamp()
|
|
257
|
+
# seconds that belong to previous day
|
|
258
|
+
seconds_to_prev = max(0.0, midnight_ts - prev_ts)
|
|
259
|
+
seconds_float += seconds_to_prev
|
|
260
|
+
# persist previous day
|
|
261
|
+
update_day_seconds(activity, prev_date.isoformat(), int(round(seconds_float)))
|
|
262
|
+
# For new day, start from previously stored seconds for that new day (if any)
|
|
263
|
+
new_day_str = now_date.isoformat()
|
|
264
|
+
stored_new_day = get_day_seconds(activity, new_day_str)
|
|
265
|
+
# remaining seconds from this delta that belong to new day:
|
|
266
|
+
seconds_for_new_day = max(0.0, now_ts - midnight_ts)
|
|
267
|
+
seconds_float = float(stored_new_day) + seconds_for_new_day
|
|
268
|
+
current_day = now_date
|
|
269
|
+
|
|
270
|
+
# display
|
|
271
|
+
disp_secs = int(round(seconds_float))
|
|
272
|
+
hrs, rem = divmod(disp_secs, 3600)
|
|
273
|
+
mins, secs = divmod(rem, 60)
|
|
274
|
+
msg = f"Timer: {hrs:02d}:{mins:02d}:{secs:02d} [Press 'p' to Pause/Resume, 'q' to Quit]"
|
|
275
|
+
|
|
276
|
+
if len(msg) >= cols:
|
|
277
|
+
msg = msg[: cols - 1]
|
|
278
|
+
sys.stdout.write(f"\r\033[K{msg}")
|
|
279
|
+
sys.stdout.flush()
|
|
280
|
+
time.sleep(0.2)
|
|
281
|
+
|
|
282
|
+
# when stopped, persist current day's seconds
|
|
283
|
+
try:
|
|
284
|
+
update_day_seconds(activity, current_day.isoformat(), int(round(seconds_float)))
|
|
285
|
+
except Exception as e:
|
|
286
|
+
print("\nError saving data:", e)
|
|
287
|
+
|
|
288
|
+
# final stats after save
|
|
289
|
+
stats_after = compute_stats_for_activity(activity)
|
|
290
|
+
print("\n\nTimer stopped.")
|
|
291
|
+
if stats_after["n"] < 2:
|
|
292
|
+
print("Not enough recordings to compute AVG/SD (need 2+ days).")
|
|
293
|
+
else:
|
|
294
|
+
print(
|
|
295
|
+
f"[AVG] = {stats_after['avg']:.2f}h [Standard Deviation of Time] = {stats_after['sd']:.2f}h Status = {status_from_avg_sd(stats_after['avg'], stats_after['sd'])}"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# --- Countdown/time-limited run (backwards compatible) ---
|
|
300
|
+
def run_countdown(target_seconds, target_input):
|
|
301
|
+
global running, paused
|
|
302
|
+
running = True
|
|
303
|
+
paused = False
|
|
304
|
+
seconds = 0
|
|
305
|
+
|
|
306
|
+
def input_listener():
|
|
307
|
+
global running, paused
|
|
308
|
+
while running:
|
|
309
|
+
key = get_char()
|
|
310
|
+
if key.lower() == "p":
|
|
311
|
+
paused = not paused
|
|
312
|
+
elif key.lower() == "q":
|
|
313
|
+
running = False
|
|
314
|
+
break
|
|
315
|
+
|
|
316
|
+
input_thread = threading.Thread(target=input_listener, daemon=True)
|
|
317
|
+
input_thread.start()
|
|
318
|
+
|
|
319
|
+
try:
|
|
320
|
+
cols = shutil.get_terminal_size((80, 20)).columns
|
|
321
|
+
except Exception:
|
|
322
|
+
cols = 80
|
|
323
|
+
|
|
324
|
+
while running and seconds < target_seconds:
|
|
325
|
+
if not paused:
|
|
326
|
+
mins, secs = divmod(seconds, 60)
|
|
327
|
+
hours, mins = divmod(mins, 60)
|
|
328
|
+
msg = f"Timer: {hours:02d}:{mins:02d}:{secs:02d} [Press 'p' to Pause/Resume, 'q' to Quit]"
|
|
329
|
+
msg = msg[: cols - 1] if len(msg) >= cols else msg
|
|
330
|
+
sys.stdout.write(f"\r\033[K{msg}")
|
|
331
|
+
sys.stdout.flush()
|
|
332
|
+
seconds += 1
|
|
333
|
+
time.sleep(1)
|
|
334
|
+
if seconds >= target_seconds:
|
|
335
|
+
sys.stdout.write("\n\n⏰ Time's up! Target of {} reached.\n".format(target_input))
|
|
336
|
+
restore_terminal()
|
|
337
|
+
print("Timer stopped.")
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# --- CLI entrypoint ---
|
|
341
|
+
def main():
|
|
342
|
+
parser = argparse.ArgumentParser(description="tm_timer - terminal timer with study/train logging")
|
|
343
|
+
parser.add_argument("--study", action="store_true", help="Run study timer (no limit until 'q')")
|
|
344
|
+
parser.add_argument("--train", action="store_true", help="Run train timer (no limit until 'q')")
|
|
345
|
+
parser.add_argument("--studystatus", action="store_true", help="Show study daily table and SD")
|
|
346
|
+
parser.add_argument("--trainstatus", action="store_true", help="Show train daily table and SD")
|
|
347
|
+
parser.add_argument("--tables", action="store_true", help="Export tables.md to current directory")
|
|
348
|
+
parser.add_argument("duration", nargs="?", help="Optional duration e.g. 1h30m (if not using --study/--train)")
|
|
349
|
+
args = parser.parse_args()
|
|
350
|
+
|
|
351
|
+
# priority actions
|
|
352
|
+
if args.studystatus:
|
|
353
|
+
print_activity_table("study")
|
|
354
|
+
return
|
|
355
|
+
if args.trainstatus:
|
|
356
|
+
print_activity_table("train")
|
|
357
|
+
return
|
|
358
|
+
if args.tables:
|
|
359
|
+
out = write_tables_md("tables.md")
|
|
360
|
+
print(f"created {out}")
|
|
361
|
+
return
|
|
362
|
+
|
|
363
|
+
if args.study:
|
|
364
|
+
run_activity("study")
|
|
365
|
+
return
|
|
366
|
+
if args.train:
|
|
367
|
+
run_activity("train")
|
|
368
|
+
return
|
|
369
|
+
|
|
370
|
+
# fallback: duration countdown (compat)
|
|
371
|
+
if args.duration:
|
|
372
|
+
try:
|
|
373
|
+
target_seconds = parse_time_input(args.duration)
|
|
374
|
+
except ValueError as e:
|
|
375
|
+
print(e)
|
|
376
|
+
sys.exit(1)
|
|
377
|
+
run_countdown(target_seconds, args.duration)
|
|
378
|
+
else:
|
|
379
|
+
parser.print_help()
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
if __name__ == "__main__":
|
|
383
|
+
try:
|
|
384
|
+
main()
|
|
385
|
+
finally:
|
|
386
|
+
restore_terminal()
|
|
387
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tm-timer
|
|
3
|
+
Version: 2.0
|
|
4
|
+
Summary: Minimalistic terminal-based timer with Study/Train tracking
|
|
5
|
+
Author: ph4nt01
|
|
6
|
+
Author-email: Ph4nt01 <ph4nt0.84@email.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Requires-Python: >=3.6
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Dynamic: author
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
Dynamic: requires-python
|
|
14
|
+
|
|
15
|
+
# In The Name of God
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## ⏱ tm : Minimalistic Terminal-based timer with Study/Train tracking
|
|
20
|
+
|
|
21
|
+
### Features:
|
|
22
|
+
|
|
23
|
+
- Run from terminal:
|
|
24
|
+
- Countdown timer: `tm 10s`, `tm 25m`, `tm 1h30m`
|
|
25
|
+
- Study session: `tm --study` (tracks across sessions, resets daily)
|
|
26
|
+
- Train session: `tm --train` (tracks across sessions, resets daily)
|
|
27
|
+
- Daily study/train totals saved in `~/.tm_timer/data.json`
|
|
28
|
+
- Tables and statistics:
|
|
29
|
+
- `tm --studystatus` / `tm --trainstatus` → show daily table and Standard Deviation
|
|
30
|
+
- `tm --tables` → export `tables.md`
|
|
31
|
+
- Statistics:
|
|
32
|
+
- Average daily hours
|
|
33
|
+
- Standard Deviation of daily hours
|
|
34
|
+
- Status report (Very Good / Good / Not Good)
|
|
35
|
+
- Controls:
|
|
36
|
+
- Press `p` to pause/resume
|
|
37
|
+
- Press `q` to quit early
|
|
38
|
+
- Supports human durations like `2h45m30s`
|
|
39
|
+
- Simple, no graphical BLOAT.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
### Installation:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
sudo apt install pipx # if not installed
|
|
47
|
+
pipx install tm-timer # to install tm-timer
|
|
48
|
+
pipx ensurepath # to add tm to PATH
|
|
49
|
+
exec $SHELL # reload shell
|
|
50
|
+
|
|
51
|
+
#or run locally with using cli.py
|
|
52
|
+
python3 cli.py 3h40m
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Example Usage:
|
|
56
|
+
bash
|
|
57
|
+
Copy code
|
|
58
|
+
tm --study
|
|
59
|
+
# Timer starts, press 'q' to stop, progress saved daily
|
|
60
|
+
|
|
61
|
+
tm --studystatus
|
|
62
|
+
# Shows study table and stats
|
|
63
|
+
|
|
64
|
+
tm --tables
|
|
65
|
+
# Exports study/train tables as tables.md
|
|
66
|
+
⏱ tm preview:
|
|
67
|
+
|
tm_timer-1.0.1/PKG-INFO
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: tm-timer
|
|
3
|
-
Version: 1.0.1
|
|
4
|
-
Summary: Terminal timer with pause/resume and alert
|
|
5
|
-
Author: ph4nt01
|
|
6
|
-
Classifier: Programming Language :: Python :: 3
|
|
7
|
-
Classifier: Environment :: Console
|
|
8
|
-
Classifier: Operating System :: POSIX :: Linux
|
|
9
|
-
Requires-Python: >=3.6
|
|
10
|
-
Dynamic: author
|
|
11
|
-
Dynamic: classifier
|
|
12
|
-
Dynamic: requires-python
|
|
13
|
-
Dynamic: summary
|
tm_timer-1.0.1/README.md
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# In The Name of God
|
|
2
|
-
|
|
3
|
-
## tm ⏱ Terminal Timer
|
|
4
|
-
|
|
5
|
-
Minimalistic Terminal-based timer.
|
|
6
|
-
|
|
7
|
-
## Install
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
pip install git+https://github.com/Ph4nt01/timer.git
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
## Usage
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
tm 10s
|
|
17
|
-
tm 25m
|
|
18
|
-
tm 1h30m45s
|
|
19
|
-
|
|
20
|
-
p — Pause/Resume
|
|
21
|
-
q — Quit early
|
|
22
|
-
|
|
23
|
-
```
|
|
24
|
-
|
tm_timer-1.0.1/tm_timer/cli.py
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import time
|
|
2
|
-
import threading
|
|
3
|
-
import sys
|
|
4
|
-
import termios
|
|
5
|
-
import tty
|
|
6
|
-
import re
|
|
7
|
-
import os
|
|
8
|
-
import atexit
|
|
9
|
-
import shutil
|
|
10
|
-
import argparse
|
|
11
|
-
|
|
12
|
-
original_term_settings = termios.tcgetattr(sys.stdin.fileno())
|
|
13
|
-
|
|
14
|
-
def restore_terminal():
|
|
15
|
-
try:
|
|
16
|
-
termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, original_term_settings)
|
|
17
|
-
except:
|
|
18
|
-
pass
|
|
19
|
-
|
|
20
|
-
atexit.register(restore_terminal)
|
|
21
|
-
|
|
22
|
-
def with_raw_mode():
|
|
23
|
-
fd = sys.stdin.fileno()
|
|
24
|
-
old_settings = termios.tcgetattr(fd)
|
|
25
|
-
tty.setraw(fd)
|
|
26
|
-
return old_settings
|
|
27
|
-
|
|
28
|
-
def get_char():
|
|
29
|
-
fd = sys.stdin.fileno()
|
|
30
|
-
old_settings = with_raw_mode()
|
|
31
|
-
try:
|
|
32
|
-
ch = sys.stdin.read(1)
|
|
33
|
-
finally:
|
|
34
|
-
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
35
|
-
return ch
|
|
36
|
-
|
|
37
|
-
def parse_time_input(user_input):
|
|
38
|
-
pattern = r"(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?"
|
|
39
|
-
match = re.fullmatch(pattern, user_input.strip().lower())
|
|
40
|
-
if not match or not any(match.groups()):
|
|
41
|
-
raise ValueError("Invalid time format. Use format like '1h30m45s'.")
|
|
42
|
-
hours = int(match.group(1)) if match.group(1) else 0
|
|
43
|
-
minutes = int(match.group(2)) if match.group(2) else 0
|
|
44
|
-
seconds = int(match.group(3)) if match.group(3) else 0
|
|
45
|
-
return hours * 3600 + minutes * 60 + seconds
|
|
46
|
-
|
|
47
|
-
def timer():
|
|
48
|
-
global paused, running, seconds, target_seconds
|
|
49
|
-
while running:
|
|
50
|
-
if not paused:
|
|
51
|
-
cols = shutil.get_terminal_size((80, 20)).columns
|
|
52
|
-
mins, secs = divmod(seconds, 60)
|
|
53
|
-
hours, mins = divmod(mins, 60)
|
|
54
|
-
msg = f"Timer: {hours:02d}:{mins:02d}:{secs:02d} [Press 'p' to Pause/Resume, 'q' to Quit]"
|
|
55
|
-
msg = msg[:cols - 1] if len(msg) >= cols else msg
|
|
56
|
-
sys.stdout.write(f"\r\033[K{msg}")
|
|
57
|
-
sys.stdout.flush()
|
|
58
|
-
seconds += 1
|
|
59
|
-
if seconds >= target_seconds:
|
|
60
|
-
sys.stdout.write("\n\n⏰ Time's up! Target of {} reached.\n".format(target_input))
|
|
61
|
-
sys.stdout.flush()
|
|
62
|
-
running = False
|
|
63
|
-
break
|
|
64
|
-
time.sleep(1)
|
|
65
|
-
|
|
66
|
-
def input_listener():
|
|
67
|
-
global paused, running
|
|
68
|
-
while running:
|
|
69
|
-
key = get_char()
|
|
70
|
-
if key.lower() == 'p':
|
|
71
|
-
paused = not paused
|
|
72
|
-
elif key.lower() == 'q':
|
|
73
|
-
running = False
|
|
74
|
-
break
|
|
75
|
-
|
|
76
|
-
def entry():
|
|
77
|
-
global paused, running, seconds, target_seconds, target_input
|
|
78
|
-
|
|
79
|
-
parser = argparse.ArgumentParser(description="Terminal Timer with pause and alert.")
|
|
80
|
-
parser.add_argument("duration", help="Timer duration (e.g., 4h, 25m, 1h30m45s)")
|
|
81
|
-
args = parser.parse_args()
|
|
82
|
-
|
|
83
|
-
try:
|
|
84
|
-
target_input = args.duration
|
|
85
|
-
target_seconds = parse_time_input(target_input)
|
|
86
|
-
except ValueError as e:
|
|
87
|
-
print(e)
|
|
88
|
-
sys.exit(1)
|
|
89
|
-
|
|
90
|
-
paused = False
|
|
91
|
-
running = True
|
|
92
|
-
seconds = 0
|
|
93
|
-
|
|
94
|
-
input_thread = threading.Thread(target=input_listener, daemon=True)
|
|
95
|
-
timer_thread = threading.Thread(target=timer)
|
|
96
|
-
|
|
97
|
-
input_thread.start()
|
|
98
|
-
timer_thread.start()
|
|
99
|
-
|
|
100
|
-
timer_thread.join()
|
|
101
|
-
restore_terminal()
|
|
102
|
-
print("Timer stopped.")
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: tm-timer
|
|
3
|
-
Version: 1.0.1
|
|
4
|
-
Summary: Terminal timer with pause/resume and alert
|
|
5
|
-
Author: ph4nt01
|
|
6
|
-
Classifier: Programming Language :: Python :: 3
|
|
7
|
-
Classifier: Environment :: Console
|
|
8
|
-
Classifier: Operating System :: POSIX :: Linux
|
|
9
|
-
Requires-Python: >=3.6
|
|
10
|
-
Dynamic: author
|
|
11
|
-
Dynamic: classifier
|
|
12
|
-
Dynamic: requires-python
|
|
13
|
-
Dynamic: summary
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|