things-client 0.1.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.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+ doc
21
+
22
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Marcin Bunsch
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,114 @@
1
+ = Things
2
+
3
+ Things is a Ruby API for the popular GTD app Things, available on the Mac.
4
+
5
+ == Installation
6
+
7
+ Currently, because this is an alpha version and it's too early for a release, installation must be done manually, like this:
8
+
9
+ git clone http://github.com/marcinbunsch/things
10
+ cd things
11
+ rake check_dependencies:runtime
12
+ rake build
13
+ sudo gem install pkg/things-0.0.1.gem
14
+
15
+ == Usage
16
+
17
+ === Application
18
+
19
+ The Application can be accessed at all times by calling Things::App. It has a few useful methods which will be described below. If you want Things to gain focus and move to the front of the window stack, you can call the Things::App.activate method.
20
+
21
+ === Todos
22
+
23
+ Todos are accessed via the Things::Todo class.
24
+
25
+ ==== Create
26
+
27
+ If you want to create a new Todo, you simple create a new instance of the Things::Todo class, supplying the parameters:
28
+
29
+ Things::Todo.new(:name => 'Take out the garbage')
30
+
31
+ When you're ready to send it to Things, simply call the save method on the instance:
32
+
33
+ todo = Things::Todo.new(:name => 'Take out the garbage')
34
+ todo.save # the Todo will appear in the Inbox
35
+
36
+ If you want to send the Todo to Things during the instantiation process, simply call create:
37
+
38
+ Things::Todo.create(:name => 'Take out the garbage') # the Todo will appear in the Inbox
39
+
40
+ ==== Read
41
+
42
+ ===== Collections
43
+
44
+ If you want to fetch a list of todos, there is a set of collections you can use.
45
+
46
+ The largest one is the collection of all Todos. You fetch it by calling:
47
+
48
+ Things::App.todos
49
+
50
+ It will return an array of Things::Todo instances.
51
+
52
+ If you only want the active (so non-finished) Todos, you can fetch a collection by calling:
53
+
54
+ Things::App.todos.active
55
+
56
+ It will return an array of Things::Todo instances.
57
+
58
+ As the gem will grow, more methods will follow.
59
+
60
+ ===== Single Todos
61
+
62
+ Things::Todo has a few useful methods if you want to fetch a single Todo.
63
+
64
+ The smartest is the Things::Todo.find method, which will accept either a name or an id.
65
+
66
+ Things::Todo.find('Take out the garbage') # => #<Things::Todo:0x115dd84>
67
+ Things::Todo.find('11111111-1111-1111-1111-111111111111') # => #<Things::Todo:0x115dd84>
68
+
69
+ If you want a more targeted approach, you can use Things::Todo.find_by_name or Things::Todo.find_by_id
70
+
71
+ ==== Update
72
+
73
+ To update a Todo, you need to fetch it, change the desired properties and save it. The following example illustrates this:
74
+
75
+ todo = Things::Todo.find('Take out the garbage') # => #<Things::Todo:0x115dd84>
76
+ todo.name = 'Take out the garbage and old boxes'
77
+ todo.save
78
+
79
+ When you open up a Things window, you'll notice that the name has changed.
80
+
81
+ ==== Delete
82
+
83
+ To delete a todo, simply call the delete method. The Todo will bo moved to Trash.
84
+
85
+ todo = Things::Todo.find('Take out the garbage') # => #<Things::Todo:0x115dd84>
86
+ todo.delete
87
+
88
+ == Roadmap
89
+
90
+ Currently Todos are fully supported, which is the core functionality of Todos, but leaves a lot of room for improvement. The first step is to create a unified API for handling AppleScript objects which behave in a similar way.
91
+
92
+ Planned features:
93
+
94
+ * Support for Projects
95
+ * Support for Areas
96
+ * Support for Tags
97
+ * Support for Scheduled Todos
98
+
99
+ == Contribution
100
+
101
+ You're more than welcome to fork and improve this gem. Usual rules:
102
+
103
+ * Fork the project.
104
+ * Make your feature addition or bug fix.
105
+ * Add tests for it. This is important so I don't break it in a
106
+ future version unintentionally.
107
+ * Commit, do not mess with rakefile, version, or history.
108
+ (if you want to have your own version, that is fine but
109
+ bump version in a commit by itself I can ignore when I pull)
110
+ * Send me a pull request.
111
+
112
+ == Copyright
113
+
114
+ Copyright (c) 2010 Marcin Bunsch. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,44 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "things-client"
8
+ gem.summary = %Q{A Ruby client for Things' Applescript API. Things is a GTD app for OS X.}
9
+ gem.email = "marcin@applicake.com"
10
+ gem.homepage = "http://github.com/marcinbunsch/things"
11
+ gem.authors = ["Marcin Bunsch"]
12
+ gem.add_development_dependency "rspec", ">= 1.2.9"
13
+ gem.add_dependency "rb-appscript", ">=0.5.3"
14
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
15
+ end
16
+ rescue LoadError
17
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
18
+ end
19
+
20
+ require 'spec/rake/spectask'
21
+ Spec::Rake::SpecTask.new(:spec) do |spec|
22
+ spec.libs << 'lib' << 'spec'
23
+ spec.spec_files = FileList['spec/**/*_spec.rb']
24
+ end
25
+
26
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
27
+ spec.libs << 'lib' << 'spec'
28
+ spec.pattern = 'spec/**/*_spec.rb'
29
+ spec.rcov = true
30
+ end
31
+
32
+ task :spec => :check_dependencies
33
+
34
+ task :default => :spec
35
+
36
+ require 'rake/rdoctask'
37
+ Rake::RDocTask.new do |rdoc|
38
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
39
+
40
+ rdoc.rdoc_dir = 'rdoc'
41
+ rdoc.title = "things-client #{version}"
42
+ rdoc.rdoc_files.include('README*')
43
+ rdoc.rdoc_files.include('lib/**/*.rb')
44
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,9 @@
1
+ # a small hack - Things references a collection of Todos as to_dos
2
+ # here we make an alias so todos is also possible
3
+ module Appscript #:nodoc:
4
+ class Reference #:nodoc:
5
+ def todos
6
+ to_dos
7
+ end
8
+ end
9
+ end
data/lib/things/app.rb ADDED
@@ -0,0 +1,30 @@
1
+ module Things
2
+ class App < Reference::Base
3
+ extend Appscript
4
+ # get the singleton Application instance
5
+ def self.instance
6
+ reference ||= app('Things')
7
+ end
8
+
9
+ # refresh the Application instance
10
+ def self.instance!
11
+ reference = app('Things')
12
+ end
13
+
14
+ # get a collection of Lists
15
+ def self.lists
16
+ List
17
+ end
18
+
19
+ # get a collection of Todos
20
+ def self.todos
21
+ Collections::Todo
22
+ end
23
+
24
+ # activate the app and bring it to front
25
+ def self.activate
26
+ instance!.activate
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,12 @@
1
+ module Things
2
+ # Things::Todo
3
+ class Area < Reference::Record
4
+
5
+ properties :name
6
+ # identifier is required for creation
7
+ identifier :area
8
+ # collection is used for findings
9
+ collection :areas
10
+
11
+ end
12
+ end
@@ -0,0 +1,42 @@
1
+ module Things
2
+ module Collections
3
+ class Todo
4
+
5
+ class << self
6
+
7
+ # Returns an Appscript Reference to the entire collection of todos
8
+ def reference
9
+ Things::App.instance.todos
10
+ end
11
+
12
+ # these are references and should be stored somewhere else...
13
+ Things::List::DEFAULTS.each do |list|
14
+ class_eval <<-"eval"
15
+ def #{list}
16
+ Things::App.lists.#{list}.todos
17
+ end
18
+ eval
19
+ end
20
+
21
+ # Returns an array of Things::Todo objects
22
+ def all
23
+ reference.get.collect { |todo| Things::Todo.build(todo) }
24
+ end
25
+
26
+ # Get all not completed Todos
27
+ # Note this returns an array of Todos, not references
28
+ # TODO: find a better way of filtering references
29
+ def active
30
+ result = (reference.get - Things::List.trash.todos.get).collect do |todo|
31
+ Things::Todo.build(todo) if todo.completion_date.get.to_s == 'missing_value'
32
+ end
33
+ result.compact
34
+ end
35
+
36
+
37
+
38
+ end
39
+
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,18 @@
1
+ module Things
2
+ class List
3
+ DEFAULTS = [:inbox, :today, :next, :scheduled, :someday, :projects, :logbook, :trash]
4
+ class << self
5
+ def all
6
+ Things::App.instance.lists
7
+ end
8
+
9
+ DEFAULTS.each do |list|
10
+ class_eval <<-"eval"
11
+ def #{list}
12
+ all['#{list.to_s.capitalize}']
13
+ end
14
+ eval
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,12 @@
1
+ module Things
2
+ # Things::Todo
3
+ class Project < Reference::Record
4
+
5
+ properties :name
6
+ # identifier is required for creation
7
+ identifier :project
8
+ # collection is used for findings
9
+ collection :projects
10
+
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ module Things
2
+ # Things::Reference
3
+ module Reference
4
+
5
+ class Base
6
+
7
+ attr_accessor :reference
8
+
9
+
10
+ end
11
+
12
+ end
13
+
14
+ end
@@ -0,0 +1,29 @@
1
+ module Things
2
+ # Things::Reference
3
+ module Reference
4
+
5
+ module Inheritance
6
+
7
+ def inheritable_attributes(*args)
8
+ @inheritable_attributes ||= [:inheritable_attributes]
9
+ @inheritable_attributes += args
10
+ args.each do |arg|
11
+ class_eval %(
12
+ class << self; attr_accessor :#{arg} end
13
+ )
14
+ end
15
+ @inheritable_attributes
16
+ end
17
+
18
+ def inherited(subclass)
19
+ @inheritable_attributes.each do |inheritable_attribute|
20
+ instance_var = "@#{inheritable_attribute}"
21
+ subclass.instance_variable_set(instance_var, instance_variable_get(instance_var))
22
+ end
23
+ end
24
+
25
+ end
26
+
27
+ end
28
+
29
+ end
@@ -0,0 +1,121 @@
1
+ module Things
2
+ # Things::Reference
3
+ module Reference
4
+
5
+ class Record < Base
6
+
7
+ extend Inheritance
8
+
9
+ inheritable_attributes :_properties, :identifier, :collection
10
+
11
+ def self.identifier(name = nil)
12
+ name ? @identifier = name : @identifier
13
+ end
14
+
15
+ def self.collection(name = nil)
16
+ name ? @collection = name : @collection
17
+ end
18
+
19
+ def self.properties(*args)
20
+ @_properties = [] if !@_properties
21
+ if args
22
+ @_properties += args
23
+ @_properties.each do |property|
24
+ attr_writer(property) if !instance_methods.include?(property.to_s)
25
+ if !instance_methods.include?(property.to_s )
26
+ class_eval <<-"eval"
27
+ def #{property}
28
+ if !@#{property}
29
+ fetched = @reference.#{property}.get rescue nil
30
+ @#{property} = fetched if fetched and fetched != :missing_value
31
+ else
32
+ @#{property}
33
+ end
34
+ end
35
+ eval
36
+ end
37
+ end
38
+ end
39
+ end
40
+ # :id_ is always present
41
+ properties :id_
42
+
43
+ def initialize(props = {})
44
+ props.each_pair do |property, value|
45
+ self.send("#{property}=", value)
46
+ end
47
+ end
48
+
49
+ # Returns whether the instance if new or has already been saved
50
+ def new?
51
+ id_.nil?
52
+ end
53
+
54
+ # Save a Todo
55
+ #
56
+ # If a todo is new, it will be created. If not, it will be updated
57
+ def save
58
+ if new?
59
+ properties = {}
60
+ (self.class.properties - [:id_]).each do |property|
61
+ properties[property] = self.send(property) if self.send(property)
62
+ end
63
+ self.reference = Things::App.instance.make(:new => self.class.identifier, :with_properties => properties)
64
+ else
65
+ (self.class.properties - [:id_]).each do |property|
66
+ self.reference.send(property).set(self.send(property)) if self.send(property)
67
+ end
68
+ end
69
+ self
70
+ end
71
+
72
+ # Delete a object
73
+ #
74
+ # This places the object in the trash
75
+ def delete
76
+ Things::App.instance.delete(self.reference) rescue false
77
+ end
78
+
79
+ # create a new object based on that supplied properties and saves it
80
+ def self.create(props)
81
+ new(props).save
82
+ end
83
+
84
+ # build a new instance and link it to the supplied reference
85
+ #
86
+ # Returns a object associated with a reference
87
+ def self.build(reference)
88
+ todo = self.new
89
+ todo.reference = reference
90
+ todo
91
+ end
92
+
93
+ # find a todo by a name or id
94
+ #
95
+ # Returns a Things::Todo object associated with a reference
96
+ def self.find(name_or_id)
97
+ find_by_name(name_or_id) || find_by_id(name_or_id)
98
+ end
99
+
100
+ # find a todo by a name
101
+ #
102
+ # Returns a Things::Todo object associated with a reference
103
+ def self.find_by_name(name)
104
+ reference = Things::App.instance.send(self.collection)[name].get rescue nil
105
+ build(reference) if reference
106
+ end
107
+
108
+ # find a todo by a id
109
+ # Returns a Things::Todo object associated with a reference
110
+ def self.find_by_id(id)
111
+ finder = Appscript.its.id_.eq(id)
112
+ reference = Things::App.instance.send(self.collection)[finder].get rescue nil
113
+ reference = reference.first if reference.is_a?(Array)
114
+ build(reference) if reference
115
+ end
116
+
117
+ end
118
+
119
+ end
120
+
121
+ end
@@ -0,0 +1,8 @@
1
+ module Things
2
+ module Status
3
+ # To do is open.
4
+ OPEN = :open
5
+ # To do has been completed.
6
+ COMPLETED = :completed
7
+ end
8
+ end
data/lib/things/tag.rb ADDED
@@ -0,0 +1,12 @@
1
+ module Things
2
+ # Things::Todo
3
+ class Tag < Reference::Record
4
+
5
+ properties :name
6
+ # identifier is required for creation
7
+ identifier :tag
8
+ # collection is used for findings
9
+ collection :tags
10
+
11
+ end
12
+ end
@@ -0,0 +1,18 @@
1
+ module Things
2
+ # Things::Todo
3
+ class Todo < Reference::Record
4
+
5
+ properties :name, :notes, :completion_date
6
+ # identifier is required for creation
7
+ identifier :to_do
8
+ # collection is used for findings
9
+ collection :todos
10
+
11
+ # Move a todo to a different list <br />
12
+ # Moving to Trash will delete the todo
13
+ def move(list)
14
+ Things::App.instance.move(reference, { :to => list })
15
+ end
16
+
17
+ end
18
+ end
data/lib/things.rb ADDED
@@ -0,0 +1,28 @@
1
+ require 'appscript'
2
+ require "#{File.dirname(__FILE__)}/appscript/reference"
3
+ #
4
+ # Things is a client for the Mac OS X GTD app Things
5
+ #
6
+ # It uses AppleScript to gain access to the Things scripting API
7
+ #
8
+ module Things
9
+ autoload :App, File.dirname(__FILE__) + '/things/app'
10
+ autoload :Todo, File.dirname(__FILE__) + '/things/todo'
11
+ autoload :List, File.dirname(__FILE__) + '/things/list'
12
+ autoload :Status, File.dirname(__FILE__) + '/things/status'
13
+ autoload :Area, File.dirname(__FILE__) + '/things/area'
14
+ autoload :Project, File.dirname(__FILE__) + '/things/project'
15
+ autoload :Tag, File.dirname(__FILE__) + '/things/tag'
16
+
17
+ module Collections
18
+ autoload :Todo, File.dirname(__FILE__) + '/things/collections/todo'
19
+ end
20
+
21
+ module Reference
22
+ autoload :Base, File.dirname(__FILE__) + '/things/reference/base'
23
+ autoload :Inheritance, File.dirname(__FILE__) + '/things/reference/inheritance'
24
+ autoload :Record, File.dirname(__FILE__) + '/things/reference/record'
25
+ end
26
+
27
+ end
28
+
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,10 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'rubygems'
4
+ require 'things'
5
+ require 'spec'
6
+ require 'spec/autorun'
7
+
8
+ Spec::Runner.configure do |config|
9
+
10
+ end
@@ -0,0 +1,43 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe "Things::App" do
4
+
5
+ describe '#instance' do
6
+
7
+ it 'should return an instance of the Things app' do
8
+ Things::App.instance.class.should == Appscript::Application
9
+ end
10
+
11
+ end
12
+
13
+ describe '#activate' do
14
+
15
+ it 'should start the app if it is not running' do
16
+
17
+ # close if running
18
+ #Things::App.instance!.quit if `ps x | grep Things`.include?('Things.app')
19
+ #{}`ps x | grep Things`.should_not match('Things.app')
20
+ #Things::App.instance!.activate
21
+ #{}`ps x | grep Things`.should match('Things.app')
22
+ end
23
+
24
+ end
25
+
26
+ describe '#lists' do
27
+
28
+ it "should return Things::Collections::List" do
29
+ Things::App.lists.should == Things::List
30
+ end
31
+
32
+ end
33
+
34
+ describe '#todos' do
35
+
36
+ it "should return Things::Collections::Todo" do
37
+ Things::App.todos.should == Things::Collections::Todo
38
+ end
39
+
40
+ end
41
+
42
+
43
+ end