lolTasks 1.0.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.
- loltasks-1.0.0/LICENSE +21 -0
- loltasks-1.0.0/MANIFEST.in +3 -0
- loltasks-1.0.0/PKG-INFO +161 -0
- loltasks-1.0.0/README.md +123 -0
- loltasks-1.0.0/lolTasks.egg-info/PKG-INFO +161 -0
- loltasks-1.0.0/lolTasks.egg-info/SOURCES.txt +11 -0
- loltasks-1.0.0/lolTasks.egg-info/dependency_links.txt +1 -0
- loltasks-1.0.0/lolTasks.egg-info/entry_points.txt +3 -0
- loltasks-1.0.0/lolTasks.egg-info/top_level.txt +1 -0
- loltasks-1.0.0/setup.cfg +4 -0
- loltasks-1.0.0/setup.py +52 -0
- loltasks-1.0.0/task.py +575 -0
- loltasks-1.0.0/task_spec.md +247 -0
loltasks-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Luke J. Stephens
|
|
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 all
|
|
13
|
+
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 FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
loltasks-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lolTasks
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A terminal-based weekly task management application with advanced text editing
|
|
5
|
+
Home-page: https://github.com/lstephensFederation/lolTasks
|
|
6
|
+
Author: Luke J. Stephens
|
|
7
|
+
Author-email: l.stephens@federation.edu.au
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Bug Reports, https://github.com/lstephensFederation/lolTasks/issues
|
|
10
|
+
Project-URL: Source, https://github.com/lstephensFederation/lolTasks
|
|
11
|
+
Keywords: task management terminal curses weekly planner
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Topic :: Utilities
|
|
23
|
+
Requires-Python: >=3.6
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Dynamic: author
|
|
27
|
+
Dynamic: author-email
|
|
28
|
+
Dynamic: classifier
|
|
29
|
+
Dynamic: description
|
|
30
|
+
Dynamic: description-content-type
|
|
31
|
+
Dynamic: home-page
|
|
32
|
+
Dynamic: keywords
|
|
33
|
+
Dynamic: license
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
Dynamic: project-url
|
|
36
|
+
Dynamic: requires-python
|
|
37
|
+
Dynamic: summary
|
|
38
|
+
|
|
39
|
+
# lolTasks
|
|
40
|
+
|
|
41
|
+
A powerful terminal-based weekly task management application built with Python and curses.
|
|
42
|
+
|
|
43
|
+
## Features
|
|
44
|
+
|
|
45
|
+
- **Weekly Task Management**: Organize tasks by week with a clean, intuitive interface
|
|
46
|
+
- **Advanced Text Editing**: Full-featured line editor with undo/redo, word navigation, and standard shortcuts
|
|
47
|
+
- **Cross-Platform**: Works on macOS, Linux, and Windows (with appropriate terminal)
|
|
48
|
+
- **Persistent Storage**: JSON-based data storage in your home directory
|
|
49
|
+
- **Vim-Style Shortcuts**: Familiar key bindings for power users
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
### From PyPI (Recommended)
|
|
54
|
+
```bash
|
|
55
|
+
pip install lolTasks
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### From Source
|
|
59
|
+
```bash
|
|
60
|
+
git clone https://github.com/lstephensFederation/lolTasks.git
|
|
61
|
+
cd lolTasks
|
|
62
|
+
pip install .
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
### Basic Usage
|
|
68
|
+
```bash
|
|
69
|
+
lolTasks
|
|
70
|
+
# or
|
|
71
|
+
task
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Command Line Options
|
|
75
|
+
```bash
|
|
76
|
+
lolTasks --help # Show help and key bindings
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Key Bindings
|
|
80
|
+
|
|
81
|
+
### Navigation
|
|
82
|
+
- `↑/↓` or `k/j`: Move selection up/down
|
|
83
|
+
- `←/→` or `h/l`: Navigate to previous/next week
|
|
84
|
+
- `Tab`: Cycle task state forward (TO-DO → PENDING → COMPLETED)
|
|
85
|
+
- `Shift+Tab`: Cycle task state backward
|
|
86
|
+
|
|
87
|
+
### Task Management
|
|
88
|
+
- `a`: Add new task after selected item
|
|
89
|
+
- `Enter`: Edit selected task (or week title if none selected)
|
|
90
|
+
- `I`: Edit task at beginning of line (vim-style)
|
|
91
|
+
- `d`: Delete selected task
|
|
92
|
+
- `r`: Toggle reorder mode for moving tasks up/down
|
|
93
|
+
- `Esc`: Exit edit mode
|
|
94
|
+
|
|
95
|
+
### Global Actions
|
|
96
|
+
- `Ctrl + U`: Undo last action
|
|
97
|
+
- `Ctrl + R`: Redo last undone action
|
|
98
|
+
- `n`: Move task to next week
|
|
99
|
+
- `p`: Move task to previous week
|
|
100
|
+
- `q`: Quit application
|
|
101
|
+
|
|
102
|
+
### Text Editing (when in edit mode)
|
|
103
|
+
- `Esc + U`: Undo last change (vim-style)
|
|
104
|
+
- `Esc + R`: Redo last undone change
|
|
105
|
+
- `Option + Left` (macOS): Skip to previous word
|
|
106
|
+
- `Option + Right` (macOS): Skip to next word
|
|
107
|
+
- `Ctrl + A`: Move to start of line
|
|
108
|
+
- `Ctrl + E`: Move to end of line
|
|
109
|
+
- `Left/Right`: Move cursor
|
|
110
|
+
- `Home/End`: Move to start/end of line
|
|
111
|
+
- `Backspace/Delete`: Delete characters
|
|
112
|
+
- `Enter/Esc`: Save and exit edit mode
|
|
113
|
+
|
|
114
|
+
## Data Storage
|
|
115
|
+
|
|
116
|
+
Tasks are stored in `~/.lolTasks/weekly_tasks.json`. The application automatically creates this directory and file on first run.
|
|
117
|
+
|
|
118
|
+
## Requirements
|
|
119
|
+
|
|
120
|
+
- Python 3.6+
|
|
121
|
+
- A terminal that supports curses (most modern terminals)
|
|
122
|
+
|
|
123
|
+
## Development
|
|
124
|
+
|
|
125
|
+
### Setup Development Environment
|
|
126
|
+
```bash
|
|
127
|
+
git clone https://github.com/lstephensFederation/lolTasks.git
|
|
128
|
+
cd lolTasks
|
|
129
|
+
pip install -e .
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Running Tests
|
|
133
|
+
```bash
|
|
134
|
+
# No tests implemented yet
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Contributing
|
|
138
|
+
|
|
139
|
+
1. Fork the repository
|
|
140
|
+
2. Create a feature branch
|
|
141
|
+
3. Make your changes
|
|
142
|
+
4. Test thoroughly
|
|
143
|
+
5. Submit a pull request
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
MIT License - see LICENSE file for details.
|
|
148
|
+
|
|
149
|
+
## Changelog
|
|
150
|
+
|
|
151
|
+
### Version 1.0.0
|
|
152
|
+
- Initial release
|
|
153
|
+
- Complete task management functionality
|
|
154
|
+
- Advanced text editing features
|
|
155
|
+
- Cross-platform compatibility
|
|
156
|
+
- Comprehensive documentation
|
|
157
|
+
|
|
158
|
+
## Support
|
|
159
|
+
|
|
160
|
+
For issues, questions, or contributions, please visit:
|
|
161
|
+
https://github.com/lstephensFederation/lolTasks
|
loltasks-1.0.0/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# lolTasks
|
|
2
|
+
|
|
3
|
+
A powerful terminal-based weekly task management application built with Python and curses.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Weekly Task Management**: Organize tasks by week with a clean, intuitive interface
|
|
8
|
+
- **Advanced Text Editing**: Full-featured line editor with undo/redo, word navigation, and standard shortcuts
|
|
9
|
+
- **Cross-Platform**: Works on macOS, Linux, and Windows (with appropriate terminal)
|
|
10
|
+
- **Persistent Storage**: JSON-based data storage in your home directory
|
|
11
|
+
- **Vim-Style Shortcuts**: Familiar key bindings for power users
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
### From PyPI (Recommended)
|
|
16
|
+
```bash
|
|
17
|
+
pip install lolTasks
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### From Source
|
|
21
|
+
```bash
|
|
22
|
+
git clone https://github.com/lstephensFederation/lolTasks.git
|
|
23
|
+
cd lolTasks
|
|
24
|
+
pip install .
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
### Basic Usage
|
|
30
|
+
```bash
|
|
31
|
+
lolTasks
|
|
32
|
+
# or
|
|
33
|
+
task
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Command Line Options
|
|
37
|
+
```bash
|
|
38
|
+
lolTasks --help # Show help and key bindings
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Key Bindings
|
|
42
|
+
|
|
43
|
+
### Navigation
|
|
44
|
+
- `↑/↓` or `k/j`: Move selection up/down
|
|
45
|
+
- `←/→` or `h/l`: Navigate to previous/next week
|
|
46
|
+
- `Tab`: Cycle task state forward (TO-DO → PENDING → COMPLETED)
|
|
47
|
+
- `Shift+Tab`: Cycle task state backward
|
|
48
|
+
|
|
49
|
+
### Task Management
|
|
50
|
+
- `a`: Add new task after selected item
|
|
51
|
+
- `Enter`: Edit selected task (or week title if none selected)
|
|
52
|
+
- `I`: Edit task at beginning of line (vim-style)
|
|
53
|
+
- `d`: Delete selected task
|
|
54
|
+
- `r`: Toggle reorder mode for moving tasks up/down
|
|
55
|
+
- `Esc`: Exit edit mode
|
|
56
|
+
|
|
57
|
+
### Global Actions
|
|
58
|
+
- `Ctrl + U`: Undo last action
|
|
59
|
+
- `Ctrl + R`: Redo last undone action
|
|
60
|
+
- `n`: Move task to next week
|
|
61
|
+
- `p`: Move task to previous week
|
|
62
|
+
- `q`: Quit application
|
|
63
|
+
|
|
64
|
+
### Text Editing (when in edit mode)
|
|
65
|
+
- `Esc + U`: Undo last change (vim-style)
|
|
66
|
+
- `Esc + R`: Redo last undone change
|
|
67
|
+
- `Option + Left` (macOS): Skip to previous word
|
|
68
|
+
- `Option + Right` (macOS): Skip to next word
|
|
69
|
+
- `Ctrl + A`: Move to start of line
|
|
70
|
+
- `Ctrl + E`: Move to end of line
|
|
71
|
+
- `Left/Right`: Move cursor
|
|
72
|
+
- `Home/End`: Move to start/end of line
|
|
73
|
+
- `Backspace/Delete`: Delete characters
|
|
74
|
+
- `Enter/Esc`: Save and exit edit mode
|
|
75
|
+
|
|
76
|
+
## Data Storage
|
|
77
|
+
|
|
78
|
+
Tasks are stored in `~/.lolTasks/weekly_tasks.json`. The application automatically creates this directory and file on first run.
|
|
79
|
+
|
|
80
|
+
## Requirements
|
|
81
|
+
|
|
82
|
+
- Python 3.6+
|
|
83
|
+
- A terminal that supports curses (most modern terminals)
|
|
84
|
+
|
|
85
|
+
## Development
|
|
86
|
+
|
|
87
|
+
### Setup Development Environment
|
|
88
|
+
```bash
|
|
89
|
+
git clone https://github.com/lstephensFederation/lolTasks.git
|
|
90
|
+
cd lolTasks
|
|
91
|
+
pip install -e .
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Running Tests
|
|
95
|
+
```bash
|
|
96
|
+
# No tests implemented yet
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Contributing
|
|
100
|
+
|
|
101
|
+
1. Fork the repository
|
|
102
|
+
2. Create a feature branch
|
|
103
|
+
3. Make your changes
|
|
104
|
+
4. Test thoroughly
|
|
105
|
+
5. Submit a pull request
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT License - see LICENSE file for details.
|
|
110
|
+
|
|
111
|
+
## Changelog
|
|
112
|
+
|
|
113
|
+
### Version 1.0.0
|
|
114
|
+
- Initial release
|
|
115
|
+
- Complete task management functionality
|
|
116
|
+
- Advanced text editing features
|
|
117
|
+
- Cross-platform compatibility
|
|
118
|
+
- Comprehensive documentation
|
|
119
|
+
|
|
120
|
+
## Support
|
|
121
|
+
|
|
122
|
+
For issues, questions, or contributions, please visit:
|
|
123
|
+
https://github.com/lstephensFederation/lolTasks
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lolTasks
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A terminal-based weekly task management application with advanced text editing
|
|
5
|
+
Home-page: https://github.com/lstephensFederation/lolTasks
|
|
6
|
+
Author: Luke J. Stephens
|
|
7
|
+
Author-email: l.stephens@federation.edu.au
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Bug Reports, https://github.com/lstephensFederation/lolTasks/issues
|
|
10
|
+
Project-URL: Source, https://github.com/lstephensFederation/lolTasks
|
|
11
|
+
Keywords: task management terminal curses weekly planner
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Topic :: Utilities
|
|
23
|
+
Requires-Python: >=3.6
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Dynamic: author
|
|
27
|
+
Dynamic: author-email
|
|
28
|
+
Dynamic: classifier
|
|
29
|
+
Dynamic: description
|
|
30
|
+
Dynamic: description-content-type
|
|
31
|
+
Dynamic: home-page
|
|
32
|
+
Dynamic: keywords
|
|
33
|
+
Dynamic: license
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
Dynamic: project-url
|
|
36
|
+
Dynamic: requires-python
|
|
37
|
+
Dynamic: summary
|
|
38
|
+
|
|
39
|
+
# lolTasks
|
|
40
|
+
|
|
41
|
+
A powerful terminal-based weekly task management application built with Python and curses.
|
|
42
|
+
|
|
43
|
+
## Features
|
|
44
|
+
|
|
45
|
+
- **Weekly Task Management**: Organize tasks by week with a clean, intuitive interface
|
|
46
|
+
- **Advanced Text Editing**: Full-featured line editor with undo/redo, word navigation, and standard shortcuts
|
|
47
|
+
- **Cross-Platform**: Works on macOS, Linux, and Windows (with appropriate terminal)
|
|
48
|
+
- **Persistent Storage**: JSON-based data storage in your home directory
|
|
49
|
+
- **Vim-Style Shortcuts**: Familiar key bindings for power users
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
### From PyPI (Recommended)
|
|
54
|
+
```bash
|
|
55
|
+
pip install lolTasks
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### From Source
|
|
59
|
+
```bash
|
|
60
|
+
git clone https://github.com/lstephensFederation/lolTasks.git
|
|
61
|
+
cd lolTasks
|
|
62
|
+
pip install .
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
### Basic Usage
|
|
68
|
+
```bash
|
|
69
|
+
lolTasks
|
|
70
|
+
# or
|
|
71
|
+
task
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Command Line Options
|
|
75
|
+
```bash
|
|
76
|
+
lolTasks --help # Show help and key bindings
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Key Bindings
|
|
80
|
+
|
|
81
|
+
### Navigation
|
|
82
|
+
- `↑/↓` or `k/j`: Move selection up/down
|
|
83
|
+
- `←/→` or `h/l`: Navigate to previous/next week
|
|
84
|
+
- `Tab`: Cycle task state forward (TO-DO → PENDING → COMPLETED)
|
|
85
|
+
- `Shift+Tab`: Cycle task state backward
|
|
86
|
+
|
|
87
|
+
### Task Management
|
|
88
|
+
- `a`: Add new task after selected item
|
|
89
|
+
- `Enter`: Edit selected task (or week title if none selected)
|
|
90
|
+
- `I`: Edit task at beginning of line (vim-style)
|
|
91
|
+
- `d`: Delete selected task
|
|
92
|
+
- `r`: Toggle reorder mode for moving tasks up/down
|
|
93
|
+
- `Esc`: Exit edit mode
|
|
94
|
+
|
|
95
|
+
### Global Actions
|
|
96
|
+
- `Ctrl + U`: Undo last action
|
|
97
|
+
- `Ctrl + R`: Redo last undone action
|
|
98
|
+
- `n`: Move task to next week
|
|
99
|
+
- `p`: Move task to previous week
|
|
100
|
+
- `q`: Quit application
|
|
101
|
+
|
|
102
|
+
### Text Editing (when in edit mode)
|
|
103
|
+
- `Esc + U`: Undo last change (vim-style)
|
|
104
|
+
- `Esc + R`: Redo last undone change
|
|
105
|
+
- `Option + Left` (macOS): Skip to previous word
|
|
106
|
+
- `Option + Right` (macOS): Skip to next word
|
|
107
|
+
- `Ctrl + A`: Move to start of line
|
|
108
|
+
- `Ctrl + E`: Move to end of line
|
|
109
|
+
- `Left/Right`: Move cursor
|
|
110
|
+
- `Home/End`: Move to start/end of line
|
|
111
|
+
- `Backspace/Delete`: Delete characters
|
|
112
|
+
- `Enter/Esc`: Save and exit edit mode
|
|
113
|
+
|
|
114
|
+
## Data Storage
|
|
115
|
+
|
|
116
|
+
Tasks are stored in `~/.lolTasks/weekly_tasks.json`. The application automatically creates this directory and file on first run.
|
|
117
|
+
|
|
118
|
+
## Requirements
|
|
119
|
+
|
|
120
|
+
- Python 3.6+
|
|
121
|
+
- A terminal that supports curses (most modern terminals)
|
|
122
|
+
|
|
123
|
+
## Development
|
|
124
|
+
|
|
125
|
+
### Setup Development Environment
|
|
126
|
+
```bash
|
|
127
|
+
git clone https://github.com/lstephensFederation/lolTasks.git
|
|
128
|
+
cd lolTasks
|
|
129
|
+
pip install -e .
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Running Tests
|
|
133
|
+
```bash
|
|
134
|
+
# No tests implemented yet
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Contributing
|
|
138
|
+
|
|
139
|
+
1. Fork the repository
|
|
140
|
+
2. Create a feature branch
|
|
141
|
+
3. Make your changes
|
|
142
|
+
4. Test thoroughly
|
|
143
|
+
5. Submit a pull request
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
MIT License - see LICENSE file for details.
|
|
148
|
+
|
|
149
|
+
## Changelog
|
|
150
|
+
|
|
151
|
+
### Version 1.0.0
|
|
152
|
+
- Initial release
|
|
153
|
+
- Complete task management functionality
|
|
154
|
+
- Advanced text editing features
|
|
155
|
+
- Cross-platform compatibility
|
|
156
|
+
- Comprehensive documentation
|
|
157
|
+
|
|
158
|
+
## Support
|
|
159
|
+
|
|
160
|
+
For issues, questions, or contributions, please visit:
|
|
161
|
+
https://github.com/lstephensFederation/lolTasks
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
task
|
loltasks-1.0.0/setup.cfg
ADDED
loltasks-1.0.0/setup.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
lolTasks - A terminal-based weekly task management application
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from setuptools import setup, find_packages
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
# Read the README file
|
|
10
|
+
this_directory = os.path.abspath(os.path.dirname(__file__))
|
|
11
|
+
with open(os.path.join(this_directory, 'README.md'), encoding='utf-8') as f:
|
|
12
|
+
long_description = f.read()
|
|
13
|
+
|
|
14
|
+
setup(
|
|
15
|
+
name="lolTasks",
|
|
16
|
+
version="1.0.0",
|
|
17
|
+
author="Luke J. Stephens",
|
|
18
|
+
author_email="l.stephens@federation.edu.au",
|
|
19
|
+
description="A terminal-based weekly task management application with advanced text editing",
|
|
20
|
+
long_description=long_description,
|
|
21
|
+
long_description_content_type="text/markdown",
|
|
22
|
+
url="https://github.com/lstephensFederation/lolTasks",
|
|
23
|
+
packages=find_packages(),
|
|
24
|
+
py_modules=['task'],
|
|
25
|
+
include_package_data=True,
|
|
26
|
+
license="MIT",
|
|
27
|
+
classifiers=[
|
|
28
|
+
"Development Status :: 4 - Beta",
|
|
29
|
+
"Intended Audience :: End Users/Desktop",
|
|
30
|
+
"Operating System :: OS Independent",
|
|
31
|
+
"Programming Language :: Python :: 3",
|
|
32
|
+
"Programming Language :: Python :: 3.6",
|
|
33
|
+
"Programming Language :: Python :: 3.7",
|
|
34
|
+
"Programming Language :: Python :: 3.8",
|
|
35
|
+
"Programming Language :: Python :: 3.9",
|
|
36
|
+
"Programming Language :: Python :: 3.10",
|
|
37
|
+
"Programming Language :: Python :: 3.11",
|
|
38
|
+
"Topic :: Utilities",
|
|
39
|
+
],
|
|
40
|
+
python_requires=">=3.6",
|
|
41
|
+
entry_points={
|
|
42
|
+
'console_scripts': [
|
|
43
|
+
'lolTasks=task:entry_point',
|
|
44
|
+
'task=task:entry_point',
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
keywords="task management terminal curses weekly planner",
|
|
48
|
+
project_urls={
|
|
49
|
+
"Bug Reports": "https://github.com/lstephensFederation/lolTasks/issues",
|
|
50
|
+
"Source": "https://github.com/lstephensFederation/lolTasks",
|
|
51
|
+
},
|
|
52
|
+
)
|
loltasks-1.0.0/task.py
ADDED
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import curses
|
|
3
|
+
import datetime
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from datetime import timedelta
|
|
8
|
+
|
|
9
|
+
DATA_DIR = os.path.expanduser('~/.lolTasks')
|
|
10
|
+
DATA_FILE = os.path.join(DATA_DIR, 'weekly_tasks.json')
|
|
11
|
+
|
|
12
|
+
STATES = ['TO-DO', 'PENDING', 'COMPLETED']
|
|
13
|
+
STATE_CYCLE_FORWARD = {s: STATES[(i + 1) % 3] for i, s in enumerate(STATES)}
|
|
14
|
+
STATE_CYCLE_BACKWARD = {s: STATES[(i - 1) % 3] for i, s in enumerate(STATES)}
|
|
15
|
+
|
|
16
|
+
# Display symbols (fixed visual width = 4 chars including spaces/brackets)
|
|
17
|
+
STATE_SYMBOLS = {
|
|
18
|
+
'TO-DO': '[ ] ',
|
|
19
|
+
'PENDING': '[~] ',
|
|
20
|
+
'COMPLETED': '[x] ',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# For consistent offset calculation during editing (always 4 chars)
|
|
24
|
+
PREFIX_WIDTH = 4
|
|
25
|
+
|
|
26
|
+
COLORS = {'TO-DO': 1, 'PENDING': 2, 'COMPLETED': 3}
|
|
27
|
+
|
|
28
|
+
def get_week_key(date):
|
|
29
|
+
y, w, _ = date.isocalendar()
|
|
30
|
+
return f"{y}-W{w:02d}"
|
|
31
|
+
|
|
32
|
+
def week_to_date(year, week):
|
|
33
|
+
d = datetime.date(year, 1, 4)
|
|
34
|
+
d -= timedelta(days=d.isocalendar()[2] - 1)
|
|
35
|
+
return d + timedelta(weeks=week - 1)
|
|
36
|
+
|
|
37
|
+
def load_data():
|
|
38
|
+
if os.path.exists(DATA_FILE):
|
|
39
|
+
with open(DATA_FILE, 'r') as f:
|
|
40
|
+
return json.load(f)
|
|
41
|
+
return {}
|
|
42
|
+
|
|
43
|
+
def save_data(data):
|
|
44
|
+
os.makedirs(DATA_DIR, exist_ok=True)
|
|
45
|
+
with open(DATA_FILE, 'w') as f:
|
|
46
|
+
json.dump(data, f, indent=4)
|
|
47
|
+
|
|
48
|
+
def get_input(stdscr, base_y, base_x, initial='', start_at_beginning=False):
|
|
49
|
+
"""Safer line editor with cursor movement + vim-style start support + undo/redo + word navigation"""
|
|
50
|
+
curses.curs_set(1)
|
|
51
|
+
stdscr.keypad(True)
|
|
52
|
+
s = list(initial)
|
|
53
|
+
pos = 0 if start_at_beginning else len(s)
|
|
54
|
+
|
|
55
|
+
# Undo/redo history
|
|
56
|
+
history = [''.join(s)]
|
|
57
|
+
history_pos = 0
|
|
58
|
+
|
|
59
|
+
_, max_x = stdscr.getmaxyx()
|
|
60
|
+
display_width = max_x - base_x - 2 # margin
|
|
61
|
+
|
|
62
|
+
def save_state():
|
|
63
|
+
nonlocal history, history_pos
|
|
64
|
+
current = ''.join(s)
|
|
65
|
+
# Remove any history after current position
|
|
66
|
+
history = history[:history_pos + 1]
|
|
67
|
+
history.append(current)
|
|
68
|
+
history_pos = len(history) - 1
|
|
69
|
+
# Limit history to 50 entries
|
|
70
|
+
if len(history) > 50:
|
|
71
|
+
history.pop(0)
|
|
72
|
+
history_pos -= 1
|
|
73
|
+
|
|
74
|
+
def undo():
|
|
75
|
+
nonlocal s, pos, history_pos
|
|
76
|
+
if history_pos > 0:
|
|
77
|
+
history_pos -= 1
|
|
78
|
+
s = list(history[history_pos])
|
|
79
|
+
pos = min(pos, len(s))
|
|
80
|
+
|
|
81
|
+
def redo():
|
|
82
|
+
nonlocal s, pos, history_pos
|
|
83
|
+
if history_pos < len(history) - 1:
|
|
84
|
+
history_pos += 1
|
|
85
|
+
s = list(history[history_pos])
|
|
86
|
+
pos = min(pos, len(s))
|
|
87
|
+
|
|
88
|
+
def skip_word_left():
|
|
89
|
+
nonlocal pos
|
|
90
|
+
# Skip whitespace
|
|
91
|
+
while pos > 0 and s[pos - 1].isspace():
|
|
92
|
+
pos -= 1
|
|
93
|
+
# Skip word
|
|
94
|
+
while pos > 0 and not s[pos - 1].isspace():
|
|
95
|
+
pos -= 1
|
|
96
|
+
|
|
97
|
+
def skip_word_right():
|
|
98
|
+
nonlocal pos
|
|
99
|
+
# Skip word
|
|
100
|
+
while pos < len(s) and not s[pos].isspace():
|
|
101
|
+
pos += 1
|
|
102
|
+
# Skip whitespace
|
|
103
|
+
while pos < len(s) and s[pos].isspace():
|
|
104
|
+
pos += 1
|
|
105
|
+
|
|
106
|
+
while True:
|
|
107
|
+
start = max(0, pos - display_width + 5)
|
|
108
|
+
visible = s[start:start + display_width]
|
|
109
|
+
visible_str = ''.join(visible)
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
stdscr.move(base_y, base_x)
|
|
113
|
+
stdscr.clrtoeol()
|
|
114
|
+
stdscr.addstr(base_y, base_x, visible_str)
|
|
115
|
+
except curses.error:
|
|
116
|
+
pass # Skip if can't draw
|
|
117
|
+
try:
|
|
118
|
+
cursor_x = base_x + (pos - start)
|
|
119
|
+
stdscr.move(base_y, cursor_x)
|
|
120
|
+
except curses.error:
|
|
121
|
+
pass
|
|
122
|
+
stdscr.refresh()
|
|
123
|
+
|
|
124
|
+
key = stdscr.getkey()
|
|
125
|
+
|
|
126
|
+
if key == '\n':
|
|
127
|
+
break
|
|
128
|
+
elif key == '\x01': # Ctrl+A - jump to start of line
|
|
129
|
+
pos = 0
|
|
130
|
+
elif key == '\x05': # Ctrl+E - jump to end of line
|
|
131
|
+
pos = len(s)
|
|
132
|
+
elif key == '\x1b': # Escape key or start of escape sequence
|
|
133
|
+
# Check for escape sequences
|
|
134
|
+
stdscr.nodelay(True)
|
|
135
|
+
try:
|
|
136
|
+
next_key = stdscr.getkey()
|
|
137
|
+
if next_key == 'u': # Esc + U = undo (vim-style)
|
|
138
|
+
undo()
|
|
139
|
+
elif next_key == 'r': # Esc + R = redo (vim-style)
|
|
140
|
+
redo()
|
|
141
|
+
elif next_key == 'b': # Option + Left on macOS (\x1bb)
|
|
142
|
+
skip_word_left()
|
|
143
|
+
elif next_key == 'f': # Option + Right on macOS (\x1bf)
|
|
144
|
+
skip_word_right()
|
|
145
|
+
elif next_key == '[': # CSI sequences
|
|
146
|
+
seq = stdscr.getkey()
|
|
147
|
+
if seq == 'D': # Left arrow
|
|
148
|
+
pos = max(0, pos - 1)
|
|
149
|
+
elif seq == 'C': # Right arrow
|
|
150
|
+
pos = min(len(s), pos + 1)
|
|
151
|
+
elif seq == '1': # \x1b[1~ (Home) or \x1b[1;9D (Option+Left)
|
|
152
|
+
next_char = stdscr.getkey()
|
|
153
|
+
if next_char == '~': # \x1b[1~ - Home
|
|
154
|
+
pos = 0
|
|
155
|
+
elif next_char == ';': # \x1b[1;9D - Option+Left
|
|
156
|
+
modifier = stdscr.getkey()
|
|
157
|
+
direction = stdscr.getkey()
|
|
158
|
+
if modifier == '9' and direction == 'D':
|
|
159
|
+
skip_word_left()
|
|
160
|
+
elif seq == '4': # \x1b[4~ (End)
|
|
161
|
+
next_char = stdscr.getkey()
|
|
162
|
+
if next_char == '~':
|
|
163
|
+
pos = len(s)
|
|
164
|
+
elif seq == '7': # \x1b[7~ (Home on some terminals)
|
|
165
|
+
next_char = stdscr.getkey()
|
|
166
|
+
if next_char == '~':
|
|
167
|
+
pos = 0
|
|
168
|
+
elif seq == '8': # \x1b[8~ (End on some terminals)
|
|
169
|
+
next_char = stdscr.getkey()
|
|
170
|
+
if next_char == '~':
|
|
171
|
+
pos = len(s)
|
|
172
|
+
else:
|
|
173
|
+
# Unknown CSI sequence
|
|
174
|
+
pass
|
|
175
|
+
else:
|
|
176
|
+
# Single escape - exit edit mode
|
|
177
|
+
break
|
|
178
|
+
except curses.error:
|
|
179
|
+
# Timeout or no more keys - treat as single escape
|
|
180
|
+
break
|
|
181
|
+
finally:
|
|
182
|
+
stdscr.nodelay(False)
|
|
183
|
+
elif key in ('KEY_LEFT', 'KEY_BACKSPACE', '\x7f', '\b'):
|
|
184
|
+
save_state()
|
|
185
|
+
if pos > 0:
|
|
186
|
+
pos -= 1
|
|
187
|
+
if key in ('\x7f', '\b'):
|
|
188
|
+
del s[pos]
|
|
189
|
+
elif key == 'KEY_RIGHT':
|
|
190
|
+
if pos < len(s):
|
|
191
|
+
pos += 1
|
|
192
|
+
elif key == 'KEY_HOME':
|
|
193
|
+
pos = 0
|
|
194
|
+
elif key == 'KEY_END':
|
|
195
|
+
pos = len(s)
|
|
196
|
+
elif key == 'KEY_DC':
|
|
197
|
+
save_state()
|
|
198
|
+
if pos < len(s):
|
|
199
|
+
del s[pos]
|
|
200
|
+
elif len(key) == 1 and 32 <= ord(key) <= 126:
|
|
201
|
+
save_state()
|
|
202
|
+
s.insert(pos, key)
|
|
203
|
+
pos += 1
|
|
204
|
+
|
|
205
|
+
curses.curs_set(0)
|
|
206
|
+
return ''.join(s)
|
|
207
|
+
|
|
208
|
+
def show_help():
|
|
209
|
+
print("Weekly Tasks App - task")
|
|
210
|
+
print("Keys:")
|
|
211
|
+
print(" ↑↓/kj Move selection / Reorder (when in reorder mode)")
|
|
212
|
+
print(" r Toggle reorder mode")
|
|
213
|
+
print(" ←→/hl Prev/Next week")
|
|
214
|
+
print(" Tab Cycle state forward")
|
|
215
|
+
print(" Shift+Tab Cycle state backward")
|
|
216
|
+
print(" I Edit current item at start of line (vim-style I)")
|
|
217
|
+
print(" a Add new task after selected")
|
|
218
|
+
print(" Enter Edit selected item (cursor at end)")
|
|
219
|
+
print(" d Delete selected task")
|
|
220
|
+
print(" n / p Shift task next / prev week")
|
|
221
|
+
print(" Ctrl+U Undo last action")
|
|
222
|
+
print(" Ctrl+R Redo last undone action")
|
|
223
|
+
print(" q Quit")
|
|
224
|
+
print()
|
|
225
|
+
print("In edit mode:")
|
|
226
|
+
print(" Esc Exit edit mode")
|
|
227
|
+
print(" Esc+u Undo (vim-style)")
|
|
228
|
+
print(" Esc+r Redo (vim-style)")
|
|
229
|
+
print(" Option+←→ Word navigation")
|
|
230
|
+
print(" Ctrl+A/E Line navigation (start/end)")
|
|
231
|
+
print(" Arrow keys Cursor movement")
|
|
232
|
+
sys.exit(0)
|
|
233
|
+
|
|
234
|
+
def main(stdscr):
|
|
235
|
+
if len(sys.argv) > 1 and sys.argv[1] in ('--help', '-h'):
|
|
236
|
+
show_help()
|
|
237
|
+
|
|
238
|
+
curses.curs_set(0)
|
|
239
|
+
stdscr.keypad(True)
|
|
240
|
+
curses.start_color()
|
|
241
|
+
curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK)
|
|
242
|
+
curses.init_pair(2, curses.COLOR_BLUE, curses.COLOR_BLACK)
|
|
243
|
+
curses.init_pair(3, curses.COLOR_GREEN, curses.COLOR_BLACK)
|
|
244
|
+
|
|
245
|
+
today = datetime.date.today()
|
|
246
|
+
current_week = get_week_key(today)
|
|
247
|
+
|
|
248
|
+
active_week = current_week
|
|
249
|
+
selected = -1
|
|
250
|
+
edit_mode = False
|
|
251
|
+
reorder_mode = False
|
|
252
|
+
scroll_offset = 0
|
|
253
|
+
force_start = False
|
|
254
|
+
|
|
255
|
+
# Global undo/redo history
|
|
256
|
+
undo_history = []
|
|
257
|
+
undo_pos = -1
|
|
258
|
+
MAX_UNDO = 20
|
|
259
|
+
|
|
260
|
+
# Save initial state
|
|
261
|
+
initial_data = load_data()
|
|
262
|
+
undo_history.append(json.dumps(initial_data))
|
|
263
|
+
undo_pos = 0
|
|
264
|
+
|
|
265
|
+
def save_undo_state():
|
|
266
|
+
nonlocal undo_history, undo_pos
|
|
267
|
+
# Save current in-memory data, not from disk
|
|
268
|
+
current_data = data.copy()
|
|
269
|
+
# Remove any history after current position
|
|
270
|
+
undo_history = undo_history[:undo_pos + 1]
|
|
271
|
+
undo_history.append(json.dumps(current_data))
|
|
272
|
+
undo_pos = len(undo_history) - 1
|
|
273
|
+
# Limit history
|
|
274
|
+
if len(undo_history) > MAX_UNDO:
|
|
275
|
+
undo_history.pop(0)
|
|
276
|
+
undo_pos -= 1
|
|
277
|
+
|
|
278
|
+
def undo():
|
|
279
|
+
nonlocal undo_history, undo_pos, selected, active, prev, nxt, data
|
|
280
|
+
if undo_pos > 0:
|
|
281
|
+
undo_pos -= 1
|
|
282
|
+
restored_data = json.loads(undo_history[undo_pos])
|
|
283
|
+
save_data(restored_data)
|
|
284
|
+
# Reload data immediately
|
|
285
|
+
data = load_data()
|
|
286
|
+
active = data[active_week]
|
|
287
|
+
nxt = data[next_week]
|
|
288
|
+
prev = data[prev_week]
|
|
289
|
+
# Adjust selected index
|
|
290
|
+
selected = max(-1, min(selected, len(active['tasks']) - 1))
|
|
291
|
+
return True
|
|
292
|
+
return False
|
|
293
|
+
|
|
294
|
+
def redo():
|
|
295
|
+
nonlocal undo_history, undo_pos, selected, active, prev, nxt, data
|
|
296
|
+
if undo_pos < len(undo_history) - 1:
|
|
297
|
+
undo_pos += 1
|
|
298
|
+
restored_data = json.loads(undo_history[undo_pos])
|
|
299
|
+
save_data(restored_data)
|
|
300
|
+
# Reload data immediately
|
|
301
|
+
data = load_data()
|
|
302
|
+
active = data[active_week]
|
|
303
|
+
nxt = data[next_week]
|
|
304
|
+
prev = data[prev_week]
|
|
305
|
+
# Adjust selected index
|
|
306
|
+
selected = max(-1, min(selected, len(active['tasks']) - 1))
|
|
307
|
+
return True
|
|
308
|
+
return False
|
|
309
|
+
|
|
310
|
+
while True:
|
|
311
|
+
data = load_data()
|
|
312
|
+
|
|
313
|
+
if active_week not in data:
|
|
314
|
+
data[active_week] = {'title': 'Editable Title here for the week', 'tasks': []}
|
|
315
|
+
save_data(data)
|
|
316
|
+
|
|
317
|
+
stdscr.clear()
|
|
318
|
+
maxy, maxx = stdscr.getmaxyx()
|
|
319
|
+
|
|
320
|
+
y, w_part = active_week.split('-W')
|
|
321
|
+
y = int(y)
|
|
322
|
+
w = int(w_part)
|
|
323
|
+
active_date = week_to_date(y, w)
|
|
324
|
+
prev_date = active_date - timedelta(weeks=1)
|
|
325
|
+
next_date = active_date + timedelta(weeks=1)
|
|
326
|
+
prev_week = get_week_key(prev_date)
|
|
327
|
+
next_week = get_week_key(next_date)
|
|
328
|
+
|
|
329
|
+
for wk in [prev_week, active_week, next_week]:
|
|
330
|
+
if wk not in data:
|
|
331
|
+
data[wk] = {'title': 'Week title', 'tasks': []}
|
|
332
|
+
save_data(data)
|
|
333
|
+
|
|
334
|
+
prev = data[prev_week]
|
|
335
|
+
active = data[active_week]
|
|
336
|
+
nxt = data[next_week]
|
|
337
|
+
|
|
338
|
+
selected = max(-1, min(selected, len(active['tasks']) - 1))
|
|
339
|
+
|
|
340
|
+
# Layout positions - ensure active title is always visible
|
|
341
|
+
title_y = 0
|
|
342
|
+
prev_start_y = 2
|
|
343
|
+
prev_end_y = 6 # Previous week: title at 0, sep at 1, tasks at 2-5 (max 4 tasks)
|
|
344
|
+
active_title_y = prev_end_y + 1 # Title at line 7
|
|
345
|
+
active_start_y = active_title_y + 2 # Tasks start at line 9
|
|
346
|
+
next_start_y = maxy - 12 if maxy > 35 else active_start_y + 12
|
|
347
|
+
|
|
348
|
+
# Adjust scroll_offset to make selected visible
|
|
349
|
+
if selected >= 0:
|
|
350
|
+
visible_rows = next_start_y - active_start_y - 1
|
|
351
|
+
if selected < scroll_offset:
|
|
352
|
+
scroll_offset = selected
|
|
353
|
+
elif selected > scroll_offset + visible_rows - 1:
|
|
354
|
+
scroll_offset = selected - (visible_rows - 1)
|
|
355
|
+
scroll_offset = max(0, scroll_offset)
|
|
356
|
+
|
|
357
|
+
# Titles & separators
|
|
358
|
+
def draw_title(y, week_key, text, attr=curses.A_NORMAL):
|
|
359
|
+
if y >= maxy: return # Skip if beyond screen
|
|
360
|
+
label = f"{week_key} – {text}"
|
|
361
|
+
if len(label) > maxx - 6:
|
|
362
|
+
label = label[:maxx-9] + "..."
|
|
363
|
+
stdscr.addstr(y, 2, label, attr)
|
|
364
|
+
|
|
365
|
+
draw_title(title_y, prev_week, prev['title'], curses.A_DIM)
|
|
366
|
+
if title_y + 1 < maxy:
|
|
367
|
+
stdscr.addstr(title_y + 1, 0, "─" * (maxx - 2), curses.A_DIM)
|
|
368
|
+
|
|
369
|
+
draw_title(active_title_y, active_week, active['title'], curses.A_BOLD)
|
|
370
|
+
if active_title_y + 1 < maxy:
|
|
371
|
+
stdscr.addstr(active_title_y + 1, 0, "═" * (maxx - 2), curses.A_BOLD)
|
|
372
|
+
|
|
373
|
+
draw_title(next_start_y - 2, next_week, nxt['title'], curses.A_DIM)
|
|
374
|
+
if next_start_y - 1 < maxy:
|
|
375
|
+
stdscr.addstr(next_start_y - 1, 0, "─" * (maxx - 2), curses.A_DIM)
|
|
376
|
+
|
|
377
|
+
# Improved tasks drawing with word-wrap for selected
|
|
378
|
+
def draw_week_tasks(base_y, week_data, is_active, sel_idx=-1, max_tasks=8, scroll_offset=0, max_y=None):
|
|
379
|
+
if max_y is None:
|
|
380
|
+
max_y = maxy
|
|
381
|
+
max_y = min(max_y, maxy - 2) # Leave room for help bar at maxy-1
|
|
382
|
+
tasks = week_data['tasks']
|
|
383
|
+
y = base_y
|
|
384
|
+
wrap_width = maxx - 16 # margin for prefix + indent
|
|
385
|
+
|
|
386
|
+
start_idx = scroll_offset if is_active else 0
|
|
387
|
+
end_idx = len(tasks)
|
|
388
|
+
if max_tasks is not None:
|
|
389
|
+
end_idx = min(end_idx, start_idx + max_tasks)
|
|
390
|
+
|
|
391
|
+
idx = start_idx
|
|
392
|
+
while idx < end_idx and y < max_y:
|
|
393
|
+
t = tasks[idx]
|
|
394
|
+
prefix = STATE_SYMBOLS[t['state']] # always 4 chars
|
|
395
|
+
text = t['text']
|
|
396
|
+
attr = curses.color_pair(COLORS.get(t['state'], 1))
|
|
397
|
+
if is_active and idx == sel_idx:
|
|
398
|
+
attr |= curses.A_REVERSE
|
|
399
|
+
if not is_active:
|
|
400
|
+
attr |= curses.A_DIM
|
|
401
|
+
|
|
402
|
+
full = prefix + text
|
|
403
|
+
if is_active and idx == sel_idx and len(full) > wrap_width:
|
|
404
|
+
lines = []
|
|
405
|
+
rem = full
|
|
406
|
+
while rem:
|
|
407
|
+
if len(rem) <= wrap_width:
|
|
408
|
+
lines.append(rem)
|
|
409
|
+
break
|
|
410
|
+
split = rem.rfind(' ', 0, wrap_width)
|
|
411
|
+
if split == -1:
|
|
412
|
+
split = wrap_width
|
|
413
|
+
lines.append(rem[:split])
|
|
414
|
+
rem = rem[split:].lstrip()
|
|
415
|
+
if rem:
|
|
416
|
+
rem = ' ' * PREFIX_WIDTH + rem
|
|
417
|
+
for line in lines:
|
|
418
|
+
if y >= max_y: break
|
|
419
|
+
try:
|
|
420
|
+
stdscr.addstr(y, 2, line, attr)
|
|
421
|
+
except curses.error:
|
|
422
|
+
pass
|
|
423
|
+
y += 1
|
|
424
|
+
else:
|
|
425
|
+
if len(full) > wrap_width + 5:
|
|
426
|
+
full = full[:wrap_width - 3] + "..."
|
|
427
|
+
if y < max_y:
|
|
428
|
+
try:
|
|
429
|
+
stdscr.addstr(y, 2, full, attr)
|
|
430
|
+
except curses.error:
|
|
431
|
+
pass
|
|
432
|
+
y += 1
|
|
433
|
+
|
|
434
|
+
idx += 1
|
|
435
|
+
|
|
436
|
+
if idx < len(tasks) and y < max_y:
|
|
437
|
+
stdscr.addstr(y, 2, "... more", curses.A_DIM)
|
|
438
|
+
|
|
439
|
+
draw_week_tasks(prev_start_y, prev, False, max_tasks=4, max_y=active_title_y - 1)
|
|
440
|
+
draw_week_tasks(active_start_y, active, True, selected, max_tasks=None, scroll_offset=scroll_offset, max_y=next_start_y - 2)
|
|
441
|
+
draw_week_tasks(next_start_y, nxt, False, max_tasks=8, max_y=maxy - 1)
|
|
442
|
+
|
|
443
|
+
# Help bar
|
|
444
|
+
mode = " [REORDER]" if reorder_mode else ""
|
|
445
|
+
hint = " (after selected)" if 0 <= selected < len(active['tasks']) else " (at end)"
|
|
446
|
+
help_txt = f"↑↓/kj:Move{'/Reorder'+mode} | r:Reorder | ←→:Week | Tab/S-Tab:State | I:Edit@start | a:Add{hint} | ⏎:Edit | d:Del | n/p:Shift | Ctrl+U:Undo | Ctrl+R:Redo | q:Quit"
|
|
447
|
+
if maxy - 1 < maxy:
|
|
448
|
+
stdscr.addstr(maxy - 1, 0, help_txt[:maxx - 1], curses.A_DIM)
|
|
449
|
+
|
|
450
|
+
stdscr.refresh()
|
|
451
|
+
|
|
452
|
+
if edit_mode:
|
|
453
|
+
if selected == -1:
|
|
454
|
+
offset = len(f"{active_week} – ")
|
|
455
|
+
new_title = get_input(stdscr, active_title_y, 2 + offset,
|
|
456
|
+
active['title'], start_at_beginning=force_start)
|
|
457
|
+
active['title'] = new_title
|
|
458
|
+
else:
|
|
459
|
+
t = active['tasks'][selected]
|
|
460
|
+
offset = PREFIX_WIDTH # ← Fixed!
|
|
461
|
+
edit_y = active_start_y + (selected - scroll_offset)
|
|
462
|
+
new_text = get_input(stdscr, edit_y, 2 + offset,
|
|
463
|
+
t['text'], start_at_beginning=force_start)
|
|
464
|
+
t['text'] = new_text
|
|
465
|
+
save_data(data)
|
|
466
|
+
edit_mode = False
|
|
467
|
+
force_start = False
|
|
468
|
+
continue
|
|
469
|
+
|
|
470
|
+
key = stdscr.getkey()
|
|
471
|
+
force_start = False
|
|
472
|
+
|
|
473
|
+
# Handle control key sequences
|
|
474
|
+
if key == '\x15': # Ctrl+U = undo
|
|
475
|
+
if undo():
|
|
476
|
+
continue
|
|
477
|
+
elif key == '\x12': # Ctrl+R = redo
|
|
478
|
+
if redo():
|
|
479
|
+
continue
|
|
480
|
+
|
|
481
|
+
# Normal mode commands
|
|
482
|
+
if key.lower() == 'q':
|
|
483
|
+
save_data(data)
|
|
484
|
+
break
|
|
485
|
+
elif key.lower() == 'r':
|
|
486
|
+
reorder_mode = not reorder_mode
|
|
487
|
+
elif key == '\t' and 0 <= selected < len(active['tasks']):
|
|
488
|
+
active['tasks'][selected]['state'] = STATE_CYCLE_FORWARD[active['tasks'][selected]['state']]
|
|
489
|
+
save_data(data)
|
|
490
|
+
save_undo_state()
|
|
491
|
+
elif key == '\x1b[Z' and 0 <= selected < len(active['tasks']):
|
|
492
|
+
active['tasks'][selected]['state'] = STATE_CYCLE_BACKWARD[active['tasks'][selected]['state']]
|
|
493
|
+
save_data(data)
|
|
494
|
+
save_undo_state()
|
|
495
|
+
elif key == 'I': # Shift+I - edit at start
|
|
496
|
+
if selected == -1 or 0 <= selected < len(active['tasks']):
|
|
497
|
+
edit_mode = True
|
|
498
|
+
force_start = True
|
|
499
|
+
elif key in ('KEY_UP', 'k'):
|
|
500
|
+
tasks = active['tasks']
|
|
501
|
+
if reorder_mode:
|
|
502
|
+
if selected > 0:
|
|
503
|
+
tasks[selected-1], tasks[selected] = tasks[selected], tasks[selected-1]
|
|
504
|
+
selected -= 1
|
|
505
|
+
save_data(data)
|
|
506
|
+
save_undo_state()
|
|
507
|
+
else:
|
|
508
|
+
if selected > 0:
|
|
509
|
+
selected -= 1
|
|
510
|
+
elif selected == -1 and tasks:
|
|
511
|
+
selected = len(tasks) - 1
|
|
512
|
+
elif key in ('KEY_DOWN', 'j'):
|
|
513
|
+
tasks = active['tasks']
|
|
514
|
+
if reorder_mode:
|
|
515
|
+
if selected < len(tasks) - 1:
|
|
516
|
+
tasks[selected+1], tasks[selected] = tasks[selected], tasks[selected+1]
|
|
517
|
+
selected += 1
|
|
518
|
+
save_data(data)
|
|
519
|
+
save_undo_state()
|
|
520
|
+
else:
|
|
521
|
+
if selected == -1 and tasks:
|
|
522
|
+
selected = 0
|
|
523
|
+
elif selected < len(tasks) - 1:
|
|
524
|
+
selected += 1
|
|
525
|
+
elif selected == len(tasks) - 1:
|
|
526
|
+
selected = -1
|
|
527
|
+
elif key in ('KEY_LEFT', 'h'):
|
|
528
|
+
active_week = prev_week
|
|
529
|
+
selected = -1
|
|
530
|
+
scroll_offset = 0
|
|
531
|
+
elif key in ('KEY_RIGHT', 'l'):
|
|
532
|
+
active_week = next_week
|
|
533
|
+
selected = -1
|
|
534
|
+
scroll_offset = 0
|
|
535
|
+
elif key.lower() == 'a':
|
|
536
|
+
tasks = active['tasks']
|
|
537
|
+
new_task = {'text': 'New task', 'state': 'TO-DO'}
|
|
538
|
+
if selected == -1 or selected >= len(tasks):
|
|
539
|
+
tasks.append(new_task)
|
|
540
|
+
selected = len(tasks) - 1
|
|
541
|
+
else:
|
|
542
|
+
pos = selected + 1
|
|
543
|
+
tasks.insert(pos, new_task)
|
|
544
|
+
selected = pos
|
|
545
|
+
save_data(data)
|
|
546
|
+
save_undo_state()
|
|
547
|
+
edit_mode = True
|
|
548
|
+
force_start = False
|
|
549
|
+
elif key == '\n' and selected is not None:
|
|
550
|
+
edit_mode = True
|
|
551
|
+
force_start = False
|
|
552
|
+
elif key.lower() == 'd' and 0 <= selected < len(active['tasks']):
|
|
553
|
+
del active['tasks'][selected]
|
|
554
|
+
selected = max(-1, selected - 1)
|
|
555
|
+
save_data(data)
|
|
556
|
+
save_undo_state()
|
|
557
|
+
elif key.lower() in ('n', 'p') and 0 <= selected < len(active['tasks']):
|
|
558
|
+
tasks = active['tasks']
|
|
559
|
+
task = tasks.pop(selected)
|
|
560
|
+
delta = 1 if key.lower() == 'n' else -1
|
|
561
|
+
target_date = active_date + timedelta(weeks=delta)
|
|
562
|
+
target_week = get_week_key(target_date)
|
|
563
|
+
if target_week not in data:
|
|
564
|
+
data[target_week] = {'title': 'Week title', 'tasks': []}
|
|
565
|
+
data[target_week]['tasks'].append(task)
|
|
566
|
+
selected = max(-1, min(selected, len(tasks) - 1))
|
|
567
|
+
save_data(data)
|
|
568
|
+
save_undo_state()
|
|
569
|
+
|
|
570
|
+
if __name__ == '__main__':
|
|
571
|
+
curses.wrapper(main)
|
|
572
|
+
|
|
573
|
+
def entry_point():
|
|
574
|
+
"""Entry point for console script"""
|
|
575
|
+
curses.wrapper(main)
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# Weekly Tasks Application Specification
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The Weekly Tasks application is a terminal-based task management tool built with Python and the curses library. It provides a text-based user interface for managing weekly tasks with different completion states, allowing users to organize and track their tasks across multiple weeks.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
### Core Functionality
|
|
10
|
+
- **Weekly Task Management**: Organize tasks by ISO week (YYYY-WW format)
|
|
11
|
+
- **Task States**: Three-state system (TO-DO, PENDING, COMPLETED) with visual indicators
|
|
12
|
+
- **Multi-Week Navigation**: View and navigate between previous, current, and next weeks
|
|
13
|
+
- **Task Operations**: Add, edit, delete, and reorder tasks
|
|
14
|
+
- **State Cycling**: Change task states forward and backward
|
|
15
|
+
- **Task Movement**: Move tasks between weeks
|
|
16
|
+
|
|
17
|
+
### User Interface
|
|
18
|
+
- **Terminal-Based**: Uses curses for full-screen text interface
|
|
19
|
+
- **Color Coding**: Different colors for each task state (Red=TO-DO, Blue=PENDING, Green=COMPLETED)
|
|
20
|
+
- **Responsive Layout**: Adapts to terminal size with scrollable task lists
|
|
21
|
+
- **Word Wrapping**: Long task descriptions wrap properly for selected items
|
|
22
|
+
- **Visual Indicators**: Checkboxes and symbols for task states
|
|
23
|
+
|
|
24
|
+
## Data Model
|
|
25
|
+
|
|
26
|
+
### Storage
|
|
27
|
+
- **Location**: `~/.lolTasks/weekly_tasks.json`
|
|
28
|
+
- **Format**: JSON file with week-based structure
|
|
29
|
+
|
|
30
|
+
### Structure
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"2026-W05": {
|
|
34
|
+
"title": "Week of February 3-9, 2026",
|
|
35
|
+
"tasks": [
|
|
36
|
+
{
|
|
37
|
+
"text": "Complete project documentation",
|
|
38
|
+
"state": "TO-DO"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"text": "Review code changes",
|
|
42
|
+
"state": "PENDING"
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Task States
|
|
50
|
+
- **TO-DO**: `[ ] ` - Red color, initial state
|
|
51
|
+
- **PENDING**: `[~] ` - Blue color, in-progress state
|
|
52
|
+
- **COMPLETED**: `[x] ` - Green color, finished state
|
|
53
|
+
|
|
54
|
+
## User Interface Layout
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
Previous Week (YYYY-WW – Title)
|
|
58
|
+
─────────────────────────────────
|
|
59
|
+
[ ] Task 1
|
|
60
|
+
[~] Task 2
|
|
61
|
+
[x] Task 3
|
|
62
|
+
|
|
63
|
+
Current Week (YYYY-WW – Title)
|
|
64
|
+
═════════════════════════════════
|
|
65
|
+
[ ] Selected task (highlighted)
|
|
66
|
+
[~] Task 2
|
|
67
|
+
[x] Task 3
|
|
68
|
+
... more
|
|
69
|
+
|
|
70
|
+
Next Week (YYYY-WW – Title)
|
|
71
|
+
─────────────────────────────────
|
|
72
|
+
[ ] Task 1
|
|
73
|
+
[~] Task 2
|
|
74
|
+
|
|
75
|
+
↑↓/kj:Move/Reorder | r:Reorder | ←→:Week | Tab/S-Tab:State | I:Edit@start | a:Add | ⏎:Edit | d:Del | n/p:Shift | q:Quit
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Key Bindings
|
|
79
|
+
|
|
80
|
+
### Navigation
|
|
81
|
+
- `↑/↓` or `k/j`: Move selection up/down
|
|
82
|
+
- `←/→` or `h/l`: Navigate to previous/next week
|
|
83
|
+
- `Tab`: Cycle task state forward (TO-DO → PENDING → COMPLETED)
|
|
84
|
+
- `Shift+Tab`: Cycle task state backward
|
|
85
|
+
|
|
86
|
+
### Task Management
|
|
87
|
+
- `a`: Add new task after selected item
|
|
88
|
+
- `Enter`: Edit selected task (or week title if none selected)
|
|
89
|
+
- `I`: Edit task at beginning of line (vim-style)
|
|
90
|
+
- `d`: Delete selected task
|
|
91
|
+
- `r`: Toggle reorder mode for moving tasks up/down
|
|
92
|
+
- `Esc`: Exit edit mode (same as Enter - saves changes)
|
|
93
|
+
|
|
94
|
+
### Global Actions
|
|
95
|
+
- `Ctrl + U`: Undo last action (add, delete, edit, reorder, move tasks)
|
|
96
|
+
- `Ctrl + R`: Redo last undone action
|
|
97
|
+
- `n`: Move task to next week
|
|
98
|
+
- `p`: Move task to previous week
|
|
99
|
+
- `q`: Quit application
|
|
100
|
+
|
|
101
|
+
### Text Editing (when in edit mode)
|
|
102
|
+
- `Esc + U`: Undo last change (vim-style)
|
|
103
|
+
- `Esc + R`: Redo last undone change (vim-style)
|
|
104
|
+
- `Option + Left` (macOS): Skip to previous word
|
|
105
|
+
- `Option + Right` (macOS): Skip to next word
|
|
106
|
+
- `Ctrl + A`: Move to start of line
|
|
107
|
+
- `Ctrl + E`: Move to end of line
|
|
108
|
+
- `Left/Right`: Move cursor
|
|
109
|
+
- `Home/End`: Move to start/end of line
|
|
110
|
+
- `Backspace/Delete`: Delete characters
|
|
111
|
+
- `Enter/Esc`: Save and exit edit mode
|
|
112
|
+
|
|
113
|
+
### Task Movement
|
|
114
|
+
- `n`: Move task to next week
|
|
115
|
+
- `p`: Move task to previous week
|
|
116
|
+
|
|
117
|
+
### General
|
|
118
|
+
- `q`: Quit application
|
|
119
|
+
|
|
120
|
+
## File Structure
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
~/.lolTasks/
|
|
124
|
+
├── task.py # Main application script
|
|
125
|
+
├── weekly_tasks.json # Task data storage
|
|
126
|
+
├── weekly_tasks.json.backup # Backup file
|
|
127
|
+
└── __pycache__/ # Python bytecode cache
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Dependencies
|
|
131
|
+
|
|
132
|
+
### Required Python Modules
|
|
133
|
+
- `curses`: Terminal user interface (built-in on Unix systems)
|
|
134
|
+
- `datetime`: Date and time handling (built-in)
|
|
135
|
+
- `json`: JSON data serialization (built-in)
|
|
136
|
+
- `os`: Operating system interface (built-in)
|
|
137
|
+
- `sys`: System-specific parameters (built-in)
|
|
138
|
+
|
|
139
|
+
### System Requirements
|
|
140
|
+
- Unix-like operating system (Linux, macOS)
|
|
141
|
+
- Python 3.x
|
|
142
|
+
- Terminal with curses support
|
|
143
|
+
|
|
144
|
+
## Installation and Usage
|
|
145
|
+
|
|
146
|
+
### Installation
|
|
147
|
+
1. Download `task.py` to a directory in PATH
|
|
148
|
+
2. Make executable: `chmod +x task.py`
|
|
149
|
+
3. Run: `./task.py`
|
|
150
|
+
|
|
151
|
+
### First Run
|
|
152
|
+
- Application creates `~/.lolTasks/` directory automatically
|
|
153
|
+
- Initializes with current week data
|
|
154
|
+
- Shows help with `--help` or `-h` flag
|
|
155
|
+
|
|
156
|
+
## Technical Implementation
|
|
157
|
+
|
|
158
|
+
### Architecture
|
|
159
|
+
- **Single File Application**: All code in `task.py`
|
|
160
|
+
- **Event-Driven UI**: Main loop processes keyboard input
|
|
161
|
+
- **In-Memory Data**: Loads/saves JSON data on operations
|
|
162
|
+
- **State Management**: Tracks UI state (selection, modes, scroll)
|
|
163
|
+
- **Global Undo/Redo**: Maintains application-level history for all operations (limited to 20 states)
|
|
164
|
+
- **Edit History**: Maintains undo/redo buffer for text editing (limited to 50 entries)
|
|
165
|
+
|
|
166
|
+
### Key Functions
|
|
167
|
+
- `main()`: Application entry point with curses wrapper
|
|
168
|
+
- `get_input()`: Safe line editing with cursor movement, undo/redo, and word navigation
|
|
169
|
+
- `draw_week_tasks()`: Render tasks with wrapping and selection
|
|
170
|
+
- `load_data()`/`save_data()`: JSON persistence
|
|
171
|
+
- `get_week_key()`: ISO week calculation
|
|
172
|
+
- `week_to_date()`: Convert week to date
|
|
173
|
+
|
|
174
|
+
### Error Handling
|
|
175
|
+
- Graceful handling of terminal resize
|
|
176
|
+
- Safe file operations with directory creation
|
|
177
|
+
- Input validation for task operations
|
|
178
|
+
- Fallback for drawing operations that exceed screen bounds
|
|
179
|
+
|
|
180
|
+
## Future Enhancements
|
|
181
|
+
|
|
182
|
+
Potential features for future versions:
|
|
183
|
+
- Task categories/tags
|
|
184
|
+
- Due dates within weeks
|
|
185
|
+
- Search functionality
|
|
186
|
+
- Export to different formats
|
|
187
|
+
- Synchronization with external services
|
|
188
|
+
- Customizable color schemes
|
|
189
|
+
- Keyboard shortcut customization
|
|
190
|
+
|
|
191
|
+
## Lessons Learned
|
|
192
|
+
|
|
193
|
+
### Undo/Redo Implementation
|
|
194
|
+
|
|
195
|
+
**Critical Timing of State Saving:**
|
|
196
|
+
- **Wrong approach**: Saving undo state *before* operations leads to broken redo functionality
|
|
197
|
+
- **Correct approach**: Save undo state *after* operations complete and data is persisted
|
|
198
|
+
- **Why**: Undo needs pre-operation state, redo needs post-operation state
|
|
199
|
+
|
|
200
|
+
**Operation Sequence Matters:**
|
|
201
|
+
```python
|
|
202
|
+
# Incorrect (breaks redo):
|
|
203
|
+
save_undo_state() # Saves pre-operation state
|
|
204
|
+
modify_data()
|
|
205
|
+
save_data()
|
|
206
|
+
|
|
207
|
+
# Correct (enables proper undo/redo):
|
|
208
|
+
modify_data()
|
|
209
|
+
save_data()
|
|
210
|
+
save_undo_state() # Saves post-operation state
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**Data Source for State Saving:**
|
|
214
|
+
- **Wrong**: Loading from disk (`load_data()`) captures stale state
|
|
215
|
+
- **Correct**: Saving current in-memory data (`data.copy()`) captures actual state
|
|
216
|
+
|
|
217
|
+
**UI State Management:**
|
|
218
|
+
- Undo/redo operations must immediately reload all UI variables (`active`, `prev`, `nxt`)
|
|
219
|
+
- Selected index must be adjusted to remain valid after state restoration
|
|
220
|
+
- Avoid relying on main loop data reloading for immediate visual feedback
|
|
221
|
+
|
|
222
|
+
**Debugging Complex State:**
|
|
223
|
+
- Add temporary debug displays to show undo position and history length
|
|
224
|
+
- Implement debug keys to test individual components
|
|
225
|
+
- Use step-by-step verification: undo works → redo works → UI updates correctly
|
|
226
|
+
|
|
227
|
+
### Terminal Key Binding Compatibility
|
|
228
|
+
|
|
229
|
+
**Escape Sequence Parsing:**
|
|
230
|
+
- **Challenge**: macOS Terminal sends complex escape sequences for modified keys
|
|
231
|
+
- **Solution**: Implement comprehensive CSI sequence parsing with modifier detection
|
|
232
|
+
- **Key Codes**: Handle both standard (\x1b[H/\x1b[F) and modified (\x1b[1;9D) sequences
|
|
233
|
+
- **Alternative**: Use standard Unix shortcuts (Ctrl+A/Ctrl+E) for better cross-platform compatibility
|
|
234
|
+
|
|
235
|
+
**Debugging Key Input:**
|
|
236
|
+
- Add temporary logging to capture actual key codes sent by terminal
|
|
237
|
+
- Test with different terminal applications (Terminal.app, iTerm2, etc.)
|
|
238
|
+
- Verify key bindings in terminal preferences match expected codes
|
|
239
|
+
- Consider standard Unix shortcuts (Ctrl+A/Ctrl+E) for better compatibility
|
|
240
|
+
|
|
241
|
+
**Cross-Terminal Compatibility:**
|
|
242
|
+
- Support multiple escape sequence formats for the same action
|
|
243
|
+
- Handle both standard curses key codes and raw escape sequences
|
|
244
|
+
- Document platform-specific key combinations in help and spec
|
|
245
|
+
|
|
246
|
+
**Key Insight**: Linear undo history with position tracking works well for simple applications, but requires careful state management to avoid corruption.</content>
|
|
247
|
+
<parameter name="filePath">/Users/lstephens/.lolTasks/task_spec.md
|