intent 0.7.0 → 0.8.0

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: a80e9d9082642b1f2325b40629ff95ffb096cdba644d0f2d5aa2af25541c2d2b
4
- data.tar.gz: 90ed06f5afbf84636fbeb70e6fab66a4868dc0d485b751178b2ff31568315237
3
+ metadata.gz: eceb2c98af31fff1168ee446ea327a674c510e96d5bcfb948cae895e391a8399
4
+ data.tar.gz: a8225ec76a08ba859b7f8abff5729527e0426e51fff1757c26e00a63f5b5a9af
5
5
  SHA512:
6
- metadata.gz: a4a23b8d4882af953e2d330c26388c91bb86513c1881192952998f6f74b5f49b682f4b609566ea18b0b6f0ad7517020ac15ccc22ce8756edc001ac5fc25c455b
7
- data.tar.gz: cfc14324613fc505a6ec0a591e73f667dd3360b4ec1d8ee7f05d86813fcbaf5e645f12b917579adc6b046223dbc251f665f65b0cd70087dac39d59cc7adfdccf
6
+ metadata.gz: 7151b6b4e09485ade42fc3dc8756aeaf933d72b96acdb5d216764866d9cf52dd04cfacb828bc4f8498e2a5d90397d7db30add5ae44f3370afb0f9c5916f24765
7
+ data.tar.gz: fb6b9209b34d67417a232b68e2a02dea9faf99e822252becc5d1b7dfc5a6fad5c8885ba4050258a587abc6e7afdc366e97afaf10be0ebbb768e45f2ba014987d
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Intent
2
2
 
3
- Automation engine with todo.txt
3
+ > **Automation engine with todo.txt**
4
4
 
5
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
6
 
@@ -16,6 +16,10 @@ gem install intent
16
16
  intent help
