-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Martin Bauer
committed
Jan 3, 2021
0 parents
commit 36b728d
Showing
13 changed files
with
784 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
*.pyc | ||
dist | ||
rata_pkg_belugame.egg-info/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
Copyright (c) 2021 The Python Packaging Authority | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
# rata: raw time tracker | ||
|
||
Terminal based time tracker with git versioning running on python 3 and urwid. | ||
|
||
![alt text](https://user-images.githubusercontent.com/16137830/103478188-1c158300-4dc5-11eb-9c61-d2ad23981745.png) | ||
|
||
It suits people who only want to track the name, duration, start and end time of tasks they worked on, e.g. for billing | ||
a project per hour. A simple to use replacement for emacs org-mode respectively vim-dotoo. | ||
|
||
It's my first urwid project and there is no test coverage. Use at own risk, though it commits all changes directly to | ||
git. So you should always be able to recover your entries. | ||
|
||
## Features | ||
|
||
- runs in the terminal and adjusts to terminal size | ||
- track time per task in a single text-based file | ||
- quickly change between, add and rename tasks | ||
- shows when you started tracking, since when you are tracking and total duration tracked per task | ||
- edit previous records with check for overlaps | ||
- keep track of all changes through automatic git commits | ||
- sort by duration/name/most recently worked on | ||
- output file loosely based on org-mode, fully text-based. Though please do not try to add something other than time | ||
records to the file. Rata won't be able to read it. | ||
|
||
## Installation | ||
|
||
pip install rata | ||
|
||
## Usage | ||
|
||
rata requires a file name in a git-versioned folder. It will create a new commit for any change. | ||
|
||
```` | ||
git init ~/timerecords | ||
rata ~/timerecords/projectFoo.txt | ||
```` | ||
|
||
### Key-bindings | ||
|
||
#### Main view | ||
|
||
- Enter: Start/Stop tracking task under cursor | ||
- up/down: Move cursor over task list | ||
- right: Show and edit list of time records of task under cursor | ||
- n: Add new task and start tracking it | ||
- r: Rename task under cursor | ||
- s: Start/Stop current/last track (independant of cursor position) | ||
- o: Toggle sorting: by name, total task duration, most recently tracked | ||
- q: Quit program | ||
|
||
#### Edit mode | ||
|
||
- Enter: Edit record under cursor (modify the timestamps and confirm with Enter again) | ||
- h: Quick-edit: Move start time under cursor 1 minute ahead | ||
- j: Quick-edit: Move start time under cursor 1 minute back | ||
- k: Quick-edit: Move end time under cursor 1 minute ahead | ||
- l: Quick-edit: Move end time under cursor 1 minute back | ||
- Esc: Go back to main view | ||
|
||
### New task mode | ||
|
||
- Enter a name for your new task | ||
- Enter: Confirm | ||
- Esc: Go back to main view | ||
|
||
## Sample output file | ||
|
||
```` | ||
* Client support | ||
:LOGBOOK: | ||
CLOCK: [2020-12-27 10:22:10] -- [2020-12-27 11:30:11] | ||
CLOCK: [2020-12-24 09:30:03] -- [2020-12-24 10:40:06] | ||
:END: | ||
* On-site meetings | ||
:LOGBOOK: | ||
CLOCK: [2020-12-20 13:44:11] -- [2020-12-20 16:50:14] | ||
CLOCK: [2020-12-25 15:00:07] -- [2020-12-25 17:38:10] | ||
```` |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import sys | ||
|
||
from .libs.application import Rata | ||
from .libs.io import is_inside_git_dir | ||
|
||
|
||
def main(): | ||
try: | ||
file_name = sys.argv[1] | ||
except IndexError: | ||
print("File name argument missing. Start rata with a path like 'rata timerecords.txt'") | ||
sys.exit() | ||
|
||
if not is_inside_git_dir(file_name): | ||
print("No git directory. rata expects the given file name to be git-versioned to commit changes.") | ||
sys.exit() | ||
|
||
Rata(file_name) | ||
|
||
|
||
if __name__ == '__main__': | ||
sys.exit(main()) |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
from itertools import cycle | ||
|
||
import urwid | ||
|
||
from .edit_tasks import make_edit_task_screen | ||
from .io import format_duration | ||
from .tasks import TaskManager | ||
from .widgets import NewTask, TaskButtonWrap, RenameTask | ||
|
||
|
||
order_modes = cycle(["duration", "most recent", "name"]) | ||
palette = [ | ||
('cyan-bg', 'black', 'light cyan'), | ||
('cyan', 'light cyan', ''), | ||
('normal', '', ''), | ||
('red', 'dark red', ''), | ||
] | ||
refresh_interval = 1 | ||
help_line = " | ".join(["Enter: start/stop task", "Right: edit", "(n)ew task", "(s)tart/stop last task", "s(o)rt tasks", | ||
"(r)ename task", "(q)uit"]) | ||
|
||
|
||
class Rata(object): | ||
sort_order = "name" | ||
message_box = urwid.AttrMap(urwid.Text(""), "error") | ||
view = "main" | ||
|
||
def __init__(self, file_name): | ||
self.file_name = file_name | ||
self.taskmanager = TaskManager(file_name) | ||
self.current_task = self.taskmanager.find_running_task() | ||
self.body = self.create_tasks_menu() | ||
self.loop = urwid.MainLoop( | ||
self.body, | ||
palette, | ||
unhandled_input=self.handle_input | ||
) | ||
self.loop.set_alarm_in(refresh_interval, self.refresh) | ||
self.loop.run() | ||
|
||
def refresh(self, loop, data): | ||
"""Update clocks on the screen: current task duration & total task duration.""" | ||
if self.view == "main": # Don't do anything if not on main screen as widgets are different | ||
if self.current_task: | ||
self.current_task.button.base_widget.set_label(self.current_task.label) | ||
self.set_message("{}: {}".format(self.current_task.name, | ||
self.current_task.running_record.label_with_duration)) | ||
else: | ||
self.set_message("") | ||
if hasattr(self.body.base_widget, "body"): | ||
self.body.base_widget.body[0].set_text(self.title) | ||
loop.set_alarm_in(refresh_interval, self.refresh) | ||
|
||
def to_main_screen(self): | ||
"""Replace what is on the screen and show the main screen again""" | ||
focus = None | ||
if self.view == "main": | ||
focus = self.body.original_widget.base_widget.get_focus()[0].base_widget | ||
self.view = "main" | ||
self.set_message("") | ||
self.body.original_widget = self.create_tasks_menu() | ||
# Move cursor to where it was before: | ||
if focus: | ||
for index, w in enumerate(self.body.original_widget.base_widget.body): | ||
if hasattr(w.base_widget, "button") and w.base_widget.task == focus.task: | ||
self.body.original_widget.base_widget.set_focus(index) | ||
|
||
def set_message(self, message, error=False): | ||
if error: | ||
self.message_box.set_attr_map({None: 'red'}) | ||
else: | ||
self.message_box.set_attr_map({None: 'cyan'}) | ||
self.message_box.base_widget.set_text(message) | ||
|
||
@property | ||
def title(self): | ||
"""Defines top line shown above the task table.""" | ||
return "Tasks {} - by {} - Total {} - Today {}".format( | ||
self.file_name, self.sort_order, format_duration(self.taskmanager.total_duration), | ||
format_duration(self.taskmanager.todays_duration)) | ||
|
||
def create_tasks_menu(self): | ||
"""Renders title and bottom line and in the middle a scrollable table of tasks with their duration""" | ||
body = [urwid.Text(self.title), urwid.Divider()] | ||
for task in self.taskmanager.tasks_by(self.sort_order): | ||
button = TaskButtonWrap(task) | ||
urwid.connect_signal(button.button, "click", self.toggle_recording_task, task) | ||
if task == self.current_task: | ||
button = urwid.AttrMap(button, "cyan") | ||
else: | ||
button = urwid.AttrMap(button, "normal", "cyan-bg") | ||
task.button = button | ||
body.append(button) | ||
body += [urwid.Divider(), | ||
self.message_box, | ||
urwid.Divider(), | ||
urwid.Text(help_line)] | ||
return urwid.Padding(urwid.ListBox(urwid.SimpleFocusListWalker(body)), left=0, right=0) | ||
|
||
def toggle_recording_task(self, button, task): | ||
"""Event handler for Enter-key on a task line: start/stop recording""" | ||
if self.current_task: | ||
self.current_task.stop() | ||
if self.current_task == task: # Enter was pressed on currently running task | ||
self.current_task = None | ||
else: | ||
self.current_task = task | ||
self.current_task.start() | ||
self.to_main_screen() | ||
|
||
def handle_input(self, key): | ||
"""Main widget key press event handler.""" | ||
if self.view == "main": # Don't do anything if not on main screen as widgets are different | ||
if key == 's': # stop tracking | ||
self._start_stop_tracking() | ||
return | ||
if key == 'r': # rename task under cursor | ||
self._rename_task() | ||
return | ||
elif key == 'n': # track new task | ||
self._new_task() | ||
return | ||
elif key == 'o': # sort | ||
self._sort() | ||
return | ||
elif key == 'right': # edit | ||
self._edit_task() | ||
return | ||
if key == 'q': # quit | ||
self._quit() | ||
return | ||
if key == 'esc': | ||
self.to_main_screen() | ||
|
||
def _quit(self): | ||
if self.current_task: | ||
self.current_task.stop() | ||
self.current_task = None | ||
raise urwid.ExitMainLoop() | ||
|
||
def _start_stop_tracking(self): | ||
if not self.taskmanager.tasks: | ||
return | ||
if self.current_task: | ||
self.current_task.stop() | ||
self.current_task = None | ||
self.to_main_screen() # Will resort | ||
else: | ||
tasks = self.taskmanager.tasks[:] | ||
tasks.sort(key=lambda t: t.most_recent, reverse=True) | ||
self.current_task = tasks[0] | ||
tasks[0].start() | ||
|
||
def _rename_task(self): | ||
if not self.taskmanager.tasks: | ||
return | ||
self.view = "rename" | ||
task = self.body.base_widget.focus.base_widget.task | ||
edit = RenameTask(self, task, u"Rename task:\n") | ||
fill = urwid.Filler(edit) | ||
self.body.original_widget = fill | ||
|
||
def _new_task(self): | ||
self.view = "new task" | ||
edit = NewTask(self, u"New task:\n") | ||
fill = urwid.Filler(edit) | ||
self.body.original_widget = fill | ||
|
||
def _sort(self): | ||
self.sort_order = next(order_modes) | ||
self.to_main_screen() # Will resort | ||
|
||
def _edit_task(self): | ||
if not self.taskmanager.tasks: | ||
return | ||
self.body.original_widget = make_edit_task_screen(self) | ||
|
||
def add_new_task(self, name): | ||
"""Event handler for when a new task name was entered: Adds and starts tracking it.""" | ||
task = self.taskmanager.add(name) | ||
if self.current_task: | ||
self.current_task.stop() | ||
self.current_task = task | ||
self.to_main_screen() | ||
task.start() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import urwid | ||
|
||
from .widgets import RecordEdit, RecordButton | ||
|
||
|
||
def make_edit_task_screen(application): | ||
application.view = "edit" | ||
body = [urwid.Text("Edit records"), urwid.Divider()] | ||
task = application.body.base_widget.focus.base_widget.task | ||
task.records.sort(key=lambda r: r.start, reverse=True) | ||
for r in task.records: | ||
button = RecordButton(r, application) | ||
urwid.connect_signal(button, 'click', _edit_record, (application, r)) | ||
body.append(button) | ||
body += [urwid.Divider(), | ||
urwid.Text("Enter: edit | h/j: Move start up/down | k/l: Move end up/down | Esc: cancel"), | ||
urwid.Divider(), | ||
application.message_box] | ||
return urwid.Padding(urwid.ListBox(urwid.SimpleFocusListWalker(body)), left=0, right=0) | ||
|
||
|
||
def _edit_record(button, record_tuple): | ||
application, record = record_tuple | ||
application.set_message("") | ||
application.body.original_widget = urwid.ListBox([ | ||
RecordEdit(application, record, edit_text=record.label), | ||
urwid.Divider(), | ||
application.message_box]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
class RecordOverlap(Exception): | ||
pass |
Oops, something went wrong.