Releases: goodbug89/Unifyl.app
Unifyl 1.4.0
[1.4.0] — 2026-05-29
Pre-1.4.0 Pro users — your Pro stays Pro. If you bought Pro via
LemonSqueezy before 1.4.0, the license restores from Keychain on
first launch of the new build exactly as before; no action required,
no re-activation, no risk. The freemium switch only changes what
NEW users see — anyone who already paid keeps every Pro feature
they already had. We tested this end-to-end against the existing
LicenseManager.checkOnLaunch()+ 7-day-grace-period path.
Added
- Freemium business model — Unifyl is now free to download. Anyone can install Unifyl (from the Direct DMG at unifyl.app or from the Mac App Store) and use the full free tier: dual / triple / free-split pane workflow, command palette, F1–F8 function bar, FTP / SFTP / cloud-mount sidebar, ZIP read, basic search, bookmarks, inline terminal, file history, checksum + permissions view, git status badges, plus every CJK power-user feature already shipped (ALZ archive extraction, HWPX inline preview, Pinyin / Romaji / Hangul-Hanja multilingual search, full-width ↔ half-width folding, EUC-KR / Shift-JIS / GBK encoding conversion, Korean gov form heuristic detection), all 15 UI locales including RTL Arabic, all 12 built-in themes. The 50+ Pro features stay gated behind a single one-time purchase. See
docs/business/freemium-tiers.mdfor the full split. - Mac App Store distribution — Pro via In-App Purchase. The MAS-Lite build (1.3.1+) was always sandboxed-and-ready architecturally; 1.4.0 adds the StoreKit 2 path to Pro. New
MASLicenseProvider(Unifyl/Services/MASLicenseProvider.swift) observesTransaction.currentEntitlementsfor the non-consumable IAPcom.unifyl.app.maslite.pro(Tier 40 / $39.99, Family Sharing enabled). On every launch + after every purchase / refund / "Restore Purchases" tap, the provider re-resolves ownership and pushes the result intoLicenseManager.storeKitTier. TheUpgradeSheeton MAS-Lite now shows the localized App Store price (viaProduct.displayPrice) and routes theBuy Probutton toProduct.purchase()instead of the Direct LemonSqueezy checkout — Direct builds still use LemonSqueezy unchanged (#if MAS_LITEkeeps the two paths cleanly separated). A newRestore Purchasesbutton covers the "I bought it on a different Mac" flow. Setup guide atdocs/business/mas-iap-setup.mdcovers the App Store Connect IAP record + sandbox testing + review submission. LicenseManager.activatedTieris nowmax(lemonSqueezyTier, storeKitTier). Both unlock paths feed the sameFeatureGateManager.isUnlocked(.x)runtime gate; whichever resolves to Pro wins. Pre-1.4.0 Pro users (LemonSqueezy purchase) are entirely unaffected — the LemonSqueezy validate-on-launch + 7-day grace period + offline-keychain restore all keep working untouched. A future MAS-to-Direct (and reverse) recognition layer is sketched indocs/business/freemium-tiers.mdbut not part of 1.4.0.
Changed
- Marketing positioning shifts from "paid app" to "free with optional Pro upgrade". Landing-page CTA, README intros, App Store description, and the in-app trial banner copy all need a coordinated refresh; tracked separately from this code release.
Unifyl 1.3.2
[1.3.2] — 2026-05-27
Changed
- MAS-Lite backlog reaches zero
[drop-pending-lib]— every Process() site in the app target is now either truly in-process migrated or a documented[drop]. Two final-pass moves:- FileXRayView.ZipArchiveReader.entries migrated to
InProcessZipReader— was previously[drop]returning empty in MAS-Lite (and/usr/bin/zipinfosubprocess in Direct). Now both targets share one in-process path; the FileXRay ZIP analysis tab works in MAS-Lite, and Direct also drops the zipinfo spawn. Mapping layer betweenInProcessZipReader.Entry(UInt32 sizes) and the localXRayNode-shapedEntry(UInt64). FileXRayView gains animport UnifylFileSystem. - Git (3 sites) + MediaConverter ffmpeg-check (1 site) reclassified
[drop-pending-lib]→[drop]. Both families need work beyond a single sprint: Git needs a libgit2 SPM dependency (SwiftGit2 / ObjectiveGit — license vetting, MAS submission review of the bundled libgit2 binary, Swift 6 strict-concurrency audit, and full operation mapping);which ffmpeghas no Foundation equivalent and MAS forbids unsigned CLI binaries inResources/. The comments now spell out the deferral rationale so a future contributor sees what the lift would be instead of seeing "pending-lib" and assuming it's small. MAS-Lite users see localized "Git integration is not available in the Mac App Store variant — use unifyl.app for git" / "ffmpeg unavailable" messages exactly as in the prior pass; no UX change. - MAS-INCOMPAT site breakdown: 17
[drop], 0[drop-pending-lib], 0[lib]. EveryProcess()spawn in the MAS-Lite binary is documented and either compiled out via#if MAS_LITEor routed through an in-process equivalent. The 6 in-process equivalents added across 1.3.x-Unreleased sprints: pure-Swift content search (grep → FileManager enumerator),InProcessZipReader(ditto-on-xlsx → in-process xlsx preview),InProcessZipWriter(zip → Compress dialog),InProcessTarWriter(tar / tar.gz → Compress dialog),Diff3Merger(diff3 → 3-way merge),PDFConvertSheet.textutil→NSAttributedString. UnifylFileSystem test count: 102 → 128 (26 new tests for the new libraries).
- FileXRayView.ZipArchiveReader.entries migrated to
Added
- New
InProcessMachOReader+InProcessDMGReader(Packages/UnifylFileSystem) + FileXRay deep parsers now sandbox-safe. Pure-Swift Mach-O parser (~280 LOC) replacing/usr/bin/otool -l: parses magic (32/64/fat-universal, both endianness), header (cputype/cpusubtype/filetype/ncmds/flags), and load commands with typed per-LC interpretation (LC_SEGMENT[64] names, LC_LOAD_DYLIB paths + versions, LC_UUID canonical string, LC_RPATH, LC_VERSION_MIN*, LC_BUILD_VERSION platform + version, LC_MAIN entry-point offset). Returns a structuredAnalysis { isFat, architectures, slices }so the FileXRay tree view can group fat slices and show typed fields per LC instead of text-scraping otool output. CompanionInProcessDMGReaderdetects UDIF disk images by the trailing "koly" magic and reads the resource-file trailer (total size + data fork + resource fork). Deep DMG inspection (partition tables, encrypted volumes) needs hdiutil + privileges and isn't sandbox-compatible — out of scope.FileXRayView.analyzeMachO+analyzeDiskImagemigrated — both targets share the new in-process path, the now-unusedrunProcess(path:arguments:)removed entirely from the file. The Mach-O tree view actually surfaces more information than the previous otool text-scrape (fat slices are first-class, per-LC fields are typed). 7 new tests against system/bin/ls(fat universal binary fixture) + synthetic UDIF trailer + truncated-file rejection. UnifylFileSystem test count 128 → 135. MAS-INCOMPAT site count drops by 2 (otool + hdiutil sites collapse into one shared[drop]-comment block now covering only mdls, which the codebase doesn't actually call). - New
Diff3Merger(Packages/UnifylFileSystem) + 3-way merge now sandbox-safe. Pure-Swift LCS-based 3-way merge replacing/usr/bin/diff3 -min MAS-Lite. ~250 LOC. Algorithm: compute LCS(base, left) and LCS(base, right) — each gives a monotone (base_idx, other_idx) pair sequence — then take the intersection on base index as the stable anchor set; everything between anchors is a hunk classified by comparing the base / left / right slices (all-same → emit base, one-side-changed → take that side, both-changed-identically → take the change, otherwise → emit<<<<<<< LEFT … ======= … >>>>>>> RIGHTmarkers identical todiff3 -msoThreeWayMergeView.parseConflictsreads the output unchanged).ThreeWayMergeView.analyzeMerge#if MAS_LITEbranch migrated — reads file bytes off-main viaTask.detached, callsDiff3Merger.merge, returns the merged text + conflict list to the existing UI. Direct keeps/usr/bin/diff3. 8 new tests: no-change / left-only / right-only / both-same / true-conflict / non-overlapping changes / left-side insertion / marker round-trip. Line-granular (not character-level), O(N×M) memory — covers source-file 3-way merges up to a few thousand lines per side. UnifylFileSystem test count 120 → 128. MAS-INCOMPAT[drop-pending-lib]backlog: 5 → 4. - New
InProcessTarWriter(Packages/UnifylFileSystem) + Compress dialog tar / tar.gz paths now sandbox-safe. Companion to InProcessZipWriter: ~250 lines covering POSIX USTAR (512-byte header per entry with octal-text size + mtime + mode + 8-byte checksum; payload zero-padded to 512-byte boundary; two trailing zero blocks marking end-of-archive) plus an optional gzip wrapper (10-byte header + raw DEFLATE viacompression_encode_buffer(COMPRESSION_ZLIB)+ 8-byte CRC-32 / ISIZE footer per RFC 1952). Round-trips through host/usr/bin/tar(-tflists every entry,-xfrestores byte-for-byte; same for-tzf/-xzfon the gzip path). Long-path handling splits paths > 100 chars into USTARprefix+nameon the latest/boundary that fits; rejects paths beyond 255 chars with typed.pathTooLong.CompressDialogView.compressTarWithProgress#if MAS_LITEbranch migrated — MAS-Lite can now create.tarand.tar.gzarchives in-process with per-entry progress. Direct keeps/usr/bin/tar. CRC-32 implementation extracted to a sharedInternalCRC32enum so the ZIP writer and gzip footer share one table + one routine instead of duplicating. 5 new tests: plain TAR round-trip / TAR directory tree /.tar.gzround-trip + gzip-magic byte check / unicode filenames / progress callback. UnifylFileSystem test count 115 → 120. MAS-INCOMPAT[drop-pending-lib]backlog: 6 → 5. - New
InProcessZipWriter(Packages/UnifylFileSystem) + Compress dialog ZIP path now sandbox-safe. Sibling to InProcessZipReader: a 300-line pure-Swift PKZIP writer using Foundation'scompression_encode_buffer(COMPRESSION_ZLIB → raw DEFLATE, same encoding as the reader's decode side) plus a standard reflected polynomial CRC-32 table. Walks source URLs depth-first viaFileManager.enumeratorwithnextObject()(Swift 6 async-safe); resolves symlinks before building entry-relative paths (/tmp↔/private/tmpon macOS used to bite the recursion). Per-entry: DEFLATE first, fall back to STORED when DEFLATE bloats incompressible data (jpg / mp4 / random). UTF-8 filename flag set (bit 11 of general-purpose). Refuses ZIP64 (4 GiB single-entry cap), passwords (ZipCrypto is weak, AES is heavier), and ZIP encryption — those stay on the Direct/usr/bin/zip -e -Ppath.CompressDialogView.compressZipWithProgress#if MAS_LITEbranch migrated — MAS-Lite can now create ZIP archives in-process with progress reporting per entry. Direct keeps/usr/bin/zipfor raw speed. 7 new tests cover single-file / multi-file / nested directory tree / unicode filenames / incompressible STORED fallback / password rejection / progress callback firing per entry. UnifylFileSystem test count 108 → 115. MAS-INCOMPAT[drop-pending-lib]backlog: 7 → 6. - New
InProcessZipReader(Packages/UnifylFileSystem) + xlsx preview now sandbox-safe. A 200-line pure-Swift ZIP reader that parses End-Of-Central-Directory + central directory + local file headers, and decompresses STORED (method 0) + DEFLATE (method 8) entries via Foundation's Compression framework (COMPRESSION_ZLIBoperates on raw DEFLATE — exactly what ZIP entries store). Sized to the few use cases that need it: xlsx / docx / pptx central-directory pull. Rejects ZIP64 (sentinel0xFFFFFFFFon size fields) and unsupported compression methods with typedReaderError. OfficePreviewView.parseAllSheets migrated to use it for both Direct AND MAS-Lite — both targets now read xlsx in-process with zerodittosubprocess, zero tmp dir, identical XML-parsing pipeline. ArchiveEngine still owns the broader read/write surface for Direct (7zz + unalz); InProcessZipReader is the narrow sandbox-safe alternative. 6 new tests cover round-trip / unicode filename / missing entry / non-zip rejection / multi-block DEFLATE. UnifylFileSystem test count 102 → 108. MAS-INCOMPAT[drop-pending-lib]backlog: 8 → 7. - MAS-Lite: pure-Swift content search (grep → FileManager enumerator). ContentSearchView's
#if MAS_LITEpath now does a real in-process directory sweep instead of returning an empty stub. UsesFileManager.default.enumerator(at:, options: [.skipsHiddenFiles, .skipsPackageDescendants])walked via.nextObject()(Swift 6 async-safe), filters by extension (the existing*.swift/*.txtglob input), respects case-sensitivity, caps individual file size at 16 MB, caps total matches at 50 (same as the Direct path'sgrep -m 50). Tries UTF-8 then ISO-Latin-1 before skipping unreadable files. Identical[ContentSearchResult]shape as the grep path so the view doesn't branch. Direct builds keep/usr/bin/grepfor raw speed. MAS-INCOMPAT[drop-pending-lib]backlog: 9 → 8. - Git + Compress stub strings translated to all 15 locales. "Git integration is not available…" + "Compress is not available…" now ship in ko / ja / zh-H...
Unifyl 1.3.1
[1.3.1] — 2026-05-26
Changed
- MAS-Lite migration: first
[lib]site replaced with native API + 3 more[drop]sites wrapped:- PDFConvertSheet.textutil → NSAttributedString in-process (true
[lib]win):convertDocToPDFfor legacy.doc/.docx/.rtf/ fallback no longer spawns/usr/bin/textutil. ReadsData(contentsOf:)off-main, then constructsNSAttributedString(data:options:documentAttributes:)on the main actor (TextKit Cocoa importers sniff the document type from the data prefix — the same path textutil(1) wraps internally). Renders to PDF via a newSelf.renderAttributedToPDF(_:destination:)helper extracted from the existingconvertTextToPDFso both code paths share one NSPrintOperation pipeline. - AboutView git fallback wrapped with
#if !MAS_LITE: the dev-build commit-count / short-hash lookup (only ever runs when CFBundleVersion is the placeholder "1", which release builds override) is gone from the MAS-Lite binary entirely. About-view footer degrades to plist values, which is correct for the App Store distribution. - PDFConvertSheet osascript Office-to-PDF wrapped with
#if !MAS_LITE: the iWork / Microsoft Office AppleScript automation path would need a per-targetcom.apple.security.scripting-targetsentitlement plus explicit consent. Reclassified as[drop]for MAS-Lite; Direct keeps the existing AppleScript flow. - AudioConverterView afconvert wrapped with
#if MAS_LITE:/usr/bin/afconvertis Apple-bundled butProcess()is still sandbox-banned. Returns a localizedConversionResult(success: false, message: …)in MAS variant. A proper[lib]replacement (AVAudioConverter + ExtAudioFile in-process) is tracked as a future sprint. - One new locale key × 15 locales:
"Audio conversion is not available in the Mac App Store variant. Use the direct download from unifyl.app to convert audio files."translated to ko / ja / zh-Hans / zh-Hant / fr / de / es / pt-BR / it / ru / ar / th / vi / tr (en baseline). Audit-guard F4 remains 0 drift. - Three mislabeled MAS-INCOMPAT tags corrected:
[lib] system_profiler→ actually git,[lib] ffmpeg→ actually afconvert,[lib] LibreOffice subprocess→ actually osascript. The 26-site backlog now reflects the real binaries. - Verification:
make audit→ 13/13 PASS, 0 WARN, 0 FAIL. BothUnifyl (Debug)andUnifyl MASLite (Debug)xcodebuildBUILD SUCCEEDED. UnifylCore tests 131/131, UnifylFileSystem 102/102.
- PDFConvertSheet.textutil → NSAttributedString in-process (true
Added
- Separate UnifylMASLite Xcode target shipped:
project.ymlnow defines a parallel application target that produces a Mac App Store-ready build alongside the Direct distribution.com.unifyl.app.maslitebundle ID (vscom.unifyl.app), separateInfo.MASLite.plist(no Sparkle keys), separateUnifyl.MASLite.entitlements(sandbox=on, user-selected files RW, security-scoped bookmarks, network.client, print),SWIFT_ACTIVE_COMPILATION_CONDITIONS=MAS_LITEso all 9[drop]wraps fire, no Sparkle SPM dependency, no 7zz/unalzResources/bin/postBuildScript. Two Xcode schemes (Unifyl MASLite (Debug)+(Release)) for everyday dev and archive. Sparkle imports inUnifylApp.swift/AboutView.swift/CheckForUpdatesView.swift(and their menu/button call sites) are#if !MAS_LITE-gated so neither variant pulls the framework needlessly. Verified:xcodebuildof both targets succeeds; the builtUnifylMASLite.apphascom.apple.security.app-sandboxlive, noFrameworks/Sparkle.framework, noResources/bin/. The two.appbundles can be installed side-by-side on one Mac.
Added
- MAS-Lite scaffold complete — 9 of 9
[drop]Process() sites wrapped:#if MAS_LITEguards now cover every subprocess invocation classified as "drop in MAS variant" — ScriptRunner.launch, InlineTerminalView.ProcessRunner.run, CommandLineBar shell exec, SSHTunnelView.startTunnel, DockerExplorerView.runShellCommand, PortViewerView.runShellCommandForPorts, ProcessFileMapView.runShellCommandForProcessMap, NewConnectionSheet.runTestProcess (ssh-keygen), MediaConverterView.convertWithFFmpeg. Each MAS_LITE branch returns or throws a localized "feature unavailable in the Mac App Store variant — use the direct download from unifyl.app …" message via a typedLocalizedErrorenum (per audit-guard F11). Bothxcodebuild(default) andxcodebuild OTHER_SWIFT_FLAGS=-DMAS_LITEBUILD SUCCEEDED. The remaining[lib]sites (git, ffmpeg main path, textutil etc.) need actual library replacements rather than wraps — separate effort. - All 15 locales now translated for F7-lite + MAS-Lite strings: 9 new entries × 14 secondary locales = 126 translations on top of en baseline.
make auditreports parity across en / ko / ja / zh-Hans / zh-Hant / de / fr / es / pt-BR / it / ru / tr / ar / th / vi with zero WARN, zero FAIL. Every CJK / European / SEA / RTL user sees the F7-lite badge ("📋 Korean government / public-institution form (heuristic)" + "%lld signature phrase(s) matched") and any MAS-Lite stub messages in their language.
Added
- F7-lite UI badge in HWPX preview: HWPXTextPreview now runs the gov-form heuristic on the extracted body and shows a tinted pill above the text when
isLikelyGovForm. Hover the badge to see every matched signature phrase. Respects theunifyl.feature.koreanGovFormDetectionopt-out. - F7 fingerprint database scaffold:
KoreanGovFormDatabase+ bundledResources/data/kr-gov-forms.jsonseed with 3 placeholder entries (표준 도급 계약서 / 부가가치세 신고서 / 민원 신청서) + SHA-256 + bytes-level matcher. Curation guide atscripts/data/README-kr-gov-forms.mddocuments the schema and 50-form rollout target. 8 unit tests on the loader + match logic. - MAS-Lite compile-flag scaffold:
Unifyl/Automation/ScriptRunner.swiftdemonstrates the canonical#if MAS_LITE / #elsewrap pattern — throws a typedLaunchError.unavailableInMASVariantinstead of runningProcess()when the flag is set. Bothxcodebuild(default Direct path) andxcodebuild OTHER_SWIFT_FLAGS=-DMAS_LITE(MAS-Lite path) succeed. Future MAS-Lite target just setsSWIFT_ACTIVE_COMPILATION_CONDITIONS=MAS_LITEand incrementally wraps the remaining 11[drop]sites. Updateddocs/business/mac-app-store-readiness.mdto mark the audit prep-work checkbox ✅.
Added
- CJK wedge F7-lite — Korean government form heuristic detection: new
KoreanGovFormDetectorenum recognises 31 high-density Korean form signature phrases (주민등록번호 / 사업자등록번호 / 신청인 / 신고인 / 결재자 / 기안자 / 별지 제 / 호 서식 / 작성일자 / 신청합니다 / 위와 같이 / 우편번호 / 도로명주소 / 직인 / 귀하 / 갑(이하 / 을(이하 etc.). Threshold of 3 distinct hits keeps single-mention false positives out of the badge set. 256 KB scan window caps worst-case latency at ~100 ms on a 1 MB document. Pure-text input (caller extracts body via HWPXParser F2). 9 unit tests including 4 real-world templates (신청서, 도급 계약서, 부가가치세 신고서). Foundation work for the full F7 fingerprint-database UI; badge wiring lands in a follow-up. - 50K-item ASCII filter benchmark: PerformanceTests gained two new cases — empty-query identity (sub-ms) and 50K ASCII substring (270 ms, under the 500 ms type-ahead budget). The suite now covers 10K and 50K across empty / ASCII / CJK / multilingual / fullwidth / regex / glob paths.
Changed
- MAS-INCOMPAT comment sweep — 26
Process()sites tagged: all subprocess invocations in the app target (Unifyl/Views/*,Unifyl/Services/*,Unifyl/Automation/*) now carry a// MAS-INCOMPAT: [drop|lib] <migration note>comment one line above. Future Mac App Store conversion cangrep -r MAS-INCOMPAT Unifyl/ | sort -uto see the entire migration backlog with per-site replacement notes (git → SwiftGit2, ffmpeg → AVAssetExportSession, tar/ditto → Compression framework, etc.). Comments-only change — zero behaviour delta.
Fixed
- Force-unwrap in AzureBlobFileSystem paging loop: the
do { ... } while marker != nil && !marker!.isEmptycondition relied on a manual nil-check + force-unwrap. Rewrote asmarker.map { !$0.isEmpty } ?? falseso a future contributor doesn't trip over the unwrap if they refactor the surrounding code.
Added
- Universal undo for encoding conversion: the "Convert to UTF-8" Replace-Original path now writes a
.unifyl-bak.<unixtime>.<ext>sidecar with the original bytes BEFORE overwriting. The batch is recorded as a singleFileOperationHistory.encodingConvertop so Cmd-Z restores every converted file at once. Power-cut safe — the backup lands before the rewrite, leaving recoverable bytes on disk even if the in-place write fails mid-flight. - 10K-item search performance benchmark: new
PerformanceTestssuite in UnifylCore that exercises ASCII substring / CJK substring / multilingual expansion / fullwidth fold / regex / glob paths against a 10K-item synthetic listing. Runs as part ofswift test; budget assertions catch regressions before release.
Changed
- Hangul → Hanja search expansion: 563 ms → 36 ms (15.6× faster on 10K items). The expansion can produce 200+ single-character Hanja candidates for a 2-syllable query; the old per-item linear loop over all candidates was the dominant per-keystroke latency on Korean-content directories. Switched to a Set membership scan over haystack chars for single-character expansion candidates; multi-character candidates (original query, Romaji hira/kata segments, fullwidth-folded forms) still use the substring path. Net effect: Korean type-ahead is now instant on listings up to 50K items.
Fixed
- Encoding conversion sheet was blocking main actor during disk I/O:
runDetection/performConversionnow hop toTask.detachedso per-file read + decode + write don't freeze the spinner during a multi-file batch.DetectionandConversionResultgained explicitSendableconformance for the boundary crossing. - Fire-and-forget
Taskafter encoding conversion: the prior code chainedTask { sleep(2); refreshPanel; dismiss }which raced manual sheet dismissa...
Unifyl 1.3.0 — CJK wedge + Global rollout
[1.3.0] — 2026-05-17
Added
- CJK wedge F1 — ALZ archive support:
.alzarchives (Korean legacy format from ESTsoft's ALZip) now extract via bundledunalzbinary (kippler/unalz, BSD-licensed, universal arm64+x86_64, codesigned). Read-only — listing, extract-all, and selective-extract supported; create/add/remove/rename throw "read-only" errors. ALZDriver routes through ArchiveEngine's split extension files (Extract / List / Create / Modify). EGG (also ESTsoft) deferred to 1.2.1 — no maintained open-source extractor. - CJK wedge F2 — HWP inline preview: HWPX files (Hancom Office's XML format, .hwpx) now preview inline in the file panel via pure-Swift section-XML text extractor (HWPXParser.extractText). 6 parser tests cover empty doc / multi-paragraph / XML entities / multi-run paragraphs / real HWPX fixture. Legacy binary .hwp formats (HWPML/HSP/HWT) deferred to F9 in 1.3.0.
- CJK wedge F3 — Multilingual filename search: type "wen" → matches 文書/文件/文字 (Pinyin → Hanzi via CC-CEDICT-derived map), "tokyo" → matches 東京 (Romaji → Kana → Kanji), "한글" → matches 韓國語 (Hangul → Hanja via Unihan map). All three transforms ship with bundled JSON resource data and per-transform tests. Opt-out toggle in Settings > General; telemetry counts expansion-driven hits for future tuning.
- Global rollout — landing page: docs/landing/ is now built per-locale (15 languages) from
docs/landing/i18n/keys.{locale}.json+ a shared template viascripts/build_landing_i18n.py. Each variant ships with the correctlang/dirattribute, hreflang map (15 + x-default), OG locale tag, and an in-nav language switcher. Arabic getsdir="rtl". Hosting layout:/is English (canonical),/ja/,/de/,/zh-Hans/, … serve translated copies. - Global rollout — README: top-5 README localizations (
README.ja.md,README.zh-Hans.md,README.de.md,README.es.md,README.fr.md) plus a language-switcher banner on the canonical English README. Abbreviated form — full plug-in SDK / contribution docs stay single-source in English. - Global rollout — Info.plist:
Unifyl/Resources/{locale}.lproj/InfoPlist.stringsfor all 15 locales. LocalizesNSHumanReadableCopyright;CFBundleDisplayName/CFBundleNamestay as the brand "Unifyl". Picked up by XcodeGen's source sweep — no project.yml change needed. - Global rollout — marketing copy:
docs/marketing/i18n/copy.{ja,zh-Hans,de,es,fr}.md— reusable per-locale tagline / long description / category tags / pricing phrasing / CTA variants, drives every directory submission from one file per language instead of ad-hoc translation.
Fixed
- RTL (Arabic) navigation directionality: 7 SF Symbols that used literal-direction variants now use auto-mirroring directional variants. Toolbar back/forward (
chevron.left/right→chevron.backward/forward), breadcrumb separators in PathBarView and ArchivePathBarView (chevron.right→chevron.forward), and rename-flow indicators in MultiRenameView / RegexRenameView / LargeFileFinderView (arrow.right→arrow.forward). Bidirectional icons (arrow.left.arrow.right) left as-is.
Unifyl 1.2.3
[1.2.3] — 2026-05-13
Fixed
- 2 NSAlert sites in sheet-hosted views switched to
beginSheetModal(for:)instead ofrunModal().NLCommandPreviewSheet(sheet host confirmed atMainWindowView:631) andAIEngineViewModel.applyKeepBest(called fromEnhancedDuplicateView, sheet status indeterminate). NSAlert.runModal() from inside a SwiftUI sheet has no guaranteed presentation path — same family of defect as the PDF Tool sheet-modal fix earlier in [Unreleased]. The other 8 NSAlert.runModal() sites were audited and confirmed safe (menu / keyboard / file-op / Sparkle launch contexts, all main-window-modal). - 15 persistence sites now route corrupt blobs through
PersistenceBackupinstead oftry?-wiping user data. The patternguard let decoded = try? JSONDecoder().decode(T.self, from: data) else { return }silently dropped the user's saved data on schema drift across an upgrade. Sites converted: SSHTunnelView (saved tunnels), ScheduledTaskView (cron jobs), NewConnectionSheet (OAuth tokens — Keychain path), IncrementalBackupView (backup profiles), FileColorSettings (per-category colors), IconThemeManager × 3 (installed themes / icon-pack mapping / external themes — all file-based, usestashCorruptFile), SSOAuthManager × 2 (Keychain — corrupt entry now deleted + logged so re-sign-in is forced instead of looping), KeychainService.debugLoadAll (DEBUG-only dev keychain), SecurityBookmarkManager (legacy migration), RecentPathsStore (recent folders), Companion SessionManager (paired devices), CompanionServerConfig (companion server config). UserDefaults blobs are stashed to<key>.corrupt.<timestamp>(recoverable fromdefaults read); on-disk files are moved to<name>.corrupt.<timestamp>.<ext>sidecars. TeamSyncClient's 403-body decode is intentionally left ontry?because that's a server-controlled response, not user data. Per memoryfeedback_persistence_decode_must_stash. - AIEngine no longer crashes the app when every disk path for the vector index fails. Previous:
fatalError("AIEngine: unable to create VectorIndex at any path")after the primary / temp / UUID-stamped emergency paths all failed (full disk, sandbox denial). Now falls back to SQLite:memory:so AI search and indexing keep working for the current session — they just don't persist across launches. ThefatalErroris preserved as a backstop for the (unreachable) case where bundled SQLite itself is broken, so support still gets a crash report. AddedVectorIndex.init()that opens SQLite:memory:. - All 7 hardcoded English alert + confirmation-dialog titles now ship native translations in all 15 locales.
.alert("Error", isPresented:)/.confirmationDialog("Sync All Differences?", ...)and friends — SwiftUI auto-infersLocalizedStringKeyfrom string literals, but the keys (System Shortcut,Reset all keyboard shortcuts?,Save Macro,Navigation Error,Save Backup Profile,Plugin Installation Failed,Sync All Differences?) had no entries in anyLocalizable.strings, so non-English locale users saw English fallback. Generated +7 entries per locale across ko / ja / zh-Hans / zh-Hant / de / fr / es / pt-BR / it / ru / tr / ar / th / vi / en (105 total). The other alert keys (Error,Move duplicates to Trash?,Leave this team?,Sign out of SSO?,Delete AI index?) were already translated. Select PDF...(and every other "Browse for file" button inside a sheet) now actually opens the panel. PDF Tool, Folder Report export, SSH Tunnel key chooser, Large-File Finder scan-root picker, Scheduled Task source/dest pickers, Audio Converter output folder, Audio Metadata album-art chooser — all of them are SwiftUI sheets, and inside a sheetNSOpenPanel.runModal()/NSSavePanel.runModal()is silently swallowed by the outer modal hierarchy. The buttons appeared dead. Switched all 9 sites across 7 views topanel.begin { … }(application-modal, attaches above the sheet) with aTask { @MainActor in … }continuation for the post-pick state mutation.- Audit-driven follow-up: 14 more sheet-hosted
NSOpenPanel.runModal()sites that were missed in the previous pass. Same family of defect as above — all hosted in SwiftUI sheets and silently swallowed. Sites converted:AIOnboardingSheet.chooseFolderAndIndex,NewConnectionSheetSSH-key picker,AdvancedSearchView× 3 (scope change, copy destination, move destination),IncrementalBackupView.pickFolder,FileSplitMergeView× 2 (split source, merge first-part),SemanticSearchViewChoose-Directory button,FileOperationConfirmViewdestination Browse,CompressDialogViewdestination Browse,MediaConverterViewoutput-folder Choose. After this pass, every confirmed sheet-hostedrunModal()on either NSAlert or panel is gone.CustomTheme.exportTheme/importThemekeeprunModal()because they're called fromThemeEditorView, which is hosted as an independentNSPanelviaViewerWindowManager— not a sheet, sorunModal()works correctly there. - Companion error messages now ship native translations in all 15 locales instead of always rendering in Korean.
CompanionError.errorDescriptionhad 10 cases (notPaired, pairingFailed, connectionLost, connectionTimeout, serverUnreachable, authenticationFailed, permissionDenied, transferFailed, pathNotAllowed, rpcError) with hardcoded Korean string literals, used in 15 sites across the Mac-side Companion server (RequestRouter / SessionManager / WebSocketServer / handlers). Non-Korean Mac users would see Korean banners and toasts during pairing failures or transfer errors. Replaced every literal withNSLocalizedString(key, value:, comment:)using English base values; new keys carry thecompanion.error.*prefix. - AI auto-classify suggestion reasons translate to all 15 locales.
AIClassifier.describeReasonreturned three hardcoded Korean strings ("N개의 유사한 파일이 존재", "관련 파일 N개 존재", "폴더명 유사도 기반 추천") that appeared in the classification preview next to every suggested destination. Wrapped each inNSLocalizedString(value:)with English source; new keys areai.classify.reason.similarExtension,ai.classify.reason.relatedFiles,ai.classify.reason.folderName. - Permission-denied banner no longer relies on substring-matching the localized error message.
PanelViewdecided whether to show the "Open System Settings ▸ Privacy & Security" affordance by doingerr.lowercased().contains("permission") || err.contains("권한"), which worked only in English- or Korean-localized installations. Ja/zh/de/fr/es users saw a generic red banner with no escape hatch. AddedPanelViewModel.lastErrorIsPermission— a typed flag set inloadCurrentDirectory's catch fromError.isPermissionDeniedError, which pattern-matches CocoafileRead/WriteNoPermission, POSIXEACCES/EPERM, andUnifylFileError.permissionDeniedinstead of the localized message. Reset alongsidelastError = nileverywhere it's cleared (initial load, retry, archive enter/exit, manual dismiss). - Help window (F1) actually focuses in the 13 non-English / non-Korean locales.
MainWindowView'sisHelpOpenhandler matchedNSApp.windows.first(where: { $0.title.contains("Help") || $0.title.contains("도움말") }), which silently failed for the other 13 localized titles ("ヘルプ", "帮助", "Hilfe", "Aide", "Ayuda", "Помощь", and so on) — F1 did nothing if the window was already open in those locales. Replaced with@Environment(\.openWindow) openWindow(id: "help"), which SwiftUI handles as "focus existing or create" by scene id, locale-independent. - Forward Delete (⌦) key now moves selected files to the Trash, same as F8. Previously only F8 and
⌘Delete(Cmd+Backspace) triggered the move-to-Trash flow; the labelled "Delete" key on full-size keyboards (orfn+deleteon laptops, keyCode 117) did nothing. Wired in bothFileTableView.handleKeyDown(when the file table has focus) andFunctionKeyMonitor(when focus is elsewhere — pathbar / toolbar / etc.) so the affordance is consistent across the window.Backspacealone (no modifier) still navigates to the parent directory — that's the TC + Finder convention and stays put. FileTableView.userDefaultsDidChangeno longer SIGTRAPs when defaults are written off-main.UserDefaults.didChangeNotificationposts on whatever thread modified defaults (e.g.XCTTelemetryLogger.+initializetriggersregisterDefaults:on a background dispatch queue during test bundle init), and Swift 6 strict concurrency traps when the @mainactorapplyColumnVisibility()is called from there. Marked the selectornonisolatedand hopped to MainActor viaTask. Real-world impact: the test runner no longer crashes at bootstrap; production launch was already on main but is now race-proof for any future off-main defaults writer.- CSV Preview now splits rows on Windows-style
\r\nline endings. Swift'sStringiteration groups\r\ninto a single extended grapheme cluster that matched neitherchar == "\r"norchar == "\n"in the char-by-char parser, so the line ending leaked into the field value and a Windows-saved CSV rendered as a single giant row. Replaced the manual check withCharacter.isNewline, which matches\n/\r/\r\n+ Unicode line separators (\u{0085},\u{2028},\u{2029}) uniformly. The pre-existingif char == "\r" { continue }branch was dead code (never reached because Swift had already merged the CR+LF). Caught by the existinghandlesCRLFunit test, which was previously failing.
Added
- CSV Preview: text-encoding auto-detect + large-file safety. The previewer used to hardcode
String(contentsOf: url, encoding: .utf8), so a Korean Excel CSV (CP949 default) or any non-UTF-8 file threw or rendered mojibake, and a multi-hundred-MB CSV would block the main thread / OOM on whole-file load. Now: BOM-checks UTF-8 / UTF-16 LE / UTF-16 BE first, then falls through `.utf8 → CP949 (kCFStringEncodingDOSKorean) → Shift_JIS → EUC-KR → Big5 → GB18030 → ...
Unifyl 1.2.2
[1.2.2] — 2026-05-11
Multi-axis polish pass on top of 1.2.1, accumulated across multiple audit-driven sessions.
Added
- 193 previously-hardcoded UI strings localized into all 14 non-English locales (ja, ko, zh-Hans, zh-Hant, de, fr, es, pt-BR, it, ru, tr, ar, th, vi). Roughly half of the app's
Text(...)literals (mostly inside viewer / tool sheets — Checksum, AI Auto-Classify, AI Search, App Uninstaller, Audio Converter, SSH Tunnel, Git History, File X-Ray, PDF Convert, Theme presets, etc.) had no entries inLocalizable.strings, so non-English locale users saw English fallback text in those surfaces despite the rest of the app being native. Each locale now ships +193 entries (343 → 536 keys) with macOS-convention vocabulary per language. - Onboarding slide 4 ("Pick a folder to begin") now localized in all 15 languages. The slide was added in 1.2.0 but its 5 NSLocalizedString keys (title, body, drag, goto, connect) were never added to
Localizable.strings, so 14 non-English locale users saw the Englishvalue:fallback for the post-onboarding "now what?" guidance. Added native translations for ko / ja / zh-Hans / zh-Hant / de / fr / es / pt-BR / it / ru / tr / ar / th / vi. Error.humaneMessageextension that maps low-level Foundation / POSIX / NSURLError codes to localized user-facing strings. Falls through toerrorDescriptionforLocalizedError-conforming types (UnifylFileError,VectorIndexError), and tolocalizedDescriptionas last resort.- Viewer NSPanel windows now remember their size and position across launches. The Hex / Log / Git / EXIF / Compare / Dir Compare / Media / Docker / Git History / Process Map / Port Viewer / Theme Editor / 3-Way Merge / File Preview viewer windows used to open at the same default-centered + offset rect every time, even after the user resized or moved them. They now derive a per-viewer autosave key from the title prefix (e.g. "Hex: foo.bin" → key "Hex"), so AppKit persists the frame to defaults under
UnifylViewer.<type>and restores it on the next open. The Main window already had this viaWindowFrameAutosavesince 1.1.0; this brings viewers to parity.
Changed
- All user-facing error banners now route through
error.humaneMessageinstead oferror.localizedDescription. ~112 user-facing error sites total: AppViewModel (26), FileOperationManager (10), AIEngineViewModel (7), PanelViewModel (5), ConnectionViewModel (1), TeamSettingsView (4), TeamOnboardingSheet (2), AuditLogView (2), plus 55 sites across 43 view-layer files (CompressDialog, FilePermissions, MediaInfo, AppUninstaller, FileDiff, DirectoryDiff, MediaConverter, SQLitePreview, JSONPreview, PlistPreview, FontPreview, VideoPreview, ArchivePreview, AudioPlayer, EnterpriseTeam, etc.). All produce localized messages forEACCES/ENOSPC/ENOENT/EBUSY/ etc. instead of raw EnglishlocalizedDescriptionfrom the system. OSLog /UnifylLoggercalls are intentionally left onlocalizedDescriptionso logs stay machine-greppable in English. - Semantic color literals migrated to DesignTokens. 93 sites across 52 view/feature files where
.foregroundStyle(.green / .red / .orange)was hard-coding success / error / warning intent now go throughDesignTokens.Colors.success / .error / .warning. The token registry already had matching values (#34D399,#F87171,#F59E0B) chosen for ≥4.5:1 contrast against the dark surface — this brings the rest of the app onto them so a designer can re-tune the status palette in one place..blueand.accentColorleft untouched (ambiguous between info-status and system-accent intent — needs case-by-case review).
Fixed
- CLDR plural rules via
Localizable.stringsdict. Five hardcoded English plural patterns (count == 1 ? "" : "s") in AppViewModel (clipboard sources missing), AIRenameView ×2 (rename suggest / will-rename count), EnterpriseTeamView (member count), DuplicateFinderView (will-go-to-Trash count) used to render "1 file" / "5 files" in English but "5 파일s" / "5 ファイルs" in non-English locales. Generated.stringsdictfor all 15 locales with proper plural categories (single-form for ja/ko/zh/th/vi, two-form one/other for en/de/fr/es/pt-BR/it/tr/vi, two-form for ru/ar — Arabic could be expanded to 6-form later if specific n=2/few/many phrasing matters), and switched the call sites toString.localizedStringWithFormat(NSLocalizedString("ai.renameSuggest", …), count). Native macOS plural infrastructure now picks the right form per locale. - Hidden-file toggle (⌘⇧.) now persists across launches.
AppViewModel.showHiddenFileswas a plainvarinitialised tofalse, so every relaunch reset it — a developer who flipped it for the session had to re-toggle each launch. Added adidSetobserver that mirrors thesplitRatio/bookmarkspersistence pattern (@AppStoragedoesn't work on@Observablestored properties — init-accessor synthesis rejects it), backed byunifyl.showHiddenFilesin UserDefaults. - OAuth callback URL scheme now declared in
Info.plist.OAuth2ConfigurationshipsredirectURI = "com.unifyl.basic://oauth2/callback"for Native Google Drive / Dropbox / OneDrive providers, butCFBundleURLTypeswas missing — macOS had no app to route the callback to, so the auth flow would hang forever. Added a single URL-types entry registeringcom.unifyl.basic. (Native OAuth providers are still gated behindunifyl.enableOAuthPilotwhile their client IDs remain placeholders.) - Smart quote / dash / period / text-replacement substitutions disabled app-wide. macOS NSTextField and NSTextView ship with these ON by default, which silently mangles file names typed into inline rename, ⇧⌘G Go to Folder, and the inline command bar (
"data".csvbecomes curly-quoted,foo--bar.txtbecomes en-dashed,omw to a folderexpands via System Settings text replacement).applicationDidFinishLaunchingflips the fourNSAutomatic*Enableddefaults tofalsefor this app's text controls without touching system-wide settings. - Custom theme load no longer silently wipes the user's saved themes on schema drift.
try?was returning nil when the on-disk theme JSON failed to decode (typical after an upgrade adds a new color token), and the nextsave()overwrote the disk file with[]. Now stashes the corrupt file viaPersistenceBackup.stashCorruptFile(recoverable for support) and logs toUnifylLogger.settings. Same pattern applied to the import-from-file path so a malformed.ultrathemefile logs the parse error instead of vanishing. - A11y polish on tab bar + status bar. TabBar's
xmarkclose button gained.accessibilityLabel("Close Tab")(was announcing as a generic "button"); thepin.fillandcloud.fillchips on the tab title gained.accessibilityHidden(true)because they're decorative — VoiceOver was announcing "image: pin.fill" before every pinned tab's title. PanelStatusBar's optional toast icon now.accessibilityHidden(true)for the same reason — the toast text right next to it carries the message. - URLSession requests now cap their wait so a hung server doesn't keep the request UI spinning forever.
TeamSyncClient.buildRequestgetstimeoutInterval = 15 s(matchesLemonSqueezyClient);IconThemeManagerper-asset SVG fetch caps at 30 s and the manifest JSON download at 60 s. Other URLSession sites already had explicit timeouts via the request builder pattern. applicationWillTerminatenow flushes UserDefaults before quit. Quick Cmd+Q right after adding a bookmark could lose the entry on systems that hadn't auto-synced defaults yet; the new handler callsUserDefaults.standard.synchronize(). Doesn't block termination (no beachball) — file copies still rely on FileOperationManager's atomic-write + half-file cleanup.- 18 Xcode build warnings cleared. PortViewerView's
errDatawas a capturedvar Data()mutated fromPipe.readabilityHandlercallbacks while the outerTask.detachedowned the closure scope — Swift 6 strict concurrency flagged it as a real data race; wrapped in a lock-protectedErrDataBox(@unchecked Sendable,NSLock-guarded). UnifylTests'override func setUp() / tearDown()were declared withoutasync throwsin@MainActorclasses, breaking MainActor isolation; converted to async-throws variants withtry await super.<x>(). Cloud HTTP calls (Dropbox/Google/OneDrive —post/patch/delete× 8) had unusedtry await client.xxx(...)results that look like silent fails to a reader; made the discard explicit with_ = try await .... Plus 1 var→let cleanup and 3 unused-let →_cleanups in tests.
Notes (audit-driven, ship-pending)
- B3 —
Task.detached[weak self]audit: swept all 80+Task.detachedsites acrossUnifyl/andPackages/. No real retain risk found. Most closures use closure-local data plusSelf.<static>calls. The few sites that touchselfeither already declare[weak self]or use intentional[self](FileOperationManager:154 — atomic copy needs strong reference). ViewModels are@MainActor final class→ sheet/window dismiss destroys them, so even captured-self closures resolve cleanly. No code change needed.
Unifyl 1.2.1
[1.2.1] — 2026-05-10
Patch release closing two more "shipped UI, missing gate or stale copy" gaps caught during a deep audit five days after the 1.2.0 → build 19 respin.
Fixed
Ctrl+M(Total Commander alias for Multi-Rename) now goes through the Pro feature gate. The keyboard shortcut opened the Multi-Rename sheet directly without theisUnlocked(.multiRename)check the menu and Command Palette already had, so a Free user could trigger a Pro feature for free via the alias. Now request-upgrade-sheet on Free, open-the-sheet on Pro — same as the other entry points.- Help / User Manual FAQ no longer cites the dropped 4-tier pricing model. A leftover sentence from the v1.0-era pricing — "Requires the AI tier ($59.99) or higher." — sat in every shipped manual's FAQ even after the build-17 sweep cleaned the Edition Comparison table. All 15 localized manuals now read "Included with Pro." in the user's language. Mirrors the rest of the now-2-tier pricing copy elsewhere in the app.
Unifyl 1.2.0
[1.2.0] — 2026-05-01
Added
- Tab middle-click closes (browser convention). Pressing the trackpad middle / mouse-3 button on a tab closes it without forcing the user to hover for the X or hit ⌘W. Pinned tabs ignore middle-click so users can't accidentally lose a saved location. Implemented via a transparent
NSViewRepresentableoverlay that intercepts onlyotherMouseDown— left-click, right-click, and double-click pass through unchanged. - Cloud chip on remote tabs. Tabs sitting on a remote VFS (SFTP / S3 / WebDAV / OAuth cloud) now show a small cyan
cloud.fillicon next to the title so users see at a glance which tabs are local vs over the network — sets expectations for paste latency and "where do my files actually live?". Hover tooltip reads "Remote connection". - Connections sidebar open / closed state persists across launches via
unifyl.showConnectionsSidebarAppStorage. Was per-window@Statethat always reset to closed on relaunch — users who want it always-on had to re-toggle every session. - Status bar shows "Space preview" hint when cursor is on a file. Right next to the cursor item info ("filename · 1.5 MB · date") an inline
Spacechip + "preview" label tells the user the Quick Look shortcut is one key away. Only shown for files (folders open with Return / double-click). Massively boosts Quick Look discoverability for users coming from non-Finder file managers. - One rotating "Did you know?" tip per launch in the active panel's status bar, ~6 s with fade. Pool of 12 tips covers Return / F2 / Space / ⌘⇧P / ⌘⇧G / ⌘K / ⌥-drag / ⌃B / ⌃G / ⌃U / ⌘Z / F-keys — surfaces shortcuts a clean panel would never advertise. Cycles via stored
unifyl.launchTipIndexcounter so users see the whole tour over launches instead of the same tip twice. Skipped on the very first launch (onboarding covers basics) and honours an opt-out viadefaults write … unifyl.showLaunchTips -bool false. - Status bar shows cursor item info in Finder style. When the user has nothing marked but is sitting on a row, the bottom status bar reads "filename · 1.5 MB · Apr 15, 2026 14:30" instead of the generic "N items · X total". Folders show "—" for size (avoids expensive recursive sum). Hover the row for the full path. Selection (red marks) still wins — the existing "K selected · …" line takes precedence so marked-item totals stay visible.
- Recent Folders dropdown in the toolbar (clock-arrow icon next to Bookmarks). Same store the Go ▸ Recent Folders submenu uses, but surfaced one click away — so users see "I just visited these places" without opening the menubar. Empty-state tooltip explains the button will populate as they navigate; cleared by an inline "Clear Recent Folders" entry.
- Filter field placeholder + tooltip teach the syntax. Was: "Filter…" — gave no hint that glob and regex work. Now placeholder reads "Filter — try *.png or /regex/" and the hover tooltip spells it out: "Plain text matches anywhere in the filename. Use *.png-style wildcards for globs, or /pattern/ for regex." Both the toolbar field and the inline panel filter bar carry the same hint.
- Permission-denied panel error offers a one-click "Open Settings" jump. When the listing fails because of a Files-and-Folders / Full-Disk-Access denial, the inline red banner now includes an "Open Settings" button that takes the user straight to System Settings ▸ Privacy & Security ▸ Files and Folders. Previously they had to know which pane to find themselves. The banner also gained an explicit X dismiss button (matching macOS toast convention) instead of relying on tap-to-dismiss.
- Status-bar Free space readout refreshes every 10 s instead of only on first appear. Was a one-shot read on
.onAppear, so after a large copy / move / delete the value was stale for the rest of the session — users couldn't trust it for "do I have room?" sanity checks. Now polls a cheap volume-resource read on aTaskloop with cancellation, plus a "Free space on the start volume" tooltip explains which volume is being measured. - Clipboard ⌘C / ⌘X / ⌘V show confirmation toasts so the user knows the action took. ⌘C → "Copied "file.txt" — paste with ⌘V" or "Copied 5 files — paste with ⌘V". ⌘X → "Cut N files — paste with ⌘V to move" (so the user knows paste will move, not copy). Successful ⌘V → "Pasted N files into "DestFolder"" / "Moved N files to "DestFolder"". Without these, the panel looked identical before/after each shortcut and users re-pressed unsure whether it took.
- Onboarding slide 4 — "Pick a folder to begin" answers the post-onboarding "now what?" moment with three concrete entry actions: drag from Finder, ⌘⇧G to type a path, ⌘K to connect to a server. The previous flow ended with a "Get Started" button on a generic command-palette slide; users dismissed onto two blank panels with no obvious next step.
- Help button in the toolbar (
?icon, ⌘?). The Help menu has always existed but new users routinely missed it; a toolbar question-mark mirrors Finder / System Settings and answers "where do I go for help?" without hunting through menus. - Empty-folder state suggests next actions instead of just stating the obvious. Was: "Empty Folder · This folder has no visible items." Now: "Nothing to show here · Drop files in to add them, or press F7 to make a new folder." When
Show Hidden Filesis off, the message also notes that hidden items might exist and offers a one-clickShow Hidden Filesaction so the user doesn't have to hunt for the toolbar's eye icon. - Folder Bookmarks can be reordered, with discoverability cues. Drag any row to reorder (entire row hit area, with a live drop-target highlight bar), or right-click for Move Up / Move Down / Remove context menu (keyboard / VoiceOver fallback). A grip icon fades in on row hover and the header subtitle shows "Drag rows to reorder · Right-click for more" whenever there's more than one bookmark — so the affordance is visible before the user has to discover it. New order persists via the existing
saveBookmarks()path. - Go ▸ Connect to Server… (⌘K). Standard ForkLift / Cyberduck / Finder convention. Previously the Connections sidebar's
+button was the only entry point; users migrating from those apps were hunting for the menu item. - Sort By submenu now has keyboard shortcuts (⌃⌥N / ⌃⌥S / ⌃⌥D / ⌃⌥K for Name / Size / Date / Kind). Sort is a top-5 file-manager operation and previously required mouse every time.
- Newly bound shortcuts on previously menu-only commands: File Permissions ⌥⌘I, File History ⌥⌘J, Theme Editor ⌥⇧⌘T, Download from Remote ⇧⌘↓, Upload to Remote ⇧⌘↑.
- Menu labels surface F-key shortcuts that SwiftUI can't render in the trailing accelerator slot: "Compress… (⌥F5)", "Extract to Folder… (⌥F9)", "Duplicate in Same Folder (⇧F5)", "New Text File + Edit (⇧F4)". The bindings already worked through
FunctionKeyMonitor; the labels now make them discoverable. - Full UI translations for 13 additional languages — Japanese, Simplified Chinese, Traditional Chinese, German, French, Spanish, Brazilian Portuguese, Italian, Russian, Turkish, Arabic, Thai, Vietnamese — each expanded from a 57-key starter set to the full 343-key surface that English and Korean already shipped. Brings previously English-fallback strings (Pro upgrade sheet, Settings panes, Audit Log, Team Management, AI tools, onboarding, error messages, About view) into native localisation across the full 15-language matrix. Format specifiers (
%@,%d), keyboard markers (⌘B, F5, ⌥⌘O…), em-dash, ellipsis, and middle-dot punctuation preserved verbatim per locale.
Changed
- Destructive confirmation dialogs use a consistent "name what + offer the safer alternative" pattern.
- Permanent delete (⇧F8): was "Permanently delete N item(s)? · This bypasses the Trash and cannot be undone." Now: "Permanently delete "filename"?" / "Permanently delete N items?" with body "These won't go to the Trash, so there's no way to get them back. Use F8 instead to move them to the Trash safely." Confirm button label sharpened to "Delete Permanently" (vs the macOS-default ambiguous "Delete").
- Port Viewer kill process: was "Kill Process · This will terminate the process listening on port X. This action cannot be undone." Now: "Quit "ProcessName" (PID X)?" with body that names the SIGKILL signal AND warns about unsaved work — the consequence the user actually cares about. Button "Force Quit" (macOS convention) replaces "Kill Process".
- Duplicate Finder bulk delete: gained the same "you can restore from the Trash if you change your mind" reassurance as the panel-level Move-to-Trash dialog.
- Pro upgrade sheet explains what the locked feature actually does, not just its name. Added
Feature.shortDescriptioncovering 30+ Pro features ("Multi-Rename — Rename hundreds of files at once with regex, EXIF, AI suggestions, numbering — and undo any batch with ⌘Z."). The headline still names the trigger ("Multi-Rename is part of Unifyl Pro") but the prominent body text now answers "what does this thing do?" before the generic "Pro also unlocks…" subhead. Description omitted for self-explanatory items (e.g. "Bookmarks"). - Trial-active chip on the upgrade sheet. When the user is in their 14-day free trial and the sheet appears (e.g. after trial expired between sheet open and now), an orange hourglass capsule reads "Free trial active · N days remaining" so they know where they stand instead of wondering whether they're already Pro.
- Toolbar Back / Forward / Up disable when there's nowhere to go, with a tooltip explaining why. Previously these always appeared enabled even at the volume root or with empty history — clicking did nothing and the user couldn't tell if the app was broken. Now the buttons grey out and the hover tooltip says "Nothing to go back to yet — open a folder first." / "Nothing to go forward to. Press Back first to enable Forward." / "Already at the vol...
Unifyl 1.1.0
[1.1.0] — 2026-04-30
Added
- File List Columns are now configurable. Six additional optional columns join the existing Size / Date Modified / Kind: Date Created, Extension, Permissions (
rwxr-xr-x), Tags, Path, and Cloud status. Toggle each in Settings ▸ General ▸ File List Columns. The Name column is always shown; Smart Folder mode still force-shows Path regardless of the toggle. Visibility flips immediately on toggle (no relaunch) via aUserDefaults.didChangeNotificationobserver inFileTableView. - Archive entry timestamps preserved on extract. ZIP central-directory DOS times and 7z
Modified =fields are now decoded in the local timezone and restored onto the extracted file viasetAttributes([.modificationDate:])after the move. Fixes the "extracted file is N hours off" symptom on machines whose local timezone differs from UTC (e.g. a 9-hour drift on KST). The DOS-time decoder also drives the panel-side ZIP listing so the inline archive view shows the same wall-clock time the file had on the archive creator's machine. - Overwrite confirmation when extracting archives or copying across panels. Extraction and cross-panel archive copy now route through the same Skip / Rename / Overwrite dialog used by regular file copies, including the "Apply to all remaining conflicts" toggle (now defaulting on so multi-file batches don't fan out into dozens of identical prompts). The dialog supports arrow-key navigation between buttons via a local
NSEventmonitor, and respects the existingunifyl.confirmOverwriteuser default. - Cmd+A selects all in the active file panel regardless of focus context. Previously the menu route went through
NSTableView.selectAll(_:)which jumped the cursor to the bottom row without marking anything; the new global key intercept and an override on the inner table view both funnel into Unifyl's red-mark selection. Text fields andNSTextViewkeep their own select-all behaviour.
Changed
- Existing Size / Date Modified / Kind columns are now also toggleable from the same Settings section (still default on).
- Multi-file drag image now shows a single large file icon with a red count badge (Finder-style) instead of a stacked-rows snapshot. The icon-plus-badge composite makes it unmistakable how many items are coming with the drag.
- F7 New Folder positions the cursor on the just-created folder. Matches Total Commander / Finder behaviour: after the reload settles, the new folder is selected so Enter / F2 / Cmd+Down act on it without an extra search step. Previously the cursor stayed wherever it was before, which on large folders meant the user had to hunt for the new entry.
10-round upgrade sweep (functionality / standards / a11y / perf / design)
- Spreadsheet preview reads use memory-mapped I/O.
OfficePreviewView's shared-strings, workbook.xml, and per-sheet XML reads all switched toData(contentsOf:options: .mappedIfSafe). Memory footprint of opening a large XLSX in preview is now bounded by the OS's page-mapping policy instead of by the file size — a 50 MB workbook drops from ~50 MB resident to ~5 MB. - Hex Editor file load is off-actor.
HexEditorView.loadFile()was reading viaData(contentsOf: fileURL)synchronously on the @mainactor — a 10 MB read from a slow network volume blocked the entire main thread. Read now happens in aTask.detached(.userInitiated)with.mappedIfSafe, and the result is written back through aResulton the main actor. - Panel error banner gets icon + semantic color + a11y label.
panelVM.lastErrorwas a flat red bar — color-blind users had no other signal it was an error. Now it's anexclamationmark.triangle.fill+ text inDesignTokens.Colors.error, withaccessibilityLabelso VoiceOver announces "Panel error: …" rather than reading just the truncated text. - Connection-test result + Git-history error use semantic tokens.
NewConnectionSheet's success / failure state chips andGitHistoryView's error icon migrated from raw.green/.red/.orangetoColors.success/Colors.error/Colors.warning. The Git error icon also moved fromexclamationmark.triangleto the filled variant for higher visual weight. - Eight more empty states unified onto
EmptyStateView.JSONPreviewView,PlistPreviewView,FontPreviewView,ScenePreviewView,SQLitePreviewView,AppUninstallerView(×2),KeyBindingSettingsView,ThreeWayMergeViewall migrated fromContentUnavailableViewto the project-styleEmptyStateView. Error-state previews passiconColor: Colors.errorso the icon hue matches the message intent. - Sheet padding tokenised (final batch).
ChecksumView,FileOperationConfirmView,FilePermissionsViewmigrated from.padding(20)/.padding(16)toDesignTokens.Spacing.l. Combined with earlier rounds' NewFolderSheet / ArchivePasswordSheet migration, the project's modal padding is now phase-locked to one source of truth.
Design
- Design tokens reviewed and extended (designer pass). Seven gaps closed in
DesignTokens.swift:- Active vs inactive panel selection. New
bgSelectedActive(accent-tinted) for the focused panel; existingbgSelectedbecomes the inactive-panel value. Total Commander / ForkLift parity — at a glance you now see which panel takes the next keystroke. - Semantic color tokens.
success(#34D399),warning(#F59E0B),error(#F87171),info(#60A5FA) replace 30+ raw.green/.orange/.redliterals across views. Code reading "this label usesColors.success" tells you WHY the label is green; designers can re-tune the status palette in one file. - Shadow scale (
Shadow.card/Shadow.popover/Shadow.modal) + a.appShadow(_:)SwiftUI modifier. Replaces magic-number.shadow(color: .black.opacity(0.6), radius: 24, ...)calls that drifted across 12 files. Applied to FileOperationProgressView, CommandPaletteView, AboutView (more sites can adopt incrementally). - Text 4-tier scale. New
textTertiarybetweentextSecondaryandtextMutedso column headers and count badges no longer collapse into the same value as disabled labels — restores visual hierarchy at typical viewing distance. borderSubtlestrengthened (#1E2235→#232742). The previous value was only ~2% lighter thanbgSurface, so divider lines vanished at typical viewing distance. New value still reads as "subtle" but actually separates sections.focusRingtoken (accent at 70% opacity) for keyboard-focused controls — AppKit's default focus ring uses the system accent, which clashed with custom Unifyl themes.- Selection contrast bumped.
bgSelected→bgSelectedActivefor the focused panel pulls ~30% more accent saturation; the "this row is selected" cue is now unambiguous against the hover background.
- Active vs inactive panel selection. New
Performance
GitStatusProvidercache no longer grows unbounded. The 5-second TTL prevented stale data from being served, but expired entries lingered forever — a long browsing session that hit hundreds of git directories accumulated a[String: CacheEntry]of all of them indefinitely. Eviction now runs on every cache miss: stale entries (older than 5× TTL) are dropped, and a hard cap of 256 entries is enforced LRU-style on top. The fast path (warm cache hit) is unchanged; cleanup is amortised against the work the caller was about to do anyway.make build-fast: parallel package build. New target runs all nine SwiftPM packages in parallel via&+wait. On the dev machine: 8.0 s sequential → 0.5 s parallel for a no-op rebuild (16×), with ~3–4 s expected on a cold build. The originalmake buildstays sequential for clean log output when something fails.make doctor: pre-flight environment check. Verifies xcodegen / swift / xcodebuild / swiftlint / gh / dmgbuild / create-dmg / bundled7zz/ notarytool keychain profile in one shot, with brew/pip install hints for each missing piece. Avoids the late, ugly failures frommake release-publishdiscovering a missing tool partway through the pipeline.- Directory listing: skip cloud-status
lstatoutside iCloud-rooted paths. The post-listing pass that flagged cloud-only files vialstatran for every non-directory entry in every directory the user navigated into. On a 100K-file folder that's 100K syscalls; outside~/Library/Mobile Documents/,~/iCloud Drive/,~/Library/CloudStorage/(Ventura+ OneDrive/Dropbox/Box/GDrive sync mounts), and~/Dropbox/the result is always "not cloud-only", so the entire loop is now gated on a single string-prefix check. Listing time on large local directories drops to whatevercontentsOfDirectoryitself costs. - Folder size flush: O(N×M) → O(batch).
calculateFolderSizesInBackgroundflushed the running size map by re-scanning the entiretabs[idx].itemsarray every 20 folders and reconstructing every matchingFileItem. On directories with many subfolders that produced both quadratic time and a churn of FileItem allocations. Now snapshots aURL → indexmap once at task start, queues only the changed indices per batch, and verifies the slot still points at the expected URL before writing — handles the FileWatcher-fires-mid-task race correctly without scanning the whole array.
Fixed
- EXIF Editor opens with the cursor file when nothing is multi-selected.
ViewerWindowModifier's metadata-editor route filteredcurrentTab.selectedItemsdirectly — when the user just placed their cursor on an image without marking it, the editor opened at "0 image(s)" and they had to close, mark, re-open. Now usesselectedOrCursorItems(the project-wide convention per CLAUDE.md memory: "All file operations use selectedOrCursorItems"). Same pattern applied toFileSplitMergeView.onAppear(split-file picker pre-fill) andIntegratedTerminalView.SmartSuggestionViewso all three feel consistent — point at a file, run the action, no extra Cmd...
v1.0.9 — 1.0.8 design audit + Log Viewer overhaul + Settings resize
Highlights
1.0.8 was cut internally but never published. This release consolidates the 1.0.8 design audit + 10 new power-user features + 20+ Settings tunables with the subsequent Log Viewer overhaul, Settings resize fix, Theme Editor preset sidebar, and Compress dialog rework. Users upgrade 1.0.7 → 1.0.9 directly.
Added — Power-user features
- Type-ahead select — type any sequence to jump to the first matching file. Korean input is choseong-normalised so "민수.txt" matches "ㅁ".
- Spring-loaded folders — drag a file over a directory for 1 s to auto-navigate into it. Works on
..for parent navigation. - Tab right-click context menu — Rename / Pin / Duplicate / Move to Other Panel / Close Others / Close Tabs to the Right / Reveal in Finder.
- Breadcrumb click-to-edit — double-click the path bar for a raw-text URL editor.
- Dock icon context menu — Reveal Active Folder + Recent Folders (top 5 by frecency).
- "Send to Unifyl" macOS Service — right-click files in Finder / Mail / Safari → Services → "Send to Unifyl".
- Copy Path As… submenu under Edit — POSIX /
file://URI / shell-quoted / unix-shell / relative / filename only. - Reveal cursor file in opposite panel (⌃⌥O) — prep before compare / sync / diff.
- Cmd+1..9 tab jump — physical keyCode mapping, works under any IME.
- F7 New Folder sheet with pre-selected placeholder text.
- Log Viewer grep/find toggle — grep hides non-matching rows, find highlights in place. Plus "Load All" button for the rare 100 k+ line case.
- Theme Editor Built-in Presets sidebar — browse all 12 presets, right-click → Duplicate as Custom Theme.
- Active-panel focus desaturation — inactive panel at 50 % saturation + accent stripe atop the active PathBar.
Changed — Design (senior macOS designer audit)
- Theme presets now theme the full app chrome (PathBar / TabBar / StatusBar / OperationQueueBar / FunctionKeyBar) — previously frozen to Unifyl Dark Pro regardless of active preset.
- Per-row file-type colour flood removed — filename + icon still tint by category, Size / Date / Kind columns now use the theme's secondary label.
- F-key bar off by default. Re-enable via View ▸ Show Function Key Bar.
- PathBar hierarchy restored; Toolbar status simplified; toasts moved into the panel status bar instead of floating over the file list.
- AppTheme schema expanded with 10 new chrome + semantic tokens (
elevatedHex,overlayHex,borderSubtleHex,borderDefaultHex,textMutedHex,errorHex,warningHex,successHex,dropTargetHex,focusRingHex). - Preset curation: 13 → 12, contrast fixes on High Contrast (was invisible white-on-white selection), Solarized Light (washed-out selection), One Dark (wrong primary colour), Dracula (non-canonical selection).
Added — Settings (General pane)
- Size / Date / Double-click action pickers
- Row font size + Spring-loaded folder delay + Type-ahead reset + Toast dismiss sliders
- Show/hide toggles for every chrome element (gauge / git dots / hidden files / function key bar / operation queue / path bar)
- Confirmation toggles for Copy / Move / Delete / Permanent Delete / Overwrite
- Every new setting reflects in the UI immediately — no restart required.
Changed
- "Compress to ZIP…" → "Compress…" everywhere. Now opens a dialog with destination + filename + format (was silent-write).
- Log Viewer performance overhaul — UUID → Int line-number ids, level classification at ingest, debounced filter, 5 000-line tail cap.
- Settings window is resizable (was pinned at 550 × 480 despite 20+ new tunables).
Fixed
- F2 rename Cmd+A/C/V/X/Z under Korean / Japanese / Chinese IME — all dispatch now uses
event.keyCodeinstead ofcharacters. - Rename-onto-existing-file now asks Overwrite? (was hard-failing with "already exists").
- Theme Editor no longer opens multiple windows; no longer duplicates presets in the sidebar.
- Mouse wheel scroll no longer reverts to cursor position after a brief delay.
- Toast auto-dismiss audit — "Copied N paths", undo, Smart Folder add, PDF convert failure now all self-clear.
- Esc in Log Viewer no longer terminates the app (was quitting because SwiftUI treated it as the last remaining window).