taskcmd 1.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +5 -0
- data/.ruby-version +1 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +60 -0
- data/LICENSE +19 -0
- data/README.md +210 -0
- data/Rakefile +10 -0
- data/bin/task +11 -0
- data/lib/taskcmd/cli/main.rb +207 -0
- data/lib/taskcmd/cli/project.rb +84 -0
- data/lib/taskcmd/cli.rb +8 -0
- data/lib/taskcmd/project.rb +37 -0
- data/lib/taskcmd/storage.rb +70 -0
- data/lib/taskcmd/task.rb +69 -0
- data/lib/taskcmd/version.rb +5 -0
- data/lib/taskcmd.rb +40 -0
- data/taskcmd.gemspec +30 -0
- metadata +90 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 723e8eb0557e705073c6d168ce52322474ff9f946f57d4a643559064c6b1d6e1
|
4
|
+
data.tar.gz: 836f07de02e71356bc6295d939a80788bda129ae504e7baa946d88d7b1ada52d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ffd928ff855d3b185035fa8bcffcbd5a9b108b9ced5abe58f17f34da698d4b7127f00c2325b5fdb0c89f049a031540c54a84547f0971d9177d44e8ee8acb5a71
|
7
|
+
data.tar.gz: 7174dd2d1fb2ec65e98799bfd9d494d5a0560433689780a7fbac7110acba3d253623642de9e8676a6980b01dda76c7fe69520ade47225b6a5bfddf044de3d5e2
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.3.1
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
taskcmd (1.0.1)
|
5
|
+
msgpack (~> 1.7.2)
|
6
|
+
thor (~> 1.3.1)
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: https://rubygems.org/
|
10
|
+
specs:
|
11
|
+
ast (2.4.2)
|
12
|
+
coderay (1.1.3)
|
13
|
+
json (2.7.2)
|
14
|
+
language_server-protocol (3.17.0.3)
|
15
|
+
method_source (1.1.0)
|
16
|
+
msgpack (1.7.2)
|
17
|
+
parallel (1.24.0)
|
18
|
+
parser (3.3.1.0)
|
19
|
+
ast (~> 2.4.1)
|
20
|
+
racc
|
21
|
+
pry (0.14.2)
|
22
|
+
coderay (~> 1.1)
|
23
|
+
method_source (~> 1.0)
|
24
|
+
racc (1.7.3)
|
25
|
+
rainbow (3.1.1)
|
26
|
+
rake (13.2.1)
|
27
|
+
regexp_parser (2.9.0)
|
28
|
+
rexml (3.2.6)
|
29
|
+
rubocop (1.63.4)
|
30
|
+
json (~> 2.3)
|
31
|
+
language_server-protocol (>= 3.17.0)
|
32
|
+
parallel (~> 1.10)
|
33
|
+
parser (>= 3.3.0.2)
|
34
|
+
rainbow (>= 2.2.2, < 4.0)
|
35
|
+
regexp_parser (>= 1.8, < 3.0)
|
36
|
+
rexml (>= 3.2.5, < 4.0)
|
37
|
+
rubocop-ast (>= 1.31.1, < 2.0)
|
38
|
+
ruby-progressbar (~> 1.7)
|
39
|
+
unicode-display_width (>= 2.4.0, < 3.0)
|
40
|
+
rubocop-ast (1.31.2)
|
41
|
+
parser (>= 3.3.0.4)
|
42
|
+
rubocop-rake (0.6.0)
|
43
|
+
rubocop (~> 1.0)
|
44
|
+
ruby-progressbar (1.13.0)
|
45
|
+
thor (1.3.1)
|
46
|
+
unicode-display_width (2.5.0)
|
47
|
+
|
48
|
+
PLATFORMS
|
49
|
+
arm64-darwin-22
|
50
|
+
ruby
|
51
|
+
|
52
|
+
DEPENDENCIES
|
53
|
+
pry
|
54
|
+
rake
|
55
|
+
rubocop
|
56
|
+
rubocop-rake
|
57
|
+
taskcmd!
|
58
|
+
|
59
|
+
BUNDLED WITH
|
60
|
+
2.5.9
|
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2024 Deepak Parpyani (dparpyani)
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,210 @@
|
|
1
|
+
# Taskcmd
|
2
|
+
|
3
|
+
A todo list manager for the command line.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
You will need to have a Ruby environment installed, then run:
|
8
|
+
|
9
|
+
```bash
|
10
|
+
gem install taskcmd
|
11
|
+
```
|
12
|
+
|
13
|
+
This should also install any dependencies, such as [msgpack](https://msgpack.org),
|
14
|
+
which taskcmd uses as its serialization format.
|
15
|
+
|
16
|
+
## Usage
|
17
|
+
|
18
|
+
### Help
|
19
|
+
|
20
|
+
This page gives an overview of how to use taskcmd, but doesn't include all details like what options
|
21
|
+
are available for each command. You can find more by using the `help` command.
|
22
|
+
|
23
|
+
For example, start by doing `task help` to see a list of all possible commands. Then, to see more info
|
24
|
+
on a specific command, you can do `task help <command>`.
|
25
|
+
|
26
|
+
You can also do `task about` to open this GitHub page.
|
27
|
+
|
28
|
+
### Managing projects
|
29
|
+
|
30
|
+
A project is a collection of tasks. You can create new projects by doing the following:
|
31
|
+
|
32
|
+
```bash
|
33
|
+
$ task project add myproject
|
34
|
+
Created project 'myproject', data will be stored at '/Users/<$USER>/.taskcmd/myproject.project.msgpack'.
|
35
|
+
'myproject' is now the active project.
|
36
|
+
```
|
37
|
+
|
38
|
+
If you're creating your first project, it will automatically be set as your active project. As you'll see later
|
39
|
+
below, taskcmd assumes you're working with tasks in your active project unless the `--project` option is
|
40
|
+
explicitly specified.
|
41
|
+
|
42
|
+
You can also switch to a different active project:
|
43
|
+
|
44
|
+
```bash
|
45
|
+
# Let's create another project to switch to
|
46
|
+
$ task project add anotherproject
|
47
|
+
Created project 'anotherproject', data will be stored at '/Users/<$USER>/.taskcmd/anotherproject.project.msgpack'.
|
48
|
+
|
49
|
+
# Now, let's switch to it
|
50
|
+
$ task project switch anotherproject
|
51
|
+
'anotherproject' is now the active project.
|
52
|
+
```
|
53
|
+
|
54
|
+
See all created projects with:
|
55
|
+
|
56
|
+
```bash
|
57
|
+
$ task project list
|
58
|
+
myproject
|
59
|
+
anotherproject (active)
|
60
|
+
```
|
61
|
+
|
62
|
+
And finally, you can delete a project (and all its tasks) by doing:
|
63
|
+
```bash
|
64
|
+
# The '-f' (force) option bypasses the confirmation
|
65
|
+
$ task project rm -f anotherproject
|
66
|
+
Project 'anotherproject' was deleted.
|
67
|
+
```
|
68
|
+
|
69
|
+
### Creating new tasks
|
70
|
+
|
71
|
+
Let's switch back to our other project and start adding a few tasks:
|
72
|
+
|
73
|
+
```bash
|
74
|
+
$ task project switch myproject
|
75
|
+
'myproject' is now the active project.
|
76
|
+
|
77
|
+
$ task add "my very first task"
|
78
|
+
Task was created in project 'myproject' successfully!
|
79
|
+
|
80
|
+
id: 1
|
81
|
+
priority: medium
|
82
|
+
description: my very first task
|
83
|
+
created_at: 2024-05-06 14:55:59 -0400
|
84
|
+
completed_at: -
|
85
|
+
|
86
|
+
$ task add "yet another task"
|
87
|
+
Task was created in project 'myproject' successfully!
|
88
|
+
|
89
|
+
id: 2
|
90
|
+
priority: medium
|
91
|
+
description: yet another task
|
92
|
+
created_at: 2024-05-06 14:56:20 -0400
|
93
|
+
completed_at: -
|
94
|
+
|
95
|
+
$ task add --priority=high "a very high priority task"
|
96
|
+
Task was created in project 'myproject' successfully!
|
97
|
+
|
98
|
+
id: 3
|
99
|
+
priority: high
|
100
|
+
description: a very high priority task
|
101
|
+
created_at: 2024-05-06 14:56:25 -0400
|
102
|
+
completed_at: -
|
103
|
+
```
|
104
|
+
|
105
|
+
You can also specify a different project by providing the `--project` or `-p` option. This option
|
106
|
+
also works with the other commands listed below that are related to managing tasks.
|
107
|
+
|
108
|
+
### Viewing tasks
|
109
|
+
|
110
|
+
List all tasks:
|
111
|
+
|
112
|
+
```bash
|
113
|
+
$ task list
|
114
|
+
id priority created at completed at description
|
115
|
+
1 medium 2024-05-06 14:55:59 -0400 - my very first task
|
116
|
+
2 medium 2024-05-06 14:56:20 -0400 - yet another task
|
117
|
+
3 high 2024-05-06 14:56:25 -0400 - a very high priority task
|
118
|
+
```
|
119
|
+
|
120
|
+
Filter tasks by priority:
|
121
|
+
|
122
|
+
```bash
|
123
|
+
$ task list --priority medium
|
124
|
+
Searching for tasks with priority: medium
|
125
|
+
|
126
|
+
id priority created at completed at description
|
127
|
+
1 medium 2024-05-06 14:55:59 -0400 - my very first task
|
128
|
+
2 medium 2024-05-06 14:56:20 -0400 - yet another task
|
129
|
+
```
|
130
|
+
|
131
|
+
Utilizing `grep` to search for tasks:
|
132
|
+
```bash
|
133
|
+
$ task list | grep my
|
134
|
+
1 medium 2024-05-06 14:55:59 -0400 - my very first task
|
135
|
+
```
|
136
|
+
|
137
|
+
### Completing tasks
|
138
|
+
|
139
|
+
Mark a task as done by specifying the task ID:
|
140
|
+
|
141
|
+
```bash
|
142
|
+
$ task done 3
|
143
|
+
Task with id=3 in project 'myproject' was marked as completed.
|
144
|
+
|
145
|
+
id: 3
|
146
|
+
priority: high
|
147
|
+
description: a very high priority task
|
148
|
+
created_at: 2024-05-06 14:56:25 -0400
|
149
|
+
completed_at: 2024-05-06 15:02:59 -0400
|
150
|
+
```
|
151
|
+
|
152
|
+
You can mark it as incomplete again by doing:
|
153
|
+
|
154
|
+
```bash
|
155
|
+
$ task undo 3
|
156
|
+
Task with id=3 in project 'myproject' was marked as incomplete.
|
157
|
+
|
158
|
+
id: 3
|
159
|
+
priority: high
|
160
|
+
description: a very high priority task
|
161
|
+
created_at: 2024-05-06 14:56:25 -0400
|
162
|
+
completed_at: -
|
163
|
+
```
|
164
|
+
|
165
|
+
### Editing tasks
|
166
|
+
|
167
|
+
Here's an example of editing a task:
|
168
|
+
|
169
|
+
```bash
|
170
|
+
$ task edit 3 --priority low --description "a low priority task now"
|
171
|
+
Task with id=3 in project 'myproject' was successfully updated!
|
172
|
+
|
173
|
+
id: 3
|
174
|
+
priority: low
|
175
|
+
description: a low priority task now
|
176
|
+
created_at: 2024-05-06 14:56:25 -0400
|
177
|
+
completed_at: -
|
178
|
+
```
|
179
|
+
|
180
|
+
### Deleting tasks
|
181
|
+
|
182
|
+
```bash
|
183
|
+
# The '-f' (force) option bypasses the confirmation
|
184
|
+
$ task rm -f 3
|
185
|
+
Found the following task with id=3 in project 'myproject'.
|
186
|
+
|
187
|
+
id: 3
|
188
|
+
priority: low
|
189
|
+
description: a low priority task now
|
190
|
+
created_at: 2024-05-06 14:56:25 -0400
|
191
|
+
completed_at: -
|
192
|
+
|
193
|
+
Task has been deleted.
|
194
|
+
```
|
195
|
+
|
196
|
+
### Backup
|
197
|
+
|
198
|
+
Taskcmd stores its config, along with the projects and tasks, in the `~/.taskcmd` directory. If you'd
|
199
|
+
like to, you can include this directory in your backups to prevent accidental loss of data.
|
200
|
+
|
201
|
+
## Development
|
202
|
+
|
203
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version,
|
204
|
+
update the version number in `version.rb`, and then run `bundle exec rake release`, which will create
|
205
|
+
a git tag for the version, push git commits and the created tag, and push the `.gem` file to
|
206
|
+
[rubygems.org](https://rubygems.org).
|
207
|
+
|
208
|
+
## Contributing
|
209
|
+
|
210
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/dparpyani/taskcmd.
|
data/Rakefile
ADDED
data/bin/task
ADDED
@@ -0,0 +1,207 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'thor'
|
4
|
+
require 'taskcmd/cli/project'
|
5
|
+
|
6
|
+
module Taskcmd::CLI
|
7
|
+
# Main CLI entry point
|
8
|
+
class Main < Thor
|
9
|
+
desc('project', 'Create and manage projects')
|
10
|
+
subcommand('project', Taskcmd::CLI::Project)
|
11
|
+
|
12
|
+
desc('list', 'List tasks in a project')
|
13
|
+
option(:project, default: Taskcmd.config_get(:active_project),
|
14
|
+
type: :string, aliases: [:p], desc: "if not set, defaults to the active project")
|
15
|
+
option(:id, type: :numeric, desc: "fetch task by id")
|
16
|
+
option(:priority, type: :string, desc: "filter by priority (#{Taskcmd::Task::PRIORITY_CHOICES.map(&:to_s)})")
|
17
|
+
option(:expand, type: :boolean, aliases: [:e], desc: "displays tasks in expanded form, instead of a table")
|
18
|
+
def list()
|
19
|
+
project = Taskcmd.storage.load_project(validate_project())
|
20
|
+
tasks = project.tasks
|
21
|
+
|
22
|
+
id = options[:id]
|
23
|
+
priority = options[:priority]&.to_sym
|
24
|
+
|
25
|
+
if id
|
26
|
+
say("Searching for task with id: #{id}\n\n")
|
27
|
+
tasks.select! { |x| x.id == id }
|
28
|
+
elsif priority
|
29
|
+
unless Taskcmd::Task::PRIORITY_CHOICES.include?(priority)
|
30
|
+
raise Taskcmd::Error, "invalid priority '#{priority}'"
|
31
|
+
end
|
32
|
+
say("Searching for tasks with priority: #{priority}\n\n")
|
33
|
+
tasks.select! { |x| x.priority == priority }
|
34
|
+
end
|
35
|
+
|
36
|
+
if tasks.empty?
|
37
|
+
say("No task found.")
|
38
|
+
else
|
39
|
+
if options[:expand]
|
40
|
+
tasks.each do |task|
|
41
|
+
say(task)
|
42
|
+
say()
|
43
|
+
end
|
44
|
+
else
|
45
|
+
headers = ['id', 'priority', 'created at', 'completed at', 'description']
|
46
|
+
print_table(
|
47
|
+
[headers] +
|
48
|
+
tasks.map {|x| [x.id, x.priority, x.created_at, x.completed_at || '-', x.description].map(&:to_s) }
|
49
|
+
)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
desc('add DESCRIPTION', 'Create a new task with the given description')
|
55
|
+
option(:project, default: Taskcmd.config_get(:active_project),
|
56
|
+
type: :string, aliases: [:p], desc: "if not set, defaults to the active project")
|
57
|
+
option(:priority, default: Taskcmd::Task::PRIORITY_MEDIUM,
|
58
|
+
type: :string, desc: "can be one of #{Taskcmd::Task::PRIORITY_CHOICES.map(&:to_s)}")
|
59
|
+
def add(description)
|
60
|
+
project = Taskcmd.storage.load_project(validate_project())
|
61
|
+
task = Taskcmd::Task.new(project.next_id)
|
62
|
+
task.priority = options[:priority].to_sym
|
63
|
+
task.description = description.strip
|
64
|
+
|
65
|
+
project.tasks.push(task)
|
66
|
+
project.increment_next_id
|
67
|
+
Taskcmd.storage.save_project(project)
|
68
|
+
|
69
|
+
say("Task was created in project '#{project.name}' successfully!\n\n")
|
70
|
+
say(task)
|
71
|
+
say()
|
72
|
+
end
|
73
|
+
|
74
|
+
desc('edit ID', 'Edit a task')
|
75
|
+
option(:project, default: Taskcmd.config_get(:active_project),
|
76
|
+
type: :string, aliases: [:p], desc: "if not set, defaults to the active project")
|
77
|
+
option(:priority, type: :string, desc: "can be one of #{Taskcmd::Task::PRIORITY_CHOICES.map(&:to_s)}")
|
78
|
+
option(:description, type: :string, desc: "new task description")
|
79
|
+
def edit(id)
|
80
|
+
project = Taskcmd.storage.load_project(validate_project())
|
81
|
+
task = project.tasks.find { |x| x.id.to_s == id.to_s }
|
82
|
+
if task.nil?
|
83
|
+
say("No task with id=#{id} in project '#{project.name}'")
|
84
|
+
return
|
85
|
+
end
|
86
|
+
|
87
|
+
if options[:priority]
|
88
|
+
task.priority = options[:priority].to_sym
|
89
|
+
end
|
90
|
+
if options[:description]
|
91
|
+
task.description = options[:description].strip
|
92
|
+
end
|
93
|
+
|
94
|
+
Taskcmd.storage.save_project(project)
|
95
|
+
say("Task with id=#{id} in project '#{project.name}' was successfully updated!\n\n")
|
96
|
+
say(task)
|
97
|
+
say()
|
98
|
+
end
|
99
|
+
|
100
|
+
desc('done ID', 'Marks a task as completed')
|
101
|
+
option(:project, default: Taskcmd.config_get(:active_project),
|
102
|
+
type: :string, aliases: [:p], desc: "if not set, defaults to the active project")
|
103
|
+
def done(id)
|
104
|
+
project = Taskcmd.storage.load_project(validate_project())
|
105
|
+
task = project.tasks.find { |x| x.id.to_s == id.to_s }
|
106
|
+
if task.nil?
|
107
|
+
say("No task with id=#{id} in project '#{project.name}'")
|
108
|
+
return
|
109
|
+
elsif task.done?
|
110
|
+
say("Task with id=#{id} in project '#{project.name}' is already done.")
|
111
|
+
return
|
112
|
+
end
|
113
|
+
|
114
|
+
task.complete!
|
115
|
+
Taskcmd.storage.save_project(project)
|
116
|
+
|
117
|
+
say("Task with id=#{id} in project '#{project.name}' was marked as completed.\n\n")
|
118
|
+
say(task)
|
119
|
+
say()
|
120
|
+
end
|
121
|
+
|
122
|
+
desc('undo ID', 'Unmarks a task as completed')
|
123
|
+
option(:project, default: Taskcmd.config_get(:active_project),
|
124
|
+
type: :string, aliases: [:p], desc: "if not set, defaults to the active project")
|
125
|
+
def undo(id)
|
126
|
+
project = Taskcmd.storage.load_project(validate_project())
|
127
|
+
task = project.tasks.find { |x| x.id.to_s == id.to_s }
|
128
|
+
if task.nil?
|
129
|
+
say("No task with id=#{id} in project '#{project.name}'")
|
130
|
+
return
|
131
|
+
elsif !task.done?
|
132
|
+
say("Task with id=#{id} in project '#{project.name}' is not marked as done.")
|
133
|
+
return
|
134
|
+
end
|
135
|
+
|
136
|
+
task.undo!
|
137
|
+
Taskcmd.storage.save_project(project)
|
138
|
+
|
139
|
+
say("Task with id=#{id} in project '#{project.name}' was marked as incomplete.\n\n")
|
140
|
+
say(task)
|
141
|
+
say()
|
142
|
+
end
|
143
|
+
|
144
|
+
desc('rm ID', 'Deletes a task')
|
145
|
+
option(:project, default: Taskcmd.config_get(:active_project),
|
146
|
+
type: :string, aliases: [:p], desc: "if not set, defaults to the active project")
|
147
|
+
option(:force, type: :boolean, aliases: [:f], desc: "deletes the task without confirming interactively")
|
148
|
+
def rm(id)
|
149
|
+
project = Taskcmd.storage.load_project(validate_project())
|
150
|
+
task = project.tasks.find { |x| x.id.to_s == id.to_s }
|
151
|
+
if task.nil?
|
152
|
+
say("No task with id=#{id} in project '#{project.name}'")
|
153
|
+
return
|
154
|
+
end
|
155
|
+
|
156
|
+
say("Found the following task with id=#{id} in project '#{project.name}'.\n\n")
|
157
|
+
say(task)
|
158
|
+
say()
|
159
|
+
|
160
|
+
if options[:force]
|
161
|
+
sure = true
|
162
|
+
else
|
163
|
+
sure = yes?("Delete this task? This is non-recoverable. Are you sure? Enter 'y' or 'yes' to confirm.", [:white, :on_red])
|
164
|
+
say()
|
165
|
+
end
|
166
|
+
|
167
|
+
if sure
|
168
|
+
project.tasks.delete(task)
|
169
|
+
Taskcmd.storage.save_project(project)
|
170
|
+
say("Task has been deleted.")
|
171
|
+
else
|
172
|
+
say("Task was not deleted.")
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
desc('about', "Opens the project's GitHub page")
|
177
|
+
def about
|
178
|
+
project_link = 'https://github.com/dparpyani/taskcmd'
|
179
|
+
say("Opening project page: #{project_link}")
|
180
|
+
|
181
|
+
if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
|
182
|
+
system('start', project_link)
|
183
|
+
elsif RbConfig::CONFIG['host_os'] =~ /darwin/
|
184
|
+
system('open', project_link)
|
185
|
+
elsif RbConfig::CONFIG['host_os'] =~ /linux|bsd/
|
186
|
+
system('xdg-open', project_link)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
no_commands do
|
191
|
+
def validate_project()
|
192
|
+
name = options[:project]
|
193
|
+
if name.nil? && Taskcmd.config_get(:active_project).nil?
|
194
|
+
raise Taskcmd::Error, "project not specified, and no active project set"
|
195
|
+
end
|
196
|
+
unless Taskcmd.storage.project_exists?(name)
|
197
|
+
raise Taskcmd::Error, "project with name '#{name}' does not exist"
|
198
|
+
end
|
199
|
+
name
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def self.exit_on_failure?
|
204
|
+
true
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'thor'
|
4
|
+
|
5
|
+
module Taskcmd::CLI
|
6
|
+
class Project < Thor
|
7
|
+
desc('list', 'Lists existing projects')
|
8
|
+
def list
|
9
|
+
projects = Taskcmd.storage.project_name_list
|
10
|
+
if projects.empty?
|
11
|
+
say("No projects.")
|
12
|
+
return
|
13
|
+
end
|
14
|
+
|
15
|
+
active_project = Taskcmd.config_get(:active_project)
|
16
|
+
projects.each do |project|
|
17
|
+
out = project + (project == active_project ? " (active)" : "")
|
18
|
+
say(out)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
desc('add NAME', 'Create a new project')
|
23
|
+
def add(name)
|
24
|
+
if Taskcmd.storage.project_exists?(name)
|
25
|
+
raise Taskcmd::Error, "project with name '#{name}' already exists"
|
26
|
+
end
|
27
|
+
|
28
|
+
project = Taskcmd::Project.new(name)
|
29
|
+
path = Taskcmd.storage.save_project(project)
|
30
|
+
say("Created project '#{name}', data will be stored at '#{path}'.")
|
31
|
+
|
32
|
+
# If this is the only project
|
33
|
+
if Taskcmd.storage.project_name_list.length == 1
|
34
|
+
Taskcmd.config_set(:active_project, name)
|
35
|
+
say("'#{name}' is now the active project.")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
desc('switch NAME', 'Switch active project')
|
40
|
+
def switch(name)
|
41
|
+
active_project = Taskcmd.config_get(:active_project)
|
42
|
+
if active_project == name
|
43
|
+
say("'#{name}' is already the active project.")
|
44
|
+
return
|
45
|
+
end
|
46
|
+
|
47
|
+
unless Taskcmd.storage.project_exists?(name)
|
48
|
+
raise Taskcmd::Error, "project with name '#{name}' does not exist"
|
49
|
+
end
|
50
|
+
|
51
|
+
Taskcmd.config_set(:active_project, name)
|
52
|
+
say("'#{name}' is now the active project.")
|
53
|
+
end
|
54
|
+
|
55
|
+
desc('rm NAME', 'Delete a project')
|
56
|
+
option(:force, type: :boolean, aliases: [:f], desc: "deletes the project and its tasks without confirming interactively")
|
57
|
+
def rm(name)
|
58
|
+
unless Taskcmd.storage.project_exists?(name)
|
59
|
+
raise Taskcmd::Error, "project with name '#{name}' does not exist"
|
60
|
+
end
|
61
|
+
|
62
|
+
path = Taskcmd.storage.project_file(name)
|
63
|
+
if options[:force]
|
64
|
+
sure = true
|
65
|
+
else
|
66
|
+
say("This will delete project '#{name}' and all its tasks, along with its file '#{path}'.", [:white, :on_red])
|
67
|
+
sure = yes?("This is non-recoverable. Are you sure? Enter 'y' or 'yes' to confirm.", [:white, :on_red])
|
68
|
+
say()
|
69
|
+
end
|
70
|
+
|
71
|
+
if sure
|
72
|
+
active_project = Taskcmd.config_get(:active_project)
|
73
|
+
if active_project == name
|
74
|
+
Taskcmd.config_set(:active_project, nil)
|
75
|
+
end
|
76
|
+
|
77
|
+
Taskcmd.storage.delete_project(name)
|
78
|
+
say("Project '#{name}' was deleted.")
|
79
|
+
else
|
80
|
+
say("Project was not deleted.")
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
data/lib/taskcmd/cli.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Taskcmd
|
4
|
+
# Project is a collection of similar tasks.
|
5
|
+
class Project
|
6
|
+
NAME_PATTERN = /\A[a-z]+{3,10}\z/.freeze
|
7
|
+
|
8
|
+
attr_reader :name, :tasks, :next_id
|
9
|
+
|
10
|
+
def initialize(name)
|
11
|
+
raise Taskcmd::Error, 'invalid project name' unless name.match?(NAME_PATTERN)
|
12
|
+
|
13
|
+
@name = name
|
14
|
+
@tasks = []
|
15
|
+
@next_id = 1
|
16
|
+
end
|
17
|
+
|
18
|
+
def increment_next_id
|
19
|
+
@next_id += 1
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_msgpack_ext
|
23
|
+
{
|
24
|
+
name: name,
|
25
|
+
tasks: tasks,
|
26
|
+
next_id: next_id,
|
27
|
+
}.to_msgpack
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.from_msgpack_ext(data)
|
31
|
+
unpacked = MessagePack.unpack(data)
|
32
|
+
new(unpacked[:name]).tap do |obj|
|
33
|
+
unpacked.each { |k, v| obj.instance_variable_set("@#{k}", v) }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Taskcmd
|
4
|
+
# Interact with local filesystem to store and load projects.
|
5
|
+
class Storage
|
6
|
+
DEFAULT_DIRECTORY_NAME = '.taskcmd'
|
7
|
+
CONFIG_FILE_NAME = 'config'
|
8
|
+
CONFIG_FILE_EXT = 'msgpack'
|
9
|
+
PROJECT_FILE_EXT = 'project.msgpack'
|
10
|
+
|
11
|
+
attr_reader :dir
|
12
|
+
|
13
|
+
def initialize(dir = nil)
|
14
|
+
@dir = dir || default_directory
|
15
|
+
end
|
16
|
+
|
17
|
+
def project_name_list
|
18
|
+
Dir.entries(dir)
|
19
|
+
.select { |x| x.end_with?(".#{PROJECT_FILE_EXT}") }
|
20
|
+
.map { |x| x.delete_suffix(".#{PROJECT_FILE_EXT}") }
|
21
|
+
end
|
22
|
+
|
23
|
+
def load_project(name)
|
24
|
+
data = File.read(project_file(name))
|
25
|
+
MessagePack.unpack(data)
|
26
|
+
end
|
27
|
+
|
28
|
+
def save_project(project)
|
29
|
+
data = MessagePack.pack(project)
|
30
|
+
path = project_file(project.name)
|
31
|
+
File.write(path, data)
|
32
|
+
path
|
33
|
+
end
|
34
|
+
|
35
|
+
def delete_project(project_name)
|
36
|
+
File.delete(project_file(project_name))
|
37
|
+
end
|
38
|
+
|
39
|
+
def project_exists?(name)
|
40
|
+
File.file?(project_file(name))
|
41
|
+
end
|
42
|
+
|
43
|
+
def project_file(name)
|
44
|
+
File.join(dir, "#{name}.#{PROJECT_FILE_EXT}")
|
45
|
+
end
|
46
|
+
|
47
|
+
def save_config(config)
|
48
|
+
data = MessagePack.pack(config)
|
49
|
+
File.write(config_file, data)
|
50
|
+
end
|
51
|
+
|
52
|
+
def load_config
|
53
|
+
return {} unless File.file?(config_file)
|
54
|
+
data = File.read(config_file)
|
55
|
+
MessagePack.unpack(data)
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def config_file
|
61
|
+
@config_file ||= File.join(dir, "#{CONFIG_FILE_NAME}.#{CONFIG_FILE_EXT}")
|
62
|
+
end
|
63
|
+
|
64
|
+
def default_directory
|
65
|
+
File.join(Dir.home, DEFAULT_DIRECTORY_NAME).tap do |dir|
|
66
|
+
Dir.mkdir(dir, 0o700) unless Dir.exist?(dir)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/taskcmd/task.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Taskcmd
|
4
|
+
# An individual Todo item
|
5
|
+
class Task
|
6
|
+
PRIORITY_LOW = :low
|
7
|
+
PRIORITY_MEDIUM = :medium
|
8
|
+
PRIORITY_HIGH = :high
|
9
|
+
PRIORITY_CHOICES = [PRIORITY_LOW, PRIORITY_MEDIUM, PRIORITY_HIGH]
|
10
|
+
|
11
|
+
attr_reader :id, :priority, :created_at, :completed_at
|
12
|
+
attr_accessor :description
|
13
|
+
|
14
|
+
def initialize(id)
|
15
|
+
raise Taskcmd::Error, 'invalid id' unless id.is_a?(Integer)
|
16
|
+
|
17
|
+
@id = id
|
18
|
+
@priority = PRIORITY_MEDIUM
|
19
|
+
@created_at = Time.now
|
20
|
+
end
|
21
|
+
|
22
|
+
def priority=(value)
|
23
|
+
raise Taskcmd::Error, "invalid priority '#{value}'" unless PRIORITY_CHOICES.include?(value)
|
24
|
+
@priority = value
|
25
|
+
end
|
26
|
+
|
27
|
+
def description=(value)
|
28
|
+
raise Taskcmd::Error, 'description cannot be empty' if value.empty?
|
29
|
+
@description = value
|
30
|
+
end
|
31
|
+
|
32
|
+
def complete!
|
33
|
+
@completed_at = Time.now
|
34
|
+
end
|
35
|
+
|
36
|
+
def undo!
|
37
|
+
@completed_at = nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def done?
|
41
|
+
!@completed_at.nil?
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_s
|
45
|
+
"id: #{id}\n" \
|
46
|
+
"priority: #{priority}\n" \
|
47
|
+
"description: #{description}\n" \
|
48
|
+
"created_at: #{created_at}\n" \
|
49
|
+
"completed_at: #{completed_at || "-"}" \
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_msgpack_ext
|
53
|
+
{
|
54
|
+
id: id,
|
55
|
+
priority: priority,
|
56
|
+
description: description,
|
57
|
+
created_at: created_at,
|
58
|
+
completed_at: completed_at,
|
59
|
+
}.to_msgpack
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.from_msgpack_ext(data)
|
63
|
+
unpacked = MessagePack.unpack(data)
|
64
|
+
new(unpacked[:id]).tap do |obj|
|
65
|
+
unpacked.each { |k, v| obj.instance_variable_set("@#{k}", v) }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/taskcmd.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'msgpack'
|
4
|
+
require 'taskcmd/project'
|
5
|
+
require 'taskcmd/storage'
|
6
|
+
require 'taskcmd/task'
|
7
|
+
require 'taskcmd/version'
|
8
|
+
|
9
|
+
MessagePack::DefaultFactory.register_type(
|
10
|
+
MessagePack::Timestamp::TYPE, # -1
|
11
|
+
Time,
|
12
|
+
packer: MessagePack::Time::Packer,
|
13
|
+
unpacker: MessagePack::Time::Unpacker
|
14
|
+
)
|
15
|
+
MessagePack::DefaultFactory.register_type(0x00, Symbol)
|
16
|
+
MessagePack::DefaultFactory.register_type(0x01, Taskcmd::Project)
|
17
|
+
MessagePack::DefaultFactory.register_type(0x02, Taskcmd::Task)
|
18
|
+
|
19
|
+
# Main app module
|
20
|
+
module Taskcmd
|
21
|
+
class Error < StandardError; end
|
22
|
+
|
23
|
+
def self.storage
|
24
|
+
@@storage ||= Taskcmd::Storage.new()
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.config_get(key)
|
28
|
+
@@config ||= storage.load_config
|
29
|
+
@@config[key]
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.config_set(key, val)
|
33
|
+
@@config ||= storage.load_config
|
34
|
+
@@config[key] = val
|
35
|
+
storage.save_config(@@config)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Load CLI after as it has dependency on module methods
|
40
|
+
require 'taskcmd/cli'
|
data/taskcmd.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/taskcmd/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'taskcmd'
|
7
|
+
spec.version = Taskcmd::VERSION
|
8
|
+
spec.authors = ['Deepak Parpyani']
|
9
|
+
spec.summary = 'Simple todo list manager for the command line'
|
10
|
+
spec.homepage = 'https://github.com/dparpyani/taskcmd'
|
11
|
+
spec.license = 'MIT'
|
12
|
+
|
13
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 3.3.1')
|
14
|
+
|
15
|
+
# Specify which files should be added to the gem when it is released.
|
16
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
17
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
18
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
19
|
+
end
|
20
|
+
spec.bindir = 'bin'
|
21
|
+
spec.executables = spec.files.grep(%r{\Abin/}) { |f| File.basename(f) }
|
22
|
+
spec.require_paths = ['lib']
|
23
|
+
|
24
|
+
# Metadata
|
25
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
26
|
+
|
27
|
+
# Dependencies
|
28
|
+
spec.add_dependency 'msgpack', '~> 1.7.2'
|
29
|
+
spec.add_dependency 'thor', '~> 1.3.1'
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: taskcmd
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Deepak Parpyani
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-05-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: msgpack
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.7.2
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.7.2
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: thor
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.3.1
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.3.1
|
41
|
+
description:
|
42
|
+
email:
|
43
|
+
executables:
|
44
|
+
- task
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- ".gitignore"
|
49
|
+
- ".rubocop.yml"
|
50
|
+
- ".ruby-version"
|
51
|
+
- Gemfile
|
52
|
+
- Gemfile.lock
|
53
|
+
- LICENSE
|
54
|
+
- README.md
|
55
|
+
- Rakefile
|
56
|
+
- bin/task
|
57
|
+
- lib/taskcmd.rb
|
58
|
+
- lib/taskcmd/cli.rb
|
59
|
+
- lib/taskcmd/cli/main.rb
|
60
|
+
- lib/taskcmd/cli/project.rb
|
61
|
+
- lib/taskcmd/project.rb
|
62
|
+
- lib/taskcmd/storage.rb
|
63
|
+
- lib/taskcmd/task.rb
|
64
|
+
- lib/taskcmd/version.rb
|
65
|
+
- taskcmd.gemspec
|
66
|
+
homepage: https://github.com/dparpyani/taskcmd
|
67
|
+
licenses:
|
68
|
+
- MIT
|
69
|
+
metadata:
|
70
|
+
rubygems_mfa_required: 'true'
|
71
|
+
post_install_message:
|
72
|
+
rdoc_options: []
|
73
|
+
require_paths:
|
74
|
+
- lib
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: 3.3.1
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
requirements: []
|
86
|
+
rubygems_version: 3.5.9
|
87
|
+
signing_key:
|
88
|
+
specification_version: 4
|
89
|
+
summary: Simple todo list manager for the command line
|
90
|
+
test_files: []
|