fewald-worklog 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 +7 -0
- data/.version +1 -0
- data/bin/wl +16 -0
- data/lib/cli.rb +149 -0
- data/lib/configuration.rb +33 -0
- data/lib/daily_log.rb +35 -0
- data/lib/date_parser.rb +69 -0
- data/lib/editor.rb +38 -0
- data/lib/hash.rb +25 -0
- data/lib/log_entry.rb +94 -0
- data/lib/person.rb +26 -0
- data/lib/printer.rb +61 -0
- data/lib/statistics.rb +38 -0
- data/lib/storage.rb +172 -0
- data/lib/string_helper.rb +35 -0
- data/lib/summary.rb +78 -0
- data/lib/templates/favicon.svg.erb +7 -0
- data/lib/templates/index.html.erb +148 -0
- data/lib/version.rb +31 -0
- data/lib/webserver.rb +115 -0
- data/lib/worklog.rb +216 -0
- data/lib/worklogger.rb +26 -0
- metadata +171 -0
data/lib/worklog.rb
ADDED
@@ -0,0 +1,216 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'optparse'
|
5
|
+
require 'rainbow'
|
6
|
+
require 'yaml'
|
7
|
+
|
8
|
+
require 'hash'
|
9
|
+
require 'daily_log'
|
10
|
+
require 'date_parser'
|
11
|
+
require 'log_entry'
|
12
|
+
require 'storage'
|
13
|
+
require 'worklogger'
|
14
|
+
require 'string_helper'
|
15
|
+
require 'printer'
|
16
|
+
require 'statistics'
|
17
|
+
|
18
|
+
# Main class providing all worklog functionality.
|
19
|
+
# This class is the main entry point for the application.
|
20
|
+
# It handles command line arguments, configuration, and logging.
|
21
|
+
class Worklog
|
22
|
+
include StringHelper
|
23
|
+
attr_reader :config
|
24
|
+
|
25
|
+
def initialize(config = nil)
|
26
|
+
@config = config || Configuration.new
|
27
|
+
@storage = Storage.new(@config)
|
28
|
+
|
29
|
+
WorkLogger.level = @config.log_level == :debug ? Logger::Severity::DEBUG : Logger::Severity::INFO
|
30
|
+
end
|
31
|
+
|
32
|
+
def add(message, options = {})
|
33
|
+
# Remove leading and trailing whitespaces
|
34
|
+
# Raise an error if the message is empty
|
35
|
+
message = message.strip
|
36
|
+
raise ArgumentError, 'Message cannot be empty' if message.empty?
|
37
|
+
|
38
|
+
date = Date.strptime(options[:date], '%Y-%m-%d')
|
39
|
+
time = Time.strptime(options[:time], '%H:%M:%S')
|
40
|
+
@storage.create_file_skeleton(date)
|
41
|
+
|
42
|
+
daily_log = @storage.load_log!(@storage.filepath(date))
|
43
|
+
daily_log.entries << LogEntry.new(time:, tags: options[:tags], ticket: options[:ticket], url: options[:url],
|
44
|
+
epic: options[:epic], message:)
|
45
|
+
|
46
|
+
# Sort by time in case an entry was added later out of order.
|
47
|
+
daily_log.entries.sort_by!(&:time)
|
48
|
+
|
49
|
+
@storage.write_log(@storage.filepath(options[:date]), daily_log)
|
50
|
+
|
51
|
+
WorkLogger.info Rainbow("Added entry on #{options[:date]}: #{message}").green
|
52
|
+
end
|
53
|
+
|
54
|
+
def edit(options = {})
|
55
|
+
date = Date.strptime(options[:date], '%Y-%m-%d')
|
56
|
+
|
57
|
+
# Load existing log
|
58
|
+
log = @storage.load_log(@storage.filepath(date))
|
59
|
+
unless log
|
60
|
+
WorkLogger.error "No work log found for #{options[:date]}. Aborting."
|
61
|
+
exit 1
|
62
|
+
end
|
63
|
+
|
64
|
+
txt = Editor::EDITOR_PREAMBLE.result_with_hash(content: YAML.dump(log))
|
65
|
+
return_val = Editor.open_editor(txt)
|
66
|
+
|
67
|
+
@storage.write_log(@storage.filepath(date),
|
68
|
+
YAML.load(return_val, permitted_classes: [Date, Time, DailyLog, LogEntry]))
|
69
|
+
WorkLogger.info Rainbow("Updated work log for #{options[:date]}").green
|
70
|
+
end
|
71
|
+
|
72
|
+
def show(options = {})
|
73
|
+
people = @storage.load_people!
|
74
|
+
printer = Printer.new(people)
|
75
|
+
|
76
|
+
start_date, end_date = start_end_date(options)
|
77
|
+
|
78
|
+
entries = @storage.days_between(start_date, end_date)
|
79
|
+
if entries.empty?
|
80
|
+
printer.no_entries(start_date, end_date)
|
81
|
+
else
|
82
|
+
entries.each do |entry|
|
83
|
+
printer.print_day(entry, entries.size > 1, options[:epics_only])
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def people(person = nil, _options = {})
|
89
|
+
all_people = @storage.load_people!
|
90
|
+
people_map = all_people.to_h { |p| [p.handle, p] }
|
91
|
+
all_logs = @storage.all_days
|
92
|
+
|
93
|
+
if person
|
94
|
+
unless people_map.key?(person)
|
95
|
+
WorkLogger.error Rainbow("No person found with handle #{person}.").red
|
96
|
+
return
|
97
|
+
end
|
98
|
+
person_detail(all_logs, all_people, people_map[person.strip])
|
99
|
+
else
|
100
|
+
puts 'People mentioned in the work log:'
|
101
|
+
|
102
|
+
mentions = {}
|
103
|
+
|
104
|
+
all_logs.map(&:people).each do |people|
|
105
|
+
mentions.merge!(people) { |_key, oldval, newval| oldval + newval }
|
106
|
+
end
|
107
|
+
|
108
|
+
# Sort the mentions by handle
|
109
|
+
mentions = mentions.to_a.sort_by { |handle, _| handle }
|
110
|
+
|
111
|
+
mentions.each do |handle, v|
|
112
|
+
if people_map.key?(handle)
|
113
|
+
print "#{Rainbow(people_map[handle].name).gold} (#{handle})"
|
114
|
+
else
|
115
|
+
print handle
|
116
|
+
end
|
117
|
+
puts ": #{v} #{pluralize(v, 'occurrence')}"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def person_detail(all_logs, all_people, person)
|
123
|
+
printer = Printer.new(all_people)
|
124
|
+
puts "All interactions with #{Rainbow(person.name).gold}"
|
125
|
+
|
126
|
+
if person.notes
|
127
|
+
puts 'Notes:'
|
128
|
+
person.notes.each do |note|
|
129
|
+
puts "* #{note}"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
puts 'Interactions:'
|
134
|
+
all_logs.each do |daily_log|
|
135
|
+
daily_log.entries.each do |entry|
|
136
|
+
printer.print_entry(daily_log, entry, true) if entry.people.include?(person.handle)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def tags(_options = {})
|
142
|
+
all_logs = @storage.all_days
|
143
|
+
|
144
|
+
puts Rainbow('Tags used in the work log:').gold
|
145
|
+
|
146
|
+
# Count all tags used in the work log
|
147
|
+
tags = all_logs.map(&:entries).flatten.map(&:tags).flatten.compact.tally
|
148
|
+
|
149
|
+
# Determine length of longest tag for formatting
|
150
|
+
# Add one additonal space for formatting
|
151
|
+
max_len = tags.empty? ? 0 : tags.keys.map(&:length).max + 1
|
152
|
+
|
153
|
+
tags.sort.each { |k, v| puts "#{Rainbow(k.ljust(max_len)).gold}: #{v} #{pluralize(v, 'occurrence')}" }
|
154
|
+
end
|
155
|
+
|
156
|
+
def stats(_options = {})
|
157
|
+
stats = Statistics.new(@config).calculate
|
158
|
+
puts "#{format_left('Total days')}: #{stats.total_days}"
|
159
|
+
puts "#{format_left('Total entries')}: #{stats.total_entries}"
|
160
|
+
puts "#{format_left('Total epics')}: #{stats.total_epics}"
|
161
|
+
puts "#{format_left('Entries per day')}: #{format('%.2f', stats.avg_entries)}"
|
162
|
+
puts "#{format_left('First entry')}: #{stats.first_entry}"
|
163
|
+
puts "#{format_left('Last entry')}: #{stats.last_entry}"
|
164
|
+
end
|
165
|
+
|
166
|
+
def summary(options = {})
|
167
|
+
start_date, end_date = start_end_date(options)
|
168
|
+
entries = @storage.days_between(start_date, end_date).map(&:entries).flatten
|
169
|
+
|
170
|
+
# Do nothing if no entries are found.
|
171
|
+
if entries.empty?
|
172
|
+
Printer.new.no_entries(start_date, end_date)
|
173
|
+
return
|
174
|
+
end
|
175
|
+
puts Summary.new(@config).generate_summary(entries)
|
176
|
+
end
|
177
|
+
|
178
|
+
def remove(options = {})
|
179
|
+
date = Date.strptime(options[:date], '%Y-%m-%d')
|
180
|
+
unless File.exist?(@storage.filepath(date))
|
181
|
+
WorkLogger.error Rainbow("No work log found for #{options[:date]}. Aborting.").red
|
182
|
+
exit 1
|
183
|
+
end
|
184
|
+
|
185
|
+
daily_log = @storage.load_log!(@storage.filepath(options[:date]))
|
186
|
+
if daily_log.entries.empty?
|
187
|
+
WorkLogger.error Rainbow("No entries found for #{options[:date]}. Aborting.").red
|
188
|
+
exit 1
|
189
|
+
end
|
190
|
+
|
191
|
+
removed_entry = daily_log.entries.pop
|
192
|
+
@storage.write_log(@storage.filepath(date), daily_log)
|
193
|
+
WorkLogger.info Rainbow("Removed entry: #{removed_entry.message}").green
|
194
|
+
end
|
195
|
+
|
196
|
+
# Parse the start and end date based on the options provided
|
197
|
+
#
|
198
|
+
# @param options [Hash] the options hash
|
199
|
+
# @return [Array] the start and end date as an array
|
200
|
+
def start_end_date(options)
|
201
|
+
if options[:days]
|
202
|
+
# Safeguard against negative days
|
203
|
+
raise ArgumentError, 'Number of days cannot be negative' if options[:days].negative?
|
204
|
+
|
205
|
+
start_date = Date.today - options[:days]
|
206
|
+
end_date = Date.today
|
207
|
+
elsif options[:from]
|
208
|
+
start_date = DateParser.parse_date_string!(options[:from], true)
|
209
|
+
end_date = DateParser.parse_date_string!(options[:to], false) if options[:to]
|
210
|
+
else
|
211
|
+
start_date = Date.strptime(options[:date], '%Y-%m-%d')
|
212
|
+
end_date = start_date
|
213
|
+
end
|
214
|
+
[start_date, end_date]
|
215
|
+
end
|
216
|
+
end
|
data/lib/worklogger.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require 'singleton'
|
5
|
+
|
6
|
+
# Singleton class for logging work log messages
|
7
|
+
class WorkLogger
|
8
|
+
include Singleton
|
9
|
+
|
10
|
+
def self.instance
|
11
|
+
@instance ||= Logger.new($stdout)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.level=(level)
|
15
|
+
instance.level = level
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.level
|
19
|
+
instance.level
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.info(message) = instance.info(message)
|
23
|
+
def self.warn(message) = instance.warn(message)
|
24
|
+
def self.error(message) = instance.error(message)
|
25
|
+
def self.debug(message) = instance.debug(message)
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fewald-worklog
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Friedrich Ewald
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: httparty
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: 0.22.0
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: 0.22.0
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: logger
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '1.6'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '1.6'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: puma
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '6.6'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '6.6'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: rack
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '3.1'
|
61
|
+
type: :runtime
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '3.1'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: rackup
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '2.2'
|
75
|
+
type: :runtime
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '2.2'
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: rainbow
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '3.1'
|
89
|
+
type: :runtime
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '3.1'
|
96
|
+
- !ruby/object:Gem::Dependency
|
97
|
+
name: thor
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '1.3'
|
103
|
+
type: :runtime
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '1.3'
|
110
|
+
description: |
|
111
|
+
Command line tool for tracking achievments, tasks and interactions.
|
112
|
+
|
113
|
+
You can add work items, view them and run a webserver to share them with other people,
|
114
|
+
for example via screen sharing.
|
115
|
+
|
116
|
+
This tool is designed to run in a terminal completely local without sharing any data with
|
117
|
+
any other service. No telemetry, no tracking, no data sharing of any kind.
|
118
|
+
executables:
|
119
|
+
- wl
|
120
|
+
extensions: []
|
121
|
+
extra_rdoc_files: []
|
122
|
+
files:
|
123
|
+
- ".version"
|
124
|
+
- bin/wl
|
125
|
+
- lib/cli.rb
|
126
|
+
- lib/configuration.rb
|
127
|
+
- lib/daily_log.rb
|
128
|
+
- lib/date_parser.rb
|
129
|
+
- lib/editor.rb
|
130
|
+
- lib/hash.rb
|
131
|
+
- lib/log_entry.rb
|
132
|
+
- lib/person.rb
|
133
|
+
- lib/printer.rb
|
134
|
+
- lib/statistics.rb
|
135
|
+
- lib/storage.rb
|
136
|
+
- lib/string_helper.rb
|
137
|
+
- lib/summary.rb
|
138
|
+
- lib/templates/favicon.svg.erb
|
139
|
+
- lib/templates/index.html.erb
|
140
|
+
- lib/version.rb
|
141
|
+
- lib/webserver.rb
|
142
|
+
- lib/worklog.rb
|
143
|
+
- lib/worklogger.rb
|
144
|
+
homepage: https://github.com/f-ewald/worklog
|
145
|
+
licenses:
|
146
|
+
- MIT
|
147
|
+
metadata:
|
148
|
+
documentation_uri: https://f-ewald.github.io/worklog
|
149
|
+
rubygems_mfa_required: 'true'
|
150
|
+
post_install_message: 'Thanks for installing worklog! Now you can use it by running
|
151
|
+
wl from your terminal.''
|
152
|
+
|
153
|
+
'
|
154
|
+
rdoc_options: []
|
155
|
+
require_paths:
|
156
|
+
- lib
|
157
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
158
|
+
requirements:
|
159
|
+
- - ">="
|
160
|
+
- !ruby/object:Gem::Version
|
161
|
+
version: 3.4.0
|
162
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
requirements: []
|
168
|
+
rubygems_version: 3.6.7
|
169
|
+
specification_version: 4
|
170
|
+
summary: Command line tool for tracking achievments, tasks and interactions.
|
171
|
+
test_files: []
|