diff --git a/README.md b/README.md
index b219865..bd9dc7f 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,10 @@
---
+
+
+
+
## Why?
I built this (heavily) with the help of [Claude Code](https://claude.ai/claude-code) because I've always wanted a macOS-native hex editor that is almost identical to [HxD](https://mh-nexus.de/en/hxd/) — because I don't like change. It also seemed like a good test for how far I could push almost entirely AI-driven development of a brand new application to serve my specific use case.
diff --git a/Strata/Resources/Info.plist b/Strata/Resources/Info.plist
index 3e8bca7..e966694 100644
--- a/Strata/Resources/Info.plist
+++ b/Strata/Resources/Info.plist
@@ -17,9 +17,9 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.1.0
+ 1.1.1
CFBundleVersion
- 2
+ 3
LSMinimumSystemVersion
$(MACOSX_DEPLOYMENT_TARGET)
NSHumanReadableCopyright
diff --git a/Strata/Sources/AppDelegate+Actions.swift b/Strata/Sources/AppDelegate+Actions.swift
index 3e7e936..35b5ba4 100644
--- a/Strata/Sources/AppDelegate+Actions.swift
+++ b/Strata/Sources/AppDelegate+Actions.swift
@@ -219,6 +219,76 @@ extension AppDelegate {
let alert = NSAlert(error: error)
alert.beginSheetModal(for: win)
}
+
+ // MARK: - Update Check
+
+ /// GitHub owner/repo for release checks.
+ static let gitHubRepo = "ConnorHowell/strata"
+
+ @objc func checkForUpdatesAction() {
+ let urlString = "https://api.github.com/repos/"
+ + "\(AppDelegate.gitHubRepo)/releases/latest"
+ guard let url = URL(string: urlString) else { return }
+
+ var request = URLRequest(url: url)
+ request.setValue("application/vnd.github+json", forHTTPHeaderField: "Accept")
+ request.timeoutInterval = 10
+
+ URLSession.shared.dataTask(with: request) { [weak self] data, _, error in
+ DispatchQueue.main.async {
+ self?.handleUpdateResponse(data: data, error: error)
+ }
+ }.resume()
+ }
+
+ private func handleUpdateResponse(data: Data?, error: Error?) {
+ guard let win = window else { return }
+
+ if let error {
+ let alert = NSAlert()
+ alert.messageText = "Update Check Failed"
+ alert.informativeText = error.localizedDescription
+ alert.alertStyle = .warning
+ alert.beginSheetModal(for: win)
+ return
+ }
+
+ guard let data,
+ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
+ let tagName = json["tag_name"] as? String else {
+ let alert = NSAlert()
+ alert.messageText = "Update Check Failed"
+ alert.informativeText = "Could not read release information from GitHub."
+ alert.alertStyle = .warning
+ alert.beginSheetModal(for: win)
+ return
+ }
+
+ let remote = tagName.trimmingCharacters(in: CharacterSet(charactersIn: "vV"))
+ let current = Bundle.main.object(
+ forInfoDictionaryKey: "CFBundleShortVersionString"
+ ) as? String ?? "0.0.0"
+
+ let alert = NSAlert()
+ if remote.compare(current, options: .numeric) == .orderedDescending {
+ alert.messageText = "Update Available"
+ alert.informativeText = "Version \(remote) is available "
+ + "(you have \(current))."
+ alert.addButton(withTitle: "Open Release Page")
+ alert.addButton(withTitle: "Later")
+ alert.beginSheetModal(for: win) { response in
+ if response == .alertFirstButtonReturn,
+ let htmlURL = json["html_url"] as? String,
+ let url = URL(string: htmlURL) {
+ NSWorkspace.shared.open(url)
+ }
+ }
+ } else {
+ alert.messageText = "You\u{2019}re Up to Date"
+ alert.informativeText = "Strata \(current) is the latest version."
+ alert.beginSheetModal(for: win)
+ }
+ }
}
// MARK: - Delegate Conformances
diff --git a/Strata/Sources/AppDelegate+Menus.swift b/Strata/Sources/AppDelegate+Menus.swift
index b5de6e2..db6e61e 100644
--- a/Strata/Sources/AppDelegate+Menus.swift
+++ b/Strata/Sources/AppDelegate+Menus.swift
@@ -42,6 +42,11 @@ extension AppDelegate {
action: #selector(showAboutPanel),
keyEquivalent: ""
)
+ menu.addItem(
+ withTitle: "Check for Updates\u{2026}",
+ action: #selector(checkForUpdatesAction),
+ keyEquivalent: ""
+ )
menu.addItem(.separator())
menu.addItem(
withTitle: "Quit Strata",
diff --git a/Strata/Sources/AppDelegate+Tools.swift b/Strata/Sources/AppDelegate+Tools.swift
index f800e74..e89c8b1 100644
--- a/Strata/Sources/AppDelegate+Tools.swift
+++ b/Strata/Sources/AppDelegate+Tools.swift
@@ -362,47 +362,18 @@ extension AppDelegate: StringsSheetDelegate {
guard let session = sessionManager.activeSession else { return }
let fileName = session.fileName
- // Resolve file URL on main thread to avoid capturing session
- // in the background closure.
- let fileURL: URL?
- let useFile: Bool
- if region == .entireFile, !session.isModified {
- fileURL = session.fileURL
- useFile = fileURL != nil
- } else {
- fileURL = nil
- useFile = false
- }
-
- // Resolve data on the main thread to avoid accessing UI
- // properties (e.g. hexGrid.selectedRange) from a background thread.
- let regionData: Data?
- if !useFile {
- regionData = dataForRegion(region)
- } else {
- regionData = nil
- }
+ // Always use piece table data — reading from the file URL
+ // directly fails in sandboxed builds where security-scoped
+ // access has expired.
+ guard let regionData = dataForRegion(region) else { return }
let progress = showProgressPanel(title: "Extracting strings\u{2026}")
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
- let matches: [StringMatch]
- if useFile, let url = fileURL {
- matches = StringsEngine.scanFile(
- url: url,
- minLength: minLength,
- encodings: encodings
- )
- } else {
- guard let data = regionData else {
- DispatchQueue.main.async { progress.close() }
- return
- }
- matches = StringsEngine.scan(
- data: data,
- minLength: minLength,
- encodings: encodings
- )
- }
+ let matches = StringsEngine.scan(
+ data: regionData,
+ minLength: minLength,
+ encodings: encodings
+ )
DispatchQueue.main.async {
progress.close()
guard let self else { return }
diff --git a/screenshot.png b/screenshot.png
new file mode 100644
index 0000000..ff6a8ce
Binary files /dev/null and b/screenshot.png differ