From 763eea0e5e6a3e97624b36fe903b457b4e0c8de8 Mon Sep 17 00:00:00 2001 From: h3p Date: Sat, 14 Feb 2026 23:15:22 +0100 Subject: [PATCH] release: prepare v0.4.15 --- CHANGELOG.md | 5 ++ Neon Vision Editor.xcodeproj/project.pbxproj | 8 +-- .../Core/AppUpdateManager.swift | 57 ++++++++++++++-- Neon Vision Editor/UI/AppUpdaterDialog.swift | 67 ++++++++++++------- Neon Vision Editor/UI/ContentView.swift | 18 ++++- Neon Vision Editor/UI/EditorTextView.swift | 3 + Neon Vision Editor/UI/NeonSettingsView.swift | 20 +++++- README.md | 12 ++-- 8 files changed, 149 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0d9a7c..570930f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to **Neon Vision Editor** are documented in this file. The format follows *Keep a Changelog*. Versions use semantic versioning with prerelease tags. +## [v0.4.15] - 2026-02-14 + +### Fixed +- Fixed the editor `Highlight Current Line` behavior on macOS so previous line background highlights are cleared and only the active line remains highlighted. + ## [v0.4.14] - 2026-02-14 ### Added diff --git a/Neon Vision Editor.xcodeproj/project.pbxproj b/Neon Vision Editor.xcodeproj/project.pbxproj index ef6ba68..ec00b5e 100644 --- a/Neon Vision Editor.xcodeproj/project.pbxproj +++ b/Neon Vision Editor.xcodeproj/project.pbxproj @@ -358,7 +358,7 @@ CODE_SIGNING_ALLOWED = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 228; + CURRENT_PROJECT_VERSION = 229; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = CS727NF72U; ENABLE_APP_SANDBOX = YES; @@ -401,7 +401,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 0.4.14; + MARKETING_VERSION = 0.4.15; PRODUCT_BUNDLE_IDENTIFIER = "h3p.Neon-Vision-Editor"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -438,7 +438,7 @@ CODE_SIGNING_ALLOWED = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 228; + CURRENT_PROJECT_VERSION = 229; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = CS727NF72U; ENABLE_APP_SANDBOX = YES; @@ -481,7 +481,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 0.4.14; + MARKETING_VERSION = 0.4.15; PRODUCT_BUNDLE_IDENTIFIER = "h3p.Neon-Vision-Editor"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Neon Vision Editor/Core/AppUpdateManager.swift b/Neon Vision Editor/Core/AppUpdateManager.swift index ef245c3..acc300c 100644 --- a/Neon Vision Editor/Core/AppUpdateManager.swift +++ b/Neon Vision Editor/Core/AppUpdateManager.swift @@ -109,6 +109,9 @@ final class AppUpdateManager: ObservableObject { @Published private(set) var automaticPromptToken: Int = 0 @Published private(set) var isInstalling: Bool = false @Published private(set) var installMessage: String? + @Published private(set) var installProgress: Double = 0 + @Published private(set) var installPhase: String = "" + @Published private(set) var awaitingInstallCompletionAction: Bool = false @Published private(set) var lastCheckResultSummary: String = "Never checked" private let owner: String @@ -250,7 +253,7 @@ final class AppUpdateManager: ObservableObject { automaticPromptToken &+= 1 installMessage = "Automatic install is disabled while running from Xcode/DerivedData." } else { - await attemptAutoInstall() + await attemptAutoInstall(interactive: false) } } else { pendingAutomaticPrompt = true @@ -321,6 +324,9 @@ final class AppUpdateManager: ObservableObject { func clearInstallMessage() { installMessage = nil + installProgress = 0 + installPhase = "" + awaitingInstallCompletionAction = false } func installUpdateNow() async { @@ -332,7 +338,24 @@ final class AppUpdateManager: ObservableObject { installMessage = "Install now is disabled while running from Xcode/DerivedData. Use Download Update instead." return } - await attemptAutoInstall() + await attemptAutoInstall(interactive: true) + } + + func completeInstalledUpdate(restart: Bool) { +#if os(macOS) + guard awaitingInstallCompletionAction else { return } + let currentApp = Bundle.main.bundleURL.standardizedFileURL + if restart { + installMessage = "Restarting app…" + NSWorkspace.shared.openApplication(at: currentApp, configuration: NSWorkspace.OpenConfiguration(), completionHandler: nil) + } else { + installMessage = "Closing app to finish update…" + } + awaitingInstallCompletionAction = false + NSApp.terminate(nil) +#else + installMessage = "Automatic install is supported on macOS only." +#endif } private func shouldRunInitialCheckNow() -> Bool { @@ -486,7 +509,7 @@ final class AppUpdateManager: ObservableObject { } } - private func attemptAutoInstall() async { + private func attemptAutoInstall(interactive: Bool) async { #if os(macOS) guard !isInstalling else { return } guard let release = latestRelease else { return } @@ -500,6 +523,9 @@ final class AppUpdateManager: ObservableObject { } isInstalling = true + installProgress = 0.01 + installPhase = "Preparing installer…" + awaitingInstallCompletionAction = false defer { isInstalling = false } do { @@ -507,11 +533,15 @@ final class AppUpdateManager: ObservableObject { // 1) verify artifact checksum from release metadata // 2) verify code signature validity + signing identity let expectedHash = try Self.extractSHA256(from: release.notes, preferredAssetName: assetName) + installProgress = 0.12 + installPhase = "Downloading release asset…" let (tmpURL, response) = try await session.download(from: downloadURL) guard let http = response as? HTTPURLResponse, 200..<300 ~= http.statusCode else { throw URLError(.badServerResponse) } + installProgress = 0.40 + installPhase = "Verifying checksum…" let actualHash = try Self.sha256Hex(of: tmpURL) guard actualHash.caseInsensitiveCompare(expectedHash) == .orderedSame else { throw UpdateError.checksumMismatch @@ -525,6 +555,8 @@ final class AppUpdateManager: ObservableObject { let downloadedZip = workDir.appendingPathComponent(assetName) try fileManager.moveItem(at: tmpURL, to: downloadedZip) + installProgress = 0.56 + installPhase = "Unpacking update…" let unzipStatus = try Self.unzip(zipURL: downloadedZip, to: unzipDir) guard unzipStatus == 0 else { throw UpdateError.installUnsupported("Failed to unpack update archive.") @@ -533,6 +565,8 @@ final class AppUpdateManager: ObservableObject { guard let appBundle = Self.findFirstAppBundle(in: unzipDir) else { throw UpdateError.installUnsupported("No .app bundle found in downloaded update.") } + installProgress = 0.70 + installPhase = "Verifying app signature…" guard try Self.verifyCodeSignatureStrictCLI(of: appBundle) else { throw UpdateError.invalidCodeSignature } @@ -556,6 +590,8 @@ final class AppUpdateManager: ObservableObject { let backupApp = targetDir.appendingPathComponent("\(currentApp.deletingPathExtension().lastPathComponent)-backup-\(Int(Date().timeIntervalSince1970)).app") do { + installProgress = 0.86 + installPhase = "Installing app update…" try fileManager.moveItem(at: currentApp, to: backupApp) try fileManager.moveItem(at: appBundle, to: currentApp) } catch { @@ -565,10 +601,19 @@ final class AppUpdateManager: ObservableObject { throw UpdateError.installUnsupported("Automatic install failed due to file permissions. Use Download Update for manual install.") } - installMessage = "Update installed. Relaunching…" - NSWorkspace.shared.openApplication(at: currentApp, configuration: NSWorkspace.OpenConfiguration(), completionHandler: nil) - NSApp.terminate(nil) + installProgress = 1.0 + installPhase = "Install complete." + if interactive { + awaitingInstallCompletionAction = true + installMessage = "Update installed. Choose “Restart App” or “Install & Close App”." + } else { + installMessage = "Update installed. Relaunching…" + NSWorkspace.shared.openApplication(at: currentApp, configuration: NSWorkspace.OpenConfiguration(), completionHandler: nil) + NSApp.terminate(nil) + } } catch { + installProgress = 0 + installPhase = "" installMessage = error.localizedDescription if let release = latestRelease { openURL(release.downloadURL ?? release.releaseURL) diff --git a/Neon Vision Editor/UI/AppUpdaterDialog.swift b/Neon Vision Editor/UI/AppUpdaterDialog.swift index 7d30c8f..07a02ad 100644 --- a/Neon Vision Editor/UI/AppUpdaterDialog.swift +++ b/Neon Vision Editor/UI/AppUpdaterDialog.swift @@ -119,8 +119,15 @@ struct AppUpdaterDialog: View { .foregroundStyle(.secondary) if appUpdateManager.isInstalling { - ProgressView("Installing update…") - .font(.caption) + VStack(alignment: .leading, spacing: 6) { + ProgressView(value: appUpdateManager.installProgress, total: 1.0) { + Text(appUpdateManager.installPhase.isEmpty ? "Installing update…" : appUpdateManager.installPhase) + .font(.caption) + } + Text("\(Int((appUpdateManager.installProgress * 100).rounded()))%") + .font(.caption2) + .foregroundStyle(.secondary) + } } if let installMessage = appUpdateManager.installMessage { @@ -140,31 +147,43 @@ struct AppUpdaterDialog: View { HStack { switch appUpdateManager.status { case .updateAvailable: - Button("Skip This Version") { - appUpdateManager.skipCurrentVersion() - isPresented = false - } + if appUpdateManager.awaitingInstallCompletionAction { + Spacer() - Button("Remind Me Tomorrow") { - appUpdateManager.remindMeTomorrow() - isPresented = false - } - - Spacer() - - Button("Download Update") { - appUpdateManager.openDownloadPage() - isPresented = false - } - - Button("Install Now") { - Task { - await appUpdateManager.installUpdateNow() + Button("Install & Close App") { + appUpdateManager.completeInstalledUpdate(restart: false) } - } - .buttonStyle(.borderedProminent) - .disabled(appUpdateManager.isInstalling) + Button("Restart App") { + appUpdateManager.completeInstalledUpdate(restart: true) + } + .buttonStyle(.borderedProminent) + } else { + Button("Skip This Version") { + appUpdateManager.skipCurrentVersion() + isPresented = false + } + + Button("Remind Me Tomorrow") { + appUpdateManager.remindMeTomorrow() + isPresented = false + } + + Spacer() + + Button("Download Update") { + appUpdateManager.openDownloadPage() + isPresented = false + } + + Button("Install Now") { + Task { + await appUpdateManager.installUpdateNow() + } + } + .buttonStyle(.borderedProminent) + .disabled(appUpdateManager.isInstalling) + } case .failed: Button("Close") { isPresented = false diff --git a/Neon Vision Editor/UI/ContentView.swift b/Neon Vision Editor/UI/ContentView.swift index 72e41b8..e984aee 100644 --- a/Neon Vision Editor/UI/ContentView.swift +++ b/Neon Vision Editor/UI/ContentView.swift @@ -1838,9 +1838,9 @@ struct ContentView: View { highlightRefreshToken: highlightRefreshToken ) .id(currentLanguage) - .frame(maxWidth: viewModel.isBrainDumpMode ? 800 : .infinity) + .frame(maxWidth: viewModel.isBrainDumpMode ? 920 : .infinity) .frame(maxHeight: .infinity) - .padding(.horizontal, viewModel.isBrainDumpMode ? 100 : 0) + .padding(.horizontal, viewModel.isBrainDumpMode ? 24 : 0) .padding(.vertical, viewModel.isBrainDumpMode ? 40 : 0) .background( Group { @@ -1856,6 +1856,11 @@ struct ContentView: View { wordCountView } } + .frame( + maxWidth: .infinity, + maxHeight: .infinity, + alignment: viewModel.isBrainDumpMode ? .top : .topLeading + ) if showProjectStructureSidebar && !viewModel.isBrainDumpMode { Divider() @@ -1872,6 +1877,15 @@ struct ContentView: View { .frame(minWidth: 220, idealWidth: 260, maxWidth: 340) } } + .background( + Group { + if viewModel.isBrainDumpMode && enableTranslucentWindow { + Color.clear.background(.ultraThinMaterial) + } else { + Color.clear + } + } + ) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) let withEvents = withTypingEvents( diff --git a/Neon Vision Editor/UI/EditorTextView.swift b/Neon Vision Editor/UI/EditorTextView.swift index 347e44b..e2d4308 100644 --- a/Neon Vision Editor/UI/EditorTextView.swift +++ b/Neon Vision Editor/UI/EditorTextView.swift @@ -1845,6 +1845,9 @@ struct CustomTextEditor: NSViewRepresentable { tv.textStorage?.beginEditing() // Clear previous coloring and apply base color tv.textStorage?.removeAttribute(.foregroundColor, range: fullRange) + // Clear previous background/underline artifacts so caret-line highlight doesn't accumulate. + tv.textStorage?.removeAttribute(.backgroundColor, range: fullRange) + tv.textStorage?.removeAttribute(.underlineStyle, range: fullRange) tv.textStorage?.addAttribute(.foregroundColor, value: baseColor, range: fullRange) // Apply colored ranges for (range, color) in coloredRanges { diff --git a/Neon Vision Editor/UI/NeonSettingsView.swift b/Neon Vision Editor/UI/NeonSettingsView.swift index 5073121..7f7327a 100644 --- a/Neon Vision Editor/UI/NeonSettingsView.swift +++ b/Neon Vision Editor/UI/NeonSettingsView.swift @@ -155,7 +155,8 @@ struct NeonSettingsView: View { .background( SettingsWindowConfigurator( minSize: NSSize(width: 900, height: 820), - idealSize: NSSize(width: 980, height: 880) + idealSize: NSSize(width: 980, height: 880), + translucentEnabled: supportsTranslucency && translucentWindow ) ) #endif @@ -1056,6 +1057,7 @@ final class FontPickerController: NSObject, NSFontChanging { struct SettingsWindowConfigurator: NSViewRepresentable { let minSize: NSSize let idealSize: NSSize + let translucentEnabled: Bool func makeNSView(context: Context) -> NSView { let view = NSView(frame: .zero) @@ -1077,11 +1079,27 @@ struct SettingsWindowConfigurator: NSViewRepresentable { width: max(window.minSize.width, minSize.width), height: max(window.minSize.height, minSize.height) ) + // Match native macOS Settings layout: centered preference tabs and hidden title text. + window.toolbarStyle = .preference + window.titleVisibility = .hidden let targetWidth = max(window.frame.size.width, idealSize.width) let targetHeight = max(window.frame.size.height, idealSize.height) if targetWidth != window.frame.size.width || targetHeight != window.frame.size.height { window.setContentSize(NSSize(width: targetWidth, height: targetHeight)) } + + // Keep settings-window translucency in sync without relying on editor view events. + window.isOpaque = !translucentEnabled + window.backgroundColor = translucentEnabled ? .clear : NSColor.windowBackgroundColor + window.titlebarAppearsTransparent = translucentEnabled + if translucentEnabled { + window.styleMask.insert(.fullSizeContentView) + } else { + window.styleMask.remove(.fullSizeContentView) + } + if #available(macOS 13.0, *) { + window.titlebarSeparatorStyle = translucentEnabled ? .none : .automatic + } } } #endif diff --git a/README.md b/README.md index 3cb2312..40d8d2a 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@

