tm-timer 1.0.1__tar.gz → 2.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.
- tm_timer-2.1/PKG-INFO +116 -0
- tm_timer-2.1/README.md +104 -0
- tm_timer-2.1/pyproject.toml +16 -0
- {tm_timer-1.0.1 → tm_timer-2.1}/setup.py +1 -1
- tm_timer-2.1/tm_timer/cli.py +387 -0
- tm_timer-2.1/tm_timer.egg-info/PKG-INFO +116 -0
- {tm_timer-1.0.1 → tm_timer-2.1}/tm_timer.egg-info/SOURCES.txt +1 -0
- tm_timer-2.1/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.1}/setup.cfg +0 -0
- {tm_timer-1.0.1 → tm_timer-2.1}/tm_timer/__init__.py +0 -0
- {tm_timer-1.0.1 → tm_timer-2.1}/tm_timer.egg-info/dependency_links.txt +0 -0
- {tm_timer-1.0.1 → tm_timer-2.1}/tm_timer.egg-info/top_level.txt +0 -0
tm_timer-2.1/PKG-INFO
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tm-timer
|
|
3
|
+
Version: 2.1
|
|
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
|
+
Dynamic: author
|
|
11
|
+
Dynamic: requires-python
|
|
12
|
+
|
|
13
|
+
# In The Name of God
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
# ⏱️ tm-timer
|
|
17
|
+
>Minimalistic Terminal timer with Study session tracking
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- Countdown timer:
|
|
22
|
+
- `tm 10s`, `tm 25m`, `tm 1h30m`
|
|
23
|
+
- Study/Train modes:
|
|
24
|
+
- `tm --study` → track study sessions (resets daily, accumulates across multiple runs)
|
|
25
|
+
- `tm --train` → track training sessions (same behavior, separate table)
|
|
26
|
+
- Daily logging:
|
|
27
|
+
- Records totals in `~/.tm_timer/data.json`
|
|
28
|
+
- Each day resets at midnight
|
|
29
|
+
- Statistics:
|
|
30
|
+
- Average daily hours
|
|
31
|
+
- Standard Deviation of Time
|
|
32
|
+
- Status report:
|
|
33
|
+
- **Very Good** (SD < AVG/2)
|
|
34
|
+
- **Good** (SD = AVG/2)
|
|
35
|
+
- **Not Good** (SD > AVG/2)
|
|
36
|
+
- Export:
|
|
37
|
+
- `tm --studystatus` → show study table + stats
|
|
38
|
+
- `tm --trainstatus` → show train table + stats
|
|
39
|
+
- `tm --tables` → export both tables to `tables.md`
|
|
40
|
+
- Controls:
|
|
41
|
+
- `p` → pause/resume
|
|
42
|
+
- `q` → quit and save
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
sudo apt install pipx # if not installed
|
|
50
|
+
pipx install tm-timer # install tm-timer
|
|
51
|
+
pipx ensurepath # add tm to PATH
|
|
52
|
+
exec $SHELL # reload shell
|
|
53
|
+
````
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Example Usage
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Start a study session
|
|
61
|
+
tm --study
|
|
62
|
+
|
|
63
|
+
# Check study stats
|
|
64
|
+
tm --studystatus
|
|
65
|
+
|
|
66
|
+
# Start a train session
|
|
67
|
+
tm --train
|
|
68
|
+
|
|
69
|
+
# Export tables to tables.md
|
|
70
|
+
tm --tables
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Example Tables Output
|
|
76
|
+
|
|
77
|
+
```markdown
|
|
78
|
+
# tm_timer Tables
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## STUDY Table
|
|
83
|
+
|
|
84
|
+
| Date | Day | Time |
|
|
85
|
+
| ---------- | --- | ------ |
|
|
86
|
+
| 2025-10-03 | FRI | 05:00:00 |
|
|
87
|
+
| 2025-10-02 | THU | 02:50:00 |
|
|
88
|
+
| 2025-09-29 | MON | 04:05:00 |
|
|
89
|
+
| 2025-09-27 | SAT | 03:36:00 |
|
|
90
|
+
|
|
91
|
+
**[AVG] = 3.98h [Standard Deviation of Time] = 0.75h Status = Very Good**
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## TRAIN Table
|
|
96
|
+
|
|
97
|
+
| Date | Day | Time |
|
|
98
|
+
| ---------- | --- | ------ |
|
|
99
|
+
| 2025-10-03 | FRI | 00:57:00 |
|
|
100
|
+
| 2025-10-02 | THU | 00:50:00 |
|
|
101
|
+
| 2025-09-30 | SUN | 00:35:00 |
|
|
102
|
+
| 2025-09-28 | SAT | 01:36:00 |
|
|
103
|
+
|
|
104
|
+
**[AVG] = 1.05h [Standard Deviation of Time] = 0.38h Status = Very Good**
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Preview
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+

|
|
113
|
+
|
|
114
|
+

|
|
115
|
+
|
|
116
|
+

|
tm_timer-2.1/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# In The Name of God
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
# ⏱️ tm-timer
|
|
5
|
+
>Minimalistic Terminal timer with Study session tracking
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Countdown timer:
|
|
10
|
+
- `tm 10s`, `tm 25m`, `tm 1h30m`
|
|
11
|
+
- Study/Train modes:
|
|
12
|
+
- `tm --study` → track study sessions (resets daily, accumulates across multiple runs)
|
|
13
|
+
- `tm --train` → track training sessions (same behavior, separate table)
|
|
14
|
+
- Daily logging:
|
|
15
|
+
- Records totals in `~/.tm_timer/data.json`
|
|
16
|
+
- Each day resets at midnight
|
|
17
|
+
- Statistics:
|
|
18
|
+
- Average daily hours
|
|
19
|
+
- Standard Deviation of Time
|
|
20
|
+
- Status report:
|
|
21
|
+
- **Very Good** (SD < AVG/2)
|
|
22
|
+
- **Good** (SD = AVG/2)
|
|
23
|
+
- **Not Good** (SD > AVG/2)
|
|
24
|
+
- Export:
|
|
25
|
+
- `tm --studystatus` → show study table + stats
|
|
26
|
+
- `tm --trainstatus` → show train table + stats
|
|
27
|
+
- `tm --tables` → export both tables to `tables.md`
|
|
28
|
+
- Controls:
|
|
29
|
+
- `p` → pause/resume
|
|
30
|
+
- `q` → quit and save
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
sudo apt install pipx # if not installed
|
|
38
|
+
pipx install tm-timer # install tm-timer
|
|
39
|
+
pipx ensurepath # add tm to PATH
|
|
40
|
+
exec $SHELL # reload shell
|
|
41
|
+
````
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Example Usage
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Start a study session
|
|
49
|
+
tm --study
|
|
50
|
+
|
|
51
|
+
# Check study stats
|
|
52
|
+
tm --studystatus
|
|
53
|
+
|
|
54
|
+
# Start a train session
|
|
55
|
+
tm --train
|
|
56
|
+
|
|
57
|
+
# Export tables to tables.md
|
|
58
|
+
tm --tables
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Example Tables Output
|
|
64
|
+
|
|
65
|
+
```markdown
|
|
66
|
+
# tm_timer Tables
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## STUDY Table
|
|
71
|
+
|
|
72
|
+
| Date | Day | Time |
|
|
73
|
+
| ---------- | --- | ------ |
|
|
74
|
+
| 2025-10-03 | FRI | 05:00:00 |
|
|
75
|
+
| 2025-10-02 | THU | 02:50:00 |
|
|
76
|
+
| 2025-09-29 | MON | 04:05:00 |
|
|
77
|
+
| 2025-09-27 | SAT | 03:36:00 |
|
|
78
|
+
|
|
79
|
+
**[AVG] = 3.98h [Standard Deviation of Time] = 0.75h Status = Very Good**
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## TRAIN Table
|
|
84
|
+
|
|
85
|
+
| Date | Day | Time |
|
|
86
|
+
| ---------- | --- | ------ |
|
|
87
|
+
| 2025-10-03 | FRI | 00:57:00 |
|
|
88
|
+
| 2025-10-02 | THU | 00:50:00 |
|
|
89
|
+
| 2025-09-30 | SUN | 00:35:00 |
|
|
90
|
+
| 2025-09-28 | SAT | 01:36:00 |
|
|
91
|
+
|
|
92
|
+
**[AVG] = 1.05h [Standard Deviation of Time] = 0.38h Status = Very Good**
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Preview
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+

