├── doc └── screens │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ └── 8.png ├── resources ├── clock.png ├── dmg-bg.png ├── clock.ai.zip ├── dmg-bg.ai.zip ├── icon.ai.zip ├── icon_128.icns ├── icon_128.png ├── icon_16.png ├── icon_64.png ├── background.jpg ├── clock-inactive.png └── make-icon.sh ├── .gitmodules ├── changelog.txt ├── test └── tracker │ └── test │ └── core.clj ├── .gitignore ├── make-dmg.sh ├── project.clj ├── tasks.sh ├── README.md └── src └── tracker └── core.clj /doc/screens/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkamenarsky/atea/HEAD/doc/screens/1.png -------------------------------------------------------------------------------- /doc/screens/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkamenarsky/atea/HEAD/doc/screens/2.png -------------------------------------------------------------------------------- /doc/screens/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkamenarsky/atea/HEAD/doc/screens/3.png -------------------------------------------------------------------------------- /doc/screens/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkamenarsky/atea/HEAD/doc/screens/4.png -------------------------------------------------------------------------------- /doc/screens/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkamenarsky/atea/HEAD/doc/screens/5.png -------------------------------------------------------------------------------- /doc/screens/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkamenarsky/atea/HEAD/doc/screens/6.png -------------------------------------------------------------------------------- /doc/screens/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkamenarsky/atea/HEAD/doc/screens/7.png -------------------------------------------------------------------------------- /doc/screens/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkamenarsky/atea/HEAD/doc/screens/8.png -------------------------------------------------------------------------------- /resources/clock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkamenarsky/atea/HEAD/resources/clock.png -------------------------------------------------------------------------------- /resources/dmg-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkamenarsky/atea/HEAD/resources/dmg-bg.png -------------------------------------------------------------------------------- /resources/clock.ai.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkamenarsky/atea/HEAD/resources/clock.ai.zip -------------------------------------------------------------------------------- /resources/dmg-bg.ai.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkamenarsky/atea/HEAD/resources/dmg-bg.ai.zip -------------------------------------------------------------------------------- /resources/icon.ai.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkamenarsky/atea/HEAD/resources/icon.ai.zip -------------------------------------------------------------------------------- /resources/icon_128.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkamenarsky/atea/HEAD/resources/icon_128.icns -------------------------------------------------------------------------------- /resources/icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkamenarsky/atea/HEAD/resources/icon_128.png -------------------------------------------------------------------------------- /resources/icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkamenarsky/atea/HEAD/resources/icon_16.png -------------------------------------------------------------------------------- /resources/icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkamenarsky/atea/HEAD/resources/icon_64.png -------------------------------------------------------------------------------- /resources/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkamenarsky/atea/HEAD/resources/background.jpg -------------------------------------------------------------------------------- /resources/clock-inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pkamenarsky/atea/HEAD/resources/clock-inactive.png -------------------------------------------------------------------------------- /resources/make-icon.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | makeicns -128 icon_128.png -64 icon_64.png -16 icon_16.png 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "yoursway-create-dmg"] 2 | path = yoursway-create-dmg 3 | url = https://github.com/andreyvit/yoursway-create-dmg.git 4 | 5 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | 1.0.3 2 | 3 | - fix for NPE on empty -times.csv 4 | - grayed out icon when inactive 5 | - fix for .tasks file names starting with a dot 6 | 7 | -------------------------------------------------------------------------------- /test/tracker/test/core.clj: -------------------------------------------------------------------------------- 1 | (ns tracker.test.core 2 | (:use [tracker.core]) 3 | (:use [clojure.test])) 4 | 5 | (deftest ttname-test 6 | (is (= ".tasks-times.csv" (ttname ".tasks"))) 7 | (is (= "tasks-times.csv" (ttname "tasks.txt")))) 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /pom.xml 2 | *jar 3 | *ai 4 | *class 5 | *jnilib 6 | /jdic-macoc/build 7 | /lib 8 | /classes 9 | /native 10 | /.lein-failures 11 | /checkouts 12 | /.lein-deps-sum 13 | .clojure 14 | .DS_Store 15 | tracker.txt 16 | atea-1.0.0.dmg 17 | /dmg 18 | -------------------------------------------------------------------------------- /make-dmg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | yoursway-create-dmg/create-dmg --volname atea-$1 --volicon resources/icon_128.icns --background resources/background.jpg --window-size 512 512 --icon-size 128 --icon atea.app 150 300 --icon Applications 340 300 atea-$1.dmg dmg 3 | 4 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject atea "1.0.3" 2 | :description "Minimalistic time tracker for MacOS" 3 | :dependencies [[org.clojure/clojure "1.3.0"]] 4 | :native-dependencies [[org.clojars.pka/jdic-macos-tray "0.0.2"]] 5 | :dev-dependencies 6 | [[native-deps "1.0.5"] 7 | [vimclojure/server "2.3.1" :exclusions [org.clojure/clojure]]] 8 | :main tracker.core) 9 | -------------------------------------------------------------------------------- /tasks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | localtask=`ls *.tasks 2> /dev/null | head -n 1` 4 | 5 | if [[ -n $localtask ]]; then 6 | echo "{:file \"`pwd`/${localtask}\"}" > ~/.atea 7 | exit 8 | fi 9 | 10 | if [[ `pwd` =~ .*/(.*) ]]; then 11 | home=`cd ~; pwd` 12 | tdir="${home}/Dropbox/tasks" 13 | 14 | mkdir -p $tdir 15 | 16 | # update .atea configuration 17 | tasks="${tdir}/${BASH_REMATCH[1]}.tasks" 18 | echo "{:file \"${tasks}\"}" > ~/.atea 19 | 20 | if [[ $1 == -o ]]; then 21 | touch $tasks 22 | open $tasks 23 | fi 24 | fi 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Atea 4 | 5 | [Atea](https://github.com/downloads/pkamenarsky/atea/atea-1.0.3.dmg) is a minimalistic text file based menu bar time tracker for MacOS (get it [here](https://github.com/downloads/pkamenarsky/atea/atea-1.0.3.dmg)). 6 | 7 | There are a lot of great task managers out there - [Fogbugz](http://www.fogcreek.com/fogbugz/), [Pivotal](http://www.pivotaltracker.com/), [Lighthouse](http://lighthouseapp.com/) and [Trello](https://trello.com/) among others. So why yet another? 8 | 9 | If you are like me and find yourself in a situation where you want to *quickly* write down a task, bug or an idea you just thought of, more often than not you end up opening up your favorite text editor and saving a `TODO.txt` somewhere. At first it contains 3 or 4 entries; then it starts to grow - and you have to invent a custom DSL just so you can track priorities, projects or time. 10 | 11 | Even though a text file based system doesn't scale well (or at all) beyond a single person, it has one unbeatable advantage over web-interface based task management tools - locally editing and reordering tasks is *much* easier and faster, especially with editors like [vim](http://www.vim.org/) or [Emacs](http://www.gnu.org/software/emacs/). 12 | 13 | ## Task entry 14 | 15 | Entering a new task in Atea is just as easy as entering a new line in a text file and saving: 16 | 17 | ![](https://github.com/pkamenarsky/atea/raw/master/doc/screens/1.png) 18 | 19 | Add more tasks: 20 | 21 | ![](https://github.com/pkamenarsky/atea/raw/master/doc/screens/2.png) 22 | 23 | Now comes the interesting part; prioritizing something is just an empty line away: 24 | 25 | ![](https://github.com/pkamenarsky/atea/raw/master/doc/screens/3.png) 26 | 27 | But what if the need arises to subdivide tasks into projects (or modules)? Just add an optional `[Project]` in front of a task; no qualifier stands for `[Default]`: 28 | 29 | ![](https://github.com/pkamenarsky/atea/raw/master/doc/screens/4.png) 30 | 31 | Done with something? Just delete it: 32 | 33 | ![](https://github.com/pkamenarsky/atea/raw/master/doc/screens/5.png) 34 | 35 | Completing all tasks of a given priority has the beneficial side-effect of pushing up all other tasks. 36 | 37 | Lines starting with a whitespace character are ignored; this allows for easy "note taking": 38 | 39 | ![](https://github.com/pkamenarsky/atea/raw/master/doc/screens/6.png) 40 | 41 | ## Time tracking 42 | 43 | Tracking time allows you to bill your clients more accurately, improve resource allocation by comparing estimates with actual times spent or just get a clear picture of what you have been doing the last couple of months. 44 | 45 | To start working on a task, just click it: 46 | 47 | ![](https://github.com/pkamenarsky/atea/raw/master/doc/screens/7.png) 48 | 49 | When you are done, stop working: 50 | 51 | ![](https://github.com/pkamenarsky/atea/raw/master/doc/screens/8.png) 52 | 53 | If you want you can append an optional estimate to any given task: 54 | 55 | Make something to eat - 5m 56 | 57 | Minutes (`m`), hours (`h`) and days (`d`) are supported. 58 | 59 | Times and estimates are saved in a separate csv file in plain text; this allows for easy data analysis by [combining](http://reallylongword.org/sedawk/) common Unix tools like [awk](http://www.grymoire.com/Unix/Awk.html) or [sed](http://www.ibm.com/developerworks/linux/library/l-sed1/index.html). 60 | 61 | ## Configuration & files 62 | 63 | Atea automatically creates an `~/.atea` file in the user home folder with the following contents: 64 | 65 | { 66 | :file "/Users/.../tasks.txt" 67 | } 68 | 69 | This is where the current task file resides; you can change it to whatever you desire. Tracked times are stored in a file named `...-times.csv`, depending on the main tasks file name. 70 | 71 | ## Dropbox integration 72 | 73 | Since tasks are stored in simple text files, [Dropbox](http://www.dropbox.com/) can be used for backup and synchronization. 74 | 75 | For example, you can start working on a task on you laptop, then move over to you main machine and have your tasks along with the current worked time automatically updated there; just set up the configuration file to point to a file in your Dropbox folder. 76 | 77 | ## Multiple projects 78 | 79 | When invoked the `tasks.sh` script takes the name of the current directory and updates the configuration to point to `/Users/.../Dropbox/tasks/.tasks`. 80 | 81 | You may associate your favorite editor to open all files with the `.tasks` extension; when the `-o` option is supplied the script automatically invokes `open` on the current tasks file, i.e. fires up the associated text editor. 82 | 83 | Place `tasks.sh` somewhere in yout path (for example `/usr/share/bin`); then, in order to edit and switch to a Dropbox backed per-project tasks file, just type: 84 | 85 | $ tasks.sh -o 86 | 87 | ## User contributions 88 | 89 | Check out [atea-contrib](https://github.com/pkamenarsky/atea-contrib), a separate repository containing user contributions for Atea. 90 | 91 | ## Building 92 | 93 | If you just want to try out Atea, you can grab the pre-packaged `dmg` file [here](https://github.com/downloads/pkamenarsky/atea/atea-1.0.3.dmg). 94 | 95 | In order to build, you'll need [Leiningen](https://github.com/technomancy/leiningen), Clojure's build and dependency tool; first you have to make a standalone jar: 96 | 97 | lein deps 98 | lein native-deps 99 | cp native/libtray.jnilib . 100 | lein uberjar 101 | 102 | At this point you can just start the jar and it will work. 103 | 104 | If you want a native app, you'll need to use Apple's Jar Bundler to create `atea.app` out of the generated standalone jar (this can be automated though, patches welcome). In order to hide the dock icon, edit the `atea.app/Contents/Info.plist` file and add the following key: 105 | 106 | LSUIElement 107 | 1 108 | 109 | Create a `dmg` folder and place the `atea.app` bundle there: 110 | 111 | mkdir dmg 112 | mv atea.app dmg 113 | ln -s /Applications dmg/Applications 114 | 115 | Update the `create-dmg` submodule: 116 | 117 | git submodule init 118 | git submodule update 119 | 120 | And then finally run the `make-dmg` script: 121 | 122 | ./make-dmg 123 | 124 | This should create a deployable `dmg` file. 125 | 126 | ## License 127 | 128 | Copyright (C) 2012 Philip Kamenarsky 129 | 130 | Distributed under the Eclipse Public License, the same as Clojure. 131 | -------------------------------------------------------------------------------- /src/tracker/core.clj: -------------------------------------------------------------------------------- 1 | (ns tracker.core 2 | (:require [clojure.string :as string]) 3 | (:import org.jdesktop.jdic.tray.internal.impl.MacSystemTrayService) 4 | (:import org.jdesktop.jdic.tray.internal.impl.MacTrayIconService) 5 | (:gen-class)) 6 | 7 | (defn load-icon [name] 8 | (javax.swing.ImageIcon. (javax.imageio.ImageIO/read (clojure.java.io/resource name)))) 9 | 10 | (defn get-tray [] 11 | (MacSystemTrayService/getInstance)) 12 | 13 | (defn create-menu [] 14 | (MacTrayIconService.)) 15 | 16 | (defn action [f] 17 | (reify java.awt.event.ActionListener 18 | (actionPerformed 19 | [this event] (f)))) 20 | 21 | ; Item management ---------------------------------------------------------- 22 | 23 | (defn now [] 24 | (.getTime (java.util.Date.))) 25 | 26 | (defn to-mins [msec] 27 | (quot msec 60000)) 28 | 29 | (defn to-str [mins] 30 | (format "[%02d:%02d]" (quot mins 60) (mod mins 60))) 31 | 32 | (defn to-time [x unit] 33 | (Math/round (* (Float. x) ({"m" 1 "h" 60 "d" 1440} unit)))) 34 | 35 | (defn key-task [task] 36 | (str (:project task) (:description task))) 37 | 38 | (defn parition-items [items] 39 | ; first group by priority into a {pri item} map, 40 | ; then group the first 10 items of every priority by project 41 | (reduce (fn [a [pri item]] 42 | (assoc a pri (group-by :project (take 10 item)))) 43 | {} 44 | (group-by :priority items))) 45 | 46 | (defn update-items [file menu items active actfn deactfn] 47 | ; project / priority section functions 48 | (let [add-section 49 | (fn [add-sep? title sec-items] 50 | (if add-sep? 51 | (.addSeparator menu)) 52 | (.addItem menu title nil) 53 | (.addSeparator menu) 54 | (doseq 55 | [item sec-items] 56 | (.addItem 57 | menu 58 | (if (= (key-task item) (key-task active)) 59 | (str "➡ " (:description item)) ;◆✦● 60 | (:description item)) 61 | (action #(actfn (assoc item :since (now))))))) 62 | 63 | add-priority 64 | (fn [add-sep? priority prjs] 65 | (add-section 66 | add-sep? 67 | (str "Priority " (inc priority) " - " (key (first prjs))) 68 | (val (first prjs))) 69 | (doseq [[prj items] (next prjs)] (add-section true prj items)))] 70 | 71 | ; remove old items 72 | (doseq [index (range (.getItemCount menu))] (.removeItem menu 0)) 73 | 74 | ; add "now working" section 75 | (when active 76 | (let [stime (to-mins (- (now) (:since active)))] 77 | (.addItem 78 | menu 79 | (str "Session: " 80 | (to-str stime) 81 | " - Sum: " 82 | (to-str (+ (:time active) stime))) 83 | nil) 84 | (.addSeparator menu) 85 | (.addItem menu "Stop work" (action #(deactfn))) 86 | (.addSeparator menu))) 87 | 88 | ; add items sorted by priority and project 89 | (let [part-items (sort (parition-items items))] 90 | (if (first part-items) 91 | (add-priority false (key (first part-items)) (val (first part-items))) 92 | (.addItem menu (str "No tasks in " file) nil)) 93 | (doseq [[pri prjs] (next part-items)] (add-priority true pri prjs))) 94 | 95 | ; quit menu item 96 | (.addSeparator menu) 97 | (.addItem menu "Quit Atea" (action #((deactfn) (System/exit 0)))))) 98 | 99 | ; IO ----------------------------------------------------------------------- 100 | 101 | (defn escape [s] 102 | (string/replace s ";" "\\;")) 103 | 104 | (defn unescape [s] 105 | (string/replace s "\\;" ";")) 106 | 107 | ; tracked tasks 108 | (defn parse-status [line] 109 | (let [match (re-matches #"# Working on \"\[(.*)\]\" - \"(.*)\" since (\d*) for (\d*)" line)] 110 | (when match 111 | {:project (match 1) 112 | :description (match 2) 113 | :since (Long. (match 3)) 114 | :time (Long. (match 4))}))) 115 | 116 | (defn write-status [active] 117 | (str "# Working on \"[" (:project active) 118 | "]\" - \"" (:description active) 119 | "\" since " (:since active) 120 | " for " (:time active))) 121 | 122 | (defn parse-ttask [line] 123 | (let [match (re-matches #"\[(.*)\];(.*);(\d*);(\d*);(\d*)" line)] 124 | (when match 125 | {:project (unescape (match 1)) 126 | :description (unescape (match 2)) 127 | :priority (Long. (match 3)) 128 | :time (Long. (match 4)) 129 | :estimate (Long. (match 5))}))) 130 | 131 | (defn write-ttask [ttask] 132 | (apply format "[%s];%s;%d;%d;%d" 133 | (escape (:project ttask)) 134 | (escape (:description ttask)) 135 | (map ttask [:priority :time :estimate]))) 136 | 137 | (defn load-ttasks [file] 138 | (try 139 | (let [lines (filter (comp not empty?) (string/split-lines (slurp file))) 140 | status (when (first lines) (parse-status (first lines)))] 141 | (if status 142 | {:active status 143 | :ttasks (map parse-ttask (next lines))} 144 | {:active nil 145 | :ttasks (map parse-ttask lines)})) 146 | (catch java.io.FileNotFoundException e 147 | {:active nil 148 | :ttasks []}))) 149 | 150 | ; tasks 151 | (defn parse-task [line] 152 | ; format is: [project] description - estimate 153 | ; project and estimate are optional 154 | (let [match (re-matches #"(\[(.*)\])?\s*(.*?)(\s*-\s*(\d+\.?\d*)([mhd]))?" line)] 155 | {:project (or (match 2) "Default") 156 | :description (match 3) 157 | :estimate (if (match 5) (to-time (match 5) (match 6)) 0) 158 | :time 0})) 159 | 160 | (defn load-tasks [file] 161 | (try 162 | ; filter out all non empty lines starting with one or more whitespaces 163 | (let [lines (filter 164 | #(not (re-matches #"\s+[^\s]+.*" %)) 165 | (string/split-lines (slurp file))) 166 | ; partition by empty or all-whitespace lines 167 | pris (filter 168 | #(not (re-matches #"\s*" (first %))) 169 | (partition-by (partial re-matches #"\s*") lines)) 170 | tasks (zipmap (range (count pris)) pris)] 171 | 172 | ; flatten into maps 173 | (for [[pri items] tasks 174 | task items] (into (parse-task task) {:priority pri :time 0}))) 175 | (catch java.io.FileNotFoundException e []))) 176 | 177 | (defn key-tasks [tasks] 178 | (zipmap (map key-task tasks) tasks)) 179 | 180 | (defn write-ttasks [file tasks ttasks new-active] 181 | (try 182 | (let [active (:active ttasks) 183 | kts (if active 184 | (update-in (key-tasks (:ttasks ttasks)) 185 | [(key-task active) :time] 186 | #(+ % (to-mins (- (now) (:since active))))) 187 | (key-tasks (:ttasks ttasks))) 188 | 189 | ; merge textfile tasks and tracked tasks 190 | tmerged (vals (merge-with (fn [t tt] {:priority (:priority t) 191 | :project (:project t) 192 | :estimate (:estimate t) 193 | :description (:description t) 194 | :time (:time tt)}) 195 | (key-tasks tasks) 196 | kts)) 197 | 198 | ; get active time 199 | tactive (when new-active 200 | (assoc new-active :time (get-in kts [(key-task new-active) :time] 0))) 201 | 202 | ; write lines 203 | lines (map write-ttask tmerged) 204 | content (string/join "\n" (if tactive 205 | (cons (write-status tactive) lines) 206 | lines))] 207 | 208 | (spit file content)) 209 | (catch java.io.FileNotFoundException e nil))) 210 | 211 | ; Track file updates ------------------------------------------------------- 212 | 213 | (defn watch-file [filename interval f] 214 | (let [file (java.io.File. filename) 215 | timestamp (atom 0) 216 | listener (reify java.awt.event.ActionListener 217 | (actionPerformed 218 | [this event] 219 | (if (not= @timestamp (.lastModified file)) 220 | (do 221 | (f) 222 | (reset! timestamp (.lastModified file)))))) 223 | timer (javax.swing.Timer. interval listener)] 224 | (.start timer) 225 | timer)) 226 | 227 | ; main --------------------------------------------------------------------- 228 | 229 | (defn rel-to-home [file] 230 | (str (System/getProperty "user.home") 231 | (java.io.File/separator) 232 | file)) 233 | 234 | (def default-cfg {:file (rel-to-home "tasks.txt")}) 235 | 236 | (defn write-default-cfg [] 237 | (spit (rel-to-home ".atea") (pr-str default-cfg))) 238 | 239 | (defn load-cfg [] 240 | (try 241 | (load-file (rel-to-home ".atea")) 242 | (catch Exception e 243 | (do 244 | (write-default-cfg) 245 | default-cfg)))) 246 | 247 | (defn ttname [tname] 248 | (let [match (re-matches #"(.+)\..*" tname)] 249 | (if match 250 | (str (match 1) "-times.csv") 251 | (str tname "-times.csv")))) 252 | 253 | (defn -main [] 254 | (let [old-file (atom nil) 255 | icon-inactive (load-icon "clock-inactive.png") 256 | icon-active (load-icon "clock.png") 257 | menu (create-menu)] 258 | (.addTrayIcon (get-tray) menu 0) 259 | (.setIcon menu icon-inactive) 260 | (.setActionListener 261 | menu 262 | (action #(let [file (:file (load-cfg)) 263 | tfile (ttname file) 264 | tasks (load-tasks file) 265 | ttasks (load-ttasks tfile)] 266 | 267 | ; if file *name* changed, write out old one first 268 | (when (and @old-file (not= @old-file file)) 269 | ; we presume this is gonna work since it worked last time :) 270 | (write-ttasks (ttname @old-file) 271 | (load-tasks @old-file) 272 | (load-ttasks (ttname @old-file)) 273 | nil)) 274 | 275 | ; update menu 276 | (when tasks 277 | (reset! old-file file) 278 | (update-items file menu tasks (:active ttasks) 279 | (fn [new-active] 280 | (write-ttasks tfile tasks ttasks new-active) 281 | (.setIcon menu icon-active)) 282 | (fn [] 283 | (write-ttasks tfile tasks ttasks nil) 284 | (.setIcon menu icon-inactive))))))) 285 | (Thread/sleep (Long/MAX_VALUE)))) 286 | 287 | --------------------------------------------------------------------------------