Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This project is vibe coded, no commit-by-commit review needed.
53 changes: 44 additions & 9 deletions Sources/Timber/TimberApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,27 +31,62 @@ final class SizingHostingView<Content: View>: NSHostingView<Content> {
}
}

/// Action icons, loaded from the bundled PNGs by explicit URL (proven to
/// resolve) instead of named lookup, with every scale wired so Retina
/// picks the right representation.
/// Action icons, loaded from the bundled PNGs by explicit URL instead of
/// named lookup, with every scale wired so Retina picks the right
/// representation.
///
/// The bundle is located defensively and never via Bundle.module: its
/// synthesized accessor calls fatalError when the bundle is missing,
/// which crashed the popover on first open in v0.1.1. A missing bundle
/// or missing PNG degrades to an SF Symbol instead of trapping.
enum TimberIcons {
static let zed = load("zed")
static let herdr = load("herdr")
static let zed = load("zed", fallbackSystemName: "terminal")
static let herdr = load("herdr", fallbackSystemName: "server.rack")

private static func load(_ name: String) -> NSImage {
private static func load(_ name: String, fallbackSystemName: String) -> NSImage {
if let image = bundledImage(named: name) {
return image
}
return NSImage(systemSymbolName: fallbackSystemName, accessibilityDescription: name) ?? NSImage()
}

private static func bundledImage(named name: String) -> NSImage? {
let dirs = candidateDirs()
guard let bundleURL = TimberModel.resourceBundleURL(bundleName: "Timber_Timber", candidateDirs: dirs) else {
return nil
}
guard let bundle = Bundle(url: bundleURL) else {
return nil
}
let image = NSImage()
var found = false
for scale in ["@3x", "@2x", ""] {
guard let url = Bundle.module.url(forResource: "\(name)\(scale)", withExtension: "png"),
let src = NSImage(contentsOf: url)
else { continue }
guard let url = bundle.url(forResource: "\(name)\(scale)", withExtension: "png") else { continue }
guard let src = NSImage(contentsOf: url) else { continue }
found = true
for rep in src.representations {
rep.size = NSSize(width: 14, height: 14)
image.addRepresentation(rep)
}
}
guard found else {
return nil
}
image.size = NSSize(width: 14, height: 14)
return image
}

private static func candidateDirs() -> [URL] {
var dirs: [URL] = []
if let resources = Bundle.main.resourceURL {
dirs.append(resources)
}
dirs.append(Bundle.main.bundleURL)
if let executables = Bundle.main.executableURL?.deletingLastPathComponent() {
dirs.append(executables)
}
return dirs
}
}

final class AppDelegate: NSObject, NSApplicationDelegate {
Expand Down
19 changes: 19 additions & 0 deletions Sources/TimberModel/TimberModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,25 @@ public enum TimberModel {
"\"" + text.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"") + "\""
}

// MARK: - Resource bundle lookup

/// Locate `<name>.bundle` under the first candidate dir that
/// contains it, without trapping: unlike the synthesized
/// `Bundle.module` accessor (which calls fatalError when the bundle
/// is missing), absence yields nil so callers can degrade gracefully
/// (e.g. fall back to SF Symbols for icons).
public static func resourceBundleURL(bundleName: String, candidateDirs: [URL]) -> URL? {
for dir in candidateDirs {
let url = dir.appendingPathComponent(bundleName + ".bundle", isDirectory: true)
var isDirectory: ObjCBool = false
let exists = FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory)
if exists, isDirectory.boolValue {
return url
}
}
return nil
}

// MARK: - Child process environment

/// PATH handed to `timber` children. GUI apps launch with a minimal
Expand Down
61 changes: 61 additions & 0 deletions Tests/TimberModelTests/TimberModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,67 @@ final class TimberModelTests: XCTestCase {
)
}

// MARK: - resource bundle lookup

/// Fixture mirroring the v0.1.1 installed layout: a flat
/// `<name>.bundle` directory (PNGs at top level, no Info.plist)
/// that the synthesized Bundle.module failed to resolve, trapping
/// the popover on first open. The defensive lookup must find it.
func testResourceBundleURLFindsFlatBundle() throws {
let root = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString, isDirectory: true)
let bundle = root.appendingPathComponent("Timber_Timber.bundle", isDirectory: true)
try FileManager.default.createDirectory(at: bundle, withIntermediateDirectories: true)
FileManager.default.createFile(atPath: bundle.appendingPathComponent("zed.png").path, contents: Data())
addTeardownBlock { try? FileManager.default.removeItem(at: root) }

let found = TimberModel.resourceBundleURL(bundleName: "Timber_Timber", candidateDirs: [root])
XCTAssertEqual(found, bundle)
// And plain Foundation resolution works on what we found.
let loaded = found.flatMap(Bundle.init(url:))
XCTAssertNotNil(loaded?.url(forResource: "zed", withExtension: "png"))
}

func testResourceBundleURLFindsStructuredBundle() throws {
let root = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString, isDirectory: true)
let resources = root.appendingPathComponent("Timber_Timber.bundle/Contents/Resources", isDirectory: true)
try FileManager.default.createDirectory(at: resources, withIntermediateDirectories: true)
FileManager.default.createFile(atPath: resources.appendingPathComponent("zed.png").path, contents: Data())
addTeardownBlock { try? FileManager.default.removeItem(at: root) }

let found = TimberModel.resourceBundleURL(bundleName: "Timber_Timber", candidateDirs: [root])
XCTAssertNotNil(found)
XCTAssertNotNil(found.flatMap(Bundle.init(url:))?.url(forResource: "zed", withExtension: "png"))
}

func testResourceBundleURLReturnsNilWhenMissing() {
let missing = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString, isDirectory: true)
XCTAssertNil(TimberModel.resourceBundleURL(bundleName: "Timber_Timber", candidateDirs: [missing]))
XCTAssertNil(TimberModel.resourceBundleURL(bundleName: "Timber_Timber", candidateDirs: []))
}

func testResourceBundleURLPrefersFirstMatch() throws {
let first = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString, isDirectory: true)
let second = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString, isDirectory: true)
for root in [first, second] {
try FileManager.default.createDirectory(
at: root.appendingPathComponent("Timber_Timber.bundle", isDirectory: true),
withIntermediateDirectories: true
)
}
addTeardownBlock {
try? FileManager.default.removeItem(at: first)
try? FileManager.default.removeItem(at: second)
}

let found = TimberModel.resourceBundleURL(bundleName: "Timber_Timber", candidateDirs: [first, second])
XCTAssertEqual(found?.deletingLastPathComponent(), first)
}

// MARK: - child PATH

func testChildPATHPrependsMissingDirs() {
Expand Down
Loading