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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 86c9e5d7f4f81b1db6abe7c6fbd2ec7daebbfef06abaa05e3129697743648e63
4
- data.tar.gz: ca5a9c91a8ee077bc018c0b5a2251d6e84dfbc4ab2ce371a1fff96dfc65596bc
3
+ metadata.gz: 1a78826427e690ed80d21f63d75c009a652d8f7e6152250a0259d21af5ec36d2
4
+ data.tar.gz: 04c84e824a0787fed413ca28155143e407d322a8a5dd4dac76e0a14ee569a790
5
5
  SHA512:
6
- metadata.gz: de2d74dc9068848a13f5b85d6fd58aefb0381d908e9ff7e2f0d2d81c688afd715dc7bc3d50b584650c3acbc5596cb4ebcd010a5c3b37de4a4ea52f296a3a2ed1
7
- data.tar.gz: f8b7273cb674c0cd9336a1d3acac3cc88a96e31d2a1cf9736ddf03f9d1c56bd5b8aa2ab7112c62655283b78a40d99f2ae70679cb101e3e02f509a66741c05834
6
+ metadata.gz: 61ed11963ba4317356bdfef7d2213fe1f1b2b29153697a34c2d6bd2ce5a09a7f61c12a296c50879909f7f650b556ee7d4f0bce0ce11fc5de891c4a140264f546
7
+ data.tar.gz: 79ba7a74906c8607edffeafc5d8555afe5853212d47b7253f8d2788567d13e4a843234a3d6934adfc1118315f3ac513e6567a5056aad1748497b73f74babe452
data/.gitignore CHANGED
@@ -7,4 +7,6 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ *.gem
10
11
  NOTES.md
12
+ .DS_Store
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.
@@ -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::Review::Manager.run(ARGV.dup)
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
@@ -8,4 +8,4 @@ $:.unshift(File.join(File.dirname(File.expand_path(path)), '..', 'lib'))
8
8
 
9
9
  require 'intent'
10
10
 
11
- Intent::Projects::Manager.run(ARGV.dup)
11
+ Intent::Dispatcher.exec_command(:projects, ARGV.dup)
data/bin/todo CHANGED
@@ -8,4 +8,4 @@ $:.unshift(File.join(File.dirname(File.expand_path(path)), '..', 'lib'))
8
8
 
9
9
  require 'intent'
10
10
 
11
- Intent::Todo::Manager.run(ARGV.dup)
11
+ Intent::Dispatcher.exec_command(:todo, ARGV.dup)
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.9.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"
@@ -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
- # Open tasks delegate to the custom formatting function
14
- print_open_task(pastel)
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,7 @@
1
+ module Intent
2
+ module Commands
3
+ module Errors
4
+ COMMAND_NOT_FOUND = "Command not found"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ module Intent
2
+ module Commands
3
+ class Intent
4
+ def run(args, output)
5
+ output.puts args
6
+ end
7
+ end
8
+ end
9
+ 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'