Skip to content

Commit 6b02445

Browse files
committed
Feature: Update folder icons upon OS theme change
They used to become stale when color was updated in System Settings > Appearance > Theme > Color. Now they update immediately.
1 parent 7815d0f commit 6b02445

10 files changed

Lines changed: 65 additions & 10 deletions

File tree

apps/desktop/src-tauri/src/commands/icons.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,10 @@ pub fn refresh_directory_icons(
3636
pub fn clear_extension_icon_cache() {
3737
icons::clear_extension_icon_cache();
3838
}
39+
40+
/// Clears cached directory icons (`dir`, `symlink-dir`, `path:*`).
41+
/// Called when the system theme or accent color changes.
42+
#[tauri::command]
43+
pub fn clear_directory_icon_cache() {
44+
icons::clear_directory_icon_cache();
45+
}

apps/desktop/src-tauri/src/file_system/volume/local_posix.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
use super::{
44
CopyScanResult, ScanConflict, SourceItemInfo, SpaceInfo, Volume, VolumeError, VolumeScanner, VolumeWatcher,
55
};
6-
use crate::file_system::listing::{FileEntry, get_single_entry, list_directory_core, list_directory_core_with_progress};
6+
use crate::file_system::listing::{
7+
FileEntry, get_single_entry, list_directory_core, list_directory_core_with_progress,
8+
};
79
use crate::indexing::scanner::{self, ScanConfig, ScanError, ScanHandle, ScanSummary};
810
use crate::indexing::watcher::{DriveWatcher, FsChangeEvent, WatcherError};
911
use crate::indexing::writer::IndexWriter;

apps/desktop/src-tauri/src/icons.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,17 @@ pub fn clear_extension_icon_cache() {
5757
}
5858
}
5959

60+
/// Clears all cached icons for directory entries (`dir`, `symlink-dir`, `path:*`).
61+
/// Called when the system theme or accent color changes, since macOS folder icons
62+
/// are tinted by the current appearance.
63+
pub fn clear_directory_icon_cache() {
64+
ensure_cache();
65+
let mut cache = ICON_CACHE.write().unwrap();
66+
if let Some(ref mut map) = *cache {
67+
map.retain(|key, _| key != "dir" && key != "symlink-dir" && !key.starts_with("path:"));
68+
}
69+
}
70+
6071
/// Converts an image to a base64 WebP data URL.
6172
fn image_to_data_url(img: &DynamicImage) -> Option<String> {
6273
// Resize to configured size

apps/desktop/src-tauri/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,7 @@ pub fn run() {
473473
commands::icons::get_icons,
474474
commands::icons::refresh_directory_icons,
475475
commands::icons::clear_extension_icon_cache,
476+
commands::icons::clear_directory_icon_cache,
476477
commands::ui::show_file_context_menu,
477478
commands::ui::show_main_window,
478479
commands::ui::update_menu_context,

apps/desktop/src/lib/accent-color.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import { invoke } from '@tauri-apps/api/core'
1212
import { listen, type UnlistenFn } from '@tauri-apps/api/event'
1313
import { getAppLogger } from '$lib/logging/logger'
14+
import { clearDirectoryIconCache } from '$lib/icon-cache'
1415

1516
const log = getAppLogger('accent-color')
1617

@@ -38,6 +39,9 @@ export async function initAccentColor(): Promise<void> {
3839
try {
3940
unlisten = await listen<string>('accent-color-changed', (event) => {
4041
applyAccentColor(event.payload)
42+
// macOS renders folder icons with the accent color baked in,
43+
// so we need to flush cached folder bitmaps and re-fetch them.
44+
void clearDirectoryIconCache()
4145
log.info('System accent color changed: {hex}', { hex: event.payload })
4246
})
4347
} catch (error) {

apps/desktop/src/lib/file-explorer/views/BriefList.svelte

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import { getSetting } from '$lib/settings/settings-store'
2323
import { formatNumber, pluralize } from '../selection/selection-info-utils'
2424
import { isScanning } from '$lib/indexing/index-state.svelte'
25-
import { extensionCacheCleared } from '$lib/icon-cache'
25+
import { iconCacheCleared } from '$lib/icon-cache'
2626
import type { RenameState } from '../rename/rename-state.svelte'
2727
2828
interface Props {
@@ -421,9 +421,9 @@
421421
prevContainerHeight = height
422422
})
423423
424-
// Re-fetch icons when the extension icon cache is cleared (settings change)
424+
// Re-fetch icons when the icon cache is cleared (settings or theme change)
425425
$effect(() => {
426-
void $extensionCacheCleared // Track the store value
426+
void $iconCacheCleared // Track the store value
427427
// Re-fetch icons for all cached entries
428428
if (cachedEntries.length > 0) {
429429
refetchIconsForEntries(cachedEntries)

apps/desktop/src/lib/file-explorer/views/FullList.svelte

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
formatDateTime,
3232
formatFileSize,
3333
} from '$lib/settings/reactive-settings.svelte'
34-
import { extensionCacheCleared } from '$lib/icon-cache'
34+
import { iconCacheCleared } from '$lib/icon-cache'
3535
import type { RenameState } from '../rename/rename-state.svelte'
3636
3737
interface Props {
@@ -337,9 +337,9 @@
337337
return getVisibleItemsCountUtil(containerHeight, rowHeight)
338338
}
339339
340-
// Re-fetch icons when the extension icon cache is cleared (settings change)
340+
// Re-fetch icons when the icon cache is cleared (settings or theme change)
341341
$effect(() => {
342-
void $extensionCacheCleared // Track the store value
342+
void $iconCacheCleared // Track the store value
343343
// Re-fetch icons for all cached entries
344344
if (cachedEntries.length > 0) {
345345
refetchIconsForEntries(cachedEntries)

apps/desktop/src/lib/icon-cache.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
getIcons,
77
refreshDirectoryIcons as refreshIconsCommand,
88
clearExtensionIconCache as clearExtensionIconCacheCommand,
9+
clearDirectoryIconCache as clearDirectoryIconCacheCommand,
910
} from './tauri-commands'
1011

1112
const STORAGE_KEY = 'cmdr-icon-cache'
@@ -20,10 +21,11 @@ const memoryCache = new Map<string, string>()
2021
export const iconCacheVersion = writable(0)
2122

2223
/**
23-
* Reactive counter that increments when extension icon cache is cleared.
24+
* Reactive counter that increments when part of the icon cache is cleared
25+
* (extension icons, directory icons, etc.).
2426
* List components subscribe to this to re-fetch icons for visible files.
2527
*/
26-
export const extensionCacheCleared = writable(0)
28+
export const iconCacheCleared = writable(0)
2729

2830
/** Load persisted cache from localStorage */
2931
function loadFromStorage(): void {
@@ -153,8 +155,27 @@ export async function clearExtensionIconCache(): Promise<void> {
153155
// Notify list components to re-fetch icons for visible files
154156
// This must happen BEFORE incrementing iconCacheVersion so components
155157
// can re-fetch before re-rendering with the cleared cache
156-
extensionCacheCleared.update((v) => v + 1)
158+
iconCacheCleared.update((v) => v + 1)
157159

158160
// Trigger reactive update so components re-fetch icons
159161
iconCacheVersion.update((v) => v + 1)
160162
}
163+
164+
/**
165+
* Clears all cached directory icons from both memory and localStorage.
166+
* Called when the system theme or accent color changes, since macOS renders
167+
* folder icons with the current accent color baked in.
168+
*/
169+
export async function clearDirectoryIconCache(): Promise<void> {
170+
await clearDirectoryIconCacheCommand()
171+
172+
for (const key of memoryCache.keys()) {
173+
if (key === 'dir' || key === 'symlink-dir' || key.startsWith('path:')) {
174+
memoryCache.delete(key)
175+
}
176+
}
177+
178+
saveToStorage()
179+
iconCacheCleared.update((v) => v + 1)
180+
iconCacheVersion.update((v) => v + 1)
181+
}

apps/desktop/src/lib/tauri-commands/file-viewer.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,14 @@ export async function clearExtensionIconCache(): Promise<void> {
162162
await invoke('clear_extension_icon_cache')
163163
}
164164

165+
/**
166+
* Clears cached directory icons (`dir`, `symlink-dir`, `path:*`).
167+
* Called when the system theme or accent color changes.
168+
*/
169+
export async function clearDirectoryIconCache(): Promise<void> {
170+
await invoke('clear_directory_icon_cache')
171+
}
172+
165173
/**
166174
* Shows a native context menu for a file.
167175
* @param path - Absolute path to the file.

apps/desktop/src/lib/tauri-commands/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export {
3636
getIcons,
3737
refreshDirectoryIcons,
3838
clearExtensionIconCache,
39+
clearDirectoryIconCache,
3940
showFileContextMenu,
4041
updateMenuContext,
4142
toggleHiddenFiles,

0 commit comments

Comments
 (0)