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 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
@@ -0,0 +1,3 @@
1
+ .vscode/
2
+ pkg/
3
+
data/.rubocop.yml ADDED
@@ -0,0 +1,5 @@
1
+ AllCops:
2
+ NewCops: enable
3
+
4
+ require:
5
+ - rubocop-rake
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.3.1
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify gem dependencies in taskcmd.gemspec
6
+ gemspec
7
+
8
+ gem 'pry'
9
+ gem 'rake'
10
+ gem 'rubocop', require: false
11
+ gem 'rubocop-rake', require: false
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
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rubocop/rake_task'
5
+
6
+ RuboCop::RakeTask.new do |task|
7
+ task.requires << 'rubocop-rake'
8
+ end
9
+
10
+ task default: %i[rubocop]
data/bin/task ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'taskcmd'
6
+
7
+ begin
8
+ Taskcmd::CLI::Main.start
9
+ rescue Taskcmd::Error => err
10
+ $stderr.puts "error: #{err.message}"
11
+ end
@@ -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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'taskcmd/cli/main'
4
+
5
+ module Taskcmd
6
+ module CLI
7
+ end
8
+ end
@@ -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
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Taskcmd
4
+ VERSION = '1.0.1'
5
+ 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: []