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.
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: []