Skip to content

Releases: goodbug89/Unifyl.app

Unifyl 1.4.0

29 May 02:14

Choose a tag to compare

[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.md for 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) observes Transaction.currentEntitlements for the non-consumable IAP com.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 into LicenseManager.storeKitTier. The UpgradeSheet on MAS-Lite now shows the localized App Store price (via Product.displayPrice) and routes the Buy Pro button to Product.purchase() instead of the Direct LemonSqueezy checkout — Direct builds still use LemonSqueezy unchanged (#if MAS_LITE keeps the two paths cleanly separated). A new Restore Purchases button covers the "I bought it on a different Mac" flow. Setup guide at docs/business/mas-iap-setup.md covers the App Store Connect IAP record + sandbox testing + review submission.
  • LicenseManager.activatedTier is now max(lemonSqueezyTier, storeKitTier). Both unlock paths feed the same FeatureGateManager.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 in docs/business/freemium-tiers.md but 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

27 May 12:07

Choose a tag to compare

[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/zipinfo subprocess 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 between InProcessZipReader.Entry (UInt32 sizes) and the local XRayNode-shaped Entry (UInt64). FileXRayView gains an import 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 ffmpeg has no Foundation equivalent and MAS forbids unsigned CLI binaries in Resources/. 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]. Every Process() spawn in the MAS-Lite binary is documented and either compiled out via #if MAS_LITE or 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.textutilNSAttributedString. UnifylFileSystem test count: 102 → 128 (26 new tests for the new libraries).

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 structured Analysis { isFat, architectures, slices } so the FileXRay tree view can group fat slices and show typed fields per LC instead of text-scraping otool output. Companion InProcessDMGReader detects 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 + analyzeDiskImage migrated — both targets share the new in-process path, the now-unused runProcess(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 -m in 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 … ======= … >>>>>>> RIGHT markers identical to diff3 -m so ThreeWayMergeView.parseConflicts reads the output unchanged). ThreeWayMergeView.analyzeMerge #if MAS_LITE branch migrated — reads file bytes off-main via Task.detached, calls Diff3Merger.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 via compression_encode_buffer(COMPRESSION_ZLIB) + 8-byte CRC-32 / ISIZE footer per RFC 1952). Round-trips through host /usr/bin/tar (-tf lists every entry, -xf restores byte-for-byte; same for -tzf / -xzf on the gzip path). Long-path handling splits paths > 100 chars into USTAR prefix + name on the latest / boundary that fits; rejects paths beyond 255 chars with typed .pathTooLong. CompressDialogView.compressTarWithProgress #if MAS_LITE branch migrated — MAS-Lite can now create .tar and .tar.gz archives in-process with per-entry progress. Direct keeps /usr/bin/tar. CRC-32 implementation extracted to a shared InternalCRC32 enum 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.gz round-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's compression_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 via FileManager.enumerator with nextObject() (Swift 6 async-safe); resolves symlinks before building entry-relative paths (/tmp/private/tmp on 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 -P path. CompressDialogView.compressZipWithProgress #if MAS_LITE branch migrated — MAS-Lite can now create ZIP archives in-process with progress reporting per entry. Direct keeps /usr/bin/zip for 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_ZLIB operates 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 (sentinel 0xFFFFFFFF on size fields) and unsupported compression methods with typed ReaderError. OfficePreviewView.parseAllSheets migrated to use it for both Direct AND MAS-Lite — both targets now read xlsx in-process with zero ditto subprocess, 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_LITE path now does a real in-process directory sweep instead of returning an empty stub. Uses FileManager.default.enumerator(at:, options: [.skipsHiddenFiles, .skipsPackageDescendants]) walked via .nextObject() (Swift 6 async-safe), filters by extension (the existing *.swift / *.txt glob input), respects case-sensitivity, caps individual file size at 16 MB, caps total matches at 50 (same as the Direct path's grep -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/grep for 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...
Read more

Unifyl 1.3.1

26 May 00:32

Choose a tag to compare

[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): convertDocToPDF for legacy .doc / .docx / .rtf / fallback no longer spawns /usr/bin/textutil. Reads Data(contentsOf:) off-main, then constructs NSAttributedString(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 new Self.renderAttributedToPDF(_:destination:) helper extracted from the existing convertTextToPDF so 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-target com.apple.security.scripting-targets entitlement plus explicit consent. Reclassified as [drop] for MAS-Lite; Direct keeps the existing AppleScript flow.
    • AudioConverterView afconvert wrapped with #if MAS_LITE: /usr/bin/afconvert is Apple-bundled but Process() is still sandbox-banned. Returns a localized ConversionResult(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. Both Unifyl (Debug) and Unifyl MASLite (Debug) xcodebuild BUILD SUCCEEDED. UnifylCore tests 131/131, UnifylFileSystem 102/102.

Added

  • Separate UnifylMASLite Xcode target shipped: project.yml now defines a parallel application target that produces a Mac App Store-ready build alongside the Direct distribution. com.unifyl.app.maslite bundle ID (vs com.unifyl.app), separate Info.MASLite.plist (no Sparkle keys), separate Unifyl.MASLite.entitlements (sandbox=on, user-selected files RW, security-scoped bookmarks, network.client, print), SWIFT_ACTIVE_COMPILATION_CONDITIONS=MAS_LITE so all 9 [drop] wraps fire, no Sparkle SPM dependency, no 7zz/unalz Resources/bin/ postBuildScript. Two Xcode schemes (Unifyl MASLite (Debug) + (Release)) for everyday dev and archive. Sparkle imports in UnifylApp.swift / AboutView.swift / CheckForUpdatesView.swift (and their menu/button call sites) are #if !MAS_LITE-gated so neither variant pulls the framework needlessly. Verified: xcodebuild of both targets succeeds; the built UnifylMASLite.app has com.apple.security.app-sandbox live, no Frameworks/Sparkle.framework, no Resources/bin/. The two .app bundles can be installed side-by-side on one Mac.

Added

  • MAS-Lite scaffold complete — 9 of 9 [drop] Process() sites wrapped: #if MAS_LITE guards 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 typed LocalizedError enum (per audit-guard F11). Both xcodebuild (default) and xcodebuild OTHER_SWIFT_FLAGS=-DMAS_LITE BUILD 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 audit reports 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 the unifyl.feature.koreanGovFormDetection opt-out.
  • F7 fingerprint database scaffold: KoreanGovFormDatabase + bundled Resources/data/kr-gov-forms.json seed with 3 placeholder entries (표준 도급 계약서 / 부가가치세 신고서 / 민원 신청서) + SHA-256 + bytes-level matcher. Curation guide at scripts/data/README-kr-gov-forms.md documents the schema and 50-form rollout target. 8 unit tests on the loader + match logic.
  • MAS-Lite compile-flag scaffold: Unifyl/Automation/ScriptRunner.swift demonstrates the canonical #if MAS_LITE / #else wrap pattern — throws a typed LaunchError.unavailableInMASVariant instead of running Process() when the flag is set. Both xcodebuild (default Direct path) and xcodebuild OTHER_SWIFT_FLAGS=-DMAS_LITE (MAS-Lite path) succeed. Future MAS-Lite target just sets SWIFT_ACTIVE_COMPILATION_CONDITIONS=MAS_LITE and incrementally wraps the remaining 11 [drop] sites. Updated docs/business/mac-app-store-readiness.md to mark the audit prep-work checkbox ✅.

Added

  • CJK wedge F7-lite — Korean government form heuristic detection: new KoreanGovFormDetector enum 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 can grep -r MAS-INCOMPAT Unifyl/ | sort -u to 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!.isEmpty condition relied on a manual nil-check + force-unwrap. Rewrote as marker.map { !$0.isEmpty } ?? false so 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 single FileOperationHistory.encodingConvert op 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 PerformanceTests suite in UnifylCore that exercises ASCII substring / CJK substring / multilingual expansion / fullwidth fold / regex / glob paths against a 10K-item synthetic listing. Runs as part of swift 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 / performConversion now hop to Task.detached so per-file read + decode + write don't freeze the spinner during a multi-file batch. Detection and ConversionResult gained explicit Sendable conformance for the boundary crossing.
  • Fire-and-forget Task after encoding conversion: the prior code chained Task { sleep(2); refreshPanel; dismiss } which raced manual sheet dismissa...
Read more

Unifyl 1.3.0 — CJK wedge + Global rollout

17 May 07:44

Choose a tag to compare

[1.3.0] — 2026-05-17

Added

  • CJK wedge F1 — ALZ archive support: .alz archives (Korean legacy format from ESTsoft's ALZip) now extract via bundled unalz binary (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 via scripts/build_landing_i18n.py. Each variant ships with the correct lang/dir attribute, hreflang map (15 + x-default), OG locale tag, and an in-nav language switcher. Arabic gets dir="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.strings for all 15 locales. Localizes NSHumanReadableCopyright; CFBundleDisplayName / CFBundleName stay 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/rightchevron.backward/forward), breadcrumb separators in PathBarView and ArchivePathBarView (chevron.rightchevron.forward), and rename-flow indicators in MultiRenameView / RegexRenameView / LargeFileFinderView (arrow.rightarrow.forward). Bidirectional icons (arrow.left.arrow.right) left as-is.

Unifyl 1.2.3

12 May 23:05

Choose a tag to compare

[1.2.3] — 2026-05-13

Fixed

  • 2 NSAlert sites in sheet-hosted views switched to beginSheetModal(for:) instead of runModal(). NLCommandPreviewSheet (sheet host confirmed at MainWindowView:631) and AIEngineViewModel.applyKeepBest (called from EnhancedDuplicateView, 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 PersistenceBackup instead of try?-wiping user data. The pattern guard 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, use stashCorruptFile), 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 from defaults read); on-disk files are moved to <name>.corrupt.<timestamp>.<ext> sidecars. TeamSyncClient's 403-body decode is intentionally left on try? because that's a server-controlled response, not user data. Per memory feedback_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. The fatalError is preserved as a backstop for the (unreachable) case where bundled SQLite itself is broken, so support still gets a crash report. Added VectorIndex.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-infers LocalizedStringKey from 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 any Localizable.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 sheet NSOpenPanel.runModal() / NSSavePanel.runModal() is silently swallowed by the outer modal hierarchy. The buttons appeared dead. Switched all 9 sites across 7 views to panel.begin { … } (application-modal, attaches above the sheet) with a Task { @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, NewConnectionSheet SSH-key picker, AdvancedSearchView × 3 (scope change, copy destination, move destination), IncrementalBackupView.pickFolder, FileSplitMergeView × 2 (split source, merge first-part), SemanticSearchView Choose-Directory button, FileOperationConfirmView destination Browse, CompressDialogView destination Browse, MediaConverterView output-folder Choose. After this pass, every confirmed sheet-hosted runModal() on either NSAlert or panel is gone. CustomTheme.exportTheme/importTheme keep runModal() because they're called from ThemeEditorView, which is hosted as an independent NSPanel via ViewerWindowManager — not a sheet, so runModal() works correctly there.
  • Companion error messages now ship native translations in all 15 locales instead of always rendering in Korean. CompanionError.errorDescription had 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 with NSLocalizedString(key, value:, comment:) using English base values; new keys carry the companion.error.* prefix.
  • AI auto-classify suggestion reasons translate to all 15 locales. AIClassifier.describeReason returned three hardcoded Korean strings ("N개의 유사한 파일이 존재", "관련 파일 N개 존재", "폴더명 유사도 기반 추천") that appeared in the classification preview next to every suggested destination. Wrapped each in NSLocalizedString(value:) with English source; new keys are ai.classify.reason.similarExtension, ai.classify.reason.relatedFiles, ai.classify.reason.folderName.
  • Permission-denied banner no longer relies on substring-matching the localized error message. PanelView decided whether to show the "Open System Settings ▸ Privacy & Security" affordance by doing err.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. Added PanelViewModel.lastErrorIsPermission — a typed flag set in loadCurrentDirectory's catch from Error.isPermissionDeniedError, which pattern-matches Cocoa fileRead/WriteNoPermission, POSIX EACCES/EPERM, and UnifylFileError.permissionDenied instead of the localized message. Reset alongside lastError = nil everywhere 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's isHelpOpen handler matched NSApp.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 (or fn+delete on laptops, keyCode 117) did nothing. Wired in both FileTableView.handleKeyDown (when the file table has focus) and FunctionKeyMonitor (when focus is elsewhere — pathbar / toolbar / etc.) so the affordance is consistent across the window. Backspace alone (no modifier) still navigates to the parent directory — that's the TC + Finder convention and stays put.
  • FileTableView.userDefaultsDidChange no longer SIGTRAPs when defaults are written off-main. UserDefaults.didChangeNotification posts on whatever thread modified defaults (e.g. XCTTelemetryLogger.+initialize triggers registerDefaults: on a background dispatch queue during test bundle init), and Swift 6 strict concurrency traps when the @mainactor applyColumnVisibility() is called from there. Marked the selector nonisolated and hopped to MainActor via Task. 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\n line endings. Swift's String iteration groups \r\n into a single extended grapheme cluster that matched neither char == "\r" nor char == "\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 with Character.isNewline, which matches \n / \r / \r\n + Unicode line separators (\u{0085}, \u{2028}, \u{2029}) uniformly. The pre-existing if char == "\r" { continue } branch was dead code (never reached because Swift had already merged the CR+LF). Caught by the existing handlesCRLF unit 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 → ...
Read more

Unifyl 1.2.2

11 May 01:34

Choose a tag to compare

[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 in Localizable.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 English value: 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.humaneMessage extension that maps low-level Foundation / POSIX / NSURLError codes to localized user-facing strings. Falls through to errorDescription for LocalizedError-conforming types (UnifylFileError, VectorIndexError), and to localizedDescription as 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 via WindowFrameAutosave since 1.1.0; this brings viewers to parity.

Changed

  • All user-facing error banners now route through error.humaneMessage instead of error.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 for EACCES / ENOSPC / ENOENT / EBUSY / etc. instead of raw English localizedDescription from the system. OSLog / UnifylLogger calls are intentionally left on localizedDescription so 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 through DesignTokens.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. .blue and .accentColor left 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 .stringsdict for 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 to String.localizedStringWithFormat(NSLocalizedString("ai.renameSuggest", …), count). Native macOS plural infrastructure now picks the right form per locale.
  • Hidden-file toggle (⌘⇧.) now persists across launches. AppViewModel.showHiddenFiles was a plain var initialised to false, so every relaunch reset it — a developer who flipped it for the session had to re-toggle each launch. Added a didSet observer that mirrors the splitRatio / bookmarks persistence pattern (@AppStorage doesn't work on @Observable stored properties — init-accessor synthesis rejects it), backed by unifyl.showHiddenFiles in UserDefaults.
  • OAuth callback URL scheme now declared in Info.plist. OAuth2Configuration ships redirectURI = "com.unifyl.basic://oauth2/callback" for Native Google Drive / Dropbox / OneDrive providers, but CFBundleURLTypes was missing — macOS had no app to route the callback to, so the auth flow would hang forever. Added a single URL-types entry registering com.unifyl.basic. (Native OAuth providers are still gated behind unifyl.enableOAuthPilot while 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".csv becomes curly-quoted, foo--bar.txt becomes en-dashed, omw to a folder expands via System Settings text replacement). applicationDidFinishLaunching flips the four NSAutomatic*Enabled defaults to false for 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 next save() overwrote the disk file with []. Now stashes the corrupt file via PersistenceBackup.stashCorruptFile (recoverable for support) and logs to UnifylLogger.settings. Same pattern applied to the import-from-file path so a malformed .ultratheme file logs the parse error instead of vanishing.
  • A11y polish on tab bar + status bar. TabBar's xmark close button gained .accessibilityLabel("Close Tab") (was announcing as a generic "button"); the pin.fill and cloud.fill chips 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.buildRequest gets timeoutInterval = 15 s (matches LemonSqueezyClient); IconThemeManager per-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.
  • applicationWillTerminate now 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 calls UserDefaults.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 errData was a captured var Data() mutated from Pipe.readabilityHandler callbacks while the outer Task.detached owned the closure scope — Swift 6 strict concurrency flagged it as a real data race; wrapped in a lock-protected ErrDataBox (@unchecked Sendable, NSLock-guarded). UnifylTests' override func setUp() / tearDown() were declared without async throws in @MainActor classes, breaking MainActor isolation; converted to async-throws variants with try await super.<x>(). Cloud HTTP calls (Dropbox/Google/OneDrive — post/patch/delete × 8) had unused try 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.detached sites across Unifyl/ and Packages/. No real retain risk found. Most closures use closure-local data plus Self.<static> calls. The few sites that touch self either 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

09 May 23:52

Choose a tag to compare

[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 the isUnlocked(.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

04 May 05:54

Choose a tag to compare

[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 NSViewRepresentable overlay that intercepts only otherMouseDown — 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.fill icon 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.showConnectionsSidebar AppStorage. Was per-window @State that 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 Space chip + "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.launchTipIndex counter 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 via defaults 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 a Task loop 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 Files is off, the message also notes that hidden items might exist and offers a one-click Show Hidden Files action 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.shortDescription covering 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...
Read more

Unifyl 1.1.0

29 Apr 22:58

Choose a tag to compare

[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 a UserDefaults.didChangeNotification observer in FileTableView.
  • 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 via setAttributes([.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 NSEvent monitor, and respects the existing unifyl.confirmOverwrite user 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 and NSTextView keep 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 to Data(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 via Data(contentsOf: fileURL) synchronously on the @mainactor — a 10 MB read from a slow network volume blocked the entire main thread. Read now happens in a Task.detached(.userInitiated) with .mappedIfSafe, and the result is written back through a Result on the main actor.
  • Panel error banner gets icon + semantic color + a11y label. panelVM.lastError was a flat red bar — color-blind users had no other signal it was an error. Now it's an exclamationmark.triangle.fill + text in DesignTokens.Colors.error, with accessibilityLabel so 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 and GitHistoryView's error icon migrated from raw .green / .red / .orange to Colors.success / Colors.error / Colors.warning. The Git error icon also moved from exclamationmark.triangle to the filled variant for higher visual weight.
  • Eight more empty states unified onto EmptyStateView. JSONPreviewView, PlistPreviewView, FontPreviewView, ScenePreviewView, SQLitePreviewView, AppUninstallerView (×2), KeyBindingSettingsView, ThreeWayMergeView all migrated from ContentUnavailableView to the project-style EmptyStateView. Error-state previews pass iconColor: Colors.error so the icon hue matches the message intent.
  • Sheet padding tokenised (final batch). ChecksumView, FileOperationConfirmView, FilePermissionsView migrated from .padding(20) / .padding(16) to DesignTokens.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; existing bgSelected becomes 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 / .red literals across views. Code reading "this label uses Colors.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 textTertiary between textSecondary and textMuted so column headers and count badges no longer collapse into the same value as disabled labels — restores visual hierarchy at typical viewing distance.
    • borderSubtle strengthened (#1E2235#232742). The previous value was only ~2% lighter than bgSurface, so divider lines vanished at typical viewing distance. New value still reads as "subtle" but actually separates sections.
    • focusRing token (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. bgSelectedbgSelectedActive for the focused panel pulls ~30% more accent saturation; the "this row is selected" cue is now unambiguous against the hover background.

Performance

  • GitStatusProvider cache 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 original make build stays sequential for clean log output when something fails.
  • make doctor: pre-flight environment check. Verifies xcodegen / swift / xcodebuild / swiftlint / gh / dmgbuild / create-dmg / bundled 7zz / notarytool keychain profile in one shot, with brew/pip install hints for each missing piece. Avoids the late, ugly failures from make release-publish discovering a missing tool partway through the pipeline.
  • Directory listing: skip cloud-status lstat outside iCloud-rooted paths. The post-listing pass that flagged cloud-only files via lstat ran 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 whatever contentsOfDirectory itself costs.
  • Folder size flush: O(N×M) → O(batch). calculateFolderSizesInBackground flushed the running size map by re-scanning the entire tabs[idx].items array every 20 folders and reconstructing every matching FileItem. On directories with many subfolders that produced both quadratic time and a churn of FileItem allocations. Now snapshots a URL → index map 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 filtered currentTab.selectedItems directly — 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 uses selectedOrCursorItems (the project-wide convention per CLAUDE.md memory: "All file operations use selectedOrCursorItems"). Same pattern applied to FileSplitMergeView.onAppear (split-file picker pre-fill) and IntegratedTerminalView.SmartSuggestionView so all three feel consistent — point at a file, run the action, no extra Cmd...
Read more

v1.0.9 — 1.0.8 design audit + Log Viewer overhaul + Settings resize

22 Apr 09:55

Choose a tag to compare

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.keyCode instead of characters.
  • 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).

Full changelog