summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBarry Warsaw <barry@python.org>2015-12-10 16:51:16 -0500
committerBarry Warsaw <barry@python.org>2015-12-10 16:51:16 -0500
commit37a138198f85750f90c20eb8f0c85093de4b8153 (patch)
tree1d0493cfddcacb1db997fe331a0b132a4400f275
parentf50df7e72c8fd373688e6610d6c5111c8ce93900 (diff)
New upstream release.upstream/0.10.0
-rw-r--r--.coveragerc5
-rw-r--r--.gitignore1
-rw-r--r--.travis.yml10
-rw-r--r--CONTRIBUTING.rst1
-rw-r--r--MANIFEST.in1
-rw-r--r--Makefile12
-rw-r--r--NEWS.rst38
-rw-r--r--PKG-INFO61
-rw-r--r--README.rst14
-rw-r--r--docs/index.rst15
-rw-r--r--gtimelog.appdata.xml17
-rw-r--r--gtimelog.rst24
-rw-r--r--gtimelogrc.example9
-rw-r--r--gtimelogrc.rst18
-rw-r--r--setup.cfg10
-rwxr-xr-xsetup.py9
-rw-r--r--src/gtimelog.egg-info/PKG-INFO (renamed from gtimelog.egg-info/PKG-INFO)61
-rw-r--r--src/gtimelog.egg-info/SOURCES.txt (renamed from gtimelog.egg-info/SOURCES.txt)17
-rw-r--r--src/gtimelog.egg-info/dependency_links.txt (renamed from gtimelog.egg-info/dependency_links.txt)0
-rw-r--r--src/gtimelog.egg-info/entry_points.txt (renamed from gtimelog.egg-info/entry_points.txt)0
-rw-r--r--src/gtimelog.egg-info/not-zip-safe (renamed from gtimelog.egg-info/not-zip-safe)0
-rw-r--r--src/gtimelog.egg-info/pbr.json1
-rw-r--r--src/gtimelog.egg-info/top_level.txt (renamed from gtimelog.egg-info/top_level.txt)0
-rw-r--r--src/gtimelog/__init__.py2
-rw-r--r--src/gtimelog/gtimelog.ui172
-rw-r--r--src/gtimelog/main.py784
-rw-r--r--src/gtimelog/settings.py7
-rw-r--r--src/gtimelog/tests.py352
-rw-r--r--src/gtimelog/timelog.py240
-rw-r--r--tox.ini20
30 files changed, 1106 insertions, 795 deletions
diff --git a/.coveragerc b/.coveragerc
index 5230a5e..32f201d 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -1,3 +1,8 @@
+[run]
+source = src/gtimelog
+omit = src/gtimelog/tests.py,src/gtimelog/main.py
+cover_pylib = False
+
[report]
exclude_lines =
pragma: nocover
diff --git a/.gitignore b/.gitignore
index 260aab2..811e744 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,4 @@ gtimelogrc.5
gtimelogrc.sample
*~
tags
+coverage.xml
diff --git a/.travis.yml b/.travis.yml
index 8b3d4b9..674e96b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,11 +1,15 @@
language: python
+sudo: false
python:
- - 2.6
- 2.7
- 3.3
+ - 3.4
+ - 3.5
install:
- - travis_retry pip install .
+ - pip install freezegun mock coverage coveralls -e .
script:
- - python setup.py test -q
+ - coverage run -m gtimelog.tests
+after_success:
+ - coveralls
notifications:
email: false
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 01d54fd..97bc470 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -46,4 +46,3 @@ Run the test suite with ::
$ ./runtests
The common ``python setup.py test`` idiom is also supported.
-
diff --git a/MANIFEST.in b/MANIFEST.in
index e8b3cec..7741643 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -2,6 +2,7 @@ include COPYING
include *.rst
include Makefile
include gtimelog
+include gtimelog.appdata.xml
include gtimelog.desktop
include gtimelogrc.example
include runtests
diff --git a/Makefile b/Makefile
index 8c1433d..d353a29 100644
--- a/Makefile
+++ b/Makefile
@@ -25,8 +25,14 @@ check test:
.PHONY: coverage
coverage:
- coverage run ./runtests
- coverage report --include 'src/gtimelog/*'
+ detox -e coverage,coverage3 -- -p
+ coverage combine
+ coverage report
+
+.PHONY: coverage-diff
+coverage-diff: coverage
+ coverage xml
+ diff-cover coverage.xml
.PHONY: clean
clean:
@@ -79,7 +85,7 @@ release: releasechecklist
# I'm chicken so I won't actually do these things yet
@echo "Please run"
@echo
- @echo " $(PYTHON) setup.py sdist register upload && git tag `$(PYTHON) setup.py --version`"
+ @echo " rm -rf dist && $(PYTHON) setup.py sdist && twine upload dist/* && git tag `$(PYTHON) setup.py --version`"
@echo
@echo "Please increment the version number in $(FILE_WITH_VERSION)"
@echo "and add a new empty entry at the top of $(FILE_WITH_CHANGELOG), then"
diff --git a/NEWS.rst b/NEWS.rst
index ac9b61b..14e3ba5 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -1,14 +1,48 @@
Changelog
---------
+0.10.0 (2015-09-29)
+~~~~~~~~~~~~~~~~~~~
+
+* Use Tango colors in the main text buffer (GH: #13).
+
+* Allow tagging entries (GH: #19)
+
+ - The syntax is ``category: text -- tag1 tag2``
+ - Per-tag summaries show up in reports
+
+* Use GtkApplication instead of own DBus server for enforcing single-instance.
+
+ - Drop --replace, --ignore-dbus command-line options because of this.
+ - Require glib and gio to be version 2.40 or newer for sane
+ GtkApplication-based command line parsing
+ (check with ``pkg-config --modversion glib-2.0 gio-2.0``).
+
+* Remove obsolete code:
+
+ - Drop support for Python 2.6 (PyGObject dropped support for it long ago).
+ - Drop PyGtk/Gtk+ 2 support code (it didn't work since 0.9.1 anyway).
+ - Drop EggTrayIcon support (it was for Gtk+ 2 only anyway).
+ - Drop the --prefer-pygtk command-line option.
+
+* Disable tray icon by default for new users (existing gtimelogrc files will be
+ untouched).
+
+* Improve tray icon selection logic for best contrast (GH: #29).
+
+
+0.9.3 (2015-09-29)
+~~~~~~~~~~~~~~~~~~
+
+* Adding new entries didn't update total weekly numbers (GH: #28).
+
0.9.2 (2014-09-28)
~~~~~~~~~~~~~~~~~~
-* Fix setup.py to work on Python 3 when your locale is not UTF-8
- (LP: #1263772).
* Note that Gtk+ 2.x is no longer supported (this regressed somewhere between
0.9.0 and 0.9.1, but I didn't notice because I have no access to a system
that has Gtk+ 2.x).
+* Fix setup.py to work on Python 3 when your locale is not UTF-8 (LP: #1263772).
* Fix two Gtk-CRITICAL warnings on startup (GH: #14).
* Fix Unicode warning when adding entries (GH: #20).
* Speed up entry addition (GH: #21).
diff --git a/PKG-INFO b/PKG-INFO
index bf6fae7..0697490 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 1.1
Name: gtimelog
-Version: 0.9.2
+Version: 0.10.0
Summary: A Gtk+ time tracking application
Home-page: http://mg.pov.lt/gtimelog/
Author: Marius Gedminas
@@ -9,11 +9,19 @@ License: GPL
Description: GTimeLog
========
- .. image:: https://travis-ci.org/gtimelog/gtimelog.png?branch=master
+ GTimeLog is a simple app for keeping track of time.
+
+ .. image:: https://pypip.in/version/gtimelog/badge.svg?style=flat
+ :target: https://pypi.python.org/pypi/gtimelog/
+ :alt: latest version
+
+ .. image:: https://travis-ci.org/gtimelog/gtimelog.svg?branch=master
:target: https://travis-ci.org/gtimelog/gtimelog
:alt: build status
- GTimeLog is a simple app for keeping track of time.
+ .. image:: https://coveralls.io/repos/gtimelog/gtimelog/badge.svg?branch=master
+ :target: https://coveralls.io/r/gtimelog/gtimelog?branch=master
+ :alt: test coverage
.. contents::
@@ -45,7 +53,7 @@ Description: GTimeLog
System requirements:
- - Python (2.6, 2.7 or 3.3)
+ - Python (2.7 or 3.3+)
- PyGObject
- gobject-introspection type libraries for GTK+, Pango
@@ -100,14 +108,48 @@ Description: GTimeLog
Changelog
---------
+ 0.10.0 (2015-09-29)
+ ~~~~~~~~~~~~~~~~~~~
+
+ * Use Tango colors in the main text buffer (GH: #13).
+
+ * Allow tagging entries (GH: #19)
+
+ - The syntax is ``category: text -- tag1 tag2``
+ - Per-tag summaries show up in reports
+
+ * Use GtkApplication instead of own DBus server for enforcing single-instance.
+
+ - Drop --replace, --ignore-dbus command-line options because of this.
+ - Require glib and gio to be version 2.40 or newer for sane
+ GtkApplication-based command line parsing
+ (check with ``pkg-config --modversion glib-2.0 gio-2.0``).
+
+ * Remove obsolete code:
+
+ - Drop support for Python 2.6 (PyGObject dropped support for it long ago).
+ - Drop PyGtk/Gtk+ 2 support code (it didn't work since 0.9.1 anyway).
+ - Drop EggTrayIcon support (it was for Gtk+ 2 only anyway).
+ - Drop the --prefer-pygtk command-line option.
+
+ * Disable tray icon by default for new users (existing gtimelogrc files will be
+ untouched).
+
+ * Improve tray icon selection logic for best contrast (GH: #29).
+
+
+ 0.9.3 (2015-09-29)
+ ~~~~~~~~~~~~~~~~~~
+
+ * Adding new entries didn't update total weekly numbers (GH: #28).
+
0.9.2 (2014-09-28)
~~~~~~~~~~~~~~~~~~
- * Fix setup.py to work on Python 3 when your locale is not UTF-8
- (LP: #1263772).
* Note that Gtk+ 2.x is no longer supported (this regressed somewhere between
0.9.0 and 0.9.1, but I didn't notice because I have no access to a system
that has Gtk+ 2.x).
+ * Fix setup.py to work on Python 3 when your locale is not UTF-8 (LP: #1263772).
* Fix two Gtk-CRITICAL warnings on startup (GH: #14).
* Fix Unicode warning when adding entries (GH: #20).
* Speed up entry addition (GH: #21).
@@ -118,11 +160,6 @@ Description: GTimeLog
on Python 3 (GH: #26).
- 0.9.1 (2013-12-23)
- ~~~~~~~~~~~~~~~~~~
- * Manual pages for gtimelog(1) and gtimelogrc(5).
-
-
Older versions
~~~~~~~~~~~~~~
@@ -136,4 +173,6 @@ Classifier: Environment :: X11 Applications :: GTK
Classifier: License :: OSI Approved :: GNU General Public License (GPL)
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
Classifier: Topic :: Office/Business
diff --git a/README.rst b/README.rst
index 2cf9acc..66f6432 100644
--- a/README.rst
+++ b/README.rst
@@ -1,11 +1,19 @@
GTimeLog
========
-.. image:: https://travis-ci.org/gtimelog/gtimelog.png?branch=master
+GTimeLog is a simple app for keeping track of time.
+
+.. image:: https://pypip.in/version/gtimelog/badge.svg?style=flat
+ :target: https://pypi.python.org/pypi/gtimelog/
+ :alt: latest version
+
+.. image:: https://travis-ci.org/gtimelog/gtimelog.svg?branch=master
:target: https://travis-ci.org/gtimelog/gtimelog
:alt: build status
-GTimeLog is a simple app for keeping track of time.
+.. image:: https://coveralls.io/repos/gtimelog/gtimelog/badge.svg?branch=master
+ :target: https://coveralls.io/r/gtimelog/gtimelog?branch=master
+ :alt: test coverage
.. contents::
@@ -37,7 +45,7 @@ You can run it from a source checkout without an explicit installation step::
System requirements:
-- Python (2.6, 2.7 or 3.3)
+- Python (2.7 or 3.3+)
- PyGObject
- gobject-introspection type libraries for GTK+, Pango
diff --git a/docs/index.rst b/docs/index.rst
index bc7b630..94fc4f9 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -53,6 +53,21 @@ Work activities can also include a category name, e.g.::
The tasks are grouped by category in the reports.
+Each entry may be additionally labelled with multiple
+(space-separated) tags, e.g.::
+
+ project3: upgrade webserver -- sysadmin www front-end
+ project3: restart mail server -- sysadmin mail
+
+Reports will then include an additional breakdown by tag: for each
+tag, the total time spent in entries marked with that tag is shown.
+Note that these times will (likely) not add up to the total reporting
+time, as each entry may be marked with several tags.
+
+Tags must be separated from the rest of the entry by `` -- ``, i.e.,
+double-dash surrounded by spaces. Tags will *not* be shown in the
+main UI pane.
+
Tasks Pane
==========
diff --git a/gtimelog.appdata.xml b/gtimelog.appdata.xml
new file mode 100644
index 0000000..06aaf9d
--- /dev/null
+++ b/gtimelog.appdata.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<application>
+ <id type="desktop">gtimelog.desktop</id>
+ <metadata_license>CC0-1.0</metadata_license>
+ <project_license>GPL-2.0</project_license>
+
+ <name>GTimeLog</name>
+ <summary>Unobtrusively keep track of your time</summary>
+ <description>
+ <p>GTimeLog is a small GTK+ app for keeping track of your time. Its main goal is to be as unobtrusive as possible.</p>
+ </description>
+ <screenshots>
+ <screenshot width="802" height="533" type="default">https://mg.pov.lt/gtimelog/gtimelog-gtk3.png</screenshot>
+ </screenshots>
+ <url type="homepage">https://mg.pov.lt/gtimelog/</url>
+ <updatecontact>marius@gedmin.as</updatecontact>
+</application>
diff --git a/gtimelog.rst b/gtimelog.rst
index 43a7095..e324b13 100644
--- a/gtimelog.rst
+++ b/gtimelog.rst
@@ -7,9 +7,9 @@ minimal time logging application
--------------------------------
:Author: Marius Gedminas <mgedmin@gedmin.as>
-:Date: 2013-12-23
+:Date: 2014-03-19
:Copyright: Marius Gedminas
-:Version: 0.9.1
+:Version: 0.10
:Manual section: 1
@@ -93,29 +93,9 @@ OPTIONS
--sample-config
Write a sample configuration file to 'gtimelogrc.sample'.
-Single-Instance Options:
-
---replace
- Replace the already running ``gtimelog`` instance.
-
---quit
- Tell an already-running ``gtimelog`` instance to quit.
-
---toggle
- Show/hide the ``gtimelog`` window if already running.
-
---ignore-dbus
- Do not check if ``gtimelog`` is already running (allows you to have
- multiple instances running).
-
-Debugging Options:
-
--debug
Show debug information.
---prefer-pygtk
- Try to use the (obsolete) pygtk library instead of pygi.
-
FILES
=====
diff --git a/gtimelogrc.example b/gtimelogrc.example
index 94421bb..50fe934 100644
--- a/gtimelogrc.example
+++ b/gtimelogrc.example
@@ -18,17 +18,14 @@ mailer = x-terminal-emulator -e mutt -H %s
# file; if there is no '%s', the name of the log file is appended.
editor = gvim
-# User interface: True enables drop-down history completion (if you have PyGtk
-# 2.4), False disables and lets you access history by pressing Up/Down.
+# User interface: True enables drop-down history completion, False disables and
+# lets you access history by pressing Up/Down. (You can always use the non-Gtk
+# completion by pressing PageUp/PageDown.)
gtk-completion = False
# Do you want a systray icon?
show_tray_icon = yes
-# Do you prefer the old systray icon (that shows time taken for the current
-# task next to the icon), or the new one (just the icon)?
-prefer_old_tray_icon = yes
-
# How many hours' work in a day.
hours = 8
diff --git a/gtimelogrc.rst b/gtimelogrc.rst
index 8221da6..9464b4d 100644
--- a/gtimelogrc.rst
+++ b/gtimelogrc.rst
@@ -184,36 +184,26 @@ show_tray_icon
Example: ``show_tray_icon = True``
-prefer_app_indicator, prefer_old_tray_icon
+prefer_app_indicator
what kind of tray icon do you prefer?
- GTimeLog supports three kinds:
+ GTimeLog supports two kinds:
- Unity application indicator
- a standard Gtk+ status icon
- - ancient EggTrayIcon that shows a ticking clock next to the icon
Support for each is conditional on the availability of installed libraries.
Example::
# prefer Unity application indicators, then fall back to the Gtk+
- # status icon, then fall back to EggTrayIcon.
+ # status icon.
prefer_app_indicator = True
Example::
- # prefer the ancient EggTrayIcon, then fall back to the Gtk+
- # status icon, then fall back to Unity app indicator.
+ # prefer the Gtk+ status icon, then fall back to Unity app indicator.
prefer_app_indicator = False
- prefer_old_tray_icon = True
-
- Example::
-
- # prefer the Gtk+ status icon, then fall back to the ancient
- # EggTrayIcon, then fall back to Unity app indicator.
- prefer_app_indicator = False
- prefer_old_tray_icon = False
start_in_tray
whether GTimeLog should start minimized
diff --git a/setup.cfg b/setup.cfg
index 861a9f5..6755780 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,3 +1,13 @@
+[flake8]
+doctests = yes
+
+[pytest]
+norecursedirs = .* *.egg-info dist build tmp scripts
+python_files = tests.py
+python_functions = !test_suite
+addopts = --doctest-modules --ignore=setup.py
+doctest_optionflags = NORMALIZE_WHITESPACE
+
[egg_info]
tag_build =
tag_date = 0
diff --git a/setup.py b/setup.py
index 5c4bd76..6adfe60 100755
--- a/setup.py
+++ b/setup.py
@@ -55,15 +55,16 @@ setup(
'License :: OSI Approved :: GNU General Public License (GPL)',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.3',
- # 2.6 might work, but I can't test it myself -- recent
- # python-gobject versions dropped support for Python 2.6
+ 'Programming Language :: Python :: 3.4',
+ 'Programming Language :: Python :: 3.5',
'Topic :: Office/Business',
],
packages=['gtimelog'],
- package_dir={'gtimelog': 'src/gtimelog'},
+ package_dir={'': 'src'},
package_data={'gtimelog': ['*.ui', '*.png']},
test_suite='gtimelog.tests',
+ tests_require=['freezegun', 'mock'],
zip_safe=False,
entry_points="""
[gui_scripts]
@@ -71,5 +72,5 @@ setup(
""",
# This is true, but pointless, because PyGObject cannot be installed via
# setuptools/distutils
-# install_requires=['PyGObject'], # or PyGTK
+# install_requires=['PyGObject'],
)
diff --git a/gtimelog.egg-info/PKG-INFO b/src/gtimelog.egg-info/PKG-INFO
index bf6fae7..0697490 100644
--- a/gtimelog.egg-info/PKG-INFO
+++ b/src/gtimelog.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 1.1
Name: gtimelog
-Version: 0.9.2
+Version: 0.10.0
Summary: A Gtk+ time tracking application
Home-page: http://mg.pov.lt/gtimelog/
Author: Marius Gedminas
@@ -9,11 +9,19 @@ License: GPL
Description: GTimeLog
========
- .. image:: https://travis-ci.org/gtimelog/gtimelog.png?branch=master
+ GTimeLog is a simple app for keeping track of time.
+
+ .. image:: https://pypip.in/version/gtimelog/badge.svg?style=flat
+ :target: https://pypi.python.org/pypi/gtimelog/
+ :alt: latest version
+
+ .. image:: https://travis-ci.org/gtimelog/gtimelog.svg?branch=master
:target: https://travis-ci.org/gtimelog/gtimelog
:alt: build status
- GTimeLog is a simple app for keeping track of time.
+ .. image:: https://coveralls.io/repos/gtimelog/gtimelog/badge.svg?branch=master
+ :target: https://coveralls.io/r/gtimelog/gtimelog?branch=master
+ :alt: test coverage
.. contents::
@@ -45,7 +53,7 @@ Description: GTimeLog
System requirements:
- - Python (2.6, 2.7 or 3.3)
+ - Python (2.7 or 3.3+)
- PyGObject
- gobject-introspection type libraries for GTK+, Pango
@@ -100,14 +108,48 @@ Description: GTimeLog
Changelog
---------
+ 0.10.0 (2015-09-29)
+ ~~~~~~~~~~~~~~~~~~~
+
+ * Use Tango colors in the main text buffer (GH: #13).
+
+ * Allow tagging entries (GH: #19)
+
+ - The syntax is ``category: text -- tag1 tag2``
+ - Per-tag summaries show up in reports
+
+ * Use GtkApplication instead of own DBus server for enforcing single-instance.
+
+ - Drop --replace, --ignore-dbus command-line options because of this.
+ - Require glib and gio to be version 2.40 or newer for sane
+ GtkApplication-based command line parsing
+ (check with ``pkg-config --modversion glib-2.0 gio-2.0``).
+
+ * Remove obsolete code:
+
+ - Drop support for Python 2.6 (PyGObject dropped support for it long ago).
+ - Drop PyGtk/Gtk+ 2 support code (it didn't work since 0.9.1 anyway).
+ - Drop EggTrayIcon support (it was for Gtk+ 2 only anyway).
+ - Drop the --prefer-pygtk command-line option.
+
+ * Disable tray icon by default for new users (existing gtimelogrc files will be
+ untouched).
+
+ * Improve tray icon selection logic for best contrast (GH: #29).
+
+
+ 0.9.3 (2015-09-29)
+ ~~~~~~~~~~~~~~~~~~
+
+ * Adding new entries didn't update total weekly numbers (GH: #28).
+
0.9.2 (2014-09-28)
~~~~~~~~~~~~~~~~~~
- * Fix setup.py to work on Python 3 when your locale is not UTF-8
- (LP: #1263772).
* Note that Gtk+ 2.x is no longer supported (this regressed somewhere between
0.9.0 and 0.9.1, but I didn't notice because I have no access to a system
that has Gtk+ 2.x).
+ * Fix setup.py to work on Python 3 when your locale is not UTF-8 (LP: #1263772).
* Fix two Gtk-CRITICAL warnings on startup (GH: #14).
* Fix Unicode warning when adding entries (GH: #20).
* Speed up entry addition (GH: #21).
@@ -118,11 +160,6 @@ Description: GTimeLog
on Python 3 (GH: #26).
- 0.9.1 (2013-12-23)
- ~~~~~~~~~~~~~~~~~~
- * Manual pages for gtimelog(1) and gtimelogrc(5).
-
-
Older versions
~~~~~~~~~~~~~~
@@ -136,4 +173,6 @@ Classifier: Environment :: X11 Applications :: GTK
Classifier: License :: OSI Approved :: GNU General Public License (GPL)
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
Classifier: Topic :: Office/Business
diff --git a/gtimelog.egg-info/SOURCES.txt b/src/gtimelog.egg-info/SOURCES.txt
index 705b74e..f62f520 100644
--- a/gtimelog.egg-info/SOURCES.txt
+++ b/src/gtimelog.egg-info/SOURCES.txt
@@ -11,22 +11,18 @@ NOTES.rst
README.rst
TODO.rst
gtimelog
+gtimelog.appdata.xml
gtimelog.desktop
gtimelog.rst
gtimelogrc.example
gtimelogrc.rst
runtests
+setup.cfg
setup.py
tox.ini
docs/formats.rst
docs/gtimelog.png
docs/index.rst
-gtimelog.egg-info/PKG-INFO
-gtimelog.egg-info/SOURCES.txt
-gtimelog.egg-info/dependency_links.txt
-gtimelog.egg-info/entry_points.txt
-gtimelog.egg-info/not-zip-safe
-gtimelog.egg-info/top_level.txt
scripts/README.rst
scripts/difftime.py
scripts/export-my-calendar.py
@@ -43,4 +39,11 @@ src/gtimelog/gtimelog.ui
src/gtimelog/main.py
src/gtimelog/settings.py
src/gtimelog/tests.py
-src/gtimelog/timelog.py \ No newline at end of file
+src/gtimelog/timelog.py
+src/gtimelog.egg-info/PKG-INFO
+src/gtimelog.egg-info/SOURCES.txt
+src/gtimelog.egg-info/dependency_links.txt
+src/gtimelog.egg-info/entry_points.txt
+src/gtimelog.egg-info/not-zip-safe
+src/gtimelog.egg-info/pbr.json
+src/gtimelog.egg-info/top_level.txt \ No newline at end of file
diff --git a/gtimelog.egg-info/dependency_links.txt b/src/gtimelog.egg-info/dependency_links.txt
index 8b13789..8b13789 100644
--- a/gtimelog.egg-info/dependency_links.txt
+++ b/src/gtimelog.egg-info/dependency_links.txt
diff --git a/gtimelog.egg-info/entry_points.txt b/src/gtimelog.egg-info/entry_points.txt
index 387b608..387b608 100644
--- a/gtimelog.egg-info/entry_points.txt
+++ b/src/gtimelog.egg-info/entry_points.txt
diff --git a/gtimelog.egg-info/not-zip-safe b/src/gtimelog.egg-info/not-zip-safe
index 8b13789..8b13789 100644
--- a/gtimelog.egg-info/not-zip-safe
+++ b/src/gtimelog.egg-info/not-zip-safe
diff --git a/src/gtimelog.egg-info/pbr.json b/src/gtimelog.egg-info/pbr.json
new file mode 100644
index 0000000..0f68d7c
--- /dev/null
+++ b/src/gtimelog.egg-info/pbr.json
@@ -0,0 +1 @@
+{"is_release": false, "git_version": "7ed2060"} \ No newline at end of file
diff --git a/gtimelog.egg-info/top_level.txt b/src/gtimelog.egg-info/top_level.txt
index 72f1082..72f1082 100644
--- a/gtimelog.egg-info/top_level.txt
+++ b/src/gtimelog.egg-info/top_level.txt
diff --git a/src/gtimelog/__init__.py b/src/gtimelog/__init__.py
index e60065b..997e093 100644
--- a/src/gtimelog/__init__.py
+++ b/src/gtimelog/__init__.py
@@ -1,3 +1,3 @@
# The gtimelog package.
-__version__ = '0.9.2'
+__version__ = '0.10.0'
diff --git a/src/gtimelog/gtimelog.ui b/src/gtimelog/gtimelog.ui
index b7e4dae..37f9a37 100644
--- a/src/gtimelog/gtimelog.ui
+++ b/src/gtimelog/gtimelog.ui
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
-<!-- Generated with glade 3.16.1 -->
+<!-- Generated with glade 3.18.1 -->
<interface>
- <requires lib="gtk+" version="3.0"/>
+ <requires lib="gtk+" version="3.10"/>
<object class="GtkDialog" id="about_dialog">
<property name="can_focus">False</property>
<property name="title" translatable="yes">About TimeLog</property>
@@ -19,12 +19,12 @@
<property name="layout_style">end</property>
<child>
<object class="GtkButton" id="ok_button">
- <property name="label">gtk-ok</property>
+ <property name="label" translatable="yes">_OK</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can_default">True</property>
<property name="receives_default">False</property>
- <property name="use_stock">True</property>
+ <property name="use_underline">True</property>
</object>
<packing>
<property name="expand">False</property>
@@ -41,10 +41,11 @@
</packing>
</child>
<child>
- <object class="GtkVBox" id="vbox2">
+ <object class="GtkBox" id="vbox2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">16</property>
+ <property name="orientation">vertical</property>
<child>
<object class="GtkLabel" id="about_text">
<property name="visible">True</property>
@@ -95,12 +96,11 @@ GTimeLog is a time tracking application.
</object>
</child>
<child>
- <object class="GtkImageMenuItem" id="appind_quit">
- <property name="label">gtk-quit</property>
+ <object class="GtkMenuItem" id="appind_quit">
<property name="visible">True</property>
<property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Quit</property>
<property name="use_underline">True</property>
- <property name="use_stock">True</property>
<signal name="activate" handler="on_quit_activate" swapped="no"/>
</object>
</child>
@@ -122,12 +122,12 @@ GTimeLog is a time tracking application.
<property name="layout_style">end</property>
<child>
<object class="GtkButton" id="cancelbutton1">
- <property name="label">gtk-cancel</property>
+ <property name="label" translatable="yes">_Cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can_default">True</property>
<property name="receives_default">False</property>
- <property name="use_stock">True</property>
+ <property name="use_underline">True</property>
</object>
<packing>
<property name="expand">False</property>
@@ -137,13 +137,13 @@ GTimeLog is a time tracking application.
</child>
<child>
<object class="GtkButton" id="okbutton1">
- <property name="label">gtk-ok</property>
+ <property name="label" translatable="yes">_OK</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can_default">True</property>
<property name="has_default">True</property>
<property name="receives_default">False</property>
- <property name="use_stock">True</property>
+ <property name="use_underline">True</property>
</object>
<packing>
<property name="expand">False</property>
@@ -186,9 +186,10 @@ GTimeLog is a time tracking application.
<property name="default_height">500</property>
<property name="icon">gtimelog.png</property>
<child>
- <object class="GtkVBox" id="vbox1">
+ <object class="GtkBox" id="vbox1">
<property name="visible">True</property>
<property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
<child>
<object class="GtkMenuBar" id="main_menu">
<property name="visible">True</property>
@@ -203,34 +204,31 @@ GTimeLog is a time tracking application.
<object class="GtkMenu" id="menuitem1_menu">
<property name="can_focus">False</property>
<child>
- <object class="GtkImageMenuItem" id="reload">
- <property name="label">_Reload</property>
+ <object class="GtkMenuItem" id="reload">
<property name="visible">True</property>
<property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Reload</property>
<property name="use_underline">True</property>
- <property name="use_stock">True</property>
<signal name="activate" handler="on_reread_activate" swapped="no"/>
<accelerator key="R" signal="activate" modifiers="GDK_CONTROL_MASK"/>
</object>
</child>
<child>
- <object class="GtkImageMenuItem" id="edit_timelog">
- <property name="label">_Edit timelog.txt</property>
+ <object class="GtkMenuItem" id="edit_timelog">
<property name="visible">True</property>
<property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Edit timelog.txt</property>
<property name="use_underline">True</property>
- <property name="use_stock">True</property>
<signal name="activate" handler="on_edit_timelog_activate" swapped="no"/>
<accelerator key="E" signal="activate" modifiers="GDK_CONTROL_MASK"/>
</object>
</child>
<child>
- <object class="GtkImageMenuItem" id="main_quit">
- <property name="label">gtk-quit</property>
+ <object class="GtkMenuItem" id="main_quit">
<property name="visible">True</property>
<property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Quit</property>
<property name="use_underline">True</property>
- <property name="use_stock">True</property>
<signal name="activate" handler="on_quit_activate" swapped="no"/>
<accelerator key="q" signal="activate" modifiers="GDK_CONTROL_MASK"/>
</object>
@@ -265,9 +263,9 @@ GTimeLog is a time tracking application.
<property name="label" translatable="yes">_Grouped</property>
<property name="use_underline">True</property>
<property name="active">True</property>
+ <property name="group">chronological</property>
<signal name="activate" handler="on_grouped_activate" swapped="no"/>
<accelerator key="2" signal="activate" modifiers="GDK_MOD1_MASK"/>
- <property name="group">chronological</property>
</object>
</child>
<child>
@@ -276,9 +274,9 @@ GTimeLog is a time tracking application.
<property name="can_focus">False</property>
<property name="label" translatable="yes">_Summary</property>
<property name="use_underline">True</property>
+ <property name="group">chronological</property>
<signal name="activate" handler="on_summary_activate" swapped="no"/>
<accelerator key="3" signal="activate" modifiers="GDK_MOD1_MASK"/>
- <property name="group">chronological</property>
</object>
</child>
<child>
@@ -313,103 +311,93 @@ GTimeLog is a time tracking application.
<object class="GtkMenu" id="menuitem2_menu">
<property name="can_focus">False</property>
<child>
- <object class="GtkImageMenuItem" id="daily_report">
- <property name="label">_Daily Report</property>
+ <object class="GtkMenuItem" id="daily_report">
<property name="visible">True</property>
<property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Daily Report</property>
<property name="use_underline">True</property>
- <property name="use_stock">True</property>
<signal name="activate" handler="on_daily_report_activate" swapped="no"/>
<accelerator key="D" signal="activate" modifiers="GDK_CONTROL_MASK"/>
</object>
</child>
<child>
- <object class="GtkImageMenuItem" id="yesterdays_report">
- <property name="label">Daily Report for _Yesterday</property>
+ <object class="GtkMenuItem" id="yesterdays_report">
<property name="visible">True</property>
<property name="can_focus">False</property>
+ <property name="label" translatable="yes">Daily Report for _Yesterday</property>
<property name="use_underline">True</property>
- <property name="use_stock">True</property>
<signal name="activate" handler="on_yesterdays_report_activate" swapped="no"/>
</object>
</child>
<child>
- <object class="GtkImageMenuItem" id="previous_day_report">
- <property name="label">Daily Report for a _Previous Day...</property>
+ <object class="GtkMenuItem" id="previous_day_report">
<property name="visible">True</property>
<property name="can_focus">False</property>
+ <property name="label" translatable="yes">Daily Report for a _Previous Day...</property>
<property name="use_underline">True</property>
- <property name="use_stock">True</property>
<signal name="activate" handler="on_previous_day_report_activate" swapped="no"/>
</object>
</child>
<child>
- <object class="GtkImageMenuItem" id="weekly_report">
- <property name="label">_Weekly Report</property>
+ <object class="GtkMenuItem" id="weekly_report">
<property name="visible">True</property>
<property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Weekly Report</property>
<property name="use_underline">True</property>
- <property name="use_stock">True</property>
<signal name="activate" handler="on_weekly_report_activate" swapped="no"/>
</object>
</child>
<child>
- <object class="GtkImageMenuItem" id="last_weeks_report">
- <property name="label">Weekly Report for _Last Week</property>
+ <object class="GtkMenuItem" id="last_weeks_report">
<property name="visible">True</property>
<property name="can_focus">False</property>
+ <property name="label" translatable="yes">Weekly Report for _Last Week</property>
<property name="use_underline">True</property>
- <property name="use_stock">True</property>
<signal name="activate" handler="on_last_weeks_report_activate" swapped="no"/>
</object>
</child>
<child>
- <object class="GtkImageMenuItem" id="previous_week_report">
- <property name="label">Weekly Report for a Pre_vious Week...</property>
+ <object class="GtkMenuItem" id="previous_week_report">
<property name="visible">True</property>
<property name="can_focus">False</property>
+ <property name="label" translatable="yes">Weekly Report for a Pre_vious Week...</property>
<property name="use_underline">True</property>
- <property name="use_stock">True</property>
<signal name="activate" handler="on_previous_week_report_activate" swapped="no"/>
</object>
</child>
<child>
- <object class="GtkImageMenuItem" id="monthly_report">
- <property name="label">_Monthly Report</property>
+ <object class="GtkMenuItem" id="monthly_report">
<property name="visible">True</property>
<property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Monthly Report</property>
<property name="use_underline">True</property>
- <property name="use_stock">True</property>
<signal name="activate" handler="on_monthly_report_activate" swapped="no"/>
</object>
</child>
<child>
- <object class="GtkImageMenuItem" id="last_month_report">
- <property name="label">Monthly Report for Last Month</property>
+ <object class="GtkMenuItem" id="last_month_report">
<property name="visible">True</property>
<property name="can_focus">False</property>
+ <property name="label" translatable="yes">Monthly Report for Last Month</property>
<property name="use_underline">True</property>
- <property name="use_stock">True</property>
<signal name="activate" handler="on_last_month_report_activate" swapped="no"/>
</object>
</child>
<child>
- <object class="GtkImageMenuItem" id="previous_month_report">
- <property name="label">Monthly Report for a Previous Month...</property>
+ <object class="GtkMenuItem" id="previous_month_report">
<property name="visible">True</property>
<property name="can_focus">False</property>
+ <property name="label" translatable="yes">Monthly Report for a Previous Month...</property>
<property name="use_underline">True</property>
- <property name="use_stock">True</property>
<signal name="activate" handler="on_previous_month_report_activate" swapped="no"/>
</object>
</child>
<child>
- <object class="GtkImageMenuItem" id="custom_range_report">
- <property name="label">Report for a Custom Date Range...</property>
+ <object class="GtkMenuItem" id="custom_range_report">
<property name="visible">True</property>
<property name="can_focus">False</property>
+ <property name="label" translatable="yes">Report for a Custom Date Range...</property>
<property name="use_underline">True</property>
- <property name="use_stock">True</property>
<signal name="activate" handler="on_custom_range_report_activate" swapped="no"/>
</object>
</child>
@@ -420,22 +408,20 @@ GTimeLog is a time tracking application.
</object>
</child>
<child>
- <object class="GtkImageMenuItem" id="open_complete_spreadsheet">
- <property name="label">_Complete Report in Spreadsheet</property>
+ <object class="GtkMenuItem" id="open_complete_spreadsheet">
<property name="visible">True</property>
<property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Complete Report in Spreadsheet</property>
<property name="use_underline">True</property>
- <property name="use_stock">True</property>
<signal name="activate" handler="on_open_complete_spreadsheet_activate" swapped="no"/>
</object>
</child>
<child>
- <object class="GtkImageMenuItem" id="open_slack_spreadsheet">
- <property name="label">Work/_Slacking stats in Spreadsheet</property>
+ <object class="GtkMenuItem" id="open_slack_spreadsheet">
<property name="visible">True</property>
<property name="can_focus">False</property>
+ <property name="label" translatable="yes">Work/_Slacking stats in Spreadsheet</property>
<property name="use_underline">True</property>
- <property name="use_stock">True</property>
<signal name="activate" handler="on_open_slack_spreadsheet_activate" swapped="no"/>
</object>
</child>
@@ -453,12 +439,11 @@ GTimeLog is a time tracking application.
<object class="GtkMenu" id="menuitem4_menu">
<property name="can_focus">False</property>
<child>
- <object class="GtkImageMenuItem" id="about">
- <property name="label">gtk-about</property>
+ <object class="GtkMenuItem" id="about">
<property name="visible">True</property>
<property name="can_focus">False</property>
+ <property name="label" translatable="yes">_About</property>
<property name="use_underline">True</property>
- <property name="use_stock">True</property>
<signal name="activate" handler="on_about_activate" swapped="no"/>
</object>
</child>
@@ -484,15 +469,16 @@ GTimeLog is a time tracking application.
</packing>
</child>
<child>
- <object class="GtkHPaned" id="hpaned1">
+ <object class="GtkPaned" id="hpaned1">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="position">600</property>
<property name="position_set">True</property>
<child>
- <object class="GtkVBox" id="vbox3">
+ <object class="GtkBox" id="vbox3">
<property name="visible">True</property>
<property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
<child>
<object class="GtkToolbar" id="toolbar">
<property name="visible">True</property>
@@ -503,7 +489,7 @@ GTimeLog is a time tracking application.
<property name="can_focus">False</property>
<property name="label" translatable="yes">Back</property>
<property name="use_underline">True</property>
- <property name="stock_id">gtk-go-back</property>
+ <property name="icon_name">go-previous</property>
<signal name="clicked" handler="on_back_toolbutton_clicked" swapped="no"/>
<accelerator key="Left" signal="clicked" modifiers="GDK_MOD1_MASK"/>
</object>
@@ -520,7 +506,6 @@ GTimeLog is a time tracking application.
<object class="GtkLabel" id="current_view_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
- <property name="xpad">6</property>
<property name="label" translatable="yes">Tuesday, 2012-01-31 (week 05)</property>
</object>
</child>
@@ -536,7 +521,7 @@ GTimeLog is a time tracking application.
<property name="can_focus">False</property>
<property name="label" translatable="yes">Forward</property>
<property name="use_underline">True</property>
- <property name="stock_id">gtk-go-forward</property>
+ <property name="icon_name">go-next</property>
<signal name="clicked" handler="on_forward_toolbutton_clicked" swapped="no"/>
<accelerator key="Right" signal="clicked" modifiers="GDK_MOD1_MASK"/>
</object>
@@ -550,9 +535,9 @@ GTimeLog is a time tracking application.
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Today</property>
- <property name="label" translatable="yes">toolbutton2</property>
+ <property name="label" translatable="yes">Today</property>
<property name="use_underline">True</property>
- <property name="stock_id">gtk-goto-last</property>
+ <property name="icon_name">go-last</property>
<signal name="clicked" handler="on_today_toolbutton_clicked" swapped="no"/>
<accelerator key="Home" signal="clicked" modifiers="GDK_MOD1_MASK"/>
</object>
@@ -598,14 +583,16 @@ GTimeLog is a time tracking application.
</packing>
</child>
<child>
- <object class="GtkVBox" id="task_list_pane">
+ <object class="GtkBox" id="task_list_pane">
<property name="visible">True</property>
<property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
- <object class="GtkVBox" id="task_list_pane_outer_vbox">
+ <object class="GtkBox" id="task_list_pane_outer_vbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
<child>
<object class="GtkToolbar" id="task_pane_toolbar">
<property name="visible">True</property>
@@ -618,11 +605,11 @@ GTimeLog is a time tracking application.
<object class="GtkLabel" id="task_pane_title_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
- <property name="xalign">0</property>
<property name="label" translatable="yes">_Tasks</property>
<property name="use_markup">True</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">task_list</property>
+ <property name="xalign">0</property>
</object>
</child>
</object>
@@ -635,9 +622,9 @@ GTimeLog is a time tracking application.
<object class="GtkToolButton" id="task_pane_close_toolbutton">
<property name="visible">True</property>
<property name="can_focus">False</property>
- <property name="label" translatable="yes">toolbutton2</property>
+ <property name="label" translatable="yes">Close</property>
<property name="use_underline">True</property>
- <property name="stock_id">gtk-close</property>
+ <property name="icon_name">window-close</property>
<signal name="clicked" handler="on_task_pane_close_button_activate" swapped="no"/>
</object>
<packing>
@@ -653,14 +640,15 @@ GTimeLog is a time tracking application.
</packing>
</child>
<child>
- <object class="GtkHBox" id="task_pane_hbox">
+ <object class="GtkBox" id="task_pane_hbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">6</property>
<child>
- <object class="GtkVBox" id="task_list_pane_vbox">
+ <object class="GtkBox" id="task_list_pane_vbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkScrolledWindow" id="scrolledwindow2">
@@ -703,9 +691,10 @@ GTimeLog is a time tracking application.
</packing>
</child>
<child>
- <object class="GtkVBox" id="task_list_pane_hack_vbox">
+ <object class="GtkBox" id="task_list_pane_hack_vbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
<child>
<placeholder/>
</child>
@@ -744,7 +733,7 @@ GTimeLog is a time tracking application.
</packing>
</child>
<child>
- <object class="GtkHBox" id="hbox1">
+ <object class="GtkBox" id="hbox1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">4</property>
@@ -753,7 +742,7 @@ GTimeLog is a time tracking application.
<object class="GtkLabel" id="time_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
- <property name="label" translatable="yes">00:12</property>
+ <property name="label">00:12</property>
<property name="mnemonic_widget">task_entry</property>
</object>
<packing>
@@ -806,22 +795,20 @@ GTimeLog is a time tracking application.
<object class="GtkMenu" id="task_list_popup_menu">
<property name="can_focus">False</property>
<child>
- <object class="GtkImageMenuItem" id="task_list_reload">
- <property name="label">gtk-refresh</property>
+ <object class="GtkMenuItem" id="task_list_reload">
<property name="visible">True</property>
<property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Reload</property>
<property name="use_underline">True</property>
- <property name="use_stock">True</property>
<signal name="activate" handler="on_task_list_reload" swapped="no"/>
</object>
</child>
<child>
- <object class="GtkImageMenuItem" id="task_list_edit">
- <property name="label">gtk-edit</property>
+ <object class="GtkMenuItem" id="task_list_edit">
<property name="visible">True</property>
<property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Edit</property>
<property name="use_underline">True</property>
- <property name="use_stock">True</property>
<signal name="activate" handler="on_task_list_edit" swapped="no"/>
</object>
</child>
@@ -847,12 +834,11 @@ GTimeLog is a time tracking application.
</object>
</child>
<child>
- <object class="GtkImageMenuItem" id="popup_quit">
- <property name="label">gtk-quit</property>
+ <object class="GtkMenuItem" id="popup_quit">
<property name="visible">True</property>
<property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Quit</property>
<property name="use_underline">True</property>
- <property name="use_stock">True</property>
<signal name="activate" handler="on_quit_activate" swapped="no"/>
</object>
</child>
@@ -874,12 +860,12 @@ GTimeLog is a time tracking application.
<property name="layout_style">end</property>
<child>
<object class="GtkButton" id="cancelbutton2">
- <property name="label">gtk-cancel</property>
+ <property name="label" translatable="yes">_Cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can_default">True</property>
<property name="receives_default">True</property>
- <property name="use_stock">True</property>
+ <property name="use_underline">True</property>
</object>
<packing>
<property name="expand">False</property>
@@ -889,13 +875,13 @@ GTimeLog is a time tracking application.
</child>
<child>
<object class="GtkButton" id="okbutton2">
- <property name="label">gtk-ok</property>
+ <property name="label" translatable="yes">_OK</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can_default">True</property>
<property name="has_default">True</property>
<property name="receives_default">True</property>
- <property name="use_stock">True</property>
+ <property name="use_underline">True</property>
</object>
<packing>
<property name="expand">False</property>
diff --git a/src/gtimelog/main.py b/src/gtimelog/main.py
index 32ce612..b1ff22b 100644
--- a/src/gtimelog/main.py
+++ b/src/gtimelog/main.py
@@ -4,14 +4,12 @@
__metaclass__ = type
import os
-import re
import sys
import errno
import codecs
import signal
import logging
import datetime
-import optparse
import tempfile
import gtimelog
@@ -25,94 +23,15 @@ try:
except NameError:
unicode = str
-
-# Which Gnome toolkit should we use? Prior to 0.7, pygtk was the default with
-# a fallback to gi (gobject introspection), except on Ubuntu where gi was
-# forced. With 0.7, gi was made the default in upstream, so the Ubuntu
-# specific patch isn't necessary.
-
-if '--prefer-pygtk' in sys.argv:
- sys.argv.remove('--prefer-pygtk')
- try:
- import pygtk
- toolkit = 'pygtk'
- except ImportError:
- try:
- import gi
- toolkit = 'gi'
- except ImportError:
- sys.exit("Please install pygobject or pygtk")
-else:
- try:
- import gi
- toolkit = 'gi'
- except ImportError:
- try:
- import pygtk
- toolkit = 'pygtk'
- except ImportError:
- sys.exit("Please install pygobject or pygtk")
-
-
-if toolkit == 'gi':
- from gi.repository import GObject as gobject
- from gi.repository import Gdk as gdk
- from gi.repository import Gtk as gtk
- from gi.repository import Pango as pango
- # These are hacks until we fully switch to GI.
- try:
- PANGO_ALIGN_LEFT = pango.TabAlign.LEFT
- except AttributeError:
- # Backwards compatible for older Pango versions with broken GIR.
- PANGO_ALIGN_LEFT = pango.TabAlign.TAB_LEFT
- GTK_RESPONSE_OK = gtk.ResponseType.OK
- gtk_status_icon_new = gtk.StatusIcon.new_from_file
- pango_tabarray_new = pango.TabArray.new
-
- if gtk._version.startswith('2'):
- gtk_version = 2
- else:
- gtk_version = 3
-
- try:
- if gtk._version.startswith('2'):
- from gi.repository import AppIndicator
- else:
- from gi.repository import AppIndicator3 as AppIndicator
- new_app_indicator = AppIndicator.Indicator.new
- APPINDICATOR_CATEGORY = (
- AppIndicator.IndicatorCategory.APPLICATION_STATUS)
- APPINDICATOR_ACTIVE = AppIndicator.IndicatorStatus.ACTIVE
- except (ImportError, gi._gi.RepositoryError):
- new_app_indicator = None
-else:
- pygtk.require('2.0')
- import gobject
- import gtk
- from gtk import gdk as gdk
- import pango
-
- gtk_version = 2
- PANGO_ALIGN_LEFT = pango.TAB_LEFT
- GTK_RESPONSE_OK = gtk.RESPONSE_OK
- gtk_status_icon_new = gtk.status_icon_new_from_file
- pango_tabarray_new = pango.TabArray
-
- try:
- import appindicator
- new_app_indicator = appindicator.Indicator
- APPINDICATOR_CATEGORY = appindicator.CATEGORY_APPLICATION_STATUS
- APPINDICATOR_ACTIVE = appindicator.STATUS_ACTIVE
- except ImportError:
- # apt-get install python-appindicator on Ubuntu
- new_app_indicator = None
+import gi
+gi.require_version('Gtk', '3.0')
+from gi.repository import GObject, GLib, Gdk, Gio, Gtk, Pango
try:
- import dbus
- import dbus.service
- import dbus.mainloop.glib
+ from gi.repository import AppIndicator3
+ have_app_indicator = True
except ImportError:
- dbus = None
+ have_app_indicator = False
from gtimelog import __version__
@@ -134,32 +53,70 @@ if not os.path.exists(icon_file_bright):
from gtimelog.settings import Settings
from gtimelog.timelog import (
- format_duration, format_duration_short, uniq,
+ format_duration, uniq,
Reports, TimeLog, TaskList, RemoteTaskList)
class IconChooser:
+ """Picks the right icon for dark or bright panel backgrounds.
+
+ Well, tries to pick it. I couldn't find a way to determine the color of
+ the panel, so I cheated by assuming it'll be the same as the color of
+ the menu bar. This is wrong for many popular themes, including:
+
+ - Adwaita
+ - Radiance
+ - Ambiance
+
+ which is why I have to maintain a list of per-theme overrides.
+ """
+
+ icon_for_background = dict(
+ # We want sufficient contrast, so:
+ # - use dark icon for bright backgrounds
+ # - use bright icon for dark backgrounds
+ bright=icon_file_dark,
+ dark=icon_file_bright,
+ )
+
+ theme_overrides = {
+ # when the menu bar color logic gets this wrong
+ 'Adwaita': 'dark', # but probably only under gnome-shell
+ 'Ambiance': 'dark',
+ 'Radiance': 'bright',
+ }
@property
def icon_name(self):
- # XXX assumes the panel's color matches a menu bar's color, which is
- # not necessarily the case! this logic works for, say,
- # Ambiance/Radiance, but it gets New Wave and Dark Room wrong.
- if toolkit == 'gi':
- menu_bar = gtk.MenuBar()
- # need to hold a reference to menu_bar to avoid LP#1016212
- style = menu_bar.get_style_context()
- color = style.get_color(gtk.StateFlags.NORMAL)
- value = (color.red + color.green + color.blue) / 3
- else:
- style = gtk.MenuBar().rc_get_style()
- color = style.text[gtk.STATE_NORMAL]
- value = color.value
- filename = icon_file_bright if value >= 0.5 else icon_file_dark
- log.debug('Menu bar color: (%g, %g, %g), averages to %g; picking %s',
- color.red, color.green, color.blue, value, filename)
+ theme_name = self.get_gtk_theme()
+ background = self.get_background()
+ if theme_name in self.theme_overrides:
+ background = self.theme_overrides[theme_name]
+ log.debug('Overriding background to %s for %s', background, theme_name)
+ filename = self.icon_for_background[background]
+ log.debug('For %s background picking icon %s', background, filename)
return filename
+ def get_gtk_theme(self):
+ theme_name = Gtk.Settings.get_default().props.gtk_theme_name
+ log.debug('GTK+ theme: %s', theme_name)
+ override = os.environ.get('GTK_THEME')
+ if override:
+ log.debug('GTK_THEME overrides the theme to %s', override)
+ theme_name = override.partition(':')[0]
+ return theme_name
+
+ def get_background(self):
+ menu_bar = Gtk.MenuBar()
+ # need to hold a reference to menu_bar to avoid LP#1016212
+ style = menu_bar.get_style_context()
+ color = style.get_color(Gtk.StateFlags.NORMAL)
+ value = (color.red + color.green + color.blue) / 3
+ background = 'bright' if value >= 0.5 else 'dark'
+ log.debug('Menu bar color: (%.3g, %.3g, %.3g), averages to %.3g (%s)',
+ color.red, color.green, color.blue, value, background)
+ return background
+
class SimpleStatusIcon(IconChooser):
"""Status icon for gtimelog in the notification area."""
@@ -168,31 +125,17 @@ class SimpleStatusIcon(IconChooser):
self.gtimelog_window = gtimelog_window
self.timelog = gtimelog_window.timelog
self.trayicon = None
- if not hasattr(gtk, 'StatusIcon'):
- # You must be using a very old PyGtk.
- return
- self.icon = gtk_status_icon_new(self.icon_name)
+ self.icon = Gtk.StatusIcon.new_from_file(self.icon_name)
self.last_tick = False
self.tick()
self.icon.connect('activate', self.on_activate)
self.icon.connect('popup-menu', self.on_popup_menu)
- if gtk_version == 2:
- self.gtimelog_window.main_window.connect(
- 'style-set', self.on_style_set)
- else: # assume Gtk+ 3
- self.gtimelog_window.main_window.connect(
- 'style-updated', self.on_style_set)
- gobject.timeout_add_seconds(1, self.tick)
+ self.gtimelog_window.main_window.connect(
+ 'style-updated', self.on_style_set)
+ GLib.timeout_add_seconds(1, self.tick)
self.gtimelog_window.entry_watchers.append(self.entry_added)
self.gtimelog_window.tray_icon = self
- def available(self):
- """Is the icon supported by this system?
-
- SimpleStatusIcon needs PyGtk 2.10 or newer
- """
- return self.icon is not None
-
def on_style_set(self, *args):
"""The user chose a different theme."""
self.icon.set_from_file(self.icon_name)
@@ -204,14 +147,9 @@ class SimpleStatusIcon(IconChooser):
def on_popup_menu(self, widget, button, activate_time):
"""The user clicked on the icon."""
tray_icon_popup_menu = self.gtimelog_window.tray_icon_popup_menu
- if toolkit == "gi":
- tray_icon_popup_menu.popup(
- None, None, gtk.StatusIcon.position_menu,
- self.icon, button, activate_time)
- else:
- tray_icon_popup_menu.popup(
- None, None, gtk.status_icon_position_menu,
- button, activate_time, self.icon)
+ tray_icon_popup_menu.popup(
+ None, None, Gtk.StatusIcon.position_menu,
+ self.icon, button, activate_time)
def entry_added(self, entry):
"""An entry has been added."""
@@ -244,153 +182,24 @@ class SimpleStatusIcon(IconChooser):
class AppIndicator(IconChooser):
"""Ubuntu's application indicator for gtimelog."""
- # XXX: on Ubuntu 10.04 the app indicator apparently doesn't understand
- # set_icon('/absolute/path'), and so gtimelog ends up being without an
- # icon. I don't know if I want to continue supporting Ubuntu 10.04.
-
def __init__(self, gtimelog_window):
self.gtimelog_window = gtimelog_window
self.timelog = gtimelog_window.timelog
self.indicator = None
- if new_app_indicator is None:
- return
- self.indicator = new_app_indicator(
- 'gtimelog', self.icon_name, APPINDICATOR_CATEGORY)
- self.indicator.set_status(APPINDICATOR_ACTIVE)
- self.indicator.set_menu(gtimelog_window.app_indicator_menu)
- self.gtimelog_window.tray_icon = self
- if gtk_version == 2:
- self.gtimelog_window.main_window.connect(
- 'style-set', self.on_style_set)
- else: # assume Gtk+ 3
+ if have_app_indicator:
+ self.indicator = AppIndicator3.Indicator.new(
+ 'gtimelog', self.icon_name, AppIndicator3.IndicatorCategory.APPLICATION_STATUS)
+ self.indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
+ self.indicator.set_menu(gtimelog_window.app_indicator_menu)
+ self.gtimelog_window.tray_icon = self
self.gtimelog_window.main_window.connect(
'style-updated', self.on_style_set)
- def available(self):
- """Is the icon supported by this system?
-
- AppIndicator needs python-appindicator
- """
- return self.indicator is not None
-
def on_style_set(self, *args):
"""The user chose a different theme."""
self.indicator.set_icon(self.icon_name)
-class OldTrayIcon(IconChooser):
- """Old tray icon for gtimelog, shows a ticking clock.
-
- Uses the old and deprecated egg.trayicon module.
- """
-
- def __init__(self, gtimelog_window):
- self.gtimelog_window = gtimelog_window
- self.timelog = gtimelog_window.timelog
- self.trayicon = None
- try:
- import egg.trayicon
- except ImportError:
- # Nothing to do here, move along or install python-gnome2-extras
- # which was later renamed to python-eggtrayicon.
- return
- self.eventbox = gtk.EventBox()
- hbox = gtk.HBox()
- self.icon = gtk.Image()
- self.icon.set_from_file(self.icon_name)
- hbox.add(self.icon)
- self.time_label = gtk.Label()
- hbox.add(self.time_label)
- self.eventbox.add(hbox)
- self.trayicon = egg.trayicon.TrayIcon('GTimeLog')
- self.trayicon.add(self.eventbox)
- self.last_tick = False
- self.tick(force_update=True)
- self.trayicon.show_all()
- if gtk_version == 2:
- self.gtimelog_window.main_window.connect(
- 'style-set', self.on_style_set)
- else: # assume Gtk+ 3
- self.gtimelog_window.main_window.connect(
- 'style-updated', self.on_style_set)
- tray_icon_popup_menu = gtimelog_window.tray_icon_popup_menu
- self.eventbox.connect_object(
- 'button-press-event', self.on_press, tray_icon_popup_menu)
- self.eventbox.connect('button-release-event', self.on_release)
- gobject.timeout_add_seconds(1, self.tick)
- self.gtimelog_window.entry_watchers.append(self.entry_added)
- self.gtimelog_window.tray_icon = self
-
- def available(self):
- """Is the icon supported by this system?
-
- OldTrayIcon needs egg.trayicon, which is now deprecated and likely
- no longer available in modern Linux distributions.
- """
- return self.trayicon is not None
-
- def on_style_set(self, *args):
- """The user chose a different theme."""
- self.icon.set_from_file(self.icon_name)
-
- def on_press(self, widget, event):
- """A mouse button was pressed on the tray icon label."""
- if event.button != 3:
- return
- main_window = self.gtimelog_window.main_window
- # This should be unnecessary, as we now show/hide menu items
- # immediatelly after showing/hiding the main window.
- if main_window.get_property('visible'):
- self.gtimelog_window.tray_show.hide()
- self.gtimelog_window.tray_hide.show()
- else:
- self.gtimelog_window.tray_show.show()
- self.gtimelog_window.tray_hide.hide()
- # I'm assuming toolkit == 'pygtk' here, since there's now way the old
- # EggTrayIcon can work with PyGI/Gtk+ 3.
- widget.popup(None, None, None, event.button, event.time)
-
- def on_release(self, widget, event):
- """A mouse button was released on the tray icon label."""
- if event.button != 1:
- return
- self.gtimelog_window.toggle_visible()
-
- def entry_added(self, entry):
- """An entry has been added."""
- self.tick(force_update=True)
-
- def tick(self, force_update=False):
- """Tick every second."""
- now = datetime.datetime.now().replace(second=0, microsecond=0)
- if now != self.last_tick or force_update: # Do not eat CPU too much
- self.last_tick = now
- last_time = self.timelog.window.last_time()
- if last_time is None:
- self.time_label.set_text(now.strftime('%H:%M'))
- else:
- self.time_label.set_text(
- format_duration_short(now - last_time))
- self.trayicon.set_tooltip_text(self.tip())
- return True
-
- def tip(self):
- """Compute tooltip text."""
- current_task = self.gtimelog_window.task_entry.get_text()
- if not current_task:
- current_task = 'nothing'
- tip = 'GTimeLog: working on {0}'.format(current_task)
- total_work, total_slacking = self.timelog.window.totals()
- tip += '\nWork done today: {0}'.format(format_duration(total_work))
- time_left = self.gtimelog_window.time_left_at_work(total_work)
- if time_left is not None:
- if time_left < datetime.timedelta(0):
- time_left = datetime.timedelta(0)
- tip += '\nTime left at work: {0}'.format(
- format_duration(time_left))
- return tip
-
-
class MainWindow:
"""Main application window."""
@@ -420,7 +229,7 @@ class MainWindow:
def _init_ui(self):
"""Initialize the user interface."""
- builder = gtk.Builder()
+ builder = Gtk.Builder()
builder.add_from_file(ui_file)
# Set initial state of menu items *before* we hook up signals
chronological_menu_item = builder.get_object('chronological')
@@ -465,9 +274,9 @@ class MainWindow:
self.tasks.loaded_callback = self.task_list_loaded
self.tasks.error_callback = self.task_list_error
self.task_list = builder.get_object('task_list')
- self.task_store = gtk.TreeStore(str, str)
+ self.task_store = Gtk.TreeStore(str, str)
self.task_list.set_model(self.task_store)
- column = gtk.TreeViewColumn('Task', gtk.CellRendererText(), text=0)
+ column = Gtk.TreeViewColumn('Task', Gtk.CellRendererText(), text=0)
self.task_list.append_column(column)
self.task_list.connect('row_activated', self.task_list_row_activated)
self.task_list_popup_menu = builder.get_object('task_list_popup_menu')
@@ -486,9 +295,9 @@ class MainWindow:
self.add_button.connect('clicked', self.add_entry)
buffer = self.log_view.get_buffer()
self.log_buffer = buffer
- buffer.create_tag('today', foreground='blue')
- buffer.create_tag('duration', foreground='red')
- buffer.create_tag('time', foreground='green')
+ buffer.create_tag('today', foreground='#204a87') # Tango dark blue
+ buffer.create_tag('duration', foreground='#ce5c00') # Tango dark orange
+ buffer.create_tag('time', foreground='#4e9a06') # Tango dark green
buffer.create_tag('slacking', foreground='gray')
self.set_up_task_list()
self.set_up_completion()
@@ -496,7 +305,10 @@ class MainWindow:
self.populate_log()
self.update_show_checkbox()
self.tick(True)
- gobject.timeout_add_seconds(1, self.tick)
+ GLib.timeout_add_seconds(1, self.tick)
+
+ def quit(self):
+ self.main_window.destroy()
def set_up_log_view_columns(self):
"""Set up tab stops in the log view."""
@@ -504,9 +316,9 @@ class MainWindow:
self.log_view.realize()
pango_context = self.log_view.get_pango_context()
em = pango_context.get_font_description().get_size()
- tabs = pango_tabarray_new(2, False)
- tabs.set_tab(0, PANGO_ALIGN_LEFT, 9 * em)
- tabs.set_tab(1, PANGO_ALIGN_LEFT, 12 * em)
+ tabs = Pango.TabArray.new(2, False)
+ tabs.set_tab(0, Pango.TabAlign.LEFT, 9 * em)
+ tabs.set_tab(1, Pango.TabAlign.LEFT, 12 * em)
self.log_view.set_tabs(tabs)
def w(self, text, tag=None):
@@ -645,7 +457,7 @@ class MainWindow:
def write_item(self, item):
buffer = self.log_buffer
- start, stop, duration, entry = item
+ start, stop, duration, tags, entry = item
self.w(format_duration(duration), 'duration')
period = '\t({0}-{1})\t'.format(
start.strftime('%H:%M'), stop.strftime('%H:%M'))
@@ -700,12 +512,12 @@ class MainWindow:
if not self.settings.enable_gtk_completion:
self.have_completion = False
return
- self.have_completion = hasattr(gtk, 'EntryCompletion')
+ self.have_completion = hasattr(Gtk, 'EntryCompletion')
if not self.have_completion:
return
- self.completion_choices = gtk.ListStore(str)
+ self.completion_choices = Gtk.ListStore(str)
self.completion_choices_as_set = set()
- completion = gtk.EntryCompletion()
+ completion = Gtk.EntryCompletion()
completion.set_model(self.completion_choices)
completion.set_text_column(0)
self.task_entry.set_completion(completion)
@@ -737,7 +549,7 @@ class MainWindow:
self.on_hide_activate()
return True
else:
- gtk.main_quit()
+ self.quit()
return False
def close_about_dialog(self, widget):
@@ -798,7 +610,7 @@ class MainWindow:
def on_quit_activate(self, widget):
"""File -> Quit selected"""
- gtk.main_quit()
+ self.quit()
def on_about_activate(self, widget):
"""Help -> About selected"""
@@ -855,7 +667,7 @@ class MainWindow:
Returns either a datetime.date, or None.
"""
- if self.calendar_dialog.run() == GTK_RESPONSE_OK:
+ if self.calendar_dialog.run() == Gtk.ResponseType.OK:
y, m1, d = self.calendar.get_date()
day = datetime.date(y, m1 + 1, d)
else:
@@ -868,7 +680,7 @@ class MainWindow:
Returns either a tuple with two datetime.date objects, or (None, None).
"""
- if self.two_calendar_dialog.run() == GTK_RESPONSE_OK:
+ if self.two_calendar_dialog.run() == Gtk.ResponseType.OK:
y1, m1, d1 = self.calendar1.get_date()
y2, m2, d2 = self.calendar2.get_date()
first = datetime.date(y1, m1 + 1, d1)
@@ -880,7 +692,7 @@ class MainWindow:
def on_calendar_day_selected_double_click(self, widget):
"""Double-click on a calendar day: close the dialog."""
- self.calendar_dialog.response(GTK_RESPONSE_OK)
+ self.calendar_dialog.response(Gtk.ResponseType.OK)
def weekly_window(self, day=None):
if not day:
@@ -983,14 +795,14 @@ class MainWindow:
def on_open_complete_spreadsheet_activate(self, widget):
"""Report -> Complete Report in Spreadsheet"""
- tempfn = tempfile.mktemp(suffix='gtimelog.csv') # XXX unsafe!
+ tempfn = tempfile.mktemp(prefix='gtimelog-', suffix='.csv') # XXX unsafe!
with open(tempfn, 'w') as f:
self.timelog.whole_history().to_csv_complete(f)
self.spawn(self.settings.spreadsheet, tempfn)
def on_open_slack_spreadsheet_activate(self, widget):
"""Report -> Work/_Slacking stats in Spreadsheet"""
- tempfn = tempfile.mktemp(suffix='gtimelog.csv') # XXX unsafe!
+ tempfn = tempfile.mktemp(prefix='gtimelog-', suffix='.csv') # XXX unsafe!
with open(tempfn, 'w') as f:
self.timelog.whole_history().to_csv_daily(f)
self.spawn(self.settings.spreadsheet, tempfn)
@@ -1001,7 +813,7 @@ class MainWindow:
def mail(self, write_draft):
"""Send an email."""
- draftfn = tempfile.mktemp(suffix='gtimelog') # XXX unsafe!
+ draftfn = tempfile.mktemp(prefix='gtimelog-') # XXX unsafe!
with codecs.open(draftfn, 'w', encoding='UTF-8') as draft:
write_draft(draft, self.settings.email, self.settings.name)
self.spawn(self.settings.mailer, draftfn)
@@ -1042,6 +854,7 @@ class MainWindow:
model = treeview.get_model()
task = model[path][1]
self.task_entry.set_text(task)
+
def grab_focus():
self.task_entry.grab_focus()
self.task_entry.set_position(-1)
@@ -1049,14 +862,11 @@ class MainWindow:
# handled _after_ row-activated, which makes the tree control steal
# the focus back from the task entry. To avoid this, wait until all
# the events have been handled.
- gobject.idle_add(grab_focus)
+ GObject.idle_add(grab_focus)
def task_list_button_press(self, menu, event):
if event.button == 3:
- if toolkit == "gi":
- menu.popup(None, None, None, None, event.button, event.time)
- else:
- menu.popup(None, None, None, event.button, event.time)
+ menu.popup(None, None, None, None, event.button, event.time)
return True
else:
return False
@@ -1073,8 +883,8 @@ class MainWindow:
self.task_pane_info_label.set_text('Loading...')
self.task_pane_info_label.show()
# let the ui update become visible
- while gtk.events_pending():
- gtk.main_iteration()
+ while Gtk.events_pending():
+ Gtk.main_iteration()
def task_list_error(self):
self.task_list_loading_failed = True
@@ -1091,23 +901,23 @@ class MainWindow:
def task_entry_key_press(self, widget, event):
"""Handle key presses in task entry."""
- if event.keyval == gdk.keyval_from_name('Escape') and self.tray_icon:
+ if event.keyval == Gdk.keyval_from_name('Escape') and self.tray_icon:
self.on_hide_activate()
return True
- if event.keyval == gdk.keyval_from_name('Prior'):
+ if event.keyval == Gdk.keyval_from_name('Prior'):
self._do_history(1)
return True
- if event.keyval == gdk.keyval_from_name('Next'):
+ if event.keyval == Gdk.keyval_from_name('Next'):
self._do_history(-1)
return True
# XXX This interferes with the completion box. How do I determine
# whether the completion box is visible or not?
if self.have_completion:
return False
- if event.keyval == gdk.keyval_from_name('Up'):
+ if event.keyval == Gdk.keyval_from_name('Up'):
self._do_history(1)
return True
- if event.keyval == gdk.keyval_from_name('Down'):
+ if event.keyval == Gdk.keyval_from_name('Down'):
self._do_history(-1)
return True
return False
@@ -1144,29 +954,7 @@ class MainWindow:
self.jump_to_today()
entry = self._get_entry_text()
-
- now = None
- date_match = re.match(r'(\d\d):(\d\d)\s+', entry)
- delta_match = re.match(r'-([1-9]\d?|1\d\d)\s+', entry)
- if date_match:
- h = int(date_match.group(1))
- m = int(date_match.group(2))
- if 0 <= h < 24 and 0 <= m <= 60:
- now = datetime.datetime.now()
- now = now.replace(hour=h, minute=m, second=0, microsecond=0)
- if self.timelog.valid_time(now):
- entry = entry[date_match.end():]
- else:
- now = None
- if delta_match:
- seconds = int(delta_match.group()) * 60
- now = datetime.datetime.now().replace(second=0, microsecond=0)
- now += datetime.timedelta(seconds=seconds)
- if self.timelog.valid_time(now):
- entry = entry[delta_match.end():]
- else:
- now = None
-
+ entry, now = self.timelog.parse_correction(entry)
if not entry:
return
self.add_history(entry)
@@ -1213,215 +1001,173 @@ class MainWindow:
return True
-if dbus:
- INTERFACE = 'lt.pov.mg.gtimelog.Service'
- OBJECT_PATH = '/lt/pov/mg/gtimelog/Service'
- SERVICE = 'lt.pov.mg.gtimelog.GTimeLog'
-
- class Service(dbus.service.Object):
- """Our DBus service, used to communicate with the main instance."""
-
- def __init__(self, main_window):
- session_bus = dbus.SessionBus()
- connection = dbus.service.BusName(SERVICE, session_bus)
- dbus.service.Object.__init__(self, connection, OBJECT_PATH)
+def make_option(long_name, short_name=None, flags=0, arg=GLib.OptionArg.NONE,
+ arg_data=None, description=None, arg_description=None):
+ # surely something like this should exist inside PyGObject itself?!
+ option = GLib.OptionEntry()
+ option.long_name = long_name.lstrip('-')
+ option.short_name = 0 if not short_name else short_name.lstrip('-')
+ option.flags = flags
+ option.arg = arg
+ option.arg_data = arg_data
+ option.description = description
+ option.arg_description = arg_description
+ return option
+
+
+class Application(Gtk.Application):
+ def __init__(self, *args, **kwargs):
+ kwargs['application_id'] = 'lt.pov.mg.gtimelog'
+ kwargs['flags'] = Gio.ApplicationFlags.HANDLES_COMMAND_LINE
+ Gtk.Application.__init__(self, *args, **kwargs)
+ self.add_main_option_entries([
+ make_option("--version", description="Show version number and exit"),
+ make_option("--tray", description="Start minimized"),
+ make_option("--toggle", description="Show/hide the GTimeLog window if already running"),
+ make_option("--quit", description="Tell an already-running GTimeLog instance to quit"),
+ make_option("--sample-config", description="Write a sample configuration file to 'gtimelogrc.sample'"),
+ make_option("--debug", description="Show debug information"),
+ ])
+ self.main_window = None
+ self.debug = False
+ self.start_minimized = False
+
+ def do_handle_local_options(self, options):
+ if options.contains('version'):
+ print(gtimelog.__version__)
+ return 0
+ if options.contains('sample-config'):
+ settings = Settings()
+ settings.save("gtimelogrc.sample")
+ print("Sample configuration file written to gtimelogrc.sample")
+ print("Edit it and save as %s" % settings.get_config_file())
+ return 0
+ self.debug = options.contains('debug')
+ self.start_minimized = options.contains('tray')
+ if options.contains('quit'):
+ print('gtimelog: Telling the already-running instance to quit')
+ return -1 # send the args to the remote instance for processing
+
+ def do_command_line(self, command_line):
+ options = command_line.get_options_dict()
+ if options.contains('toggle') and self.main_window is not None:
+ # NB: Even if there's no tray icon, it's still possible to
+ # hide the gtimelog window. Bug or feature?
+ self.main_window.toggle_visible()
+ return 0
+ if options.contains('quit'):
+ if self.main_window:
+ self.main_window.quit()
+ else:
+ print('gtimelog: not running')
+ return 0
- self.main_window = main_window
+ self.do_activate()
+ return 0
- @dbus.service.method(INTERFACE)
- def ToggleFocus(self):
- self.main_window.toggle_visible()
+ def do_activate(self):
+ if self.main_window is not None:
+ self.main_window.main_window.present()
+ return
- @dbus.service.method(INTERFACE)
- def Present(self):
- self.main_window.on_show_activate()
+ debug = self.debug
+ start_minimized = self.start_minimized
- @dbus.service.method(INTERFACE)
- def Quit(self):
- gtk.main_quit()
+ log.addHandler(logging.StreamHandler(sys.stdout))
+ if debug:
+ log.setLevel(logging.DEBUG)
+ else:
+ log.setLevel(logging.INFO)
+ if debug:
+ print('GTimeLog version: %s' % gtimelog.__version__)
+ print('Python version: %s' % sys.version)
+ print('Gtk+ version: %s.%s.%s' % (Gtk.MAJOR_VERSION, Gtk.MINOR_VERSION, Gtk.MICRO_VERSION))
+ print('Config directory: %s' % Settings().get_config_dir())
+ print('Data directory: %s' % Settings().get_data_dir())
-def main():
- """Run the program."""
- parser = optparse.OptionParser(usage='%prog [options]',
- version=gtimelog.__version__)
- parser.add_option('--tray', action='store_true',
- help="start minimized")
- parser.add_option('--sample-config', action='store_true',
- help="write a sample configuration file to 'gtimelogrc.sample'")
-
- dbus_options = optparse.OptionGroup(parser, "Single-Instance Options")
- dbus_options.add_option('--replace', action='store_true',
- help="replace the already running GTimeLog instance")
- dbus_options.add_option('--quit', action='store_true',
- help="tell an already-running GTimeLog instance to quit")
- dbus_options.add_option('--toggle', action='store_true',
- help="show/hide the GTimeLog window if already running")
- dbus_options.add_option('--ignore-dbus', action='store_true',
- help="do not check if GTimeLog is already running"
- " (allows you to have multiple instances running)")
- parser.add_option_group(dbus_options)
-
- debug_options = optparse.OptionGroup(parser, "Debugging Options")
- debug_options.add_option('--debug', action='store_true',
- help="show debug information")
- debug_options.add_option('--prefer-pygtk', action='store_true',
- help="try to use the (obsolete) pygtk library instead of pygi")
- parser.add_option_group(debug_options)
-
- opts, args = parser.parse_args()
-
- log.addHandler(logging.StreamHandler(sys.stdout))
- if opts.debug:
- log.setLevel(logging.DEBUG)
- else:
- log.setLevel(logging.INFO)
-
- if opts.sample_config:
settings = Settings()
- settings.save("gtimelogrc.sample")
- print("Sample configuration file written to gtimelogrc.sample")
- print("Edit it and save as %s" % settings.get_config_file())
- return
-
- global dbus
-
- if opts.debug:
- print('GTimeLog version: %s' % gtimelog.__version__)
- print('Python version: %s' % sys.version)
- print('Toolkit: %s' % toolkit)
- print('Gtk+ version: %s' % gtk_version)
- print('D-Bus available: %s' % ('yes' if dbus else 'no'))
- print('Config directory: %s' % Settings().get_config_dir())
- print('Data directory: %s' % Settings().get_data_dir())
-
- if opts.ignore_dbus:
- dbus = None
-
- # Let's check if there is already an instance of GTimeLog running
- # and if it is make it present itself or when it is already presented
- # hide it and then quit.
- if dbus:
- dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
-
+ configdir = settings.get_config_dir()
+ datadir = settings.get_data_dir()
try:
- session_bus = dbus.SessionBus()
- dbus_service = session_bus.get_object(SERVICE, OBJECT_PATH)
- if opts.replace or opts.quit:
- print('gtimelog: Telling the already-running instance to quit')
- dbus_service.Quit()
- if opts.quit:
- sys.exit()
- elif opts.toggle:
- dbus_service.ToggleFocus()
- print('gtimelog: Already running, toggling visibility')
- sys.exit()
- elif opts.tray:
- print('gtimelog: Already running, not doing anything')
- sys.exit()
- else:
- dbus_service.Present()
- print('gtimelog: Already running, presenting main window')
- sys.exit()
- except dbus.DBusException as e:
- if e.get_dbus_name() == 'org.freedesktop.DBus.Error.ServiceUnknown':
- # gtimelog is not running: that's fine and not an error at all
- if opts.quit:
- print('gtimelog is not running')
- sys.exit()
- elif opts.quit or opts.replace or opts.toggle:
- # we need dbus to work for this, so abort
- sys.exit('gtimelog: %s' % e)
- else:
- # otherwise just emit a warning
- print("gtimelog: dbus is not available:\n %s" % e)
- else: # not dbus
- if opts.quit or opts.replace or opts.toggle:
- sys.exit("gtimelog: dbus not available")
-
- settings = Settings()
- configdir = settings.get_config_dir()
- datadir = settings.get_data_dir()
- try:
- # Create configdir if it doesn't exist.
- os.makedirs(configdir)
- except OSError as error:
- if error.errno != errno.EEXIST:
- # XXX: not the most friendly way of error reporting for a GUI app
- raise
- try:
- # Create datadir if it doesn't exist.
- os.makedirs(datadir)
- except OSError as error:
- if error.errno != errno.EEXIST:
- raise
-
- settings_file = settings.get_config_file()
- if not os.path.exists(settings_file):
- if opts.debug:
- print('Saving settings to %s' % settings_file)
- settings.save(settings_file)
- else:
- if opts.debug:
- print('Loading settings from %s' % settings_file)
- settings.load(settings_file)
- if opts.debug:
- print('Assuming date changes at %s' % settings.virtual_midnight)
- print('Loading time log from %s' % settings.get_timelog_file())
- timelog = TimeLog(settings.get_timelog_file(),
- settings.virtual_midnight)
- if settings.task_list_url:
- if opts.debug:
- print('Loading cached remote tasks from %s' %
- os.path.join(datadir, 'remote-tasks.txt'))
- tasks = RemoteTaskList(settings.task_list_url,
- os.path.join(datadir, 'remote-tasks.txt'))
- else:
- if opts.debug:
- print('Loading tasks from %s' % os.path.join(datadir, 'tasks.txt'))
- tasks = TaskList(os.path.join(datadir, 'tasks.txt'))
- main_window = MainWindow(timelog, settings, tasks)
- start_in_tray = False
- if settings.show_tray_icon:
- if settings.prefer_app_indicator:
- icons = [AppIndicator, SimpleStatusIcon, OldTrayIcon]
- elif settings.prefer_old_tray_icon:
- icons = [OldTrayIcon, SimpleStatusIcon, AppIndicator]
+ # Create configdir if it doesn't exist.
+ os.makedirs(configdir)
+ except OSError as error:
+ if error.errno != errno.EEXIST:
+ # XXX: not the most friendly way of error reporting for a GUI app
+ raise
+ try:
+ # Create datadir if it doesn't exist.
+ os.makedirs(datadir)
+ except OSError as error:
+ if error.errno != errno.EEXIST:
+ raise
+
+ settings_file = settings.get_config_file()
+ if not os.path.exists(settings_file):
+ if debug:
+ print('Saving settings to %s' % settings_file)
+ settings.save(settings_file)
else:
- icons = [SimpleStatusIcon, OldTrayIcon, AppIndicator]
- if opts.debug:
- print('Tray icon preference: %s' % ', '.join(icon_class.__name__
- for icon_class in icons))
- for icon_class in icons:
- tray_icon = icon_class(main_window)
- if tray_icon.available():
- if opts.debug:
- print('Tray icon: %s' % icon_class.__name__)
+ if debug:
+ print('Loading settings from %s' % settings_file)
+ settings.load(settings_file)
+ if debug:
+ print('Assuming date changes at %s' % settings.virtual_midnight)
+ print('Loading time log from %s' % settings.get_timelog_file())
+ timelog = TimeLog(settings.get_timelog_file(),
+ settings.virtual_midnight)
+ if settings.task_list_url:
+ if debug:
+ print('Loading cached remote tasks from %s' %
+ os.path.join(datadir, 'remote-tasks.txt'))
+ tasks = RemoteTaskList(settings.task_list_url,
+ os.path.join(datadir, 'remote-tasks.txt'))
+ else:
+ if debug:
+ print('Loading tasks from %s' % os.path.join(datadir, 'tasks.txt'))
+ tasks = TaskList(os.path.join(datadir, 'tasks.txt'))
+ self.main_window = MainWindow(timelog, settings, tasks)
+ self.add_window(self.main_window.main_window)
+ start_in_tray = False
+
+ if settings.show_tray_icon:
+ if debug:
+ print('Tray icon preference: %s' % ('AppIndicator'
+ if settings.prefer_app_indicator
+ else 'SimpleStatusIcon'))
+
+ if settings.prefer_app_indicator and have_app_indicator:
+ tray_icon = AppIndicator(self.main_window)
+ else:
+ tray_icon = SimpleStatusIcon(self.main_window)
+
+ if tray_icon:
+ if debug:
+ print('Using: %s' % tray_icon.__class__.__name__)
+
start_in_tray = (settings.start_in_tray
if settings.start_in_tray
- else opts.tray)
- break # found one that works
- else:
- if opts.debug:
- print('%s not available' % icon_class.__name__)
- if not start_in_tray:
- main_window.on_show_activate()
- else:
- if opts.debug:
- print('Starting minimized')
- if dbus:
- try:
- service = Service(main_window) # noqa
- except dbus.DBusException as e:
- print("gtimelog: dbus is not available:\n %s" % e)
- # This is needed to make ^C terminate gtimelog when we're using
- # gobject-introspection.
- signal.signal(signal.SIGINT, signal.SIG_DFL)
- try:
- gtk.main()
- except KeyboardInterrupt:
- pass
+ else start_minimized)
+
+ if debug:
+ print('GTK+ completion: %s' % ('enabled' if settings.enable_gtk_completion else 'disabled'))
+ if not start_in_tray:
+ self.main_window.on_show_activate()
+ else:
+ if debug:
+ print('Starting minimized')
+
+ # This is needed to make ^C terminate gtimelog when we're using
+ # gobject-introspection.
+ signal.signal(signal.SIGINT, signal.SIG_DFL)
+
+
+def main():
+ """Run the program."""
+ app = Application()
+ app.run(sys.argv)
if __name__ == '__main__':
main()
diff --git a/src/gtimelog/settings.py b/src/gtimelog/settings.py
index baed32c..fb68277 100644
--- a/src/gtimelog/settings.py
+++ b/src/gtimelog/settings.py
@@ -48,9 +48,8 @@ class Settings(object):
edit_task_list_cmd = ''
show_office_hours = True
- show_tray_icon = True
+ show_tray_icon = False
prefer_app_indicator = True
- prefer_old_tray_icon = False
start_in_tray = False
report_style = 'plain'
@@ -108,8 +107,6 @@ class Settings(object):
config.set('gtimelog', 'show_tray_icon', str(self.show_tray_icon))
config.set('gtimelog', 'prefer_app_indicator',
str(self.prefer_app_indicator))
- config.set('gtimelog', 'prefer_old_tray_icon',
- str(self.prefer_old_tray_icon))
config.set('gtimelog', 'report_style', str(self.report_style))
config.set('gtimelog', 'start_in_tray', str(self.start_in_tray))
return config
@@ -144,8 +141,6 @@ class Settings(object):
self.show_tray_icon = config.getboolean('gtimelog', 'show_tray_icon')
self.prefer_app_indicator = config.getboolean('gtimelog',
'prefer_app_indicator')
- self.prefer_old_tray_icon = config.getboolean('gtimelog',
- 'prefer_old_tray_icon')
self.report_style = config.get('gtimelog', 'report_style')
self.start_in_tray = config.getboolean('gtimelog', 'start_in_tray')
diff --git a/src/gtimelog/tests.py b/src/gtimelog/tests.py
index 34aa50b..8e60ff4 100644
--- a/src/gtimelog/tests.py
+++ b/src/gtimelog/tests.py
@@ -1,17 +1,24 @@
"""Tests for gtimelog"""
+import datetime
import doctest
-import unittest
import os
-import tempfile
import shutil
+import tempfile
+import textwrap
+import unittest
+import sys
from pprint import pprint
-
try:
from cStringIO import StringIO
except ImportError:
from io import StringIO
+import freezegun
+import mock
+
+from gtimelog.timelog import TimeLog
+
def doctest_as_hours():
"""Tests for as_hours
@@ -248,6 +255,22 @@ def doctest_uniq():
"""
+def doctest_TimeWindow_repr():
+ """Test for TimeWindow.__repr__
+
+ >>> from datetime import datetime, time
+ >>> min = datetime(2013, 12, 3)
+ >>> max = datetime(2013, 12, 4)
+ >>> vm = time(2, 0)
+
+ >>> from gtimelog.timelog import TimeWindow
+ >>> window = TimeWindow('/nosuchfile', min, max, vm)
+ >>> window
+ <TimeWindow: 2013-12-03 00:00:00..2013-12-04 00:00:00>
+
+ """
+
+
def doctest_TimeWindow_reread_no_file():
"""Test for TimeWindow.reread
@@ -404,7 +427,7 @@ def doctest_TimeWindow_last_entry():
>>> window.items = [
... (datetime(2013, 12, 4, 9, 0), 'started **'),
... ]
- >>> start, stop, duration, entry = window.last_entry()
+ >>> start, stop, duration, tags, entry = window.last_entry()
>>> start == stop == datetime(2013, 12, 4, 9, 0)
True
>>> duration
@@ -418,7 +441,7 @@ def doctest_TimeWindow_last_entry():
... (datetime(2013, 12, 3, 12, 0), 'stuff'),
... (datetime(2013, 12, 4, 9, 0), 'started **'),
... ]
- >>> start, stop, duration, entry = window.last_entry()
+ >>> start, stop, duration, tags, entry = window.last_entry()
>>> start == stop == datetime(2013, 12, 4, 9, 0)
True
>>> duration
@@ -433,7 +456,7 @@ def doctest_TimeWindow_last_entry():
... (datetime(2013, 12, 4, 9, 0), 'started **'),
... (datetime(2013, 12, 4, 9, 31), 'gtimelog: tests'),
... ]
- >>> start, stop, duration, entry = window.last_entry()
+ >>> start, stop, duration, tags, entry = window.last_entry()
>>> start
datetime.datetime(2013, 12, 4, 9, 0)
>>> stop
@@ -468,7 +491,6 @@ def doctest_TimeWindow_to_csv_complete():
>>> from gtimelog.timelog import TimeWindow
>>> window = TimeWindow(sampledata, min, max, vm)
- >>> import sys
>>> window.to_csv_complete(sys.stdout)
task,time (minutes)
etc,60
@@ -499,7 +521,6 @@ def doctest_TimeWindow_to_csv_daily():
>>> from gtimelog.timelog import TimeWindow
>>> window = TimeWindow(sampledata, min, max, vm)
- >>> import sys
>>> window.to_csv_daily(sys.stdout)
date,day-start (hours),slacking (hours),work (hours)
2008-06-03,12.75,0.0,3.0
@@ -509,11 +530,84 @@ def doctest_TimeWindow_to_csv_daily():
"""
+def doctest_TimeWindow_icalendar():
+ r"""Tests for TimeWindow.icalendar
+
+ >>> from datetime import datetime, time
+ >>> min = datetime(2008, 6, 1)
+ >>> max = datetime(2008, 7, 1)
+ >>> vm = time(2, 0)
+
+ >>> sampledata = StringIO(r'''
+ ... 2008-06-03 12:45: start **
+ ... 2008-06-03 13:00: something
+ ... 2008-06-03 15:45: something, else; with special\chars
+ ... 2008-06-05 12:45: start **
+ ... 2008-06-05 13:15: something
+ ... 2008-06-05 14:15: rest **
+ ... ''')
+
+ >>> from gtimelog.timelog import TimeWindow
+ >>> window = TimeWindow(sampledata, min, max, vm)
+
+ >>> with freezegun.freeze_time("2015-05-18 15:40"):
+ ... with mock.patch('socket.getfqdn') as mock_getfqdn:
+ ... mock_getfqdn.return_value = 'localhost'
+ ... window.icalendar(sys.stdout)
+ ... # doctest: +REPORT_NDIFF
+ BEGIN:VCALENDAR
+ PRODID:-//mg.pov.lt/NONSGML GTimeLog//EN
+ VERSION:2.0
+ BEGIN:VEVENT
+ UID:be5f9be205c2308f7f1a30d6c399d6bd@localhost
+ SUMMARY:start **
+ DTSTART:20080603T124500
+ DTEND:20080603T124500
+ DTSTAMP:20150518T154000Z
+ END:VEVENT
+ BEGIN:VEVENT
+ UID:33c7e212fed11eda71d5acd4bd22119b@localhost
+ SUMMARY:something
+ DTSTART:20080603T124500
+ DTEND:20080603T130000
+ DTSTAMP:20150518T154000Z
+ END:VEVENT
+ BEGIN:VEVENT
+ UID:b10c11beaf91df16964a46b4c87420b1@localhost
+ SUMMARY:something\, else\; with special\\chars
+ DTSTART:20080603T130000
+ DTEND:20080603T154500
+ DTSTAMP:20150518T154000Z
+ END:VEVENT
+ BEGIN:VEVENT
+ UID:04964eef67ec22178d74fe4c0f06aa2a@localhost
+ SUMMARY:start **
+ DTSTART:20080605T124500
+ DTEND:20080605T124500
+ DTSTAMP:20150518T154000Z
+ END:VEVENT
+ BEGIN:VEVENT
+ UID:2b51ea6d1c26f02d58051a691657068d@localhost
+ SUMMARY:something
+ DTSTART:20080605T124500
+ DTEND:20080605T131500
+ DTSTAMP:20150518T154000Z
+ END:VEVENT
+ BEGIN:VEVENT
+ UID:bd6bfd401333dbbf34fec941567d5d06@localhost
+ SUMMARY:rest **
+ DTSTART:20080605T131500
+ DTEND:20080605T141500
+ DTSTAMP:20150518T154000Z
+ END:VEVENT
+ END:VCALENDAR
+
+ """
+
+
def doctest_Reports_weekly_report_categorized():
r"""Tests for Reports.weekly_report_categorized
- >>> import sys
-
>>> from datetime import datetime, time
>>> from gtimelog.timelog import TimeWindow, Reports
@@ -577,8 +671,6 @@ def doctest_Reports_weekly_report_categorized():
def doctest_Reports_monthly_report_categorized():
r"""Tests for Reports.monthly_report_categorized
- >>> import sys
-
>>> from datetime import datetime, time
>>> from gtimelog.timelog import TimeWindow, Reports
@@ -639,8 +731,6 @@ def doctest_Reports_monthly_report_categorized():
def doctest_Reports_report_categories():
r"""Tests for Reports._report_categories
- >>> import sys
-
>>> from datetime import datetime, time, timedelta
>>> from gtimelog.timelog import TimeWindow, Reports
@@ -668,8 +758,6 @@ def doctest_Reports_report_categories():
def doctest_Reports_daily_report():
r"""Tests for Reports.daily_report
- >>> import sys
-
>>> from datetime import datetime, time
>>> from gtimelog.timelog import TimeWindow, Reports
@@ -725,8 +813,6 @@ def doctest_Reports_daily_report():
def doctest_Reports_weekly_report_plain():
r"""Tests for Reports.weekly_report_plain
- >>> import sys
-
>>> from datetime import datetime, time
>>> from gtimelog.timelog import TimeWindow, Reports
@@ -776,8 +862,6 @@ def doctest_Reports_weekly_report_plain():
def doctest_Reports_monthly_report_plain():
r"""Tests for Reports.monthly_report_plain
- >>> import sys
-
>>> from datetime import datetime, time
>>> from gtimelog.timelog import TimeWindow, Reports
@@ -827,8 +911,6 @@ def doctest_Reports_monthly_report_plain():
def doctest_Reports_custom_range_report_categorized():
r"""Tests for Reports.custom_range_report_categorized
- >>> import sys
-
>>> from datetime import datetime, time
>>> from gtimelog.timelog import TimeWindow, Reports
@@ -944,6 +1026,123 @@ def doctest_TaskList_real_file():
"""
+class TestTimeLog(unittest.TestCase):
+
+ def setUp(self):
+ self.tempdir = None
+
+ def tearDown(self):
+ if self.tempdir:
+ shutil.rmtree(self.tempdir)
+
+ def mkdtemp(self):
+ if self.tempdir is None:
+ self.tempdir = tempfile.mkdtemp(prefix='gtimelog-test-')
+ return self.tempdir
+
+ def test_appending_clears_window_cache(self):
+ # Regression test for https://github.com/gtimelog/gtimelog/issues/28
+ tempfile = os.path.join(self.mkdtemp(), 'timelog.txt')
+ timelog = TimeLog(tempfile, datetime.time(2, 0))
+
+ w = timelog.window_for_day(datetime.date(2014, 11, 12))
+ self.assertEqual(list(w.all_entries()), [])
+
+ timelog.append('started **', now=datetime.datetime(2014, 11, 12, 10, 00))
+ w = timelog.window_for_day(datetime.date(2014, 11, 12))
+ self.assertEqual(len(list(w.all_entries())), 1)
+
+ @freezegun.freeze_time("2015-05-12 16:27")
+ def test_valid_time_accepts_any_time_in_the_past_when_log_is_empty(self):
+ timelog = TimeLog(StringIO(), datetime.time(2, 0))
+ past = datetime.datetime(2015, 5, 12, 14, 20)
+ self.assertTrue(timelog.valid_time(past))
+
+ @freezegun.freeze_time("2015-05-12 16:27")
+ def test_valid_time_rejects_times_in_the_future(self):
+ timelog = TimeLog(StringIO(), datetime.time(2, 0))
+ future = datetime.datetime(2015, 5, 12, 16, 30)
+ self.assertFalse(timelog.valid_time(future))
+
+ @freezegun.freeze_time("2015-05-12 16:27")
+ def test_valid_time_rejects_times_before_last_entry(self):
+ timelog = TimeLog(StringIO("2015-05-12 15:00: did stuff"),
+ datetime.time(2, 0))
+ past = datetime.datetime(2015, 5, 12, 14, 20)
+ self.assertFalse(timelog.valid_time(past))
+
+ @freezegun.freeze_time("2015-05-12 16:27")
+ def test_valid_time_accepts_times_between_last_entry_and_now(self):
+ timelog = TimeLog(StringIO("2015-05-12 15:00: did stuff"),
+ datetime.time(2, 0))
+ past = datetime.datetime(2015, 5, 12, 15, 20)
+ self.assertTrue(timelog.valid_time(past))
+
+ def test_parse_correction_leaves_regular_text_alone(self):
+ timelog = TimeLog(StringIO(), datetime.time(2, 0))
+ self.assertEqual(timelog.parse_correction("did stuff"),
+ ("did stuff", None))
+
+ @freezegun.freeze_time("2015-05-12 16:27")
+ def test_parse_correction_recognizes_absolute_times(self):
+ timelog = TimeLog(StringIO(), datetime.time(2, 0))
+ self.assertEqual(timelog.parse_correction("15:20 did stuff"),
+ ("did stuff", datetime.datetime(2015, 5, 12, 15, 20)))
+
+ @freezegun.freeze_time("2015-05-13 00:27")
+ def test_parse_correction_handles_virtual_midnight_yesterdays_time(self):
+ # Regression test for https://github.com/gtimelog/gtimelog/issues/33
+ timelog = TimeLog(StringIO(), datetime.time(2, 0))
+ self.assertEqual(timelog.parse_correction("15:20 did stuff"),
+ ("did stuff", datetime.datetime(2015, 5, 12, 15, 20)))
+
+ @freezegun.freeze_time("2015-05-13 00:27")
+ def test_parse_correction_handles_virtual_midnight_todays_time(self):
+ timelog = TimeLog(StringIO(), datetime.time(2, 0))
+ self.assertEqual(timelog.parse_correction("00:15 did stuff"),
+ ("did stuff", datetime.datetime(2015, 5, 13, 00, 15)))
+
+ @freezegun.freeze_time("2015-05-12 16:27")
+ def test_parse_correction_ignores_future_absolute_times(self):
+ timelog = TimeLog(StringIO(), datetime.time(2, 0))
+ self.assertEqual(timelog.parse_correction("17:20 did stuff"),
+ ("17:20 did stuff", None))
+
+ @freezegun.freeze_time("2015-05-12 16:27")
+ def test_parse_correction_ignores_bad_absolute_times(self):
+ timelog = TimeLog(StringIO(), datetime.time(2, 0))
+ self.assertEqual(timelog.parse_correction("19:60 did stuff"),
+ ("19:60 did stuff", None))
+ self.assertEqual(timelog.parse_correction("24:00 did stuff"),
+ ("24:00 did stuff", None))
+
+ @freezegun.freeze_time("2015-05-12 16:27")
+ def test_parse_correction_ignores_absolute_times_before_last_entry(self):
+ timelog = TimeLog(StringIO("2015-05-12 16:00: stuff"),
+ datetime.time(2, 0))
+ self.assertEqual(timelog.parse_correction("15:20 did stuff"),
+ ("15:20 did stuff", None))
+
+ @freezegun.freeze_time("2015-05-12 16:27")
+ def test_parse_correction_recognizes_relative_times(self):
+ timelog = TimeLog(StringIO(), datetime.time(2, 0))
+ self.assertEqual(timelog.parse_correction("-20 did stuff"),
+ ("did stuff", datetime.datetime(2015, 5, 12, 16, 7)))
+
+ @freezegun.freeze_time("2015-05-12 16:27")
+ def test_parse_correction_ignores_relative_times_before_last_entry(self):
+ timelog = TimeLog(StringIO("2015-05-12 16:00: stuff"),
+ datetime.time(2, 0))
+ self.assertEqual(timelog.parse_correction("-30 did stuff"),
+ ("-30 did stuff", None))
+
+ @freezegun.freeze_time("2015-05-12 16:27")
+ def test_parse_correction_ignores_bad_relative_times(self):
+ timelog = TimeLog(StringIO(), datetime.time(2, 0))
+ self.assertEqual(timelog.parse_correction("-200 did stuff"),
+ ("-200 did stuff", None))
+
+
class TestSettings(unittest.TestCase):
def setUp(self):
@@ -1046,6 +1245,113 @@ class TestSettings(unittest.TestCase):
self.settings.save(os.path.join(tempdir, 'config'))
+class TestTagging (unittest.TestCase):
+
+ TEST_TIMELOG = textwrap.dedent("""
+ 2014-05-27 10:03: arrived
+ 2014-05-27 10:13: edx: introduce topic to new sysadmins -- edx
+ 2014-05-27 10:30: email
+ 2014-05-27 12:11: meeting: how to support new courses? -- edx meeting
+ 2014-05-27 15:12: edx: write test procedure for EdX instances -- edx sysadmin
+ 2014-05-27 17:03: cluster: set-up accounts, etc. -- sysadmin hpc
+ 2014-05-27 17:14: support: how to run statistics on Hydra? -- support hydra
+ 2014-05-27 17:36: off: pause **
+ 2014-05-27 17:38: email
+ 2014-05-27 19:06: off: dinner & family **
+ 2014-05-27 22:19: cluster: fix shmmax-shmall issue -- sysadmin hpc
+ """)
+
+ def setUp(self):
+ from gtimelog.timelog import TimeWindow
+ self.tw = TimeWindow(
+ filename=StringIO(self.TEST_TIMELOG),
+ min_timestamp=datetime.datetime(2014, 5, 27, 9, 0),
+ max_timestamp=datetime.datetime(2014, 5, 27, 23, 59),
+ virtual_midnight=datetime.time(2, 0))
+
+ def test_TimeWindow_set_of_all_tags(self):
+ tags = self.tw.set_of_all_tags()
+ self.assertEqual(tags,
+ set(['edx', 'hpc', 'hydra',
+ 'meeting', 'support', 'sysadmin']))
+
+ def test_TimeWindow_totals_per_tag1(self):
+ """Test aggregate time per tag, 1 entry only"""
+ result = self.tw.totals('meeting')
+ self.assertEqual(len(result), 2)
+ work, slack = result
+ self.assertEqual(work, (
+ # start/end times are manually extracted from the TEST_TIMELOG sample
+ (datetime.timedelta(hours=12, minutes=11) - datetime.timedelta(hours=10, minutes=30))
+ ))
+ self.assertEqual(slack, datetime.timedelta(0))
+
+ def test_TimeWindow_totals_per_tag2(self):
+ """Test aggregate time per tag, several entries"""
+ result = self.tw.totals('hpc')
+ self.assertEqual(len(result), 2)
+ work, slack = result
+ self.assertEqual(work, (
+ # start/end times are manually extracted from the TEST_TIMELOG sample
+ (datetime.timedelta(hours=17, minutes=3) - datetime.timedelta(hours=15, minutes=12))
+ + (datetime.timedelta(hours=22, minutes=19) - datetime.timedelta(hours=19, minutes=6))
+ ))
+ self.assertEqual(slack, datetime.timedelta(0))
+
+ def test_TimeWindow__split_entry_and_tags1(self):
+ """Test `TimeWindow._split_entry_and_tags` with simple entry"""
+ result = self.tw._split_entry_and_tags('email')
+ self.assertEqual(len(result), 2)
+ self.assertEqual(result[0], 'email')
+ self.assertEqual(result[1], set())
+
+ def test_TimeWindow__split_entry_and_tags2(self):
+ """Test `TimeWindow._split_entry_and_tags` with simple entry and tags"""
+ result = self.tw._split_entry_and_tags('restart CFEngine server -- sysadmin cfengine issue327')
+ self.assertEqual(len(result), 2)
+ self.assertEqual(result[0], 'restart CFEngine server')
+ self.assertEqual(result[1], set(['sysadmin', 'cfengine', 'issue327']))
+
+ def test_TimeWindow__split_entry_and_tags3(self):
+ """Test `TimeWindow._split_entry_and_tags` with category, entry, and tags"""
+ result = self.tw._split_entry_and_tags('tooling: tagging support in gtimelog -- tooling gtimelog')
+ self.assertEqual(len(result), 2)
+ self.assertEqual(result[0], 'tooling: tagging support in gtimelog')
+ self.assertEqual(result[1], set(['tooling', 'gtimelog']))
+
+ def test_TimeWindow__split_entry_and_tags4(self):
+ """Test `TimeWindow._split_entry_and_tags` with slack-type entry"""
+ result = self.tw._split_entry_and_tags('read news -- reading **')
+ self.assertEqual(len(result), 2)
+ self.assertEqual(result[0], 'read news **')
+ self.assertEqual(result[1], set(['reading']))
+
+ def test_TimeWindow__split_entry_and_tags5(self):
+ """Test `TimeWindow._split_entry_and_tags` with slack-type entry"""
+ result = self.tw._split_entry_and_tags('read news -- reading ***')
+ self.assertEqual(len(result), 2)
+ self.assertEqual(result[0], 'read news ***')
+ self.assertEqual(result[1], set(['reading']))
+
+ def test_Reports__report_tags(self):
+ from gtimelog.timelog import Reports
+ rp = Reports(self.tw)
+ txt = StringIO()
+ # use same tags as in tests above, so we know the totals
+ rp._report_tags(txt, ['meeting', 'hpc'])
+ self.assertEqual(
+ txt.getvalue().strip(),
+ textwrap.dedent("""
+ Time spent in each area:
+
+ hpc 5:04
+ meeting 1:41
+
+ Note that area totals may not add up to the period totals,
+ as each entry may be belong to multiple areas (or none at all).
+ """).strip())
+
+
def additional_tests(): # for setup.py
return doctest.DocTestSuite(optionflags=doctest.NORMALIZE_WHITESPACE)
@@ -1058,7 +1364,7 @@ def test_suite():
def main():
- unittest.TextTestRunner().run(test_suite())
+ unittest.main(module='gtimelog.tests', defaultTest='test_suite')
if __name__ == '__main__':
diff --git a/src/gtimelog/timelog.py b/src/gtimelog/timelog.py
index b6a12d8..cc1ad11 100644
--- a/src/gtimelog/timelog.py
+++ b/src/gtimelog/timelog.py
@@ -6,9 +6,11 @@ import codecs
import csv
import datetime
import os
+import socket
import sys
import re
import urllib
+from hashlib import md5
from operator import itemgetter
@@ -138,6 +140,10 @@ class TimeWindow(object):
self.virtual_midnight = virtual_midnight
self.reread(callback)
+ def __repr__(self):
+ return '<TimeWindow: {}..{}>'.format(self.min_timestamp,
+ self.max_timestamp)
+
def reread(self, callback=None):
"""Parse the time log file and update self.items.
@@ -189,10 +195,37 @@ class TimeWindow(object):
return None
return self.items[-1][0]
+ @staticmethod
+ def _split_entry_and_tags(entry):
+ """
+ Split the entry title (proper) from the trailing tags.
+
+ Tags are separated from the title by a `` -- `` marker:
+ anything *before* the marker is the entry title,
+ anything *following* it is the (space-separated) set of tags.
+
+ Return a tuple consisting of entry title and set of tags.
+ """
+ if ' -- ' in entry:
+ entry, tags_bundle = entry.split(' -- ', 1)
+ # there might be spaces preceding ' -- '
+ entry = entry.rstrip()
+ tags = set(tags_bundle.split())
+ # put back '**' and '***' if they were in the tags part
+ if '***' in tags:
+ entry += ' ***'
+ tags.remove('***')
+ elif '**' in tags:
+ entry += ' **'
+ tags.remove('**')
+ else:
+ tags = set()
+ return entry, tags
+
def all_entries(self):
"""Iterate over all entries.
- Yields (start, stop, duration, entry) tuples. The first entry
+ Yields (start, stop, duration, tags, entry) tuples. The first entry
has a duration of 0.
"""
stop = None
@@ -204,13 +237,24 @@ class TimeWindow(object):
self.virtual_midnight):
start = stop
duration = stop - start
- yield start, stop, duration, entry
+ # tags are appended to the entry title, separated by ' -- '
+ entry, tags = self._split_entry_and_tags(entry)
+ yield start, stop, duration, tags, entry
+
+ def set_of_all_tags(self):
+ """
+ Return set of all tags mentioned in entries.
+ """
+ all_tags = set()
+ for _, _, _, entry_tags, _ in self.all_entries():
+ all_tags.update(entry_tags)
+ return all_tags
def count_days(self):
"""Count days that have entries."""
count = 0
last = None
- for start, stop, duration, entry in self.all_entries():
+ for start, stop, duration, tags, entry in self.all_entries():
if last is None or different_days(last, start,
self.virtual_midnight):
last = start
@@ -236,7 +280,8 @@ class TimeWindow(object):
if different_days(start, stop, self.virtual_midnight):
start = stop
duration = stop - start
- return start, stop, duration, entry
+ entry, tags = self._split_entry_and_tags(entry)
+ return start, stop, duration, tags, entry
def grouped_entries(self, skip_first=True):
"""Return consolidated entries (grouped by entry title).
@@ -247,7 +292,7 @@ class TimeWindow(object):
"""
work = {}
slack = {}
- for start, stop, duration, entry in self.all_entries():
+ for start, stop, duration, tags, entry in self.all_entries():
if skip_first:
skip_first = False
continue
@@ -297,9 +342,12 @@ class TimeWindow(object):
None, datetime.timedelta(0)) + duration
return entries, totals
- def totals(self):
+ def totals(self, tag=None):
"""Calculate total time of work and slacking entries.
+ If optional argument `tag` is given, only compute
+ totals for entries marked with the given tag.
+
Returns (total_work, total_slacking) tuple.
Slacking entries are identified by finding two asterisks in the title.
@@ -318,7 +366,9 @@ class TimeWindow(object):
(that is, it would be true if sum could operate on timedeltas).
"""
total_work = total_slacking = datetime.timedelta(0)
- for start, stop, duration, entry in self.all_entries():
+ for start, stop, duration, tags, entry in self.all_entries():
+ if tag is not None and tag not in tags:
+ continue
if '**' in entry:
total_slacking += duration
else:
@@ -330,15 +380,13 @@ class TimeWindow(object):
output.write("BEGIN:VCALENDAR\n")
output.write("PRODID:-//mg.pov.lt/NONSGML GTimeLog//EN\n")
output.write("VERSION:2.0\n")
- try:
- import socket
- idhost = socket.getfqdn()
- except: # can it actually ever fail?
- idhost = 'localhost'
+ idhost = socket.getfqdn()
dtstamp = datetime.datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
- for start, stop, duration, entry in self.all_entries():
+ def _hash(start, stop, entry):
+ return md5(("%s%s%s" % (start, stop, entry)).encode('UTF-8')).hexdigest()
+ for start, stop, duration, tags, entry in self.all_entries():
output.write("BEGIN:VEVENT\n")
- output.write("UID:%s@%s\n" % (hash((start, stop, entry)), idhost))
+ output.write("UID:%s@%s\n" % (_hash(start, stop, entry), idhost))
output.write("SUMMARY:%s\n" % (entry.replace('\\', '\\\\'))
.replace(';', '\\;')
.replace(',', '\\,'))
@@ -380,7 +428,7 @@ class TimeWindow(object):
d0 = datetime.timedelta(0)
days = {} # date -> [time_started, slacking, work]
dmin = None
- for start, stop, duration, entry in self.all_entries():
+ for start, stop, duration, tags, entry in self.all_entries():
if dmin is None:
dmin = start.date()
day = days.setdefault(start.date(),
@@ -400,9 +448,9 @@ class TimeWindow(object):
dmin += datetime.timedelta(days=1)
# convert to hours, and a sortable list
- items = [(day, as_hours(start), as_hours(slacking), as_hours(work))
- for day, (start, slacking, work) in days.items()]
- items.sort()
+ items = sorted(
+ (day, as_hours(start), as_hours(slacking), as_hours(work))
+ for day, (start, slacking, work) in days.items())
writer.writerows(items)
@@ -412,8 +460,7 @@ class Reports(object):
def __init__(self, window):
self.window = window
- def _categorizing_report(self, output, email, who, subject, period_name,
- estimated_column=False):
+ def _categorizing_report(self, output, email, who, subject, period_name):
"""A report that displays entries by category.
Writes a report template in RFC-822 format to output.
@@ -456,10 +503,7 @@ class Reports(object):
output.write("No work done this %s.\n" % period_name)
return
output.write(" " * 46)
- if estimated_column:
- output.write("estimated actual\n")
- else:
- output.write(" time\n")
+ output.write(" time\n")
total_work, total_slacking = window.totals()
entries, totals = window.categorized_work_entries()
@@ -484,13 +528,8 @@ class Reports(object):
continue # skip empty "arrival" entries
entry = entry[:1].upper() + entry[1:]
- if estimated_column:
- output.write(u" %-46s %-14s %s\n" %
- (entry, '-',
- format_duration_short(duration)))
- else:
- output.write(u" %-61s %+5s\n" %
- (entry, format_duration_short(duration)))
+ output.write(u" %-61s %+5s\n" %
+ (entry, format_duration_short(duration)))
output.write('-' * 70 + '\n')
output.write(u"%+70s\n" % format_duration_short(totals[cat]))
@@ -508,6 +547,45 @@ class Reports(object):
for time, cat in ordered_by_time:
output.write(line_format % (cat, format_duration_short(time)))
+ tags = self.window.set_of_all_tags()
+ if tags:
+ self._report_tags(output, tags)
+
+ def _report_tags(self, output, tags):
+ """Helper method that lists time spent per tag.
+
+ Use this to add a section in a report looks similar to this:
+
+ sysadmin: 2 hours 1 min
+ www: 18 hours 45 min
+ mailserver: 3 hours
+
+ Note that duration may not add up to the total working time,
+ as a single entry can have multiple or no tags at all!
+
+ Argument `tags` is a set of tags (string). It is not modified.
+ """
+ output.write('\n')
+ output.write('Time spent in each area:\n')
+ output.write('\n')
+ # sum work and slacking time per tag; we do not care in this report
+ tags_totals = {}
+ for tag in tags:
+ spent_working, spent_slacking = self.window.totals(tag)
+ tags_totals[tag] = spent_working + spent_slacking
+ # compute width of tag label column
+ max_tag_length = max([len(tag) for tag in tags_totals.keys()])
+ line_format = ' %-' + str(max_tag_length + 4) + 's %+5s\n'
+ # sort by time spent (descending)
+ for tag, spent in sorted(tags_totals.items(),
+ key=(lambda it: it[1]),
+ reverse=True):
+ output.write(line_format % (tag, format_duration_short(spent)))
+ output.write('\n')
+ output.write(
+ 'Note that area totals may not add up to the period totals,\n'
+ 'as each entry may be belong to multiple areas (or none at all).\n')
+
def _report_categories(self, output, categories):
"""A helper method that lists time spent per category.
@@ -533,8 +611,7 @@ class Reports(object):
cat, format_duration_long(duration)))
output.write('\n')
- def _plain_report(self, output, email, who, subject, period_name,
- estimated_column=False):
+ def _plain_report(self, output, email, who, subject, period_name):
"""Format a report that does not categorize entries.
Writes a report template in RFC-822 format to output.
@@ -549,10 +626,7 @@ class Reports(object):
output.write("No work done this %s.\n" % period_name)
return
output.write(" " * 46)
- if estimated_column:
- output.write("estimated actual\n")
- else:
- output.write(" time\n")
+ output.write(" time\n")
work, slack = window.grouped_entries()
total_work, total_slacking = window.totals()
categories = {}
@@ -572,12 +646,8 @@ class Reports(object):
None, datetime.timedelta(0)) + duration
entry = entry[:1].upper() + entry[1:]
- if estimated_column:
- output.write(u"%-46s %-14s %s\n" %
- (entry, '-', format_duration_long(duration)))
- else:
- output.write(u"%-62s %s\n" %
- (entry, format_duration_long(duration)))
+ output.write(u"%-62s %s\n" %
+ (entry, format_duration_long(duration)))
output.write('\n')
output.write("Total work done this %s: %s\n" %
(period_name, format_duration_long(total_work)))
@@ -585,50 +655,46 @@ class Reports(object):
if categories:
self._report_categories(output, categories)
- def weekly_report_categorized(self, output, email, who,
- estimated_column=False):
+ tags = self.window.set_of_all_tags()
+ if tags:
+ self._report_tags(output, tags)
+
+ def weekly_report_categorized(self, output, email, who):
"""Format a weekly report with entries displayed under categories."""
week = self.window.min_timestamp.isocalendar()[1]
subject = u'Weekly report for %s (week %02d)' % (who, week)
return self._categorizing_report(output, email, who, subject,
- period_name='week',
- estimated_column=estimated_column)
+ period_name='week')
- def monthly_report_categorized(self, output, email, who,
- estimated_column=False):
+ def monthly_report_categorized(self, output, email, who):
"""Format a monthly report with entries displayed under categories."""
month = self.window.min_timestamp.strftime('%Y/%m')
subject = u'Monthly report for %s (%s)' % (who, month)
return self._categorizing_report(output, email, who, subject,
- period_name='month',
- estimated_column=estimated_column)
+ period_name='month')
- def weekly_report_plain(self, output, email, who, estimated_column=False):
+ def weekly_report_plain(self, output, email, who):
"""Format a weekly report ."""
week = self.window.min_timestamp.isocalendar()[1]
subject = u'Weekly report for %s (week %02d)' % (who, week)
return self._plain_report(output, email, who, subject,
- period_name='week',
- estimated_column=estimated_column)
+ period_name='week')
- def monthly_report_plain(self, output, email, who, estimated_column=False):
+ def monthly_report_plain(self, output, email, who):
"""Format a monthly report ."""
month = self.window.min_timestamp.strftime('%Y/%m')
subject = u'Monthly report for %s (%s)' % (who, month)
return self._plain_report(output, email, who, subject,
- period_name='month',
- estimated_column=estimated_column)
+ period_name='month')
- def custom_range_report_categorized(self, output, email, who,
- estimated_column=False):
+ def custom_range_report_categorized(self, output, email, who):
"""Format a custom range report with entries displayed under categories."""
min = self.window.min_timestamp.strftime('%Y-%m-%d')
max = self.window.max_timestamp - datetime.timedelta(1)
max = max.strftime('%Y-%m-%d')
subject = u'Custom date range report for %s (%s - %s)' % (who, min, max)
return self._categorizing_report(output, email, who, subject,
- period_name='custom range',
- estimated_column=estimated_column)
+ period_name='custom range')
def daily_report(self, output, email, who):
"""Format a daily report.
@@ -652,7 +718,7 @@ class Reports(object):
if not items:
output.write("No work done today.\n")
return
- start, stop, duration, entry = items[0]
+ start, stop, duration, tags, entry = items[0]
entry = entry[:1].upper() + entry[1:]
output.write("%s at %s\n" % (entry, start.strftime('%H:%M')))
output.write('\n')
@@ -689,6 +755,10 @@ class Reports(object):
output.write("Time spent slacking: %s\n" %
format_duration_long(total_slacking))
+ tags = self.window.set_of_all_tags()
+ if tags:
+ self._report_tags(output, tags)
+
class TimeLog(object):
"""Time log.
@@ -723,6 +793,10 @@ class TimeLog(object):
Returns None if the file doesn't exist.
"""
+ # Accept any file-like object instead of a filename (for the benefit of
+ # unit tests).
+ if hasattr(self.filename, 'read'):
+ return None
try:
return os.stat(self.filename).st_mtime
except OSError:
@@ -809,8 +883,15 @@ class TimeLog(object):
self.window.items.append((now, entry))
line = '%s: %s' % (now.strftime("%Y-%m-%d %H:%M"), entry)
self.raw_append(line)
+ for (min, max), cached in self._cache.items():
+ if cached is not self.window and min <= now < max:
+ cached.items.append((now, entry))
def valid_time(self, time):
+ """Is this a valid time for a correction?
+
+ Valid times are those between the last timelog entry and now.
+ """
if time > datetime.datetime.now():
return False
last = self.window.last_time()
@@ -818,6 +899,41 @@ class TimeLog(object):
return False
return True
+ def parse_correction(self, entry):
+ """Recognize a time correction.
+
+ Corrections are entries that begin with a timestamp (HH:MM) or a
+ relative number of minutes (-MM).
+
+ Returns a tuple (entry, timestamp). ``timestamp`` will be None
+ if no correction was recognized. ``entry`` will have the leading
+ timestamp stripped.
+ """
+ now = None
+ date_match = re.match(r'(\d\d):(\d\d)\s+', entry)
+ delta_match = re.match(r'-([1-9]\d?|1\d\d)\s+', entry)
+ if date_match:
+ h = int(date_match.group(1))
+ m = int(date_match.group(2))
+ if 0 <= h < 24 and 0 <= m < 60:
+ now = datetime.datetime.combine(self.virtual_today(),
+ datetime.time(h, m))
+ if now.time() < self.virtual_midnight:
+ now += datetime.timedelta(1)
+ if self.valid_time(now):
+ entry = entry[date_match.end():]
+ else:
+ now = None
+ if delta_match:
+ seconds = int(delta_match.group()) * 60
+ now = datetime.datetime.now().replace(second=0, microsecond=0)
+ now += datetime.timedelta(seconds=seconds)
+ if self.valid_time(now):
+ entry = entry[delta_match.end():]
+ else:
+ now = None
+ return entry, now
+
class TaskList(object):
"""Task list.
diff --git a/tox.ini b/tox.ini
index 462dc16..0e89908 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,16 +1,28 @@
[tox]
envlist =
- py26,py27,py33
+ py27,py33,py34,py35
[testenv]
setenv =
LC_ALL=C
+deps =
+ freezegun
+ mock
commands =
- python setup.py test -q
+ python setup.py -q test
[testenv:coverage]
+usedevelop = true
deps =
+ {[testenv]deps}
coverage
commands =
- coverage run -m gtimelog.tests
- coverage report
+ coverage run {posargs} -m gtimelog.tests
+
+[testenv:coverage3]
+basepython = python3
+usedevelop = true
+deps =
+ {[testenv:coverage]deps}
+commands =
+ coverage run {posargs} -m gtimelog.tests