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
14 changes: 14 additions & 0 deletions Sources/Stag/Capture/CaptureFilename.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Foundation

/// Assembles capture filenames from their parts so the real save path
/// (`CaptureManager`) and the settings preview (`PreferencesWindow`) share one
/// rule and can't drift. Format: `<prefix><slug + space?><timestamp>.<ext>`.
enum CaptureFilename {
/// Builds a capture filename. An empty `prefix` falls back to `"Stag_"`; an
/// empty `slug` is omitted along with its separating space.
static func make(prefix: String, slug: String, timestamp: String, ext: String) -> String {
let resolvedPrefix = prefix.isEmpty ? "Stag_" : prefix
let middle = slug.isEmpty ? "" : "\(slug) "
return "\(resolvedPrefix)\(middle)\(timestamp).\(ext)"
}
}
11 changes: 7 additions & 4 deletions Sources/Stag/CaptureManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -363,13 +363,16 @@ final class CaptureManager {
let prefs = store.preferences
let saveDir = URL(fileURLWithPath: prefs.expandedSavePath)
try? FileManager.default.createDirectory(at: saveDir, withIntermediateDirectories: true)
let ext = prefs.defaultFormat == .jpeg ? "jpg" : "png"
let prefix = prefs.filePrefix.isEmpty ? "Stag_" : prefs.filePrefix
// Smart filename: insert the source app (e.g. "Slack") between the prefix
// and the timestamp when available — falls back to prefix+timestamp.
let slug = prefs.useSmartFilenames ? CaptureContext.shared.filenameSlug() : ""
let middle = slug.isEmpty ? "" : "\(slug) "
let url = saveDir.appendingPathComponent("\(prefix)\(middle)\(Date().shotTimestamp).\(ext)")
let filename = CaptureFilename.make(
prefix: prefs.filePrefix,
slug: slug,
timestamp: Date().shotTimestamp,
ext: prefs.defaultFormat.fileExtension
)
let url = saveDir.appendingPathComponent(filename)

switch prefs.defaultFormat {
case .png: image.pngWrite(to: url)
Expand Down
8 changes: 8 additions & 0 deletions Sources/Stag/Models/Preferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ import Combine

enum CaptureFormat: String, Codable, CaseIterable {
case png, jpeg

/// File extension for saved captures ("png" / "jpg").
var fileExtension: String {
switch self {
case .png: return "png"
case .jpeg: return "jpg"
}
}
}

enum AfterCaptureAction: String, Codable, CaseIterable {
Expand Down
10 changes: 7 additions & 3 deletions Sources/Stag/Views/PreferencesWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -599,9 +599,13 @@ private struct PreferencesView: View {
// MARK: Helpers

private var filenamePreview: String {
let prefix = prefs.filePrefix.isEmpty ? "Stag_" : prefs.filePrefix
let mid = prefs.useSmartFilenames ? "Safari " : ""
return "\(prefix)\(mid)2026-01-01.png"
let slug = prefs.useSmartFilenames ? "Safari" : ""
return CaptureFilename.make(
prefix: prefs.filePrefix,
slug: slug,
timestamp: "2026-01-01",
ext: prefs.defaultFormat.fileExtension
)
}

private func actionDisplayName(_ action: AfterCaptureAction) -> String {
Expand Down
46 changes: 46 additions & 0 deletions Tests/StagTests/CaptureFilenameTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import XCTest
@testable import Stag

/// Filename assembly shared by CaptureManager (real save) and PreferencesWindow
/// (settings preview), plus CaptureFormat's extension mapping.
final class CaptureFilenameTests: XCTestCase {

func testEmptyPrefixFallsBackToStag() {
XCTAssertEqual(
CaptureFilename.make(prefix: "", slug: "", timestamp: "2026-01-01", ext: "png"),
"Stag_2026-01-01.png"
)
}

func testCustomPrefixIsUsedVerbatim() {
XCTAssertEqual(
CaptureFilename.make(prefix: "Shot-", slug: "", timestamp: "2026-01-01", ext: "png"),
"Shot-2026-01-01.png"
)
}

func testSlugInsertedWithSeparatingSpace() {
XCTAssertEqual(
CaptureFilename.make(prefix: "", slug: "Slack", timestamp: "T", ext: "png"),
"Stag_Slack T.png"
)
}

func testEmptySlugOmitsSpace() {
let result = CaptureFilename.make(prefix: "Stag_", slug: "", timestamp: "T", ext: "jpg")
XCTAssertEqual(result, "Stag_T.jpg")
XCTAssertFalse(result.contains(" "))
}

func testExtensionIsApplied() {
XCTAssertEqual(
CaptureFilename.make(prefix: "P", slug: "App", timestamp: "ts", ext: "jpg"),
"PApp ts.jpg"
)
}

func testCaptureFormatFileExtension() {
XCTAssertEqual(CaptureFormat.png.fileExtension, "png")
XCTAssertEqual(CaptureFormat.jpeg.fileExtension, "jpg")
}
}
Loading