|
|
101
|
+
|
|
102
|
+

|
|
103
|
+
|
|
104
|
+

|
|
@@ -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.1"
|
|
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"[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("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,116 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tm-timer
|
|
3
|
+
Version: 2.1
|
|
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
|
+
Dynamic: author
|
|
11
|
+
Dynamic: requires-python
|
|
12
|
+
|
|
13
|
+
# In The Name of God
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
# ⏱️ tm-timer
|
|
17
|
+
>Minimalistic Terminal timer with Study session tracking
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- Countdown timer:
|
|
22
|
+
- `tm 10s`, `tm 25m`, `tm 1h30m`
|
|
23
|
+
- Study/Train modes:
|
|
24
|
+
- `tm --study` → track study sessions (resets daily, accumulates across multiple runs)
|
|
25
|
+
- `tm --train` → track training sessions (same behavior, separate table)
|
|
26
|
+
- Daily logging:
|
|
27
|
+
- Records totals in `~/.tm_timer/data.json`
|
|
28
|
+
- Each day resets at midnight
|
|
29
|
+
- Statistics:
|
|
30
|
+
- Average daily hours
|
|
31
|
+
- Standard Deviation of Time
|
|
32
|
+
- Status report:
|
|
33
|
+
- **Very Good** (SD < AVG/2)
|
|
34
|
+
- **Good** (SD = AVG/2)
|
|
35
|
+
- **Not Good** (SD > AVG/2)
|
|
36
|
+
- Export:
|
|
37
|
+
- `tm --studystatus` → show study table + stats
|
|
38
|
+
- `tm --trainstatus` → show train table + stats
|
|
39
|
+
- `tm --tables` → export both tables to `tables.md`
|
|
40
|
+
- Controls:
|
|
41
|
+
- `p` → pause/resume
|
|
42
|
+
- `q` → quit and save
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
sudo apt install pipx # if not installed
|
|
50
|
+
pipx install tm-timer # install tm-timer
|
|
51
|
+
pipx ensurepath # add tm to PATH
|
|
52
|
+
exec $SHELL # reload shell
|
|
53
|
+
````
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Example Usage
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Start a study session
|
|
61
|
+
tm --study
|
|
62
|
+
|
|
63
|
+
# Check study stats
|
|
64
|
+
tm --studystatus
|
|
65
|
+
|
|
66
|
+
# Start a train session
|
|
67
|
+
tm --train
|
|
68
|
+
|
|
69
|
+
# Export tables to tables.md
|
|
70
|
+
tm --tables
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Example Tables Output
|
|
76
|
+
|
|
77
|
+
```markdown
|
|
78
|
+
# tm_timer Tables
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## STUDY Table
|
|
83
|
+
|
|
84
|
+
| Date | Day | Time |
|
|
85
|
+
| ---------- | --- | ------ |
|
|
86
|
+
| 2025-10-03 | FRI | 05:00:00 |
|
|
87
|
+
| 2025-10-02 | THU | 02:50:00 |
|
|
88
|
+
| 2025-09-29 | MON | 04:05:00 |
|
|
89
|
+
| 2025-09-27 | SAT | 03:36:00 |
|
|
90
|
+
|
|
91
|
+
**[AVG] = 3.98h [Standard Deviation of Time] = 0.75h Status = Very Good**
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## TRAIN Table
|
|
96
|
+
|
|
97
|
+
| Date | Day | Time |
|
|
98
|
+
| ---------- | --- | ------ |
|
|
99
|
+
| 2025-10-03 | FRI | 00:57:00 |
|
|
100
|
+
| 2025-10-02 | THU | 00:50:00 |
|
|
101
|
+
| 2025-09-30 | SUN | 00:35:00 |
|
|
102
|
+
| 2025-09-28 | SAT | 01:36:00 |
|
|
103
|
+
|
|
104
|
+
**[AVG] = 1.05h [Standard Deviation of Time] = 0.38h Status = Very Good**
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Preview
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+

|
|
113
|
+
|
|
114
|
+

|
|
115
|
+
|
|
116
|
+

|
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
|