Skip to content

Sephyi/finderconf

Repository files navigation

finderconf

Capture, version, and apply macOS Finder view settings as TOML profiles.

Why

No existing tool manages per-folder Finder views. Each project root — list vs icon, column widths, sort order — has to be hand-configured, and the settings vanish when you clone the repo on a new machine. finderconf captures a folder's view as a named, human-editable TOML profile and applies it recursively to any directory tree. Global Finder preferences (com.apple.finder.plist) are also supported through the same profile format.

Install

# Recommended: isolated install via uv
uv tool install finderconf

# Or run without installing
uvx finderconf

Your first five minutes

  1. Set up a reference folder. Open any folder in Finder and adjust the view exactly how you want your future folders to look: list/icon/column/gallery, column widths, sort order, arrange-by, icon size.
  2. Capture it as a profile. Finder has to have recorded view state for the folder first — any in-Finder view change does that. The settings don't live inside the folder itself; Finder writes them into the parent folder's .DS_Store keyed by the child's name, so as long as the reference folder's parent is user-writable, adjusting any view option is enough. Then:
    finderconf export ~/dev/my-repo project-root
    The profile lands in ~/.config/finderconf/profiles/project-root.toml — open it, read it, tweak it.
  3. Preview where you'll apply it. Always dry-run before a recursive apply:
    finderconf apply project-root ~/dev --depth 1 --dry-run
    The table shows every directory that would receive the profile and whether each would write, skip (filter mismatch), or warn (iCloud/network volume).
  4. Apply it. Same command without --dry-run. The writes all land in the target's own .DS_Store (because that's the parent of every candidate), that file is backed up once to ~/.local/state/finderconf/backups/ before the first write, and Finder is restarted after the batch so the changes become visible immediately.
  5. If something looks off, revert. finderconf restore <path> --last rolls that folder back to its most recent backup.

Prefer a guided wizard? finderconf init walks you through creating a profile without capturing from an existing folder.

Commands

Command Description
finderconf export <path> <name> Capture a folder's .DS_Store into a named profile
finderconf apply <name> <path> Apply a profile recursively to a directory tree
finderconf list List saved profiles; --verbose shows sections and timestamps
finderconf show <path> Inspect a live folder's Finder settings
finderconf edit <name> Open a profile in $EDITOR; validates on exit
finderconf set <name> <key> <value> Set a single profile value via dot-path (preserves comments)
finderconf diff <name> <path> Colored diff between a profile and a live folder
finderconf delete <name> Remove a profile; --force skips confirmation
finderconf init Interactive wizard to create a new profile
finderconf restore <path> Re-apply last backup or clean .DS_Store
finderconf global export <name> Snapshot com.apple.finder.plist into a profile's [global]
finderconf global apply <name> Write [global] section via defaults write + restart Finder
finderconf doctor Check environment: iCloud paths, DS_Store lib version, backups

Profile format

Profiles live in ~/.config/finderconf/profiles/<name>.toml. All sections are optional — applying a profile only touches the domains present in it.

Per-folder view settings

[meta]
name        = "project-root"
description = "Rust/TS project roots"
schema      = 1

[view]
# list | icon | column | gallery
mode = "list"

[icon_view]
icon_size         = 64
text_size         = 12
label_on_bottom   = true
show_icon_preview = true
arrange_by        = "name"   # none|name|dateModified|dateCreated|size|kind|label

[list_view]
icon_size      = 16
text_size      = 13
sort_column    = "dateModified"
sort_ascending = false

[[list_view.columns]]
id = "name";         visible = true;  width = 300
[[list_view.columns]]
id = "dateModified"; visible = true;  width = 180
[[list_view.columns]]
id = "size";         visible = true;  width = 100

[window]
sidebar_width  = 200
show_sidebar   = true
show_toolbar   = true
show_path_bar  = true
show_status_bar = true

Global Finder preferences

The [global] section maps to ~/Library/Preferences/com.apple.finder.plist and is only touched by finderconf global apply <name> or finderconf apply <name> <path> --as-default.

[global]
show_hidden_files                        = false   # AppleShowAllFiles
show_all_extensions                      = true    # AppleShowAllExtensions
default_view_style                       = "list"  # list|icon|column|gallery
new_window_target                        = "home"  # home|desktop|documents|icloud|recents|custom
new_window_target_path                   = ""      # required when custom
default_search_scope                     = "current" # current|this_mac|previous
remove_trash_items_after_days            = 30      # 0 disables auto-cleanup
show_warning_before_emptying_trash       = true
show_warning_before_removing_from_icloud = true
keep_folders_on_top_in_windows           = true
keep_folders_on_top_when_sorting         = true

How it works

  • Per-folder view settings live in the parent directory's .DS_Store, keyed by the child's basename — that's where Finder itself reads and writes them on macOS Sonoma+. finderconf uses the maintained ds_store Python library (v1.3.2+) with plistlib for binary plist blobs. For edge cases where the parent isn't user-writable (e.g. ~/ when /Users/ is not writable), settings are read and written in the folder's own .DS_Store under filename "." as a fallback.
  • Global Finder preferences live in ~/Library/Preferences/com.apple.finder.plist; finderconf reads via plistlib and writes via defaults write subprocess to avoid cfprefsd cache-staleness issues.
  • Profiles are TOML files in ~/.config/finderconf/profiles/, round-tripped via tomlkit so hand-written comments survive finderconf set edits.

