timet 1.5.9 → 1.6.1
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/.deepsource.toml +2 -0
- data/CHANGELOG.md +31 -0
- data/README.md +29 -1
- data/lib/timet/application.rb +33 -12
- data/lib/timet/application_helper.rb +21 -11
- data/lib/timet/color_codes.rb +9 -0
- data/lib/timet/discord_notifier.rb +100 -0
- data/lib/timet/tag_distribution.rb +66 -0
- data/lib/timet/time_report.rb +22 -8
- data/lib/timet/version.rb +2 -2
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6a2aba5642b2c8c60f5ef5b606723afe9ffe94552490c320ab232e5b0cf23a94
|
4
|
+
data.tar.gz: a8bd210b3ea489a562e567e879c08cf37f113fae09c3126e2ef39840574451d8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: eee39e94ae55bc98dbcb0a48ee61ecd9833a0e6a6c4837994dfcef6609fb2dfa5e0db379551b80c9565c9db9d12ea6915d7d15385d0bff12249609536184ea57
|
7
|
+
data.tar.gz: 1f65a58025fc48be1ead36f74c8a20990015f7e40b3b4ef754bd6ea395ff784d75d4d8be4ced8ab25ebf3349b36f32318b278f5809d9abe33d46037c333f81a9
|
data/.deepsource.toml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,34 @@
|
|
1
|
+
## [1.6.1] - 2025-10-10
|
2
|
+
|
3
|
+
**New Features:**
|
4
|
+
|
5
|
+
- **Search Option for Summary Command:** Introduced a new `--search` option for the `summary` command, allowing users to filter time tracking reports by tags or notes.
|
6
|
+
- **Discord Integration:** Added support for sending notifications to Discord channels upon Pomodoro timer completion.
|
7
|
+
|
8
|
+
**Improvements:**
|
9
|
+
|
10
|
+
- **Dependency Updates:** Updated `aws-partitions`, `aws-sdk-kms`, `aws-sdk-s3`, `bigdecimal`, `httparty`, `icalendar`, `json`, `multi_xml`, `prism`, and `rubocop` to their latest versions for better performance and security.
|
11
|
+
- **README Updates:** Added new examples for the `--search` option and updated the project homepage URL.
|
12
|
+
- **Time Report Filtering:** Enhanced `TimeReport` to incorporate the `--search` option for filtering items by tag or notes.
|
13
|
+
|
14
|
+
|
15
|
+
## [1.6.0] - 2025-09-24
|
16
|
+
|
17
|
+
**New Features:**
|
18
|
+
|
19
|
+
- **Enhanced Summary Report:** Added a new `--report` option to the `summary` command, providing a detailed, color-coded explanation of tag distribution, including percentages, total duration, average duration, and standard deviation.
|
20
|
+
- **Improved Edit Command:** The `edit` command now supports editing the last tracked item by simply running `timet edit` without an ID.
|
21
|
+
- **Resume Pomodoro Option:** The `resume` command now accepts a `--pomodoro=[min]` option to specify a pomodoro duration when resuming a task.
|
22
|
+
- **Bold Text Formatting:** Introduced bold text formatting for improved readability in terminal output.
|
23
|
+
|
24
|
+
**Improvements:**
|
25
|
+
|
26
|
+
- **Robust Session Management:** Refactored `run_linux_session` and `run_mac_session` to use `fork` and `system` calls, enhancing security and robustness by avoiding complex shell escaping.
|
27
|
+
- **Dependency Updates:** Updated `aws-sdk-s3`, `rubocop`, and numerous other gems to their latest versions for better performance and security.
|
28
|
+
- **README Updates:** Added new interactive mode examples for the `edit` command and updated the project homepage URL.
|
29
|
+
- **Timezone Consistency:** Improved time handling in `ValidationEditHelper` specs by consistently using `Time.parse().getlocal()` for better timezone accuracy.
|
30
|
+
|
31
|
+
|
1
32
|
## [1.5.9] - 2025-07-30
|
2
33
|
|
3
34
|
**Improvements:**
|
data/README.md
CHANGED
@@ -35,8 +35,10 @@
|
|
35
35
|
- **Block Time Plot:** Visualizes the distribution of tracked time across a specified range of dates. Each column represents the amount of time tracked during a specific hour, with a header showing the hours and a row for each date displaying the time blocks for each hour.
|
36
36
|
- **Tag Distribution Plot:** Illustrates the proportion of total tracked time allocated to each tag, showing the relative contribution of each tag to the overall time tracked.
|
37
37
|
- **Detailed Statistics:** Displays detailed statistics for each tag, including total duration, average duration, and standard deviation.
|
38
|
+
- **Enhanced Summary Report:** Provides a detailed, color-coded explanation of tag distribution, including percentages, total duration, average duration, and standard deviation, via the `--report` option in the `summary` command.
|
38
39
|
- **iCalendar Export:** Easily export your time tracking data to iCalendar format for integration with calendar applications.
|
39
40
|
- **S3 Cloud Backup:** Seamlessly backup and sync your time tracking data with S3-compatible storage services, providing an additional layer of data protection and accessibility.
|
41
|
+
- **Discord Integration:** Receive notifications in your Discord channels upon Pomodoro timer completion, enhancing real-time productivity tracking.
|
40
42
|
|
41
43
|
## Examples:
|
42
44
|
|
@@ -97,7 +99,7 @@ gem install timet
|
|
97
99
|
|
98
100
|
### Pomodoro Integration
|
99
101
|
|
100
|
-
The `pomodoro` option in the `start` command enhances time tracking by integrating the Pomodoro Technique. Users can specify a Pomodoro session length in minutes, like `pomodoro=25`, to start a 25-minute work interval. The app automatically tracks time and notifies users when the interval ends, helping maintain focus.
|
102
|
+
The `pomodoro` option in the `start` command enhances time tracking by integrating the Pomodoro Technique. Users can specify a Pomodoro session length in minutes, like `pomodoro=25`, to start a 25-minute work interval. The app automatically tracks time and notifies users when the interval ends, helping maintain focus. This notification is now automatically triggered upon the completion of the Pomodoro timer, ensuring timely alerts.
|
101
103
|
|
102
104
|
**Benefits:**
|
103
105
|
|
@@ -105,6 +107,24 @@ gem install timet
|
|
105
107
|
- **Focus:** Encourages disciplined work practices.
|
106
108
|
- **Productivity:** Helps achieve higher productivity and better time management.
|
107
109
|
|
110
|
+
### Discord Integration
|
111
|
+
|
112
|
+
Timet now supports sending notifications to Discord channels upon Pomodoro timer completion. To enable this feature, you need to configure a webhook in your Discord channel and export the webhook URL as an environment variable.
|
113
|
+
|
114
|
+
#### Setup:
|
115
|
+
|
116
|
+
1. **Create a Discord Webhook:** In your Discord server, go to `Server Settings` -> `Integrations` -> `Webhooks` -> `New Webhook`. Copy the generated webhook URL.
|
117
|
+
2. **Export Environment Variable:** Before running `timet`, export the webhook URL as `DISCORD_WEBHOOK_URL`:
|
118
|
+
|
119
|
+
```bash
|
120
|
+
export DISCORD_WEBHOOK_URL="YOUR_DISCORD_WEBHOOK_URL_HERE"
|
121
|
+
```
|
122
|
+
|
123
|
+
Replace `"YOUR_DISCORD_WEBHOOK_URL_HERE"` with the actual webhook URL you copied from Discord.
|
124
|
+
|
125
|
+
Once configured, Timet will automatically send a notification to your Discord channel when a Pomodoro session ends.
|
126
|
+
|
127
|
+
---
|
108
128
|
---
|
109
129
|
|
110
130
|
- **`timet stop`:** Stops tracking the current task, records the elapsed time, and displays the total time spent on all tasks.
|
@@ -150,6 +170,12 @@ gem install timet
|
|
150
170
|
|
151
171
|
**Interactive Mode:**
|
152
172
|
|
173
|
+
```bash
|
174
|
+
timet edit
|
175
|
+
```
|
176
|
+
|
177
|
+
This will edit the last tracked item.
|
178
|
+
|
153
179
|
```bash
|
154
180
|
timet edit 1
|
155
181
|
```
|
@@ -202,6 +228,8 @@ gem install timet
|
|
202
228
|
| `timet summary month (m)` | Display a report of tracked time for the month. | `timet su m` |
|
203
229
|
| `timet su t --csv=[filename]` | Display a report of tracked time for today and export to CSV file | `timet su t --csv=file.csv` |
|
204
230
|
| `timet su w --ics=[filename]` | Display a report of tracked time for week and export to iCalendar file | `timet su w --ics=file.csv` |
|
231
|
+
| `timet su t --report` | Display a detailed report of tag distribution for today. | `timet su t --report` |
|
232
|
+
| `timet summary [time_scope] --search=[query]`| Display a report of tracked time filtered by tag or notes. | `timet su week --search="bug"` |
|
205
233
|
| `timet delete [id]` | Delete a task by its ID. | `timet d [id]` |
|
206
234
|
| `timet cancel` | Cancel active time tracking. | `timet c` |
|
207
235
|
| `timet edit [id]` | Update a task's notes, tag, start, or end fields. | `timet e [id]` |
|
data/lib/timet/application.rb
CHANGED
@@ -9,6 +9,7 @@ require_relative 'application_helper'
|
|
9
9
|
require_relative 'time_helper'
|
10
10
|
require_relative 'version'
|
11
11
|
require_relative 'database_sync_helper'
|
12
|
+
require_relative 'discord_notifier'
|
12
13
|
require 'tempfile'
|
13
14
|
require 'digest'
|
14
15
|
module Timet
|
@@ -89,6 +90,14 @@ module Timet
|
|
89
90
|
end
|
90
91
|
end
|
91
92
|
end
|
93
|
+
|
94
|
+
def resume_complete_task(id)
|
95
|
+
item = id ? @db.find_item(id) : @db.last_item
|
96
|
+
tag = item[3]
|
97
|
+
notes = item[4]
|
98
|
+
pomodoro = options[:pomodoro]
|
99
|
+
start(tag, notes, pomodoro)
|
100
|
+
end
|
92
101
|
end
|
93
102
|
|
94
103
|
desc "start [tag] --notes='' --pomodoro=[min]",
|
@@ -129,6 +138,7 @@ module Timet
|
|
129
138
|
return puts 'A task is currently being tracked.' unless VALID_STATUSES_FOR_INSERTION.include?(@db.item_status)
|
130
139
|
|
131
140
|
@db.insert_item(start_time, tag, notes, pomodoro, start_time, start_time)
|
141
|
+
DiscordNotifier.pomodoro_started(pomodoro) if pomodoro.positive? # Notify that a Pomodoro session has started
|
132
142
|
play_sound_and_notify(pomodoro * 60, tag) if pomodoro.positive?
|
133
143
|
summary
|
134
144
|
end
|
@@ -151,12 +161,13 @@ module Timet
|
|
151
161
|
return unless @db.item_status == :in_progress
|
152
162
|
|
153
163
|
last_id = @db.fetch_last_id
|
164
|
+
@db.find_item(last_id) # Fetch the item to get pomodoro duration
|
154
165
|
@db.update_item(last_id, 'end', TimeHelper.current_timestamp)
|
155
|
-
|
156
166
|
summary
|
157
167
|
end
|
158
168
|
|
159
|
-
desc 'resume (r) [id]', 'Resume last task (id is an optional parameter) => tt resume'
|
169
|
+
desc 'resume (r) [id] --pomodoro=[min]', 'Resume last task (id is an optional parameter) => tt resume'
|
170
|
+
option :pomodoro, type: :numeric, desc: 'Pomodoro time in minutes'
|
160
171
|
# Resumes the last tracking session if it was completed.
|
161
172
|
#
|
162
173
|
# @return [void] This method does not return a value; it performs side effects such as resuming a tracking session
|
@@ -188,15 +199,19 @@ module Timet
|
|
188
199
|
end
|
189
200
|
end
|
190
201
|
|
191
|
-
desc 'summary (su) [time_scope] [tag] --csv=csv_filename --ics=ics_filename',
|
202
|
+
desc 'summary (su) [time_scope] [tag] --csv=csv_filename --ics=ics_filename --report',
|
192
203
|
'Display a summary of tracked time and export to CSV.
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
204
|
+
|
205
|
+
Examples:
|
206
|
+
> tt su yesterday [today (t), yesterday (y), week (w), month (m)]
|
207
|
+
> tt su 2024-10-03..2024-10-20
|
208
|
+
> tt su month --csv=myfile
|
209
|
+
> tt su week --csv=mycalendar
|
210
|
+
> tt su week --search "bug"'
|
198
211
|
option :csv, type: :string, desc: 'Export to CSV'
|
199
212
|
option :ics, type: :string, desc: 'Export to iCalendar'
|
213
|
+
option :report, type: :string, desc: 'Display report'
|
214
|
+
option :search, type: :string, desc: 'Filter by tag or notes'
|
200
215
|
# Generates a summary of tracking items based on the provided time_scope and tag, and optionally exports the summary
|
201
216
|
# to a CSV file and/or an iCalendar file.
|
202
217
|
#
|
@@ -210,12 +225,17 @@ module Timet
|
|
210
225
|
options = build_options(time_scope, tag)
|
211
226
|
report = TimeReport.new(@db, options)
|
212
227
|
display_and_export_report(report, options)
|
228
|
+
return unless options[:report]
|
229
|
+
|
230
|
+
report.print_tag_explanation_report
|
213
231
|
end
|
214
232
|
|
215
233
|
desc 'edit (e) [id] [field] [value]',
|
216
|
-
'Edit task, [field] (notes, tag, start or end) and [value] are optional parameters.
|
217
|
-
|
218
|
-
|
234
|
+
'Edit task, [id] [field] (notes, tag, start or end) and [value] are optional parameters. Examples:
|
235
|
+
|
236
|
+
Update last item => tt edit
|
237
|
+
Update notes => tt edit 12 notes "Update note"
|
238
|
+
Update start time => tt edit 12 start 12:33'
|
219
239
|
# Edits a specific tracking item by its ID, allowing the user to modify fields such as notes, tag, start time, or
|
220
240
|
# end time.
|
221
241
|
#
|
@@ -240,7 +260,8 @@ module Timet
|
|
240
260
|
# a new value.
|
241
261
|
# @note The method then validates and updates the item using `validate_and_update(item, field, new_value)`.
|
242
262
|
# @note Finally, it displays the updated item details using `display_item(updated_item)`.
|
243
|
-
def edit(id, field = nil, new_value = nil)
|
263
|
+
def edit(id = nil, field = nil, new_value = nil)
|
264
|
+
id = @db.fetch_last_id if id.nil?
|
244
265
|
item = @db.find_item(id)
|
245
266
|
return puts "No tracked time found for id: #{id}" unless item
|
246
267
|
|
@@ -111,10 +111,13 @@ module Timet
|
|
111
111
|
# @param tag [String] A tag or label for the session, used in the notification message.
|
112
112
|
# @return [void]
|
113
113
|
def run_linux_session(time, tag)
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
114
|
+
pid = fork do
|
115
|
+
sleep Integer(time)
|
116
|
+
system('tput', 'bel')
|
117
|
+
DiscordNotifier.pomodoro_ended(time / 60) # Notify that a Pomodoro session has ended
|
118
|
+
system('tt', 'stop')
|
119
|
+
system('notify-send', '--icon=clock', show_message(tag))
|
120
|
+
end
|
118
121
|
Process.detach(pid)
|
119
122
|
end
|
120
123
|
|
@@ -124,12 +127,17 @@ module Timet
|
|
124
127
|
# @param _tag [String] A tag or label for the session, not used in the notification message on macOS.
|
125
128
|
# @return [void]
|
126
129
|
def run_mac_session(time, tag)
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
130
|
+
pid = fork do
|
131
|
+
sleep Integer(time)
|
132
|
+
system('afplay', '/System/Library/Sounds/Basso.aiff')
|
133
|
+
DiscordNotifier.pomodoro_ended(time / 60) # Notify that a Pomodoro session has ended
|
134
|
+
system('tt', 'stop')
|
135
|
+
message = show_message(tag)
|
136
|
+
# Escape for AppleScript
|
137
|
+
escaped_message = message.gsub('\\', '\\\\').gsub('"', '\"')
|
138
|
+
applescript_command = "display notification \"#{escaped_message}\""
|
139
|
+
system('osascript', '-e', applescript_command)
|
140
|
+
end
|
133
141
|
Process.detach(pid)
|
134
142
|
end
|
135
143
|
|
@@ -208,7 +216,9 @@ module Timet
|
|
208
216
|
filter: time_scope,
|
209
217
|
tag: tag,
|
210
218
|
csv: csv_filename,
|
211
|
-
ics: ics_filename
|
219
|
+
ics: ics_filename,
|
220
|
+
report: options[:report],
|
221
|
+
search: options[:search]
|
212
222
|
}
|
213
223
|
end
|
214
224
|
|
data/lib/timet/color_codes.rb
CHANGED
@@ -6,6 +6,7 @@ module Timet
|
|
6
6
|
RESET = "\u001b[0m"
|
7
7
|
UNDERLINE = "\e[4m"
|
8
8
|
BLINK = "\e[5m"
|
9
|
+
BOLD = "\e[1m"
|
9
10
|
|
10
11
|
def self.reset
|
11
12
|
RESET
|
@@ -19,6 +20,10 @@ module Timet
|
|
19
20
|
BLINK
|
20
21
|
end
|
21
22
|
|
23
|
+
def self.bold
|
24
|
+
BOLD
|
25
|
+
end
|
26
|
+
|
22
27
|
def self.color(num)
|
23
28
|
"\u001b[38;5;#{num}m"
|
24
29
|
end
|
@@ -51,6 +56,10 @@ class String
|
|
51
56
|
"#{Timet::ColorCodes.blink}#{self}#{Timet::ColorCodes.reset}"
|
52
57
|
end
|
53
58
|
|
59
|
+
def bold
|
60
|
+
"#{Timet::ColorCodes.bold}#{self}#{Timet::ColorCodes.reset}"
|
61
|
+
end
|
62
|
+
|
54
63
|
def green
|
55
64
|
"#{Timet::ColorCodes.color(10)}#{self}#{Timet::ColorCodes.reset}"
|
56
65
|
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'uri'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Timet
|
6
|
+
# Handles sending notifications to Discord via webhooks for Pomodoro events.
|
7
|
+
#
|
8
|
+
# This class provides methods to send structured messages to a Discord channel
|
9
|
+
# when a Pomodoro session starts, ends, or a break ends. It relies on a
|
10
|
+
# Discord webhook URL configured via the `DISCORD_WEBHOOK_URL` environment variable.
|
11
|
+
class DiscordNotifier
|
12
|
+
DISCORD_WEBHOOK_URL = ENV.fetch('DISCORD_WEBHOOK_URL', nil)
|
13
|
+
|
14
|
+
# Sends a notification to the configured Discord webhook.
|
15
|
+
#
|
16
|
+
# @param message_content [String] The main text content of the message.
|
17
|
+
# @param embed_data [Hash, nil] An optional hash representing a Discord embed object.
|
18
|
+
# See Discord API documentation for embed structure.
|
19
|
+
# @return [void]
|
20
|
+
def self.send_notification(message_content, embed_data = nil)
|
21
|
+
return unless DISCORD_WEBHOOK_URL
|
22
|
+
|
23
|
+
uri = URI.parse(DISCORD_WEBHOOK_URL)
|
24
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
25
|
+
http.use_ssl = true
|
26
|
+
|
27
|
+
request = Net::HTTP::Post.new(uri.path, { 'Content-Type' => 'application/json' })
|
28
|
+
|
29
|
+
payload = {
|
30
|
+
content: message_content,
|
31
|
+
username: 'Timet Pomodoro',
|
32
|
+
avatar_url: 'https://gravatar.com/avatar/b4921f111e1d481e3f5f35101432bce5'
|
33
|
+
}
|
34
|
+
payload[:embeds] = [embed_data] if embed_data
|
35
|
+
|
36
|
+
request.body = payload.to_json
|
37
|
+
|
38
|
+
response = http.request(request)
|
39
|
+
unless response.is_a?(Net::HTTPSuccess)
|
40
|
+
puts "Failed to send Discord notification: #{response.code} - #{response.body}"
|
41
|
+
end
|
42
|
+
rescue StandardError => e
|
43
|
+
puts "Error sending Discord notification: #{e.message}"
|
44
|
+
end
|
45
|
+
|
46
|
+
# Sends a notification indicating that a Pomodoro work session has started.
|
47
|
+
#
|
48
|
+
# The notification includes a title, description, color, and fields for duration
|
49
|
+
# and the next scheduled event (short break).
|
50
|
+
#
|
51
|
+
# Sends a notification indicating that a Pomodoro work session has started.
|
52
|
+
#
|
53
|
+
# The notification includes a title, description, color, and fields for duration
|
54
|
+
# and the next scheduled event (short break).
|
55
|
+
#
|
56
|
+
# @param duration [Integer] The duration of the Pomodoro session in minutes.
|
57
|
+
# @return [void]
|
58
|
+
def self.pomodoro_started(duration)
|
59
|
+
embed = {
|
60
|
+
title: 'Pomodoro Session Started! 🍅',
|
61
|
+
description: "Time to focus for #{duration} minutes!",
|
62
|
+
color: 0x00FF00, # Green
|
63
|
+
fields: [
|
64
|
+
{ name: 'Duration', value: "#{duration} minutes", inline: true },
|
65
|
+
{ name: 'Next Up', value: 'Short Break', inline: true }
|
66
|
+
],
|
67
|
+
timestamp: Time.now.utc.iso8601
|
68
|
+
}
|
69
|
+
send_notification('Focus time!', embed)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Sends a notification indicating that a Pomodoro work session has ended.
|
73
|
+
#
|
74
|
+
# The notification includes a title, description, color, and fields for duration
|
75
|
+
# and the next scheduled event (work session).
|
76
|
+
#
|
77
|
+
# Sends a notification indicating that a Pomodoro work session has ended.
|
78
|
+
#
|
79
|
+
# The notification includes a title, description, color, and fields for duration
|
80
|
+
# and the next scheduled event (work session).
|
81
|
+
#
|
82
|
+
# @param duration [Integer] The duration of the Pomodoro session in minutes.
|
83
|
+
# @return [void]
|
84
|
+
def self.pomodoro_ended(duration)
|
85
|
+
break_duration = (duration / 5).to_i # Assuming a 1/5th break duration
|
86
|
+
break_duration = 5 if break_duration == 0 # Minimum 5 minute break
|
87
|
+
embed = {
|
88
|
+
title: 'Pomodoro Session Ended! 🎉',
|
89
|
+
description: "Time for a #{break_duration} minute break!",
|
90
|
+
color: 0xFFA500, # Orange
|
91
|
+
fields: [
|
92
|
+
{ name: 'Duration', value: "#{break_duration} minutes", inline: true },
|
93
|
+
{ name: 'Next Up', value: 'Work Session', inline: true }
|
94
|
+
],
|
95
|
+
timestamp: Time.now.utc.iso8601
|
96
|
+
}
|
97
|
+
send_notification('Break time!', embed)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative 'time_statistics'
|
4
|
+
require_relative 'color_codes' # Ensure color methods are available
|
4
5
|
module Timet
|
5
6
|
# The TagDistribution module provides functionality to format and display the distribution of tags based on their
|
6
7
|
# durations. This is particularly useful for visualizing how time is distributed across different tags in a project
|
@@ -54,6 +55,71 @@ module Timet
|
|
54
55
|
puts 'SD:'.rjust(4).red + 'The standard deviation of the durations'.gray
|
55
56
|
end
|
56
57
|
|
58
|
+
# Generates and prints an explanation of the time report based on tag distribution.
|
59
|
+
#
|
60
|
+
# @param time_stats [Object] An object containing the time statistics.
|
61
|
+
# @param total [Numeric] The total duration of all tags combined in seconds.
|
62
|
+
# @return [void] This method outputs the explanation to the standard output.
|
63
|
+
def print_explanation(time_stats, total)
|
64
|
+
explanations = []
|
65
|
+
high_sd_threshold = 0.5
|
66
|
+
moderate_sd_threshold = 0.2
|
67
|
+
|
68
|
+
# --- Introduction ---
|
69
|
+
total_duration_hours = (total / 3600.0).round(1)
|
70
|
+
explanations << "\n---"
|
71
|
+
explanations << 'Time Report Summary'.bold
|
72
|
+
explanations << "This report provides a detailed breakdown of time spent across various categories, totaling #{"#{total_duration_hours}h".bold} of tracked work.".white
|
73
|
+
explanations << "\n"
|
74
|
+
|
75
|
+
# --- Individual Category Explanations ---
|
76
|
+
explanations << 'Category Breakdown'.bold
|
77
|
+
time_stats.sorted_duration_by_tag.each do |tag, duration|
|
78
|
+
explanation = "#{"#{tag.capitalize}".bold}:"
|
79
|
+
|
80
|
+
# Percentage
|
81
|
+
percentage = (duration.to_f / total * 100).round(1)
|
82
|
+
explanation += " This category consumed #{"#{percentage}%".bold} of the total tracked time."
|
83
|
+
|
84
|
+
# Total Duration
|
85
|
+
total_hours = (duration / 3600.0).round(1)
|
86
|
+
explanation += " The cumulative time spent was #{"#{total_hours}h".bold}, indicating the overall effort dedicated to this area."
|
87
|
+
|
88
|
+
# Average Duration
|
89
|
+
avg_minutes = (time_stats.average_by_tag[tag] / 60.0).round(1)
|
90
|
+
explanation += " On average, each task took #{"#{avg_minutes}min".bold}, which helps in understanding the typical time commitment per task."
|
91
|
+
|
92
|
+
# Standard Deviation
|
93
|
+
sd_minutes = (time_stats.standard_deviation_by_tag[tag] / 60.0).round(1)
|
94
|
+
avg_duration_seconds = time_stats.average_by_tag[tag]
|
95
|
+
|
96
|
+
if sd_minutes > avg_duration_seconds / 60.0 * high_sd_threshold
|
97
|
+
explanation += " A high standard deviation of #{"#{sd_minutes}min".bold} relative to the average suggests significant variability in task durations. This could imply inconsistent task definitions, varying complexity, or frequent interruptions.".red
|
98
|
+
elsif sd_minutes > avg_duration_seconds / 60.0 * moderate_sd_threshold
|
99
|
+
explanation += " A moderate standard deviation of #{"#{sd_minutes}min".bold} indicates some variation in task durations.".blue
|
100
|
+
else
|
101
|
+
explanation += " A low standard deviation of #{"#{sd_minutes}min".bold} suggests that task durations were quite consistent and predictable.".green
|
102
|
+
end
|
103
|
+
|
104
|
+
explanations << explanation.white
|
105
|
+
end
|
106
|
+
|
107
|
+
# --- Overall Summary ---
|
108
|
+
if time_stats.sorted_duration_by_tag.any?
|
109
|
+
sorted_categories = time_stats.sorted_duration_by_tag.map do |tag, duration|
|
110
|
+
[tag, (duration.to_f / total * 100).round(1)]
|
111
|
+
end.sort_by { |_, percentage| -percentage }
|
112
|
+
|
113
|
+
major_categories = sorted_categories.select { |_, percentage| percentage > 10 }
|
114
|
+
if major_categories.size > 1
|
115
|
+
total_percentage = major_categories.sum { |_, percentage| percentage }
|
116
|
+
category_names = major_categories.map { |c, _| "'#{c.capitalize}'" }.join(' and ')
|
117
|
+
explanations << "\nTogether, #{category_names} dominate the time spent, accounting for nearly #{"#{total_percentage.round}%".bold} of the total.".white
|
118
|
+
end
|
119
|
+
end
|
120
|
+
puts explanations.join("\n")
|
121
|
+
end
|
122
|
+
|
57
123
|
# Prints the summary information including total duration, average duration, and standard deviation.
|
58
124
|
#
|
59
125
|
# @param time_stats [Object] An object containing the time statistics, including totals.
|
data/lib/timet/time_report.rb
CHANGED
@@ -60,7 +60,8 @@ module Timet
|
|
60
60
|
@csv_filename = options[:csv]
|
61
61
|
@ics_filename = options[:ics]
|
62
62
|
@filter = formatted_filter(options[:filter])
|
63
|
-
@
|
63
|
+
@search_query = options[:search]
|
64
|
+
@items = options[:filter] ? filter_items(@filter, options[:tag], @search_query) : @db.all_items
|
64
65
|
@table = Table.new(@filter, @items, @db)
|
65
66
|
end
|
66
67
|
|
@@ -94,6 +95,17 @@ module Timet
|
|
94
95
|
tag_distribution(colors)
|
95
96
|
end
|
96
97
|
|
98
|
+
# Prints the tag distribution explanation.
|
99
|
+
# This method is a public wrapper for the private `print_explanation` method
|
100
|
+
# from the `TagDistribution` module.
|
101
|
+
#
|
102
|
+
# @return [void] This method outputs the explanation to the standard output.
|
103
|
+
def print_tag_explanation_report
|
104
|
+
time_stats = TimeStatistics.new(@items)
|
105
|
+
total = time_stats.total_duration
|
106
|
+
print_explanation(time_stats, total) if total.positive?
|
107
|
+
end
|
108
|
+
|
97
109
|
# Displays a single row of the report.
|
98
110
|
#
|
99
111
|
# This method formats and prints a single row of the report, including the table header, the specified row,
|
@@ -165,13 +177,13 @@ module Timet
|
|
165
177
|
# filter_items('2021-10-01..2021-10-31', 'work')
|
166
178
|
#
|
167
179
|
# @note The method filters the items based on the specified date range and tag.
|
168
|
-
def filter_items(filter, tag)
|
180
|
+
def filter_items(filter, tag, search_query)
|
169
181
|
if Timet::Utils.date_ranges.key?(filter)
|
170
182
|
start_date, end_date = Timet::Utils.date_ranges[filter]
|
171
|
-
filter_by_date_range(start_date, end_date, tag)
|
183
|
+
filter_by_date_range(start_date, end_date, tag, search_query)
|
172
184
|
elsif Timet::Utils.valid_date_format?(filter)
|
173
185
|
start_date, end_date = filter.split('..').map { |x| Date.parse(x) }
|
174
|
-
filter_by_date_range(start_date, end_date, tag)
|
186
|
+
filter_by_date_range(start_date, end_date, tag, search_query)
|
175
187
|
else
|
176
188
|
puts 'Invalid filter. Supported filters: today, yesterday, week, month'
|
177
189
|
[]
|
@@ -190,15 +202,17 @@ module Timet
|
|
190
202
|
# filter_by_date_range(Date.new(2021, 10, 1), Date.new(2021, 10, 31), 'work')
|
191
203
|
#
|
192
204
|
# @note The method filters the items based on the specified date range and tag.
|
193
|
-
def filter_by_date_range(start_date, end_date = nil, tag = nil)
|
205
|
+
def filter_by_date_range(start_date, end_date = nil, tag = nil, search_query = nil)
|
194
206
|
start_time = TimeHelper.date_to_timestamp(start_date)
|
195
207
|
end_time = TimeHelper.calculate_end_time(start_date, end_date)
|
196
|
-
|
208
|
+
query_parts = [
|
197
209
|
"start >= #{start_time}",
|
198
210
|
"start < #{end_time}",
|
199
|
-
"tag like '%#{tag}%'",
|
200
211
|
'(deleted IS NULL OR deleted = 0)'
|
201
|
-
]
|
212
|
+
]
|
213
|
+
query_parts << "tag like '%#{tag}%'" if tag
|
214
|
+
query_parts << "(tag LIKE '%#{search_query}%' OR notes LIKE '%#{search_query}%')" if search_query
|
215
|
+
query = query_parts.join(' and ')
|
202
216
|
@db.execute_sql(
|
203
217
|
"select * from items where #{query} ORDER BY id DESC"
|
204
218
|
)
|
data/lib/timet/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: timet
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.6.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Frank Vielma
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-10-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -142,6 +142,7 @@ files:
|
|
142
142
|
- lib/timet/database.rb
|
143
143
|
- lib/timet/database_sync_helper.rb
|
144
144
|
- lib/timet/database_syncer.rb
|
145
|
+
- lib/timet/discord_notifier.rb
|
145
146
|
- lib/timet/item_data_helper.rb
|
146
147
|
- lib/timet/s3_supabase.rb
|
147
148
|
- lib/timet/table.rb
|
@@ -158,11 +159,11 @@ files:
|
|
158
159
|
- lib/timet/version.rb
|
159
160
|
- lib/timet/week_info.rb
|
160
161
|
- sig/timet.rbs
|
161
|
-
homepage: https://
|
162
|
+
homepage: https://timet.frankv.top
|
162
163
|
licenses:
|
163
164
|
- MIT
|
164
165
|
metadata:
|
165
|
-
homepage_uri: https://
|
166
|
+
homepage_uri: https://timet.frankv.top
|
166
167
|
source_code_uri: https://github.com/frankvielma/timet
|
167
168
|
changelog_uri: https://github.com/frankvielma/timet/blob/main/CHANGELOG.md
|
168
169
|
documentation_uri: https://rubydoc.info/gems/timet
|