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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e5079b49daa5deed838350e66235ce5301518e928a478f405423f268e431390
4
- data.tar.gz: e7ef91dd990022437605bdfe406387e1499b9d08de5f6c233211332d9cb5ea0d
3
+ metadata.gz: 6a2aba5642b2c8c60f5ef5b606723afe9ffe94552490c320ab232e5b0cf23a94
4
+ data.tar.gz: a8bd210b3ea489a562e567e879c08cf37f113fae09c3126e2ef39840574451d8
5
5
  SHA512:
6
- metadata.gz: a6f4a1db83a82e0a0e53787a1b8847b7ceaa2694e0e4fbe318271bfc00a33c078d525d832f3be0b136d11c9b4a580ff3648ef9a1064e8921be6d7e79cb59c910
7
- data.tar.gz: 7cd3c8513a161e3864501cec96317b833bdda14d937779b97d0feb680a894284f34d7427ba1197ad8ca0a721ede075b13b549489e5beaaa1cbfb751fa71f927f
6
+ metadata.gz: eee39e94ae55bc98dbcb0a48ee61ecd9833a0e6a6c4837994dfcef6609fb2dfa5e0db379551b80c9565c9db9d12ea6915d7d15385d0bff12249609536184ea57
7
+ data.tar.gz: 1f65a58025fc48be1ead36f74c8a20990015f7e40b3b4ef754bd6ea395ff784d75d4d8be4ced8ab25ebf3349b36f32318b278f5809d9abe33d46037c333f81a9
data/.deepsource.toml CHANGED
@@ -11,3 +11,5 @@ enabled = true
11
11
  [coverage]
12
12
  reporter = "simplecov"
13
13
  paths = ["coverage/.resultset.json"]
14
+
15
+ ignore_issues = ["RB-RL1039", "RB-RL1058", "RB-E1003", "RB-RL1054"]
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]` |
@@ -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
- [time_scope] => [today (t), yesterday (y), week (w), month (m). => tt su yesterday
194
- [start_date]..[end_date]] => tt su 2024-10-03..2024-10-20
195
- [tag] => tt su Task1
196
- --csv=csv_filename => tt su month --csv=myfile
197
- --ics=ics_filename => tt su week --csv=mycalendar'
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
- Update notes => tt edit 12 notes "Update note"
218
- Update start time => tt edit 12 start 12:33'
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
- escaped_message = Shellwords.shellescape(show_message(tag))
115
- notification_command = "notify-send --icon=clock #{escaped_message}"
116
- command = "sleep #{time} && tput bel && tt stop 0 && #{notification_command} &"
117
- pid = Kernel.spawn(command)
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
- # Escape double quotes and backslashes for AppleScript, then shell-escape the entire AppleScript command
128
- escaped_message_for_applescript = show_message(tag).gsub('\\', '\\\\').gsub('"', '\"')
129
- escaped_applescript_command = Shellwords.shellescape("display notification \"#{escaped_message_for_applescript}\"")
130
- notification_command = "osascript -e #{escaped_applescript_command}"
131
- command = "sleep #{time} && afplay /System/Library/Sounds/Basso.aiff && tt stop 0 && #{notification_command} &"
132
- pid = Kernel.spawn(command)
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
 
@@ -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.
@@ -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
- @items = options[:filter] ? filter_items(@filter, options[:tag]) : @db.all_items
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
- query = [
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
- ].join(' and ')
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
@@ -6,6 +6,6 @@ module Timet
6
6
  # @return [String] The version number in the format 'major.minor.patch'.
7
7
  #
8
8
  # @example Get the version of the Timet application
9
- # Timet::VERSION # => '1.5.9'
10
- VERSION = '1.5.9'
9
+ # Timet::VERSION # => '1.6.1'
10
+ VERSION = '1.6.1'
11
11
  end
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.5.9
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-07-30 00:00:00.000000000 Z
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://frankvielma.github.io/posts/timet-a-powerful-command-line-tool-for-tracking-your-time/
162
+ homepage: https://timet.frankv.top
162
163
  licenses:
163
164
  - MIT
164
165
  metadata:
165
- homepage_uri: https://frankvielma.github.io/posts/timet-a-powerful-command-line-tool-for-tracking-your-time/
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