intent 0.6.0 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
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'