> Status: **active release** -> Latest release: **v0.4.14** +> Latest release: **v0.4.15** > Platform target: **macOS 26 (Tahoe)** compatible with **macOS Sequoia** > Apple Silicon: tested / Intel: not tested @@ -25,7 +25,7 @@ Prebuilt binaries are available on [GitHub Releases](https://github.com/h3pdesign/Neon-Vision-Editor/releases). -- Latest release: **v0.4.14** +- Latest release: **v0.4.15** - TestFlight beta: [Join here](https://testflight.apple.com/join/YWB2fGAP) - Architecture: Apple Silicon (Intel not tested) - Notarization: *is finally there* @@ -122,6 +122,10 @@ If macOS blocks first launch: ## Changelog +### v0.4.15 (summary) + +- Fixed the editor `Highlight Current Line` behavior on macOS so previous line background highlights are cleared and only the active line remains highlighted. + ### v0.4.14 (summary) - Added centralized theme canonicalization with an explicit `Custom` option in settings so legacy/case-variant values resolve consistently across launches. @@ -164,12 +168,12 @@ Full release history: [`CHANGELOG.md`](CHANGELOG.md) ## Release Integrity -- Tag: `v0.4.14` +- Tag: `v0.4.15` - Tagged commit: `TBD` - Verify local tag target: ```bash -git rev-parse --verify v0.4.14 +git rev-parse --verify v0.4.15 ``` - Verify downloaded artifact checksum locally: