Wallpaper Switcher is a macOS application (SwiftUI + SPM) that helps users select wallpapers and apply them using pywal. It features a grid-based wallpaper browser with sorting, search, keyboard navigation, and integration with the pywal color scheme system.
Platform: macOS 14+ Language: Swift 6.2 Package Manager: Swift Package Manager (SPM) Dependencies: None (standalone SPM project)
wal-pick/
├── Sources/
│ ├── ImagePicker/ # Main application logic
│ │ ├── EmptyFileDocument.swift
│ │ ├── OptimizedCache.swift
│ │ ├── RandomOverlayView.swift
│ │ ├── SettingsView.swift
│ │ ├── ThumbnailCache.swift
│ │ ├── Types.swift
│ │ └── WallpaperSwitcherView.swift
│ └── App/
│ └── AppMain.swift # App entry point
├── Tests/
│ └── ImagePickerTests/
│ └── WallpaperSwitcherViewModelTests.swift
├── Resources/
│ └── Fonts/
│ ├── NunitoSans-Variable.ttf
│ └── NunitoSans.zip
├── docs/
│ ├── OverlayConstraints.md
│ └── plans/
├── build_app.sh # Create macOS app bundle
├── test_wal.sh # Test wal binary
├── debug_wal.sh # Debug wal path
└── Package.swift # SPM package definition
swift build # Debug build
swift build --configuration release # Release build./build_app.sh
# Creates: WallpaperSwitcher.app/Contents/MacOS/WallpaperSwitcher./test_wal.sh
# Tests wal binary execution with dummy file./debug_wal.sh
# Checks wal binary existence, permissions, and executionswift testrm -rf .build- ImagePicker (target): Core logic, shared types, caching, UI components
- App (target): App entry point, window configuration, font registration
- ImagePickerTests (test target): Unit tests
ImageFile: Represents a wallpaper with id, url, name, dateModified, fileSizeSortOption: Enum for sorting (Name, Date Modified, File Size)NavigationDirection: Enum for keyboard navigation (left, right, up, down)AppConfig: Configuration with defaults, Codable persistenceSettingsManager: ObservableObject managing config
- Main UI with grid layout, search, sorting
- Keyboard navigation (arrow keys, Enter)
- Wallpaper selection and wal execution
- Random wallpaper overlay
- Shell command execution for wal/pywalfox
- Configuration UI for wallpaper folder, wal path, grid columns
- Pywalfox integration toggle
ThumbnailCache: Disk + memory cache for thumbnails (16:9 ratio)OptimizedImageCache: In-memory NSCache for images
runShellCommand(): Async shell execution with environment PATH- Logs to Desktop/wallpaper_switcher.log
- Types: PascalCase (e.g.,
WallpaperSwitcherView,SettingsManager) - Methods/Properties: camelCase (e.g.,
loadWallpapers,wallpaperFolderPath) - Singletons:
sharedsuffix (e.g.,ThumbnailCache.shared) - Managers:
Managersuffix (e.g.,SettingsManager) - Views:
Viewsuffix (e.g.,SettingsView,RandomOverlayView) - Caches:
Cachesuffix (e.g.,ThumbnailCache,OptimizedImageCache)
@StateObjectfor view-level state (e.g., viewModel)@ObservedObjectfor external state (e.g., settingsManager)@EnvironmentObjectfor shared state across views@Environment(\.openWindow)for window management@FocusStatefor keyboard focus management
.background(.ultraThinMaterial)- Glass morphism effect.clipShape(RoundedRectangle(...))- Rounded corners.onKeyPress(...)- Keyboard input handling.transition(...)- View animations
- Use
asyncfunctions for shell commands and file operations Task { ... }for background workawait MainActor.run { ... }for UI updates on main thread
@MainActor
final class ViewModelTests: XCTestCase {
func testSomething() {
// Test code
}
}WallpaperSwitcherViewModelTests.swift: Tests search query behavior
Tests verify wal binary execution, not UI behavior.
- Location:
~/Library/Application Support/ImagePicker/config.json - Config:
AppConfigstruct (Codable) - Auto-save:
SettingsManager.configsetter triggers save - Load on init:
SettingsManagerloads config in init
- WARNING: Multiple concurrent access warnings in
ThumbnailCache(lines 1084, 1088)- Capturing
imageFilesin concurrent dispatch queue - Consider using actor or proper synchronization
- Capturing
- All caches are
@unchecked Sendable- verify thread safety - Use
@MainActorfor UI-related code
- WARNING: Unreachable catch in
WallpaperSwitcherView.swift:1097doblock doesn't throw, making catch unreachable- Remove or simplify error handling
- Required: wal binary path must be configured
- Fallback paths: Checks common locations if configured path missing
- Permissions: Must be executable (755)
- Command format:
wal -i <dummy-file> -n - Logs: Debug logs written to Desktop/wallpaper_switcher.log
- Dummy file: Configurable, defaults to
~/Pictures/dummy-file.jpg - Thumbs cache:
~/Library/Caches/ImagePicker/Thumbnails/ - App config:
~/Library/Application Support/ImagePicker/config.json
- Focus: Grid is auto-focused on appear (0.1s delay)
- Keys: Arrow keys move selection, Enter selects
- Command+F: Focus search field
- pywalfox: Optional Firefox theme updater (toggle in Settings)
- Accent color: Extracted from wal colors (index 7)
- System reload: Requires killing WallpaperAgent, Dock, ControlCenter
- Console:
print()statements for debugging - File:
/tmp/wallswitcherlogs/app.log - Debug: Desktop/wallpaper_switcher.log (wallpaper switching process)
- One type per file (no large files with multiple types)
- Public types first, then private helpers
- Comments at top of file for module-level documentation
@Publishedfor observable properties@Statefor local view state@Bindingfor two-way binding@Environmentfor dependency injection
Task {
do {
// Async work
let result = await someAsyncFunction()
await MainActor.run {
// UI update
self.uiProperty = result
}
} catch {
// Error handling
}
}private func runShellCommand(_ command: String) async -> Bool {
let task = Process()
task.executableURL = URL(fileURLWithPath: "/bin/zsh")
task.arguments = ["-c", command]
let pipe = Pipe()
task.standardOutput = pipe
task.standardError = pipe
// Configure environment with common paths
var environment = ProcessInfo.processInfo.environment
environment["PATH"] = existingPath + ":/usr/local/bin:/opt/homebrew/bin"
try? task.run()
let output = String(data: pipe.fileHandleForReading.readToEnd() ?? Data(), encoding: .utf8)
task.waitUntilExit()
return task.terminationStatus == 0
}class SomeCache: @unchecked Sendable {
static let shared = SomeCache()
private init() {
// Initialize
}
func doSomething() {
// Thread-safe operation
}
}@MainActor
public struct AppConfig: Codable, Sendable {
static let `default` = AppConfig(/* defaults */)
private static let configURL = URL(fileURLWithPath: "path/to/config.json")
@MainActor public static func load() -> AppConfig {
// Load from file or return default
}
@MainActor public func save() {
// Save to file
}
}.onKeyPress(.leftArrow) {
viewModel.moveSelection(direction: .left, columns: config.gridColumns)
return .handled
}
.onKeyPress(.rightArrow) {
viewModel.moveSelection(direction: .right, columns: config.gridColumns)
return .handled
}
// ... etc.task {
if let nsImage = await OptimizedImageCache.shared.loadThumbnail(
for: wallpaper.url,
size: CGSize(width: cardWidth, height: imageHeight)
) {
thumbnailImage = Image(nsImage: nsImage)
}
}# Check if wal exists
ls -la /usr/local/bin/wal
ls -la /opt/homebrew/bin/wal
# Test wal
./test_wal.sh
# Check permissions
stat -f "%A" /usr/local/bin/walcat ~/Desktop/wallpaper_switcher.log
cat /tmp/wallswitcherlogs/app.logcat ~/Library/Application\ Support/ImagePicker/config.jsonswift build
swift test- OverlayConstraints.md: Layout constraints for overlays
- plans/: Feature design documents (keyboard navigation)
- README.md: Original ImagePicker documentation (outdated)
- Fix warnings first: Address Sendable and unreachable catch warnings before major changes
- Thread safety: Be careful with concurrent access to shared state
- Wal integration: Changes to wal execution require testing on macOS
- Keyboard navigation: New keyboard features need focus state management
- Cache cleanup: Thumbnail cache should be cleaned periodically (call
cleanupOldCache()) - Logging: Use both console print() and file logging for debugging
- Settings persistence: Config auto-saves on change - don't forget to save explicitly
- UI updates: Always use
await MainActor.runfor UI updates from background tasks - App bundle: Use
./build_app.shfor creating distributable app, notswift builddirectly - Environment: PATH includes
.local/bin,/usr/local/bin,/opt/homebrew/binfor wal