PCFoundation¶
PCFoundation is the base layer of Peach Commander. It is the one module every
other module is allowed to depend on, and it depends on nothing inside the
project — only Foundation, os, Security, and CryptoKit from the platform.
It contains no AppKit: nothing here draws or touches a window, so the whole
module is usable from tests, command-line tools, and background actors.
Think of it as the project's standard library: value types, pure algorithms,
codecs, formatters, and the two stateful services that own persistence
(ConfigStore) and secrets (SecretStore). If a piece of logic is pure,
UI-free, and reusable, it belongs here.
Source: Sources/PCFoundation/ (58 Swift files). Tests: Tests/PCFoundationTests/.
Purpose and responsibility¶
PCFoundation exists to keep the higher layers thin and testable. Its responsibilities fall into a few groups:
- Configuration and persistence — the INI document model, the actor that reads/writes config atomically, and the resolver that decides where config lives.
- Secrets — a Keychain-backed credential store behind a protocol, with an in-memory implementation for tests.
- Formatting and parsing — byte sizes, dates, file lists, checksum files, and the many small codecs (Base64, uu/xx, hex rendering).
- Pure algorithms — Myers line/character diff, natural sort, wildcard and path matching, CRC-32/cryptographic hashing.
- Value models — the small
Sendablestructs that describe things like panel tabs, column sets, keymaps, hotlists, and rename masks, kept here (not inPCApp) precisely so their encode/decode round-trips can be unit-tested without a UI.
The guiding rule is: no I/O-hidden-in-a-getter, no global mutable state, no
AppKit. Anything that must do I/O (ConfigStore, PathCompleter) does it
explicitly and is either an actor or takes its inputs as parameters.
Position in the module graph¶
graph TD
PCApp --> PCFoundation
PCCommands --> PCFoundation
PCOperations --> PCFoundation
PCArchive --> PCFoundation
PCNet --> PCFoundation
PCPluginHost --> PCFoundation
PCVFS --> PCFoundation
PCApp -.also depends on.-> PCVFS
subgraph base
PCFoundation
end
Dependencies point down to PCFoundation. PCFoundation has no upward or
sideways dependencies — it never imports PCVFS, PCApp, or any plugin ABI.
Everything above it (PCVFS and, transitively, PCCommands, PCOperations,
PCArchive, PCNet, PCPluginHost, and PCApp) consumes it.
Public interfaces and key types¶
Configuration¶
INIDocument(INIDocument.swift) — an ordered, comment- and order-preserving INI model. Parsing never fails: unrecognized lines are retained verbatim as.commenttokens so nothing is silently dropped. Section and key matching is case-insensitive;set(_:section:key:)rewrites a key in place, inserts after the last key of an existing section, or appends a new section, always preserving surrounding comments and blank lines.serialized()round-trips byte-for-byte except for normalizing the trailing newline. This matters because config files are meant to be hand-edited and synced (see ADR-007).ConfigStore(ConfigStore.swift) — a Swift actor wrapping oneINIDocumentbehind typed accessors:bool/int/double/string(_:_:default:)for reads andsetBool/setInt/setDouble/setString(_:_:_:)for writes. Writes update memory immediately, broadcast aConfigChangeto subscribers, and schedule a debounced atomic write (default 1.0 s).changes()returns anAsyncStream<ConfigChange>for UI live-binding; multiple concurrent subscribers are supported.flush()forces an immediate write (e.g. on quit). A[meta] version=1key is ensured on first write to give future migrations an anchor.ConfigChange—Sendable{ section, key }value yielded on every write.ConfigPaths(ConfigPaths.swift) — resolves the config root and all well-known file URLs within it (mainConfig→peachcmd.ini,session,hotlist,workspaces,aliases,userCommands,buttonBar,userKeymap,mainMenu,pluginsConfig,columns,ftpSites, and JSON preset files).resolve(arguments:environment:)picks the root in priority order:-ConfigRoot <path>launch argument,PEACHCMD_CONFIG_ROOTenvironment variable,~/Library/Application Support/PeachCommander.
The resolved directory is created if missing. Both override paths take
injectable arguments/environment, which is how tests point the whole app
at an isolated temp directory (F-277). App config never uses
UserDefaults — only ConfigStore honors the override, and UserDefaults
would pollute the user's real preferences.
Secrets¶
SecretStore(SecretStore.swift) — protocol forsetPassword/password/deletePasswordkeyed by(service, account).KeychainSecretStore— production implementation overSecurity.frameworkgeneric-password items,kSecAttrAccessibleWhenUnlocked, idempotent writes (delete-then-add). ThrowsSecretStoreError(.unexpectedStatus(OSStatus)/.dataDecodingFailed).InMemorySecretStore— thread-safe (NSLock) test/preview double so credential round-trips can be verified without touching the real Keychain.
FTP/SFTP passwords and key passphrases live only in the Keychain, never in
any .ini file (ADR-007).
Formatting and codecs¶
ByteSize(PCFoundation.swift) — human-readable byte counts (.bytes,.kb,.mb,.bytesWithSep) plusByteSize.parse("1.5M")using binary units (1 K = 1024).ByteFormatter/ByteFormat(ByteFormatter.swift) — render a byte range as text, spaced hex, C array, Pythonbytes, or Base64 (hex-viewer "copy as…").Base64Codec(Base64Codec.swift) — RFC 4648 encode/decode with optional 76-column MIME wrapping.UUCodec(UUCodec.swift) — uuencode/xxencode encode and decode.ChecksumAlgorithm,ChecksumHasher,CRC32(ChecksumAlgorithm.swift) — CRC-32 (table-based IEEE 802.3, for.sfv) plus MD5/SHA-1/SHA-256/SHA-512 via CryptoKit, exposed as an incrementalChecksumHasherso large files stream chunk-by-chunk. Verified against published test vectors.ChecksumFile/ChecksumEntry(ChecksumFile.swift) — parse/generate SFV and coreutils-style (md5sum/shasum) checksum files.- Additional formatters:
PanelDateFormatter,FileListFormatter,SelectionSummaryFormatter,StructuredTextFormatter,MarkdownRenderer.
Pure algorithms¶
LineDiff(LineDiff.swift) — a classic Myers O(ND) shortest-edit diff.compare(left:right:options:)returns alignedDiffRows (.equal/.insert/.delete/.change), coalescing delete+insert runs into paired.changerows the way side-by-side viewers expect.intraLine(_:_:)gives grapheme-level differing ranges for intra-line highlighting.DiffOptionscontrols case, whitespace (WhitespaceMode), and CRLF-vs-LF normalization — normalization affects equality only; emitted rows always reference original indices. Deterministic and Foundation-only.naturalCompare(_:_:natural:)(PCFoundation.swift) — locale-aware, numeric-aware sort (file2<file10) vialocalizedStandardCompare; the TC "logical order" default (F-026).WildcardMask(PCFoundation.swift) — TC-style masks like"*.c;*.h|*.bak"(;separates patterns,|separates include/exclude), compiled to case-insensitive regex.PathUtils(PCFoundation.swift) —parent/filename/fileExtension, hidden-file check, Unicode NFC/NFD normalization and canonical-equivalence comparison (nameEquivalent), and the macOS colon/slash display mapping (POSIX:shown as/, F-100).PathResolver(PathResolver.swift) — lexically resolve a user-typed path (~expansion, relative-to-base,./..) without touching disk.PathCompleter(PathCompleter.swift) — directory-entry completion for the command line (reads a directory; the one path helper that does I/O).- Text helpers:
TextScanning(IdentifierScanner, bracket matching),ByteSearch,OccurrenceFinder,TypeAheadSearch,SpotlightPredicate.
Value models (encode/decode round-trips)¶
Kept in PCFoundation so their serialization is unit-testable without a UI:
PanelTabs/PanelTabState + WorkspaceCodec, ColumnSet, Keymap,
Hotlist, AliasStore, UserCommands, MenuFile, ButtonBar, CopyRenameMask
/ MultiRenameEngine / RenameValidator, SyncModel / SyncPresetStore,
NavigationHistory, ACLEntry, PosixPermissions, NetworkShare,
DescriptionFile, SymbolTree, XMLTree / XPathQuery, HexDocument,
WincmdImporter (reads Total Commander wincmd.ini).
Logging¶
PCFoundationLogger(PCFoundation.swift) — thinos.Loggerwrapper (subsystemcom.peachcommander), used module-internally (e.g. byConfigStorewhen a write fails or a config file is undecodable).
Inputs and outputs¶
- In: launch arguments and environment (
ConfigPaths.resolve), INI/JSON/ checksum text, raw byte buffers, line arrays, filesystem directory contents (PathCompleteronly). - Out: serialized INI text (atomic disk writes via
ConfigStore), Keychain items (KeychainSecretStore), formatted strings, diff rows, hashes,AsyncStream<ConfigChange>events, andos_logentries.
Lifecycle¶
ConfigPaths.resolve(...) runs once early in app startup to fix the config root
and create it if needed. Each well-known config file is then wrapped in a
long-lived ConfigStore actor: the initializer reads the file (or starts empty),
and — if the file exists but is not valid UTF-8 — moves it aside as <name>.bak
and starts fresh so a corrupt file never blocks launch. During the session,
writes accumulate in memory and are flushed by the debounce timer; flush() is
called on quit to guarantee the final state hits disk. Everything else in the
module is stateless (value types, enum namespaces of static functions) and
has no lifecycle of its own.
Threading and concurrency¶
Follows the project's Swift-Concurrency-first rule (ADR-008, no GCD in new code):
ConfigStoreis anactor— all reads/writes are serialized through it; callersawait. The debounced write is a childTaskthat is cancelled and rescheduled on each change, so a burst of edits coalesces into a single atomic I/O. Change notifications useAsyncStreamcontinuations stored per subscriber and cleaned up on stream termination.- Most types are pure value types marked
Sendable(INIDocument,ConfigPaths,DiffRow,ByteFormat,ChecksumAlgorithm,ConfigChange, the panel/keymap/column models), safe to pass across actor boundaries. SecretStoreisSendable;KeychainSecretStoreis a value type,InMemorySecretStoreis@unchecked Sendableguarded by anNSLock.ChecksumHasheris a reference type and is not thread-safe — it is meant to be driven by a single streaming consumer.
Error handling¶
- Parsing never throws.
INIDocument(parsing:)and the checksum-file parser treat unrecognized input as preserved comments rather than errors. - Config is fail-soft.
ConfigStorerecovers from an undecodable file (.bakrename), and every typed reader takes an explicitdefault:so a missing/garbage value degrades to a caller-chosen fallback rather than crashing. Write failures are logged viaPCFoundationLogger.error, not thrown to callers (writes are fire-and-forget under debounce). - Secrets throw explicitly.
SecretStoremethodsthrow SecretStoreErrorso credential failures are surfaced to the UI. - Codecs return optionals.
Base64Codec.decode,ByteSize.parse, and the path resolvers returnnilon malformed/empty input.
Testing¶
PCFoundation is the most thoroughly unit-tested module in the project — a
deliberate consequence of keeping logic pure and UI-free. There is roughly one
test file per source file under Tests/PCFoundationTests/ (60 test files),
covering the INI round-trip (INIDocumentTests), the debounced actor
(ConfigStoreTests), path normalization (PathNormalizationTests), the Myers
diff (LineDiffTests), checksums against known vectors
(ChecksumAlgorithmTests/ChecksumFileTests), the codecs, and every value-model
encode/decode round-trip. Secrets are tested through InMemorySecretStore so no
run touches the real Keychain. These tests are part of the project-wide battery
(~1304 tests across 9 targets) and run in CI on macos-14.
Extension points¶
- New config keys: add typed accessors on
ConfigStoreor a new URL onConfigPaths; no schema change is needed becauseINIDocumentis free-form and preserves unknown keys. - New checksum algorithm: add a case to
ChecksumAlgorithmand wire it intoChecksumHasher(file-extension and hex-width switches are exhaustive, so the compiler flags every site to update). - New copy-as / codec format: add a case to
ByteFormat/ByteFormatter, or a new codecenumfollowing theBase64Codec/UUCodecpattern. - Alternative secret backend: implement the
SecretStoreprotocol. - New value model: add a
Sendablestruct plus a codecenumhere (not inPCApp) so it stays unit-testable.
Open questions¶
ByteSize.formattedandByteFormattercurrently hardcode English unit labels ("bytes", "KB"); localization of these strings is handled at the app layer and PCFoundation does not yet expose a localized-formatting hook.- The
[meta] version=1marker written byConfigStoreis a forward-looking anchor; no migration framework consumes it yet.