- Common Lisp 96.2%
- Emacs Lisp 3.8%
| doc | ||
| src | ||
| tests | ||
| chronometrist-clim.asd | ||
| chronometrist-clobber.asd | ||
| chronometrist-plist.asd | ||
| chronometrist-sqlite.asd | ||
| chronometrist-tests.asd | ||
| chronometrist.asd | ||
| clim.org | ||
| README.org | ||
| TODO.org | ||
| UNLICENSE | ||
| WTFPL | ||
Chronometrist
Explanation
Chronometrist is a friendly and powerful personal time tracker and analyzer. This is a Common Lisp port of the Emacs Lisp original.
Currently, it contains
- an import-only plist-group backend
- an SQLite backend (nearly complete)
- a Clobber (object store) backend (nearly complete)
- a CLIM frontend (WIP)
Currently, this port can -
-
import from a plist-group file and export to an SQLite or Clobber database
(ql:quickload '(chronometrist-plist chronometrist-clobber)) (in-package :chronometrist) (let ((plg (make-instance 'chronometrist.plist-group:backend :file #P"/path/to/old/file")) (clobber (make-instance 'chronometrist.clobber:backend :file #P"/path/to/new/file"))) (chronometrist:initialize clobber) (chronometrist:migrate plg clobber :if-exists :overwrite)) -
display the CLIM GUI -
(ql:quickload '(chronometrist-clim)) (chronometrist-clim:run)
See TODO.org for ongoing work, future plans, and ideas.
Characteristics
- Made for personal use. By default, your data is stored on your machine and is only accessible to you.
- Don't make assumptions about the user's profession.
- (WIP) Hooks to integrate time tracking into your workflow.
- (WIP) Make it easy to edit and correct data.
- (WIP) Support mouse and keyboard use equally.
- (Planned) Extensions to automatically insert data from a variety of sources.
Motivation for the Common Lisp port
In March 2022, work began on the long-awaited Common Lisp port of Chronometrist, which aims to create -
- a greater variety of backends (e.g. SQLite)
- a common reusable library for frontends to use,
-
a greater variety of frontends, such as -
- a command line interface (CLI), for UNIX scripting;
- a terminal user inteface (TUI), for those so inclined;
- a CLIM (Common Lisp Interface Manager) GUI 1,
- Qt and Android interfaces using LQML,
- web frontends (possibly via Parenscript or CLOG),
- and perhaps even an interface for wearable devices!
The port was also driven by the desire to have access to Common Lisp's better performance, and features such as namespaces, a de facto standard build system, multithreading, SQLite bindings, a more fully-featured implementation of CLOS and MOP, and type annotations, checking, and inference.
The older Emacs Lisp codebase will probably become an Emacs frontend to a future Common Lisp CLI client.
Source code overview
Data structures
Intervals
Chronometrist stores ranges of time as instances of interval. Each interval has a names (a list of strings), start and stop timestamps, and optional properties (a Lisp property list).
Intervals which represent ongoing activities are said to be "active", and have no stop time associated with them.
It is planned to add support for storing events, as instances of event. An event, much like an interval, has a name and optional properties, but it is only associated with a time (timestamp) rather than a time range.
As far as possible, instances of local-time:timestamp are used to represent time, on account of the variety of operations available for them. time.lisp defines various time-related helpers.
Tasks
The concept of a task serves two purposes -
- Output
- A time tracker may display a flat list of tasks. Some users may prefer a foldable tree instead. The contents and hierarchy of these tasks may differ between users and situations.
- Input
- A task can be clocked into, i.e. an interval is inserted into the backend with certain tags and/or property-values.
To support this, Chronometrist provides a tree of task instances. A "task" is essentially a query, a means of selecting intervals by various means -
- the names present in the intervals
- the property-values in the interval property lists
- whether the intervals pass a given filter function
Each task contains a name (a string), depth (as an integer), an optional parent (an instance of task), and optional children (a list of instances of task).
While the task hierarchy affects how data is displayed and entered, neither the hierarchy nor instances of task are actually stored in instances of interval. This allows the user to change the task hierarchy while minimizing its effect on the data displayed in the frontend.
For instance, if the user has a task tree Foo → Bar → Quux and inheritance is enabled, clocking into Quux will create an interval whose names are ("Bar" "Foo" "Quux") (sorted alphabetically). Later, if the user changes their task tree to Quux → Bar → Foo, clocking into Foo will still create an interval whose names are ("Bar" "Foo" "Quux"). Intervals and durations which appeared under Quux before will now show up under Foo, because they both have the same ancestors and are equivalent in our view.
The basic task class
With the basic task class, the user specifies tags, properties, and/or property-values, which are used to determine both what intervals are displayed, as well as what tags, properties, and/or property-values are inserted into new intervals.
The inheriting-task class
This is designed to create tasks which display the intervals matched by their children, and which may (configurably) insert the tags of their ancestors when creating new intervals.
The user is expected to only specify task names and children. Tags are derived from names.
Configuration
chronometrist-cl's configuration has the following requirements -
- Users should be able to make persistent changes to the configuration, without requiring Lisp knowledge or changes to the init.lisp
- Extensions should be able to add new configuration options.
Configuration is implemented using the Ubiquitous library.
chronometrist-cl has two kinds of configuration options -
- global, which have a single value applicable to the entire program, and
- task-specific, which can potentially have a different value for each task.
Ubiquitous stores the configuration as a (serialization of a) hash table with the keys :global and :task, corresponding to the aforementioned types of configuration.
The :task key has another hash table as a value, whose keys are all tasks (as strings), with the exception of a single :default key. The value of the :default key is a hash table containing default values for task-specific options.
Note that while define-global-option and define-task-option require a non-keyword symbol as the option name (mostly for consistency with defun etc), options are defined as keywords based on the name, and thus all other functions expect options to be keywords.
See also - reference documentation for the configuration protocol.
CLIM frontend
The CLIM frontend uses CLIM panes for each view of data Chronometrist provides. Each pane has a display-pane method to display its contents.
Several different CLIM panes implementing different kinds of views have been experimented with, and these are present in the source code, even if not used. Work is underway to make a single "unified" pane, with the aim of presenting a single information-rich view which minimizes user interaction.
Tables
The tables in the CLIM frontend are designed to be easy to extend.
The table class represents a table. The columns displayed in the table are accessed by the table-schema (table) method, which returns a list of column instances.
The row class provides row-specific information, such as the index of the row. (Provided by default through the index-mixin class and index accessor.)
The column class represents a column descriptor (a list of which forms a table-schema), and may be subclassed to provide any column-specific information, such as the label text for the column. (Provided by default through the label accessor.) Pre-defined column subclasses include index-column and task-column.
The display-cell method -
- determines the contents of a cell based on the row, column, table, frame, and pane,
- displays the contents (possibly delegating to a CLIM
presentmethod), and -
returns the contents of the cell.
- This is necessary to be able to mutably hold state as well as aid testing.
Midnight-spanning intervals
A unique problem in working with Chronometrist, one I had never foreseen, was intervals which start on one day and end on another. For instance, you start working on something at 2021-01-01 23:00 hours and stop on 2021-01-02 01:00.
If not accounted for, midnight-spanning intervals can mess up data consumption in all sorts of unforeseen ways. Consider two of the most common operations throughout the program -
- Attempting to find the intervals belonging to a given date might end up with missing midnight-spanning intervals, near the start and the end of the day;
- Attempting to find the time spent on a task on a given day can yield inaccurate results. If the day's intervals used for this contain a midnight-spanning interval, the duration will include the previous day's time from the interval as well as the target day's.
There are a few different approaches for dealing with them. In this implementation, all backend reader functions can potentially return midnight-spanning intervals, and it's up to the client code to split them if necessary.
See interval operations for functions to split midnight-spanning intervals, check whether two intervals were split from one, and unify two split intervals into one.
License
I'd like for all software to be liberated - transparent, trustable, and accessible for anyone to use, study, or improve.
I'd like anyone using my software to credit me for the work.
I'd like to receive financial support for my efforts, so I can spend all my time doing what I find meaningful.
But I don't want to make demands or threats (e.g. via legal conditions) to accomplish all that, nor restrict my services to only those who can pay.
Thus, Chronometrist is released under your choice of Unlicense or the WTFPL.
Thanks
beach for trusting a passionate-but-inexperienced newbie with funding.
jackdaniel and other contributors for laying the foundations for this application through their work on McCLIM, and answering my regular stream of questions.
gilberth, hayley, ck_, rotateq, and many others for helping me out with CLIM and Common Lisp.
The task duration pane was inspired by the Android application, A Time Tracker
Reference
Entries
names (object) generic function
entry (standard-object) class
Protocol class for entries in the Chronometrist database.
interval (chronometrist:entry) class
A time range spent on a specific task, with optional properties.
make-interval (names start &optional stop properties) function
interval-equal? (a b) function
interval-duration (interval) function
split-interval (interval &optional (day-start-time (global-value *day-start-time*))) function
Return a list of two intervals if INTERVAL spans a midnight, else nil.
trim-intervals (intervals start end) function
Return INTERVALS with timestamps changed to fall between START and END.
intervals-split-p (old new) function
Return t if intervals OLD and NEW are split.
Split intervals means that the stop time of OLD must be the same as the start time of NEW, and they must have identical properties (except start and stop).
interval-unify (old new) function
Return a single interval which has the start time of OLD and the stop time of NEW.
OLD and NEW must be instances of chronometrist:interval.
event (chronometrist:entry) class
A named timestamp with optional properties.
Tasks
*task-types* variable
Available task types as a list of symbols.
define-task (name direct-superclasses direct-slots &rest options) macro function
Define a user-facing task class called NAME.
Like defclass, but NAME is added to *task-types*.
DIRECT-SUPERCLASSES should include chronometrist.task:task or one of its subclasses.
task (chronometrist-clim:task-mixin standard-object) class
Protocol class representing a 'task' in a Chronometrist interface.
name (object) generic function
properties (task operation) generic function
Return a list of keywords for TASK and OPERATION.
OPERATION can be :SELECT, which means the keywords are intended to be matched against the properties of existing intervals, or :INSERT, which means the properties are intended to be inserted into the properties of a new instance of interval.
property-values (task operation) generic function
Return a property list for TASK and OPERATION.
OPERATION can be :SELECT, which means the plist is intended to be matched against the properties of existing intervals, or :INSERT, which means the plist is intended to be inserted into the properties of a new instance of interval.
parent (object) generic function
children (object) generic function
depth (object) generic function
properties-match? (task interval) generic function
Return non-nil if INTERVAL has the same properties as TASK.
property-values-match? (task interval) generic function
Return non-nil if INTERVAL has the same property-values as TASK.
interval-match? (task interval) generic function
Return non-nil if INTERVAL should be considered part of TASK.
task-interval (task) generic function
Return an INTERVAL for TASK.
task-equal? (a b) function
ancestors (task) function
Return a list containing TASK and each of its ancestors.
task-ancestor? (ancestor successor) function
Return non-nil if task A is an ancestor of task B.
map-task-tree #'task-tree function
Call FUNCTION with each descendent node of TASK-TREE. TASK-TREE should be a list of task instances.
task-tree-to-list (task-tree) function
Simple task
simple-task (chronometrist.task:task chronometrist.task::update-parent-mixin) class
A task which displays and creates intervals based on a statically-defined set of tags, properties, and/or property-values.
make-simple-task (name &key (depth 0) parent (tags (list name)) properties children) function
Inheriting task
inheriting-task (chronometrist.task:simple-task) class
A task whose tags are derived from its own name and the names of its ancestors (for new intervals)/successors (for display).
make-inheriting-task (name &key (depth 0) parent properties children) function
Time operations
apply-time (time timestamp) function
Return an instance of local-time:timestamp for TIMESTAMP, with time modified to TIME.
TIME must be integer seconds from midnight.
TIMESTAMP must be an instance of local-time:timestamp.
midnight (&optional (timestamp (now))) function
Return a local-time:timestamp representing the midnight for TIMESTAMP. TIMESTAMP should be an instance of local-time:timestamp.
Configuration protocol
option (standard-object) class
Protocol class for all configuration options.
global-option (chronometrist:option) class
Protocol class for options which have one value for the entire application.
task-option (chronometrist:option) class
Protocol class for options which can have different values for each task.
global-value (option) generic function
Return the configuration value for OPTION. OPTION must be a keyword.
global-value (new-value option) generic function
Set the value for configuration OPTION to NEW-VALUE. OPTION must be a keyword.
task-value (task option) generic function
Return the TASK-specific configuration value for OPTION.
TASK must be a string. OPTION must be a keyword.
If a TASK-specific value is not found, return a default value.
The secondary value is T if the task-specific value was provided, and NIL if the default was used.
task-value (new-value task keyword) generic function
Set the TASK-specific configuration value for OPTION to NEW-VALUE.
TASK must be a string. OPTION must be a keyword.
define-global-option (name default &key documentation (value-class t)) macro function
Define an application-wide configuration option called NAME.
The values for this option must be instances of CLASS.
DEFAULT is the default value for this option.
define-task-option (name default &key documentation (value-class t)) macro function
Define a task-specific configuration option called NAME.
The values for this option must be instances of VALUE-CLASS.
DEFAULT is the default value for this option.
load-configuration () function
Predefined options
Global options
database-directory (chronometrist:global-option) class
Absolute pathname for the database directory.
database-pathname-name (chronometrist:global-option) class
Base name for the database file.
init-pathname (chronometrist:global-option) class
Absolute pathname for the user's init file.
allow-concurrent-intervals (chronometrist:global-option) class
Whether to allow the creation of concurrent intervals.
day-start-time (chronometrist:global-option) class
Time at which a day is considered to start, as number of seconds from midnight.
week-start-day (chronometrist:global-option) class
Day on which the week is considered to start, as an integer between 0-6.
Task options
show (chronometrist:task-option) class
Whether to hide or show this task in the application.
goal (chronometrist:task-option) class
The daily time goal for this task, in seconds.
property-presets (chronometrist:task-option) class
Preset property-value combinations for this task, to be used as suggestions.
Backend protocol
This section lists the current backend protocol, with some remarks.
backend (standard-object) class
Protocol class for Chronometrist backends.
file-backend-mixin (standard-object) class
Mixin for backends storing data in a single file.
define-backend (name direct-superclasses direct-slots &rest options) macro function
Define an instance of chronometrist:backend.
Like defclass, but also define a *<name>* special variable holding an instance of the class.
If DIRECT-SUPERCLASSES do not contain chronometrist:backend or a subclass of the same, chronometrist:backend is added to the DIRECT-SUPERCLASSES.
Writers
initialize (backend) generic function
Initialize and return BACKEND.
Disk-based backends should use this to create their file(s), if it does not already exist.
cleanup (backend) generic function
Perform cleanup for BACKEND. Called at application exit.
insert (backend object &key &allow-other-keys) generic function
Insert OBJECT into BACKEND.
Return non-nil if insertion is successful.
OBJECT may be an instance of chronometrist:interval or chronometrist:event.
update (backend old new) generic function
In BACKEND, update OLD object with NEW.
OBJECT may be an instance of chronometrist:interval or chronometrist:event.
remove (backend object &key &allow-other-keys) generic function
Remove OBJECT from BACKEND.
Return non-nil if OBJECT is successfully removed.
Signal an error if OBJECT was not found.
OBJECT may be an instance of chronometrist:interval or chronometrist:event.
Extended writer protocol
clock-in (backend task) generic function
Insert a new active interval for TASK to BACKEND.
clock-out (backend interval) generic function
In BACKEND, clock out of INTERVAL.
=migrate (input-backend output-backend &key if-exists interval-function
&allow-other-keys)= :generic:function:
Save data from INPUT-BACKEND to OUTPUT-BACKEND.
IF-EXISTS may be :ERROR (the default), :OVERWRITE, or :MERGE.
INTERVAL-FUNCTION should be a function that accepts and returns an instance of interval. It is called for every interval to be inserted.
Readers
active-backend () function
Return an object representing the currently active backend.
intervals (backend &key start end) generic function
Return a list of inactive intervals from BACKEND, or NIL if no intervals were found.
Second return value is a list of active intervals.
Both returned lists should be in reverse chronological order.
If START and END are provided, return intervals intersecting with that time range. START and END should be instances of local-time:timestamp.
If END is NIL, return all intervals from START.
If BACKEND is file-based, it must signal a FILE-DOES-NOT-EXIST condition if the file does not exist.
Extended reader protocol
latest-interval (backend) generic function
Return the latest interval from BACKEND, or nil if BACKEND contains no intervals.
Return value may be active, i.e. it may or may not have a stop value.
If the latest record starts on one day and ends on another, the entire (unsplit) record must be returned.
list-tasks (backend) generic function
Return all tasks recorded in BACKEND as a list of chronometrist:task instances.
active-days (backend task &key start end) generic function
From BACKEND, return number of days on which TASK had recorded time.
current-task (&optional (backend (active-backend))) function
Return the name of the active task as a string, or nil if not clocked in.
=task-duration (task &key (start (midnight))
(end (adjust-timestamp start (offset hour 24))) (backend (active-backend)) (intervals nil intervals-supplied?))= :function:
Return time spent on TASK between START and END.
TASK should be an instance of chronometrist:task.
The return value is a duration in seconds, as an integer.
- Change optional arguments to keyword arguments?
-
Rename
task-duration-one-daytotask-duration, and change the signature to -(defun task-duration (task &key (start (midnight)) (end (local-time:adjust-timestamp start (offset :hour 24))) (backend (active-backend)) (intervals nil intervals-supplied?)) ...)Shorter name would result in better indentation and thus easier reading of calls.
Conditions
no-active-backend condition
Condition raised when no active backend has been specified.
Of questionable utility
on-add (backend)on-modify (backend)on-remove (backend)on-change (backend)verify (backend)task-records-for-date (backend task date &key &allow-other-keys)(default method provided)
Table protocol
table (standard-object) class
A class for tables which are easy to extend with new columns.
schema (object) generic function
fold-mixin (standard-object) class
folded? (object) generic function
index-mixin (standard-object) class
index (object) generic function
row (chronometrist-clim:index-mixin) class
foldable-row (chronometrist-clim:fold-mixin chronometrist-clim:row) class
column (chronometrist-clim:label-mixin) class
label (object) generic function
index-column (chronometrist-clim:column chronometrist-clim:index-mixin) class
task-name-column (chronometrist-clim:column) class
display-cell (frame pane table column row view) generic function
Display and return the contents of a cell in TABLE.
FRAME and PANE are the CLIM frame and pane as passed to the display function.
TABLE, ROW, and COLUMN are instances of table, row, and column, respectively.
cell (chronometrist-clim:label-mixin) class
header (chronometrist-clim:cell) class
index (chronometrist-clim:cell chronometrist-clim:index-mixin) class
McCLIM also has an incomplete ncurses backend - when completed, a CLIM frontend could provide a TUI "for free".