Skip to content

Commit 5e6b5ee

Browse files
committed
feat: add 'Hide Status Bar' setting and functionality to manage visibility
1 parent fd3fe18 commit 5e6b5ee

File tree

5 files changed

+270
-6
lines changed

5 files changed

+270
-6
lines changed

src-tauri/src/lib.rs

Lines changed: 186 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ struct AppSettings {
8989
analytics_enabled: bool,
9090
#[serde(default)]
9191
hide_icon_on_close: bool,
92+
#[serde(default)]
93+
hide_status_bar: bool,
9294
}
9395

9496
#[derive(Serialize, Deserialize, Clone)]
@@ -177,6 +179,7 @@ impl Default for AppSettings {
177179
autostart: false, // default to disabled
178180
analytics_enabled: true, // default to enabled
179181
hide_icon_on_close: false, // default to disabled
182+
hide_status_bar: false, // default to disabled
180183
}
181184
}
182185
}
@@ -949,7 +952,8 @@ pub fn run() {
949952
add_session_tag,
950953
write_excel_file,
951954
start_oauth_server,
952-
set_dock_visibility
955+
set_dock_visibility,
956+
set_status_bar_visibility
953957
])
954958
.setup(|app| {
955959
// Track app started event (if enabled)
@@ -1373,3 +1377,184 @@ fn set_dock_visibility_native(visible: bool) {
13731377
}
13741378
}
13751379
}
1380+
1381+
// Status bar visibility management using Carbon APIs
1382+
//
1383+
// Implementation Notes:
1384+
// This feature uses Apple's Carbon SetSystemUIMode API, which is a pure C function
1385+
// from the ApplicationServices framework. This approach is much safer than using
1386+
// Objective-C APIs because:
1387+
//
1388+
// 1. No foreign exceptions: C APIs return error codes instead of throwing exceptions
1389+
// 2. Direct system integration: Carbon APIs are lower-level and more stable
1390+
// 3. Robust fallback system: Multiple approaches with retry mechanisms
1391+
// 4. Comprehensive error handling: Detailed OSStatus code interpretation
1392+
//
1393+
// The implementation uses:
1394+
// - Primary: SetSystemUIMode with K_UI_MODE_CONTENT_SUPPRESSED (hides menu bar, keeps dock)
1395+
// - Fallback 1: Retry with delay for transient errors
1396+
// - Fallback 2: Conservative two-step approach for hiding
1397+
// - Detailed error reporting with manual recovery instructions
1398+
#[tauri::command]
1399+
async fn set_status_bar_visibility(_app: AppHandle, visible: bool) -> Result<(), String> {
1400+
#[cfg(target_os = "macos")]
1401+
{
1402+
match set_system_ui_mode_safe(visible) {
1403+
Ok(_) => {
1404+
println!("✅ Status bar visibility successfully set to: {}", if visible { "visible" } else { "hidden" });
1405+
Ok(())
1406+
},
1407+
Err(e) => {
1408+
eprintln!("❌ Failed to set status bar visibility: {}", e);
1409+
Err(format!("Failed to set status bar visibility: {}", e))
1410+
}
1411+
}
1412+
}
1413+
1414+
#[cfg(not(target_os = "macos"))]
1415+
{
1416+
return Err("Status bar visibility is only supported on macOS".to_string());
1417+
}
1418+
}
1419+
1420+
#[cfg(target_os = "macos")]
1421+
fn set_system_ui_mode_safe(visible: bool) -> Result<(), String> {
1422+
use libc::{c_int, c_uint};
1423+
use std::thread;
1424+
use std::time::Duration;
1425+
1426+
// Carbon SetSystemUIMode constants
1427+
const K_UI_MODE_NORMAL: c_uint = 0; // Normal mode - menu bar visible
1428+
const K_UI_MODE_CONTENT_SUPPRESSED: c_uint = 1; // Menu bar hidden, dock visible
1429+
#[allow(dead_code)]
1430+
const K_UI_MODE_CONTENT_HIDDEN: c_uint = 2; // Menu bar hidden, dock auto-hide
1431+
#[allow(dead_code)]
1432+
const K_UI_MODE_ALL_HIDDEN: c_uint = 3; // Everything hidden
1433+
1434+
// OSStatus codes
1435+
const NO_ERR: c_int = 0;
1436+
const PARAM_ERR: c_int = -50;
1437+
const MEM_FULL_ERR: c_int = -108;
1438+
1439+
// SystemUIMode and SystemUIOptions are both UInt32 (c_uint)
1440+
type SystemUIMode = c_uint;
1441+
type SystemUIOptions = c_uint;
1442+
type OSStatus = c_int;
1443+
1444+
// External declaration for Carbon SetSystemUIMode function
1445+
// This is a C function from ApplicationServices framework
1446+
extern "C" {
1447+
fn SetSystemUIMode(inMode: SystemUIMode, inOptions: SystemUIOptions) -> OSStatus;
1448+
}
1449+
1450+
// Try the primary approach with Carbon SetSystemUIMode
1451+
let primary_result = unsafe {
1452+
let mode = if visible {
1453+
K_UI_MODE_NORMAL // Show menu bar
1454+
} else {
1455+
K_UI_MODE_CONTENT_SUPPRESSED // Hide menu bar but keep dock visible
1456+
};
1457+
1458+
let options: SystemUIOptions = 0; // No special options
1459+
1460+
println!("🔧 Carbon API: Setting SystemUIMode to {} ({})",
1461+
mode, if visible { "normal/visible" } else { "content suppressed/hidden" });
1462+
1463+
// Call the Carbon function - this is a pure C API call
1464+
let result: OSStatus = SetSystemUIMode(mode, options);
1465+
1466+
if result == NO_ERR {
1467+
println!("✅ Carbon API: SetSystemUIMode succeeded");
1468+
Ok(())
1469+
} else {
1470+
let error_msg = format!("Carbon API failed with OSStatus: {} ({})",
1471+
result, get_osstatus_description(result));
1472+
eprintln!("❌ Carbon API: {}", error_msg);
1473+
Err((result, error_msg))
1474+
}
1475+
};
1476+
1477+
// If primary approach succeeded, return success
1478+
if primary_result.is_ok() {
1479+
return Ok(());
1480+
}
1481+
1482+
// If primary approach failed, try fallback methods
1483+
let (status_code, error_msg) = primary_result.unwrap_err();
1484+
1485+
eprintln!("🔄 Primary method failed, attempting fallback approaches...");
1486+
1487+
// Fallback 1: Try with a small delay and retry
1488+
if status_code == PARAM_ERR || status_code == MEM_FULL_ERR {
1489+
println!("🔄 Fallback 1: Retrying after brief delay...");
1490+
thread::sleep(Duration::from_millis(100));
1491+
1492+
let retry_result = unsafe {
1493+
let mode = if visible { K_UI_MODE_NORMAL } else { K_UI_MODE_CONTENT_SUPPRESSED };
1494+
let result: OSStatus = SetSystemUIMode(mode, 0);
1495+
1496+
if result == NO_ERR {
1497+
println!("✅ Fallback 1: Retry succeeded");
1498+
Ok(())
1499+
} else {
1500+
Err(format!("Retry failed with OSStatus: {}", result))
1501+
}
1502+
};
1503+
1504+
if retry_result.is_ok() {
1505+
return Ok(());
1506+
}
1507+
}
1508+
1509+
// Fallback 2: For hiding, try a more conservative approach
1510+
if !visible {
1511+
println!("🔄 Fallback 2: Trying conservative hide approach...");
1512+
1513+
let conservative_result = unsafe {
1514+
// Try normal mode first, then content suppressed
1515+
SetSystemUIMode(K_UI_MODE_NORMAL, 0);
1516+
thread::sleep(Duration::from_millis(50));
1517+
let result: OSStatus = SetSystemUIMode(K_UI_MODE_CONTENT_SUPPRESSED, 0);
1518+
1519+
if result == NO_ERR {
1520+
println!("✅ Fallback 2: Conservative approach succeeded");
1521+
Ok(())
1522+
} else {
1523+
Err(format!("Conservative approach failed with OSStatus: {}", result))
1524+
}
1525+
};
1526+
1527+
if conservative_result.is_ok() {
1528+
return Ok(());
1529+
}
1530+
}
1531+
1532+
// All methods failed - provide detailed error information
1533+
let detailed_error = format!(
1534+
"All status bar visibility methods failed. Primary error: {}. \
1535+
This might be due to system restrictions or macOS version compatibility. \
1536+
You can manually hide the menu bar using System Preferences > Dock & Menu Bar > 'Automatically hide and show the menu bar'.",
1537+
error_msg
1538+
);
1539+
1540+
eprintln!("❌ {}", detailed_error);
1541+
Err(detailed_error)
1542+
}
1543+
1544+
#[cfg(target_os = "macos")]
1545+
fn get_osstatus_description(status: libc::c_int) -> &'static str {
1546+
match status {
1547+
0 => "No error - Success",
1548+
-50 => "Parameter error - Invalid parameters passed to function",
1549+
-108 => "Memory full error - Insufficient memory available",
1550+
-25291 => "Invalid system UI mode - The specified UI mode is not valid",
1551+
-25292 => "Operation not supported in current mode - Cannot change UI mode in current state",
1552+
-25293 => "System UI server not available - UI server is not responding",
1553+
-25294 => "System UI mode locked - UI mode changes are currently locked",
1554+
-128 => "User canceled - Operation was canceled by user",
1555+
-43 => "File not found - Required system component not found",
1556+
-5000 => "System policy error - Operation blocked by system policy",
1557+
-1 => "General error - Unspecified error occurred",
1558+
_ => "Unknown error - Undocumented error code"
1559+
}
1560+
}

