ptimelog 0.5.3 → 0.10.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/.envrc +1 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +12 -3
- data/.rubocop_todo.yml +2 -2
- data/.ruby-version +1 -1
- data/.tool-versions +1 -0
- data/LICENSE.txt +1 -1
- data/README.md +54 -17
- data/lib/ptimelog.rb +5 -0
- data/lib/ptimelog/app.rb +7 -44
- data/lib/ptimelog/command/add.rb +65 -0
- data/lib/ptimelog/command/base.rb +7 -3
- data/lib/ptimelog/command/edit.rb +27 -3
- data/lib/ptimelog/command/show.rb +21 -11
- data/lib/ptimelog/command/version.rb +16 -0
- data/lib/ptimelog/configuration.rb +13 -10
- data/lib/ptimelog/day.rb +71 -0
- data/lib/ptimelog/deprecation_warning.rb +52 -0
- data/lib/ptimelog/entry.rb +43 -30
- data/lib/ptimelog/named_date.rb +26 -4
- data/lib/ptimelog/null_pathname.rb +9 -0
- data/lib/ptimelog/script.rb +4 -6
- data/lib/ptimelog/timelog.rb +12 -0
- data/lib/ptimelog/version.rb +16 -2
- data/ptimelog.gemspec +9 -1
- metadata +29 -9
- data/.ruby-gemset +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3a7ad17dbf09bc8d7694f9261ffcc8216af2f1ec04be03e62ccabadf9732e8b0
|
4
|
+
data.tar.gz: 4ee34a775b9300e50971421adedee472fbfa5f5c0ec226b93e299d17474df411
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 198b9c6aefbab6f64b617a6b2ad125f9e9921c829e1740ca08faa41c69c797fda1cca1181665adec6b68b51560b3a5bed88ad3b23a8bceba6382dd4b222bb129
|
7
|
+
data.tar.gz: d37ed829daa68c7527caafe929874c6523986609661f9a9155463f0307ac2fb9d14d3237878d6c6659f799ca59c9536e8cae46770f21cdb13916753277beaf2c
|
data/.envrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
PATH_add exe
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
@@ -1,6 +1,15 @@
|
|
1
1
|
inherit_from: .rubocop_todo.yml
|
2
2
|
|
3
|
-
|
3
|
+
# require:
|
4
|
+
# - rubocop-rake
|
5
|
+
# - rubocop-rspec
|
6
|
+
# - rubocop-packaging
|
7
|
+
# - rubocop-performance
|
8
|
+
|
9
|
+
AllCops:
|
10
|
+
NewCops: enable
|
11
|
+
|
12
|
+
Layout/LineLength:
|
4
13
|
Max: 120
|
5
14
|
|
6
15
|
Metrics/BlockLength:
|
@@ -13,12 +22,12 @@ Style/TrailingCommaInArrayLiteral:
|
|
13
22
|
Style/TrailingCommaInHashLiteral:
|
14
23
|
EnforcedStyleForMultiline: consistent_comma
|
15
24
|
|
16
|
-
Layout/
|
25
|
+
Layout/HashAlignment:
|
17
26
|
EnforcedHashRocketStyle: table
|
18
27
|
EnforcedColonStyle: table
|
19
28
|
EnforcedLastArgumentHashStyle: always_inspect # default
|
20
29
|
|
21
|
-
Naming/
|
30
|
+
Naming/MethodParameterName:
|
22
31
|
AllowedNames:
|
23
32
|
- fn
|
24
33
|
# default allowed names
|
data/.rubocop_todo.yml
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
# This configuration was generated by
|
2
2
|
# `rubocop --auto-gen-config`
|
3
|
-
# on
|
3
|
+
# on 2020-01-22 14:10:50 +0100 using RuboCop version 0.79.0.
|
4
4
|
# The point is for the user to remove these configuration records
|
5
5
|
# one by one as the offenses are removed from the code base.
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
7
7
|
# versions of RuboCop, may require this file to be generated again.
|
8
8
|
|
9
|
-
# Offense count:
|
9
|
+
# Offense count: 4
|
10
10
|
Metrics/AbcSize:
|
11
11
|
Max: 16
|
12
12
|
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.
|
1
|
+
2.7.0
|
data/.tool-versions
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby 2.7.2
|
data/LICENSE.txt
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
The MIT License (MIT)
|
2
2
|
|
3
|
-
Copyright (c) 2017-
|
3
|
+
Copyright (c) 2017-2020 Matthias Viehweger
|
4
4
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
data/README.md
CHANGED
@@ -1,6 +1,8 @@
|
|
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 to the PuzzleTime Timetracking Website.
|
4
|
+
|
5
|
+
[](https://travis-ci.org/kronn/ptimelog)
|
4
6
|
|
5
7
|
## Approach
|
6
8
|
|
@@ -17,17 +19,17 @@ small tooling to transfer timelog-entries from gtimelog's timelog.txt to the Puz
|
|
17
19
|
- [x] infer time-account from ticket-format
|
18
20
|
- [x] support user-supplied ticket-parsers
|
19
21
|
- [x] get ticket-parser from tags
|
20
|
-
- [
|
22
|
+
- [x] merge equal adjacent entries into one
|
21
23
|
- [ ] complete login/entry automation
|
22
24
|
- [ ] handle authentication
|
23
|
-
- [ ] login and store cookie
|
25
|
+
- [ ] login and store cookie (https://stackoverflow.com/questions/12399087/curl-to-access-a-page-that-requires-a-login-from-a-different-page#12399176)
|
24
26
|
- [ ] send user and pwd with every request
|
25
27
|
- [ ] make entries
|
26
28
|
- [ ] open day in browser for review
|
27
|
-
- [
|
28
|
-
- [
|
29
|
+
- [x] avoid duplicate entries
|
30
|
+
- [x] start/end time as indicator?
|
29
31
|
- [x] offer rounding times to the next 5, 10 or 15 minutes
|
30
|
-
- [
|
32
|
+
- [x] allow to add entries from the command-line
|
31
33
|
- [ ] handle time-account and billable better
|
32
34
|
- [ ] import time-accounts from ptime (https://time.puzzle.ch/work_items/search.json?q=search%20terms)
|
33
35
|
- [ ] with a dedicated cli?
|
@@ -37,6 +39,7 @@ small tooling to transfer timelog-entries from gtimelog's timelog.txt to the Puz
|
|
37
39
|
- [ ] from *-notation
|
38
40
|
- [ ] allow to have a list of "favourite" time-accounts
|
39
41
|
- [ ] select best-matching time-account according to tags, possibly limited to the favourites
|
42
|
+
- [x] combine billable and account-lookup into one script
|
40
43
|
- [ ] add cli-help
|
41
44
|
- [ ] use commander for CLI?
|
42
45
|
|
@@ -57,10 +60,13 @@ Currently supported actions are
|
|
57
60
|
- show
|
58
61
|
- upload
|
59
62
|
- edit
|
63
|
+
- add
|
64
|
+
- version
|
60
65
|
|
61
66
|
### Date-Identifier
|
62
67
|
|
63
|
-
To handle a specific date, the format YYYY-MM-DD is expected, e.g. 2017-12-25.
|
68
|
+
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.
|
64
70
|
|
65
71
|
For reusability in a shell-history the following keywords are supported:
|
66
72
|
|
@@ -73,27 +79,58 @@ If nothing is specified, the action is applied to entries of the last day.
|
|
73
79
|
|
74
80
|
### Edit-Identifier
|
75
81
|
|
76
|
-
When the action is "edit", the next argument is treated as script that should
|
82
|
+
When the action is "edit", the next argument is treated as script that should
|
83
|
+
be edited.
|
77
84
|
|
78
85
|
If nothing is passed, the main timelog.txt is loaded.
|
79
86
|
|
80
87
|
Otherwise, a script to determine the time-account is loaded.
|
81
88
|
|
89
|
+
### Adding entries
|
90
|
+
|
91
|
+
In order to add entries with the ptimelog-cli, the complete entry needs to be
|
92
|
+
quoted on the command-line to count as one argument.
|
93
|
+
|
94
|
+
$ ptimelog add 'ticket 1337: Implement requirements -- client coding'
|
95
|
+
|
96
|
+
While this requires some knowledge of the file-format, it is no different than
|
97
|
+
entering the same string in gTimelog. For now, the entry is added to the
|
98
|
+
timelog.txt as it is passed. By default, the date/time added to the entry is
|
99
|
+
the one when the command is executed.
|
100
|
+
|
101
|
+
You can prefix a positive or negative signed number to slightly skew the entry
|
102
|
+
(think: '-5 meeting' or '+5 lunch \*\*') or even set a precise time ('10:30
|
103
|
+
meeting').
|
104
|
+
|
105
|
+
$ ptimelog add '-5 meeting: Discuss requirements -- client planning'
|
106
|
+
|
107
|
+
### Showing the Version
|
108
|
+
|
109
|
+
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.
|
111
|
+
|
112
|
+
### Formatting the Output
|
113
|
+
|
114
|
+
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`:
|
117
|
+
|
118
|
+
ptimelog show today | column -t -s ∴
|
119
|
+
|
82
120
|
## Helper-Scripts
|
83
121
|
|
84
122
|
ptimelog can prefill the account-number and billable-state of an entry.
|
85
123
|
|
86
124
|
The tags are used to determine a script that helps infer the time-account.
|
87
|
-
These scripts should be located in `~/.config/ptimelog/
|
125
|
+
These scripts should be located in `~/.config/ptimelog/inferers/` and be named
|
88
126
|
like the first tag used. The script gets the ticket, the description and all
|
89
|
-
remaining tags passed as arguments.
|
90
|
-
ID of the time-account.
|
127
|
+
remaining tags passed as arguments.
|
91
128
|
|
92
|
-
|
93
|
-
|
94
|
-
|
129
|
+
The output of the script should be the ID of the time-account and the
|
130
|
+
billable-state as "true" or "false". Both items need to be separated by
|
131
|
+
whitespace, so you can output those two on the same line or on different lines.
|
95
132
|
|
96
|
-
Since these
|
133
|
+
Since these scripts are called a lot, it is better to write them in a compiled
|
97
134
|
language. If you only like ruby, take a look at crystal. For such simple
|
98
135
|
scripts, the code is almost identical and "just" needs to be compiled.
|
99
136
|
|
@@ -102,8 +139,8 @@ scripts, the code is almost identical and "just" needs to be compiled.
|
|
102
139
|
A config-file is read from `$HOME/.config/ptimelog/config`. It is expected
|
103
140
|
to be a YAML-file. Currently, it supports the following keys:
|
104
141
|
|
105
|
-
|
106
|
-
|
142
|
+
- rounding: [integer or false, default 15]
|
143
|
+
- base_url: [url to your puzzletime-installation, default https://time.puzzle.ch]
|
107
144
|
|
108
145
|
## Development
|
109
146
|
|
data/lib/ptimelog.rb
CHANGED
@@ -6,17 +6,22 @@ $LOAD_PATH.unshift File.dirname(__FILE__)
|
|
6
6
|
module Ptimelog
|
7
7
|
autoload :App, 'ptimelog/app'
|
8
8
|
autoload :Configuration, 'ptimelog/configuration'
|
9
|
+
autoload :Day, 'ptimelog/day'
|
10
|
+
autoload :DeprecationWarning, 'ptimelog/deprecation_warning'
|
9
11
|
autoload :Entry, 'ptimelog/entry'
|
10
12
|
autoload :NamedDate, 'ptimelog/named_date'
|
13
|
+
autoload :NullPathname, 'ptimelog/null_pathname'
|
11
14
|
autoload :Script, 'ptimelog/script'
|
12
15
|
autoload :Timelog, 'ptimelog/timelog'
|
13
16
|
autoload :VERSION, 'ptimelog/version'
|
14
17
|
|
15
18
|
# Collection of commands available at the CLI
|
16
19
|
module Command
|
20
|
+
autoload :Add, 'ptimelog/command/add'
|
17
21
|
autoload :Base, 'ptimelog/command/base'
|
18
22
|
autoload :Edit, 'ptimelog/command/edit'
|
19
23
|
autoload :Show, 'ptimelog/command/show'
|
20
24
|
autoload :Upload, 'ptimelog/command/upload'
|
25
|
+
autoload :Version, 'ptimelog/command/version'
|
21
26
|
end
|
22
27
|
end
|
data/lib/ptimelog/app.rb
CHANGED
@@ -1,58 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Ptimelog
|
4
|
-
# Wrapper for everything
|
4
|
+
# Wrapper for everything, dispatching to a command
|
5
5
|
class App
|
6
6
|
def initialize(args)
|
7
7
|
@config = Configuration.instance
|
8
|
-
command = (args[0] ||
|
8
|
+
command = (args[0] || 'show')
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
@date = NamedDate.new.date(args[1])
|
16
|
-
Command::Upload.new
|
17
|
-
when :edit
|
18
|
-
file = args[1]
|
19
|
-
Command::Edit.new(file)
|
20
|
-
else
|
21
|
-
raise ArgumentError, "Unsupported Command #{@command}"
|
22
|
-
end
|
10
|
+
constant_name = command.to_s[0].upcase + command[1..].downcase
|
11
|
+
command_class = Command.const_get(constant_name.to_sym)
|
12
|
+
raise ArgumentError, "Unsupported Command '#{command}'" if command_class.nil?
|
13
|
+
|
14
|
+
@command = command_class.new(args[1]) # e.g. Ptimelog::Command::Show.new('today')
|
23
15
|
end
|
24
16
|
|
25
17
|
def run
|
26
|
-
@command.entries = entries if @command.needs_entries?
|
27
|
-
|
28
18
|
@command.run
|
29
19
|
end
|
30
|
-
|
31
|
-
private
|
32
|
-
|
33
|
-
def timelog
|
34
|
-
Timelog.load
|
35
|
-
end
|
36
|
-
|
37
|
-
def entries
|
38
|
-
timelog.each_with_object({}) do |(date, lines), entries|
|
39
|
-
next unless date # guard against the machine
|
40
|
-
next unless @date == :all || @date == date # limit to one day if passed
|
41
|
-
|
42
|
-
entries[date] = []
|
43
|
-
start = nil # at the start of the day, we have no previous end
|
44
|
-
|
45
|
-
lines.each do |line|
|
46
|
-
entry = Entry.from_timelog(line)
|
47
|
-
entry.start_time = start
|
48
|
-
|
49
|
-
entries[date] << entry if entry.valid?
|
50
|
-
|
51
|
-
start = entry.finish_time # store previous ending for nice display of next entry
|
52
|
-
end
|
53
|
-
|
54
|
-
entries
|
55
|
-
end
|
56
|
-
end
|
57
20
|
end
|
58
21
|
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
module Ptimelog
|
6
|
+
module Command
|
7
|
+
# add a new entrie with the current date and time
|
8
|
+
class Add < Base
|
9
|
+
def initialize(task)
|
10
|
+
super()
|
11
|
+
|
12
|
+
@task = task
|
13
|
+
@timelog = Ptimelog::Timelog.instance
|
14
|
+
@new_lines = []
|
15
|
+
end
|
16
|
+
|
17
|
+
def needs_entries?
|
18
|
+
false
|
19
|
+
end
|
20
|
+
|
21
|
+
def run
|
22
|
+
add_empty_line if @timelog.previous_entry.date == yesterday
|
23
|
+
add_entry(*parse_task(@task))
|
24
|
+
|
25
|
+
save_file
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def parse_task(line)
|
31
|
+
matches = line.match('(?<time>\d{1,2}:\d{2} )?(?<offset>[+-]\d+ )?(?<task>.*)')
|
32
|
+
formatted_time = if matches[:time]
|
33
|
+
Time.parse(matches[:time])
|
34
|
+
else
|
35
|
+
Time.now
|
36
|
+
end
|
37
|
+
.localtime
|
38
|
+
.then { |time| time + (matches[:offset].to_i * 60) }
|
39
|
+
.strftime('%F %R')
|
40
|
+
|
41
|
+
[formatted_time, matches[:task]]
|
42
|
+
end
|
43
|
+
|
44
|
+
def add_entry(date_time, task)
|
45
|
+
@new_lines << "#{date_time}: #{task}"
|
46
|
+
end
|
47
|
+
|
48
|
+
def add_empty_line
|
49
|
+
@new_lines << ''
|
50
|
+
end
|
51
|
+
|
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
|
59
|
+
|
60
|
+
def yesterday
|
61
|
+
NamedDate.new.named_date('yesterday')
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -4,9 +4,13 @@ module Ptimelog
|
|
4
4
|
module Command
|
5
5
|
# Foundation and common API for all commands
|
6
6
|
class Base
|
7
|
-
def initialize
|
8
|
-
@config
|
9
|
-
|
7
|
+
def initialize(day = nil)
|
8
|
+
@config = Configuration.instance
|
9
|
+
|
10
|
+
return unless needs_entries?
|
11
|
+
|
12
|
+
@entries = {}
|
13
|
+
self.entries = Ptimelog::Day.new(day).entries
|
10
14
|
end
|
11
15
|
|
12
16
|
def needs_entries?
|
@@ -13,7 +13,7 @@ module Ptimelog
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def run
|
16
|
-
launch_editor(@file)
|
16
|
+
launch_editor(find_file(@file))
|
17
17
|
end
|
18
18
|
|
19
19
|
private
|
@@ -21,10 +21,34 @@ module Ptimelog
|
|
21
21
|
def launch_editor(file)
|
22
22
|
editor = `which $EDITOR`.chomp
|
23
23
|
|
24
|
-
file = file.nil? ? Timelog.timelog_txt : @scripts.parser(@file)
|
25
|
-
|
26
24
|
exec "#{editor} #{file}"
|
27
25
|
end
|
26
|
+
|
27
|
+
def find_file(requested_file)
|
28
|
+
%i[
|
29
|
+
timelog
|
30
|
+
existing_inferer
|
31
|
+
empty_inferer
|
32
|
+
].each do |file_lookup|
|
33
|
+
valid, filename = send(file_lookup, requested_file)
|
34
|
+
|
35
|
+
return filename if valid
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def timelog(file)
|
40
|
+
[file.nil?, Timelog.timelog_txt]
|
41
|
+
end
|
42
|
+
|
43
|
+
def existing_inferer(file)
|
44
|
+
fn = @scripts.inferer(file)
|
45
|
+
|
46
|
+
[fn.exist?, fn]
|
47
|
+
end
|
48
|
+
|
49
|
+
def empty_inferer(file)
|
50
|
+
[true, @scripts.inferer(file)]
|
51
|
+
end
|
28
52
|
end
|
29
53
|
end
|
30
54
|
end
|
@@ -4,17 +4,30 @@ module Ptimelog
|
|
4
4
|
module Command
|
5
5
|
# show entries of one day or all of them
|
6
6
|
class Show < Base
|
7
|
+
def initialize(*args)
|
8
|
+
@durations = Hash.new(0)
|
9
|
+
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
7
13
|
def needs_entries?
|
8
14
|
true
|
9
15
|
end
|
10
16
|
|
11
17
|
def run
|
12
18
|
@entries.each do |date, list|
|
13
|
-
puts date,
|
19
|
+
puts date,
|
20
|
+
'----------'
|
21
|
+
|
22
|
+
next if list.empty?
|
23
|
+
|
14
24
|
list.each do |entry|
|
15
25
|
puts entry
|
16
26
|
end
|
17
|
-
puts
|
27
|
+
puts '----------',
|
28
|
+
"Total work done: #{duration(date)} hours",
|
29
|
+
'----------------------------',
|
30
|
+
nil
|
18
31
|
end
|
19
32
|
end
|
20
33
|
|
@@ -23,18 +36,15 @@ module Ptimelog
|
|
23
36
|
@entries[date] = []
|
24
37
|
|
25
38
|
list.each do |entry|
|
26
|
-
@
|
27
|
-
|
28
|
-
[
|
29
|
-
entry.ticket,
|
30
|
-
entry.description,
|
31
|
-
entry.tags,
|
32
|
-
entry.account,
|
33
|
-
].compact.join(' ∴ '),
|
34
|
-
].compact.join(' ')
|
39
|
+
@durations[date] += entry.duration
|
40
|
+
@entries[date] << entry.to_s
|
35
41
|
end
|
36
42
|
end
|
37
43
|
end
|
44
|
+
|
45
|
+
def duration(date)
|
46
|
+
Time.at(@durations[date]).utc.strftime('%H:%M')
|
47
|
+
end
|
38
48
|
end
|
39
49
|
end
|
40
50
|
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'singleton'
|
4
4
|
require 'pathname'
|
5
|
+
require 'yaml'
|
5
6
|
|
6
7
|
module Ptimelog
|
7
8
|
# Wrapper around configuration-options and -loading
|
@@ -9,10 +10,10 @@ module Ptimelog
|
|
9
10
|
include Singleton
|
10
11
|
|
11
12
|
CONFIGURATION_DEFAULTS = {
|
12
|
-
base_url
|
13
|
-
rounding
|
14
|
-
dir
|
15
|
-
timelog
|
13
|
+
'base_url' => 'https://time.puzzle.ch',
|
14
|
+
'rounding' => 15,
|
15
|
+
'dir' => '~/.config/ptimelog',
|
16
|
+
'timelog' => '~/.local/share/gtimelog/timelog.txt',
|
16
17
|
}.freeze
|
17
18
|
|
18
19
|
def initialize
|
@@ -21,10 +22,12 @@ module Ptimelog
|
|
21
22
|
|
22
23
|
def reset
|
23
24
|
@config = load_config(
|
24
|
-
Pathname.new(CONFIGURATION_DEFAULTS[
|
25
|
+
Pathname.new(CONFIGURATION_DEFAULTS['dir'])
|
26
|
+
.expand_path
|
27
|
+
.join('config')
|
25
28
|
)
|
26
|
-
wrap_with_pathname(
|
27
|
-
wrap_with_pathname(
|
29
|
+
wrap_with_pathname('dir')
|
30
|
+
wrap_with_pathname('timelog')
|
28
31
|
end
|
29
32
|
|
30
33
|
def load_config(fn)
|
@@ -34,13 +37,13 @@ module Ptimelog
|
|
34
37
|
end
|
35
38
|
|
36
39
|
def [](key)
|
37
|
-
@config[key.
|
40
|
+
@config[key.to_s]
|
38
41
|
end
|
39
42
|
|
40
43
|
def []=(key, value)
|
41
|
-
@config[key.
|
44
|
+
@config[key.to_s] = value
|
42
45
|
|
43
|
-
wrap_with_pathname(key.
|
46
|
+
wrap_with_pathname(key.to_s) if %w[dir timelog].include?(key.to_s)
|
44
47
|
end
|
45
48
|
|
46
49
|
private
|
data/lib/ptimelog/day.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ptimelog
|
4
|
+
# Wrap loading of all entries of a day
|
5
|
+
class Day
|
6
|
+
def initialize(date)
|
7
|
+
@date = NamedDate.new.date(date)
|
8
|
+
end
|
9
|
+
|
10
|
+
def entries
|
11
|
+
timelog.each_with_object({}) do |(date, lines), entries|
|
12
|
+
next unless date # guard against the machine
|
13
|
+
next unless @date.to_s == 'all' || @date == date # limit to one day if passed
|
14
|
+
|
15
|
+
entries[date] = join_similar(entries_of_day(lines)) # lines |> entries_of_day |> join_similar
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def timelog
|
22
|
+
Timelog.load
|
23
|
+
end
|
24
|
+
|
25
|
+
def entries_of_day(lines)
|
26
|
+
entries = []
|
27
|
+
start = nil # at the start of the day, we have no previous end
|
28
|
+
|
29
|
+
lines.each do |line|
|
30
|
+
entry = Entry.from_timelog(line)
|
31
|
+
entry.start_time = start
|
32
|
+
|
33
|
+
entries << entry if entry.valid?
|
34
|
+
|
35
|
+
start = entry.finish_time # store previous ending for nice display of next entry
|
36
|
+
end
|
37
|
+
|
38
|
+
entries
|
39
|
+
end
|
40
|
+
|
41
|
+
def join_similar(list)
|
42
|
+
return [] if list.empty?
|
43
|
+
return list if list.one?
|
44
|
+
|
45
|
+
one, *tail = list
|
46
|
+
two, *rest = tail
|
47
|
+
|
48
|
+
joined = maybe_join(one, two)
|
49
|
+
|
50
|
+
if joined.one?
|
51
|
+
join_similar(joined + rest)
|
52
|
+
else
|
53
|
+
[one] + join_similar(tail)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def maybe_join(one, two)
|
58
|
+
if one.ticket == two.ticket &&
|
59
|
+
one.description == two.description &&
|
60
|
+
one.finish_time == two.start_time
|
61
|
+
|
62
|
+
joined = one.dup
|
63
|
+
joined.finish_time = two.finish_time
|
64
|
+
|
65
|
+
[joined]
|
66
|
+
else
|
67
|
+
[one, two]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ptimelog
|
4
|
+
# Allow to add some (hopefully) helpful deprecation warning
|
5
|
+
module DeprecationWarning
|
6
|
+
def self.included(base)
|
7
|
+
base.send :extend, ClassMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
# Keep track of wether the deprecation have been shown already or not
|
11
|
+
module ClassMethods
|
12
|
+
def deprecation_warning_rendered?
|
13
|
+
@deprecation_warning_rendered == true
|
14
|
+
end
|
15
|
+
|
16
|
+
def reset_deprecation_warning!
|
17
|
+
@deprecation_warning_rendered = false
|
18
|
+
end
|
19
|
+
|
20
|
+
def deprecation_warning_rendered!
|
21
|
+
@deprecation_warning_rendered = true
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def deprecate(*args)
|
26
|
+
warn deprecate_header(*args)
|
27
|
+
|
28
|
+
return if self.class.deprecation_warning_rendered?
|
29
|
+
|
30
|
+
warn deprecate_message(*args)
|
31
|
+
self.class.deprecation_warning_rendered!
|
32
|
+
end
|
33
|
+
|
34
|
+
def deprecate_header(_)
|
35
|
+
raise <<~MESSAGE
|
36
|
+
deprecate_header(args) not implemented
|
37
|
+
|
38
|
+
Please define a header/short-info for the deprecation, rendered every
|
39
|
+
time the deprecation is hit.
|
40
|
+
MESSAGE
|
41
|
+
end
|
42
|
+
|
43
|
+
def deprecate_message(_)
|
44
|
+
raise <<~MESSAGE
|
45
|
+
deprecate_message(args) not implemented
|
46
|
+
|
47
|
+
Please define a message () for the deprecation, rendered only the first
|
48
|
+
time the deprecation is hit.
|
49
|
+
MESSAGE
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/lib/ptimelog/entry.rb
CHANGED
@@ -1,14 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'time'
|
4
|
+
|
3
5
|
module Ptimelog
|
4
6
|
# Dataclass to wrap an entry
|
5
7
|
class Entry
|
6
|
-
# allow to read everything
|
7
|
-
attr_reader :date, :start_time, :finish_time, :ticket, :description,
|
8
|
-
:tags, :billable, :account
|
9
|
-
|
10
8
|
# define only trivial writers, omit special and derived values
|
11
|
-
|
9
|
+
attr_accessor :date, :ticket, :description
|
10
|
+
|
11
|
+
# allow to read everything else
|
12
|
+
attr_reader :start_time, :finish_time, :tags, :billable, :account
|
13
|
+
|
14
|
+
BILLABLE = 1
|
15
|
+
NON_BILLABLE = 0
|
12
16
|
|
13
17
|
def initialize(config = Configuration.instance)
|
14
18
|
@config = config
|
@@ -49,32 +53,43 @@ module Ptimelog
|
|
49
53
|
end
|
50
54
|
|
51
55
|
def valid?
|
52
|
-
@start_time && !hidden?
|
56
|
+
@start_time && duration.positive? && !hidden?
|
53
57
|
end
|
54
58
|
|
55
59
|
def hidden?
|
56
|
-
@description
|
60
|
+
@description.to_s.end_with?('**') # hide lunch and breaks
|
61
|
+
end
|
62
|
+
|
63
|
+
def billable?
|
64
|
+
@billable == BILLABLE
|
57
65
|
end
|
58
66
|
|
59
67
|
def infer_ptime_settings
|
60
|
-
|
61
|
-
|
68
|
+
return if hidden?
|
69
|
+
return unless @script.inferer(script_name).exist?
|
70
|
+
|
71
|
+
@account, @billable = infer_account_and_billable
|
72
|
+
end
|
73
|
+
|
74
|
+
def duration
|
75
|
+
(Time.parse(@finish_time) - Time.parse(@start_time)).to_i
|
62
76
|
end
|
63
77
|
|
64
78
|
def to_s
|
79
|
+
billable = billable? ? '($)' : nil
|
80
|
+
tag_list = Array(@tags).compact
|
81
|
+
|
82
|
+
tags = tag_list.join(' ') if tag_list.any?
|
83
|
+
desc = [@ticket, @description].compact.join(': ')
|
84
|
+
acc = [@account, billable].compact.join(' ') if @account
|
85
|
+
|
65
86
|
[
|
66
|
-
@start_time, '-', @finish_time,
|
67
|
-
[
|
68
|
-
@ticket,
|
69
|
-
@description,
|
70
|
-
@tags,
|
71
|
-
@account,
|
72
|
-
].compact.join(' : '),
|
87
|
+
@start_time, '-', @finish_time, '∴',
|
88
|
+
[desc, tags, acc].compact.join(' ∴ '),
|
73
89
|
].compact.join(' ')
|
74
90
|
end
|
75
91
|
|
76
92
|
# make sortable/def <=>
|
77
|
-
# duration if start and finish is set
|
78
93
|
|
79
94
|
private
|
80
95
|
|
@@ -92,24 +107,22 @@ module Ptimelog
|
|
92
107
|
end.map { |part| part.to_s.rjust(2, '0') }.join(':')
|
93
108
|
end
|
94
109
|
|
95
|
-
def
|
96
|
-
|
97
|
-
|
98
|
-
parser_name = @tags.first
|
99
|
-
parser = @script.parser(parser_name)
|
100
|
-
|
101
|
-
return unless parser.exist?
|
110
|
+
def script_name
|
111
|
+
@script_name ||= @tags.to_a.first.to_s
|
112
|
+
end
|
102
113
|
|
103
|
-
|
104
|
-
|
114
|
+
def script_args
|
115
|
+
@script_args ||= @tags.to_a[1..].to_a.map(&:inspect).join(' ')
|
105
116
|
end
|
106
117
|
|
107
|
-
def
|
108
|
-
script = @script.
|
118
|
+
def infer_account_and_billable
|
119
|
+
script = @script.inferer(script_name)
|
120
|
+
|
121
|
+
cmd = %(#{script} "#{@ticket}" "#{@description}" #{script_args})
|
109
122
|
|
110
|
-
|
123
|
+
account, billable = `#{cmd}`.chomp.split
|
111
124
|
|
112
|
-
|
125
|
+
[account, (billable == 'true' ? BILLABLE : NON_BILLABLE)]
|
113
126
|
end
|
114
127
|
end
|
115
128
|
end
|
data/lib/ptimelog/named_date.rb
CHANGED
@@ -9,17 +9,39 @@ module Ptimelog
|
|
9
9
|
named_date(arg) || :all
|
10
10
|
end
|
11
11
|
|
12
|
-
def named_date(date)
|
12
|
+
def named_date(date) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/AbcSize
|
13
13
|
case date.to_s
|
14
|
-
when 'yesterday' then
|
14
|
+
when 'yesterday' then yesterday
|
15
15
|
when 'today' then Date.today.to_s
|
16
|
-
when 'last', '' then
|
17
|
-
when
|
16
|
+
when 'last', '' then last_entry.to_s || yesterday
|
17
|
+
when 'mon', 'monday' then previous_weekday('monday')
|
18
|
+
when 'tue', 'tuesday' then previous_weekday('tuesday')
|
19
|
+
when 'wed', 'wednesday' then previous_weekday('wednesday')
|
20
|
+
when 'thu', 'thursday' then previous_weekday('thursday')
|
21
|
+
when 'fri', 'friday' then previous_weekday('friday')
|
22
|
+
when 'sat', 'saturday' then previous_weekday('saturday')
|
23
|
+
when 'sun', 'sunday' then previous_weekday('sunday')
|
24
|
+
when /\d{4}(-\d{2}){2}/ then date.to_s
|
18
25
|
end
|
19
26
|
end
|
20
27
|
|
21
28
|
private
|
22
29
|
|
30
|
+
def previous_weekday(date)
|
31
|
+
Date.today.prev_day(7)
|
32
|
+
.step(Date.today.prev_day)
|
33
|
+
.find { |d| d.send(:"#{date}?") }
|
34
|
+
.to_s
|
35
|
+
end
|
36
|
+
|
37
|
+
def last_entry
|
38
|
+
timelog.to_h.keys.compact.sort[-2]
|
39
|
+
end
|
40
|
+
|
41
|
+
def yesterday
|
42
|
+
Date.today.prev_day.to_s
|
43
|
+
end
|
44
|
+
|
23
45
|
def timelog
|
24
46
|
Timelog.load
|
25
47
|
end
|
data/lib/ptimelog/script.rb
CHANGED
@@ -8,13 +8,11 @@ module Ptimelog
|
|
8
8
|
@config_dir = config_dir
|
9
9
|
end
|
10
10
|
|
11
|
-
def
|
12
|
-
|
13
|
-
|
14
|
-
end
|
11
|
+
def inferer(name)
|
12
|
+
return NullPathname.new if name.to_s.empty?
|
13
|
+
raise if name =~ %r{[\\/]} # prevent relavtive paths, stupidly, FIXME: really check FS
|
15
14
|
|
16
|
-
|
17
|
-
@config_dir.join('billable').expand_path
|
15
|
+
@config_dir.join('inferers').join(name).expand_path
|
18
16
|
end
|
19
17
|
end
|
20
18
|
end
|
data/lib/ptimelog/timelog.rb
CHANGED
@@ -15,6 +15,14 @@ module Ptimelog
|
|
15
15
|
def timelog_txt
|
16
16
|
Pathname.new(Configuration.instance[:timelog]).expand_path
|
17
17
|
end
|
18
|
+
|
19
|
+
def previous_entry
|
20
|
+
lines = timelog_txt.readlines.last(2)
|
21
|
+
last_line = lines.map(&:chomp).delete_if(&:empty?).last
|
22
|
+
last_entry = instance.tokenize(last_line)
|
23
|
+
|
24
|
+
Entry.from_timelog(last_entry)
|
25
|
+
end
|
18
26
|
end
|
19
27
|
|
20
28
|
def load
|
@@ -25,6 +33,10 @@ module Ptimelog
|
|
25
33
|
self.class.timelog_txt
|
26
34
|
end
|
27
35
|
|
36
|
+
def previous_entry
|
37
|
+
self.class.previous_entry
|
38
|
+
end
|
39
|
+
|
28
40
|
def read
|
29
41
|
timelog_txt.read
|
30
42
|
end
|
data/lib/ptimelog/version.rb
CHANGED
@@ -1,6 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
#
|
3
|
+
# the version, following semver
|
4
|
+
#
|
5
|
+
# Someone wanted to have documentation, so here goes...
|
6
|
+
#
|
7
|
+
# Please note that the version-string is not frozen. Although it of course is,
|
8
|
+
# because all strings in this file are frozen. That's what the magic comment
|
9
|
+
# at the top of the file does. :gasp:
|
10
|
+
#
|
11
|
+
# What else? Yeah, the VERSION-constant is part of the module Ptimelog because
|
12
|
+
# that is what is described here.
|
13
|
+
#
|
14
|
+
# So, I truly hope you are happy now, that I documented this file properly. For
|
15
|
+
# any remaining questions, please open an issue or even better a pull-request
|
16
|
+
# with an improvement. Keep in mind that this is also covered by rspec so I
|
17
|
+
# expect (pun intended) 100% test-coverage for any additional code.
|
4
18
|
module Ptimelog
|
5
|
-
VERSION = '0.
|
19
|
+
VERSION = '0.10.0'
|
6
20
|
end
|
data/ptimelog.gemspec
CHANGED
@@ -22,11 +22,19 @@ Gem::Specification.new do |spec|
|
|
22
22
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
23
23
|
spec.require_paths = ['lib']
|
24
24
|
|
25
|
+
spec.required_ruby_version = '>= 2.7.0'
|
26
|
+
|
27
|
+
spec.add_dependency 'naught'
|
28
|
+
|
25
29
|
spec.add_development_dependency 'bundler'
|
26
30
|
spec.add_development_dependency 'overcommit', '~> 0.45'
|
27
31
|
spec.add_development_dependency 'pry', '~> 0.12'
|
28
|
-
spec.add_development_dependency 'rake', '
|
32
|
+
spec.add_development_dependency 'rake', '>= 12.3.3'
|
29
33
|
spec.add_development_dependency 'rspec', '~> 3.0'
|
30
34
|
spec.add_development_dependency 'rubocop', '~> 0.50'
|
35
|
+
# spec.add_development_dependency 'rubocop-rake'
|
36
|
+
# spec.add_development_dependency 'rubocop-rspec'
|
37
|
+
# spec.add_development_dependency 'rubocop-packaging'
|
38
|
+
# spec.add_development_dependency 'rubocop-performance'
|
31
39
|
spec.add_development_dependency 'timecop', '~> 0.9'
|
32
40
|
end
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ptimelog
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matthias Viehweger
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-01-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: naught
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: bundler
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -56,16 +70,16 @@ dependencies:
|
|
56
70
|
name: rake
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
58
72
|
requirements:
|
59
|
-
- - "
|
73
|
+
- - ">="
|
60
74
|
- !ruby/object:Gem::Version
|
61
|
-
version:
|
75
|
+
version: 12.3.3
|
62
76
|
type: :development
|
63
77
|
prerelease: false
|
64
78
|
version_requirements: !ruby/object:Gem::Requirement
|
65
79
|
requirements:
|
66
|
-
- - "
|
80
|
+
- - ">="
|
67
81
|
- !ruby/object:Gem::Version
|
68
|
-
version:
|
82
|
+
version: 12.3.3
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
84
|
name: rspec
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -116,13 +130,14 @@ executables:
|
|
116
130
|
extensions: []
|
117
131
|
extra_rdoc_files: []
|
118
132
|
files:
|
133
|
+
- ".envrc"
|
119
134
|
- ".gitignore"
|
120
135
|
- ".overcommit.yml"
|
121
136
|
- ".rspec"
|
122
137
|
- ".rubocop.yml"
|
123
138
|
- ".rubocop_todo.yml"
|
124
|
-
- ".ruby-gemset"
|
125
139
|
- ".ruby-version"
|
140
|
+
- ".tool-versions"
|
126
141
|
- ".travis.yml"
|
127
142
|
- Gemfile
|
128
143
|
- LICENSE.txt
|
@@ -133,13 +148,18 @@ files:
|
|
133
148
|
- exe/ptimelog
|
134
149
|
- lib/ptimelog.rb
|
135
150
|
- lib/ptimelog/app.rb
|
151
|
+
- lib/ptimelog/command/add.rb
|
136
152
|
- lib/ptimelog/command/base.rb
|
137
153
|
- lib/ptimelog/command/edit.rb
|
138
154
|
- lib/ptimelog/command/show.rb
|
139
155
|
- lib/ptimelog/command/upload.rb
|
156
|
+
- lib/ptimelog/command/version.rb
|
140
157
|
- lib/ptimelog/configuration.rb
|
158
|
+
- lib/ptimelog/day.rb
|
159
|
+
- lib/ptimelog/deprecation_warning.rb
|
141
160
|
- lib/ptimelog/entry.rb
|
142
161
|
- lib/ptimelog/named_date.rb
|
162
|
+
- lib/ptimelog/null_pathname.rb
|
143
163
|
- lib/ptimelog/script.rb
|
144
164
|
- lib/ptimelog/timelog.rb
|
145
165
|
- lib/ptimelog/version.rb
|
@@ -156,14 +176,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
156
176
|
requirements:
|
157
177
|
- - ">="
|
158
178
|
- !ruby/object:Gem::Version
|
159
|
-
version:
|
179
|
+
version: 2.7.0
|
160
180
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
161
181
|
requirements:
|
162
182
|
- - ">="
|
163
183
|
- !ruby/object:Gem::Version
|
164
184
|
version: '0'
|
165
185
|
requirements: []
|
166
|
-
rubygems_version: 3.
|
186
|
+
rubygems_version: 3.1.4
|
167
187
|
signing_key:
|
168
188
|
specification_version: 4
|
169
189
|
summary: Move time-entries from gTimelog to PuzzleTime
|
data/.ruby-gemset
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
gpuzzletime
|