ptimelog 0.10.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: 3a7ad17dbf09bc8d7694f9261ffcc8216af2f1ec04be03e62ccabadf9732e8b0
4
- data.tar.gz: 4ee34a775b9300e50971421adedee472fbfa5f5c0ec226b93e299d17474df411
3
+ metadata.gz: cd30deb9f9246b10264f5e9af3a35c279ffee1c0766947b47d52c4deaf00a608
4
+ data.tar.gz: a544d9b97ecca711bd66dc6ecf9325d3110601b6bbaa2b7a499ffda152be4916
5
5
  SHA512:
6
- metadata.gz: 198b9c6aefbab6f64b617a6b2ad125f9e9921c829e1740ca08faa41c69c797fda1cca1181665adec6b68b51560b3a5bed88ad3b23a8bceba6382dd4b222bb129
7
- data.tar.gz: d37ed829daa68c7527caafe929874c6523986609661f9a9155463f0307ac2fb9d14d3237878d6c6659f799ca59c9536e8cae46770f21cdb13916753277beaf2c
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,8 +1,6 @@
1
1
  # pTimeLog
2
2
 
3
- small tooling to transfer timelog-entries from gtimelog's timelog.txt to the PuzzleTime Timetracking Website.
4
-
5
- [![Build Status](https://travis-ci.org/kronn/ptimelog.svg?branch=master)](https://travis-ci.org/kronn/ptimelog)
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.
6
4
 
7
5
  ## Approach
8
6
 
@@ -20,6 +18,7 @@ small tooling to transfer timelog-entries from gtimelog's timelog.txt to the Puz
20
18
  - [x] support user-supplied ticket-parsers
21
19
  - [x] get ticket-parser from tags
22
20
  - [x] merge equal adjacent entries into one
21
+ - [x] caculate, show and upload durations
23
22
  - [ ] complete login/entry automation
24
23
  - [ ] handle authentication
25
24
  - [ ] login and store cookie (https://stackoverflow.com/questions/12399087/curl-to-access-a-page-that-requires-a-login-from-a-different-page#12399176)
@@ -40,8 +39,17 @@ small tooling to transfer timelog-entries from gtimelog's timelog.txt to the Puz
40
39
  - [ ] allow to have a list of "favourite" time-accounts
41
40
  - [ ] select best-matching time-account according to tags, possibly limited to the favourites
42
41
  - [x] combine billable and account-lookup into one script
43
- - [ ] add cli-help
44
- - [ ] 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
45
53
 
46
54
  ## Installation
47
55
 
@@ -53,29 +61,36 @@ Install it with:
53
61
 
54
62
  $ ptimelog ACTION DATE
55
63
 
64
+ Maybe it's best to explore the commands with `ptimelog help` and going from
65
+ there.
66
+
56
67
  ### Actions
57
68
 
58
69
  Currently supported actions are
59
70
 
60
71
  - show
61
72
  - upload
73
+ - confirm
62
74
  - edit
63
75
  - add
64
76
  - version
77
+ - help
78
+ - info
65
79
 
66
80
  ### Date-Identifier
67
81
 
68
82
  To handle a specific date, the format YYYY-MM-DD is expected, e.g. 2017-12-25.
69
- Please note that you should not work on that day, unless you bring presents.
83
+ Please note that you should not work on that day, unless you bring presents or
84
+ release a new version of Ruby.
70
85
 
71
86
  For reusability in a shell-history the following keywords are supported:
72
87
 
73
88
  - today
74
89
  - yesterday
75
- - last
76
90
  - all
77
91
 
78
- 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.
79
94
 
80
95
  ### Edit-Identifier
81
96
 
@@ -83,9 +98,13 @@ When the action is "edit", the next argument is treated as script that should
83
98
  be edited.
84
99
 
85
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.
86
104
 
87
105
  Otherwise, a script to determine the time-account is loaded.
88
106
 
107
+
89
108
  ### Adding entries
90
109
 
91
110
  In order to add entries with the ptimelog-cli, the complete entry needs to be
@@ -104,18 +123,25 @@ meeting').
104
123
 
105
124
  $ ptimelog add '-5 meeting: Discuss requirements -- client planning'
106
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
+
107
129
  ### Showing the Version
108
130
 
109
131
  I got tired of asking rubygems which version I installed, so I took on the
110
- herculean task of letting ptimelog show its own version.
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.
111
135
 
112
136
  ### Formatting the Output
113
137
 
114
138
  In order to format the output of the show-action into a table, a hopefully
115
- convienient field-marker has been chosen. I think it is unlikely, that ∴ is
116
- being used in a time-entry. Therefore, you can pipe the output into `column`:
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`:
117
143
 
118
- ptimelog show today | column -t -s ∴
144
+ ptimelog show today | column -t -s'|'
119
145
 
120
146
  ## Helper-Scripts
121
147
 
@@ -134,6 +160,37 @@ Since these scripts are called a lot, it is better to write them in a compiled
134
160
  language. If you only like ruby, take a look at crystal. For such simple
135
161
  scripts, the code is almost identical and "just" needs to be compiled.
136
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
+
137
194
  ## Configuration
138
195
 
139
196
  A config-file is read from `$HOME/.config/ptimelog/config`. It is expected
@@ -141,6 +198,18 @@ to be a YAML-file. Currently, it supports the following keys:
141
198
 
142
199
  - rounding: [integer or false, default 15]
143
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.
144
213
 
145
214
  ## Development
146
215
 
@@ -159,7 +228,6 @@ git commits and tags, and push the `.gem` file to
159
228
 
160
229
  Bug reports and pull requests are welcome on GitHub at https://github.com/kronn/ptimelog.
161
230
 
162
-
163
231
  ## License
164
232
 
165
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,24 +5,27 @@ 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
14
- @new_lines = []
15
- end
12
+ @data_source = DataSource.new(
13
+ Configuration.instance,
14
+ NamedDate.new.named_date('today')
15
+ )
16
16
 
17
- def needs_entries?
18
- false
17
+ options.on('--debug', 'Show debugging output') { @debug = true }
19
18
  end
20
19
 
21
- def run
22
- add_empty_line if @timelog.previous_entry.date == yesterday
23
- add_entry(*parse_task(@task))
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)
24
27
 
25
- save_file
28
+ backend.add(entry)
26
29
  end
27
30
 
28
31
  private
@@ -36,29 +39,29 @@ module Ptimelog
36
39
  end
37
40
  .localtime
38
41
  .then { |time| time + (matches[:offset].to_i * 60) }
39
- .strftime('%F %R')
42
+ .strftime('%R')
40
43
 
41
44
  [formatted_time, matches[:task]]
42
45
  end
43
46
 
44
- def add_entry(date_time, task)
45
- @new_lines << "#{date_time}: #{task}"
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
54
+ end
46
55
  end
47
56
 
48
- def add_empty_line
49
- @new_lines << ''
57
+ def backend
58
+ @data_source.backend
50
59
  end
51
60
 
52
- def save_file
53
- @timelog.timelog_txt.open('a') do |log|
54
- @new_lines.each do |line|
55
- log << "#{line}\n"
56
- end
57
- end
58
- end
61
+ def debug(msg)
62
+ return unless @debug
59
63
 
60
- def yesterday
61
- NamedDate.new.named_date('yesterday')
64
+ warn msg
62
65
  end
63
66
  end
64
67
  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