pttm 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.
pttm-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,96 @@
1
+ Metadata-Version: 2.4
2
+ Name: pttm
3
+ Version: 0.1.0
4
+ Summary: Pomodoro Terminal Timer & Manager
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: linkify-it-py==2.1.0
8
+ Requires-Dist: markdown-it-py==4.2.0
9
+ Requires-Dist: mdit-py-plugins==0.6.1
10
+ Requires-Dist: mdurl==0.1.2
11
+ Requires-Dist: platformdirs==4.10.0
12
+ Requires-Dist: Pygments==2.20.0
13
+ Requires-Dist: rich==15.0.0
14
+ Requires-Dist: textual==8.2.7
15
+ Requires-Dist: typing_extensions==4.15.0
16
+ Requires-Dist: uc-micro-py==2.0.0
17
+
18
+ # PTTM - Pomodoro Terminal Timer & Manager
19
+
20
+ PTTM is a terminal-based Pomodoro app built with [Textual](https://textual.textualize.io/). It provides a timer, task tracking, and configurable work/break settings in a compact TUI.
21
+
22
+ ## Features
23
+
24
+ - Pomodoro timer with focus, short break, and long break modes
25
+ - Task list with per-task Pomodoro counts
26
+ - Persistent configuration stored as JSON
27
+ - Keyboard shortcuts for common timer and task actions
28
+ - In-app settings tab for adjusting timing values
29
+
30
+ ## Requirements
31
+
32
+ - Python 3.10 or newer
33
+
34
+ ## Installation
35
+
36
+ Install the dependencies from the project root:
37
+
38
+ ```bash
39
+ pip install -r requirements.txt
40
+ ```
41
+
42
+ ## Run
43
+
44
+ From the repository root, start the app with:
45
+
46
+ ```bash
47
+ python pttm.py
48
+ ```
49
+
50
+ You can also run the app module directly:
51
+
52
+ ```bash
53
+ python -m pttm.app
54
+ ```
55
+
56
+ ## Keyboard Shortcuts
57
+
58
+ - `q` quit
59
+ - `s` start or pause the timer
60
+ - `r` reset the current timer
61
+ - `ctrl+r` reset the full session
62
+ - `k` skip to the next timer mode
63
+ - `f` switch to focus mode
64
+ - `g` switch to short break mode
65
+ - `b` switch to long break mode
66
+ - `t` focus the new task input
67
+ - `ctrl+p` show or hide the shortcuts screen
68
+
69
+ ## Configuration
70
+
71
+ The app reads and writes a JSON config file. By default, the file is stored in your user config directory. You can override the location by setting `PMO_CONFIG_PATH` before launch.
72
+
73
+ Example:
74
+
75
+ ```bash
76
+ export PMO_CONFIG_PATH=./pmo_config.json
77
+ python pttm.py
78
+ ```
79
+
80
+ The config includes timer settings, completed focus session count, and task data.
81
+
82
+ ## Tests
83
+
84
+ Run the test suite with:
85
+
86
+ ```bash
87
+ python -m unittest test_pttm.py
88
+ ```
89
+
90
+ ## Project Layout
91
+
92
+ - `pttm/app.py` application entry point
93
+ - `pttm/config.py` config load/save helpers
94
+ - `pttm/clock.py` ASCII clock rendering
95
+ - `pttm/widgets/` UI components
96
+ - `pttm/pttm.css` Textual stylesheet
pttm-0.1.0/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # PTTM - Pomodoro Terminal Timer & Manager
2
+
3
+ PTTM is a terminal-based Pomodoro app built with [Textual](https://textual.textualize.io/). It provides a timer, task tracking, and configurable work/break settings in a compact TUI.
4
+
5
+ ## Features
6
+
7
+ - Pomodoro timer with focus, short break, and long break modes
8
+ - Task list with per-task Pomodoro counts
9
+ - Persistent configuration stored as JSON
10
+ - Keyboard shortcuts for common timer and task actions
11
+ - In-app settings tab for adjusting timing values
12
+
13
+ ## Requirements
14
+
15
+ - Python 3.10 or newer
16
+
17
+ ## Installation
18
+
19
+ Install the dependencies from the project root:
20
+
21
+ ```bash
22
+ pip install -r requirements.txt
23
+ ```
24
+
25
+ ## Run
26
+
27
+ From the repository root, start the app with:
28
+
29
+ ```bash
30
+ python pttm.py
31
+ ```
32
+
33
+ You can also run the app module directly:
34
+
35
+ ```bash
36
+ python -m pttm.app
37
+ ```
38
+
39
+ ## Keyboard Shortcuts
40
+
41
+ - `q` quit
42
+ - `s` start or pause the timer
43
+ - `r` reset the current timer
44
+ - `ctrl+r` reset the full session
45
+ - `k` skip to the next timer mode
46
+ - `f` switch to focus mode
47
+ - `g` switch to short break mode
48
+ - `b` switch to long break mode
49
+ - `t` focus the new task input
50
+ - `ctrl+p` show or hide the shortcuts screen
51
+
52
+ ## Configuration
53
+
54
+ The app reads and writes a JSON config file. By default, the file is stored in your user config directory. You can override the location by setting `PMO_CONFIG_PATH` before launch.
55
+
56
+ Example:
57
+
58
+ ```bash
59
+ export PMO_CONFIG_PATH=./pmo_config.json
60
+ python pttm.py
61
+ ```
62
+
63
+ The config includes timer settings, completed focus session count, and task data.
64
+
65
+ ## Tests
66
+
67
+ Run the test suite with:
68
+
69
+ ```bash
70
+ python -m unittest test_pttm.py
71
+ ```
72
+
73
+ ## Project Layout
74
+
75
+ - `pttm/app.py` application entry point
76
+ - `pttm/config.py` config load/save helpers
77
+ - `pttm/clock.py` ASCII clock rendering
78
+ - `pttm/widgets/` UI components
79
+ - `pttm/pttm.css` Textual stylesheet
@@ -0,0 +1,3 @@
1
+ # Pomodoro TUI Package Public API
2
+ from pttm.config import load_config, save_config, CONFIG_FILE
3
+ from pttm.clock import make_clock_ascii
pttm-0.1.0/pttm/app.py ADDED
@@ -0,0 +1,183 @@
1
+ import json
2
+ import os
3
+ from textual.app import App, ComposeResult
4
+ from textual.widgets import Header, TabbedContent, TabPane, Label, Input
5
+ from pttm.config import load_config, save_config
6
+ from pttm.widgets.dashboard import Dashboard
7
+ from pttm.widgets.settings_tab import SettingsTab
8
+ from pttm.widgets.shortcuts_screen import ShortcutsScreen
9
+ from pttm.widgets.timer_widget import TimerWidget
10
+ from pttm.widgets.task_list_widget import TaskListWidget
11
+
12
+ class PomodoroApp(App):
13
+ CSS_PATH = "pttm.css"
14
+ TITLE = "TS PMO"
15
+ COMMANDS = set()
16
+ ENABLE_COMMAND_PALETTE = False
17
+
18
+ BINDINGS = [
19
+ ("q", "quit", "Quit"),
20
+ ("s", "toggle_timer", "Start/Pause"),
21
+ ("r", "reset_timer", "Reset"),
22
+ ("ctrl+r", "reset_session", "Reset Session"),
23
+ ("k", "skip_timer", "Skip"),
24
+ ("f", "set_mode_focus", "Focus Mode"),
25
+ ("g", "set_mode_short", "Short Break"),
26
+ ("b", "set_mode_long", "Long Break"),
27
+ ("t", "focus_todo_input", "Focus Todo"),
28
+ ("ctrl+p", "toggle_shortcuts", "Shortcuts"),
29
+ ]
30
+
31
+ def __init__(self, **kwargs):
32
+ super().__init__(**kwargs)
33
+ self.config = load_config()
34
+ # Log config to startup file next to config file
35
+ from pttm.config import CONFIG_FILE
36
+ log_dir = os.path.dirname(CONFIG_FILE)
37
+ if log_dir:
38
+ os.makedirs(log_dir, exist_ok=True)
39
+ log_file = os.path.join(log_dir, "pmo_startup.log")
40
+ else:
41
+ log_file = "pmo_startup.log"
42
+ try:
43
+ with open(log_file, "w") as f:
44
+ f.write(json.dumps(self.config, indent=4))
45
+ except Exception:
46
+ pass
47
+ self.active_task_id = None
48
+
49
+ def compose(self) -> ComposeResult:
50
+ # yield Header(show_clock=True)
51
+ with TabbedContent(initial="timer-tab"):
52
+ with TabPane("Timer", id="timer-tab"):
53
+ yield TimerWidget()
54
+ with TabPane("Tasks", id="tasks-tab"):
55
+ yield TaskListWidget()
56
+ with TabPane("Settings", id="settings-tab"):
57
+ yield SettingsTab()
58
+
59
+ def action_toggle_timer(self) -> None:
60
+ try:
61
+ timer_widget = self.query_one(TimerWidget)
62
+ timer_widget.is_running = not timer_widget.is_running #type: ignore
63
+ except Exception:
64
+ pass
65
+
66
+ def action_reset_timer(self) -> None:
67
+ try:
68
+ timer_widget = self.query_one(TimerWidget)
69
+ timer_widget.reset_timer_to_mode()
70
+ except Exception:
71
+ pass
72
+
73
+ def action_skip_timer(self) -> None:
74
+ try:
75
+ timer_widget = self.query_one(TimerWidget)
76
+ timer_widget.transition_to_next()
77
+ except Exception:
78
+ pass
79
+
80
+ def action_set_mode_focus(self) -> None:
81
+ try:
82
+ timer_widget = self.query_one(TimerWidget)
83
+ timer_widget.mode = "Focus"
84
+ timer_widget.reset_timer_to_mode()
85
+ except Exception:
86
+ pass
87
+
88
+ def action_set_mode_short(self) -> None:
89
+ try:
90
+ timer_widget = self.query_one(TimerWidget)
91
+ timer_widget.mode = "Short Break"
92
+ timer_widget.reset_timer_to_mode()
93
+ except Exception:
94
+ pass
95
+
96
+ def action_set_mode_long(self) -> None:
97
+ try:
98
+ timer_widget = self.query_one(TimerWidget)
99
+ timer_widget.mode = "Long Break"
100
+ timer_widget.reset_timer_to_mode()
101
+ except Exception:
102
+ pass
103
+
104
+ def action_focus_todo_input(self) -> None:
105
+ try:
106
+ input_box = self.query_one("#new-task-input", Input)
107
+ input_box.focus()
108
+ except Exception:
109
+ pass
110
+
111
+ def action_toggle_shortcuts(self) -> None:
112
+ if isinstance(self.screen, ShortcutsScreen):
113
+ self.pop_screen()
114
+ else:
115
+ self.push_screen(ShortcutsScreen())
116
+
117
+ def action_reset_session(self) -> None:
118
+ try:
119
+ timer_widget = self.query_one(TimerWidget)
120
+ timer_widget.is_running = False #type: ignore
121
+ timer_widget.completed_focus_sessions = 0
122
+ self.config["completed_focus_sessions"] = 0
123
+ save_config(self.config)
124
+
125
+ timer_widget.query_one("#session-count-display", Label).update("Completed: 0 sessions")
126
+ timer_widget.mode = "Focus"
127
+ timer_widget.reset_timer_to_mode()
128
+ self.notify("Pomodoro session reset completely.", title="Session Reset", severity="information")
129
+ except Exception:
130
+ pass
131
+
132
+ def update_active_task_display(self) -> None:
133
+ try:
134
+ timer_widget = self.query_one(TimerWidget)
135
+ active_label = timer_widget.query_one("#active-task-display", Label)
136
+
137
+ active_task = None
138
+ if self.active_task_id:
139
+ for task in self.config.get("tasks", []):
140
+ if task["id"] == self.active_task_id:
141
+ active_task = task
142
+ break
143
+
144
+ if active_task:
145
+ active_label.update(f"Active Task: [bold #f9e2af]{active_task['title']}[/bold #f9e2af]")
146
+ timer_widget.query_one("#timer-clock").add_class("timer-clock-active")
147
+ else:
148
+ active_label.update("No active task selected")
149
+ timer_widget.query_one("#timer-clock").remove_class("timer-clock-active")
150
+ except Exception:
151
+ pass
152
+
153
+ def increment_active_task_pomodoro(self) -> None:
154
+ if not self.active_task_id:
155
+ return
156
+
157
+ for task in self.config["tasks"]:
158
+ if task["id"] == self.active_task_id:
159
+ task["pomodoros"] += 1
160
+ self.notify(f"Pomodoro recorded for: {task['title']}", title="Task Updated", severity="success") #type: ignore
161
+ break
162
+ save_config(self.config)
163
+
164
+ try:
165
+ task_list_widget = self.query_one(TaskListWidget)
166
+ task_list_widget.refresh_tasks()
167
+ except Exception:
168
+ pass
169
+
170
+ def on_settings_updated(self) -> None:
171
+ try:
172
+ timer_widget = self.query_one(TimerWidget)
173
+ if not timer_widget.is_running:
174
+ timer_widget.reset_timer_to_mode()
175
+ except Exception:
176
+ pass
177
+
178
+ def main():
179
+ app = PomodoroApp()
180
+ app.run()
181
+
182
+ if __name__ == "__main__":
183
+ main()
@@ -0,0 +1,28 @@
1
+ # Big blocky font digits for the clock
2
+ DIGITS = {
3
+ '0': ["███", "█ █", "█ █", "█ █", "███"],
4
+ '1': [" █ ", "██ ", " █ ", " █ ", "███"],
5
+ '2': ["███", " █", "███", "█ ", "███"],
6
+ '3': ["███", " █", "███", " █", "███"],
7
+ '4': ["█ █", "█ █", "███", " █", " █"],
8
+ '5': ["███", "█ ", "███", " █", "███"],
9
+ '6': ["███", "█ ", "███", "█ █", "███"],
10
+ '7': ["███", " █", " █", " █", " █"],
11
+ '8': ["███", "█ █", "███", "█ █", "███"],
12
+ '9': ["███", "█ █", "███", " █", "███"],
13
+ ':': [" ", " █ ", " ", " █ ", " "],
14
+ ' ': [" ", " ", " ", " ", " "]
15
+ }
16
+
17
+ def make_clock_ascii(seconds: int) -> str:
18
+ mins = seconds // 60
19
+ secs = seconds % 60
20
+ time_str = f"{mins:02d}:{secs:02d}"
21
+
22
+ lines = ["", "", "", "", ""]
23
+ for char in time_str:
24
+ char_lines = DIGITS.get(char, DIGITS[' '])
25
+ for idx in range(5):
26
+ lines[idx] += char_lines[idx] + " "
27
+
28
+ return "\n".join(lines)
@@ -0,0 +1,53 @@
1
+ import json
2
+ import os
3
+ import platformdirs
4
+
5
+ CONFIG_PATH_ENV = os.getenv("PMO_CONFIG_PATH")
6
+ if CONFIG_PATH_ENV:
7
+ CONFIG_FILE = CONFIG_PATH_ENV
8
+ else:
9
+ CONFIG_DIR = platformdirs.user_config_dir("pttm", appauthor=False)
10
+ CONFIG_FILE = os.path.join(CONFIG_DIR, "pttm_config.json")
11
+
12
+ DEFAULT_CONFIG = {
13
+ "settings": {
14
+ "focus_time": 25,
15
+ "short_break_time": 5,
16
+ "long_break_time": 15,
17
+ "long_break_interval": 4
18
+ },
19
+ "completed_focus_sessions": 0,
20
+ "tasks": [
21
+ {"id": "1", "title": "Task 1", "completed": True, "pomodoros": 1},
22
+ {"id": "2", "title": "Task 2", "completed": False, "pomodoros": 0},
23
+ {"id": "3", "title": "Task 3", "completed": False, "pomodoros": 0}
24
+ ]
25
+ }
26
+
27
+ def load_config():
28
+ if os.path.exists(CONFIG_FILE):
29
+ try:
30
+ with open(CONFIG_FILE, "r") as f:
31
+ config = json.load(f)
32
+ # Ensure all sections exist
33
+ if "settings" not in config:
34
+ config["settings"] = DEFAULT_CONFIG["settings"]
35
+ if "tasks" not in config:
36
+ config["tasks"] = DEFAULT_CONFIG["tasks"]
37
+ if "completed_focus_sessions" not in config:
38
+ config["completed_focus_sessions"] = 0
39
+ return config
40
+ except Exception:
41
+ return DEFAULT_CONFIG.copy()
42
+ return DEFAULT_CONFIG.copy()
43
+
44
+ def save_config(config):
45
+ try:
46
+ dir_name = os.path.dirname(CONFIG_FILE)
47
+ if dir_name:
48
+ os.makedirs(dir_name, exist_ok=True)
49
+ with open(CONFIG_FILE, "w") as f:
50
+ json.dump(config, f, indent=4)
51
+ except Exception:
52
+ pass
53
+