rubyfocus 0.3.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.
@@ -0,0 +1,82 @@
1
+ module Rubyfocus
2
+ # The Searchable module allows you to easily search arrays of items by
3
+ # effectively supplying find and select methods. Passing a hash to either
4
+ # of these methods allows you to search by given properties - for example:
5
+ #
6
+ # data_store.find(name: "Foobar")
7
+ #
8
+ # Is basically the same as:
9
+ #
10
+ # data_store.find{ |o| o.name == "Foobard" }
11
+ #
12
+ # If you pass +find+ or +select+ a String or Fixnum, it will look for objects
13
+ # whose +id+ equals this value.
14
+ #
15
+ # You should ensure that your class' +array+ method is overwritten to point
16
+ # to the appriopriate array.
17
+ module Searchable
18
+ # This method will return only the *first* object that matches the given
19
+ # criteria, or +nil+ if no object is found.
20
+ def find(arg=nil, &blck)
21
+ fs_block = find_select_block(arg) || blck
22
+ if fs_block
23
+ return array.find(&fs_block)
24
+ else
25
+ raise ArgumentError, "ItemArray#find called with #{arg.class} argument."
26
+ end
27
+ end
28
+
29
+ # This method will return an array of all objects that match the given
30
+ # criteria, or +[]+ if no object is found.
31
+ def select(arg=nil, &blck)
32
+ fs_block = find_select_block(arg) || blck
33
+ if fs_block
34
+ return array.select(&fs_block)
35
+ else
36
+ raise ArgumentError, "ItemArray#select called with #{arg.class} argument."
37
+ end
38
+ end
39
+
40
+ # This method should be overriden
41
+ def array
42
+ raise RuntimeError, "Method #{self.class}#array has not been implemented!"
43
+ end
44
+
45
+ private
46
+ # This method determines the correct block to use in find or select operations
47
+ def find_select_block(arg)
48
+ case arg
49
+ when Fixnum # ID
50
+ string_id = arg.to_s
51
+ Proc.new{ |item| item.id == string_id }
52
+ when String
53
+ Proc.new{ |item| item.id == arg }
54
+ when Hash
55
+ Proc.new{ |item| arg.all?{ |k,v| item.send(k) == v } }
56
+ else
57
+ nil
58
+ end
59
+ end
60
+ end
61
+
62
+
63
+ # A sample class, SearchableArray, is basically a wrapper around a standard array
64
+ class SearchableArray
65
+ include Searchable
66
+ attr_reader :array
67
+
68
+ # Takes the same arguments as the array it wraps
69
+ def initialize(*args, &blck)
70
+ @array = Array.new(*args, &blck)
71
+ end
72
+
73
+ # In all other respects, it's an array - handily governed by this method
74
+ def method_missing meth, *args, &blck
75
+ if @array.respond_to?(meth)
76
+ @array.send(meth, *args, &blck)
77
+ else
78
+ super(meth, *args, &blck)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,15 @@
1
+ class Rubyfocus::Context < Rubyfocus::RankedItem
2
+ include Rubyfocus::Parser
3
+ def self.matches_node?(node)
4
+ return (node.name == "context")
5
+ end
6
+
7
+ idref :container
8
+ attr_accessor :location
9
+
10
+ def apply_xml(n)
11
+ super(n)
12
+ conditional_set(:container_id,n.at_xpath("xmlns:context")) { |e| e["idref"] }
13
+ conditional_set(:location, n.at_xpath("xmlns:location")){ |e| Rubyfocus::Location.new(e) }
14
+ end
15
+ end
@@ -0,0 +1,18 @@
1
+ class Rubyfocus::Folder < Rubyfocus::RankedItem
2
+ include Rubyfocus::Parser
3
+ def self.matches_node?(node)
4
+ return (node.name == "folder")
5
+ end
6
+
7
+ idref :container
8
+
9
+ def apply_xml(n)
10
+ super(n)
11
+ conditional_set(:container_id, n.at_xpath("xmlns:folder")){ |e| e["idref"] }
12
+ end
13
+
14
+ private
15
+ def inspect_properties
16
+ super + %w(container_id)
17
+ end
18
+ end
@@ -0,0 +1,63 @@
1
+ # The Rubyfocus Item represents an item found in an Omnifocus XML file, and thus also
2
+ # any Omnifocus entity.
3
+ #
4
+ # The Rubyfocus Item has a parent "document" as well as a series of properties determined
5
+ # by the XML file proper. You can pass an XML document at creation or leave it blank. You
6
+ # can always apply XML later using Item#apply_xml or Item#<<
7
+ #
8
+ # By separating these two methods, we can also patch the object with another XML node, e.g.
9
+ # through an update. This is important!
10
+ class Rubyfocus::Item
11
+ include Rubyfocus::IDRef
12
+ include Rubyfocus::ConditionalExec
13
+ attr_accessor :id, :added, :modified, :document
14
+
15
+ def initialize(document=nil, n=nil)
16
+ self.document = document
17
+
18
+ case n
19
+ when Nokogiri::XML::Element
20
+ apply_xml(n)
21
+ when Hash
22
+ n.each do |k,v|
23
+ setter = "#{k}="
24
+ send(setter,v) if respond_to?(setter)
25
+ end
26
+ end
27
+ end
28
+
29
+ def apply_xml(n)
30
+ self.id ||= n["id"] # This should not change once set!
31
+ conditional_set(:added, n.at_xpath("xmlns:added")) { |e| Time.parse(e) }
32
+ conditional_set(:modified, n.at_xpath("xmlns:modified")) { |e| Time.parse(e) }
33
+ end
34
+ alias_method :<<, :apply_xml
35
+
36
+ #---------------------------------------
37
+ # Inspection method
38
+
39
+ def inspect
40
+ msgs = inspect_properties.select{ |sym| self.respond_to?(sym) && !self.send(sym).nil? }
41
+ "#<#{self.class} " + msgs.map{ |e| %|#{e}=#{self.send(e).inspect}| }.join(" ") + ">"
42
+ end
43
+
44
+ def to_serial
45
+ inspect_properties.each_with_object({}){ |s,hsh| hsh[s] = self.send(s) }
46
+ end
47
+
48
+ #---------------------------------------
49
+ # Document set/get methods
50
+ def document= d
51
+ @document.remove_element(self) if @document
52
+ @document = d
53
+ @document.add_element(self) if @document
54
+ end
55
+
56
+ #-------------------------------------------------------------------------------
57
+ # Private inspect methods
58
+
59
+ private
60
+ def inspect_properties
61
+ %w(id added modified)
62
+ end
63
+ end
@@ -0,0 +1,13 @@
1
+ class Rubyfocus::NamedItem < Rubyfocus::Item
2
+ attr_accessor :name
3
+
4
+ def apply_xml(n)
5
+ super(n)
6
+ conditional_set(:name, n.at_xpath("xmlns:name"), &:inner_html)
7
+ end
8
+
9
+ private
10
+ def inspect_properties
11
+ super + %w(name)
12
+ end
13
+ end
@@ -0,0 +1,76 @@
1
+ # The project represents an OmniFocus project object.
2
+ class Rubyfocus::Project < Rubyfocus::Task
3
+ # It's parseable
4
+ include Rubyfocus::Parser
5
+
6
+ #-------------------------------------------------------------------------------
7
+ # Parsing stuff
8
+
9
+ # Projects are <task>s with an interior <project> node
10
+ def self.matches_node?(node)
11
+ return (node.name == "task" && (node/"project").size > 0)
12
+ end
13
+
14
+ # Singleton: contains one-off tasks
15
+ attr_accessor :singleton
16
+
17
+ # How often to we review this project?
18
+ attr_accessor :review_interval
19
+
20
+ # When did we last review the project?
21
+ attr_accessor :last_review
22
+
23
+ # What's the status of the project? Valid options: :active, :inactive, :done. Default
24
+ # is :active
25
+ attr_accessor :status
26
+
27
+ # Initialize the project with a document and optional node to base it off of.
28
+ # Also sets default values
29
+ def initialize(document=nil, n=nil)
30
+ @singleton = false
31
+ @order = :sequential
32
+ @status = :active
33
+ super(document, n)
34
+ end
35
+
36
+ # Apply XML node to project
37
+ def apply_xml(n)
38
+ super(n)
39
+
40
+ #First, set project
41
+ p = n.at_xpath("xmlns:project")
42
+ conditional_set(:singleton, p.at_xpath("xmlns:singleton")) { |e| e.inner_html == "true" }
43
+ conditional_set(:review_interval, p.at_xpath("xmlns:review-interval") ) { |e| Rubyfocus::ReviewPeriod.from_string(e.inner_html) }
44
+ conditional_set(:last_review, p.at_xpath("xmlns:last-review")) { |e| Time.parse e.inner_html }
45
+ conditional_set(:status, p.at_xpath("xmlns:status")) { |e| e.inner_html.to_sym }
46
+ end
47
+
48
+ alias_method :singleton?, :singleton
49
+
50
+ private
51
+ def inspect_properties
52
+ super + %w(singleton review_interval last_review)
53
+ end
54
+
55
+ public
56
+
57
+ #-------------------------------------------------------------------------------
58
+ # Convenience methods
59
+
60
+ # Status methods
61
+ def active?; status == :active; end
62
+ def on_hold?; status == :inactive; end
63
+ def completed?; status == :done; end
64
+ def dropped?; status == :dropped; end
65
+
66
+ #---------------------------------------
67
+ # Convert to a task
68
+ def to_task
69
+ t = Rubyfocus::Task.new(self.document)
70
+ instance_variables.each do |ivar|
71
+ setter = ivar.to_s.gsub(/^@/,"") + "="
72
+ t.send(setter, self.instance_variable_get(ivar)) if t.respond_to?(setter)
73
+ end
74
+ t
75
+ end
76
+ end
@@ -0,0 +1,13 @@
1
+ class Rubyfocus::RankedItem < Rubyfocus::NamedItem
2
+ attr_accessor :rank
3
+
4
+ def apply_xml(n)
5
+ super(n)
6
+ conditional_set(:rank, n.at_xpath("xmlns:rank")){ |e| e.inner_html.to_i }
7
+ end
8
+
9
+ private
10
+ def inspect_properties
11
+ super + %w(rank)
12
+ end
13
+ end
@@ -0,0 +1,17 @@
1
+ class Rubyfocus::Setting < Rubyfocus::Item
2
+ include Rubyfocus::Parser
3
+ def self.matches_node?(node)
4
+ return (node.name == "xmlns:plist")
5
+ end
6
+
7
+ attr_accessor :value
8
+
9
+ def apply_xml(n)
10
+ super(n)
11
+ conditional_set(:value, n.at_xpath("xmlns:plist").children.first){ |e| Rubyfocus::XMLTranslator.parse(e) }
12
+ end
13
+
14
+ def inspect
15
+ form_inspector(:id, :name, :value, :added, :modified)
16
+ end
17
+ end
@@ -0,0 +1,133 @@
1
+ class Rubyfocus::Task < Rubyfocus::RankedItem
2
+ include Rubyfocus::Parser
3
+ def self.matches_node?(node)
4
+ return (node.name == "task")
5
+ end
6
+
7
+ # Inherits from RankedItem:
8
+ # * rank
9
+ # Inherits from NamedItem:
10
+ # * name
11
+ # Inherits from Item:
12
+ # * id
13
+ # * added
14
+ # * modified
15
+ # * document
16
+
17
+ attr_accessor :note, :flagged, :order, :start, :due, :completed
18
+ idref :container, :context
19
+
20
+ def initialize(document, n=nil)
21
+ @order = :sequential
22
+ @flagged = false
23
+ super(document,n)
24
+ end
25
+
26
+ def apply_xml(n)
27
+ super(n)
28
+ conditional_set(:container_id, n.at_xpath("xmlns:task") || n.at_xpath("xmlns:project/xmlns:folder")){ |e| e["idref"] }
29
+
30
+ conditional_set(:context_id, n.at_xpath("xmlns:context")) { |e| e["idref"] }
31
+ conditional_set(:note, n.at_xpath("xmlns:note")) { |e| e.inner_html.strip }
32
+ conditional_set(:order, n.at_xpath("xmlns:order")) { |e| e.inner_html.to_sym }
33
+ conditional_set(:flagged, n.at_xpath("xmlns:flagged")) { |e| e.inner_html == "true" }
34
+ conditional_set(:start, n.at_xpath("xmlns:start")) { |e| Time.parse(e.inner_html) }
35
+ conditional_set(:due, n.at_xpath("xmlns:due")) { |e| Time.parse(e.inner_html) }
36
+ conditional_set(:completed, n.at_xpath("xmlns:completed")){ |e| Time.parse(e.inner_html) }
37
+ end
38
+
39
+ # Convenience methods
40
+ def completed?; !self.completed.nil?; end
41
+ alias_method :flagged?, :flagged
42
+
43
+ # Collect all child tasks. If child tasks have their own subtasks, will instead fetch those.
44
+ def tasks
45
+ @tasks ||= if self.id.nil?
46
+ []
47
+ else
48
+ t_arr = document.tasks.select(container_id: self.id)
49
+ i = 0
50
+ while i < t_arr.size
51
+ task = t_arr[i]
52
+ if task.has_subtasks?
53
+ t_arr += t_arr.delete_at(i).tasks
54
+ else
55
+ i += 1
56
+ end
57
+ end
58
+ t_arr
59
+ end
60
+ end
61
+
62
+ # Collect only immediate tasks: I don't care about this subtasks malarky
63
+ def immediate_tasks
64
+ document.tasks.select(container_id: self.id)
65
+ end
66
+
67
+ # The first non-completed task, determined by order
68
+ def next_available_task
69
+ nat_candidate = immediate_tasks.select{ |t| !t.completed? }.sort_by(&:rank).first
70
+ if nat_candidate.has_subtasks?
71
+ nat_candidate.next_available_task
72
+ else
73
+ nat_candidate
74
+ end
75
+ end
76
+
77
+ # A list of all tasks that you can take action on. Actionable tasks
78
+ # are tasks that are:
79
+ # * not completed
80
+ # * not blocked (as part of a sequential project or task group)
81
+ # * not due to start in the future
82
+ def actionable_tasks
83
+ @actionable_tasks ||= next_tasks.select{ |t| !t.deferred? }
84
+ end
85
+
86
+ # A list of all tasks that are not blocked.
87
+ def next_tasks
88
+ @next_tasks ||= incomplete_tasks.select{ |t| !t.blocked? }
89
+ end
90
+
91
+ # A list of all tasks that aren't complete
92
+ def incomplete_tasks
93
+ @incomplete_tasks ||= tasks.select{ |t| !t.completed? }
94
+ end
95
+
96
+ # Are there any tasks on this project which aren't completed?
97
+ def tasks_remain?
98
+ tasks.any?{ |t| t.completed.nil? }
99
+ end
100
+
101
+ # Does this task have any subtasks?
102
+ def has_subtasks?
103
+ tasks.size > 0
104
+ end
105
+
106
+ # Can we only start this task at some point in the future?
107
+ def deferred?
108
+ start && start > Time.now
109
+ end
110
+
111
+ # Can we attack this task, or does its container stop that happening?
112
+ def blocked?
113
+ container && (container.order == :sequential) && (container.next_available_task != self)
114
+ end
115
+
116
+ #---------------------------------------
117
+ # Conversion methods
118
+
119
+ # Convert the task to a project
120
+ def to_project
121
+ p = Rubyfocus::Project.new(self.document)
122
+ instance_variables.each do |ivar|
123
+ setter = ivar.to_s.gsub(/^@/,"") + "="
124
+ p.send(setter, self.instance_variable_get(ivar)) if p.respond_to?(setter)
125
+ end
126
+ p
127
+ end
128
+
129
+ private
130
+ def inspect_properties
131
+ super + %w(note container_id context_id order flagged start due completed)
132
+ end
133
+ end
@@ -0,0 +1,17 @@
1
+ # A location file. Really a collection of properties and initializer.
2
+ class Rubyfocus::Location
3
+ attr_accessor :name, :latitude, :longitude, :radius, :notification_flags
4
+
5
+ def initialize(n)
6
+ @notification_flags = 0
7
+ self.name = n["name"]
8
+ self.latitude = n["latitude"].to_f
9
+ self.longitude = n["longitude"].to_f
10
+ self.radius = n["radius"].to_i
11
+ self.notification_flags = n["notificationFlags"].to_i
12
+ end
13
+
14
+ def inspect
15
+ %|#<Rubyfocus::Location latitude="#{self.latitude}" longitude="#{self.longitude}">|
16
+ end
17
+ end
@@ -0,0 +1,142 @@
1
+ # The patch class represents a text-file patch, storing update, delete, and creation operations.
2
+ # It should also be able to apply itself to an existing document.
3
+ class Rubyfocus::Patch
4
+ # The fetcher this patch belongs to. We mainly use this to work out how to fetch content for the patch proper
5
+ attr_accessor :fetcher
6
+
7
+ # Operations to be performed on a document
8
+ attr_accessor :update, :delete, :create
9
+
10
+ # The file the patch loads from
11
+ attr_accessor :file
12
+
13
+ # These record the transformation in terms of patch ID values.
14
+ attr_accessor :from_ids, :to_id
15
+
16
+ # The time the file was submitted
17
+ attr_accessor :time
18
+
19
+ # By default we initialize patches from a file. To initialize from a string, use the .from_string method.
20
+ # This class will lazily load data from the file proper
21
+ def initialize(fetcher=nil, file=nil)
22
+ @fetcher = fetcher
23
+ @file = file
24
+ @update = []
25
+ @create = []
26
+ @delete = []
27
+
28
+ if file
29
+ if File.basename(file) =~ /^(\d+)=(.*)\./
30
+ time_string = $1
31
+ self.time = if (time_string == "00000000000000")
32
+ Time.at(0)
33
+ else
34
+ Time.parse(time_string)
35
+ end
36
+
37
+ ids = $2.split("+")
38
+ self.to_id = ids.pop
39
+ self.from_ids = ids
40
+ else
41
+ raise ArgumentError, "Constructed patch from a malformed patch file: #{file}."
42
+ end
43
+ end
44
+ end
45
+
46
+ # Load from a string.
47
+ def self.from_string(fetcher, str)
48
+ n = new(fetcher)
49
+ n.load_data(str)
50
+ n
51
+ end
52
+
53
+ # Loads data from the file. Optional argument +str+ if you want to supply your own data,
54
+ # otherwise will load file data
55
+ def load_data(str=nil)
56
+ return if @data_loaded
57
+ @data_loaded = true
58
+
59
+ str ||= fetcher.patch(self.file)
60
+ doc = Nokogiri::XML(str)
61
+ doc.root.children.select{ |n| !n.text?}.each do |child|
62
+ case child["op"]
63
+ when "update"
64
+ @update << child
65
+ when "delete"
66
+ @delete << child
67
+ when "reference" # Ignore!
68
+ when nil
69
+ @create << child
70
+ else
71
+ raise RuntimeError, "Rubyfocus::Patch encountered unknown operation type #{child["op"]}."
72
+ end
73
+ end
74
+ end
75
+
76
+ # Update, delete and create methods
77
+ def update; load_data; @update; end
78
+ def delete; load_data; @delete; end
79
+ def create; load_data; @create; end
80
+
81
+ # Can we apply this patch to a given document?
82
+ def can_patch?(document)
83
+ self.from_ids.include?(document.patch_id)
84
+ end
85
+
86
+ # Apply this patch to a document. Check to make sure ids match
87
+ def apply_to(document)
88
+ if can_patch?(document)
89
+ apply_to!(document)
90
+ else
91
+ raise RuntimeError, "Patch ID mismatch (patch from_ids: [#{self.from_ids.join(", ")}], document.patch_id: #{document.patch_id}"
92
+ end
93
+ end
94
+
95
+ # Apply this patch to a document. Who needs error checking amirite?
96
+ def apply_to!(document)
97
+ # Updates modify elements
98
+ self.update.each do |node|
99
+ elem = document[node["id"]]
100
+
101
+ # Tasks can become projects and v.v.: check this.
102
+ if [Rubyfocus::Task, Rubyfocus::Project].include?(elem.class)
103
+ should_be_project = (node.at_xpath("xmlns:project") != nil)
104
+ if (elem.class == Rubyfocus::Project) && !should_be_project
105
+ elem.document = nil # Remove this from current document
106
+ elem = elem.to_task # Convert to task
107
+ elem.document = document # Insert again!
108
+ elsif (elem.class == Rubyfocus::Task) && should_be_project
109
+ elem.document = nil # Remove this from current document
110
+ elem = elem.to_project # Convert to task
111
+ elem.document = document # Insert again!
112
+ end
113
+ end
114
+
115
+ elem.apply_xml(node) if elem
116
+ end
117
+
118
+ # Deletes remove elements
119
+ self.delete.each do |node|
120
+ document.remove_element(node["id"])
121
+ end
122
+
123
+ # Creates make new elements
124
+ self.create.each do |node|
125
+ if Rubyfocus::Parser.parse(document, node).nil?
126
+ raise RuntimeError, "Encountered unparsable XML during patch reading: #{node}."
127
+ end
128
+ end
129
+
130
+ # Modify current patch_id to show new value
131
+ document.patch_id = self.to_id
132
+ end
133
+
134
+ # String representation
135
+ def to_s
136
+ if from_ids.size == 1
137
+ "(#{from_ids.first} -> #{to_id})"
138
+ else
139
+ "([#{from_ids.join(", ")}] -> #{to_id})"
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,43 @@
1
+ # The ReviewPeriod represents a review period used with Projects in OmniFocus
2
+ # The ReviewPeriod is made up of three sections:
3
+ # * The precursor symbol (~ or @, use unknown)
4
+ # * A numerical "size"
5
+ # * A unit ([d]ays, [w]eeks, [m]onths or [y]ears)
6
+
7
+ class Rubyfocus::ReviewPeriod
8
+ attr_accessor :size, :unit
9
+
10
+ ALLOWED_UNITS = %i(days weeks months years)
11
+
12
+ def self.from_string(str)
13
+ if str =~ /^[@~]?(\d+)([a-z])$/
14
+ size = $1.to_i
15
+ unit = {"d" => :days, "w" => :weeks, "m" => :months, "y" => :years}[$2]
16
+ new(size: size, unit: unit)
17
+ else
18
+ raise ArgumentError, "Unrecognised review period format: \"#{str}\"."
19
+ end
20
+ end
21
+
22
+ def initialize(size:0, unit: :months)
23
+ self.size = size
24
+ self.unit = unit
25
+ end
26
+
27
+ def unit= value
28
+ raise ArgumentError, "Tried to set ReviewPeriod.unit to invalid value \"#{value}\"." unless ALLOWED_UNITS.include?(value)
29
+ @unit = value
30
+ @short_unit = nil
31
+ end
32
+
33
+ def short_unit
34
+ @short_unit ||= @unit.to_s[0]
35
+ end
36
+
37
+ def to_s
38
+ "#{size}#{short_unit}"
39
+ end
40
+
41
+ alias_method :inspect, :to_s
42
+ alias_method :to_serial, :to_s
43
+ end
@@ -0,0 +1,36 @@
1
+ module Rubyfocus::XMLTranslator
2
+ class << self
3
+ VALID_NODE_NAMES = %w(string true false integer array)
4
+
5
+ # Actual parsing method
6
+ def parse(node)
7
+ method_name = node.name
8
+ if VALID_NODE_NAMES.include?(method_name)
9
+ self.send(method_name, node)
10
+ else
11
+ raise RuntimeError, "Does not recognise node type: #{method_name}."
12
+ end
13
+ end
14
+
15
+ # Individual parsing methods
16
+ def string(node)
17
+ node.inner_html
18
+ end
19
+
20
+ def true(node)
21
+ true
22
+ end
23
+
24
+ def false(node)
25
+ false
26
+ end
27
+
28
+ def integer(node)
29
+ node.inner_html.to_i
30
+ end
31
+
32
+ def array(node)
33
+ node.children.map{ |child| parse(child) }
34
+ end
35
+ end
36
+ end
data/lib/rubyfocus.rb ADDED
@@ -0,0 +1,16 @@
1
+ require "time"
2
+ require "nokogiri"
3
+ require "zip"
4
+ require "yaml"
5
+ require "httparty"
6
+
7
+ module Rubyfocus; end
8
+ # Require library files
9
+ Dir[File.join(__dir__, "rubyfocus/includes/*")].each{ |f| require f }
10
+ %w(fetcher local_fetcher oss_fetcher).each{ |f| require File.join(__dir__, "rubyfocus/fetchers", f) }
11
+ Dir[File.join(__dir__, "rubyfocus/*.rb")].each{ |f| require f }
12
+
13
+ # Need to load items in a specific order
14
+ %w(item named_item ranked_item task project context folder setting).each do |f|
15
+ require File.join(__dir__, "rubyfocus/items", f)
16
+ end