intent 0.7.0 → 0.8.0

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: 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