Skip to main content

kasl/libs/messages/
display.rs

1//! Display implementation for kasl application messages.
2//!
3//! Provides the core `Display` trait implementation for the `Message` enum, enabling automatic conversion of structured message data into human-readable text.
4//!
5//! ## Usage
6//!
7//! ```rust
8//! use kasl::libs::messages::types::Message;
9//!
10//! let message = Message::TaskCreated;
11//! println!("{}", message);
12//!
13//! let parameterized = Message::MonitorStarted {
14//!     pause_threshold: 60,
15//!     poll_interval: 500,
16//!     activity_threshold: 30,
17//! };
18//! println!("{}", parameterized);
19//! ```
20
21use super::types::Message;
22use std::fmt::{Display, Formatter, Result};
23
24impl Display for Message {
25    /// Converts a `Message` enum variant into human-readable text.
26    ///
27    /// # Examples
28    ///
29    /// ```rust
30    /// use kasl::libs::messages::Message;
31    ///
32    /// // Automatic formatting through Display trait
33    /// let message = Message::TaskCreated;
34    /// println!("{}", message); // "Task created successfully"
35    ///
36    /// // With parameters
37    /// let date_message = Message::WorkdayStarting("2025-01-15".to_string());
38    /// println!("{}", date_message); // "Starting workday for 2025-01-15"
39    /// ```
40    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
41        let text = match self {
42            // === AUTOSTART MESSAGES ===
43            Message::AutostartEnabled => "Autostart has been enabled. Kasl will start automatically on system boot.".to_string(),
44            Message::AutostartEnabledUser => "Autostart has been enabled for current user. Kasl will start when you log in.".to_string(),
45            Message::AutostartDisabled => "Autostart has been disabled.".to_string(),
46            Message::AutostartAlreadyDisabled => "Autostart was already disabled.".to_string(),
47            Message::AutostartEnableFailed(error) => format!("Failed to enable autostart: {}", error),
48            Message::AutostartDisableFailed(error) => format!("Failed to disable autostart: {}", error),
49            Message::AutostartStatus(status) => format!("Autostart is currently: {}", status),
50            Message::AutostartRequiresAdmin => "Administrator privileges required for system-level autostart. Trying user-level alternative...".to_string(),
51            Message::AutostartCheckingAlternative => "Checking alternative autostart method...".to_string(),
52
53            // === TASK MESSAGES ===
54            Message::TaskCreated => "Task created successfully".to_string(),
55            Message::TaskUpdated => "Task updated successfully".to_string(),
56            Message::TaskNotFound => "Task not found".to_string(),
57            Message::TaskUpdateFailed => "Failed to update task".to_string(),
58            Message::TasksDeletedCount(count) => format!("Deleted {} task(s) successfully.", count),
59            Message::TasksNotFoundForDate(date) => format!("Tasks not found for {}, report not sent.", date),
60            Message::TasksNotFoundSad => "No tasks found.".to_string(),
61            Message::TasksHeader => "Tasks:".to_string(),
62            Message::TasksDiscoverySummary { incomplete, jira, gitlab } => {
63                let mut parts = Vec::new();
64                if *incomplete > 0 {
65                    parts.push(format!("{} incomplete", incomplete));
66                }
67                if *jira > 0 {
68                    parts.push(format!("{} jira", jira));
69                }
70                if *gitlab > 0 {
71                    parts.push(format!("{} gitlab", gitlab));
72                }
73                format!("Found: {}", parts.join(", "))
74            }
75            Message::TasksDiscoverySeparator => "──────── other ────────".to_string(),
76            Message::TasksDiscoverySearchingIncomplete => "Looking for incomplete tasks...".to_string(),
77            Message::TasksDiscoveryFetchingExternal => "Fetching GitLab commits and Jira issues...".to_string(),
78            Message::NoTaskIdsProvided => "No task IDs provided for deletion.".to_string(),
79            Message::TasksNotFoundForIds(ids) => format!("No tasks found with IDs: {:?}", ids),
80            Message::TasksToBeDeleted => "The following tasks will be deleted:".to_string(),
81            Message::ConfirmDeleteTask => "Are you sure you want to delete this task?".to_string(),
82            Message::ConfirmDeleteTasks(count) => format!("Are you sure you want to delete {} tasks?", count),
83            Message::ConfirmDeleteAllTodayTasks(count) => format!("Are you sure you want to delete ALL {} tasks for today?", count),
84            Message::ConfirmDeleteAllTodayTasksFinal => "This action cannot be undone. Are you REALLY sure?".to_string(),
85            Message::NoTasksForToday => "No tasks found for today.".to_string(),
86            Message::TaskNotFoundWithId(id) => format!("Task with ID {} not found.", id),
87            Message::CurrentTaskState => "Current task:".to_string(),
88            Message::TaskEditPreview => "Task after changes:".to_string(),
89            Message::ConfirmTaskUpdate => "Save changes?".to_string(),
90            Message::NoChangesDetected => "No changes detected.".to_string(),
91            Message::NoTasksSelected => "No tasks selected for editing.".to_string(),
92            Message::SelectTasksToEdit => "Select tasks to edit (space to select, enter to confirm)".to_string(),
93            Message::EditingTask(name) => format!("Editing task: {}", name),
94            Message::TaskUpdatedWithName(name) => format!("Task '{}' updated successfully.", name),
95            Message::TaskSkippedNoChanges(name) => format!("Task '{}' - no changes, skipped.", name),
96            Message::TaskEditingCompleted => "Task editing completed.".to_string(),
97            Message::PromptTaskNameEdit => "Task name".to_string(),
98            Message::PromptTaskCommentEdit => "Comment (optional)".to_string(),
99            Message::PromptTaskCompletenessEdit => "Completeness (0-100)".to_string(),
100            Message::TaskCompletenessRange => "Completeness must be between 0 and 100".to_string(),
101
102            // === WORKDAY MESSAGES ===
103            Message::WorkdayEnded => "Workday ended for today.".to_string(),
104            Message::WorkdayNeverStarted(date) => {
105                format!(
106                    "No workday was started on {}, so there is nothing to end. `kasl watch` opens the day, and `kasl report` shows what is recorded.",
107                    date
108                )
109            }
110            Message::WorkdayNotFoundForDate(date) => format!("No workday record found for {}", date),
111            Message::WorkdayCreateFailed => "Failed to create workday".to_string(),
112            Message::WorkdayStarting(date) => format!("Starting workday for {}", date),
113            Message::WorkdayCouldNotFindAfterFinalizing(date) => {
114                format!("Could not find workday for {} after finalizing.", date)
115            }
116
117            // === CONFIGURATION MESSAGES ===
118            Message::ConfigSaved => "Configuration saved successfully".to_string(),
119            Message::ConfigDeleted => "Configuration deleted successfully".to_string(),
120            Message::ConfigParseError => "Failed to parse configuration".to_string(),
121            Message::ConfigSaveError => "Failed to save configuration".to_string(),
122            Message::ConfigModuleGitLab => "GitLab settings".to_string(),
123            Message::ConfigModuleJira => "Jira settings".to_string(),
124            Message::ConfigModuleSiServer => "SiServer settings".to_string(),
125            Message::ConfigModuleMonitor => "Monitor settings".to_string(),
126            Message::ConfigModuleServer => "Server settings".to_string(),
127            Message::ConfigModuleProductivity => "Productivity settings".to_string(),
128            Message::ConfigModuleTaskDiscovery => "Task discovery settings".to_string(),
129            Message::ConfigModuleJiraInbox => "Jira inbox settings".to_string(),
130            Message::TaskDiscoveryIgnoreListHeader => "Current ignore list:".to_string(),
131            Message::TaskDiscoveryIgnoreListEmpty => "Ignore list is empty.".to_string(),
132            Message::TaskDiscoveryIgnoreNameAdded(name) => format!("Added '{}' to ignore list.", name),
133            Message::TaskDiscoveryIgnoreNameExists(name) => format!("'{}' is already in the ignore list.", name),
134            Message::TaskDiscoveryIgnoreNamesAdded(count) => {
135                format!("Added {} name(s) to the discovery ignore list.", count)
136            }
137
138            // === REPORT MESSAGES ===
139            Message::DailyReportSent(date) => {
140                format!(
141                    "Your report dated {} has been successfully submitted\nWait for a message to your email address",
142                    date
143                )
144            }
145            Message::MonthlyReportSent(date) => {
146                format!(
147                    "Your monthly report dated {} has been successfully submitted\nWait for a message to your email address",
148                    date
149                )
150            }
151            Message::MonthlyReportTriggered => "It's the last working day of the month. Submitting the monthly report as well...".to_string(),
152            Message::ReportSendFailed(status) => format!("Failed to send report. Status: {}", status),
153            Message::MonthlyReportSendFailed(status) => format!("Failed to send monthly report. Status: {}", status),
154            Message::ReportHeader(date) => format!("Report for {}", date),
155            Message::WorkingHoursForMonth(month_year) => format!("Working hours for {}", month_year),
156            Message::ReportPayloadHeading { url, date } => {
157                format!("This is what {} would send to {}, as a multipart form:", date, url)
158            }
159
160            // === EXPORT MESSAGES ===
161            Message::ExportingData(data, format) => format!("Exporting {} in {} format...", data, format),
162            Message::ExportCompleted(path) => format!("Export completed successfully: {}", path),
163            Message::ExportingAllData => "Exporting all data...".to_string(),
164
165            // === TEMPLATE MESSAGES ===
166            Message::TemplateCreated(name) => format!("Template '{}' created successfully.", name),
167            Message::TemplateUpdated(name) => format!("Template '{}' updated successfully.", name),
168            Message::TemplateDeleted(name) => format!("Template '{}' deleted successfully.", name),
169            Message::TemplateNotFound(name) => format!("Template '{}' not found.", name),
170            Message::TemplateAlreadyExists(name) => format!("Template '{}' already exists.", name),
171            Message::TemplateCreateFailed => "Failed to create template.".to_string(),
172            Message::NoTemplatesFound => "No templates found.".to_string(),
173            Message::TemplateListHeader => "Task Templates:".to_string(),
174            Message::SelectTemplateToEdit => "Select template to edit".to_string(),
175            Message::SelectTemplateToDelete => "Select template to delete".to_string(),
176            Message::ConfirmDeleteTemplate(name) => format!("Delete template '{}'?", name),
177            Message::EditingTemplate(name) => format!("Editing template: {}", name),
178            Message::NoTemplatesMatchingQuery(query) => format!("No templates matching '{}'", query),
179            Message::TemplateSearchResults(query) => format!("Templates matching '{}':", query),
180            Message::SelectTemplateAction => "What would you like to do?".to_string(),
181            Message::PromptTemplateName => "Template name (unique identifier)".to_string(),
182            Message::PromptTemplateTaskName => "Task name".to_string(),
183            Message::PromptTemplateComment => "Comment (optional)".to_string(),
184            Message::PromptTemplateCompleteness => "Default completeness (0-100)".to_string(),
185            Message::CreatingTaskFromTemplate(name) => format!("Creating task from template '{}'", name),
186            Message::SelectTemplate => "Select a template".to_string(),
187            Message::CreateTemplateFirst => "Create templates with 'kasl template add'".to_string(),
188
189            // === TAG MESSAGES ===
190            Message::TagCreated(name) => format!("Tag '{}' created successfully.", name),
191            Message::TagUpdated(name) => format!("Tag '{}' updated successfully.", name),
192            Message::TagDeleted(name) => format!("Tag '{}' deleted successfully.", name),
193            Message::TagNotFound(name) => format!("Tag '{}' not found.", name),
194            Message::TagAlreadyExists(name) => format!("Tag '{}' already exists.", name),
195            Message::NoTagsFound => "No tags found.".to_string(),
196            Message::TagListHeader => "Tags:".to_string(),
197            Message::EditingTag(name) => format!("Editing tag: {}", name),
198            Message::SelectTagAction => "What would you like to do?".to_string(),
199            Message::SelectTagToEdit => "Select tag to edit".to_string(),
200            Message::SelectTagToDelete => "Select tag to delete".to_string(),
201            Message::ConfirmDeleteTag(name) => format!("Delete tag '{}'?", name),
202            Message::ConfirmDeleteTagWithTasks(name, count) => format!("Tag '{}' is used by {} task(s). Delete anyway?", name, count),
203            Message::PromptTagName => "Tag name".to_string(),
204            Message::PromptTagColor => "Tag color (e.g., blue, green, red)".to_string(),
205            Message::NoTasksWithTag(tag) => format!("No tasks found with tag '{}'.", tag),
206            Message::TasksWithTag(tag) => format!("Tasks with tag '{}':", tag),
207            Message::TagsAddedToTask(tags) => format!("Tags added: {}", tags),
208
209            // === SHORT INTERVALS MESSAGES ===
210            Message::ShortIntervalsDetected(count, duration) => format!("Found {} short work intervals (total: {})", count, duration),
211            Message::PromptMinWorkInterval => "Minimum work interval (minutes)".to_string(),
212
213            // === TIME ADJUSTMENT MESSAGES ===
214            Message::WorkdayUpdateFailed => "Failed to update workday.".to_string(),
215
216            // === PAUSE MESSAGES ===
217            Message::PausesTitle(date) => format!("Pauses for {}", date),
218
219            // === MONITOR MESSAGES ===
220            Message::MonitorStarted {
221                pause_threshold,
222                poll_interval,
223                activity_threshold,
224            } => {
225                format!(
226                    "Monitor is running with pause threshold {}s, poll interval {}ms, activity threshold {}s",
227                    pause_threshold, poll_interval, activity_threshold
228                )
229            }
230            Message::MonitorExitedNormally => "Monitor exited normally".to_string(),
231            Message::MonitorShuttingDown => "Shutting down monitor...".to_string(),
232            Message::MonitorError(error) => format!("Monitor error: {}", error),
233            Message::MonitorTaskPanicked(error) => format!("Monitor task panicked: {}", error),
234            Message::PauseStarted => "Pause Start".to_string(),
235            Message::PauseEnded => "Pause End".to_string(),
236
237            // === WATCHER/DAEMON MESSAGES ===
238            Message::WatcherStarted(pid) => format!("Watcher started in the background (PID: {}).", pid),
239            Message::WatcherStopped(pid) => format!("Watcher process (PID: {}) stopped successfully.", pid),
240            Message::WatcherNotRunning => "Watcher is not running.".to_string(),
241            Message::WatcherNotRunningPidNotFound => "Watcher does not appear to be running (PID file not found).".to_string(),
242            Message::WatcherStartingForeground => "Starting watcher in foreground... Press Ctrl+C to exit.".to_string(),
243            Message::WatcherAlreadyRunningPid(pid) => format!("A watcher is already running (PID: {}) - left it as it is.", pid),
244            Message::WatcherAlreadyRunning => "A watcher is already running for this user - stop it with `kasl watch --stop`, then run this again.".to_string(),
245            Message::WatcherStoppingExisting(pid) => format!("Stopping existing watcher (PID: {})...", pid),
246            Message::WatcherFailedToStopExisting(error) => format!("Warning: Failed to stop existing daemon: {}", error),
247            Message::WatcherReceivedSigterm => "Received SIGTERM, shutting down gracefully...".to_string(),
248            Message::WatcherReceivedSigint => "Received SIGINT, shutting down gracefully...".to_string(),
249            Message::WatcherReceivedCtrlC => "Received Ctrl+C, shutting down gracefully...".to_string(),
250            Message::WatcherCtrlCListenFailed(error) => format!("Failed to listen for Ctrl+C: {}", error),
251            Message::WatcherSignalHandlingNotSupported => "Warning: Signal handling not supported on this platform".to_string(),
252            Message::DaemonModeNotSupported => "Daemon mode is not supported on this platform.".to_string(),
253            Message::FailedToGetCurrentExecutable => "Failed to get the path of the current executable".to_string(),
254            Message::FailedToCreateSigtermHandler => "Failed to create SIGTERM handler".to_string(),
255            Message::FailedToCreateSigintHandler => "Failed to create SIGINT handler".to_string(),
256
257            // === UPDATE MESSAGES ===
258            Message::UpdateAvailable { app_name, latest } => {
259                format!(
260                    "A new version of {} is available: v{}\nUpgrade now by running: {} self-update",
261                    app_name, latest, app_name
262                )
263            }
264            Message::UpdateCompleted { app_name, version } => {
265                format!("The {} application has been successfully updated to version {}!", app_name, version)
266            }
267            Message::NoUpdateRequired => "No update required. You are using the latest version!".to_string(),
268            Message::UpdateDownloadUrlNotSet => "Download URL not set".to_string(),
269            Message::UpdateLatestTagNotFound(url) => format!("Could not resolve the latest release tag from {}", url),
270            Message::WatcherStoppingForUpdate => "Stopping watcher for update...".to_string(),
271            Message::WatcherRestartingAfterUpdate => "Restarting watcher after update...".to_string(),
272            Message::WatcherStoppingForConfig => "Stopping watcher to apply new configuration...".to_string(),
273            Message::WatcherRestartingAfterConfig => "Restarting watcher with updated configuration...".to_string(),
274            Message::WatcherRestarted => "Watcher successfully restarted with new configuration".to_string(),
275            Message::WatcherRestartFailed { error } => format!("Warning: Failed to restart watcher: {}", error),
276            Message::UpdateBinaryNotFoundInArchive => "Binary not found in the release archive.".to_string(),
277
278            // === AUTHENTICATION MESSAGES ===
279            Message::WrongPassword(count) => format!("You entered the wrong password {} times!", count),
280
281            // === API MESSAGES ===
282            Message::GitlabFetchFailed(error) => format!("[kasl] Failed to get GitLab events: {}", error),
283            Message::GitlabUserIdFailed(error) => format!("[kasl] Failed to get GitLab user ID: {}", error),
284            Message::JiraFetchFailed(error) => format!("[kasl] Failed to get Jira issues: {}", error),
285            Message::SiServerConfigNotFound => "SiServer configuration not found in config file.".to_string(),
286            Message::SiServerSessionFailed(error) => format!("[kasl] Failed to get SiServer session for rest dates: {}", error),
287            Message::SiServerRestDatesFailed(error) => format!("[kasl] Failed to request rest dates: {}", error),
288            Message::SiServerRestDatesParsingFailed(error) => format!("[kasl] Failed to parse rest dates response: {}", error),
289
290            // === KASL-SERVER MESSAGES ===
291            Message::KaslServerReached { url, version } => format!("{} is kasl-server {}", url, version),
292            Message::KaslServerConnected { user_name, agent_name } => {
293                format!("Connected as {} (agent '{}')", user_name, agent_name)
294            }
295            Message::KaslServerConfigured(url) => format!("Configured server: {}", url),
296            Message::KaslServerDatabaseUnhealthy(state) => {
297                format!(
298                    "The server answered, but reports its database as '{}' - uploads will fail until that clears.",
299                    state
300                )
301            }
302            Message::KaslServerUnreachable(error) => format!("Cannot reach the server: {}", error),
303            Message::KaslServerTokenRejected(error) => format!("The stored token no longer works: {}", error),
304            Message::KaslServerUrlNeedsScheme(url) => {
305                format!(
306                    "'{}' has no scheme - write http:// or https:// so it is clear whether the token crosses the network in the clear.",
307                    url
308                )
309            }
310            Message::KaslServerTokenEmpty => "No token entered; nothing was changed.".to_string(),
311            Message::KaslServerTokenMissing => "A server is configured but no token is stored - run `kasl server connect` again.".to_string(),
312            Message::KaslServerTokenNotRemoved(error) => {
313                format!("The address was forgotten, but the stored token could not be removed: {}", error)
314            }
315            Message::KaslServerNotConnected => "This machine is not connected to a kasl-server.".to_string(),
316            Message::KaslServerDisconnected => "Disconnected; the stored token has been removed.".to_string(),
317            Message::KaslServerNoDayToPush(date) => format!("No workday recorded for {} - nothing to send.", date),
318            Message::KaslServerDayPushed { date, pauses, tasks } => {
319                format!("Sent {} to the server: {} pauses, {} tasks", date, pauses, tasks)
320            }
321            Message::KaslServerTasksDeleted(count) => {
322                format!("{} task(s) removed on the server - deleted here since the last upload", count)
323            }
324            Message::KaslServerPushRejected(error) => {
325                format!(
326                    "{}
327The server will not accept this day as sent; fix it here and push again.",
328                    error
329                )
330            }
331            Message::KaslServerPushTokenRejected(error) => {
332                format!(
333                    "{}
334Run `kasl server connect` to connect again with a token your administrator issues.",
335                    error
336                )
337            }
338            Message::KaslServerDayQueued(date) => {
339                format!("{} is queued and will be sent with the next successful push.", date)
340            }
341            Message::KaslServerQueueEmpty => "Nothing is waiting to be sent.".to_string(),
342            Message::KaslServerQueueOwed(count) => match count {
343                1 => "1 day is waiting to be sent:".to_string(),
344                _ => format!("{} days are waiting to be sent:", count),
345            },
346            Message::KaslServerQueueEntry { date, attempts, last_error } => {
347                // The attempt count and the last reason are the answer to
348                // "why is this one still here", which is otherwise invisible.
349                let tried = match attempts {
350                    1 => "1 attempt".to_string(),
351                    _ => format!("{} attempts", attempts),
352                };
353                match last_error {
354                    Some(error) => format!("  {} - {}, last: {}", date, tried, error),
355                    None => format!("  {} - {}", date, tried),
356                }
357            }
358            Message::KaslServerQueueSending(count) => match count {
359                1 => "Sending 1 day...".to_string(),
360                _ => format!("Sending {} days...", count),
361            },
362            Message::KaslServerDayRefused { date, reason } => {
363                format!("{} was refused and has been dropped from the queue: {}", date, reason)
364            }
365            Message::KaslServerDayDeferred { date, reason } => {
366                format!("{} is still waiting: {}", date, reason)
367            }
368            Message::KaslServerFlushSummary { accepted, refused, deferred } => {
369                format!("{} sent, {} refused, {} still waiting", accepted, refused, deferred)
370            }
371            Message::KaslServerBackfillRange { from, to, days } => match days {
372                1 => format!("1 recorded day between {} and {}", from, to),
373                _ => format!("{} recorded days between {} and {}", days, from, to),
374            },
375            Message::KaslServerBackfillNoDays { from, to } => {
376                format!("No workdays recorded between {} and {} - nothing to send.", from, to)
377            }
378            Message::KaslServerBackfillOrderReversed => "The start of the range is after its end; swap --from and --to.".to_string(),
379            Message::KaslServerBackfillWholeHistory { from, to, days } => match days {
380                1 => format!("1 recorded day, the whole history from {} to {}", from, to),
381                _ => format!("{} recorded days, the whole history from {} to {}", days, from, to),
382            },
383            Message::KaslServerBackfillNothingRecorded => "No workday has ever been recorded on this machine - nothing to send.".to_string(),
384            Message::KaslServerCompatibility { server_version, api_version } => {
385                format!("server {} · api {} · ok", server_version, api_version)
386            }
387            Message::KaslServerPrivacyHeading(level) => format!("Privacy level on this server: {}", level),
388            Message::KaslServerPrivacySummary(summary) => summary.clone(),
389            Message::KaslServerPrivacyStoredHeading => "What it stores:".to_string(),
390            Message::KaslServerPrivacyStored { what, detail } => format!("  {} - {}", what, detail),
391            Message::KaslServerPrivacyNeverHeading => "What it never collects:".to_string(),
392            Message::KaslServerPrivacyBullet(line) => format!("  {}", line),
393            Message::KaslServerPrivacyVisibleHeading => "Who can see it:".to_string(),
394            Message::KaslServerPrivacyRetention(text) => format!("Retention: {}", text),
395            Message::KaslServerPrivacyOnChange(text) => format!("If the level changes: {}", text),
396            Message::KaslServerPrivacyUpdatedAt(when) => format!("This level was last set {}.", when),
397            Message::KaslServerPrivacySetByAdmin => {
398                "The level is the installation's, set by an administrator on the server. kasl shows it; it cannot widen or narrow it from here.".to_string()
399            }
400            Message::KaslServerPushRetryable(error) => {
401                format!(
402                    "{}
403The day is unchanged here and stays queued - `kasl server flush` sends it when the server is back.",
404                    error
405                )
406            }
407
408            // === DATABASE MESSAGES ===
409            Message::DatabaseOperationFailed { operation, error } => {
410                format!("Database operation '{}' failed (continuing monitoring): {}", operation, error)
411            }
412            Message::NoIdSet => "No ID set".to_string(),
413
414            // === FILE SYSTEM MESSAGES ===
415            Message::FileNotFound => "File not found".to_string(),
416            Message::InvalidPidFileContent => "Invalid PID file content".to_string(),
417
418            // === SYSTEM/PATH MESSAGES ===
419            Message::PathConfigured => "PATH successfully configured for system-wide access".to_string(),
420            Message::PathConfigWarning { error } => format!(
421                "Warning: Could not configure PATH automatically. {}\nYou may need to run as administrator or manually add kasl to your PATH.",
422                error
423            ),
424            Message::PathRegistryQueryError { status } => format!("Registry query failed (exit code: {})", status),
425            Message::PathRegistryUpdateError { status, stderr } => {
426                if stderr.trim().is_empty() {
427                    format!("Registry update failed (exit code: {})", status)
428                } else {
429                    format!("Registry update failed (exit code: {}): {}", status, stderr.trim())
430                }
431            }
432            Message::FailedToJoinPaths => "Failed to join paths".to_string(),
433            Message::FailedToExecuteRegQuery => "Failed to execute reg query".to_string(),
434            Message::FailedToParseRegOutput => "Failed to parse reg query output".to_string(),
435            Message::FailedToGetPathFromReg => "Failed to get PATH value from reg query".to_string(),
436            Message::FailedToExecuteRegSet => "Failed to execute reg set".to_string(),
437            Message::FailedToOpenProcess(code) => format!("Failed to open process: error code {}", code),
438            Message::FailedToTerminateProcess(code) => format!("Failed to terminate process: error code {}", code),
439            Message::ProcessTerminationNotSupported => "Process termination not supported on this platform".to_string(),
440
441            // === PRODUCTIVITY MESSAGES ===
442            Message::MonthlyProductivity(percentage) => format!("Monthly work productivity: {:.1}%", percentage),
443            Message::LowProductivityWarning { current, threshold } => {
444                format!(
445                    "🔴 LOW PRODUCTIVITY: {:.1}% (minimum: {:.1}%)\n   If an absence is missing from the day, record it: kasl pauses add --start HH:MM --minutes N",
446                    current, threshold
447                )
448            }
449            Message::ProductivityTooLowToSend { current, threshold } => {
450                format!(
451                    "Cannot send report: productivity {:.1}% is below minimum {:.1}%\nIf an absence is missing from the day, record it: kasl pauses add --start HH:MM --minutes N",
452                    current, threshold
453                )
454            }
455            Message::ManualPauseCreated {
456                start_time,
457                end_time,
458                duration_minutes,
459            } => {
460                format!("Pause recorded: {} - {} ({} minutes)", start_time, end_time, duration_minutes)
461            }
462            Message::ManualPauseOverlaps { start_time, end_time } => {
463                format!("Overlaps an existing pause ({} - {})", start_time, end_time)
464            }
465            Message::ManualPauseRemoved(id) => format!("Pause {} removed", id),
466            Message::ManualPauseNotFound(id) => format!("no pause with id '{}' - see `kasl pauses list`", id),
467
468            // === ENCRYPTION/SECRET MESSAGES ===
469
470            // === PROMPTS ===
471            Message::PromptTaskName => "Enter task name (multi-line paste OK)".to_string(),
472            Message::TaskNameMergedFromPaste => "Merged pasted lines into the task name.".to_string(),
473            Message::PromptTaskComment => "Enter comment".to_string(),
474            Message::PromptTaskCompleteness => "Enter completeness".to_string(),
475            Message::PromptMinPauseDuration => "Enter minimum pause duration (minutes)".to_string(),
476            Message::PromptPauseThreshold => "Enter pause threshold (seconds)".to_string(),
477            Message::PromptPollInterval => "Enter poll interval (milliseconds)".to_string(),
478            Message::PromptActivityThreshold => "Enter activity threshold (seconds)".to_string(),
479            Message::PromptMinProductivityThreshold => "Enter minimum productivity threshold (%)".to_string(),
480            Message::PromptWorkdayHours => "Enter expected workday duration (hours)".to_string(),
481            Message::PromptMinWorkdayFraction => "Enter minimum workday fraction before suggesting breaks (0.0-1.0)".to_string(),
482            Message::PromptServerApiUrl => "Enter server API URL".to_string(),
483            Message::PromptServerAuthToken => "Enter server auth token".to_string(),
484            Message::PromptKaslServerUrl => "kasl-server URL".to_string(),
485            Message::PromptKaslServerToken => "Agent token (issued by your administrator)".to_string(),
486            Message::PromptSelectModules => "Select nodes to configure".to_string(),
487            Message::PromptSelectTasksToImport => "Select tasks to import".to_string(),
488            Message::PromptSelectTasksToIgnore => "Select tasks to ignore (optional)".to_string(),
489            Message::PromptSelectIgnoreNamesToRemove => "Select ignore names to remove (optional)".to_string(),
490            Message::PromptAddIgnoreName => "Add ignore name (empty to finish)".to_string(),
491
492            // === GENERAL MESSAGES ===
493            Message::OperationCompleted => "Operation completed successfully".to_string(),
494            Message::OperationCancelled => "Operation cancelled".to_string(),
495            Message::InvalidInput => "Invalid input provided".to_string(),
496            Message::PermissionDenied => "Permission denied".to_string(),
497            Message::DeprecatedCommand(old, new) => {
498                format!("`kasl {}` is now `kasl {}`. The old name still works but will be removed in 2.0.", old, new)
499            }
500
501            // === ERROR LOGGING ===
502            Message::ErrorSendingEvents(error) => format!("[kasl] Error sending events: {}", error),
503            Message::ErrorSendingMonthlyReport(error) => format!("[kasl] Error sending monthly report: {}", error),
504            Message::ErrorInRdevListener(error) => format!("Error in rdev listener: {:?}", error),
505            Message::ErrorRequestingRestDates(error) => format!("Error requesting rest dates: {}", error),
506
507            // === SPECIFIC UI MESSAGES ===
508            Message::SelectingTask(name) => format!("Selected task: {}", name),
509
510            // === JIRA INBOX MESSAGES ===
511            Message::JiraInboxRequiresJiraConfig => "Jira inbox requires Jira to be configured. Run `kasl setup` and select Jira.".to_string(),
512            Message::JiraInboxEmpty => "Jira inbox is empty.".to_string(),
513            Message::JiraInboxListHeader => "Jira inbox:".to_string(),
514            Message::JiraInboxListSliced { shown, total, what } => {
515                if what.is_empty() {
516                    format!("Jira inbox: {} of {} issues:", shown, total)
517                } else {
518                    format!("Jira inbox: {} of {} issues ({}):", shown, total, what)
519                }
520            }
521            Message::JiraInboxNoMatch { total, what } => format!("No issue matches ({}); {} in the inbox.", what, total),
522            Message::JiraInboxSynced {
523                fetched,
524                new_count,
525                changed,
526                gone,
527            } => {
528                format!("Jira inbox synced: {} fetched, {} new, {} changed, {} gone.", fetched, new_count, changed, gone)
529            }
530            Message::JiraInboxNewIssues(count) => format!("Jira inbox: {} new issue(s).", count),
531            Message::JiraInboxNotFound(key) => format!("Issue '{}' not found in inbox.", key),
532            Message::JiraInboxPinned(key) => format!("Pinned {}.", key),
533            Message::JiraInboxUnpinned(key) => format!("Unpinned {}.", key),
534            Message::JiraInboxDismissed(key) => format!("Dismissed {}.", key),
535            Message::JiraInboxSnoozed(key, until) => format!("Snoozed {} until {}.", key, until),
536            Message::JiraInboxUnsnoozed(key) => format!("Woke {}.", key),
537            Message::JiraInboxAllSnoozed(count) => {
538                format!("Jira inbox is clear; {} issue(s) are snoozed. `kasl inbox --snoozed` shows them.", count)
539            }
540            Message::JiraInboxWoke(count) => format!("Jira inbox: {} snoozed issue(s) came back.", count),
541            Message::JiraInboxTriaged {
542                taken,
543                snoozed,
544                dismissed,
545                skipped,
546                left,
547            } => {
548                // Only what happened: a run of twenty skips should not read as
549                // a wall of zeroes about things that did not.
550                let mut parts = Vec::new();
551                if *taken > 0 {
552                    parts.push(format!("{} taken", taken));
553                }
554                if *snoozed > 0 {
555                    parts.push(format!("{} snoozed", snoozed));
556                }
557                if *dismissed > 0 {
558                    parts.push(format!("{} dismissed", dismissed));
559                }
560                if *skipped > 0 {
561                    parts.push(format!("{} skipped", skipped));
562                }
563                let what = if parts.is_empty() { "nothing decided".to_string() } else { parts.join(", ") };
564                if *left > 0 {
565                    format!("Triaged: {}; {} left untouched.", what, left)
566                } else {
567                    format!("Triaged: {}.", what)
568                }
569            }
570            Message::JiraInboxOpened(key) => format!("Opened {} in browser.", key),
571            Message::JiraInboxTaken(key) => format!("Imported {} into tasks.", key),
572            Message::ToastActionUnknown(name) => format!("Unknown toast action '{}'; expected take, snooze or dismiss.", name),
573            Message::ToastActionApplied(what) => format!("{}.", what),
574            Message::ToastActionFailed(key, why) => format!("Could not act on {} from the toast: {}", key, why),
575            Message::JiraInboxAlreadyTaken(key, name) => format!("{} is already taken as '{}'.", key, name),
576            Message::JiraInboxSummary { total, fresh, taken } => {
577                // Only the parts that carry information: "3 in the inbox" says
578                // enough when none are new and none are taken.
579                let mut parts = Vec::new();
580                if *fresh > 0 {
581                    parts.push(format!("{} new", fresh));
582                }
583                if *taken > 0 {
584                    parts.push(format!("{} taken", taken));
585                }
586                let detail = if parts.is_empty() {
587                    String::new()
588                } else {
589                    format!(" ({})", parts.join(", "))
590                };
591                format!("{} in the inbox{}", total, detail)
592            }
593            Message::JiraInboxOpenFailed(err) => format!("Failed to open browser: {}", err),
594            Message::PromptJiraInboxEnabled => "Enable Jira inbox polling?".to_string(),
595            Message::PromptJiraInboxPollInterval => "Jira inbox poll interval (seconds)".to_string(),
596            Message::PromptJiraInboxNotify => "Show toast notifications for new issues?".to_string(),
597            Message::PromptJiraInboxSortFieldId => "Sort field id for ranking (e.g. customfield_12345 for Scoring; empty to skip)".to_string(),
598            Message::PromptJiraInboxSortFieldLabel => "Label for sort field (default Scoring)".to_string(),
599            Message::PromptJiraInboxExtraFieldId => "Additional custom field id to fetch (empty to finish)".to_string(),
600            Message::PromptJiraInboxExtraFieldLabel => "Label for this custom field".to_string(),
601
602            // === MIGRATION MESSAGES ===
603            Message::MigrationsFound(count) => format!("Found {} pending database migrations", count),
604            Message::RunningMigration(version, name) => format!("Running migration v{}: {}", version, name),
605            Message::MigrationCompleted(version) => format!("✓ Migration v{} completed", version),
606            Message::MigrationFailed(version, error) => format!("✗ Migration v{} failed: {}", version, error),
607            Message::AllMigrationsCompleted => "All database migrations completed successfully".to_string(),
608            Message::DatabaseVersion(version) => format!("Current database version: {}", version),
609            Message::DatabaseUpToDate => "Database schema is up to date".to_string(),
610            Message::DatabaseNeedsUpdate => "Database schema needs to be updated".to_string(),
611            Message::MigrationHistory => "Migration history:".to_string(),
612            Message::NothingToRollback => "Nothing to rollback".to_string(),
613            Message::RollingBack(from, to) => format!("Rolling back from v{} to v{}", from, to),
614            Message::RollbackCompleted(version) => format!("Rollback to v{} completed", version),
615        };
616
617        write!(f, "{}", text)
618    }
619}