src/core/pomodoro-timer.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2380,9 +2380,15 @@ export class PomodoroTimer {
23802380
const overtimePrefix = isOvertime ? '+' : '';
23812381
const fullTimerText = `${overtimePrefix}${timerText}`;
23822382

2383-
// Add session counter to timer text
2383+
// Show real timer but add invisible padding characters to maintain fixed width
23842384
const sessionCounter = `(${this.completedPomodoros}/${this.totalSessions})`;
2385-
const completeTimerText = `${fullTimerText} ${sessionCounter}`;
2385+
const realText = `${fullTimerText} ${sessionCounter}`;
2386+
2387+
// Add invisible characters (zero-width spaces) to pad to maximum possible length
2388+
// Maximum length would be: "+99:99 (99/99)" = 14 characters
2389+
const maxLength = 14;
2390+
const padding = '\u200B'.repeat(maxLength - realText.length); // zero-width space
2391+
const completeTimerText = realText + padding;
23862392

23872393
// Define icons for different modes
23882394
const modeIcons = {

src/index.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,15 @@ <h3>System Integration</h3>
806806
</label>
807807
<p class="setting-description">Hide the app icon from the dock when closing the window with X. The app will continue running in the system tray.</p>
808808
</div>
809+
810+
<div class="setting-item">
811+
<label class="checkbox-label">
812+
<input type="checkbox" id="hide-status-bar">
813+
<span class="checkmark"></span>
814+
Hide Status Bar
815+
</label>
816+
<p class="setting-description">Hide the status bar at the top of the screen when the app is focused. The status bar will be restored when switching to other apps.</p>
817+
</div>
809818
</div>
810819

811820
<div class="settings-section">

src/managers/settings-manager.js

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ export class SettingsManager {
8484
advanced: { ...defaultSettings.advanced, ...loadedSettings.advanced },
8585
autostart: loadedSettings.autostart !== undefined ? loadedSettings.autostart : defaultSettings.autostart,
8686
analytics_enabled: loadedSettings.analytics_enabled !== undefined ? loadedSettings.analytics_enabled : defaultSettings.analytics_enabled,
87-
hide_icon_on_close: loadedSettings.hide_icon_on_close !== undefined ? loadedSettings.hide_icon_on_close : defaultSettings.hide_icon_on_close
87+
hide_icon_on_close: loadedSettings.hide_icon_on_close !== undefined ? loadedSettings.hide_icon_on_close : defaultSettings.hide_icon_on_close,
88+
hide_status_bar: loadedSettings.hide_status_bar !== undefined ? loadedSettings.hide_status_bar : defaultSettings.hide_status_bar
8889
};
8990
}
9091

@@ -120,7 +121,8 @@ export class SettingsManager {
120121
},
121122
autostart: false, // default to disabled
122123
analytics_enabled: true, // Analytics enabled by default
123-
hide_icon_on_close: false // Hide icon on close disabled by default
124+
hide_icon_on_close: false, // Hide icon on close disabled by default
125+
hide_status_bar: false // Hide status bar disabled by default
124126
};
125127
}
126128