17
17
  ```
18
18
 
19
+ ## The ledger concept
20
+
21
+ Docs and an essay about managing text-based computational ledgers ‘coming soon’ (eventually).
22
+
19
23
  ## Copyright & License
20
24
 
21
25
  Copyright 2024 Mark Rickerby <https://maetl.net>.
data/bin/project ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ path = __FILE__
3
+ while File.symlink?(path)
4
+ path = File.expand_path(File.readlink(path), File.dirname(path))
5
+ end
6
+ $:.unshift(File.join(File.dirname(File.expand_path(path)), '..', 'lib'))
7
+
8
+ require 'intent'
9
+
10
+ Intent::Dispatcher.exec_command(:project, ARGV.dup)
data/intent.gemspec CHANGED
@@ -32,6 +32,7 @@ Gem::Specification.new do |spec|
32
32
  spec.add_runtime_dependency "tty-table", "~> 0.12.0"
33
33
  spec.add_runtime_dependency "tty-tree", "~> 0.4.0"
34
34
  spec.add_runtime_dependency "nanoid", "~> 2.0.0"
35
+ spec.add_runtime_dependency "paint", "~> 2.3.0"
35
36
 
36
37
  spec.add_development_dependency "bundler"
37
38
  spec.add_development_dependency "rake"
@@ -0,0 +1,32 @@
1
+ module Intent
2
+ class Args
3
+ def initialize(input)
4
+ @arity = input.length
5
+
6
+ unless empty?
7
+ case @arity
8
+ when 1 then interpret_args_unary(input[0])
9
+ when 2 then interpret_args_binary(input[0], input[1])
10
+ when 3 then interpret_args_ternary(input[0], input[1], input[2])
11
+ end
12
+ end
13
+ end
14
+
15
+ def empty?
16
+ @arity == 0
17
+ end
18
+
19
+ private
20
+
21
+ def interpret_args_unary(arg1)
22
+
23
+ end
24
+
25
+ def interpret_args_binary(arg1, arg2)
26
+ end
27
+
28
+ def interpret_args_ternary(arg1, arg2, arg3)
29
+
30
+ end
31
+ end
32
+ end
@@ -1,8 +1,17 @@
1
1
  module Intent
2
2
  module Commands
3
- class Intent
3
+ class Intent < Base
4
4
  def run(args, output)
5
- output.puts args
5
+ if args.empty?
6
+ print_help(output)
7
+ else
8
+ case args.first.to_sym
9
+ when :help
10
+ print_help(output)
11
+ else
12
+ raise Core::Errors::COMMAND_NOT_FOUND
13
+ end
14
+ end
6
15
  end
7
16
  end
8
17
  end
@@ -1,5 +1,3 @@
1
- require 'tty-reader'
2
-
3
1
  module Intent
4
2
  module Commands
5
3
  class Inventory < Base
@@ -28,6 +26,12 @@ module Intent
28
26
  when :folder then assign_folder(args, output)
29
27
  when :box then assign_box(args, output)
30
28
  end
29
+ # CONCEPT:
30
+ # Verbs::Assign.invoke_rewrite()
31
+ # verbs.assign.invoke_rewrite(documents.inventory, noun)
32
+ when :sync then sync_inventory(args, output)
33
+ else
34
+ raise Errors:COMMAND_NOT_FOUND
31
35
  end
32
36
  end
33
37
  end
@@ -35,7 +39,7 @@ module Intent
35
39
  private
36
40
 
37
41
  def inventory_tree
38
- pastel = Pastel.new
42
+ color = ::Intent::UI::TermColor.new
39
43
  root = {}
40
44
  documents.inventory.boxes.each do |box|
41
45
  color_code = case box.tags[:id][0].downcase.to_sym
@@ -46,21 +50,23 @@ module Intent
46
50
  else
47
51
  :white
48
52
  end
49
- box_key = "#{pastel.decorate(box.tags[:id], :bold, color_code)} #{box.text}"
53
+ box_key = "#{color.bold.decorate(box.tags[:id], color_code)} #{box.text}"
50
54
  child_items = documents.inventory.items_in(box.tags[:id]).map do |item|
51
- "#{pastel.decorate(item.tags[:id], :bold, color_code)} #{item.text}"
55
+ "#{color.bold.decorate(item.tags[:id], color_code)} #{item.text}"
52
56
  end
53
57
  root[box_key] = child_items
54
58
  end
55
59
  root
56
60
  end
57
61
 
62
+ # ui format reader
58
63
  def inventory_units_of(type)
59
64
  documents.inventory.units_of(type).map do |unit|
60
65
  [unit.text, unit.tags[:sku]]
61
66
  end.to_h
62
67
  end
63
68
 
69
+ # ui format reader
64
70
  def inventory_unassigned_folders
65
71
  folder_types = documents.inventory.units_of(:folder)
66
72
  documents.inventory.unassigned_folders.map do |folder|
@@ -69,6 +75,16 @@ module Intent
69
75
  end.to_h
70
76
  end
71
77
 
78
+ # ui format reader
79
+ def inventory_assigned_boxes
80
+ box_types = documents.inventory.units_of(:box)
81
+ documents.inventory.assigned_boxes.map do |box|
82
+ unit_label = box_types.find { |f| f.tags[:sku] == box.tags[:sku] }
83
+ ["#{box.text} #{box.tags[:id]} (#{unit_label.text})", box.tags[:id]]
84
+ end.to_h
85
+ end
86
+
87
+ # ui format reader
72
88
  def inventory_unassigned_boxes
73
89
  box_types = documents.inventory.units_of(:box)
74
90
  documents.inventory.unassigned_boxes.map do |box|
@@ -98,6 +114,7 @@ module Intent
98
114
 
99
115
  def add_folder(args, output)
100
116
  skus = inventory_units_of(:folder)
117
+ projects = documents.projects.all_tokens
101
118
  prompt = TTY::Prompt.new
102
119
  ref = self
103
120
 
@@ -105,12 +122,35 @@ module Intent
105
122
  key(:sku).select('sku:', skus, filter: true)
106
123
  key(:id).ask('id:', default: ref.generate_id)
107
124
  key(:label).ask('label:', default: '[Unlabelled Folder]')
108
- key(:active).yes?('is active:')
109
125
  end
110
-
111
- label = item[:active] ? "#{item[:label]} @active" : item[:label]
112
126
 
113
- documents.inventory.add_folder!(label, item[:id], item[:sku])
127
+ should_assign_projects = prompt.yes?('assign to projects:')
128
+
129
+ if should_assign_projects
130
+ assigned_projects = prompt.multi_select('projects:', projects, filter: true)
131
+ label = "#{item[:label]} #{assigned_projects.join(' ')}"
132
+ else
133
+ label = item[:label]
134
+ end
135
+
136
+ should_file_in = prompt.select('file in:', {
137
+ "Active, not in box" => :active,
138
+ "Unassigned box" => :unassigned,
139
+ "Assigned project box" => :assigned
140
+ })
141
+
142
+ case should_file_in
143
+ when :active
144
+ label << " @active"
145
+ in_box = nil
146
+ when :unassigned
147
+ in_box = prompt.select('box', inventory_unassigned_boxes)
148
+ when :assigned
149
+ # TODO: filter on selected projects only
150
+ in_box = prompt.select('box:', inventory_assigned_boxes)
151
+ end
152
+
153
+ documents.inventory.add_folder!(label, item[:id], item[:sku], in_box)
114
154
  end
115
155
 
116
156
  def add_box(args, output)
@@ -125,7 +165,7 @@ module Intent
125
165
  end
126
166
 
127
167
  # Repository write pattern
128
- documents.inventory.add_box!(label, item[:id], item[:sku])
168
+ documents.inventory.add_box!(item[:label], item[:id], item[:sku])
129
169
 
130
170
  # Alternative design
131
171
  # noun = create_noun(:box, label, tags)
@@ -169,6 +209,11 @@ module Intent
169
209
  box.projects.concat(details[:projects])
170
210
  documents.inventory.save!
171
211
  end
212
+
213
+ def sync_inventory(args, output)
214
+ result = documents.inventory.sync!
215
+ output.puts "Sync inventory result: #{result}"
216
+ end
172
217
  end
173
218
  end
174
219
  end
@@ -0,0 +1,91 @@
1
+ module Intent
2
+ module Commands
3
+ class Project < Projects
4
+ def run(args, output)
5
+ if args.empty?
6
+ print_help(output)
7
+ else
8
+ case args.first.to_sym
9
+ when :help
10
+ print_help(output)
11
+ when :link
12
+ if args[1].nil?
13
+ # launch into projects selection?
14
+ raise "Need to link to a noun at this stage, sorry"
15
+ else
16
+ case args[1].to_sym
17
+ when :notes then link_notes(args, output)
18
+ else
19
+ raise "Noun not implemented"
20
+ end
21
+ end
22
+ else
23
+ raise Errors:COMMAND_NOT_FOUND
24
+ end
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def link_notes(args, output, needs_assign=false)
31
+ prompt = TTY::Prompt.new
32
+
33
+ project = if args.last.start_with?('+')
34
+ # args.last
35
+ # if documents.working_directory.is_linked?
36
+ # documents.working_directory.project
37
+ # else
38
+ # end
39
+ raise "Good idea and should work but not implemented properly, sorry"
40
+ else
41
+ # TODO: need to test if working_directory is documents_dir
42
+ #
43
+ if documents.working_directory.is_linked?
44
+ documents.working_directory.project
45
+ else
46
+ needs_assign = true
47
+ p_name = prompt.select('assign directory to project:', documents.projects.all_tokens)
48
+ p_id = prompt.ask('id for this directory:', default: generate_id)
49
+ p_name
50
+ end
51
+ end
52
+
53
+ if needs_assign
54
+ c_id = documents.inventory.local_computer_id
55
+ documents.projects.add_directory!(documents.working_directory.path, p_name, p_id, c_id)
56
+ end
57
+
58
+ project_ref = project.sub('+', '')
59
+
60
+ # Symlink done using maetl local notes convention, this could be improved
61
+ # and made more flexible. Would also be good to delegate this outside
62
+ # of the UI routine link_notes()
63
+ target_path = File.join(Env.documents_dir, project.sub('+', ''), 'notes.md')
64
+
65
+ if File.exist?(target_path)
66
+ # TODO: use directory instance given by input filtering so that
67
+ # operations can work on both working dir and a path provided in the UI
68
+ symbolic_path = File.join(documents.working_directory.path, 'NOTES.md')
69
+ File.symlink(target_path, symbolic_path)
70
+
71
+ # ignore_path = File.join(documents.working_directory.path, '.gitignore')
72
+ # if File.exist?(ignore_path)
73
+ # unless File.read(ignore_path).to_s.include?('NOTES.md')
74
+ # File.write(ignore_path, "NOTES.md\n", mode: 'a+')
75
+ # end
76
+ # end
77
+ else
78
+ output.puts "#{project} does not have notes"
79
+ end
80
+
81
+ # output.puts Dir.pwd
82
+ # output.puts ::Intent::Env.documents_dir
83
+ # output.puts ::Intent::Env.computer_serial
84
+ # Check if current working directory is a linked directory
85
+ #item = documents.projects.linked_directory(Dir.pwd)
86
+ #output.puts item
87
+ #File.symlink("DOCUMENT_DIR/project-name/notes.md", "Projects/project-name/NOTES.md")
88
+ end
89
+ end
90
+ end
91
+ end
@@ -1,5 +1,3 @@
1
- require 'readline'
2
-
3
1
  module Intent
4
2
  module Commands
5
3
  class Projects < Base
@@ -14,8 +12,10 @@ module Intent
14
12
  documents.projects.all.each do |task|
15
13
  output.puts task.highlight_as_project
16
14
  end
17
- when :add
18
- add_project(args, output)
15
+ when :add then add_project(args, output)
16
+ when :sync then sync_project(args, output)
17
+ else
18
+ raise Errors:COMMAND_NOT_FOUND
19
19
  end
20
20
  end
21
21
  end
@@ -29,6 +29,11 @@ module Intent
29
29
  name = "+#{name}" unless name.start_with?('+')
30
30
  output.puts name
31
31
  end
32
+
33
+ def sync_project(args, output)
34
+ result = documents.projects.sync!
35
+ output.puts "Sync projects result: #{result}"
36
+ end
32
37
  end
33
38
  end
34
39
  end
@@ -3,7 +3,7 @@ require 'intent/commands/errors'
3
3
  require 'intent/commands/intent'
4
4
  require 'intent/commands/todo'
5
5
  require 'intent/commands/projects'
6
- #require 'intent/commands/project'
6
+ require 'intent/commands/project'
7
7
  #require 'intent/commands/ideas'
8
8
  #require 'intent/commands/idea'
9
9
  require 'intent/commands/inventory'
@@ -0,0 +1,37 @@
1
+ module Intent
2
+ module Core
3
+ class Directory
4
+ attr_reader :path
5
+
6
+ def initialize(path, projects)
7
+ @path = path
8
+ @ledger = projects
9
+ @record = projects.all.find { |d| d.tags[:is] == 'directory' && d.text == @path }
10
+ end
11
+
12
+ def project
13
+ record.projects.first
14
+ end
15
+
16
+ def is_linked?
17
+ !record.nil? && record.projects.any?
18
+ end
19
+
20
+ def assign!(project)
21
+ if record.nil?
22
+ @record = Record.new("#{Date.today} #{@path} is:directory type:#{type} sku:#{sku}")
23
+ ledger.append(record)
24
+ else
25
+ record.projects << project
26
+ end
27
+
28
+ ledger.save!
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :ledger
34
+ attr_reader :record
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,17 @@
1
+ module Intent
2
+ module Core
3
+ class Documents
4
+ attr_reader :projects
5
+ attr_reader :inventory
6
+ attr_reader :inbox
7
+ attr_reader :working_directory
8
+
9
+ def initialize
10
+ @projects = Projects.new("#{Intent::Env.documents_dir}/projects.txt")
11
+ @inventory = Inventory.new("#{Intent::Env.documents_dir}/inventory.txt")
12
+ @inbox = Inbox.new("#{Intent::Env.documents_dir}/todo.txt")
13
+ @working_directory = Directory.new(Dir.pwd, @projects)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,29 @@
1
+ module Intent
2
+ module Core
3
+ class Inbox
4
+ attr_reader :list
5
+
6
+ def initialize(db_path)
7
+ @list = List.new(db_path)
8
+ end
9
+
10
+ def all
11
+ @list.by_not_done
12
+ end
13
+
14
+ def focused
15
+ @list.by_context('@focus').by_not_done
16
+ end
17
+
18
+ def focused_projects
19
+ focused.map { |t| t.projects }.flatten.uniq
20
+ end
21
+
22
+ def add_line!(line)
23
+ record = Record.new("#{Date.today} #{line}")
24
+ @list.prepend(record)
25
+ @list.save!
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,101 @@
1
+ module Intent
2
+ module Core
3
+ class Inventory
4
+ attr_reader :list
5
+ attr_reader :ledger_path
6
+
7
+ def initialize(db_path)
8
+ @ledger_path = db_path
9
+ @list = List.new(db_path)
10
+ end
11
+
12
+ def all
13
+ list.by_not_done
14
+ end
15
+
16
+ def folder_by_id(id)
17
+ all.find { |i| i.tags[:is] == 'folder' && i.tags[:id] == id }
18
+ end
19
+
20
+ def folders
21
+ all.filter { |i| i.tags[:is] == 'folder' }
22
+ end
23
+
24
+ def items_in(id)
25
+ all.filter { |i| i.tags[:in] == id }
26
+ end
27
+
28
+ def unassigned_folders
29
+ all.filter { |i| i.tags[:is] == 'folder' && i.projects.empty? }
30
+ end
31
+
32
+ def assigned_folders
33
+ all.filter { |i| i.tags[:is] == 'folder' && i.projects.any? }
34
+ end
35
+
36
+ def boxes
37
+ all.filter { |i| i.tags[:is] == 'box' }
38
+ end
39
+
40
+ def unassigned_boxes
41
+ all.filter { |i| i.tags[:is] == 'box' && i.projects.empty? }
42
+ end
43
+
44
+ def assigned_boxes
45
+ all.filter { |i| i.tags[:is] == 'box' && i.projects.any? }
46
+ end
47
+
48
+ def units_of(noun)
49
+ all.filter { |i| i.tags[:is] == 'unit' && i.tags[:type] == noun.to_s }
50
+ end
51
+
52
+ def local_computer_id
53
+ all.find { |i| i.tags[:is] == 'computer' && i.tags[:serial] == Env.computer_serial }.tags[:id]
54
+ end
55
+
56
+ def add_unit!(description, type, sku)
57
+ record = Record.new("#{Date.today} #{description} is:unit type:#{type} sku:#{sku}")
58
+ @list.append(record)
59
+ @list.save!
60
+ end
61
+
62
+ def add_item!(description, id, type, sku, box=nil)
63
+ description << " in:#{box}" unless box.nil?
64
+ record = Record.new("#{Date.today} #{description} id:#{id} is:#{type} sku:#{sku}")
65
+ @list.append(record)
66
+ @list.save!
67
+ end
68
+
69
+ def add_folder!(description, id, sku, box=nil)
70
+ add_item!(description, id, :folder, sku, box)
71
+ end
72
+
73
+ def add_box!(description, id, sku)
74
+ add_item!(description, id, :box, sku)
75
+ end
76
+
77
+ def save!
78
+ @list.save!
79
+ end
80
+
81
+ def sync!
82
+ ledger_file = File.basename(ledger_path)
83
+
84
+ if repo.status.changed?(ledger_file)
85
+ repo.add(ledger_file)
86
+ repo.commit("Synchronizing inventory [#{Time.new}]")
87
+ repo.push
88
+ true # Result:OK
89
+ else
90
+ false # Result::NO_CHANGES
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ def repo
97
+ @repo ||= Git.open(Env.documents_dir, :log => Logger.new(STDERR, level: Logger::INFO))
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,58 @@
1
+ module Intent
2
+ module Core
3
+ class Projects
4
+ attr_reader :list
5
+ attr_reader :ledger_path
6
+
7
+ def initialize(db_path)
8
+ @ledger_path = db_path
9
+ @list = List.new(db_path)
10
+ end
11
+
12
+ def all
13
+ list.by_not_done
14
+ end
15
+
16
+ def all_tokens
17
+ all.map { |item| item.projects.first }.uniq
18
+ end
19
+
20
+ def linked_directory(path)
21
+ all.find { |item| item.text == path && ['repository', 'directory'].include?(item.tags[:is]) }
22
+ end
23
+
24
+ def sync!
25
+ ledger_file = File.basename(ledger_path)
26
+
27
+ if repo.status.changed?(ledger_file)
28
+ repo.add(ledger_file)
29
+ repo.commit("Synchronizing projects [#{Time.new}]")
30
+ repo.push
31
+ true # Result:OK
32
+ else
33
+ false # Result::NO_CHANGES
34
+ end
35
+ end
36
+
37
+ def add_directory!(path, project, id, computer)
38
+ record = Record.new([
39
+ Date.today,
40
+ path,
41
+ project,
42
+ "is:directory",
43
+ "id:#{id}",
44
+ "computer:#{computer}"
45
+ ].join(' '))
46
+
47
+ @list.append(record)
48
+ @list.save!
49
+ end
50
+
51
+ private
52
+
53
+ def repo
54
+ @repo ||= Git.open(Env.documents_dir, :log => Logger.new(STDERR, level: Logger::INFO))
55
+ end
56
+ end
57
+ end
58
+ end
data/lib/intent/core.rb CHANGED
@@ -1,25 +1,25 @@
1
1
  module Intent
2
- module Env
3
- def self.documents_dir
4
- File.expand_path(ENV['INTENT_DOCUMENTS_DIR'] || "").to_s
5
- end
2
+ module Core
3
+ List = ::Todo::List
4
+ Record = ::Todo::Task
6
5
 
7
- def self.inbox_dir
8
- ENV['INTENT_INBOX_DIR']
9
- end
6
+ VERBS = [:add, :assign, :list, :link, :sync]
10
7
 
11
- def self.assets_dir
12
- ENV['INTENT_ARCHIVE_DIR']
13
- end
8
+ NOUNS = {
9
+ inventory: [:unit, :box, :folder, :computer],
10
+ projects: [:project, :directory, :repository],
11
+ todo: [:task]
12
+ }
13
+
14
+ class Action
15
+ attr_reader :verb
16
+ attr_reader :noun
14
17
 
15
- def self.projects_dir
16
- ENV['INTENT_PROJECTS_DIR']
18
+ def initialize(verb_sym, noun_r)
19
+ @verb = Verbs.instance_for(verb_sym)
20
+ @noun = Noun.new(noun_r.type, noun_r.label, noun_r.tags)
21
+ end
17
22
  end
18
- end
19
-
20
- module Core
21
- List = ::Todo::List
22
- Record = ::Todo::Task
23
23
 
24
24
  class Noun
25
25
  def initialize(type, label, tags)
@@ -35,150 +35,11 @@ module Intent
35
35
  p tags
36
36
  end
37
37
  end
38
-
39
- class Projects
40
- attr_reader :list
41
-
42
- def initialize(db_path)
43
- @list = List.new(db_path)
44
- end
45
-
46
- def all
47
- list.by_not_done
48
- end
49
-
50
- def all_tokens
51
- all.map { |project| project.projects.first }
52
- end
53
- end
54
-
55
- class Inventory
56
- attr_reader :list
57
-
58
- def initialize(db_path)
59
- @list = List.new(db_path)
60
- end
61
-
62
- def all
63
- list.by_not_done
64
- end
65
-
66
- def folder_by_id(id)
67
- all.find { |i| i.tags[:is] == 'folder' && i.tags[:id] == id }
68
- end
69
-
70
- def folders
71
- all.filter { |i| i.tags[:is] == 'folder' }
72
- end
73
-
74
- def items_in(id)
75
- all.filter { |i| i.tags[:in] == id }
76
- end
77
-
78
- def unassigned_folders
79
- all.filter { |i| i.tags[:is] == 'folder' && i.projects.empty? }
80
- end
81
-
82
- def assigned_folders
83
- all.filter { |i| i.tags[:is] == 'folder' && i.projects.any? }
84
- end
85
-
86
- def boxes
87
- all.filter { |i| i.tags[:is] == 'box' }
88
- end
89
-
90
- def unassigned_boxes
91
- all.filter { |i| i.tags[:is] == 'box' && i.projects.empty? }
92
- end
93
-
94
- def assigned_boxes
95
- all.filter { |i| i.tags[:is] == 'box' && i.projects.any? }
96
- end
97
-
98
- def units_of(noun)
99
- all.filter { |i| i.tags[:is] == 'unit' && i.tags[:type] == noun.to_s }
100
- end
101
-
102
- def add_unit!(description, type, sku)
103
- record = Record.new("#{Date.today} #{description} is:unit type:#{type} sku:#{sku}")
104
- @list.append(record)
105
- @list.save!
106
- end
107
-
108
- def add_item!(description, id, type, sku)
109
- record = Record.new("#{Date.today} #{description} id:#{id} is:#{type} sku:#{sku}")
110
- @list.append(record)
111
- @list.save!
112
- end
113
-
114
- def add_folder!(description, id, sku)
115
- add_item!(description, id, :folder, sku)
116
- end
117
-
118
- def add_box!(description, id, sku)
119
- add_item!(description, id, :box, sku)
120
- end
121
-
122
- def save!
123
- @list.save!
124
- end
125
- end
126
-
127
- class Inbox
128
- attr_reader :list
129
-
130
- def initialize(db_path)
131
- @list = List.new(db_path)
132
- end
133
-
134
- def all
135
- @list.by_not_done
136
- end
137
-
138
- def focused
139
- @list.by_context('@focus').by_not_done
140
- end
141
-
142
- def focused_projects
143
- focused.map { |t| t.projects }.flatten.uniq
144
- end
145
-
146
- def add_line!(line)
147
- record = Record.new("#{Date.today} #{line}")
148
- @list.prepend(record)
149
- @list.save!
150
- end
151
- end
152
-
153
- class Documents
154
- attr_reader :projects
155
- attr_reader :inventory
156
- attr_reader :inbox
157
-
158
- def initialize
159
- @projects = Projects.new("#{Intent::Env.documents_dir}/projects.txt")
160
- @inventory = Inventory.new("#{Intent::Env.documents_dir}/inventory.txt")
161
- @inbox = Inbox.new("#{Intent::Env.documents_dir}/todo.txt")
162
- end
163
- end
164
- end
165
-
166
- class Dispatcher
167
- def self.exec_command(command, args, output=STDOUT)
168
- command = init_command(command).new
169
- command.run(args, output)
170
- end
171
-
172
- def self.init_command(command)
173
- case command
174
- when :intent then return Commands::Intent
175
- when :inventory then return Commands::Inventory
176
- when :projects then return Commands::Projects
177
- when :project then return Commands::Project
178
- when :todo then return Commands::Todo
179
- else
180
- raise Commands::Errors::COMMAND_NOT_FOUND
181
- end
182
- end
183
38
  end
184
39
  end
40
+
41
+ require 'intent/core/projects'
42
+ require 'intent/core/inventory'
43
+ require 'intent/core/inbox'
44
+ require 'intent/core/directory'
45
+ require 'intent/core/documents'
@@ -1,3 +1,5 @@
1
- require 'pathname'
2
-
3
- require 'intent/desktop/commands'
1
+ require 'intent/verbs/add'
2
+ require 'intent/verbs/assign'
3
+ require 'intent/verbs/link'
4
+ require 'intent/verbs/list'
5
+ require 'intent/verbs/sync'
@@ -0,0 +1,20 @@
1
+ module Intent
2
+ class Dispatcher
3
+ def self.exec_command(command, args, output=STDOUT)
4
+ command = init_command(command).new
5
+ command.run(args, output)
6
+ end
7
+
8
+ def self.init_command(command)
9
+ case command
10
+ when :intent then return Commands::Intent
11
+ when :inventory then return Commands::Inventory
12
+ when :projects then return Commands::Projects
13
+ when :project then return Commands::Project
14
+ when :todo then return Commands::Todo
15
+ else
16
+ raise Commands::Errors::COMMAND_NOT_FOUND
17
+ end
18
+ end
19
+ end
20
+ end
data/lib/intent/env.rb ADDED
@@ -0,0 +1,26 @@
1
+ module Intent
2
+ module Env
3
+ def self.documents_dir
4
+ File.expand_path(ENV['INTENT_DOCUMENTS_DIR'] || "").to_s
5
+ end
6
+
7
+ def self.inbox_dir
8
+ ENV['INTENT_INBOX_DIR']
9
+ end
10
+
11
+ def self.assets_dir
12
+ ENV['INTENT_ARCHIVE_DIR']
13
+ end
14
+
15
+ def self.projects_dir
16
+ ENV['INTENT_PROJECTS_DIR']
17
+ end
18
+
19
+ def self.computer_serial
20
+ # macOS: `system_profiler SPHardwareDataType`
21
+ # Win: `wmic bios get serialnumber`
22
+ # Linux: `ls /sys/devices/virtual/dmi/id`
23
+ ENV['INTENT_COMPUTER_SERIAL']
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,82 @@
1
+ ====================================================================
2
+ Intent . Havin a blast on the command line
3
+ ====================================================================
4
+
5
+ Welcome to the only task manager you will (n)ever need.
6
+
7
+ These docs are not written for a wide audience, but here’s the
8
+ essentials:
9
+
10
+ Verbs
11
+ -----
12
+
13
+ ### `add [noun]`
14
+
15
+ Adds [noun] to the ledger denoted by the given command.
16
+
17
+ ```
18
+ projects add
19
+ projects add project
20
+ projects add directory
21
+ projects add repository
22
+
23
+ project add
24
+
25
+ inventory add
26
+ inventory add folder
27
+ inventory add box
28
+ inventory add unit
29
+ inventory add computer
30
+
31
+ todo add
32
+
33
+ inbox add
34
+ ```
35
+
36
+ ### `assign [noun]`
37
+
38
+ Assigns projects, contexts, tags and associations to ap.
39
+
40
+ ```
41
+ projects assign
42
+
43
+ project assign
44
+
45
+ inventory assign
46
+ ```
47
+
48
+ ### `list [noun]`
49
+
50
+ Lists elements in the ledger.
51
+
52
+ ```
53
+ projects list
54
+
55
+ project list
56
+
57
+ inventory list
58
+
59
+ todo list
60
+
61
+ inbox list
62
+ ```
63
+
64
+ ### `link [noun]`
65
+
66
+ Links elements from a local directory to an assigned project.
67
+
68
+ ```
69
+ project link
70
+ ```
71
+
72
+ ### `sync [noun]`
73
+
74
+ Synchronises changes to the Git origin.
75
+
76
+ ```
77
+ projects sync
78
+
79
+ inventory sync
80
+ ```
81
+
82
+ Documentation is patchy and not always up to date.
@@ -0,0 +1,80 @@
1
+ module Intent
2
+ module UI
3
+ # Shim to convert between Pastel to Paint gems
4
+ # without needing to edit existing call sites.
5
+ #
6
+ # It might be helpful to normalize this convention with an API
7
+ # anyway as we generally want to use the 8/16 colour defaults
8
+ # as they pick up user terminal customisation properly, whereas
9
+ # going full 256 or 24 bit colour means generic RGB values are likely
10
+ # to look shit on customised terminal backgrounds.
11
+ #
12
+ # This way we get the benefits of *mostly* sticking to the terminal
13
+ # defaults, while extending the range of colours with a few carefully
14
+ # chosen values.
15
+ class TermColor
16
+ def initialize
17
+ @decoration_scope = []
18
+ end
19
+
20
+ def decorate(text, *args)
21
+ decoration_scope.push(*args)
22
+ return_decorator(text)
23
+ end
24
+
25
+ def bold(text=nil)
26
+ decoration_scope.push(:bold)
27
+ return_decorator(text)
28
+ end
29
+
30
+ def red(text=nil)
31
+ decoration_scope.push(:red)
32
+ return_decorator(text)
33
+ end
34
+
35
+ def green(text)
36
+ decoration_scope.push(:green)
37
+ return_decorator(text)
38
+ end
39
+
40
+ def blue(text)
41
+ decoration_scope.push(:blue)
42
+ return_decorator(text)
43
+ end
44
+
45
+ def yellow(text)
46
+ decoration_scope.push(:yellow)
47
+ return_decorator(text)
48
+ end
49
+
50
+ def cyan(text)
51
+ decoration_scope.push(:cyan)
52
+ return_decorator(text)
53
+ end
54
+
55
+ def orange(text)
56
+ decoration_scope.push('orange')
57
+ return_decorator(text)
58
+ end
59
+
60
+ def brown(text)
61
+ decoration_scope.push('tan')
62
+ return_decorator(text)
63
+ end
64
+
65
+ private
66
+
67
+ attr_reader :decoration_scope
68
+
69
+ def return_decorator(text)
70
+ if text.nil?
71
+ self
72
+ else
73
+ decorated = Paint[text, *decoration_scope]
74
+ decoration_scope.clear
75
+ decorated
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -1,3 +1,3 @@
1
1
  module Intent
2
- VERSION = '0.7.0'
2
+ VERSION = '0.8.0'
3
3
  end
data/lib/intent.rb CHANGED
@@ -1,21 +1,37 @@
1
1
  #require 'random'
2
- require 'todo-txt'
2
+ #require 'pathname'
3
+
4
+ # Pastel still required but deprecated
3
5
  require 'pastel'
6
+ require 'paint'
7
+
8
+ # CLI UI tools
4
9
  require 'tty-prompt'
5
10
  require 'tty-table'
6
11
  require 'tty-tree'
12
+
13
+ # Identifiers
7
14
  require 'nanoid'
15
+ #require 'calyx'
16
+
17
+ # Interact with git repos
18
+ require 'logger'
19
+ require 'git'
20
+
21
+ # todo.txt data structure support
22
+ require 'todo-txt'
8
23
  require 'gem_ext/todo-txt'
9
24
 
10
25
  Todo.customize do |options|
11
26
  options.require_completed_on = false
12
27
  end
13
28
 
29
+ # Intent library
14
30
  require 'intent/version'
31
+ require 'intent/env'
32
+ require 'intent/args'
15
33
  require 'intent/core'
34
+ #require 'intent/verbs'
16
35
  require 'intent/commands'
17
- # require 'intent/todo'
18
- # require 'intent/review'
19
- # require 'intent/projects'
20
- # require 'intent/desktop'
21
-
36
+ require 'intent/ui/term_color'
37
+ require 'intent/dispatcher'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: intent
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mark Rickerby
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-02-16 00:00:00.000000000 Z
11
+ date: 2024-02-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: todo-txt
@@ -206,6 +206,20 @@ dependencies:
206
206
  - - "~>"
207
207
  - !ruby/object:Gem::Version
208
208
  version: 2.0.0
209
+ - !ruby/object:Gem::Dependency
210
+ name: paint
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - "~>"
214
+ - !ruby/object:Gem::Version
215
+ version: 2.3.0
216
+ type: :runtime
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - "~>"
221
+ - !ruby/object:Gem::Version
222
+ version: 2.3.0
209
223
  - !ruby/object:Gem::Dependency
210
224
  name: bundler
211
225
  requirement: !ruby/object:Gem::Requirement
@@ -254,9 +268,9 @@ email:
254
268
  executables:
255
269
  - intent
256
270
  - inventory
271
+ - project
257
272
  - project_doc
258
273
  - projects
259
- - projects_active
260
274
  - todo
261
275
  - todo_review
262
276
  extensions: []
@@ -269,30 +283,42 @@ files:
269
283
  - Rakefile
270
284
  - bin/intent
271
285
  - bin/inventory
286
+ - bin/project
272
287
  - bin/project_doc
273
288
  - bin/projects
274
- - bin/projects_active
275
289
  - bin/todo
276
290
  - bin/todo_review
277
291
  - intent.gemspec
278
292
  - lib/gem_ext/todo-txt.rb
279
293
  - lib/intent.rb
294
+ - lib/intent/args.rb
280
295
  - lib/intent/commands.rb
281
296
  - lib/intent/commands/base.rb
282
297
  - lib/intent/commands/errors.rb
283
298
  - lib/intent/commands/intent.rb
284
299
  - lib/intent/commands/inventory.rb
300
+ - lib/intent/commands/project.rb
285
301
  - lib/intent/commands/projects.rb
286
302
  - lib/intent/commands/todo.rb
287
303
  - lib/intent/core.rb
304
+ - lib/intent/core/directory.rb
305
+ - lib/intent/core/documents.rb
306
+ - lib/intent/core/inbox.rb
307
+ - lib/intent/core/inventory.rb
308
+ - lib/intent/core/projects.rb
288
309
  - lib/intent/desktop.rb
310
+ - lib/intent/dispatcher.rb
311
+ - lib/intent/env.rb
289
312
  - lib/intent/projects.rb
290
313
  - lib/intent/review.rb
314
+ - lib/intent/text/intent.help.txt
291
315
  - lib/intent/text/inventory.help.txt
292
316
  - lib/intent/text/project.help.txt
293
317
  - lib/intent/text/projects.help.txt
294
318
  - lib/intent/text/todo.help.txt
295
319
  - lib/intent/todo.rb
320
+ - lib/intent/ui/projects_active.rb
321
+ - lib/intent/ui/term_color.rb
296
322
  - lib/intent/ui/ttyui.rb
297
323
  - lib/intent/verbs/add.rb
298
324
  - lib/intent/verbs/cite.rb