├── .gitignore ├── LICENSE.txt ├── ReadMe.md └── things /.gitignore: -------------------------------------------------------------------------------- 1 | # .DS_Store files 2 | .DS_Store 3 | 4 | # don't track .pyc files 5 | *.pyc 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2014 Armin Briegel 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 4 | 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 7 | -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | #ThingsCLITool# 2 | 3 | Command line tool for [Things GTD app](http://culturedcode.com/things) by CulturedCode 4 | 5 | ## usage ## 6 | 7 | ``` 8 | things [list, area or project name] [help|open|show] [add|check|uncheck|cancel|delete|today|next|defer itemname] 9 | ``` 10 | 11 | ## commands ## 12 | 13 | ### listing items### 14 | 15 | ` things `: lists the items in “Today” 16 | 17 | `things listname`: list the items in the list, area or project named `listname` 18 | 19 | `things show` or `things open`: open the "Today" view in Things 20 | 21 | `things listname show`: show the list, area or project named `listname` in Things 22 | 23 | ###list output### 24 | The result of a things command will look like this: 25 | 26 | ``` 27 | > things 'Things CLI Tool' 28 | Things: project Things CLI Tool 29 | *√ flag item as Today 30 | √ uncheck an item 31 | √ add an item 32 | √ check an item 33 | √ cancel an item 34 | √ delete an item 35 | - tag an item 36 | - read tags of an item 37 | *- upload to github 38 | ``` 39 | 40 | The first line is show the name of the list, area or project the command was working on. 41 | 42 | Then the tool will list the items. Completed items have a `√` and open (not completed) items have a `-`. Canceled items have an `x`. 43 | 44 | Items marked for 'Today' will also have an asterisk `*`. 45 | 46 | ### adding items### 47 | 48 | `things add` or `things new`: opens the quick entry panel 49 | 50 | ` things add todo item`: adds a new item named `todo item` to the Inbox 51 | 52 | `things listname add todo item`: adds a new item named `todo item` to the list, area or project named `listname` 53 | 54 | ### checking items### 55 | 56 | `things check someitem`: checks the first item in the "Today" list whose name starts with `someitem` 57 | 58 | `things uncheck someitem`: unchecks the first item in the "Today" list whose name starts with `someitem` 59 | 60 | `things cancel someitem`: cancels the first item in the "Today" list whose name starts with `someitem` 61 | 62 | `things listname check someitem`: checks the first item in the list, area or project named `listname` whose name starts with `someitem` 63 | 64 | `things listname uncheck someitem`: unchecks the first item in the list, area or project named `listname` whose name starts with `someitem` 65 | 66 | `things listname cancel someitem`: cancels the first item in the list, area or project named `listname` whose name starts with `someitem` 67 | 68 | 69 | ### more options### 70 | 71 | `things listname today someitem`: marks the first item in the list, area or project named `listname` for 'Today' 72 | 73 | `things next someitem` or `things defer someitem`: removes the first item in the 'Today' list whose name starts with `someitem` from the Today list (removes the golden star) 74 | 75 | `things listname next someitem` or ` things listname defer someitem`: removes the first item in the list, area or project named `listname` whose name starts with `someitem` from the Today list (removes the golden star) 76 | 77 | `things delete someitem`: deletes the first item in the "Today" list whose name starts with `someitem` 78 | 79 | `things listname delete someitem`: deletes the first item in the liast, area or project named `listname` whose name starts with `someitem` 80 | 81 | ###other commands### 82 | 83 | `things help`: shows usage 84 | 85 | ## installation## 86 | 87 | The things script obviously requires the Things app to be setup and running. It will be happy anywhere in your `$PATH`, or alternatively you can clone the project anywhere and put a symlink in `/usr/local/bin` or elsewhere in your `$PATH`. 88 | 89 | -------------------------------------------------------------------------------- /things: -------------------------------------------------------------------------------- 1 | #!/usr/bin/osascript 2 | 3 | # 4 | # Copyright 2014 Armin Briegel 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | property newline : " 19 | " 20 | 21 | on getlistwithname(listname) 22 | tell application "Things" 23 | if (name of every area) contains listname then 24 | return area listname 25 | else if (name of every list) contains listname then 26 | return list listname 27 | else if (name of every project) contains listname then 28 | return project listname 29 | else 30 | return missing value 31 | end if 32 | end tell 33 | end getlistwithname 34 | 35 | on formatlist(l) 36 | return ((class of l) as string) & space & ((name of l) as string) 37 | end formatlist 38 | 39 | on formattodo(t) 40 | tell application "Things" 41 | if (status of t) is completed then 42 | set tickmark to "√" 43 | else if (status of t) is open then 44 | set tickmark to "-" 45 | else 46 | set tickmark to "x️" 47 | end if 48 | 49 | if my todoisinlist(t, list "Today") then 50 | set today to "*" 51 | else 52 | set today to space 53 | end 54 | 55 | set output to today & tickmark & space & name of t 56 | 57 | if class of t is project then 58 | set output to output & space & "(" & (count of to dos of t) & ")" 59 | end 60 | 61 | return output 62 | end tell 63 | end formattodo 64 | 65 | 66 | on todoisinlist(t, l) 67 | tell app "Things" 68 | return (id of t) is in (id of every to do of l) 69 | end 70 | end 71 | 72 | on listTodos(listname) 73 | tell application "Things" 74 | set target to my getlistwithname(listname) 75 | 76 | if target is missing value then 77 | return "cannot find list named '" & listname & "'" 78 | end if 79 | 80 | (*if (name of every area) contains listname then 81 | set target to area listname 82 | set listtype to "Area" 83 | else if (name of every list) contains listname then 84 | set target to list listname 85 | set listtype to "List" 86 | else if (name of every project) contains listname then 87 | set target to project listname 88 | set listtype to "Project" 89 | else 90 | return "cannot find list or project '"&listname&"'" 91 | end*) 92 | 93 | set output to "Things: " & my formatlist(target) & newline 94 | 95 | if count of to dos of target is greater than 0 then 96 | repeat with x in (every to do of target) 97 | if not (my todoisinlist(x, list "Trash") or my todoisinlist(x, list "Logbook")) then 98 | set output to output & my formattodo(x) & newline 99 | end 100 | end repeat 101 | else 102 | set output to output & "no items" & newline 103 | end 104 | end tell 105 | return output 106 | end listTodos 107 | 108 | on addTodoToList(t, l) 109 | tell application "Things" 110 | set target to my getlistwithname(l) 111 | if target is missing value then 112 | return "cannot find list or project '"&listname&"'" 113 | end 114 | 115 | make new to do with properties {name: t} at target 116 | 117 | my listTodos(l) 118 | end 119 | end 120 | 121 | on setTodoStatusOnList(t, s, l) 122 | tell application "Things" 123 | set target to my getlistwithname(l) 124 | 125 | if target is missing value then 126 | return "cannot find list or project '"&listname&"'" 127 | end 128 | 129 | set the_items to every to do of target whose name starts with t 130 | 131 | if count of the_items is greater than 0 then 132 | set x to first to do of target whose name starts with t 133 | if s is "completed" then 134 | set status of x to completed 135 | else if s is "canceled" then 136 | set status of x to canceled 137 | else if s is "open" then 138 | set status of x to open 139 | end 140 | return my listTodos(l) 141 | else 142 | return "cannot find to do starting with '"&t&"' in "& my formatlist(target) 143 | end 144 | end 145 | end 146 | 147 | 148 | on deleteTodoOnList(t, l) 149 | tell application "Things" 150 | set target to my getlistwithname(l) 151 | 152 | if target is missing value then 153 | return "cannot find list or project '"&listname&"'" 154 | end 155 | 156 | set the_items to every to do of target whose name starts with t 157 | 158 | if count of the_items is greater than 0 then 159 | delete first to do of target whose name starts with t 160 | return my listTodos(l) 161 | else 162 | return "cannot find to do starting with '"&t&"' in "& my formatlist(target) 163 | end 164 | end 165 | end 166 | 167 | on moveTo(t, sourcelistname, destlistname) 168 | tell application "Things" 169 | set sourcelist to my getlistwithname(sourcelistname) 170 | if sourcelist is missing value then 171 | return "cannot find list or project '"&sourcelistname&"'" 172 | end 173 | 174 | set destlist to my getlistwithname(destlistname) 175 | if destlist is missing value then 176 | return "cannot find list or project '"&destlistname&"'" 177 | end 178 | 179 | set the_items to every to do of sourcelist whose name starts with t 180 | 181 | if count of the_items is greater than 0 then 182 | move first to do of sourcelist whose name starts with t to destlist 183 | return my listTodos(sourcelistname) 184 | else 185 | return "cannot find to do starting with '"&t&"' in "& my formatlist(sourcelist) 186 | end 187 | end 188 | 189 | end 190 | 191 | on run argv 192 | set modifyverbs to {"add", "check", "uncheck", "cancel", "delete", "today", "next", "defer"} 193 | 194 | if (count of argv) is 0 then -- no arguments, show Today list 195 | return my listTodos("Today") 196 | end if 197 | 198 | copy item 1 of argv to arg1 199 | 200 | if (count of argv) is 1 then 201 | 202 | -- help 203 | if arg1 is in {"help", "-h", "-help", "--help"} then 204 | return "things Command Line Tool: an CLI interface to the GTD app Things" & newline & ¬ 205 | "usage: things [list, area or project name] [help|open|show] [add|check|uncheck|cancel|delete|today|next|defer itemname]" 206 | end if 207 | 208 | -- if that one argument is open, activate the application 209 | if arg1 is in {"open", "show"} then 210 | tell application "Things" 211 | activate 212 | show list "Today" 213 | end tell 214 | return 215 | end if 216 | 217 | -- add with out other args, open quick entry panel 218 | if arg1 is in {"add", "new"} then 219 | tell app "Things" to show quick entry panel 220 | return 221 | end 222 | 223 | -- interpret argument 1 as list name 224 | return my listTodos(arg1) 225 | end if 226 | 227 | copy item 2 of argv to arg2 228 | 229 | if (count of argv) is 2 then -- verb with no list name, assume default targets 230 | 231 | if arg1 is in modifyverbs then 232 | if arg1 is "add" then 233 | -- default target for add is "Inbox" 234 | return my addTodoToList(arg2, "Inbox") 235 | else if arg1 is "check" then 236 | -- default target for check is "Today" 237 | return my setTodoStatusOnList(arg2, "completed", "Today") 238 | else if arg1 is "uncheck" then 239 | -- default target for check is "Today" 240 | return my setTodoStatusOnList(arg2, "open", "Today") 241 | else if arg1 is "cancel" then 242 | -- default target for delete is "Today" 243 | return my setTodoStatusOnList(arg2, "canceled", "Today") 244 | else if arg1 is "delete" then 245 | -- default target for check is "Today" 246 | return my deleteTodoOnList(arg2, "Today") 247 | else if arg1 is "today" then 248 | -- default target for today is "Today", so do nothing 249 | return my listTodos("Today") 250 | else if arg1 is in {"next", "defer"} then 251 | -- default target for today is "Today", so do nothing 252 | return my moveTo(arg2, "Today", "Next") 253 | end 254 | end 255 | 256 | if arg2 is in {"open", "show"} then 257 | set target to my getlistwithname(arg1) 258 | tell application "Things" 259 | activate 260 | show target 261 | return 262 | end 263 | else 264 | return "do not understand '" & arg2 & "'" 265 | end 266 | end 267 | 268 | 269 | copy item 3 of argv to arg3 270 | 271 | if count of argv is greater than 3 then 272 | -- merge all following arguments 273 | repeat with x in items 4 through -1 of argv 274 | set arg3 to arg3 & space & x 275 | end 276 | end 277 | 278 | if (count of argv) is greater than 2 then 279 | if arg2 is in modifyverbs then 280 | if arg2 is "add" then 281 | return my addTodoToList(arg3, arg1) 282 | else if arg2 is "check" then 283 | return my setTodoStatusOnList(arg3, "completed", arg1) 284 | else if arg2 is "uncheck" then 285 | return my setTodoStatusOnList(arg3, "open", arg1) 286 | else if arg2 is "cancel" then 287 | return my setTodoStatusOnList(arg3, "canceled", arg1) 288 | else if arg2 is "delete" then 289 | return my deleteTodoOnList(arg3, arg1) 290 | else if arg2 is "today" then 291 | return my moveto(arg3, arg1, "Today") 292 | else if arg2 is in {"next", "defer"} then 293 | return my moveTo(arg3, arg1, "Next") 294 | end 295 | else 296 | return "do not understand '" & arg2 & "'" 297 | end 298 | 299 | 300 | end 301 | end run 302 | --------------------------------------------------------------------------------