@@ -203,6 +205,9 @@ export class SettingsManager {
203205

204206
// Populate hide icon on close setting
205207
this.loadHideIconOnCloseSetting();
208+
209+
// Populate hide status bar setting
210+
this.loadHideStatusBarSetting();
206211
}
207212

208213
setupEventListeners() {
@@ -921,6 +926,65 @@ export class SettingsManager {
921926
}
922927
}
923928

929+
async loadHideStatusBarSetting() {
930+
try {
931+
// Get current hide status bar setting from our stored settings
932+
const hideStatusBar = this.settings.hide_status_bar;
933+
934+
const checkbox = document.getElementById('hide-status-bar');
935+
if (checkbox) {
936+
checkbox.checked = hideStatusBar;
937+
938+
// Setup event listener for the hide status bar checkbox
939+
checkbox.addEventListener('change', async (e) => {
940+
await this.toggleHideStatusBar(e.target.checked);
941+
});
942+
}
943+
} catch (error) {
944+
console.error('Failed to load hide status bar setting:', error);
945+
// Default to disabled if we can't check the status
946+
const checkbox = document.getElementById('hide-status-bar');
947+
if (checkbox) {
948+
checkbox.checked = false;
949+
checkbox.addEventListener('change', async (e) => {
950+
await this.toggleHideStatusBar(e.target.checked);
951+
});
952+
}
953+
}
954+
}
955+
956+
async toggleHideStatusBar(enabled) {
957+
try {
958+
// Call the Tauri command to update the status bar visibility
959+
await invoke('set_status_bar_visibility', { visible: !enabled });
960+
961+
// Update our settings
962+
this.settings.hide_status_bar = enabled;
963+
964+
// Show user feedback
965+
if (enabled) {
966+
console.log('Hide status bar enabled');
967+
NotificationUtils.showNotificationPing('✓ Status bar hidden - Will hide when app is focused', 'success');
968+
} else {
969+
console.log('Hide status bar disabled');
970+
NotificationUtils.showNotificationPing('✓ Status bar visible - Will show when app is focused', 'success');
971+
}
972+
973+
// Schedule auto-save to persist the setting
974+
this.scheduleAutoSave();
975+
976+
} catch (error) {
977+
console.error('Failed to toggle hide status bar:', error);
978+
NotificationUtils.showNotificationPing('❌ Failed to toggle status bar visibility: ' + error, 'error');
979+
980+
// Revert the checkbox state on error
981+
const checkbox = document.getElementById('hide-status-bar');
982+
if (checkbox) {
983+
checkbox.checked = !enabled;
984+
}
985+
}
986+
}
987+
924988
// Theme management functions
925989
async applyTheme(theme) {
926990
const html = document.documentElement;

src/utils/theme-loader.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class ThemeLoader {
3939
// that gets updated by the build process or manually maintained
4040

4141
// This could be enhanced to use a build-time script that generates this list
42-
const knownThemes = [
42+
const knownThemes = [
4343
'espresso.css',
4444
'pipboy.css',
4545
'pommodore64.css'

0 commit comments

Comments
 (0)