What apply actually does

A recursive apply is a five-step pipeline; every step is observable and reversible:

  1. Resolve + guard. The target path is resolved through symlinks and rejected if it is a system path (/System, /Library, /usr, …) or — unless --allow-outside-home is passed — outside $HOME.
  2. Plan. A BFS walk collects candidate directories, respects --depth and --filter, skips hidden folders, and flags iCloud/network/Time Machine paths as warnings (skipped without --force).
  3. Lock. An exclusive fcntl.flock on ~/.local/state/finderconf/.lock prevents two concurrent applies from racing.
  4. Merge writes into each parent's .DS_Store. Every candidate's records land in its parent's .DS_Store, keyed by the candidate's basename. Before the first write touches a given parent file, that file (if present) is backed up once to ~/.local/state/finderconf/backups/. The write itself reads the existing store, drops only the managed-section codes (vstl, icvp, lsvp, clmv, Flwv, bwsp) for the target basename, inserts the new records, and atomically renames (.DS_Store.tmp.<pid>os.replace). Records for other siblings, and non-managed codes like Iloc, moDD, and vSrn, are preserved verbatim. A per-candidate failure is captured without stopping the batch.
  5. Restart Finder. killall Finder (Finder respawns automatically) so the new view settings are visible immediately. Skipped when --dry-run or --no-restart.

Safety

  • System paths refused: targets resolving to /, /System, /Library, /usr, /bin, /sbin, /etc, /var, /private, or /Applications are rejected after symlink expansion.
  • Path traversal guard: target must resolve inside $HOME unless --allow-outside-home is passed.
  • Atomic writes: .DS_Store is written to .DS_Store.tmp.<pid> in the same directory as the target and then renamed with os.replace(); profile TOML uses the same pattern.
  • Record-level merge: writing a profile for a directory replaces only the managed-section records for that directory's basename in the parent's .DS_Store. Records for sibling folders and non-managed codes (Iloc, moDD, vSrn, …) are preserved.
  • Backup on write (default): before the first destructive change to a given parent .DS_Store, that file is copied to ~/.local/state/finderconf/backups/. Each parent is snapshotted once per apply run, regardless of how many siblings are being written. Use finderconf restore to revert. Skip with --no-backup.
  • Volume awareness: iCloud Drive paths, non-local network mounts, and Time Machine volumes trigger a warning and require --force to proceed.
  • Dry-run honesty: --dry-run writes nothing, backs up nothing, does not restart Finder, and prints a Rich table of planned changes.
  • Concurrency: a file lock at ~/.local/state/finderconf/.lock prevents two recursive applies from racing.

Requirements

  • macOS Sonoma (14) or later
  • Python 3.14 or later
  • Finder must be running for applied settings to take effect immediately

Limitations

  • iCloud Drive: .DS_Store sync behavior is inconsistent; doctor warns but cannot guarantee correctness.
  • Time Machine volumes: effectively read-only for .DS_Store writes.
  • Non-local network volumes: DSDontWriteNetworkStores may silently suppress writes; doctor checks this key.
  • Per-file icon positions (Iloc blobs) are not managed in v1.
  • Background image/color application is exposed in the schema but not written in v1 (read-only).

Troubleshooting

No Finder view settings found for <path> — Finder records view state in the parent folder's .DS_Store keyed by the child's basename, and writes it lazily. A folder with no .DS_Store nearby — or a .DS_Store that doesn't yet have a record for this specific child — triggers this error. Open the folder in Finder, change any view option (toggle a column, resize one), then retry finderconf export. For directories whose parent isn't user-writable (e.g. ~/), Finder falls back to the folder's own .DS_Store under filename "."; finderconf checks both locations.

Settings don't update after apply — Ensure Finder is running (finderconf doctor reports this). If you ran with --no-restart, kill Finder manually: killall Finder.

another finderconf apply is running — A stale lock file from a crashed run. Remove it: rm ~/.local/state/finderconf/.lock.

warn-volume on a folder I want to configure — The path resolves under iCloud Drive, a /Volumes/* mount, or a Time Machine backup. Pass --force if you really want to proceed (expect inconsistent behavior).

Refusing to write to system path — The target resolves to (or under) a protected system directory. This guard cannot be disabled.

Path is outside $HOME — The target is on a mounted volume or /tmp. Pass --allow-outside-home to override. .DS_Store behaviour on external volumes varies by filesystem and volume type.

Development

git clone https://github.com/Sephyi/finderconf.git
cd finderconf
uv sync
uv run pytest
uv run ruff check
pre-commit install

Running the CLI from a local clone

# Fast iteration (editable install via uv sync)
uv run finderconf --help
uv run finderconf export ~/dev/foo bar

# As a module (good for pdb)
uv run python -m finderconf --help

# Install on $PATH pointing at this tree
uv tool install --from . finderconf
# Re-run with --force after code changes to pick them up.

# One-shot ephemeral run
uvx --from . finderconf --help

See CONTRIBUTING.md for commit style, TDD expectations, and PR guidelines.

License

MIT — see LICENSE.

Acknowledgements

About

macOS CLI that captures Finder view settings (list/icon/column layout, widths, sort) as TOML profiles, applied recursively to any directory tree.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Contributors

Languages