intent 0.6.0 → 0.7.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 +4 -4
- data/.gitignore +2 -0
- data/LICENSE.txt +1 -1
- data/README.md +23 -0
- data/bin/{review → intent} +1 -2
- data/bin/inventory +12 -0
- data/bin/projects +1 -1
- data/bin/todo +1 -1
- data/intent.gemspec +11 -1
- data/lib/gem_ext/todo-txt.rb +26 -2
- data/lib/intent/commands/base.rb +40 -0
- data/lib/intent/commands/errors.rb +7 -0
- data/lib/intent/commands/intent.rb +9 -0
- data/lib/intent/commands/inventory.rb +210 -0
- data/lib/intent/commands/projects.rb +34 -0
- data/lib/intent/commands/todo.rb +51 -0
- data/lib/intent/commands.rb +9 -0
- data/lib/intent/core.rb +185 -0
- data/lib/intent/desktop.rb +3 -0
- data/lib/intent/projects.rb +1 -1
- data/lib/intent/review.rb +10 -1
- data/lib/intent/text/inventory.help.txt +6 -0
- data/lib/intent/text/project.help.txt +7 -0
- data/lib/intent/text/projects.help.txt +9 -0
- data/lib/intent/text/todo.help.txt +15 -0
- data/lib/intent/todo.rb +3 -1
- data/lib/intent/ui/term_color.rb +80 -0
- data/lib/intent/verbs/add.rb +21 -0
- data/lib/intent/verbs/cite.rb +14 -0
- data/lib/intent/verbs/install.rb +20 -0
- data/lib/intent/verbs/review.rb +131 -0
- data/lib/intent/{todo/manager.rb → verbs/todo.rb} +3 -16
- data/lib/intent/version.rb +1 -1
- data/lib/intent.rb +22 -3
- metadata +170 -12
- data/.travis.yml +0 -4
- data/lib/intent/projects/manager.rb +0 -20
- data/lib/intent/review/manager.rb +0 -17
- /data/lib/intent/{projects → verbs}/status.rb +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1a78826427e690ed80d21f63d75c009a652d8f7e6152250a0259d21af5ec36d2
|
4
|
+
data.tar.gz: 04c84e824a0787fed413ca28155143e407d322a8a5dd4dac76e0a14ee569a790
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 61ed11963ba4317356bdfef7d2213fe1f1b2b29153697a34c2d6bd2ce5a09a7f61c12a296c50879909f7f650b556ee7d4f0bce0ce11fc5de891c4a140264f546
|
7
|
+
data.tar.gz: 79ba7a74906c8607edffeafc5d8555afe5853212d47b7253f8d2788567d13e4a843234a3d6934adfc1118315f3ac513e6567a5056aad1748497b73f74babe452
|
data/.gitignore
CHANGED
data/LICENSE.txt
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
The MIT License (MIT)
|
2
2
|
|
3
|
-
Copyright (c) 2016 Mark Rickerby
|
3
|
+
Copyright (c) 2016-2024 Mark Rickerby <https://maetl.net>
|
4
4
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
data/README.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# Intent
|
2
|
+
|
3
|
+
Automation engine with todo.txt
|
4
|
+
|
5
|
+
This tool is mired deep in the obscurity and pedantry of the text-based computing culture that originated in the 1970s and still continues today. You probably don’t want to use this.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
```
|
10
|
+
gem install intent
|
11
|
+
```
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
```
|
16
|
+
intent help
|
17
|
+
```
|
18
|
+
|
19
|
+
## Copyright & License
|
20
|
+
|
21
|
+
Copyright 2024 Mark Rickerby <https://maetl.net>.
|
22
|
+
|
23
|
+
All documentation and modelling concepts are **CC BY-NC**. Source code and command line tools are MIT.
|
data/bin/{review → intent}
RENAMED
@@ -1,5 +1,4 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
|
3
2
|
path = __FILE__
|
4
3
|
while File.symlink?(path)
|
5
4
|
path = File.expand_path(File.readlink(path), File.dirname(path))
|
@@ -8,4 +7,4 @@ $:.unshift(File.join(File.dirname(File.expand_path(path)), '..', 'lib'))
|
|
8
7
|
|
9
8
|
require 'intent'
|
10
9
|
|
11
|
-
Intent::
|
10
|
+
Intent::Dispatcher.exec_command(:intent, ARGV.dup)
|
data/bin/inventory
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
path = __FILE__
|
3
|
+
|
4
|
+
while File.symlink?(path)
|
5
|
+
path = File.expand_path(File.readlink(path), File.dirname(path))
|
6
|
+
end
|
7
|
+
|
8
|
+
$:.unshift(File.join(File.dirname(File.expand_path(path)), '..', 'lib'))
|
9
|
+
|
10
|
+
require 'intent'
|
11
|
+
|
12
|
+
Intent::Dispatcher.exec_command(:inventory, ARGV.dup)
|
data/bin/projects
CHANGED
data/bin/todo
CHANGED
data/intent.gemspec
CHANGED
@@ -20,9 +20,19 @@ Gem::Specification.new do |spec|
|
|
20
20
|
|
21
21
|
spec.add_runtime_dependency "todo-txt", "~> 0.12"
|
22
22
|
spec.add_runtime_dependency "pastel", "~> 0.8"
|
23
|
-
spec.add_runtime_dependency "git", "~> 1.
|
23
|
+
spec.add_runtime_dependency "git", "~> 1.19.1"
|
24
24
|
spec.add_runtime_dependency "terminal-notifier", "~> 2.0"
|
25
25
|
spec.add_runtime_dependency "ghost", "~> 1.0.0"
|
26
|
+
spec.add_runtime_dependency "unicode_plot", "0.0.5"
|
27
|
+
spec.add_runtime_dependency "kdl", "1.0.3"
|
28
|
+
spec.add_runtime_dependency "sorted_set", "1.0.3"
|
29
|
+
spec.add_runtime_dependency "strings-ansi", "0.2.0"
|
30
|
+
spec.add_runtime_dependency "bibtex-ruby", "6.1.0"
|
31
|
+
spec.add_runtime_dependency "tty-prompt", "~> 0.23.1"
|
32
|
+
spec.add_runtime_dependency "tty-table", "~> 0.12.0"
|
33
|
+
spec.add_runtime_dependency "tty-tree", "~> 0.4.0"
|
34
|
+
spec.add_runtime_dependency "nanoid", "~> 2.0.0"
|
35
|
+
spec.add_runtime_dependency "paint", "~> 2.3.0"
|
26
36
|
|
27
37
|
spec.add_development_dependency "bundler"
|
28
38
|
spec.add_development_dependency "rake"
|
data/lib/gem_ext/todo-txt.rb
CHANGED
@@ -10,13 +10,37 @@ class Todo::Task
|
|
10
10
|
# Completed tasks are rendered with a strikethrough
|
11
11
|
pastel.strikethrough(to_s)
|
12
12
|
else
|
13
|
-
|
14
|
-
|
13
|
+
[
|
14
|
+
pastel.red(print_priority),
|
15
|
+
pastel.yellow(created_on.to_s),
|
16
|
+
text,
|
17
|
+
pastel.bold.magenta(print_contexts),
|
18
|
+
pastel.bold.blue(print_projects),
|
19
|
+
pastel.bold.cyan(print_tags)
|
20
|
+
].reject { |item| !item || item.nil? || item.empty? }.join(' ')
|
15
21
|
end
|
16
22
|
end
|
17
23
|
|
24
|
+
def highlight_as_project
|
25
|
+
return to_s unless STDOUT.tty?
|
26
|
+
|
27
|
+
pastel = Pastel.new
|
28
|
+
[
|
29
|
+
pastel.bold.cyan(print_projects),
|
30
|
+
pastel.red(print_project_context)
|
31
|
+
].reject { |item| !item || item.nil? || item.empty? }.join(' ')
|
32
|
+
end
|
33
|
+
|
34
|
+
def text=(label)
|
35
|
+
@text = label
|
36
|
+
end
|
37
|
+
|
18
38
|
private
|
19
39
|
|
40
|
+
def print_project_context
|
41
|
+
'active' if contexts.include?('@active')
|
42
|
+
end
|
43
|
+
|
20
44
|
def print_open_task(pastel)
|
21
45
|
[
|
22
46
|
pastel.red(print_priority),
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Intent
|
2
|
+
module Commands
|
3
|
+
class Base
|
4
|
+
attr_reader :identity
|
5
|
+
attr_reader :documents
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@identity = strip_classname
|
9
|
+
@documents = ::Intent::Core::Documents.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def print_help(output)
|
13
|
+
output.puts(File.read(help_txt_path))
|
14
|
+
end
|
15
|
+
|
16
|
+
def generate_id
|
17
|
+
Nanoid.generate(size: 8, alphabet: ID_ALPHABET)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
T_CLASS_PREFIX = 'Intent::Commands::'
|
23
|
+
|
24
|
+
ID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
25
|
+
|
26
|
+
def strip_classname
|
27
|
+
self.class.to_s.sub(T_CLASS_PREFIX, '').downcase
|
28
|
+
end
|
29
|
+
|
30
|
+
def help_txt_path
|
31
|
+
# TODO: does this work on Windows?
|
32
|
+
"#{__dir__}/../text/#{identity}.help.txt"
|
33
|
+
end
|
34
|
+
|
35
|
+
def create_noun(type, label, tags)
|
36
|
+
Intent::Core::Noun.new(type, label, tags)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
require 'tty-reader'
|
2
|
+
|
3
|
+
module Intent
|
4
|
+
module Commands
|
5
|
+
class Inventory < Base
|
6
|
+
def run(args, output)
|
7
|
+
if args.empty?
|
8
|
+
print_help(output)
|
9
|
+
else
|
10
|
+
case args.first.to_sym
|
11
|
+
when :help
|
12
|
+
print_help(output)
|
13
|
+
when :list
|
14
|
+
tree = TTY::Tree.new(inventory_tree)
|
15
|
+
output.puts(tree.render)
|
16
|
+
when :add
|
17
|
+
noun = args[1].to_sym
|
18
|
+
case noun
|
19
|
+
when :folder then add_folder(args, output)
|
20
|
+
when :box then add_box(args, output)
|
21
|
+
when :stock then add_stock(args, output)
|
22
|
+
else
|
23
|
+
raise "Noun not found"
|
24
|
+
end
|
25
|
+
when :assign
|
26
|
+
noun = args[1].to_sym
|
27
|
+
case noun
|
28
|
+
when :folder then assign_folder(args, output)
|
29
|
+
when :box then assign_box(args, output)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def inventory_tree
|
38
|
+
color = ::Intent::UI::TermColor.new
|
39
|
+
root = {}
|
40
|
+
documents.inventory.boxes.each do |box|
|
41
|
+
color_code = case box.tags[:id][0].downcase.to_sym
|
42
|
+
when :g then :green
|
43
|
+
when :y then :yellow
|
44
|
+
when :b then :blue
|
45
|
+
when :r then :red
|
46
|
+
else
|
47
|
+
:white
|
48
|
+
end
|
49
|
+
box_key = "#{color.bold.decorate(box.tags[:id], color_code)} #{box.text}"
|
50
|
+
child_items = documents.inventory.items_in(box.tags[:id]).map do |item|
|
51
|
+
"#{color.bold.decorate(item.tags[:id], color_code)} #{item.text}"
|
52
|
+
end
|
53
|
+
root[box_key] = child_items
|
54
|
+
end
|
55
|
+
root
|
56
|
+
end
|
57
|
+
|
58
|
+
# ui format reader
|
59
|
+
def inventory_units_of(type)
|
60
|
+
documents.inventory.units_of(type).map do |unit|
|
61
|
+
[unit.text, unit.tags[:sku]]
|
62
|
+
end.to_h
|
63
|
+
end
|
64
|
+
|
65
|
+
# ui format reader
|
66
|
+
def inventory_unassigned_folders
|
67
|
+
folder_types = documents.inventory.units_of(:folder)
|
68
|
+
documents.inventory.unassigned_folders.map do |folder|
|
69
|
+
unit_label = folder_types.find { |f| f.tags[:sku] == folder.tags[:sku] }
|
70
|
+
["#{folder.text} #{folder.tags[:id]} (#{unit_label.text})", folder.tags[:id]]
|
71
|
+
end.to_h
|
72
|
+
end
|
73
|
+
|
74
|
+
# ui format reader
|
75
|
+
def inventory_assigned_boxes
|
76
|
+
box_types = documents.inventory.units_of(:box)
|
77
|
+
documents.inventory.assigned_boxes.map do |box|
|
78
|
+
unit_label = box_types.find { |f| f.tags[:sku] == box.tags[:sku] }
|
79
|
+
["#{box.text} #{box.tags[:id]} (#{unit_label.text})", box.tags[:id]]
|
80
|
+
end.to_h
|
81
|
+
end
|
82
|
+
|
83
|
+
# ui format reader
|
84
|
+
def inventory_unassigned_boxes
|
85
|
+
box_types = documents.inventory.units_of(:box)
|
86
|
+
documents.inventory.unassigned_boxes.map do |box|
|
87
|
+
unit_label = box_types.find { |f| f.tags[:sku] == box.tags[:sku] }
|
88
|
+
["#{box.text} #{box.tags[:id]} (#{unit_label.text})", box.tags[:id]]
|
89
|
+
end.to_h
|
90
|
+
end
|
91
|
+
|
92
|
+
def add_stock(args, output)
|
93
|
+
prompt = TTY::Prompt.new
|
94
|
+
type = prompt.select('type of stock:', [:folder, :box])
|
95
|
+
ref = self
|
96
|
+
|
97
|
+
unit = prompt.collect do
|
98
|
+
key(:sku).ask('sku:', default: ref.generate_id)
|
99
|
+
key(:label).ask('label:', default: "[Undocumented #{type.capitalize}]")
|
100
|
+
key(:qty).ask('quantity:', default: 1, convert: :int)
|
101
|
+
end
|
102
|
+
|
103
|
+
documents.inventory.add_unit!(unit[:label], type, unit[:sku])
|
104
|
+
|
105
|
+
unit[:qty].times do
|
106
|
+
label = "[Unlabelled #{type.capitalize}]"
|
107
|
+
documents.inventory.add_item!(label, ref.generate_id, type, unit[:sku])
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def add_folder(args, output)
|
112
|
+
skus = inventory_units_of(:folder)
|
113
|
+
projects = documents.projects.all_tokens
|
114
|
+
prompt = TTY::Prompt.new
|
115
|
+
ref = self
|
116
|
+
|
117
|
+
item = prompt.collect do
|
118
|
+
key(:sku).select('sku:', skus, filter: true)
|
119
|
+
key(:id).ask('id:', default: ref.generate_id)
|
120
|
+
key(:label).ask('label:', default: '[Unlabelled Folder]')
|
121
|
+
end
|
122
|
+
|
123
|
+
should_assign_projects = prompt.yes?('assign to projects:')
|
124
|
+
|
125
|
+
if should_assign_projects
|
126
|
+
assigned_projects = prompt.multi_select('projects:', projects, filter: true)
|
127
|
+
label = "#{item[:label]} #{assigned_projects.join(' ')}"
|
128
|
+
else
|
129
|
+
label = item[:label]
|
130
|
+
end
|
131
|
+
|
132
|
+
should_file_in = prompt.select('file in:', {
|
133
|
+
"Active, not in box" => :active,
|
134
|
+
"Unassigned box" => :unassigned,
|
135
|
+
"Assigned project box" => :assigned
|
136
|
+
})
|
137
|
+
|
138
|
+
case should_file_in
|
139
|
+
when :active
|
140
|
+
label << " @active"
|
141
|
+
in_box = nil
|
142
|
+
when :unassigned
|
143
|
+
in_box = prompt.select('box', inventory_unassigned_boxes)
|
144
|
+
when :assigned
|
145
|
+
# TODO: filter on selected projects only
|
146
|
+
in_box = prompt.select('box:', inventory_assigned_boxes)
|
147
|
+
end
|
148
|
+
|
149
|
+
documents.inventory.add_folder!(label, item[:id], item[:sku], in_box)
|
150
|
+
end
|
151
|
+
|
152
|
+
def add_box(args, output)
|
153
|
+
skus = inventory_units_of(:box)
|
154
|
+
prompt = TTY::Prompt.new
|
155
|
+
ref = self
|
156
|
+
|
157
|
+
item = prompt.collect do
|
158
|
+
key(:sku).select('sku:', skus, filter: true)
|
159
|
+
key(:id).ask('id:', default: ref.generate_id)
|
160
|
+
key(:label).ask('label:', default: '[Unlabelled Box]')
|
161
|
+
end
|
162
|
+
|
163
|
+
# Repository write pattern
|
164
|
+
documents.inventory.add_box!(item[:label], item[:id], item[:sku])
|
165
|
+
|
166
|
+
# Alternative design
|
167
|
+
# noun = create_noun(:box, label, tags)
|
168
|
+
# Add.invoke(:append, documents.inventory, noun)
|
169
|
+
end
|
170
|
+
|
171
|
+
def assign_folder(args, output)
|
172
|
+
projects = documents.projects.all_tokens
|
173
|
+
folders = inventory_unassigned_folders
|
174
|
+
prompt = TTY::Prompt.new
|
175
|
+
|
176
|
+
folder_id = prompt.select('folder:', folders)
|
177
|
+
folder = documents.inventory.folder_by_id(folder_id)
|
178
|
+
|
179
|
+
details = prompt.collect do
|
180
|
+
key(:projects).multi_select('projects:', projects)
|
181
|
+
key(:label).ask('label:', default: folder.text)
|
182
|
+
# TODO: is active
|
183
|
+
end
|
184
|
+
|
185
|
+
folder.text = details[:label]
|
186
|
+
folder.projects.concat(details[:projects])
|
187
|
+
documents.inventory.save!
|
188
|
+
end
|
189
|
+
|
190
|
+
def assign_box(args, output)
|
191
|
+
projects = documents.projects.all_tokens
|
192
|
+
boxes = inventory_unassigned_boxes
|
193
|
+
prompt = TTY::Prompt.new
|
194
|
+
|
195
|
+
box_id = prompt.select('box:', boxes)
|
196
|
+
box = documents.inventory.box_by_id(box_id)
|
197
|
+
|
198
|
+
details = prompt.collect do
|
199
|
+
key(:projects).multi_select('projects:', projects)
|
200
|
+
key(:label).ask('label:', default: box.text)
|
201
|
+
# TODO: is active
|
202
|
+
end
|
203
|
+
|
204
|
+
box.text = details[:label]
|
205
|
+
box.projects.concat(details[:projects])
|
206
|
+
documents.inventory.save!
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'readline'
|
2
|
+
|
3
|
+
module Intent
|
4
|
+
module Commands
|
5
|
+
class Projects < Base
|
6
|
+
def run(args, output)
|
7
|
+
if args.empty?
|
8
|
+
print_help(output)
|
9
|
+
else
|
10
|
+
case args.first.to_sym
|
11
|
+
when :help
|
12
|
+
print_help(output)
|
13
|
+
when :list
|
14
|
+
documents.projects.all.each do |task|
|
15
|
+
output.puts task.highlight_as_project
|
16
|
+
end
|
17
|
+
when :add
|
18
|
+
add_project(args, output)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def add_project(args, output)
|
26
|
+
prompt = TTY::Prompt.new
|
27
|
+
name = prompt.ask('Project identifier: ')
|
28
|
+
name = name.downcase.gsub("_", "-").gsub(" ", "-").gsub(/[^0-9a-z\-]/i, '')
|
29
|
+
name = "+#{name}" unless name.start_with?('+')
|
30
|
+
output.puts name
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Intent
|
2
|
+
module Commands
|
3
|
+
class Todo < Base
|
4
|
+
def run(args, output)
|
5
|
+
if args.empty?
|
6
|
+
print_help(output)
|
7
|
+
else
|
8
|
+
case args.first.to_sym
|
9
|
+
when :add then add_line(args, output)
|
10
|
+
when :list then list_draw(args, output)
|
11
|
+
when :focus then focus_draw(args, output)
|
12
|
+
else
|
13
|
+
raise "Verb not found"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def add_line(args, output)
|
21
|
+
reader = TTY::Reader.new
|
22
|
+
line = reader.read_line("task line: ")
|
23
|
+
documents.inbox.add_line!(line)
|
24
|
+
end
|
25
|
+
|
26
|
+
def focus_draw(args, output)
|
27
|
+
pastel = Pastel.new
|
28
|
+
documents.inbox.focused_projects.each do |project|
|
29
|
+
output.puts pastel.green(project)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def list_draw(args, output)
|
34
|
+
filtered_list = documents.inbox.all
|
35
|
+
|
36
|
+
unless args[1].nil?
|
37
|
+
case args[1][0]
|
38
|
+
when '@'
|
39
|
+
filtered_list = filtered_list.by_context(args[1]).by_not_done
|
40
|
+
when '+'
|
41
|
+
filtered_list = filtered_list.by_project(args[1])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
filtered_list.by_not_done.each do |task|
|
46
|
+
output.puts task.to_s_highlighted
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'intent/commands/base'
|
2
|
+
require 'intent/commands/errors'
|
3
|
+
require 'intent/commands/intent'
|
4
|
+
require 'intent/commands/todo'
|
5
|
+
require 'intent/commands/projects'
|
6
|
+
#require 'intent/commands/project'
|
7
|
+
#require 'intent/commands/ideas'
|
8
|
+
#require 'intent/commands/idea'
|
9
|
+
require 'intent/commands/inventory'
|