├── .gitignore ├── .travis.yml ├── AUTHORS.md ├── MANIFEST.in ├── Makefile ├── README.md ├── bin ├── _preamble.py └── timrun.py ├── environment.yml ├── gif ├── tim_hledger.gif └── tim_intro.gif ├── pdt_test.py ├── req.txt ├── setup.py ├── site ├── CNAME ├── favicon.ico ├── index.html └── styles.css ├── test ├── actions.t ├── fin.t ├── format.t ├── interrupt.t ├── note.t ├── on.t ├── status.t ├── tag.t └── timing.t ├── tim ├── __init__.py ├── coloring.py └── timscript.py └── todo.txt /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | tim.egg-info/ 4 | **/*.pyc 5 | **/*.swp 6 | **/.*project 7 | **/*.err 8 | *.egg-info/* 9 | build/* 10 | dist/* 11 | venv/* 12 | #### joe made this: http://goel.io/joe 13 | 14 | #####=== Eclipse ===##### 15 | *.pydevproject 16 | .metadata 17 | .gradle 18 | bin/ 19 | tmp/ 20 | *.tmp 21 | *.bak 22 | *.swp 23 | *~.nib 24 | local.properties 25 | .settings/ 26 | .loadpath 27 | 28 | # External tool builders 29 | .externalToolBuilders/ 30 | 31 | # Locally stored "Eclipse launch configurations" 32 | *.launch 33 | 34 | # CDT-specific 35 | .cproject 36 | 37 | # PDT-specific 38 | .buildpath 39 | 40 | # sbteclipse plugin 41 | .target 42 | 43 | # TeXlipse plugin 44 | .texlipse 45 | 46 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | # command to install dependencies 5 | install: "pip install -r req.txt" 6 | # command to run tests 7 | script: cram test 8 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | Matthias Kauer 2 | Shrikant Sharat K (http://ti.sharats.me/, https://twitter.com/sharat87) 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.md 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | PYENV := source .pyenv/bin/activate 4 | 5 | .PHONY: default tests itests serve-site gen-site 6 | 7 | default: tests 8 | 9 | tests: 10 | ${PYENV} && cram test 11 | 12 | itests: 13 | ${PYENV} && cram -i test 14 | 15 | deps: 16 | ${PYENV} && pip install -r requirements.txt 17 | 18 | serve-site: 19 | ${PYENV} cd site && pygreen serve 20 | 21 | gen-site: 22 | ${PYENV} && cd site && pygreen gen build 23 | ${PYENV} && ghp-import -n site/build 24 | rm -r site/build 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![build status](https://travis-ci.org/MatthiasKauer/tim.png?branch=master)](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 | ![tim intro gif](gif/tim_intro.gif) 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 | ![tim hledger eval gif](gif/tim_hledger.gif) 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 | -------------------------------------------------------------------------------- /bin/_preamble.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Twisted Matrix Laboratories. 2 | # See LICENSE for details. 3 | # This makes sure that users don't have to set up their environment 4 | # specially in order to run these programs from bin/. 5 | # This helper is shared by many different actual scripts. It is not intended to 6 | # be packaged or installed, it is only a developer convenience. By the time 7 | # Twisted is actually installed somewhere, the environment should already be set 8 | # up properly without the help of this tool. 9 | 10 | # Note (Matthias Kauer): Twisted is under MIT license. 11 | 12 | import sys, os 13 | path = os.path.abspath(sys.argv[0]) 14 | while os.path.dirname(path) != path: 15 | if os.path.exists(os.path.join(path, 'tim', '__init__.py')): 16 | sys.path.insert(0, path) 17 | break 18 | path = os.path.dirname(path) 19 | -------------------------------------------------------------------------------- /bin/timrun.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | starter script to be used during development (where paths would otherwise be messed up) 5 | 6 | :copyright: 2015 by Matthias Kauer 7 | :license: BSD 8 | """ 9 | import sys 10 | 11 | try: 12 | import _preamble 13 | except ImportError: 14 | sys.exit(-1) 15 | 16 | from tim.timscript import main 17 | main() 18 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: py27dev 2 | dependencies: 3 | - colorama=0.3.3=py27_0 4 | - ipython=3.2.0=py27_0 5 | - numpy=1.9.2=py27_0 6 | - pip=7.0.3=py27_0 7 | - pyreadline=2.0=py27_0 8 | - python=2.7.10=0 9 | - pyyaml=3.11=py27_1 10 | - scipy=0.15.1=np19py27_0 11 | - setuptools=17.1.1=py27_0 12 | - pip: 13 | - cram==0.6 14 | - parsedatetime==1.5 15 | - python-wordpress-xmlrpc==2.3 16 | - pytz==2015.4 17 | -------------------------------------------------------------------------------- /gif/tim_hledger.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatthiasKauer/tim/b7d5fad48843e5b2846ec349bab586119b1e640e/gif/tim_hledger.gif -------------------------------------------------------------------------------- /gif/tim_intro.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatthiasKauer/tim/b7d5fad48843e5b2846ec349bab586119b1e640e/gif/tim_intro.gif -------------------------------------------------------------------------------- /pdt_test.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import parsedatetime as pdt 3 | import sys 4 | import pytz 5 | # http://stackoverflow.com/questions/13218506/how-to-get-system-timezone-setting-and-pass-it-to-pytz-timezone 6 | from tzlocal import get_localzone # $ pip install tzlocal 7 | local_tz = get_localzone() 8 | 9 | def parse_args(argv=sys.argv): 10 | cal = pdt.Calendar() 11 | timestr = ' '.join(argv) 12 | # ret = cal.parseDT(timestr, sourceTime=datetime.utcnow(), tzinfo=pytz.utc)[0] 13 | ret = cal.parseDT(timestr, tzinfo=local_tz)[0] 14 | ret_utc = ret.astimezone(pytz.utc) 15 | print(ret) 16 | print(ret_utc) 17 | 18 | ret_loc = ret_utc.astimezone(local_tz) 19 | print(ret_loc) 20 | 21 | isostring= ret_utc.isoformat() 22 | 23 | print(isostring) 24 | ret_loc2 = cal.parseDT(isostring)[0] 25 | print(ret_loc2) 26 | 27 | test_str= "2015-06-26T23:30:00+0000" 28 | 29 | #%z is only available after Python 3.2; http://stackoverflow.com/a/23122493 30 | if(sys.version_info > (3,2)): 31 | ret_loc3 = datetime.strptime(test_str, '%Y-%m-%dT%H:%M:%S%z') 32 | print("test %z", ret_loc3) 33 | 34 | custom_str = datetime.strftime(ret_utc, '%Y-%m-%dT%H:%M:%SZ') 35 | ret_loc4 = pytz.utc.localize(datetime.strptime(custom_str, '%Y-%m-%dT%H:%M:%SZ')) 36 | print(ret_loc4) 37 | 38 | if __name__ == '__main__': 39 | parse_args() 40 | -------------------------------------------------------------------------------- /req.txt: -------------------------------------------------------------------------------- 1 | argparse==1.2.1 2 | colorama==0.3.3 3 | cram==0.6 4 | parsedatetime==1.5 5 | pytz==2015.4 6 | tzlocal==1.2 7 | pyyaml 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | from setuptools import setup, find_packages 4 | from tim import __version__ 5 | 6 | #PyPI guide: https://hynek.me/articles/sharing-your-labor-of-love-pypi-quick-and-dirty/ 7 | def read(*paths): 8 | """Build a file path from *paths* and return the contents.""" 9 | with open(os.path.join(*paths), 'r') as f: 10 | return f.read() 11 | 12 | setup( 13 | name='tim-ledger_diary', 14 | version = __version__, 15 | install_requires=[ 16 | 'argparse>=1.1', 17 | 'parsedatetime>=1.2', 18 | 'colorama>=0.3.0', 19 | 'pyyaml>=3.0', 20 | 'tzlocal>=1.2', 21 | 'pytz>=2015.2', 22 | ], 23 | packages = find_packages(), 24 | entry_points={'console_scripts': ['tim=tim.timscript:main']}, 25 | description="command line time logger with hledger backend for number crunching", 26 | long_description=(read('README.md') + "\n\n" + 27 | read("AUTHORS.md")), 28 | license="MIT", 29 | author="Matthias Kauer", 30 | author_email="mk.software@zuez.org", 31 | url="https://github.com/MatthiasKauer/tim", 32 | platforms=["Any"], 33 | classifiers=[ 34 | "Development Status :: 4 - Beta", 35 | "Environment :: Console", 36 | "Intended Audience :: End Users/Desktop", 37 | "License :: OSI Approved :: MIT License", 38 | "Operating System :: OS Independent", 39 | "Programming Language :: Python", 40 | ], 41 | ) 42 | 43 | -------------------------------------------------------------------------------- /site/CNAME: -------------------------------------------------------------------------------- 1 | ti.sharats.me 2 | -------------------------------------------------------------------------------- /site/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatthiasKauer/tim/b7d5fad48843e5b2846ec349bab586119b1e640e/site/favicon.ico -------------------------------------------------------------------------------- /site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | ti — A silly simple time tracker 4 | 5 | 6 | 7 | <%block filter="markdown"> 8 | <% with open('../README.markdown') as f: readme = f.read() %> 9 | ${readme} 10 | 11 | 12 |
13 | 15 | 18 |
19 | 21 | 27 | 30 |
31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /site/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font: 18px sans-serif; 3 | background-color: #F8F8FF; 4 | width: 960px; 5 | margin: 0 auto; 6 | text-shadow: 1px 2px 1px rgba(240, 240, 255, .8); 7 | } 8 | 9 | .sociale { 10 | position: fixed; 11 | right: 0; 12 | top: 0; 13 | background-color: rgba(255, 255, 255, .8); 14 | border-style: solid; 15 | border-color: rgba(0, 0, 0, .2); 16 | border-width: 0 0 1px 1px; 17 | border-bottom-left-radius: 6px; 18 | box-shadow: 0 1px 3px rgba(0, 0, 0, .2); 19 | padding-top: 12px; 20 | opacity: .6; 21 | } 22 | 23 | .sociale:hover { 24 | opacity: 1; 25 | } 26 | 27 | .sociale > * { 28 | display: block; 29 | /* The plusone widget sets zero margin in element style. So need !important. */ 30 | margin: 0 12px 12px !important; 31 | } 32 | 33 | p { 34 | line-height: 1.5em; 35 | } 36 | 37 | pre, p code { 38 | font: monospace; 39 | background-color: rgba(0, 0, 0, .1); 40 | border-radius: 3px; 41 | } 42 | 43 | p code { 44 | padding: 3px 6px; 45 | } 46 | 47 | pre { 48 | padding: 3px 12px 7px; 49 | box-shadow: inset 0 0 6px rgba(0, 0, 0, .1); 50 | } 51 | -------------------------------------------------------------------------------- /test/actions.t: -------------------------------------------------------------------------------- 1 | Setup 2 | 3 | $ export SHEET_FILE=$TMP/sheet-actions 4 | $ alias tim="$TESTDIR/../bin/timrun.py --no-color" 5 | 6 | Running an unknown action 7 | 8 | $ tim almost-definitely-a-nonexistent-action 9 | I don't understand command 'almost-definitely-a-nonexistent-action' 10 | #self.filename: /home/matthias/Seafile/todo2/tim/tim-sheet.json 11 | 12 | -------------------------------------------------------------------------------- /test/fin.t: -------------------------------------------------------------------------------- 1 | Setup 2 | 3 | $ export SHEET_FILE=$TMP/sheet-fin 4 | $ alias ti="$TESTDIR/../bin/ti --no-color" 5 | 6 | Running fin when not working 7 | 8 | $ ti fin 9 | For all I know, you aren't working on anything. I don't know what to do. 10 | See `ti -h` to know how to start working. 11 | [1] 12 | 13 | Incorrectly running it (above) shouldn't create any file 14 | 15 | $ test -f $SHEET_FILE 16 | [1] 17 | 18 | Start a task and then fin 19 | 20 | $ ti on testing-my-foot 21 | Start working on testing-my-foot. 22 | $ ti fin 23 | So you stopped working on testing-my-foot. 24 | $ test -f $SHEET_FILE 25 | 26 | Fin a tagged activity 27 | 28 | $ ti on tagged-one 29 | Start working on tagged-one. 30 | $ ti tag woohoo 31 | Okay, tagged current work with 1 tags. 32 | $ ti fin 33 | So you stopped working on tagged-one. 34 | 35 | Check the current file existence 36 | 37 | $ ti on awesomeness 38 | Start working on awesomeness. 39 | $ ti fin 40 | So you stopped working on awesomeness. 41 | $ test -f $SHEET_FILE 42 | -------------------------------------------------------------------------------- /test/format.t: -------------------------------------------------------------------------------- 1 | Setup 2 | 3 | $ export SHEET_FILE=$TMP/sheet-format 4 | $ alias ti="$TESTDIR/../bin/ti --no-color" 5 | 6 | Confirm no sheet file 7 | 8 | $ test -e $SHEET_FILE 9 | [1] 10 | 11 | Start a project, add content to current file 12 | 13 | $ ti on a-project 14 | Start working on a-project. 15 | 16 | End a project, add content to sheet file 17 | 18 | $ ti fin 19 | So you stopped working on a-project. 20 | 21 | Another one, with notes 22 | 23 | $ ti on another-project 24 | Start working on another-project. 25 | $ ti note a simple note 26 | Yep, noted to `another-project`. 27 | 28 | End and check 29 | 30 | $ ti fin 31 | So you stopped working on another-project. 32 | 33 | Another one, with tags 34 | 35 | $ ti on yet-another-project 36 | Start working on yet-another-project. 37 | $ ti tag hella 38 | Okay, tagged current work with 1 tags. 39 | $ ti fin 40 | So you stopped working on yet-another-project. 41 | -------------------------------------------------------------------------------- /test/interrupt.t: -------------------------------------------------------------------------------- 1 | Setup 2 | 3 | $ export SHEET_FILE=$TMP/sheet-actions 4 | $ alias ti="$TESTDIR/../bin/ti --no-color" 5 | 6 | Go two deep in interrupts 7 | 8 | $ ti o task 9 | Start working on task. 10 | $ ti i interrupt1 11 | So you stopped working on task. 12 | Start working on interrupt: interrupt1. 13 | You are now 1 deep in interrupts. 14 | $ ti i interrupt2 15 | So you stopped working on interrupt: interrupt1. 16 | Start working on interrupt: interrupt2. 17 | You are now 2 deep in interrupts. 18 | $ ti f 19 | So you stopped working on interrupt: interrupt2. 20 | Start working on interrupt: interrupt1. 21 | You are now 1 deep in interrupts. 22 | $ ti f 23 | So you stopped working on interrupt: interrupt1. 24 | Start working on task. 25 | Congrats, you're out of interrupts! 26 | $ ti f 27 | So you stopped working on task. 28 | -------------------------------------------------------------------------------- /test/note.t: -------------------------------------------------------------------------------- 1 | Setup 2 | 3 | $ export SHEET_FILE=$TMP/sheet 4 | $ alias ti="$TESTDIR/../bin/ti --no-color" 5 | 6 | Note when not working 7 | 8 | $ ti note hee-haw 9 | For all I know, you aren't working on anything. I don't know what to do. 10 | See `ti -h` to know how to start working. 11 | [1] 12 | 13 | Start working and then note 14 | 15 | $ ti on donkey-music 16 | Start working on donkey-music. 17 | $ ti note hee-haw 18 | Yep, noted to `donkey-music`. 19 | 20 | Add another longer note 21 | 22 | $ ti note holla hoy with a longer musical? note 23 | Yep, noted to `donkey-music`. 24 | 25 | Note with external editor 26 | FIXME: Need a better EDITOR to test with 27 | 28 | $ EDITOR="false" ti note 29 | Please provide some text to be noted. 30 | -------------------------------------------------------------------------------- /test/on.t: -------------------------------------------------------------------------------- 1 | Setup 2 | 3 | $ export SHEET_FILE=$TMP/sheet-on 4 | $ alias ti="$TESTDIR/../bin/ti --no-color" 5 | 6 | Start working 7 | 8 | $ ti on my-project 9 | Start working on my-project. 10 | $ test -f $SHEET_FILE 11 | $ ti fin 12 | So you stopped working on my-project. 13 | 14 | Start working while working 15 | 16 | $ ti on project1 17 | Start working on project1. 18 | $ ti on project2 19 | You are already working on project1. Stop it or use a different sheet. 20 | [1] 21 | -------------------------------------------------------------------------------- /test/status.t: -------------------------------------------------------------------------------- 1 | Setup 2 | 3 | $ export SHEET_FILE=$TMP/sheet-status 4 | $ alias ti="$TESTDIR/../bin/ti --no-color" 5 | 6 | Status when not working 7 | 8 | $ ti status 9 | For all I know, you aren't working on anything. I don't know what to do. 10 | See `ti -h` to know how to start working. 11 | [1] 12 | 13 | Status after on-ing a task 14 | 15 | $ ti on conqering-the-world 16 | Start working on conqering-the-world. 17 | $ ti status 18 | You have been working on `conqering-the-world` for less than a minute. 19 | 20 | After adding tags 21 | 22 | $ ti tag awesome 23 | Okay, tagged current work with 1 tags. 24 | $ ti status 25 | You have been working on `conqering-the-world` for less than a minute. 26 | 27 | Status after fin-ing it 28 | 29 | $ ti fin 30 | So you stopped working on conqering-the-world. 31 | $ ti status 32 | For all I know, you aren't working on anything. I don't know what to do. 33 | See `ti -h` to know how to start working. 34 | [1] 35 | 36 | Short alias `s` for status 37 | 38 | $ ti s 39 | For all I know, you aren't working on anything. I don't know what to do. 40 | See `ti -h` to know how to start working. 41 | [1] 42 | -------------------------------------------------------------------------------- /test/tag.t: -------------------------------------------------------------------------------- 1 | Setup 2 | 3 | $ export SHEET_FILE=$TMP/sheet-tag 4 | $ alias ti="$TESTDIR/../bin/ti --no-color" 5 | 6 | When not working 7 | 8 | $ ti tag a-tag 9 | For all I know, you aren't working on anything. I don't know what to do. 10 | See `ti -h` to know how to start working. 11 | [1] 12 | 13 | Not giving a tag to add 14 | 15 | $ ti on something 16 | Start working on something. 17 | $ ti tag 18 | Please provide at least one tag to add. 19 | -------------------------------------------------------------------------------- /test/timing.t: -------------------------------------------------------------------------------- 1 | Setup 2 | 3 | $ export SHEET_FILE=$TMP/sheet-timing 4 | $ alias ti="$TESTDIR/../bin/ti --no-color" 5 | 6 | Timing a start 7 | 8 | $ 9 | -------------------------------------------------------------------------------- /tim/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.6.0' 2 | 3 | -------------------------------------------------------------------------------- /tim/coloring.py: -------------------------------------------------------------------------------- 1 | import colorama as cr 2 | cr.init() 3 | 4 | class TimColorer(object): 5 | 6 | def __init__(self, use_color): 7 | """TODO: to be defined1. """ 8 | 9 | self.use_color = use_color 10 | 11 | def red(self, str): 12 | if self.use_color: 13 | return cr.Fore.RED + str + cr.Fore.RESET 14 | else: 15 | return str 16 | 17 | def green(self, str): 18 | if self.use_color: 19 | return cr.Fore.GREEN + str + cr.Fore.RESET 20 | else: 21 | return str 22 | 23 | def yellow(self, str): 24 | if self.use_color: 25 | return cr.Fore.YELLOW + str + cr.Fore.RESET 26 | else: 27 | return str 28 | 29 | def blue(self, str): 30 | if self.use_color: 31 | return cr.Back.WHITE + cr.Fore.BLUE + str + cr.Fore.RESET + cr.Back.RESET 32 | else: 33 | return str 34 | 35 | def bold(self, str): 36 | #doesn't do much on my ConEmu Windows 7 system, but let's see 37 | if self.use_color: 38 | return cr.Style.BRIGHT + str + cr.Style.RESET_ALL 39 | else: 40 | return str 41 | 42 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------