ptimelog 0.9.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/AGENTS.md +42 -0
- data/PLAN-ADD-COMMAND.md +140 -0
- data/README.md +117 -12
- data/exe/ptimelog +26 -2
- data/lib/ptimelog/command/add.rb +50 -11
- data/lib/ptimelog/command/confirm.rb +82 -0
- data/lib/ptimelog/command/edit.rb +47 -7
- data/lib/ptimelog/command/info.rb +48 -0
- data/lib/ptimelog/command/show.rb +46 -26
- data/lib/ptimelog/command/upload.rb +49 -18
- data/lib/ptimelog/configuration.rb +20 -10
- data/lib/ptimelog/data_source.rb +43 -0
- data/lib/ptimelog/day.rb +9 -34
- data/lib/ptimelog/entry.rb +92 -25
- data/lib/ptimelog/error.rb +7 -0
- data/lib/ptimelog/joiner.rb +47 -0
- data/lib/ptimelog/named_date.rb +26 -4
- data/lib/ptimelog/operon.rb +170 -0
- data/lib/ptimelog/script.rb +1 -1
- data/lib/ptimelog/timelog.rb +66 -1
- data/lib/ptimelog/version.rb +9 -1
- data/lib/ptimelog.rb +14 -3
- data/opencode.json +37 -0
- metadata +136 -47
- data/.envrc +0 -1
- data/.gitignore +0 -10
- data/.overcommit.yml +0 -26
- data/.rspec +0 -2
- data/.rubocop.yml +0 -33
- data/.rubocop_todo.yml +0 -16
- data/.ruby-gemset +0 -1
- data/.ruby-version +0 -1
- data/.travis.yml +0 -7
- data/bin/console +0 -15
- data/bin/setup +0 -8
- data/lib/ptimelog/app.rb +0 -21
- data/lib/ptimelog/command/base.rb +0 -29
- data/lib/ptimelog/command/version.rb +0 -16
- data/ptimelog.gemspec +0 -34
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cd30deb9f9246b10264f5e9af3a35c279ffee1c0766947b47d52c4deaf00a608
|
|
4
|
+
data.tar.gz: a544d9b97ecca711bd66dc6ecf9325d3110601b6bbaa2b7a499ffda152be4916
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 63dd6436e43358e1e59ba11277c470f37880745f7033001c8776d427d2656827007fe4a231551466bcf9328de3532ef04c621b324ccce14dfe253c7e91a93c62
|
|
7
|
+
data.tar.gz: 81e2c263b3785429f79ac9df991ecb6c19517620efd6351cafcba13450a7691ed3415545825e0c488a635ea8478099224fc05de383ff5f2d45ce9c6069ead2af
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
## Technical
|
|
2
|
+
|
|
3
|
+
- written in Ruby
|
|
4
|
+
- Tests with RSpec, execute `rspec` to run them
|
|
5
|
+
- Code-Style is checked with RuboCop, run `rubocop` to check
|
|
6
|
+
|
|
7
|
+
## Ideas
|
|
8
|
+
|
|
9
|
+
The project is a CLI that collects time-tracking information into entries and allows to show them.
|
|
10
|
+
The data-backend is either the data of gtimelog or of a dialy note in Obsidian.md
|
|
11
|
+
|
|
12
|
+
### gtimelog
|
|
13
|
+
|
|
14
|
+
gtimelog is a GTK GUI app, that uses a single file to store when a task has ended.
|
|
15
|
+
|
|
16
|
+
This allows to infer the duration of tasks by reading mulitple lines.
|
|
17
|
+
|
|
18
|
+
### Obsidian.md
|
|
19
|
+
|
|
20
|
+
Obsidian.md (mostly called obsidian) is a note-taking application allows to have daily notes in markdown-format.
|
|
21
|
+
|
|
22
|
+
The plugin 'dayplanner' adds a special section to daily notes that are time-tracking entries. They are grouped under a headline and formatted like this
|
|
23
|
+
|
|
24
|
+
```markdown
|
|
25
|
+
- 10:00 - 11:00 Task 17: Hard work, Analysis, Feedback -- work client mail
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
This describes a time-entry from 10:00 to 11:00 o'clock. The entry's ticket is "Task 17" and its description is "Hard work, Analysis, Feedback". The entry is tagged with "work", "client" and "mail"
|
|
29
|
+
|
|
30
|
+
## Development-Workflow
|
|
31
|
+
|
|
32
|
+
- Always write a spec first
|
|
33
|
+
- The spec should define the desired state
|
|
34
|
+
- The spec should be failing at first
|
|
35
|
+
- Then add the implementation
|
|
36
|
+
- The spec should now be successful
|
|
37
|
+
- The whole spec-suite should also still be without errors
|
|
38
|
+
- run `rspec` to ensure that
|
|
39
|
+
- if any spec fails, fix the new implementation until all specs pass
|
|
40
|
+
- Lastly, run `rubocop` to ensure the code-style is good.
|
|
41
|
+
- verify completeness by running `rspec` and `rubocop`.
|
|
42
|
+
- Propose a commit-message, summarizing the need for the change
|
data/PLAN-ADD-COMMAND.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Plan: `add`-command with Obsidian/Operon as datasource
|
|
2
|
+
|
|
3
|
+
Collected ideas and findings for a later planning session. Nothing here is
|
|
4
|
+
implemented yet.
|
|
5
|
+
|
|
6
|
+
## 1. Current state
|
|
7
|
+
|
|
8
|
+
- **Decision:** with Obsidian/Operon as datasource, `add` stays disabled for now.
|
|
9
|
+
- **`add` crashes with both backends today.** `Command::Add#execute` calls
|
|
10
|
+
`backend.add(entry)` (`lib/ptimelog/command/add.rb:28`). Neither
|
|
11
|
+
`Ptimelog::Operon` nor `Ptimelog::Timelog` defines `add`; the timelog
|
|
12
|
+
version is commented out (`lib/ptimelog/timelog.rb:50`). So `ptimelog add …`
|
|
13
|
+
reaches `previous_entry`, then fails with `NoMethodError`.
|
|
14
|
+
- **Already built and committed:** `Operon#previous_entry`
|
|
15
|
+
(`lib/ptimelog/operon.rb:44`). It returns the running timer's start, else the
|
|
16
|
+
end of the day's last session, else `operon_start_time` (default `08:00`).
|
|
17
|
+
The entry is hidden (`start **`) and has specs.
|
|
18
|
+
- **README** already states that `add` is not available for the Obsidian
|
|
19
|
+
datasource.
|
|
20
|
+
- **Tests:** the general-flow spec in `spec/ptimelog/command/add_spec.rb` is
|
|
21
|
+
commented out, and the `with obsidian+operon as backend` context is empty.
|
|
22
|
+
|
|
23
|
+
## 2. Hard limits from the Operon docs
|
|
24
|
+
|
|
25
|
+
1. **`trackers`, `duration` and `activeTracker` belong to Operon.** They can be
|
|
26
|
+
read, but should not be edited by hand (`DOCS-012:92-117`). `add` cannot
|
|
27
|
+
write tracked time.
|
|
28
|
+
2. **`datetimeStart` and `datetimeEnd` are user-settable fields**
|
|
29
|
+
(`DOCS-012:70-71`). They describe a *planned* block — the only safe thing
|
|
30
|
+
`add` could write.
|
|
31
|
+
3. **ptimelog only reads tracked time** (deliberate decision). A planned block
|
|
32
|
+
written by `add` does not show up in `ptimelog show` until **Log as
|
|
33
|
+
tracked** is pressed in Operon. The flow is always two steps.
|
|
34
|
+
4. **`{{operonId}}` resolves on its own.** A probe showed Operon replaces a
|
|
35
|
+
literal `{{operonId:: {{operonId}}}}` shortly after the line appears,
|
|
36
|
+
without a restart. ptimelog never has to mint ids. Should it ever be needed:
|
|
37
|
+
ids are 7 characters of `[a-z0-9]` (`DOCS-015`), e.g.
|
|
38
|
+
`SecureRandom.random_number(36**7).to_s(36).rjust(7, '0')`.
|
|
39
|
+
5. **Other template variables are unproven** on a line ptimelog writes
|
|
40
|
+
(`{{date}}`, `{{status}}`, `{{priority}}`, see `DOCS-061`). Write literal
|
|
41
|
+
values for those.
|
|
42
|
+
|
|
43
|
+
## 3. Shape of a written line
|
|
44
|
+
|
|
45
|
+
```markdown
|
|
46
|
+
- [ ] <description> {{operonId:: {{operonId}}}} {{ticket:: …}} {{ptimelog:: …}} {{datetimeStart:: YYYY-MM-DDTHH:MM:00}} {{datetimeEnd:: …}} {{dateScheduled:: YYYY-MM-DD}}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- **Start:** `previous_entry.finish_time`. **End:** the parsed time or offset;
|
|
50
|
+
`parse_task` in `add.rb` already handles `-5`, `+5` and `HH:MM`.
|
|
51
|
+
- **`dateScheduled`:** matches the Operon setting
|
|
52
|
+
`inlineTaskDailyNoteAddScheduledDate: true` and makes the task show under
|
|
53
|
+
`## Heute geplant`.
|
|
54
|
+
- **Appending:** add the line as plain text so nothing else in the note moves.
|
|
55
|
+
Do not rewrite the note through a markdown parser; the old dayplanner adapter
|
|
56
|
+
had escaping trouble with `[`, `]`, `#`, `_` and `-`.
|
|
57
|
+
|
|
58
|
+
## 4. Where the line goes
|
|
59
|
+
|
|
60
|
+
Operon's Task Router (`DOCS-136:26-38`) offers five destinations: Daily Notes,
|
|
61
|
+
Weekly Notes, Specific File, Active File, Ask Every Time. A CLI can only serve
|
|
62
|
+
**Daily Notes** and **Specific File**. Operon has no "inbox" concept; Specific
|
|
63
|
+
File *is* the inbox.
|
|
64
|
+
|
|
65
|
+
Current local setup:
|
|
66
|
+
|
|
67
|
+
- **Operon** (`.obsidian/plugins/operon/data.json → ui.taskCreationProfile`,
|
|
68
|
+
version 3): `specific-file`, pointing at `Operon/Tasks/Operon Inbox.md`, with
|
|
69
|
+
heading keyword `Heute neu`.
|
|
70
|
+
- **Obsidian Core daily notes** (`.obsidian/daily-notes.json`): folder `daily`,
|
|
71
|
+
format `YYYY-MM-DD`, template `intern/Templates/Daily Note.md`. The template
|
|
72
|
+
has no tokens, so creating a missing note is a plain copy.
|
|
73
|
+
|
|
74
|
+
Placement depends on the destination:
|
|
75
|
+
|
|
76
|
+
| Destination | Line lands |
|
|
77
|
+
|---------------|----------------------------------------------------------------------------|
|
|
78
|
+
| Daily Notes | under the first heading containing `Heute neu`; `##` created if missing |
|
|
79
|
+
| Specific File | under a date heading like `## [[2026-10-08]]`; the Inbox already uses them |
|
|
80
|
+
|
|
81
|
+
Missing daily notes (`DOCS-137:56-64`): Operon creates them from the template;
|
|
82
|
+
with `createDailyNotesAsOperonTask` on, the note becomes the task's parent. That
|
|
83
|
+
setting is currently off.
|
|
84
|
+
|
|
85
|
+
## 5. Design options
|
|
86
|
+
|
|
87
|
+
Proposed config: `operon_task_add_location` with the values `"daily"`, an
|
|
88
|
+
absolute path, or `false` (default, disables `add`).
|
|
89
|
+
|
|
90
|
+
Three ways to determine the destination:
|
|
91
|
+
|
|
92
|
+
- **A. Read Operon's own settings** (`data.json` plus `daily-notes.json`)
|
|
93
|
+
- Pro: never drifts from what Operon's Task Creator does.
|
|
94
|
+
- Con: depends on Operon's private plugin data. It is versioned, its mode
|
|
95
|
+
values are undocumented, and it is not a published contract
|
|
96
|
+
(`DOCS-044`, `DOCS-046`).
|
|
97
|
+
- **B. Own ptimelog key**, reading only Core's `daily-notes.json` for the daily
|
|
98
|
+
folder and format (earlier recommendation)
|
|
99
|
+
- Pro: small, explicit, owned by ptimelog.
|
|
100
|
+
- Con: repeats a setting Operon already has, so the two can disagree.
|
|
101
|
+
- **C. Delegate to `operon-cli`** (`operon task create`, `DOCS-127`)
|
|
102
|
+
- Pro: the supported route, with preview, verified writes and parent linking.
|
|
103
|
+
- Con: not installed here, needs Obsidian running plus the official Obsidian
|
|
104
|
+
CLI, native Linux is "public beta", and the local Obsidian is a flatpak.
|
|
105
|
+
A dead end for now.
|
|
106
|
+
|
|
107
|
+
## 6. Open questions
|
|
108
|
+
|
|
109
|
+
1. **Is `add` worth it at all,** given its output is invisible to `show` until
|
|
110
|
+
tracked in Operon? The alternative is capturing directly in Operon.
|
|
111
|
+
2. **Destination:** A, B or C. Should `"daily"` follow Core's
|
|
112
|
+
`daily-notes.json` or a ptimelog-owned folder?
|
|
113
|
+
3. **Missing note:** create it (from the template for daily notes, empty for a
|
|
114
|
+
specific file) or refuse with a hint?
|
|
115
|
+
4. **Dry run:** add `add --dry-run` that prints the line without writing
|
|
116
|
+
(about 3 lines; `confirm` already has `-n`)?
|
|
117
|
+
5. **Concurrent writes:** if Obsidian has the note open, its next save can
|
|
118
|
+
overwrite ptimelog's line. Accept the risk, or document "close Obsidian
|
|
119
|
+
first"?
|
|
120
|
+
|
|
121
|
+
## 7. Verification procedure
|
|
122
|
+
|
|
123
|
+
1. **Specs first:** exactly one line appended; the rest of the note stays
|
|
124
|
+
byte-identical; the line reads back with the same fields; start equals
|
|
125
|
+
`previous_entry`; `show` does **not** list it.
|
|
126
|
+
2. **Dry run:** prints the exact line and writes nothing.
|
|
127
|
+
3. **Real write with Obsidian closed:** back up the note, run `add`. `show`
|
|
128
|
+
must be unchanged. In Obsidian the task gets a real id and appears on the
|
|
129
|
+
Calendar. After **Log as tracked**, `show` has exactly one new entry with
|
|
130
|
+
matching times. Restore the note.
|
|
131
|
+
4. **Regression check:** the session-parity check against
|
|
132
|
+
`~/Obsidian/Things/export-operon-time` still matches (37/37 sessions over
|
|
133
|
+
11 days at the time of writing).
|
|
134
|
+
|
|
135
|
+
## 8. Possible small step before the full design
|
|
136
|
+
|
|
137
|
+
Today "disabled" means a `NoMethodError`. A one-method `Operon#add` raising a
|
|
138
|
+
`Ptimelog::Error` ("adding entries is not supported for the Obsidian
|
|
139
|
+
datasource") would turn that into a clear message — same pattern as
|
|
140
|
+
`Operon::VaultNotFound`, with one spec.
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pTimeLog
|
|
2
2
|
|
|
3
|
-
small tooling to transfer timelog-entries from gtimelog's timelog.txt to the PuzzleTime
|
|
3
|
+
small tooling to transfer timelog-entries from gtimelog's timelog.txt or the tasks of an Obsidian vault to the PuzzleTime time-tracking web-application.
|
|
4
4
|
|
|
5
5
|
## Approach
|
|
6
6
|
|
|
@@ -18,14 +18,15 @@ small tooling to transfer timelog-entries from gtimelog's timelog.txt to the Puz
|
|
|
18
18
|
- [x] support user-supplied ticket-parsers
|
|
19
19
|
- [x] get ticket-parser from tags
|
|
20
20
|
- [x] merge equal adjacent entries into one
|
|
21
|
+
- [x] caculate, show and upload durations
|
|
21
22
|
- [ ] complete login/entry automation
|
|
22
23
|
- [ ] handle authentication
|
|
23
24
|
- [ ] login and store cookie (https://stackoverflow.com/questions/12399087/curl-to-access-a-page-that-requires-a-login-from-a-different-page#12399176)
|
|
24
25
|
- [ ] send user and pwd with every request
|
|
25
26
|
- [ ] make entries
|
|
26
27
|
- [ ] open day in browser for review
|
|
27
|
-
- [
|
|
28
|
-
- [
|
|
28
|
+
- [x] avoid duplicate entries
|
|
29
|
+
- [x] start/end time as indicator?
|
|
29
30
|
- [x] offer rounding times to the next 5, 10 or 15 minutes
|
|
30
31
|
- [x] allow to add entries from the command-line
|
|
31
32
|
- [ ] handle time-account and billable better
|
|
@@ -38,8 +39,17 @@ small tooling to transfer timelog-entries from gtimelog's timelog.txt to the Puz
|
|
|
38
39
|
- [ ] allow to have a list of "favourite" time-accounts
|
|
39
40
|
- [ ] select best-matching time-account according to tags, possibly limited to the favourites
|
|
40
41
|
- [x] combine billable and account-lookup into one script
|
|
41
|
-
- [
|
|
42
|
-
- [
|
|
42
|
+
- [x] add cli-help
|
|
43
|
+
- [x] use ~~commander~~ _cmd_parse_ for CLI
|
|
44
|
+
- [x] support Obsidian as datasource
|
|
45
|
+
- [x] with explicit config
|
|
46
|
+
- [x] with reading the config from the Obsidian Vault
|
|
47
|
+
- [x] time-entries from the sessions Operon tracked on its tasks
|
|
48
|
+
- [ ] time-account/billable/metadata lookup from a note
|
|
49
|
+
- [x] support show-command: output the entries of a day
|
|
50
|
+
- [x] support upload-command: send the entries of a day to ptime
|
|
51
|
+
- [ ] support add-command: add a new entry to the current day
|
|
52
|
+
- [ ] support "last"-day for obsidian
|
|
43
53
|
|
|
44
54
|
## Installation
|
|
45
55
|
|
|
@@ -51,35 +61,88 @@ Install it with:
|
|
|
51
61
|
|
|
52
62
|
$ ptimelog ACTION DATE
|
|
53
63
|
|
|
64
|
+
Maybe it's best to explore the commands with `ptimelog help` and going from
|
|
65
|
+
there.
|
|
66
|
+
|
|
54
67
|
### Actions
|
|
55
68
|
|
|
56
69
|
Currently supported actions are
|
|
57
70
|
|
|
58
71
|
- show
|
|
59
72
|
- upload
|
|
73
|
+
- confirm
|
|
60
74
|
- edit
|
|
75
|
+
- add
|
|
76
|
+
- version
|
|
77
|
+
- help
|
|
78
|
+
- info
|
|
61
79
|
|
|
62
80
|
### Date-Identifier
|
|
63
81
|
|
|
64
|
-
To handle a specific date, the format YYYY-MM-DD is expected, e.g. 2017-12-25.
|
|
82
|
+
To handle a specific date, the format YYYY-MM-DD is expected, e.g. 2017-12-25.
|
|
83
|
+
Please note that you should not work on that day, unless you bring presents or
|
|
84
|
+
release a new version of Ruby.
|
|
65
85
|
|
|
66
86
|
For reusability in a shell-history the following keywords are supported:
|
|
67
87
|
|
|
68
88
|
- today
|
|
69
89
|
- yesterday
|
|
70
|
-
- last
|
|
71
90
|
- all
|
|
72
91
|
|
|
73
|
-
|
|
92
|
+
Currently, you need to pass a day. The previously exisiting "last" worked fine
|
|
93
|
+
to timelog.txt, but not for Obsidian.
|
|
74
94
|
|
|
75
95
|
### Edit-Identifier
|
|
76
96
|
|
|
77
|
-
When the action is "edit", the next argument is treated as script that should
|
|
97
|
+
When the action is "edit", the next argument is treated as script that should
|
|
98
|
+
be edited.
|
|
78
99
|
|
|
79
100
|
If nothing is passed, the main timelog.txt is loaded.
|
|
101
|
+
:warn: The Obsidian-Datasource has no single file, because an entry can
|
|
102
|
+
come from any note of the vault. Without an argument, this command only
|
|
103
|
+
edits an inferer-script, which is the thing that can still be edited.
|
|
80
104
|
|
|
81
105
|
Otherwise, a script to determine the time-account is loaded.
|
|
82
106
|
|
|
107
|
+
|
|
108
|
+
### Adding entries
|
|
109
|
+
|
|
110
|
+
In order to add entries with the ptimelog-cli, the complete entry needs to be
|
|
111
|
+
quoted on the command-line to count as one argument.
|
|
112
|
+
|
|
113
|
+
$ ptimelog add 'ticket 1337: Implement requirements -- client coding'
|
|
114
|
+
|
|
115
|
+
While this requires some knowledge of the file-format, it is no different than
|
|
116
|
+
entering the same string in gTimelog. For now, the entry is added to the
|
|
117
|
+
timelog.txt as it is passed. By default, the date/time added to the entry is
|
|
118
|
+
the one when the command is executed.
|
|
119
|
+
|
|
120
|
+
You can prefix a positive or negative signed number to slightly skew the entry
|
|
121
|
+
(think: '-5 meeting' or '+5 lunch \*\*') or even set a precise time ('10:30
|
|
122
|
+
meeting').
|
|
123
|
+
|
|
124
|
+
$ ptimelog add '-5 meeting: Discuss requirements -- client planning'
|
|
125
|
+
|
|
126
|
+
:warn: Not available for the Obsidian-Datasource. Tracked time belongs to
|
|
127
|
+
a task in Operon, and only Operon may write it.
|
|
128
|
+
|
|
129
|
+
### Showing the Version
|
|
130
|
+
|
|
131
|
+
I got tired of asking rubygems which version I installed, so I took on the
|
|
132
|
+
herculean task of letting ptimelog show its own version. With the switch to a
|
|
133
|
+
cmd_parse-based CLI, this has been partially offloaded to cmd_parse. You can
|
|
134
|
+
imagine the weight that has been lifted off my shoulders.
|
|
135
|
+
|
|
136
|
+
### Formatting the Output
|
|
137
|
+
|
|
138
|
+
In order to format the output of the show-action into a table, a hopefully
|
|
139
|
+
convienient field-marker has been chosen. After 0.10.0, I switched from '∴' to
|
|
140
|
+
'|', which is much more widely used, but also much easier to find on keyboard.
|
|
141
|
+
This removes one need to copy an unusual separator, maybe even using the mouse...
|
|
142
|
+
Anyway, you can pipe the output into `column`:
|
|
143
|
+
|
|
144
|
+
ptimelog show today | column -t -s'|'
|
|
145
|
+
|
|
83
146
|
## Helper-Scripts
|
|
84
147
|
|
|
85
148
|
ptimelog can prefill the account-number and billable-state of an entry.
|
|
@@ -97,13 +160,56 @@ Since these scripts are called a lot, it is better to write them in a compiled
|
|
|
97
160
|
language. If you only like ruby, take a look at crystal. For such simple
|
|
98
161
|
scripts, the code is almost identical and "just" needs to be compiled.
|
|
99
162
|
|
|
163
|
+
## Operon
|
|
164
|
+
|
|
165
|
+
With `obsidian: true`, entries are read from the time-sessions
|
|
166
|
+
[Operon](https://github.com/hasanyilmaz/operon) tracked on the tasks of your
|
|
167
|
+
vault. Operon stores them on the task itself, so one entry comes from one
|
|
168
|
+
session, not from one task:
|
|
169
|
+
|
|
170
|
+
{{trackers:: 2026-10-07T11:00:00/2026-10-07T12:00:00; 2026-10-07T14:00:00/2026-10-07T15:00:00}}
|
|
171
|
+
|
|
172
|
+
Only tracked time is read. A running timer and a planned block
|
|
173
|
+
(`datetimeStart`/`datetimeEnd`) are no time yet, so they are skipped. A
|
|
174
|
+
session counts on the day it started on. Notes below `archive/` and Operons
|
|
175
|
+
own documentation are skipped.
|
|
176
|
+
|
|
177
|
+
Ticket and account-tags come from two custom keys, which you can create in
|
|
178
|
+
**Settings → Operon → Core → Custom Keys**:
|
|
179
|
+
|
|
180
|
+
- [ ] HIT-12345: Zeug machen {{operonId:: …}} {{ticket:: HIT-12345}} {{ptimelog:: hit sac mv}}
|
|
181
|
+
|
|
182
|
+
`ticket` is the text puzzletime is asked for. `ptimelog` is a single,
|
|
183
|
+
space-separated string of tags, identical to what the dayplanner-entries used
|
|
184
|
+
to carry after their `--`. The first tag selects the inferer-script, the rest
|
|
185
|
+
are passed to it. A task without that key is still listed, but has no account.
|
|
186
|
+
|
|
187
|
+
If your vault already uses other names for these keys, map them with
|
|
188
|
+
`operon_custom_keys`. A key you leave out keeps its default:
|
|
189
|
+
|
|
190
|
+
operon_custom_keys:
|
|
191
|
+
ticket: issue
|
|
192
|
+
inferer: account
|
|
193
|
+
|
|
100
194
|
## Configuration
|
|
101
195
|
|
|
102
196
|
A config-file is read from `$HOME/.config/ptimelog/config`. It is expected
|
|
103
197
|
to be a YAML-file. Currently, it supports the following keys:
|
|
104
198
|
|
|
105
|
-
|
|
106
|
-
|
|
199
|
+
- rounding: [integer or false, default 15]
|
|
200
|
+
- base_url: [url to your puzzletime-installation, default https://time.puzzle.ch]
|
|
201
|
+
- obsidian: [boolean, default false, read from an Obsidian vault instead of timelog.txt]
|
|
202
|
+
- obsidian_vault: [path to that vault, default ~/path/to/obsidian-vault]
|
|
203
|
+
- operon_custom_keys: [hash, default { ticket: ticket, inferer: ptimelog }, names of the keys Operon stores ticket and tags in]
|
|
204
|
+
|
|
205
|
+
If the default path of `obsidian_vault` works for you, I really want to talk to
|
|
206
|
+
you :-)
|
|
207
|
+
|
|
208
|
+
Operons tracked time is stored to the second, so set `rounding: false` to keep
|
|
209
|
+
it exact.
|
|
210
|
+
|
|
211
|
+
It might still work with the timelog.txt-config. I do not use it anymore, but
|
|
212
|
+
I do not want to break things needlessly. So I left it in.
|
|
107
213
|
|
|
108
214
|
## Development
|
|
109
215
|
|
|
@@ -122,7 +228,6 @@ git commits and tags, and push the `.gem` file to
|
|
|
122
228
|
|
|
123
229
|
Bug reports and pull requests are welcome on GitHub at https://github.com/kronn/ptimelog.
|
|
124
230
|
|
|
125
|
-
|
|
126
231
|
## License
|
|
127
232
|
|
|
128
233
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/exe/ptimelog
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
require 'cmdparse'
|
|
5
|
+
require 'ptimelog'
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
parser = CmdParse::CommandParser.new(handle_exceptions: true)
|
|
8
|
+
parser.main_options.version = Ptimelog::VERSION
|
|
9
|
+
parser.main_options.program_name = 'ptimelog'
|
|
10
|
+
parser.main_options.banner = Ptimelog::BANNER
|
|
11
|
+
|
|
12
|
+
parser.add_command(CmdParse::VersionCommand.new)
|
|
13
|
+
parser.add_command(CmdParse::HelpCommand.new, default: true)
|
|
14
|
+
parser.add_command(Ptimelog::Command::Info.new)
|
|
15
|
+
|
|
16
|
+
parser.add_command(Ptimelog::Command::Add.new)
|
|
17
|
+
parser.add_command(Ptimelog::Command::Edit.new)
|
|
18
|
+
parser.add_command(Ptimelog::Command::Show.new)
|
|
19
|
+
parser.add_command(Ptimelog::Command::Upload.new)
|
|
20
|
+
parser.add_command(Ptimelog::Command::Confirm.new)
|
|
21
|
+
|
|
22
|
+
begin
|
|
23
|
+
parser.parse
|
|
24
|
+
rescue Ptimelog::Error => e
|
|
25
|
+
warn <<~MSG
|
|
26
|
+
An Error occured: #{e.class}
|
|
27
|
+
|
|
28
|
+
#{e.message}
|
|
29
|
+
MSG
|
|
30
|
+
end
|
data/lib/ptimelog/command/add.rb
CHANGED
|
@@ -5,25 +5,64 @@ require 'pathname'
|
|
|
5
5
|
module Ptimelog
|
|
6
6
|
module Command
|
|
7
7
|
# add a new entrie with the current date and time
|
|
8
|
-
class Add <
|
|
9
|
-
def initialize
|
|
10
|
-
super()
|
|
8
|
+
class Add < CmdParse::Command
|
|
9
|
+
def initialize
|
|
10
|
+
super('add', takes_commands: false)
|
|
11
11
|
|
|
12
|
-
@
|
|
13
|
-
|
|
12
|
+
@data_source = DataSource.new(
|
|
13
|
+
Configuration.instance,
|
|
14
|
+
NamedDate.new.named_date('today')
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
options.on('--debug', 'Show debugging output') { @debug = true }
|
|
14
18
|
end
|
|
15
19
|
|
|
16
|
-
def
|
|
17
|
-
|
|
20
|
+
def execute(task_line)
|
|
21
|
+
start = backend.previous_entry.finish_time
|
|
22
|
+
finish, task = parse_task(task_line)
|
|
23
|
+
ticket, desc = task.split(':', 2)
|
|
24
|
+
|
|
25
|
+
entry = create_entry(start, finish, ticket, desc)
|
|
26
|
+
debug(entry.to_s)
|
|
27
|
+
|
|
28
|
+
backend.add(entry)
|
|
18
29
|
end
|
|
19
30
|
|
|
20
|
-
|
|
21
|
-
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def parse_task(line)
|
|
34
|
+
matches = line.match('(?<time>\d{1,2}:\d{2} )?(?<offset>[+-]\d+ )?(?<task>.*)')
|
|
35
|
+
formatted_time = if matches[:time]
|
|
36
|
+
Time.parse(matches[:time])
|
|
37
|
+
else
|
|
38
|
+
Time.now
|
|
39
|
+
end
|
|
40
|
+
.localtime
|
|
41
|
+
.then { |time| time + (matches[:offset].to_i * 60) }
|
|
42
|
+
.strftime('%R')
|
|
43
|
+
|
|
44
|
+
[formatted_time, matches[:task]]
|
|
45
|
+
end
|
|
22
46
|
|
|
23
|
-
|
|
24
|
-
|
|
47
|
+
def create_entry(start, finish, ticket, desc)
|
|
48
|
+
Entry.new.tap do |e|
|
|
49
|
+
e.date = Date.today
|
|
50
|
+
e.start_time = start
|
|
51
|
+
e.finish_time = finish
|
|
52
|
+
e.ticket = ticket
|
|
53
|
+
e.description = desc
|
|
25
54
|
end
|
|
26
55
|
end
|
|
56
|
+
|
|
57
|
+
def backend
|
|
58
|
+
@data_source.backend
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def debug(msg)
|
|
62
|
+
return unless @debug
|
|
63
|
+
|
|
64
|
+
warn msg
|
|
65
|
+
end
|
|
27
66
|
end
|
|
28
67
|
end
|
|
29
68
|
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ptimelog
|
|
4
|
+
module Command
|
|
5
|
+
# show entries of one day or all of them
|
|
6
|
+
class Confirm < CmdParse::Command
|
|
7
|
+
attr_reader :entries
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
super('confirm', takes_commands: false)
|
|
11
|
+
@durations = false
|
|
12
|
+
@dry_run = false
|
|
13
|
+
@entries = {}
|
|
14
|
+
@config = Configuration.instance
|
|
15
|
+
|
|
16
|
+
options.on('-d', '--durations', 'Summarize durations by ticket') { @durations = true }
|
|
17
|
+
options.on('-n', '--dry-run', 'Do not execute, just dry-run') { @dry_run = true }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def execute(maybe_named_day) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity
|
|
21
|
+
@day = Ptimelog::NamedDate.new.named_date(maybe_named_day)
|
|
22
|
+
@entries = Ptimelog::DataSource.new(@config, @day).entries
|
|
23
|
+
|
|
24
|
+
@entries.each do |date, list|
|
|
25
|
+
next if list.empty?
|
|
26
|
+
|
|
27
|
+
valids = Ptimelog::Joiner.new(compact_on_ticket_only: @durations).join_similar(
|
|
28
|
+
list.select(&:valid?).sort_by { |entry| entry.ticket.to_s }
|
|
29
|
+
).sort_by(&:start_time)
|
|
30
|
+
total_duration = duration(valids.sum(&:duration))
|
|
31
|
+
|
|
32
|
+
output(date, valids, total_duration)
|
|
33
|
+
|
|
34
|
+
upload(date) if ask('Upload this day? [Yn]', 'y').downcase == 'y'
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def output(date, entries, total_duration)
|
|
41
|
+
puts date,
|
|
42
|
+
'----------'
|
|
43
|
+
|
|
44
|
+
entries.each do |entry|
|
|
45
|
+
puts entry
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
puts '----------',
|
|
49
|
+
"Total work done: #{total_duration} hours",
|
|
50
|
+
'----------------------------',
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def ask(question, default = nil)
|
|
55
|
+
puts question
|
|
56
|
+
|
|
57
|
+
answer = $stdin.gets.chomp.strip
|
|
58
|
+
|
|
59
|
+
if answer.empty?
|
|
60
|
+
raise 'No answer and no default given' if default.nil?
|
|
61
|
+
|
|
62
|
+
default
|
|
63
|
+
else
|
|
64
|
+
answer
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def upload(date)
|
|
69
|
+
options = @durations ? '-d' : ''
|
|
70
|
+
cmd = "#{$PROGRAM_NAME} upload #{date} #{options}"
|
|
71
|
+
|
|
72
|
+
if @dry_run
|
|
73
|
+
puts cmd
|
|
74
|
+
else
|
|
75
|
+
system(cmd)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def duration(durations) = Time.at(durations).utc.strftime('%H:%M')
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -4,20 +4,46 @@ module Ptimelog
|
|
|
4
4
|
module Command
|
|
5
5
|
# edit one file. without argument, it will edit the timelog, otherwise a
|
|
6
6
|
# parser-script is loaded
|
|
7
|
-
class Edit <
|
|
8
|
-
def initialize
|
|
9
|
-
super()
|
|
7
|
+
class Edit < CmdParse::Command
|
|
8
|
+
def initialize
|
|
9
|
+
super('edit', takes_commands: false)
|
|
10
10
|
|
|
11
|
+
options.on('--init', 'Add a starting template') do
|
|
12
|
+
@init_content = <<~BASH
|
|
13
|
+
#!/usr/bin/env bash
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
IFS=$'\n\t'
|
|
16
|
+
|
|
17
|
+
echo "0" # this is the ID
|
|
18
|
+
echo "false" # true/false for billable
|
|
19
|
+
echo "true" # true/false for publishable
|
|
20
|
+
BASH
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
@config = Configuration.instance
|
|
11
24
|
@scripts = Script.new(@config[:dir])
|
|
12
|
-
@file = file
|
|
13
25
|
end
|
|
14
26
|
|
|
15
|
-
def
|
|
16
|
-
|
|
27
|
+
def execute(file = nil)
|
|
28
|
+
target = find_file(file)
|
|
29
|
+
|
|
30
|
+
return puts(no_file_hint) if target.is_a?(NullPathname)
|
|
31
|
+
|
|
32
|
+
launch_editor(target)
|
|
17
33
|
end
|
|
18
34
|
|
|
19
35
|
private
|
|
20
36
|
|
|
37
|
+
def no_file_hint
|
|
38
|
+
<<~MSG
|
|
39
|
+
No file to edit.
|
|
40
|
+
|
|
41
|
+
An entry can come from any note of the vault, so the Obsidian-Datasource
|
|
42
|
+
has no single file. Pass the name of an inferer-script instead,
|
|
43
|
+
e.g. 'ptimelog edit hit'.
|
|
44
|
+
MSG
|
|
45
|
+
end
|
|
46
|
+
|
|
21
47
|
def launch_editor(file)
|
|
22
48
|
editor = `which $EDITOR`.chomp
|
|
23
49
|
|
|
@@ -28,6 +54,7 @@ module Ptimelog
|
|
|
28
54
|
%i[
|
|
29
55
|
timelog
|
|
30
56
|
existing_inferer
|
|
57
|
+
prefilled_inferer
|
|
31
58
|
empty_inferer
|
|
32
59
|
].each do |file_lookup|
|
|
33
60
|
valid, filename = send(file_lookup, requested_file)
|
|
@@ -37,7 +64,10 @@ module Ptimelog
|
|
|
37
64
|
end
|
|
38
65
|
|
|
39
66
|
def timelog(file)
|
|
40
|
-
[
|
|
67
|
+
[
|
|
68
|
+
file.nil?,
|
|
69
|
+
Ptimelog::DataSource.new(@config, Ptimelog::NamedDate.new.named_date(:today)).file,
|
|
70
|
+
]
|
|
41
71
|
end
|
|
42
72
|
|
|
43
73
|
def existing_inferer(file)
|
|
@@ -46,6 +76,16 @@ module Ptimelog
|
|
|
46
76
|
[fn.exist?, fn]
|
|
47
77
|
end
|
|
48
78
|
|
|
79
|
+
def prefilled_inferer(file)
|
|
80
|
+
fn = @scripts.inferer(file)
|
|
81
|
+
|
|
82
|
+
return [false, fn] unless fn.empty? && @init_content
|
|
83
|
+
|
|
84
|
+
fn.write(@init_content)
|
|
85
|
+
|
|
86
|
+
[fn.exist?, fn]
|
|
87
|
+
end
|
|
88
|
+
|
|
49
89
|
def empty_inferer(file)
|
|
50
90
|
[true, @scripts.inferer(file)]
|
|
51
91
|
end
|