diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5ea6b19..db72a70 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,10 +1,10 @@ +use base64::{engine::general_purpose, Engine as _}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs; use std::sync::{Arc, LazyLock, Mutex}; use std::thread; use std::time::{Duration, Instant}; -use base64::{Engine as _, engine::general_purpose}; use tauri::menu::{Menu, MenuItem}; use tauri::tray::{TrayIconBuilder, TrayIconEvent}; use tauri::{Emitter, Manager}; @@ -46,8 +46,8 @@ struct ManualSession { start_time: String, // "HH:MM" end_time: String, // "HH:MM" notes: Option, - created_at: String, // ISO string - date: String, // Date string for the session date + created_at: String, // ISO string + date: String, // Date string for the session date tags: Option>, // Array of tag objects } @@ -89,6 +89,8 @@ struct AppSettings { analytics_enabled: bool, #[serde(default)] hide_icon_on_close: bool, + #[serde(default)] + hide_status_bar: bool, } #[derive(Serialize, Deserialize, Clone)] @@ -174,9 +176,10 @@ impl Default for AppSettings { smart_pause_timeout: 30, // default 30 seconds }, advanced: AdvancedSettings::default(), - autostart: false, // default to disabled - analytics_enabled: true, // default to enabled + autostart: false, // default to disabled + analytics_enabled: true, // default to enabled hide_icon_on_close: false, // default to disabled + hide_status_bar: false, // default to disabled } } } @@ -418,25 +421,26 @@ async fn load_session_data(app: AppHandle) -> Result, St return Ok(None); } - let content = - fs::read_to_string(&file_path).map_err(|e| format!("Failed to read session file: {}", e))?; + let content = fs::read_to_string(&file_path) + .map_err(|e| format!("Failed to read session file: {}", e))?; let mut session: PomodoroSession = serde_json::from_str(&content).map_err(|e| format!("Failed to parse session: {}", e))?; // Get today's date string let today = chrono::Local::now().format("%a %b %d %Y").to_string(); - + // If the saved session is not from today, reset the counters but keep the date updated if session.date != today { session.completed_pomodoros = 0; session.total_focus_time = 0; session.current_session = 1; session.date = today; - + // Save the reset session back to file let json = serde_json::to_string_pretty(&session) .map_err(|e| format!("Failed to serialize reset session: {}", e))?; - fs::write(file_path, json).map_err(|e| format!("Failed to write reset session file: {}", e))?; + fs::write(file_path, json) + .map_err(|e| format!("Failed to write reset session file: {}", e))?; } Ok(Some(session)) @@ -630,7 +634,7 @@ async fn show_window(app: AppHandle) -> Result<(), String> { // Ignore error, just proceed with showing window } } - + window .show() .map_err(|e| format!("Failed to show window: {}", e))?; @@ -824,7 +828,8 @@ async fn save_manual_sessions(sessions: Vec, app: AppHandle) -> R let json = serde_json::to_string_pretty(&sessions) .map_err(|e| format!("Failed to serialize manual sessions: {}", e))?; - fs::write(file_path, json).map_err(|e| format!("Failed to write manual sessions file: {}", e))?; + fs::write(file_path, json) + .map_err(|e| format!("Failed to write manual sessions file: {}", e))?; // Track manual sessions saved analytics (if enabled) if are_analytics_enabled(&app).await { @@ -851,8 +856,8 @@ async fn load_manual_sessions(app: AppHandle) -> Result, Stri let content = fs::read_to_string(file_path) .map_err(|e| format!("Failed to read manual sessions file: {}", e))?; - let sessions: Vec = - serde_json::from_str(&content).map_err(|e| format!("Failed to parse manual sessions: {}", e))?; + let sessions: Vec = serde_json::from_str(&content) + .map_err(|e| format!("Failed to parse manual sessions: {}", e))?; Ok(sessions) } @@ -861,13 +866,13 @@ async fn load_manual_sessions(app: AppHandle) -> Result, Stri async fn save_manual_session(session: ManualSession, app: AppHandle) -> Result<(), String> { // Load existing sessions let mut sessions = load_manual_sessions(app.clone()).await?; - + // Remove existing session with same ID if it exists (for updates) sessions.retain(|s| s.id != session.id); - + // Add the new/updated session sessions.push(session); - + // Save all sessions back save_manual_sessions(sessions, app).await } @@ -876,24 +881,25 @@ async fn save_manual_session(session: ManualSession, app: AppHandle) -> Result<( async fn delete_manual_session(session_id: String, app: AppHandle) -> Result<(), String> { // Load existing sessions let mut sessions = load_manual_sessions(app.clone()).await?; - + // Remove the session with the specified ID sessions.retain(|s| s.id != session_id); - + // Save the updated sessions back save_manual_sessions(sessions, app).await } #[tauri::command] -async fn get_manual_sessions_for_date(date: String, app: AppHandle) -> Result, String> { +async fn get_manual_sessions_for_date( + date: String, + app: AppHandle, +) -> Result, String> { let sessions = load_manual_sessions(app).await?; - + // Filter sessions for the specified date - let filtered_sessions: Vec = sessions - .into_iter() - .filter(|s| s.date == date) - .collect(); - + let filtered_sessions: Vec = + sessions.into_iter().filter(|s| s.date == date).collect(); + Ok(filtered_sessions) } @@ -949,7 +955,8 @@ pub fn run() { add_session_tag, write_excel_file, start_oauth_server, - set_dock_visibility + set_dock_visibility, + set_status_bar_visibility ]) .setup(|app| { // Track app started event (if enabled) @@ -1048,7 +1055,7 @@ pub fn run() { if let tauri::WindowEvent::CloseRequested { api, .. } = event { // Always prevent close api.prevent_close(); - + // Check if we should hide the app icon let app_handle_clone = app_handle_for_close.clone(); tauri::async_runtime::spawn(async move { @@ -1056,24 +1063,34 @@ pub fn run() { Ok(settings) => { if settings.hide_icon_on_close { // Hide the window and set app as dock hidden - if let Some(window) = app_handle_clone.get_webview_window("main") { + if let Some(window) = + app_handle_clone.get_webview_window("main") + { let _ = window.hide(); // Use macOS specific API to hide from dock #[cfg(target_os = "macos")] { - let _ = set_dock_visibility(app_handle_clone.clone(), false).await; + let _ = set_dock_visibility( + app_handle_clone.clone(), + false, + ) + .await; } } } else { // Just hide the window without hiding from dock - if let Some(window) = app_handle_clone.get_webview_window("main") { + if let Some(window) = + app_handle_clone.get_webview_window("main") + { let _ = window.hide(); } } } Err(_) => { // Default behavior: just hide the window - if let Some(window) = app_handle_clone.get_webview_window("main") { + if let Some(window) = + app_handle_clone.get_webview_window("main") + { let _ = window.hide(); } } @@ -1124,6 +1141,21 @@ pub fn run() { let _ = app_handle.track_event("app_exited", None); app_handle.flush_events_blocking(); } + tauri::RunEvent::Reopen { .. } => { + // When the user clicks on the dock icon, show the window + if let Some(window) = app_handle.get_webview_window("main") { + let _ = window.show(); + let _ = window.set_focus(); + // If the app was previously hidden from dock, restore it + #[cfg(target_os = "macos")] + { + let app_handle_clone = app_handle.clone(); + tauri::async_runtime::spawn(async move { + let _ = set_dock_visibility(app_handle_clone, true).await; + }); + } + } + } _ => {} }); }) @@ -1137,10 +1169,10 @@ async fn load_tags(app: AppHandle) -> Result, String> { .map_err(|e| format!("Failed to get app data directory: {}", e))?; let file_path = app_data_dir.join("tags.json"); - + if file_path.exists() { - let content = fs::read_to_string(&file_path) - .map_err(|e| format!("Failed to read tags: {}", e))?; + let content = + fs::read_to_string(&file_path).map_err(|e| format!("Failed to read tags: {}", e))?; Ok(serde_json::from_str(&content).unwrap_or_else(|_| Vec::new())) } else { // Return default focus tag if no tags exist @@ -1179,13 +1211,13 @@ async fn save_tags(tags: Vec, app: AppHandle) -> Result<(), String> { #[tauri::command] async fn save_tag(tag: Tag, app: AppHandle) -> Result<(), String> { let mut tags = load_tags(app.clone()).await?; - + // Remove existing tag with same ID if it exists (for updates) tags.retain(|t| t.id != tag.id); - + // Add the new/updated tag tags.push(tag); - + // Save all tags back save_tags(tags, app).await } @@ -1193,10 +1225,10 @@ async fn save_tag(tag: Tag, app: AppHandle) -> Result<(), String> { #[tauri::command] async fn delete_tag(tag_id: String, app: AppHandle) -> Result<(), String> { let mut tags = load_tags(app.clone()).await?; - + // Remove the tag with the specified ID tags.retain(|t| t.id != tag_id); - + // Save the updated tags back save_tags(tags, app).await } @@ -1209,7 +1241,7 @@ async fn load_session_tags(app: AppHandle) -> Result, String> { .map_err(|e| format!("Failed to get app data directory: {}", e))?; let file_path = app_data_dir.join("session_tags.json"); - + if file_path.exists() { let content = fs::read_to_string(&file_path) .map_err(|e| format!("Failed to read session tags: {}", e))?; @@ -1243,7 +1275,6 @@ async fn add_session_tag(session_tag: SessionTag, app: AppHandle) -> Result<(), save_session_tags(session_tags, app).await } - #[tauri::command] async fn update_tray_menu( app: AppHandle, @@ -1319,11 +1350,11 @@ async fn write_excel_file(path: String, data: String) -> Result<(), String> { let decoded_data = general_purpose::STANDARD .decode(data) .map_err(|e| format!("Failed to decode base64 data: {}", e))?; - + // Write the binary data to file fs::write(&path, decoded_data) .map_err(|e| format!("Failed to write Excel file to {}: {}", path, e))?; - + Ok(()) } @@ -1346,12 +1377,12 @@ async fn set_dock_visibility(app: AppHandle, visible: bool) -> Result<(), String }) .map_err(|e| format!("Failed to run on main thread: {}", e))?; } - + #[cfg(not(target_os = "macos"))] { return Err("Dock visibility is only supported on macOS".to_string()); } - + Ok(()) } @@ -1359,7 +1390,7 @@ async fn set_dock_visibility(app: AppHandle, visible: bool) -> Result<(), String fn set_dock_visibility_native(visible: bool) { use cocoa::appkit::{NSApp, NSApplication, NSApplicationActivationPolicy}; use cocoa::base::nil; - + unsafe { let app = NSApp(); if app != nil { @@ -1368,8 +1399,211 @@ fn set_dock_visibility_native(visible: bool) { } else { NSApplicationActivationPolicy::NSApplicationActivationPolicyAccessory }; - + app.setActivationPolicy_(policy); } } } + +// Status bar visibility management using Carbon APIs +// +// Implementation Notes: +// This feature uses Apple's Carbon SetSystemUIMode API, which is a pure C function +// from the ApplicationServices framework. This approach is much safer than using +// Objective-C APIs because: +// +// 1. No foreign exceptions: C APIs return error codes instead of throwing exceptions +// 2. Direct system integration: Carbon APIs are lower-level and more stable +// 3. Robust fallback system: Multiple approaches with retry mechanisms +// 4. Comprehensive error handling: Detailed OSStatus code interpretation +// +// The implementation uses: +// - Primary: SetSystemUIMode with K_UI_MODE_CONTENT_SUPPRESSED (hides menu bar, keeps dock) +// - Fallback 1: Retry with delay for transient errors +// - Fallback 2: Conservative two-step approach for hiding +// - Detailed error reporting with manual recovery instructions +#[tauri::command] +async fn set_status_bar_visibility(_app: AppHandle, visible: bool) -> Result<(), String> { + #[cfg(target_os = "macos")] + { + match set_system_ui_mode_safe(visible) { + Ok(_) => { + println!( + "βœ… Status bar visibility successfully set to: {}", + if visible { "visible" } else { "hidden" } + ); + Ok(()) + } + Err(e) => { + eprintln!("❌ Failed to set status bar visibility: {}", e); + Err(format!("Failed to set status bar visibility: {}", e)) + } + } + } + + #[cfg(not(target_os = "macos"))] + { + return Err("Status bar visibility is only supported on macOS".to_string()); + } +} + +#[cfg(target_os = "macos")] +fn set_system_ui_mode_safe(visible: bool) -> Result<(), String> { + use libc::{c_int, c_uint}; + use std::thread; + use std::time::Duration; + + // Carbon SetSystemUIMode constants + const K_UI_MODE_NORMAL: c_uint = 0; // Normal mode - menu bar visible + const K_UI_MODE_CONTENT_SUPPRESSED: c_uint = 1; // Menu bar hidden, dock visible + #[allow(dead_code)] + const K_UI_MODE_CONTENT_HIDDEN: c_uint = 2; // Menu bar hidden, dock auto-hide + #[allow(dead_code)] + const K_UI_MODE_ALL_HIDDEN: c_uint = 3; // Everything hidden + + // OSStatus codes + const NO_ERR: c_int = 0; + const PARAM_ERR: c_int = -50; + const MEM_FULL_ERR: c_int = -108; + + // SystemUIMode and SystemUIOptions are both UInt32 (c_uint) + type SystemUIMode = c_uint; + type SystemUIOptions = c_uint; + type OSStatus = c_int; + + // External declaration for Carbon SetSystemUIMode function + // This is a C function from ApplicationServices framework + extern "C" { + fn SetSystemUIMode(inMode: SystemUIMode, inOptions: SystemUIOptions) -> OSStatus; + } + + // Try the primary approach with Carbon SetSystemUIMode + let primary_result = unsafe { + let mode = if visible { + K_UI_MODE_NORMAL // Show menu bar + } else { + K_UI_MODE_CONTENT_SUPPRESSED // Hide menu bar but keep dock visible + }; + + let options: SystemUIOptions = 0; // No special options + + println!( + "πŸ”§ Carbon API: Setting SystemUIMode to {} ({})", + mode, + if visible { + "normal/visible" + } else { + "content suppressed/hidden" + } + ); + + // Call the Carbon function - this is a pure C API call + let result: OSStatus = SetSystemUIMode(mode, options); + + if result == NO_ERR { + println!("βœ… Carbon API: SetSystemUIMode succeeded"); + Ok(()) + } else { + let error_msg = format!( + "Carbon API failed with OSStatus: {} ({})", + result, + get_osstatus_description(result) + ); + eprintln!("❌ Carbon API: {}", error_msg); + Err((result, error_msg)) + } + }; + + // If primary approach succeeded, return success + if primary_result.is_ok() { + return Ok(()); + } + + // If primary approach failed, try fallback methods + let (status_code, error_msg) = primary_result.unwrap_err(); + + eprintln!("πŸ”„ Primary method failed, attempting fallback approaches..."); + + // Fallback 1: Try with a small delay and retry + if status_code == PARAM_ERR || status_code == MEM_FULL_ERR { + println!("πŸ”„ Fallback 1: Retrying after brief delay..."); + thread::sleep(Duration::from_millis(100)); + + let retry_result = unsafe { + let mode = if visible { + K_UI_MODE_NORMAL + } else { + K_UI_MODE_CONTENT_SUPPRESSED + }; + let result: OSStatus = SetSystemUIMode(mode, 0); + + if result == NO_ERR { + println!("βœ… Fallback 1: Retry succeeded"); + Ok(()) + } else { + Err(format!("Retry failed with OSStatus: {}", result)) + } + }; + + if retry_result.is_ok() { + return Ok(()); + } + } + + // Fallback 2: For hiding, try a more conservative approach + if !visible { + println!("πŸ”„ Fallback 2: Trying conservative hide approach..."); + + let conservative_result = unsafe { + // Try normal mode first, then content suppressed + SetSystemUIMode(K_UI_MODE_NORMAL, 0); + thread::sleep(Duration::from_millis(50)); + let result: OSStatus = SetSystemUIMode(K_UI_MODE_CONTENT_SUPPRESSED, 0); + + if result == NO_ERR { + println!("βœ… Fallback 2: Conservative approach succeeded"); + Ok(()) + } else { + Err(format!( + "Conservative approach failed with OSStatus: {}", + result + )) + } + }; + + if conservative_result.is_ok() { + return Ok(()); + } + } + + // All methods failed - provide detailed error information + let detailed_error = format!( + "All status bar visibility methods failed. Primary error: {}. \ + This might be due to system restrictions or macOS version compatibility. \ + You can manually hide the menu bar using System Preferences > Dock & Menu Bar > 'Automatically hide and show the menu bar'.", + error_msg + ); + + eprintln!("❌ {}", detailed_error); + Err(detailed_error) +} + +#[cfg(target_os = "macos")] +fn get_osstatus_description(status: libc::c_int) -> &'static str { + match status { + 0 => "No error - Success", + -50 => "Parameter error - Invalid parameters passed to function", + -108 => "Memory full error - Insufficient memory available", + -25291 => "Invalid system UI mode - The specified UI mode is not valid", + -25292 => { + "Operation not supported in current mode - Cannot change UI mode in current state" + } + -25293 => "System UI server not available - UI server is not responding", + -25294 => "System UI mode locked - UI mode changes are currently locked", + -128 => "User canceled - Operation was canceled by user", + -43 => "File not found - Required system component not found", + -5000 => "System policy error - Operation blocked by system policy", + -1 => "General error - Unspecified error occurred", + _ => "Unknown error - Undocumented error code", + } +} diff --git a/src/core/pomodoro-timer.js b/src/core/pomodoro-timer.js index b83c580..b73f19e 100644 --- a/src/core/pomodoro-timer.js +++ b/src/core/pomodoro-timer.js @@ -200,7 +200,7 @@ export class PomodoroTimer { window.addEventListener('sessionAdded', async (event) => { const { date } = event.detail; const today = new Date().toDateString(); - + // Only update dots if the session was added for today if (date === today) { await this.updateProgressDots(); @@ -210,7 +210,7 @@ export class PomodoroTimer { window.addEventListener('sessionDeleted', async (event) => { const { date } = event.detail; const today = new Date().toDateString(); - + // Only update dots if the session was deleted from today if (date === today) { await this.updateProgressDots(); @@ -220,7 +220,7 @@ export class PomodoroTimer { window.addEventListener('sessionUpdated', async (event) => { const { date } = event.detail; const today = new Date().toDateString(); - + // Only update dots if the session was updated for today if (date === today) { await this.updateProgressDots(); @@ -769,14 +769,14 @@ export class PomodoroTimer { if (sessionElapsed >= this.maxSessionTime) { this.maxSessionTimeReached = true; this.pauseTimer(); - + // Show notification const maxTimeInMinutes = Math.floor(this.maxSessionTime / (60 * 1000)); NotificationUtils.showNotificationPing( `Session automatically paused after ${maxTimeInMinutes} minutes. Take a break! πŸ›‘`, 'warning' ); - + // Show desktop notification if enabled NotificationUtils.showDesktopNotification( 'Session Time Limit Reached', @@ -861,28 +861,28 @@ export class PomodoroTimer { adjustTimer(minutes) { // Convert minutes to seconds const adjustment = minutes * 60; - + // Add the adjustment to the current time remaining this.timeRemaining += adjustment; - + // Ensure we don't go below 0 seconds if (this.timeRemaining < 0) { this.timeRemaining = 0; } - + // If timer is running, we need to update the accuracy tracking if (this.isRunning && this.timerStartTime) { // Calculate how much time should remain based on the adjustment const now = Date.now(); const elapsedSinceStart = Math.floor((now - this.timerStartTime) / 1000); - + // Update the timer duration to account for the adjustment this.timerDuration = elapsedSinceStart + this.timeRemaining; } - + // Update the display immediately this.updateDisplay(); - + // Show notification const action = minutes > 0 ? 'added' : 'subtracted'; const absMinutes = Math.abs(minutes); @@ -914,7 +914,7 @@ export class PomodoroTimer { checkForMidnightReset() { const newDateString = new Date().toDateString(); - + if (newDateString !== this.currentDateString) { console.log('πŸŒ™ Date change detected:', this.currentDateString, 'β†’', newDateString); this.currentDateString = newDateString; @@ -930,10 +930,10 @@ export class PomodoroTimer { // Use enhanced loadSessionData with force reset to ensure clean state await this.loadSessionData(true); - + // Update display to show the reset state this.updateDisplay(); - + // Save the reset state await this.saveSessionData(); @@ -952,7 +952,7 @@ export class PomodoroTimer { // Update all visual elements this.updateTrayIcon(); - + // Refresh navigation charts if available if (window.navigationManager) { try { @@ -986,20 +986,20 @@ export class PomodoroTimer { // OVERTIME: If in overtime and continuous sessions, save session and skip double-counting if (this.timeRemaining < 0 && this.allowContinuousSessions) { shouldSaveSession = true; - + // During overtime, if session was completed but not saved, now save it with overtime included if (this.sessionCompletedButNotSaved && this.currentMode === 'focus') { // Calculate total time including overtime const now = Date.now(); const totalElapsedTime = Math.floor((now - this.sessionStartTime) / 1000); this.lastCompletedSessionTime = totalElapsedTime; - + // Only save if session lasted at least 1 minute if (this.lastCompletedSessionTime > 60) { await this.saveCompletedFocusSession(); } } - + this.sessionCompletedButNotSaved = false; // Move to next mode as usual @@ -1009,24 +1009,24 @@ export class PomodoroTimer { } else { this.currentMode = 'break'; } - + // Reset display state tracking when switching to break modes this._lastLoggedState = null; this._lastAutoPausedLogged = false; this._lastPausedLogged = false; } else { this.currentMode = 'focus'; - + // Reset display state tracking when switching to focus mode this._lastLoggedState = null; this._lastAutoPausedLogged = false; this._lastPausedLogged = false; - + // Restore TagManager display when returning to focus mode if (window.tagManager) { window.tagManager.updateStatusDisplay(); } - + if (this.completedPomodoros < this.totalSessions) { this.currentSession = this.completedPomodoros + 1; } @@ -1037,14 +1037,14 @@ export class PomodoroTimer { if (shouldSaveSession) { this.saveSessionData(); } - + // Reset session start time for next session (after saving) console.log('πŸ”„ Resetting sessionStartTime after overtime skip:', { beforeReset: this.sessionStartTime, beforeResetISO: this.sessionStartTime ? new Date(this.sessionStartTime).toISOString() : null }); this.sessionStartTime = null; - + const messages = { focus: 'Focus session skipped. Time for a break! 😌', break: 'Break skipped. Ready to focus? πŸ…', @@ -1068,7 +1068,7 @@ export class PomodoroTimer { const actualElapsedTime = this.currentSessionElapsedTime || (this.durations.focus - this.timeRemaining); this.totalFocusTime += actualElapsedTime; this.lastCompletedSessionTime = actualElapsedTime; - + // Preserve session start time for saving console.log('Preserving session start time:', { before: this.lastSessionStartTime, @@ -1090,14 +1090,14 @@ export class PomodoroTimer { } else { this.currentMode = 'break'; } - + // Reset display state tracking when switching to break modes this._lastLoggedState = null; this._lastAutoPausedLogged = false; this._lastPausedLogged = false; } else { this.currentMode = 'focus'; - + // Reset display state tracking when switching to focus mode this._lastLoggedState = null; this._lastAutoPausedLogged = false; @@ -1118,14 +1118,14 @@ export class PomodoroTimer { if (shouldSaveSession) { this.saveSessionData(); } - + // Reset session start time for next session (after saving) console.log('πŸ”„ Resetting sessionStartTime after normal skip:', { beforeReset: this.sessionStartTime, beforeResetISO: this.sessionStartTime ? new Date(this.sessionStartTime).toISOString() : null }); this.sessionStartTime = null; - + const messages = { focus: 'Focus session skipped. Time for a break! 😌', break: 'Break skipped. Ready to focus? πŸ…', @@ -1165,7 +1165,7 @@ export class PomodoroTimer { // Store the actual elapsed time for undo functionality this.lastCompletedSessionTime = actualElapsedTime; - + // Preserve session start time for saving console.log('Preserving session start time (timer completion):', { before: this.lastSessionStartTime, @@ -1194,7 +1194,7 @@ export class PomodoroTimer { } else { this.currentMode = 'break'; } - + // Reset display state tracking when switching to break modes this._lastLoggedState = null; this._lastAutoPausedLogged = false; @@ -1208,7 +1208,7 @@ export class PomodoroTimer { } else { // Traditional behavior - go back to focus this.currentMode = 'focus'; - + // Reset display state tracking when switching to focus mode this._lastLoggedState = null; this._lastAutoPausedLogged = false; @@ -1325,7 +1325,7 @@ export class PomodoroTimer { // Store the actual elapsed time for undo functionality this.lastCompletedSessionTime = actualElapsedTime; - + // Preserve session start time for saving console.log('Preserving session start time (overtime):', { before: this.lastSessionStartTime, @@ -2174,7 +2174,7 @@ export class PomodoroTimer { // Use preserved session start time if available, otherwise fall back to calculating backwards let startHour, startMinute; const actualSessionStartTime = this.lastSessionStartTime; - + console.log('Session saving debug:', { lastSessionStartTime: this.lastSessionStartTime, sessionStartTime: this.sessionStartTime, @@ -2183,7 +2183,7 @@ export class PomodoroTimer { nowISO: now.toISOString(), nowLocal: now.toString() }); - + if (actualSessionStartTime) { const sessionStart = new Date(actualSessionStartTime); startHour = sessionStart.getHours(); @@ -2206,7 +2206,7 @@ export class PomodoroTimer { const endHour = now.getHours(); const endMinute = now.getMinutes(); - + console.log('Final time values:', { startHour: startHour, startMinute: startMinute, @@ -2218,7 +2218,7 @@ export class PomodoroTimer { // Get current tags from TagManager const currentTags = window.tagManager ? window.tagManager.getCurrentTags() : []; - + const sessionData = { id: `timer_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, session_type: 'focus', @@ -2233,7 +2233,7 @@ export class PomodoroTimer { try { await window.sessionManager.addSession(sessionData); console.log('Timer session saved to SessionManager:', sessionData); - + // Clear the preserved session start time after successful save this.lastSessionStartTime = null; console.log('Cleared lastSessionStartTime after successful save'); @@ -2244,10 +2244,10 @@ export class PomodoroTimer { async loadSessionData(forceReset = false) { const today = new Date().toDateString(); - + // Update current date string for midnight monitoring this.currentDateString = today; - + try { const data = await invoke('load_session_data'); if (data && data.date === today && !forceReset) { @@ -2268,7 +2268,7 @@ export class PomodoroTimer { } catch (error) { console.error('Failed to load session data from Tauri, using localStorage:', error); const saved = localStorage.getItem('pomodoro-session'); - + if (saved) { const data = JSON.parse(saved); if (data.date === today && !forceReset) { @@ -2361,28 +2361,12 @@ export class PomodoroTimer { // Update tray icon with timer information async updateTrayIcon() { try { - let displayMinutes, displaySeconds, isOvertime = false; - - if (this.timeRemaining < 0 && this.allowContinuousSessions) { - // Show overtime in continuous sessions - isOvertime = true; - const overtimeSeconds = Math.abs(this.timeRemaining); - displayMinutes = Math.floor(overtimeSeconds / 60); - displaySeconds = overtimeSeconds % 60; - } else { - // Normal display or traditional mode - const absTime = Math.abs(this.timeRemaining); - displayMinutes = Math.floor(absTime / 60); - displaySeconds = absTime % 60; - } - - const timerText = `${displayMinutes.toString().padStart(2, '0')}:${displaySeconds.toString().padStart(2, '0')}`; - const overtimePrefix = isOvertime ? '+' : ''; - const fullTimerText = `${overtimePrefix}${timerText}`; + // Check status bar display setting + const settingsManager = window.settingsManager; + const statusBarDisplay = settingsManager ? settingsManager.settings.status_bar_display : 'default'; - // Add session counter to timer text - const sessionCounter = `(${this.completedPomodoros}/${this.totalSessions})`; - const completeTimerText = `${fullTimerText} ${sessionCounter}`; + let displayText = ''; + let modeIcon; // Define icons for different modes const modeIcons = { @@ -2392,18 +2376,54 @@ export class PomodoroTimer { }; // Show pause icon if timer is paused or auto-paused - let modeIcon; if (this.isPaused || this.isAutoPaused) { modeIcon = '⏸️'; - } else if (isOvertime) { + } else if (this.timeRemaining < 0 && this.allowContinuousSessions) { // Show overtime indicator in tray modeIcon = '⏰'; } else { modeIcon = modeIcons[this.currentMode] || '🧠'; } + // Set display text based on status bar display mode + if (statusBarDisplay === 'icon-only') { + // Show only the mode icon in displayText, and pass empty string for modeIcon to avoid duplication + displayText = modeIcon; + modeIcon = ''; // Clear modeIcon to avoid showing it twice + } else { + // Default mode: show timer in mm:ss format + let displayMinutes, displaySeconds, isOvertime = false; + + if (this.timeRemaining < 0 && this.allowContinuousSessions) { + // Show overtime in continuous sessions + isOvertime = true; + const overtimeSeconds = Math.abs(this.timeRemaining); + displayMinutes = Math.floor(overtimeSeconds / 60); + displaySeconds = overtimeSeconds % 60; + } else { + // Normal display or traditional mode + const absTime = Math.abs(this.timeRemaining); + displayMinutes = Math.floor(absTime / 60); + displaySeconds = absTime % 60; + } + + const timerText = `${displayMinutes.toString().padStart(2, '0')}:${displaySeconds.toString().padStart(2, '0')}`; + const overtimePrefix = isOvertime ? '+' : ''; + const fullTimerText = `${overtimePrefix}${timerText}`; + + // Show timer with session counter + const sessionCounter = `(${this.completedPomodoros}/${this.totalSessions})`; + const realText = `${fullTimerText} ${sessionCounter}`; + + // Add invisible characters (zero-width spaces) to pad to maximum possible length + // Maximum length would be: "+99:99 (99/99)" = 14 characters + const maxLength = 14; + const padding = '\u200B'.repeat(maxLength - realText.length); // zero-width space + displayText = realText + padding; + } + await invoke('update_tray_icon', { - timerText: completeTimerText, + timerText: displayText, isRunning: this.isRunning, sessionMode: this.currentMode, currentSession: this.currentSession, @@ -2430,7 +2450,7 @@ export class PomodoroTimer { } this.totalSessions = settings.timer.total_sessions; - + // Update max session time (convert from minutes to milliseconds) this.maxSessionTime = (settings.timer.max_session_time || 120) * 60 * 1000; diff --git a/src/index.html b/src/index.html index de838bb..db70a4b 100644 --- a/src/index.html +++ b/src/index.html @@ -804,7 +804,18 @@

System Integration

Hide Icon on Close -

Hide the app icon from the dock when closing the window with X. The app will continue running in the system tray.

+

Hide the app icon from the dock when closing the window with X. The app + will continue running in the system tray.

+ + +
+ + +

Choose how the timer information is displayed in the system status + bar/tray.

diff --git a/src/managers/settings-manager.js b/src/managers/settings-manager.js index 5098782..b1adcba 100644 --- a/src/managers/settings-manager.js +++ b/src/managers/settings-manager.js @@ -64,6 +64,16 @@ export class SettingsManager { console.log('πŸ“‹ Raw loaded settings:', loadedSettings); // Merge loaded settings with defaults to ensure all fields exist this.settings = this.mergeWithDefaults(loadedSettings); + + // Migrate old hide_status_bar setting to new status_bar_display setting + if (loadedSettings.hide_status_bar !== undefined && loadedSettings.status_bar_display === undefined) { + // If old setting existed but new one doesn't, migrate + this.settings.status_bar_display = loadedSettings.hide_status_bar ? 'icon-only' : 'default'; + // Schedule save to persist the migrated setting + this.scheduleAutoSave(); + console.log('πŸ”„ Migrated hide_status_bar setting to status_bar_display:', this.settings.status_bar_display); + } + console.log('πŸ“‹ Final merged settings:', this.settings); this.populateSettingsUI(); } catch (error) { @@ -84,7 +94,8 @@ export class SettingsManager { advanced: { ...defaultSettings.advanced, ...loadedSettings.advanced }, autostart: loadedSettings.autostart !== undefined ? loadedSettings.autostart : defaultSettings.autostart, analytics_enabled: loadedSettings.analytics_enabled !== undefined ? loadedSettings.analytics_enabled : defaultSettings.analytics_enabled, - hide_icon_on_close: loadedSettings.hide_icon_on_close !== undefined ? loadedSettings.hide_icon_on_close : defaultSettings.hide_icon_on_close + hide_icon_on_close: loadedSettings.hide_icon_on_close !== undefined ? loadedSettings.hide_icon_on_close : defaultSettings.hide_icon_on_close, + status_bar_display: loadedSettings.status_bar_display !== undefined ? loadedSettings.status_bar_display : defaultSettings.status_bar_display }; } @@ -120,7 +131,8 @@ export class SettingsManager { }, autostart: false, // default to disabled analytics_enabled: true, // Analytics enabled by default - hide_icon_on_close: false // Hide icon on close disabled by default + hide_icon_on_close: false, // Hide icon on close disabled by default + status_bar_display: 'default' // Status bar display mode: 'default' or 'icon-only' }; } @@ -136,7 +148,7 @@ export class SettingsManager { document.getElementById('break-duration').value = this.settings.timer.break_duration; document.getElementById('long-break-duration').value = this.settings.timer.long_break_duration; document.getElementById('total-sessions').value = this.settings.timer.total_sessions; - + // Populate max session time const maxSessionTimeField = document.getElementById('max-session-time'); if (maxSessionTimeField) { @@ -203,6 +215,9 @@ export class SettingsManager { // Populate hide icon on close setting this.loadHideIconOnCloseSetting(); + + // Populate status bar display setting + this.loadStatusBarDisplaySetting(); } setupEventListeners() { @@ -433,7 +448,7 @@ export class SettingsManager { this.settings.timer.break_duration = parseInt(document.getElementById('break-duration').value); this.settings.timer.long_break_duration = parseInt(document.getElementById('long-break-duration').value); this.settings.timer.total_sessions = parseInt(document.getElementById('total-sessions').value); - + // Max session time setting const maxSessionTimeField = document.getElementById('max-session-time'); if (maxSessionTimeField) { @@ -921,6 +936,67 @@ export class SettingsManager { } } + async loadStatusBarDisplaySetting() { + try { + // Get current status bar display setting from our stored settings + const statusBarDisplay = this.settings.status_bar_display || 'default'; + + const select = document.getElementById('status-bar-display'); + if (select) { + select.value = statusBarDisplay; + + // Setup event listener for the status bar display select + select.addEventListener('change', async (e) => { + await this.updateStatusBarDisplay(e.target.value); + }); + } + } catch (error) { + console.error('Failed to load status bar display setting:', error); + // Default to 'default' if we can't check the status + const select = document.getElementById('status-bar-display'); + if (select) { + select.value = 'default'; + select.addEventListener('change', async (e) => { + await this.updateStatusBarDisplay(e.target.value); + }); + } + } + } + + async updateStatusBarDisplay(displayMode) { + try { + // Update our settings + this.settings.status_bar_display = displayMode; + + // Apply the display mode immediately if timer exists + if (window.pomodoroTimer) { + await window.pomodoroTimer.updateTrayIcon(); + } + + // Show user feedback + if (displayMode === 'icon-only') { + console.log('Status bar display set to icon only'); + NotificationUtils.showNotificationPing('βœ“ Status bar will show icon only', 'success'); + } else { + console.log('Status bar display set to default (mm:ss)'); + NotificationUtils.showNotificationPing('βœ“ Status bar will show timer (mm:ss)', 'success'); + } + + // Schedule auto-save to persist the setting + this.scheduleAutoSave(); + + } catch (error) { + console.error('Failed to update status bar display:', error); + NotificationUtils.showNotificationPing('❌ Failed to update status bar display: ' + error, 'error'); + + // Revert the select state on error + const select = document.getElementById('status-bar-display'); + if (select) { + select.value = this.settings.status_bar_display || 'default'; + } + } + } + // Theme management functions async applyTheme(theme) { const html = document.documentElement; diff --git a/src/styles/settings.css b/src/styles/settings.css index bdf963f..ece87e9 100644 --- a/src/styles/settings.css +++ b/src/styles/settings.css @@ -83,6 +83,41 @@ box-shadow: 0 0 0 3px var(--input-focus-shadow); } +/* Setting select dropdowns */ +.setting-item select, +.setting-select { + padding: 0.75rem; + border: 2px solid var(--input-border); + border-radius: 8px; + font-size: 1rem; + background: var(--setting-item-bg); + color: var(--text-color); + transition: all 0.2s ease; + min-width: 200px; + cursor: pointer; +} + +.setting-item select:focus, +.setting-select:focus { + outline: none; + border-color: var(--accent-color); + box-shadow: 0 0 0 3px var(--input-focus-shadow); +} + +.setting-item select:hover, +.setting-select:hover { + border-color: var(--accent-color); +} + +/* Setting labels for selects */ +.setting-item .setting-label { + font-weight: 600; + font-size: 1rem; + color: var(--text-color); + margin-bottom: 0.5rem; + display: block; +} + /* Shortcut Items */ .shortcut-item { display: flex; diff --git a/src/utils/theme-loader.js b/src/utils/theme-loader.js index 41d1a3f..b121c03 100644 --- a/src/utils/theme-loader.js +++ b/src/utils/theme-loader.js @@ -39,7 +39,7 @@ class ThemeLoader { // that gets updated by the build process or manually maintained // This could be enhanced to use a build-time script that generates this list - const knownThemes = [ + const knownThemes = [ 'espresso.css', 'pipboy.css', 'pommodore64.css'