31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/todo.txt:
--------------------------------------------------------------------------------
1 | (A) 2015-07-24 switch should be like begin if working on nothing (or add another syntax? or WARN more vehemently that nothing was done!)
2 | (A) 2015-09-04 backups must include minute or even second (now they overwrite if fixing sth immediately)
3 | (A) 2015-07-21 remove sheet info from every command (output only w/ version?)
4 | (B) 2015-10-19 status (and maybe pt) should output local time along w/ UTC
5 | (B) 2015-10-19 print time should include hypothetical length of time; queue into status maybe
6 | (B) 2015-08-01 last status should also show duration
7 | (B) 2015-07-07 status should output when the last end was (if nothing is worked on currently)
8 | (B) 2015-07-07 Add stats command (informing me about DB size)
9 | (C) 2015-07-07 Make a relabel command?
10 | (C) 2015-06-20 Make tests work [tried to get cram running on Windows but it wouldn't; raised issue w/ them; now waiting; {{https://bitbucket.org/brodie/cram/issues/32/windows-usage-instructions#comment-None}} from 2015/06, in 2015/07 still no replies; BATS {{https://blog.engineyard.com/2014/bats-test-command-line-tools}} appears like an alternative but it's unix only as well. aruba in ruby {{https://github.com/cucumber/aruba}} may be crossplatform; doctest may be usable {{https://docs.python.org/2/library/doctest.html}}; parse from inside unit tests {{http://dustinrcollins.com/testing-python-command-line-apps}}, or I stick w/ cram and just run my tests on unix only, travis may be an option for that]
11 | (C) 2015-07-03 split data files after 1 week (or month) to facilitate merges (merging would also be easier in hledger format b/c the lines don't have so many identical starts)
12 | (C) 2015-06-26 ini should only output default parts of the ini not the parts the user has added (as seen when I still had old config settings after upgrading)
13 | (C) 2015-07-06 add command for config editing
14 | (C) 2015-07-01 edit must catch the case when sheet doesn't exist (currently shutil copy just errors out)
15 | (C) 2015-06-24 make sure colorama {{https://pypi.python.org/pypi/colorama}} is initialized properly; test demo on Windows; allow bold only mode (bright in colorama, see demo)
16 | (C) 2015-06-30 use gitpython for backing up sheet instead of copying all the time {{http://stackoverflow.com/questions/1456269/python-git-module-experiences}}
17 | (C) 2015-06-24 let ppl disable color codes
18 | (C) 2015-06-23 make --version work
19 | (C) 2015-06-23 Write a temporary now line to hledger file s.t. ongoing task is reflected? (might be trouble later when adjusting to ledger file entirely; maybe write a temp file that is included from main file via ledger syntax)
20 | (C) 2015-06-20 Adjust README
21 | (C) 2015-06-22 Adjust requirements in setup.py
22 | (C) 2015-06-20 Add conda environment for Windows development (test whether it is usable with fresh environment)
23 | (D) 2015-06-30 consider re-activating ti's editor backup via temp file
24 | (D) 2015-06-20 Adjust usage help
25 | (D) 2015-06-20 Publish to pypi
26 | (D) 2015-06-20 Publish on web site
27 | x (A) 2015-06-24 make finish output a time (and add duration too!)
28 | x (A) 2015-06-23 make ini writing and checking (output current config?) easy
29 | x (A) 2015-06-23 specify working dir instead of sheetname => move hledger output over as well
30 | x (A) 2015-06-22 Add config option for alternative usage folder (want to share over seafile) [can now move sheet to whereever]
31 | x (B) 2015-06-26 add parsing experimentation functionality
32 | x (B) 2015-06-26 "at 9:26" times are treated as if they are in UTC messing up my calculations [fixed w/ pytz and tzlocal]
33 | x (B) 2015-06-24 make times "at 12:23" work for switching [parsedatetime seems to be a tremendous library]
34 | x (B) 2015-06-24 tim hl1 etc should allow additional parameters to be curried, e.g., for filtering
35 | x (B) 2015-06-20 Simplify code further (delele unneeded)
36 | x (B) 2015-06-23 remove fuzzy output (or make crisper; I hate seeing about an hour all the time in status)
37 | x (C) 2015-06-20 Make installing easy [setup.py works on Windows]
38 | x (D) 2015-06-24 add editor config
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/MatthiasKauer/tim)
2 | **Note: I'm in the process of adapting the cram tests to tim; this is difficult on Windows and happens only when I feel like booting up my Linux machine. I am using tim daily already however**
3 |
4 | ## tim in a nutshell
5 | tim provides a command-line interface for recording time logs. Its design goals are the following:
6 |
7 | * Simplicity. If a time tracker tool makes me think for more than 3-5 seconds, I lose my line
8 | of thought and forget what I was doing. For this reason, I loved [ti](https://github.com/sharat87/ti) and simplified it further.
9 | * Stand on the shoulder of giants. All aggregation is handled by [hledger](http://hledger.org). Convenience commands are added to the tim interface.
10 | * Text file storage. Its *your* data. Location of your data can be adjusted in ```~/.tim.ini```. Consider ```tim ini``` if you want to move away from the default location.
11 |
12 | Oh and by the way, the source is a fairly small python script, so if you know
13 | python, you may want to skim over it to get a better feel of how it works.
14 |
15 | The following animation shows the basic commands begin, switch, end.
16 | Data is recorded in json format and can be manually adjusted using any text editor. On my system, vim is assigned for this task.
17 | 
18 |
19 | When calling ```tim hl```, commands are piped to [hledger](http://hledger.org) for aggregation. Hledger must be installed separately which is simple thanks to their single exe binary for Windows and the integration in most Linux package management systems (```sudo apt-get install hledger``` should work, for instance).
20 | The next animation demonstrates how ```tim hl balance``` and the associated ```tim hl1``` (for the data of today) aggregate data. Depth of the data tree can be adjusted, and filtering works as well. Hledger is very powerful in that regard.
21 | Try
22 | ```
23 | tim hl balance --help
24 | ```
25 | to see all options that hledger offers.
26 |
27 | 
28 | Since hledger is primarily an accounting tool, not all its commands are useful for tim. ```hledger balance``` is arguably the most useful. Others I use are
29 | ```
30 | tim hl activity
31 | tim hl print
32 | ```
33 |
34 | ## Installation
35 | ### tim
36 | tim is on PyPI: https://pypi.python.org/pypi/tim-ledger_diary
37 |
38 | Install it via:
39 | ```
40 | pip install tim-ledger_diary
41 | ```
42 |
43 | ### hledger
44 | [hledger](http://hledger.org) must be installed separately. Download the hledger binary for Windows and add it to PATH.
45 |
46 | On Ubuntu, install via
47 | ```
48 | sudo apt-get install hledger
49 | ```
50 | At this point, I don't think you need a specific version. Choose the most recent one on your system and report back if things don't work in that way.
51 | There's a good chance you can also make this tool work with other command-line tools that share the same timelog format like [ledger-cli](http://www.ledger-cli.org/), but I haven't tested that.
52 |
53 |
54 |
55 | ## differences to ti
56 | tim tries to simplify [ti](https://github.com/sharat87/ti) by relying on [hledger](http://hledger.org/) (which must be on your path) for number crunching.
57 |
58 | Biggest changes:
59 |
60 | * hledger omits tasks that are too short. 4min, rounded up to 0.1 h seems to be the cut-off.
61 | * interrupts are gone because the stack is complex; you can call switch if you want to start work on something else. If you enter finish, nothing is automatically started.
62 | * hl command hands over your data to hledger to perform aggregations. [hledger manual](http://hledger.org/manual.html#timelog)
63 | * note is gone.
64 | * tag is gone (for now)
65 |
66 | ## Caveats
67 | ### File size considerations
68 | My tim-sheet grows roughly 2KB / day. That's about 700kB / year. Probably less if I don't track weekends.
69 | Writing line by line the way I am doing it now is starting to get slow already however (at 6KB). hledger itself is significantly faster. As soon as this difference bothers me enough I will switch to storing in hledger format directly s.t. the speed will no longer be an issue.
70 |
71 | ## For developers
72 | ### Python environment installation
73 | #### Windows
74 | We develop using Anaconda with package manager [conda](http://conda.io/).
75 | You can install all packages in our environment (inspect environment.yml beforehand; expect 2-3 min of linking/downloading, probably more if your conda base installation is still very basic or has vastly different packages than mine) using:
76 | ```
77 | conda env create
78 | ```
79 | if it already exists you may have to remove it first.
80 |
81 | * Read on top of environment.yml
82 | * Confirm via ```conda env list```
83 | * Remove ```conda env remove --name ```
84 |
85 | If you feel like updating the environment, run ```conda env export -f environment.yml``` and commit it to the repository.
86 |
87 | *Note*: If you have used the previous bash version of `ti`, which was horribly
88 | tied up to only work on linux, you might notice the lack of *plugins* in this
89 | python version. I am not really missing them, so I might not add them. If anyone
90 | has any interesting use cases for it, I'm willing to consider.
91 |
92 | ## Who?
93 | [ti](https://github.com/sharat87/ti) has been created by Shrikant Sharat
94 | ([@sharat87](https://twitter.com/#!sharat87)).
95 | Adjustments in tim are by [Matthias Kauer](http://matthiaskauer.com/about).
96 | Feel free to open an issue to discuss the program or write an email for other enquiries.
97 |
98 | ## License
99 | MIT
100 |
--------------------------------------------------------------------------------
/tim/timscript.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # encoding: utf-8
3 |
4 | from __future__ import print_function
5 | from __future__ import unicode_literals
6 |
7 | import sys
8 | from datetime import datetime, timedelta, date
9 | from collections import defaultdict
10 | import os
11 | import subprocess
12 | import math
13 | import ConfigParser
14 | import StringIO
15 | import shutil
16 | import json, yaml
17 |
18 | import pytz
19 | import parsedatetime
20 | # http://stackoverflow.com/questions/13218506/how-to-get-system-timezone-setting-and-pass-it-to-pytz-timezone
21 | from tzlocal import get_localzone # $ pip install tzlocal
22 | local_tz = get_localzone()
23 |
24 | from tim import __version__
25 | from tim.coloring import TimColorer
26 |
27 | date_format = '%Y-%m-%dT%H:%M:%SZ'
28 |
29 | class JsonStore(object):
30 | """handles time log in json form"""
31 |
32 | def __init__(self):
33 | cfg_fname = os.path.abspath(os.path.expanduser('~/.tim.ini'))
34 | self.cfg = ConfigParser.SafeConfigParser()
35 |
36 | self.cfg.add_section('tim')
37 | self.cfg.set('tim', 'folder', os.path.abspath(os.path.expanduser('~')))
38 | self.cfg.set('tim', 'editor', "vim")
39 | self.cfg.read(cfg_fname) #no error if not found
40 | self.filename = os.path.abspath(os.path.join(self.cfg.get('tim','folder'), 'tim-sheet.json'))
41 | print("#self.filename: %s" % (self.filename))
42 |
43 | def load(self):
44 | """read from file"""
45 | if os.path.exists(self.filename):
46 | with open(self.filename) as f:
47 | data = json.load(f)
48 |
49 | else:
50 | data = {'work': [], 'interrupt_stack': []}
51 |
52 | return data
53 |
54 | def dump(self, data):
55 | """write data to file"""
56 | with open(self.filename, 'w') as f:
57 | json.dump(data, f, separators=(',', ': '), indent=2)
58 |
59 |
60 | def action_switch(name, time):
61 | action_end(time)
62 | action_begin(name, time)
63 |
64 |
65 | def action_begin(name, time):
66 | data = store.load()
67 | work = data['work']
68 |
69 | if work and 'end' not in work[-1]:
70 | print('You are already working on ' + tclr.yellow(work[-1]['name']) +
71 | '. Stop it or use a different sheet.', file=sys.stderr)
72 | raise SystemExit(1)
73 |
74 | entry = {
75 | 'name': name,
76 | 'start': time,
77 | }
78 |
79 | work.append(entry)
80 | store.dump(data)
81 |
82 | print('Start working on ' + tclr.green(name) + ' at ' + time + '.')
83 |
84 |
85 | def action_printtime(time):
86 | print("You entered '" + time + "' as a test")
87 |
88 |
89 | def action_end(time, back_from_interrupt=True):
90 | ensure_working()
91 |
92 | data = store.load()
93 |
94 | current = data['work'][-1]
95 | current['end'] = time
96 |
97 | start_time = parse_isotime(current['start'])
98 | # print(type(start_time), type(time))
99 | diff = timegap(start_time, parse_isotime(time))
100 | print('You stopped working on ' + tcrl.red(current['name']) + ' at ' + time + ' (total: ' + tclr.bold(diff) + ').')
101 | store.dump(data)
102 |
103 |
104 | def action_status():
105 | ensure_working()
106 | # except SystemExit(1):
107 | # return
108 |
109 | data = store.load()
110 | current = data['work'][-1]
111 |
112 | start_time = parse_isotime(current['start'])
113 | diff = timegap(start_time, datetime.utcnow())
114 |
115 | print('You have been working on {0} for {1}.'
116 | .format(tclr.green(current['name']), diff))
117 |
118 |
119 | def action_hledger(param):
120 | # print("hledger param", param)
121 | data = store.load()
122 | work = data['work']
123 |
124 | # hlfname = os.path.expanduser('~/.tim.hledger')
125 | hlfname = os.path.join( store.cfg.get('tim', 'folder'), '.tim.hledger-temp')
126 | hlfile = open(hlfname, 'w')
127 |
128 | for item in work:
129 | if 'end' in item:
130 | str_on = "i %s %s" % (parse_isotime(item['start']), item['name'])
131 | str_off = "o %s" % (parse_isotime(item['end']))
132 | # print(str_on + "\n" + str_off)
133 |
134 | hlfile.write(str_on + "\n")
135 | hlfile.write(str_off + "\n")
136 | # hlfile.write("\n")
137 |
138 | hlfile.close()
139 |
140 | cmd_list = ['hledger'] + ['-f'] + [hlfname] + param
141 | print("tim executes: " + " ".join(cmd_list))
142 | subprocess.call(cmd_list)
143 |
144 |
145 | def action_ini():
146 | out_str = StringIO.StringIO()
147 |
148 | store.cfg.write(out_str)
149 | print("#this is the ini file for tim - a tiny time keeping tool with hledger in the back")
150 | print("#I suggest you call tim ini > %s to start using this optional config file"
151 | %(os.path.abspath(os.path.expanduser('~/.tim.ini'))))
152 |
153 | print(out_str.getvalue())
154 |
155 |
156 | def action_version():
157 | print("tim version " + __version__)
158 |
159 |
160 | def action_edit():
161 | editor_cfg = store.cfg.get('tim', 'editor')
162 | print(editor_cfg)
163 | if 'EDITOR' in os.environ:
164 | cmd = os.getenv('EDITOR')
165 | if editor_cfg is not "":
166 | cmd = editor_cfg
167 | else:
168 | print("Please set the 'EDITOR' environment variable or adjust editor= in ini file", file=sys.stderr)
169 | raise SystemExit(1)
170 |
171 | bakname = os.path.abspath(store.filename + '.bak-' + date.today().strftime("%Y%m%d"))
172 | shutil.copy(store.filename, bakname)
173 | print("Created backup of main sheet at " + bakname + ".")
174 | print("You must delete those manually! Now begin editing!")
175 | subprocess.check_call(cmd + ' ' + store.filename, shell=True)
176 |
177 |
178 | def ensure_working():
179 | data = store.load()
180 | work_data = data.get('work')
181 | is_working = work_data and 'end' not in data['work'][-1]
182 | if is_working:
183 | return True
184 |
185 | # print(has_data)
186 | if work_data:
187 | last = work_data[-1]
188 | print("For all I know, you last worked on {} from {} to {}".format(
189 | tclr.blue(last['name']), tclr.green(last['start']), tcrl.red(last['end'])),
190 | file=sys.stderr)
191 | # print(data['work'][-1])
192 | else:
193 | print("For all I know, you " + tclr.bold("never") + " worked on anything."
194 | " I don't know what to do.", file=sys.stderr)
195 |
196 | print('See `ti -h` to know how to start working.', file=sys.stderr)
197 | raise SystemExit(1)
198 |
199 |
200 | def to_datetime(timestr):
201 | #Z denotes zulu for UTC (https://tools.ietf.org/html/rfc3339#section-2)
202 | # dt = parse_engtime(timestr).isoformat() + "Z"
203 | dt = parse_engtime(timestr).strftime(date_format)
204 | return dt
205 |
206 |
207 | def parse_engtime(timestr):
208 | #http://stackoverflow.com/questions/4615250/python-convert-relative-date-string-to-absolute-date-stamp
209 | cal = parsedatetime.Calendar()
210 | if timestr is None or timestr is "":\
211 | timestr = 'now'
212 |
213 | #example from here: https://github.com/bear/parsedatetime/pull/60
214 | ret = cal.parseDT(timestr, tzinfo=local_tz)[0]
215 | ret_utc = ret.astimezone(pytz.utc)
216 | # ret = cal.parseDT(timestr, sourceTime=datetime.utcnow())[0]
217 | return ret_utc
218 |
219 |
220 | def parse_isotime(isotime):
221 | return datetime.strptime(isotime, date_format )
222 |
223 |
224 | def timegap(start_time, end_time):
225 | diff = end_time - start_time
226 |
227 | mins = math.floor(diff.seconds / 60)
228 | hours = math.floor(mins/60)
229 | rem_mins = mins - hours * 60
230 |
231 | if mins == 0:
232 | return 'under 1 minute'
233 | elif mins < 59:
234 | return '%d minutes' % (mins)
235 | elif mins < 1439:
236 | return '%d hours and %d minutes' % (hours, rem_mins)
237 | else:
238 | return "more than a day " + tcrl.red("(%d hours)" %(hours))
239 |
240 |
241 | def helpful_exit(msg=__doc__):
242 | print(msg, file=sys.stderr)
243 | raise SystemExit
244 |
245 |
246 | def parse_args(argv=sys.argv):
247 | global use_color
248 |
249 | argv = [arg.decode('utf-8') for arg in argv]
250 |
251 | if '--no-color' in argv:
252 | use_color = False
253 | argv.remove('--no-color')
254 |
255 | # prog = argv[0]
256 | if len(argv) == 1:
257 | helpful_exit('You must specify a command.')
258 |
259 | head = argv[1]
260 | tail = argv[2:]
261 |
262 | if head in ['-h', '--help', 'h', 'help']:
263 | helpful_exit()
264 |
265 | elif head in ['e', 'edit']:
266 | fn = action_edit
267 | args = {}
268 |
269 | elif head in ['bg', 'begin','o', 'on']:
270 | if not tail:
271 | helpful_exit('Need the name of whatever you are working on.')
272 |
273 | fn = action_begin
274 | args = {
275 | 'name': tail[0],
276 | 'time': to_datetime(' '.join(tail[1:])),
277 | }
278 |
279 | elif head in ['sw', 'switch']:
280 | if not tail:
281 | helpful_exit('I need the name of whatever you are working on.')
282 |
283 | fn = action_switch
284 | args = {
285 | 'name': tail[0],
286 | 'time': to_datetime(' '.join(tail[1:])),
287 | }
288 |
289 | elif head in ['f', 'fin', 'end', 'nd']:
290 | fn = action_end
291 | args = {'time': to_datetime(' '.join(tail))}
292 |
293 | elif head in ['st', 'status']:
294 | fn = action_status
295 | args = {}
296 |
297 | elif head in ['l', 'log']:
298 | fn = action_log
299 | args = {'period': tail[0] if tail else None}
300 |
301 | elif head in ['hl', 'hledger']:
302 | fn = action_hledger
303 | args = {'param': tail}
304 |
305 | elif head in ['hl1']:
306 | fn = action_hledger
307 | args = {'param': ['balance', '--daily','--begin', 'today'] + tail}
308 |
309 | elif head in ['hl2']:
310 | fn = action_hledger
311 | args = {'param': ['balance', '--daily','--begin', 'this week'] + tail}
312 |
313 | elif head in ['hl3']:
314 | fn = action_hledger
315 | args = {'param': ['balance', '--weekly','--begin', 'this month'] + tail}
316 |
317 | elif head in ['hl4']:
318 | fn = action_hledger
319 | args = {'param': ['balance', '--monthly','--begin', 'this year'] + tail}
320 |
321 | elif head in ['ini']:
322 | fn = action_ini
323 | args = {}
324 |
325 | elif head in ['--version', '-v']:
326 | fn = action_version
327 | args = {}
328 |
329 | elif head in ['pt', 'printtime']:
330 | fn = action_printtime
331 | args = {'time': to_datetime(' '.join(tail))}
332 | else:
333 | helpful_exit("I don't understand command '" + head + "'")
334 |
335 | return fn, args
336 |
337 |
338 | def main():
339 | fn, args = parse_args()
340 | fn(**args)
341 |
342 |
343 | store = JsonStore()
344 | tclr = TimColorer(use_color=True)
345 | # use_color = True
346 |
347 | if __name__ == '__main__':
348 | main()
349 |
--------------------------------------------------------------------------------