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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: efde9baef775527079705c4a87dfa656a8df4f186381a88c6ebd51e0cfdfd4c8
4
- data.tar.gz: d68a4f17937165e47fe381dedade07ab625ae98c426a30c6cf09d806a2b77466
3
+ metadata.gz: cd30deb9f9246b10264f5e9af3a35c279ffee1c0766947b47d52c4deaf00a608
4
+ data.tar.gz: a544d9b97ecca711bd66dc6ecf9325d3110601b6bbaa2b7a499ffda152be4916
5
5
  SHA512:
6
- metadata.gz: 439357c8e138db02f373cdefedbcd3abd79e16aa5997a8140449327352e0efbdb436b293989b0a19086fcbe41a07c09e7743a3e68b9945cd885080e31b06f056
7
- data.tar.gz: 2ca4a1333cd8f21fc07deb995f2ab31b62ada7a8b95286f050bb3f481adbddf85471df28be2c80b5a838c0a91d0e770dbf144830f1349298157161284d545a6f
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
@@ -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 TimeTracking Website.
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
- - [ ] avoid duplicate entries
28
- - [ ] start/end time as indicator?
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
- - [ ] add cli-help
42
- - [ ] use commander for CLI?
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. Please note that you should not work on that day, unless you bring presents.
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
- If nothing is specified, the action is applied to entries of the last day.
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 be edited.
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
- - rounding: [integer or false, default 15]
106
- - base_url: [url to your puzzletime-installation, default https://time.puzzle.ch]
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
- require_relative '../lib/ptimelog'
4
+ require 'cmdparse'
5
+ require 'ptimelog'
5
6
 
6
- Ptimelog::App.new(ARGV).run
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
@@ -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 < Base
9
- def initialize(task)
10
- super()
8
+ class Add < CmdParse::Command
9
+ def initialize
10
+ super('add', takes_commands: false)
11
11
 
12
- @task = task
13
- @timelog = Ptimelog::Timelog.instance
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 needs_entries?
17
- false
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
- def run
21
- date_time = Time.now.strftime('%F %R')
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
- @timelog.timelog_txt.open('a') do |log|
24
- log << "#{date_time}: #{@task}\n"
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 < Base
8
- def initialize(file)
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 run
16
- launch_editor(find_file(@file))
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
- [file.nil?, Timelog.timelog_txt]
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