2026-02-11 10:20:17 +00:00
import SwiftUI
#if os ( macOS )
import AppKit
#endif
struct NeonSettingsView : View {
2026-02-13 00:19:31 +00:00
private static var cachedEditorFonts : [ String ] = [ ]
2026-02-11 10:20:17 +00:00
let supportsOpenInTabs : Bool
let supportsTranslucency : Bool
@ EnvironmentObject private var supportPurchaseManager : SupportPurchaseManager
2026-02-14 13:24:01 +00:00
@ EnvironmentObject private var appUpdateManager : AppUpdateManager
2026-02-12 09:15:40 +00:00
@ Environment ( \ . horizontalSizeClass ) private var horizontalSizeClass
2026-02-19 08:44:24 +00:00
@ Environment ( \ . colorScheme ) private var systemColorScheme
2026-02-11 10:20:17 +00:00
@ AppStorage ( " SettingsOpenInTabs " ) private var openInTabs : String = " system "
@ AppStorage ( " SettingsEditorFontName " ) private var editorFontName : String = " "
2026-02-12 22:20:39 +00:00
@ AppStorage ( " SettingsUseSystemFont " ) private var useSystemFont : Bool = false
2026-02-11 10:20:17 +00:00
@ AppStorage ( " SettingsEditorFontSize " ) private var editorFontSize : Double = 14
@ AppStorage ( " SettingsLineHeight " ) private var lineHeight : Double = 1.0
@ AppStorage ( " SettingsAppearance " ) private var appearance : String = " system "
@ AppStorage ( " EnableTranslucentWindow " ) private var translucentWindow : Bool = false
2026-02-12 22:20:39 +00:00
@ AppStorage ( " SettingsReopenLastSession " ) private var reopenLastSession : Bool = true
@ AppStorage ( " SettingsOpenWithBlankDocument " ) private var openWithBlankDocument : Bool = true
@ AppStorage ( " SettingsDefaultNewFileLanguage " ) private var defaultNewFileLanguage : String = " plain "
@ AppStorage ( " SettingsConfirmCloseDirtyTab " ) private var confirmCloseDirtyTab : Bool = true
@ AppStorage ( " SettingsConfirmClearEditor " ) private var confirmClearEditor : Bool = true
2026-02-14 13:24:01 +00:00
@ AppStorage ( AppUpdateManager . autoCheckEnabledKey ) private var autoCheckForUpdates : Bool = true
@ AppStorage ( AppUpdateManager . updateIntervalKey ) private var updateCheckIntervalRaw : String = AppUpdateCheckInterval . daily . rawValue
@ AppStorage ( AppUpdateManager . autoDownloadEnabledKey ) private var autoDownloadUpdates : Bool = false
2026-02-11 10:20:17 +00:00
@ AppStorage ( " SettingsShowLineNumbers " ) private var showLineNumbers : Bool = true
@ AppStorage ( " SettingsHighlightCurrentLine " ) private var highlightCurrentLine : Bool = false
2026-02-12 22:20:39 +00:00
@ AppStorage ( " SettingsHighlightMatchingBrackets " ) private var highlightMatchingBrackets : Bool = false
@ AppStorage ( " SettingsShowScopeGuides " ) private var showScopeGuides : Bool = false
@ AppStorage ( " SettingsHighlightScopeBackground " ) private var highlightScopeBackground : Bool = false
2026-02-11 10:20:17 +00:00
@ AppStorage ( " SettingsLineWrapEnabled " ) private var lineWrapEnabled : Bool = false
@ AppStorage ( " SettingsIndentStyle " ) private var indentStyle : String = " spaces "
@ AppStorage ( " SettingsIndentWidth " ) private var indentWidth : Int = 4
@ AppStorage ( " SettingsAutoIndent " ) private var autoIndent : Bool = true
@ AppStorage ( " SettingsAutoCloseBrackets " ) private var autoCloseBrackets : Bool = false
@ AppStorage ( " SettingsTrimTrailingWhitespace " ) private var trimTrailingWhitespace : Bool = false
@ AppStorage ( " SettingsTrimWhitespaceForSyntaxDetection " ) private var trimWhitespaceForSyntaxDetection : Bool = false
@ AppStorage ( " SettingsCompletionEnabled " ) private var completionEnabled : Bool = false
@ AppStorage ( " SettingsCompletionFromDocument " ) private var completionFromDocument : Bool = false
@ AppStorage ( " SettingsCompletionFromSyntax " ) private var completionFromSyntax : Bool = false
2026-02-12 22:20:39 +00:00
@ AppStorage ( " SelectedAIModel " ) private var selectedAIModelRaw : String = AIModel . appleIntelligence . rawValue
2026-02-11 10:20:17 +00:00
@ AppStorage ( " SettingsActiveTab " ) private var settingsActiveTab : String = " general "
@ AppStorage ( " SettingsTemplateLanguage " ) private var settingsTemplateLanguage : String = " swift "
2026-02-14 13:24:01 +00:00
@ State private var grokAPIToken : String = " "
@ State private var openAIAPIToken : String = " "
@ State private var geminiAPIToken : String = " "
@ State private var anthropicAPIToken : String = " "
2026-02-11 10:20:17 +00:00
@ State private var showSupportPurchaseDialog : Bool = false
2026-02-19 08:09:35 +00:00
@ State private var showDataDisclosureDialog : Bool = false
2026-02-12 22:20:39 +00:00
@ State private var availableEditorFonts : [ String ] = [ ]
2026-02-19 08:44:24 +00:00
@ State private var moreSectionTab : String = " support "
2026-02-12 17:31:51 +00:00
private let privacyPolicyURL = URL ( string : " https://github.com/h3pdesign/Neon-Vision-Editor/blob/main/PRIVACY.md " )
2026-02-11 10:20:17 +00:00
@ AppStorage ( " SettingsThemeName " ) private var selectedTheme : String = " Neon Glow "
@ AppStorage ( " SettingsThemeTextColor " ) private var themeTextHex : String = " #EDEDED "
@ AppStorage ( " SettingsThemeBackgroundColor " ) private var themeBackgroundHex : String = " #0E1116 "
@ AppStorage ( " SettingsThemeCursorColor " ) private var themeCursorHex : String = " #4EA4FF "
@ AppStorage ( " SettingsThemeSelectionColor " ) private var themeSelectionHex : String = " #2A3340 "
@ AppStorage ( " SettingsThemeKeywordColor " ) private var themeKeywordHex : String = " #F5D90A "
2026-02-19 08:09:35 +00:00
@ AppStorage ( " SettingsThemeStringColor " ) private var themeStringHex : String = " #4EA4FF "
2026-02-11 10:20:17 +00:00
@ AppStorage ( " SettingsThemeNumberColor " ) private var themeNumberHex : String = " #FFB86C "
@ AppStorage ( " SettingsThemeCommentColor " ) private var themeCommentHex : String = " #7F8C98 "
2026-02-16 13:39:27 +00:00
@ AppStorage ( " SettingsThemeTypeColor " ) private var themeTypeHex : String = " #32D269 "
2026-02-19 08:09:35 +00:00
@ AppStorage ( " SettingsThemeBuiltinColor " ) private var themeBuiltinHex : String = " #EC7887 "
2026-02-11 10:20:17 +00:00
private var inputFieldBackground : Color {
#if os ( macOS )
Color ( nsColor : . windowBackgroundColor )
#else
Color ( . secondarySystemBackground )
#endif
}
2026-02-14 20:57:32 +00:00
private let themes : [ String ] = editorThemeNames
2026-02-11 10:20:17 +00:00
private let templateLanguages : [ String ] = [
" swift " , " python " , " javascript " , " typescript " , " php " , " java " , " kotlin " , " go " , " ruby " , " rust " ,
2026-02-13 11:02:39 +00:00
" cobol " , " dotenv " , " proto " , " graphql " , " rst " , " nginx " , " sql " , " html " , " expressionengine " , " css " , " c " , " cpp " ,
2026-02-11 10:20:17 +00:00
" csharp " , " objective-c " , " json " , " xml " , " yaml " , " toml " , " csv " , " ini " , " vim " , " log " , " ipynb " ,
" markdown " , " bash " , " zsh " , " powershell " , " standard " , " plain "
]
2026-02-12 09:15:40 +00:00
private var isCompactSettingsLayout : Bool {
#if os ( iOS )
horizontalSizeClass = = . compact
#else
false
#endif
}
2026-02-11 10:20:17 +00:00
2026-02-19 08:09:35 +00:00
private var useTwoColumnSettingsLayout : Bool {
2026-02-18 22:56:46 +00:00
#if os ( iOS )
2026-02-19 08:09:35 +00:00
horizontalSizeClass = = . regular
2026-02-18 22:56:46 +00:00
#else
2026-02-19 08:09:35 +00:00
false
2026-02-18 22:56:46 +00:00
#endif
}
2026-02-19 08:09:35 +00:00
private var standardLabelWidth : CGFloat {
useTwoColumnSettingsLayout ? 180 : 140
}
private var startupLabelWidth : CGFloat {
useTwoColumnSettingsLayout ? 220 : 180
2026-02-18 22:56:46 +00:00
}
2026-02-13 14:03:43 +00:00
private enum UI {
static let space6 : CGFloat = 6
static let space8 : CGFloat = 8
static let space10 : CGFloat = 10
static let space12 : CGFloat = 12
static let space16 : CGFloat = 16
static let space20 : CGFloat = 20
static let fieldCorner : CGFloat = 6
static let groupPadding : CGFloat = 14
static let sidePaddingCompact : CGFloat = 12
static let sidePaddingRegular : CGFloat = 28
2026-02-19 08:44:24 +00:00
static let sidePaddingIPadRegular : CGFloat = 40
2026-02-13 14:03:43 +00:00
static let topPadding : CGFloat = 18
static let bottomPadding : CGFloat = 24
2026-02-19 08:44:24 +00:00
static let cardCorner : CGFloat = 12
static let cardStrokeOpacity : Double = 0.15
2026-02-13 14:03:43 +00:00
}
private enum Typography {
static let sectionHeadline = Font . headline
static let sectionSubheadline = Font . subheadline
static let footnote = Font . footnote
static let monoBody = Font . system ( size : 13 , weight : . regular , design : . monospaced )
2026-02-19 08:09:35 +00:00
static let sectionTitle = Font . title3 . weight ( . semibold )
2026-02-13 14:03:43 +00:00
}
2026-02-11 10:20:17 +00:00
init (
supportsOpenInTabs : Bool = true ,
supportsTranslucency : Bool = true
) {
self . supportsOpenInTabs = supportsOpenInTabs
self . supportsTranslucency = supportsTranslucency
}
2026-02-19 08:09:35 +00:00
private var settingsTabs : some View {
2026-02-11 10:20:17 +00:00
TabView ( selection : $ settingsActiveTab ) {
generalTab
. tabItem { Label ( " General " , systemImage : " gearshape " ) }
. tag ( " general " )
editorTab
. tabItem { Label ( " Editor " , systemImage : " slider.horizontal.3 " ) }
. tag ( " editor " )
templateTab
. tabItem { Label ( " Templates " , systemImage : " doc.badge.plus " ) }
. tag ( " templates " )
themeTab
. tabItem { Label ( " Themes " , systemImage : " paintpalette " ) }
. tag ( " themes " )
2026-02-19 08:09:35 +00:00
#if os ( iOS )
moreTab
. tabItem { Label ( " More " , systemImage : " ellipsis.circle " ) }
. tag ( " more " )
#else
2026-02-11 10:20:17 +00:00
supportTab
. tabItem { Label ( " Support " , systemImage : " heart " ) }
. tag ( " support " )
2026-02-19 08:44:24 +00:00
aiTab
. tabItem { Label ( " AI " , systemImage : " brain.head.profile " ) }
. tag ( " ai " )
2026-02-19 08:09:35 +00:00
#endif
2026-02-14 13:24:01 +00:00
#if os ( macOS )
if ReleaseRuntimePolicy . isUpdaterEnabledForCurrentDistribution {
updatesTab
. tabItem { Label ( " Updates " , systemImage : " arrow.triangle.2.circlepath.circle " ) }
. tag ( " updates " )
}
#endif
2026-02-11 10:20:17 +00:00
}
2026-02-19 08:09:35 +00:00
}
var body : some View {
settingsTabs
. tint ( . blue )
2026-02-12 09:15:40 +00:00
#if os ( macOS )
2026-02-13 14:03:43 +00:00
. frame ( minWidth : 900 , idealWidth : 980 , minHeight : 820 , idealHeight : 880 )
. background (
SettingsWindowConfigurator (
minSize : NSSize ( width : 900 , height : 820 ) ,
2026-02-14 22:15:22 +00:00
idealSize : NSSize ( width : 980 , height : 880 ) ,
translucentEnabled : supportsTranslucency && translucentWindow
2026-02-13 14:03:43 +00:00
)
)
2026-02-12 09:15:40 +00:00
#endif
2026-02-12 22:20:39 +00:00
. preferredColorScheme ( preferredColorSchemeOverride )
2026-02-11 10:20:17 +00:00
. onAppear {
2026-02-12 22:20:39 +00:00
settingsActiveTab = " general "
2026-02-19 08:44:24 +00:00
moreSectionTab = useTwoColumnSettingsLayout ? " support " : " ai "
2026-02-14 20:57:32 +00:00
selectedTheme = canonicalThemeName ( selectedTheme )
2026-02-19 08:09:35 +00:00
migrateLegacyPinkSettingsIfNeeded ( )
2026-02-13 00:19:31 +00:00
loadAvailableEditorFontsIfNeeded ( )
2026-02-11 10:20:17 +00:00
if supportPurchaseManager . supportProduct = = nil {
Task { await supportPurchaseManager . refreshStoreState ( ) }
}
2026-02-14 13:24:01 +00:00
appUpdateManager . setAutoCheckEnabled ( autoCheckForUpdates )
appUpdateManager . setUpdateInterval ( selectedUpdateInterval )
appUpdateManager . setAutoDownloadEnabled ( autoDownloadUpdates )
2026-02-11 10:20:17 +00:00
#if os ( macOS )
2026-02-12 22:20:39 +00:00
applyAppearanceImmediately ( )
#endif
}
. onChange ( of : appearance ) { _ , _ in
#if os ( macOS )
applyAppearanceImmediately ( )
2026-02-11 10:20:17 +00:00
#endif
}
2026-02-12 22:20:39 +00:00
. onChange ( of : showScopeGuides ) { _ , enabled in
if enabled && lineWrapEnabled {
lineWrapEnabled = false
}
}
. onChange ( of : highlightScopeBackground ) { _ , enabled in
if enabled && lineWrapEnabled {
lineWrapEnabled = false
}
}
. onChange ( of : lineWrapEnabled ) { _ , enabled in
if enabled {
showScopeGuides = false
highlightScopeBackground = false
}
}
2026-02-14 13:24:01 +00:00
. onChange ( of : autoCheckForUpdates ) { _ , enabled in
appUpdateManager . setAutoCheckEnabled ( enabled )
}
. onChange ( of : updateCheckIntervalRaw ) { _ , _ in
appUpdateManager . setUpdateInterval ( selectedUpdateInterval )
}
. onChange ( of : autoDownloadUpdates ) { _ , enabled in
appUpdateManager . setAutoDownloadEnabled ( enabled )
}
. onChange ( of : settingsActiveTab ) { _ , newValue in
2026-02-19 08:09:35 +00:00
#if os ( iOS )
2026-02-19 08:44:24 +00:00
if newValue = = " more " {
moreSectionTab = " support "
2026-02-19 08:09:35 +00:00
}
#else
2026-02-14 13:24:01 +00:00
if newValue = = " ai " {
loadAPITokensIfNeeded ( )
}
2026-02-19 08:09:35 +00:00
#endif
2026-02-14 13:24:01 +00:00
}
2026-02-19 08:09:35 +00:00
. onChange ( of : moreSectionTab ) { _ , newValue in
if newValue = = " ai " && settingsActiveTab = = " more " {
loadAPITokensIfNeeded ( )
}
2026-02-18 22:56:46 +00:00
}
2026-02-14 20:57:32 +00:00
. onChange ( of : selectedTheme ) { _ , newValue in
let canonical = canonicalThemeName ( newValue )
if canonical != newValue {
selectedTheme = canonical
}
}
2026-02-11 10:20:17 +00:00
. confirmationDialog ( " Support Neon Vision Editor " , isPresented : $ showSupportPurchaseDialog , titleVisibility : . visible ) {
Button ( " Support \( supportPurchaseManager . supportPriceLabel ) " ) {
Task { await supportPurchaseManager . purchaseSupport ( ) }
}
Button ( " Cancel " , role : . cancel ) { }
} message : {
Text ( " Optional one-time purchase to support development. No features are locked behind this purchase. " )
}
. alert (
" App Store " ,
isPresented : Binding (
get : { supportPurchaseManager . statusMessage != nil } ,
set : { if ! $0 { supportPurchaseManager . statusMessage = nil } }
)
) {
Button ( " OK " , role : . cancel ) { }
} message : {
Text ( supportPurchaseManager . statusMessage ? ? " " )
}
2026-02-19 08:09:35 +00:00
. sheet ( isPresented : $ showDataDisclosureDialog ) {
dataDisclosureDialog
}
2026-02-11 10:20:17 +00:00
}
2026-02-14 13:24:01 +00:00
private func loadAPITokensIfNeeded ( ) {
if grokAPIToken . isEmpty { grokAPIToken = SecureTokenStore . token ( for : . grok ) }
if openAIAPIToken . isEmpty { openAIAPIToken = SecureTokenStore . token ( for : . openAI ) }
if geminiAPIToken . isEmpty { geminiAPIToken = SecureTokenStore . token ( for : . gemini ) }
if anthropicAPIToken . isEmpty { anthropicAPIToken = SecureTokenStore . token ( for : . anthropic ) }
}
2026-02-12 22:20:39 +00:00
private var preferredColorSchemeOverride : ColorScheme ? {
2026-02-12 23:58:32 +00:00
ReleaseRuntimePolicy . preferredColorScheme ( for : appearance )
2026-02-12 22:20:39 +00:00
}
#if os ( macOS )
private func applyAppearanceImmediately ( ) {
let target : NSAppearance ?
switch appearance {
case " light " :
target = NSAppearance ( named : . aqua )
case " dark " :
target = NSAppearance ( named : . darkAqua )
default :
target = nil
}
NSApp . appearance = target
for window in NSApp . windows {
window . appearance = target
}
}
#endif
2026-02-11 10:20:17 +00:00
private var generalTab : some View {
settingsContainer {
2026-02-19 08:09:35 +00:00
settingsSectionHeader (
icon : " gearshape " ,
title : " General " ,
subtitle : " Window behavior, startup defaults, and confirmation preferences. "
)
2026-02-11 10:20:17 +00:00
2026-02-19 08:09:35 +00:00
if useTwoColumnSettingsLayout {
LazyVGrid ( columns : [ GridItem ( . flexible ( ) , spacing : UI . space16 ) , GridItem ( . flexible ( ) , spacing : UI . space16 ) ] , spacing : UI . space16 ) {
windowSection
editorFontSection
startupSection
confirmationsSection
}
} else {
windowSection
editorFontSection
startupSection
confirmationsSection
}
}
}
private var windowSection : some View {
GroupBox ( " Window " ) {
VStack ( alignment : . leading , spacing : UI . space12 ) {
if supportsOpenInTabs {
2026-02-13 14:03:43 +00:00
HStack ( alignment : . center , spacing : UI . space12 ) {
2026-02-19 08:09:35 +00:00
Text ( " Open in Tabs " )
. frame ( width : isCompactSettingsLayout ? nil : standardLabelWidth , alignment : . leading )
Picker ( " " , selection : $ openInTabs ) {
Text ( " Follow System " ) . tag ( " system " )
Text ( " Always " ) . tag ( " always " )
Text ( " Never " ) . tag ( " never " )
2026-02-11 10:20:17 +00:00
}
. pickerStyle ( . segmented )
}
2026-02-19 08:09:35 +00:00
}
2026-02-11 10:20:17 +00:00
2026-02-19 08:09:35 +00:00
HStack ( alignment : . center , spacing : UI . space12 ) {
Text ( " Appearance " )
. frame ( width : isCompactSettingsLayout ? nil : standardLabelWidth , alignment : . leading )
Picker ( " " , selection : $ appearance ) {
Text ( " System " ) . tag ( " system " )
Text ( " Light " ) . tag ( " light " )
Text ( " Dark " ) . tag ( " dark " )
2026-02-11 10:20:17 +00:00
}
2026-02-19 08:09:35 +00:00
. pickerStyle ( . segmented )
2026-02-11 10:20:17 +00:00
}
2026-02-19 08:09:35 +00:00
if supportsTranslucency {
Toggle ( " Translucent Window " , isOn : $ translucentWindow )
2026-02-12 22:20:39 +00:00
. frame ( maxWidth : . infinity , alignment : . leading )
2026-02-19 08:09:35 +00:00
}
}
. padding ( UI . groupPadding )
}
}
2026-02-12 22:20:39 +00:00
2026-02-19 08:09:35 +00:00
private var editorFontSection : some View {
GroupBox ( " Editor Font " ) {
VStack ( alignment : . leading , spacing : UI . space12 ) {
Toggle ( " Use System Font " , isOn : $ useSystemFont )
. frame ( maxWidth : . infinity , alignment : . leading )
HStack ( alignment : . center , spacing : UI . space12 ) {
Text ( " Font " )
. frame ( width : isCompactSettingsLayout ? nil : standardLabelWidth , alignment : . leading )
VStack ( alignment : . leading , spacing : UI . space8 ) {
HStack ( spacing : UI . space8 ) {
Text ( useSystemFont ? " System " : ( editorFontName . isEmpty ? " System " : editorFontName ) )
. font ( Typography . footnote )
. foregroundStyle ( . secondary )
Button ( showFontList ? " Hide Font List " : " Show Font List " ) {
showFontList . toggle ( )
2026-02-12 22:20:39 +00:00
}
2026-02-19 08:09:35 +00:00
. buttonStyle ( . borderless )
}
if showFontList {
Picker ( " " , selection : selectedFontBinding ) {
Text ( " System " ) . tag ( systemFontSentinel )
ForEach ( availableEditorFonts , id : \ . self ) { fontName in
Text ( fontName ) . tag ( fontName )
2026-02-13 17:53:09 +00:00
}
2026-02-12 22:20:39 +00:00
}
2026-02-19 08:09:35 +00:00
. pickerStyle ( . menu )
. padding ( . vertical , UI . space6 )
. padding ( . horizontal , UI . space8 )
. background ( inputFieldBackground )
. overlay (
RoundedRectangle ( cornerRadius : UI . fieldCorner )
. stroke ( Color . secondary . opacity ( 0.35 ) , lineWidth : 1 )
)
. cornerRadius ( UI . fieldCorner )
2026-02-12 22:20:39 +00:00
}
2026-02-19 08:09:35 +00:00
}
. frame ( maxWidth : isCompactSettingsLayout ? . infinity : 240 , alignment : . leading )
2026-02-11 10:20:17 +00:00
#if os ( macOS )
2026-02-19 08:09:35 +00:00
Button ( " Choose… " ) {
useSystemFont = false
showFontList = true
2026-02-12 22:20:39 +00:00
}
2026-02-19 08:09:35 +00:00
. disabled ( useSystemFont )
#endif
}
2026-02-12 22:20:39 +00:00
2026-02-19 08:09:35 +00:00
HStack ( alignment : . center , spacing : UI . space12 ) {
Text ( " Font Size " )
. frame ( width : isCompactSettingsLayout ? nil : standardLabelWidth , alignment : . leading )
Stepper ( value : $ editorFontSize , in : 10. . . 28 , step : 1 ) {
Text ( " \( Int ( editorFontSize ) ) pt " )
2026-02-11 10:20:17 +00:00
}
2026-02-19 08:09:35 +00:00
. frame ( maxWidth : isCompactSettingsLayout ? . infinity : 220 , alignment : . leading )
}
2026-02-11 10:20:17 +00:00
2026-02-19 08:09:35 +00:00
HStack ( alignment : . center , spacing : UI . space12 ) {
Text ( " Line Height " )
. frame ( width : isCompactSettingsLayout ? nil : standardLabelWidth , alignment : . leading )
Slider ( value : $ lineHeight , in : 1.0 . . . 1.8 , step : 0.05 )
. frame ( maxWidth : isCompactSettingsLayout ? . infinity : 240 )
Text ( String ( format : " %.2fx " , lineHeight ) )
. frame ( width : 54 , alignment : . trailing )
2026-02-11 10:20:17 +00:00
}
}
2026-02-19 08:09:35 +00:00
. padding ( UI . groupPadding )
}
}
2026-02-12 22:20:39 +00:00
2026-02-19 08:09:35 +00:00
private var startupSection : some View {
GroupBox ( " Startup " ) {
VStack ( alignment : . leading , spacing : UI . space12 ) {
Toggle ( " Open with Blank Document " , isOn : $ openWithBlankDocument )
. disabled ( reopenLastSession )
Toggle ( " Reopen Last Session " , isOn : $ reopenLastSession )
HStack ( alignment : . center , spacing : UI . space12 ) {
Text ( " Default New File Language " )
. frame ( width : isCompactSettingsLayout ? nil : startupLabelWidth , alignment : . leading )
Picker ( " " , selection : $ defaultNewFileLanguage ) {
ForEach ( templateLanguages , id : \ . self ) { lang in
Text ( languageLabel ( for : lang ) ) . tag ( lang )
2026-02-12 22:20:39 +00:00
}
}
2026-02-19 08:09:35 +00:00
. pickerStyle ( . menu )
2026-02-12 22:20:39 +00:00
}
2026-02-19 08:09:35 +00:00
Text ( " Tip: Enable only one startup mode to keep app launch behavior predictable. " )
. font ( Typography . footnote )
. foregroundStyle ( . secondary )
}
. padding ( UI . groupPadding )
. onChange ( of : openWithBlankDocument ) { _ , isEnabled in
if isEnabled {
reopenLastSession = false
2026-02-16 19:02:43 +00:00
}
2026-02-19 08:09:35 +00:00
}
. onChange ( of : reopenLastSession ) { _ , isEnabled in
if isEnabled {
openWithBlankDocument = false
2026-02-16 19:02:43 +00:00
}
2026-02-12 22:20:39 +00:00
}
2026-02-19 08:09:35 +00:00
}
}
2026-02-12 22:20:39 +00:00
2026-02-19 08:09:35 +00:00
private var confirmationsSection : some View {
GroupBox ( " Confirmations " ) {
VStack ( alignment : . leading , spacing : UI . space12 ) {
Toggle ( " Confirm Before Closing Dirty Tab " , isOn : $ confirmCloseDirtyTab )
Toggle ( " Confirm Before Clearing Editor " , isOn : $ confirmClearEditor )
2026-02-12 22:20:39 +00:00
}
2026-02-19 08:09:35 +00:00
. padding ( UI . groupPadding )
2026-02-11 10:20:17 +00:00
}
}
2026-02-12 22:20:39 +00:00
private let systemFontSentinel = " __system__ "
@ State private var selectedFontValue : String = " __system__ "
2026-02-13 17:53:09 +00:00
@ State private var showFontList : Bool = {
#if os ( macOS )
false
#else
true
#endif
} ( )
2026-02-12 22:20:39 +00:00
private var selectedFontBinding : Binding < String > {
Binding (
get : {
if useSystemFont { return systemFontSentinel }
if editorFontName . isEmpty { return systemFontSentinel }
2026-02-16 19:02:43 +00:00
if availableEditorFonts . isEmpty { return systemFontSentinel }
if ! availableEditorFonts . contains ( editorFontName ) { return systemFontSentinel }
2026-02-12 22:20:39 +00:00
return editorFontName
} ,
2026-02-16 19:02:43 +00:00
set : { newValue in
selectedFontValue = newValue
if newValue = = systemFontSentinel {
useSystemFont = true
} else {
useSystemFont = false
editorFontName = newValue
}
}
2026-02-12 22:20:39 +00:00
)
}
2026-02-13 00:19:31 +00:00
private func loadAvailableEditorFontsIfNeeded ( ) {
if ! availableEditorFonts . isEmpty {
2026-02-16 19:02:43 +00:00
syncSelectedFontValue ( )
2026-02-13 00:19:31 +00:00
return
}
if ! Self . cachedEditorFonts . isEmpty {
availableEditorFonts = Self . cachedEditorFonts
2026-02-16 19:02:43 +00:00
syncSelectedFontValue ( )
2026-02-13 00:19:31 +00:00
return
}
// D e f e r f o n t d i s c o v e r y u n t i l a f t e r t h e i n i t i a l s e t t i n g s v i e w a p p e a r s .
DispatchQueue . main . async {
populateEditorFonts ( )
}
}
private func populateEditorFonts ( ) {
2026-02-12 22:20:39 +00:00
#if os ( macOS )
let names = NSFontManager . shared . availableFonts
#else
let names = UIFont . familyNames
. sorted ( )
. flatMap { UIFont . fontNames ( forFamilyName : $0 ) }
#endif
var merged = Array ( Set ( names ) ) . sorted ( )
if ! editorFontName . isEmpty && ! merged . contains ( editorFontName ) {
merged . insert ( editorFontName , at : 0 )
}
2026-02-13 00:19:31 +00:00
Self . cachedEditorFonts = merged
2026-02-12 22:20:39 +00:00
availableEditorFonts = merged
2026-02-16 19:02:43 +00:00
syncSelectedFontValue ( )
}
private func syncSelectedFontValue ( ) {
if useSystemFont || editorFontName . isEmpty {
selectedFontValue = systemFontSentinel
return
}
selectedFontValue = availableEditorFonts . contains ( editorFontName ) ? editorFontName : systemFontSentinel
2026-02-12 22:20:39 +00:00
}
2026-02-14 13:24:01 +00:00
private var selectedUpdateInterval : AppUpdateCheckInterval {
AppUpdateCheckInterval ( rawValue : updateCheckIntervalRaw ) ? ? . daily
}
2026-02-11 10:20:17 +00:00
private var editorTab : some View {
settingsContainer ( maxWidth : 760 ) {
2026-02-19 08:09:35 +00:00
settingsSectionHeader (
icon : " slider.horizontal.3 " ,
title : " Editor " ,
subtitle : " Display, indentation, editing behavior, and completion sources. "
)
2026-02-11 10:20:17 +00:00
GroupBox ( " Editor " ) {
2026-02-16 19:02:43 +00:00
VStack ( alignment : . leading , spacing : 16 ) {
2026-02-13 14:03:43 +00:00
VStack ( alignment : . leading , spacing : UI . space10 ) {
2026-02-11 10:20:17 +00:00
Text ( " Display " )
2026-02-13 14:03:43 +00:00
. font ( Typography . sectionHeadline )
2026-02-11 10:20:17 +00:00
Toggle ( " Show Line Numbers " , isOn : $ showLineNumbers )
Toggle ( " Highlight Current Line " , isOn : $ highlightCurrentLine )
2026-02-12 22:20:39 +00:00
Toggle ( " Highlight Matching Brackets " , isOn : $ highlightMatchingBrackets )
Toggle ( " Show Scope Guides (Non-Swift) " , isOn : $ showScopeGuides )
Toggle ( " Highlight Scoped Region " , isOn : $ highlightScopeBackground )
2026-02-11 10:20:17 +00:00
Toggle ( " Line Wrap " , isOn : $ lineWrapEnabled )
2026-02-12 22:20:39 +00:00
Text ( " When Line Wrap is enabled, scope guides/scoped region are turned off to avoid layout conflicts. " )
2026-02-13 14:03:43 +00:00
. font ( Typography . footnote )
2026-02-12 22:20:39 +00:00
. foregroundStyle ( . secondary )
Text ( " Scope guides are intended for non-Swift languages. Swift favors matching-token highlight. " )
2026-02-13 14:03:43 +00:00
. font ( Typography . footnote )
2026-02-12 22:20:39 +00:00
. foregroundStyle ( . secondary )
2026-02-11 10:20:17 +00:00
Text ( " Invisible character markers are disabled to avoid whitespace glyph artifacts. " )
2026-02-13 14:03:43 +00:00
. font ( Typography . footnote )
2026-02-11 10:20:17 +00:00
. foregroundStyle ( . secondary )
}
2026-02-16 19:02:43 +00:00
. frame ( maxWidth : . infinity , alignment : . leading )
2026-02-11 10:20:17 +00:00
Divider ( )
2026-02-13 14:03:43 +00:00
VStack ( alignment : . leading , spacing : UI . space10 ) {
2026-02-11 10:20:17 +00:00
Text ( " Indentation " )
2026-02-13 14:03:43 +00:00
. font ( Typography . sectionHeadline )
2026-02-11 10:20:17 +00:00
Picker ( " Indent Style " , selection : $ indentStyle ) {
Text ( " Spaces " ) . tag ( " spaces " )
Text ( " Tabs " ) . tag ( " tabs " )
}
. pickerStyle ( . segmented )
Stepper ( value : $ indentWidth , in : 2. . . 8 , step : 1 ) {
Text ( " Indent Width: \( indentWidth ) " )
}
}
2026-02-16 19:02:43 +00:00
. frame ( maxWidth : . infinity , alignment : . leading )
2026-02-11 10:20:17 +00:00
Divider ( )
2026-02-13 14:03:43 +00:00
VStack ( alignment : . leading , spacing : UI . space10 ) {
2026-02-11 10:20:17 +00:00
Text ( " Editing " )
2026-02-13 14:03:43 +00:00
. font ( Typography . sectionHeadline )
2026-02-11 10:20:17 +00:00
Toggle ( " Auto Indent " , isOn : $ autoIndent )
Toggle ( " Auto Close Brackets " , isOn : $ autoCloseBrackets )
Toggle ( " Trim Trailing Whitespace " , isOn : $ trimTrailingWhitespace )
Toggle ( " Trim Edges for Syntax Detection " , isOn : $ trimWhitespaceForSyntaxDetection )
}
2026-02-16 19:02:43 +00:00
. frame ( maxWidth : . infinity , alignment : . leading )
2026-02-11 10:20:17 +00:00
Divider ( )
2026-02-13 14:03:43 +00:00
VStack ( alignment : . leading , spacing : UI . space10 ) {
2026-02-11 10:20:17 +00:00
Text ( " Completion " )
2026-02-13 14:03:43 +00:00
. font ( Typography . sectionHeadline )
2026-02-11 10:20:17 +00:00
Toggle ( " Enable Completion " , isOn : $ completionEnabled )
Toggle ( " Include Words in Document " , isOn : $ completionFromDocument )
Toggle ( " Include Syntax Keywords " , isOn : $ completionFromSyntax )
2026-02-19 08:09:35 +00:00
Text ( " For lower latency on large files, keep only one completion source enabled. " )
2026-02-18 22:56:46 +00:00
. font ( Typography . footnote )
. foregroundStyle ( . secondary )
}
. frame ( maxWidth : . infinity , alignment : . leading )
2026-02-11 10:20:17 +00:00
}
2026-02-16 19:02:43 +00:00
. frame ( maxWidth : . infinity , alignment : . leading )
2026-02-13 14:03:43 +00:00
. padding ( UI . groupPadding )
2026-02-11 10:20:17 +00:00
}
}
}
private var templateTab : some View {
settingsContainer ( maxWidth : 640 ) {
2026-02-19 08:09:35 +00:00
settingsSectionHeader (
icon : " doc.badge.plus " ,
title : " Templates " ,
subtitle : " Control language-specific starter content used when inserting templates. "
)
2026-02-11 10:20:17 +00:00
GroupBox ( " Completion Template " ) {
2026-02-13 14:03:43 +00:00
VStack ( alignment : . leading , spacing : UI . space12 ) {
HStack ( alignment : . center , spacing : UI . space12 ) {
2026-02-11 10:20:17 +00:00
Text ( " Language " )
2026-02-19 08:09:35 +00:00
. frame ( width : isCompactSettingsLayout ? nil : standardLabelWidth , alignment : . leading )
2026-02-11 10:20:17 +00:00
Picker ( " " , selection : $ settingsTemplateLanguage ) {
ForEach ( templateLanguages , id : \ . self ) { lang in
Text ( languageLabel ( for : lang ) ) . tag ( lang )
}
}
2026-02-12 09:15:40 +00:00
. frame ( maxWidth : isCompactSettingsLayout ? . infinity : 220 , alignment : . leading )
2026-02-11 10:20:17 +00:00
. pickerStyle ( . menu )
2026-02-13 14:03:43 +00:00
. padding ( . vertical , UI . space6 )
. padding ( . horizontal , UI . space8 )
2026-02-11 10:20:17 +00:00
. background ( Color . clear )
. overlay (
RoundedRectangle ( cornerRadius : 8 )
. stroke ( Color . secondary . opacity ( 0.35 ) , lineWidth : 1 )
)
. cornerRadius ( 8 )
}
TextEditor ( text : templateBinding ( for : settingsTemplateLanguage ) )
2026-02-13 14:03:43 +00:00
. font ( Typography . monoBody )
2026-02-11 10:20:17 +00:00
. frame ( minHeight : 200 , maxHeight : 320 )
. scrollContentBackground ( . hidden )
. background ( Color . clear )
. overlay (
RoundedRectangle ( cornerRadius : 8 )
. stroke ( Color . secondary . opacity ( 0.2 ) , lineWidth : 1 )
)
2026-02-13 14:03:43 +00:00
HStack ( spacing : UI . space12 ) {
2026-02-19 08:09:35 +00:00
Button ( " Reset to Default " , role : . destructive ) {
2026-02-11 10:20:17 +00:00
UserDefaults . standard . removeObject ( forKey : templateOverrideKey ( for : settingsTemplateLanguage ) )
}
Button ( " Use Default Template " ) {
if let fallback = defaultTemplate ( for : settingsTemplateLanguage ) {
UserDefaults . standard . set ( fallback , forKey : templateOverrideKey ( for : settingsTemplateLanguage ) )
}
}
2026-02-19 08:09:35 +00:00
. buttonStyle ( . borderedProminent )
2026-02-11 10:20:17 +00:00
}
. frame ( maxWidth : . infinity , alignment : . leading )
}
2026-02-13 14:03:43 +00:00
. padding ( UI . groupPadding )
2026-02-11 10:20:17 +00:00
}
}
}
private var themeTab : some View {
let isCustom = selectedTheme = = " Custom "
let palette = themePaletteColors ( for : selectedTheme )
2026-02-19 08:44:24 +00:00
let previewTheme = currentEditorTheme ( colorScheme : effectiveSettingsColorScheme )
2026-02-11 10:20:17 +00:00
return settingsContainer ( maxWidth : 760 ) {
2026-02-19 08:09:35 +00:00
settingsSectionHeader (
icon : " paintpalette " ,
title : " Themes " ,
subtitle : " Pick a preset or customize token colors for your editing environment. "
)
HStack ( alignment : . top , spacing : UI . space16 ) {
Group {
2026-02-11 10:20:17 +00:00
#if os ( macOS )
2026-02-19 08:09:35 +00:00
let listView = List ( themes , id : \ . self , selection : $ selectedTheme ) { theme in
2026-02-11 10:20:17 +00:00
HStack {
Text ( theme )
2026-02-19 08:09:35 +00:00
Spacer ( minLength : 8 )
2026-02-11 10:20:17 +00:00
if theme = = selectedTheme {
2026-02-19 08:09:35 +00:00
Image ( systemName : " checkmark.circle.fill " )
2026-02-11 10:20:17 +00:00
. foregroundStyle ( . secondary )
}
}
. contentShape ( Rectangle ( ) )
. listRowBackground ( Color . clear )
}
2026-02-19 08:09:35 +00:00
. frame ( minWidth : 200 )
. listStyle ( . plain )
. background ( Color . clear )
if #available ( macOS 13.0 , * ) {
listView . scrollContentBackground ( . hidden )
} else {
listView
}
#else
let listView = List {
ForEach ( themes , id : \ . self ) { theme in
HStack {
Text ( theme )
Spacer ( )
if theme = = selectedTheme {
Image ( systemName : " checkmark " )
. foregroundStyle ( . secondary )
}
}
. contentShape ( Rectangle ( ) )
. onTapGesture {
selectedTheme = theme
}
. listRowBackground ( Color . clear )
}
}
. frame ( minWidth : isCompactSettingsLayout ? nil : 200 )
. listStyle ( . plain )
. background ( Color . clear )
if #available ( iOS 16.0 , * ) {
listView . scrollContentBackground ( . hidden )
} else {
listView
}
2026-02-11 10:20:17 +00:00
#endif
2026-02-19 08:09:35 +00:00
}
. padding ( UI . space8 )
2026-02-19 08:44:24 +00:00
. background ( settingsCardBackground ( cornerRadius : UI . cardCorner ) )
2026-02-11 10:20:17 +00:00
2026-02-19 08:09:35 +00:00
VStack ( alignment : . leading , spacing : UI . space12 ) {
HStack ( alignment : . firstTextBaseline , spacing : UI . space8 ) {
Text ( " Theme Colors " )
. font ( Typography . sectionHeadline )
Text ( isCustom ? " Custom " : " Preset " )
. font ( . caption . weight ( . semibold ) )
. padding ( . horizontal , 8 )
. padding ( . vertical , 3 )
. background (
Capsule ( )
. fill ( isCustom ? Color . blue . opacity ( 0.18 ) : Color . secondary . opacity ( 0.16 ) )
)
. foregroundStyle ( isCustom ? . blue : . secondary )
}
2026-02-11 10:20:17 +00:00
2026-02-19 08:09:35 +00:00
HStack ( spacing : UI . space8 ) {
Circle ( ) . fill ( palette . background ) . frame ( width : 12 , height : 12 )
Circle ( ) . fill ( palette . text ) . frame ( width : 12 , height : 12 )
Circle ( ) . fill ( palette . cursor ) . frame ( width : 12 , height : 12 )
Circle ( ) . fill ( palette . selection ) . frame ( width : 12 , height : 12 )
Spacer ( )
Text ( selectedTheme )
. font ( . caption )
. foregroundStyle ( . secondary )
}
. padding ( . horizontal , UI . space10 )
. padding ( . vertical , UI . space8 )
. background (
RoundedRectangle ( cornerRadius : 10 , style : . continuous )
. fill ( . thinMaterial )
)
2026-02-11 10:20:17 +00:00
2026-02-19 08:44:24 +00:00
themePreviewSnippet ( previewTheme : previewTheme )
2026-02-19 08:09:35 +00:00
VStack ( alignment : . leading , spacing : UI . space10 ) {
Text ( " Base " )
. font ( Typography . sectionSubheadline )
. foregroundStyle ( . secondary )
colorRow ( title : " Text " , color : isCustom ? hexBinding ( $ themeTextHex , fallback : . white ) : . constant ( palette . text ) )
. disabled ( ! isCustom )
colorRow ( title : " Background " , color : isCustom ? hexBinding ( $ themeBackgroundHex , fallback : . black ) : . constant ( palette . background ) )
. disabled ( ! isCustom )
colorRow ( title : " Cursor " , color : isCustom ? hexBinding ( $ themeCursorHex , fallback : . blue ) : . constant ( palette . cursor ) )
. disabled ( ! isCustom )
colorRow ( title : " Selection " , color : isCustom ? hexBinding ( $ themeSelectionHex , fallback : . gray ) : . constant ( palette . selection ) )
. disabled ( ! isCustom )
}
. padding ( UI . space12 )
2026-02-19 08:44:24 +00:00
. background ( settingsCardBackground ( cornerRadius : UI . cardCorner ) )
2026-02-19 08:09:35 +00:00
VStack ( alignment : . leading , spacing : UI . space10 ) {
Text ( " Syntax " )
. font ( Typography . sectionSubheadline )
. foregroundStyle ( . secondary )
colorRow ( title : " Keywords " , color : isCustom ? hexBinding ( $ themeKeywordHex , fallback : . yellow ) : . constant ( palette . keyword ) )
. disabled ( ! isCustom )
colorRow ( title : " Strings " , color : isCustom ? hexBinding ( $ themeStringHex , fallback : . blue ) : . constant ( palette . string ) )
. disabled ( ! isCustom )
colorRow ( title : " Numbers " , color : isCustom ? hexBinding ( $ themeNumberHex , fallback : . orange ) : . constant ( palette . number ) )
. disabled ( ! isCustom )
colorRow ( title : " Comments " , color : isCustom ? hexBinding ( $ themeCommentHex , fallback : . gray ) : . constant ( palette . comment ) )
. disabled ( ! isCustom )
colorRow ( title : " Types " , color : isCustom ? hexBinding ( $ themeTypeHex , fallback : . green ) : . constant ( palette . type ) )
. disabled ( ! isCustom )
colorRow ( title : " Builtins " , color : isCustom ? hexBinding ( $ themeBuiltinHex , fallback : . red ) : . constant ( palette . builtin ) )
. disabled ( ! isCustom )
}
. padding ( UI . space12 )
2026-02-19 08:44:24 +00:00
. background ( settingsCardBackground ( cornerRadius : UI . cardCorner ) )
2026-02-11 10:20:17 +00:00
Text ( isCustom ? " Custom theme applies immediately. " : " Select Custom to edit colors. " )
2026-02-13 14:03:43 +00:00
. font ( Typography . footnote )
2026-02-11 10:20:17 +00:00
. foregroundStyle ( . secondary )
}
. frame ( maxWidth : . infinity , alignment : . leading )
2026-02-19 08:09:35 +00:00
. padding ( UI . space12 )
2026-02-19 08:44:24 +00:00
. background ( settingsCardBackground ( cornerRadius : 14 ) )
2026-02-11 10:20:17 +00:00
}
2026-02-12 23:28:35 +00:00
#if os ( iOS )
. padding ( . top , 20 )
#endif
2026-02-11 10:20:17 +00:00
}
}
2026-02-19 08:09:35 +00:00
private var selectedAIModelBinding : Binding < AIModel > {
Binding (
get : { AIModel ( rawValue : selectedAIModelRaw ) ? ? . appleIntelligence } ,
set : { selectedAIModelRaw = $0 . rawValue }
)
}
private var moreTab : some View {
settingsContainer ( maxWidth : 560 ) {
VStack ( alignment : . leading , spacing : UI . space12 ) {
settingsSectionHeader (
icon : " ellipsis.circle " ,
title : " More " ,
subtitle : " AI setup, provider credentials, and support options. "
)
Picker ( " More Section " , selection : $ moreSectionTab ) {
Text ( " Support " ) . tag ( " support " )
2026-02-19 08:44:24 +00:00
Text ( " AI " ) . tag ( " ai " )
2026-02-19 08:09:35 +00:00
}
. pickerStyle ( . segmented )
}
. padding ( UI . groupPadding )
2026-02-19 08:44:24 +00:00
. background ( settingsCardBackground ( cornerRadius : 14 ) )
2026-02-19 08:09:35 +00:00
ZStack {
if moreSectionTab = = " ai " {
aiSection
. transition ( . opacity )
} else {
supportSection
. transition ( . opacity )
}
}
. animation ( . easeOut ( duration : 0.15 ) , value : moreSectionTab )
}
}
2026-02-11 10:20:17 +00:00
private var aiTab : some View {
2026-02-19 08:09:35 +00:00
settingsContainer ( maxWidth : 560 ) {
settingsSectionHeader (
icon : " brain.head.profile " ,
title : " AI " ,
subtitle : " AI model, privacy disclosure, and provider credentials. "
)
aiSection
}
}
private var supportTab : some View {
settingsContainer ( maxWidth : 560 ) {
settingsSectionHeader (
icon : " heart " ,
title : " Support " ,
subtitle : " Optional one-time support purchase and build-specific options. "
)
supportSection
}
}
private var aiSection : some View {
VStack ( spacing : UI . space20 ) {
2026-02-12 22:20:39 +00:00
GroupBox ( " AI Model " ) {
2026-02-13 14:03:43 +00:00
VStack ( alignment : . leading , spacing : UI . space12 ) {
2026-02-12 22:20:39 +00:00
Picker ( " Model " , selection : selectedAIModelBinding ) {
Text ( " Apple Intelligence " ) . tag ( AIModel . appleIntelligence )
Text ( " Grok " ) . tag ( AIModel . grok )
Text ( " OpenAI " ) . tag ( AIModel . openAI )
Text ( " Gemini " ) . tag ( AIModel . gemini )
Text ( " Anthropic " ) . tag ( AIModel . anthropic )
}
. pickerStyle ( . menu )
2026-02-19 08:09:35 +00:00
Text ( " The selected AI model is used for AI-assisted code completion. " )
2026-02-13 14:03:43 +00:00
. font ( Typography . footnote )
2026-02-12 22:20:39 +00:00
. foregroundStyle ( . secondary )
2026-02-19 08:09:35 +00:00
Button ( " Data Disclosure " ) {
showDataDisclosureDialog = true
}
. buttonStyle ( . bordered )
2026-02-12 22:20:39 +00:00
}
2026-02-13 14:03:43 +00:00
. padding ( UI . groupPadding )
2026-02-12 22:20:39 +00:00
}
2026-02-19 08:09:35 +00:00
. frame ( maxWidth : . infinity , alignment : . leading )
2026-02-12 22:20:39 +00:00
2026-02-11 10:20:17 +00:00
GroupBox ( " AI Provider API Keys " ) {
2026-02-13 14:03:43 +00:00
VStack ( alignment : . center , spacing : UI . space12 ) {
2026-02-11 10:20:17 +00:00
aiKeyRow ( title : " Grok " , placeholder : " sk-… " , value : $ grokAPIToken , provider : . grok )
aiKeyRow ( title : " OpenAI " , placeholder : " sk-… " , value : $ openAIAPIToken , provider : . openAI )
aiKeyRow ( title : " Gemini " , placeholder : " AIza… " , value : $ geminiAPIToken , provider : . gemini )
aiKeyRow ( title : " Anthropic " , placeholder : " sk-ant-… " , value : $ anthropicAPIToken , provider : . anthropic )
}
. frame ( maxWidth : . infinity , alignment : . center )
2026-02-13 14:03:43 +00:00
. padding ( UI . groupPadding )
2026-02-11 10:20:17 +00:00
}
2026-02-18 22:56:46 +00:00
. frame ( maxWidth : . infinity , alignment : . leading )
}
}
2026-02-19 08:09:35 +00:00
private var supportSection : some View {
VStack ( spacing : 0 ) {
2026-02-11 10:20:17 +00:00
GroupBox ( " Support Development " ) {
2026-02-13 14:03:43 +00:00
VStack ( alignment : . leading , spacing : UI . space12 ) {
2026-02-11 10:20:17 +00:00
Text ( " In-App Purchase is optional and only used to support the app. " )
. foregroundStyle ( . secondary )
2026-02-12 15:55:14 +00:00
Text ( " One-time, non-consumable purchase. No subscription and no auto-renewal. " )
2026-02-13 14:03:43 +00:00
. font ( Typography . footnote )
2026-02-12 15:55:14 +00:00
. foregroundStyle ( . secondary )
2026-02-11 10:20:17 +00:00
if supportPurchaseManager . canUseInAppPurchases {
Text ( " Price: \( supportPurchaseManager . supportPriceLabel ) " )
2026-02-13 14:03:43 +00:00
. font ( Typography . sectionHeadline )
2026-02-11 10:20:17 +00:00
if supportPurchaseManager . hasSupported {
Label ( " Thank you for your support. " , systemImage : " checkmark.seal.fill " )
. foregroundStyle ( . green )
}
2026-02-13 14:03:43 +00:00
HStack ( spacing : UI . space12 ) {
2026-02-11 10:20:17 +00:00
Button ( supportPurchaseManager . isPurchasing ? " Purchasing… " : " Support the App " ) {
showSupportPurchaseDialog = true
}
2026-02-19 08:09:35 +00:00
. buttonStyle ( . borderedProminent )
2026-02-11 10:20:17 +00:00
. disabled ( supportPurchaseManager . isPurchasing || supportPurchaseManager . isLoadingProducts )
Button ( " Refresh Price " ) {
2026-02-16 19:02:43 +00:00
Task { await supportPurchaseManager . refreshPrice ( ) }
2026-02-11 10:20:17 +00:00
}
2026-02-19 08:09:35 +00:00
. buttonStyle ( . bordered )
2026-02-11 10:20:17 +00:00
. disabled ( supportPurchaseManager . isLoadingProducts )
}
} else {
Text ( " Direct notarized builds are unaffected: all editor features stay fully available without any purchase. " )
2026-02-13 14:03:43 +00:00
. font ( Typography . footnote )
2026-02-11 10:20:17 +00:00
. foregroundStyle ( . secondary )
Text ( " Support purchase is available only in App Store/TestFlight builds. " )
2026-02-13 14:03:43 +00:00
. font ( Typography . footnote )
2026-02-11 10:20:17 +00:00
. foregroundStyle ( . secondary )
}
2026-02-12 15:55:14 +00:00
2026-02-12 17:31:51 +00:00
if let privacyPolicyURL {
Link ( " Privacy Policy " , destination : privacyPolicyURL )
. font ( . footnote . weight ( . semibold ) )
}
2026-02-12 15:55:14 +00:00
2026-02-11 10:20:17 +00:00
if supportPurchaseManager . canBypassInCurrentBuild {
Divider ( )
Text ( " TestFlight/Sandbox: You can bypass purchase for testing. " )
2026-02-13 14:03:43 +00:00
. font ( Typography . footnote )
2026-02-11 10:20:17 +00:00
. foregroundStyle ( . secondary )
2026-02-13 14:03:43 +00:00
HStack ( spacing : UI . space12 ) {
2026-02-11 10:20:17 +00:00
Button ( " Bypass Purchase (Testing) " ) {
supportPurchaseManager . bypassForTesting ( )
}
Button ( " Clear Bypass " ) {
supportPurchaseManager . clearBypassForTesting ( )
}
}
}
}
2026-02-13 14:03:43 +00:00
. padding ( UI . groupPadding )
2026-02-11 10:20:17 +00:00
}
}
}
2026-02-14 13:24:01 +00:00
#if os ( macOS )
private var updatesTab : some View {
settingsContainer ( maxWidth : 620 ) {
GroupBox ( " GitHub Release Updates " ) {
VStack ( alignment : . leading , spacing : UI . space12 ) {
Toggle ( " Automatically check for updates " , isOn : $ autoCheckForUpdates )
HStack ( alignment : . center , spacing : UI . space12 ) {
Text ( " Check Interval " )
2026-02-19 08:09:35 +00:00
. frame ( width : isCompactSettingsLayout ? nil : standardLabelWidth , alignment : . leading )
2026-02-14 13:24:01 +00:00
Picker ( " " , selection : $ updateCheckIntervalRaw ) {
ForEach ( AppUpdateCheckInterval . allCases ) { interval in
Text ( interval . title ) . tag ( interval . rawValue )
}
}
. pickerStyle ( . menu )
. frame ( maxWidth : isCompactSettingsLayout ? . infinity : 220 , alignment : . leading )
}
. disabled ( ! autoCheckForUpdates )
Toggle ( " Automatically install updates when available " , isOn : $ autoDownloadUpdates )
. disabled ( ! autoCheckForUpdates )
HStack ( spacing : UI . space8 ) {
Button ( " Check Now " ) {
Task { await appUpdateManager . checkForUpdates ( source : . manual ) }
}
. buttonStyle ( . borderedProminent )
if let checkedAt = appUpdateManager . lastCheckedAt {
Text ( " Last checked: \( checkedAt . formatted ( date : . abbreviated , time : . shortened ) ) " )
. font ( Typography . footnote )
. foregroundStyle ( . secondary )
}
}
VStack ( alignment : . leading , spacing : UI . space6 ) {
Text ( " Last check result: \( appUpdateManager . lastCheckResultSummary ) " )
. font ( Typography . footnote )
. foregroundStyle ( . secondary )
if let pausedUntil = appUpdateManager . pausedUntil , pausedUntil > Date ( ) {
Text ( " Auto-check pause active until \( pausedUntil . formatted ( date : . abbreviated , time : . shortened ) ) ( \( appUpdateManager . consecutiveFailureCount ) consecutive failures). " )
. font ( Typography . footnote )
. foregroundStyle ( . orange )
}
}
Text ( " Uses GitHub release assets only. App Store Connect releases are not used by this updater. " )
. font ( Typography . footnote )
. foregroundStyle ( . secondary )
}
. padding ( UI . groupPadding )
}
}
}
#endif
2026-02-19 08:09:35 +00:00
private var dataDisclosureDialog : some View {
NavigationStack {
ScrollView {
VStack ( alignment : . leading , spacing : UI . space16 ) {
Text ( " Data Disclosure " )
. font ( . title3 . weight ( . semibold ) )
Text ( " The application does not collect analytics data, usage telemetry, advertising identifiers, device fingerprints, or background behavioral metrics. No automatic data transmission to developer-controlled servers occurs. " )
Text ( " AI-assisted code completion is an optional feature. External network communication only occurs when a user explicitly enables AI completion and selects an external AI provider within the application settings. " )
Text ( " When AI completion is triggered, the application transmits only the minimal contextual text necessary to generate a completion suggestion. This typically includes the code immediately surrounding the cursor position or the active selection. " )
Text ( " The application does not automatically transmit full project folders, unrelated files, entire file system contents, contact data, location data, or device-specific identifiers. " )
Text ( " Authentication credentials (API keys) for external AI providers are stored securely in the system keychain and are transmitted only to the user-selected provider for the purpose of completing the AI request. " )
Text ( " All external communication is performed over encrypted HTTPS connections. If AI completion is disabled, the application performs no external AI-related network requests. " )
}
. font ( Typography . footnote )
. frame ( maxWidth : . infinity , alignment : . leading )
. padding ( UI . space20 )
}
. navigationTitle ( " AI Data Disclosure " )
#if os ( iOS )
. navigationBarTitleDisplayMode ( . inline )
. toolbar {
ToolbarItem ( placement : . topBarTrailing ) {
Button ( " Done " ) {
showDataDisclosureDialog = false
}
}
}
#endif
}
}
private func settingsSectionHeader ( icon : String , title : String , subtitle : String ) -> some View {
HStack ( alignment : . top , spacing : UI . space12 ) {
Image ( systemName : icon )
. font ( . title3 . weight ( . semibold ) )
. foregroundStyle ( . secondary )
. frame ( width : 28 , alignment : . center )
VStack ( alignment : . leading , spacing : UI . space6 ) {
Text ( title )
. font ( Typography . sectionTitle )
Text ( subtitle )
. font ( Typography . footnote )
. foregroundStyle ( . secondary )
}
Spacer ( minLength : 0 )
}
. frame ( maxWidth : . infinity , alignment : . leading )
}
2026-02-11 10:20:17 +00:00
private func settingsContainer < Content : View > ( maxWidth : CGFloat = 560 , @ ViewBuilder _ content : ( ) -> Content ) -> some View {
2026-02-19 08:44:24 +00:00
let effectiveMaxWidth = settingsEffectiveMaxWidth ( base : maxWidth )
return ScrollView {
2026-02-19 08:09:35 +00:00
VStack ( alignment : settingsShouldUseLeadingAlignment ? . leading : . center , spacing : UI . space20 ) {
2026-02-11 10:20:17 +00:00
content ( )
}
2026-02-19 08:44:24 +00:00
. frame ( maxWidth : effectiveMaxWidth , alignment : . center )
2026-02-19 08:09:35 +00:00
. frame ( maxWidth : . infinity , maxHeight : . infinity , alignment : settingsShouldUseLeadingAlignment ? . topLeading : . top )
2026-02-13 14:03:43 +00:00
. padding ( . top , UI . topPadding )
. padding ( . bottom , UI . bottomPadding )
2026-02-19 08:44:24 +00:00
. padding ( . horizontal , settingsHorizontalPadding )
2026-02-11 10:20:17 +00:00
}
2026-02-19 08:09:35 +00:00
. background ( . ultraThinMaterial )
2026-02-11 10:20:17 +00:00
}
2026-02-19 08:44:24 +00:00
private var settingsHorizontalPadding : CGFloat {
#if os ( iOS )
if isCompactSettingsLayout { return UI . sidePaddingCompact }
if useTwoColumnSettingsLayout { return UI . sidePaddingIPadRegular }
return UI . sidePaddingRegular
#else
return isCompactSettingsLayout ? UI . sidePaddingCompact : UI . sidePaddingRegular
#endif
}
private func settingsEffectiveMaxWidth ( base : CGFloat ) -> CGFloat {
#if os ( iOS )
if useTwoColumnSettingsLayout { return max ( base , 780 ) }
return base
#else
return base
#endif
}
private func settingsCardBackground ( cornerRadius : CGFloat ) -> some View {
RoundedRectangle ( cornerRadius : cornerRadius , style : . continuous )
. fill ( . regularMaterial )
. overlay (
RoundedRectangle ( cornerRadius : cornerRadius , style : . continuous )
. stroke ( Color . secondary . opacity ( UI . cardStrokeOpacity ) , lineWidth : 1 )
)
}
private var effectiveSettingsColorScheme : ColorScheme {
preferredColorSchemeOverride ? ? systemColorScheme
}
2026-02-11 10:20:17 +00:00
private func colorRow ( title : String , color : Binding < Color > ) -> some View {
HStack {
Text ( title )
2026-02-19 08:09:35 +00:00
. frame ( width : isCompactSettingsLayout ? nil : standardLabelWidth , alignment : . leading )
2026-02-11 10:20:17 +00:00
ColorPicker ( " " , selection : color )
. labelsHidden ( )
Spacer ( )
}
}
private func aiKeyRow ( title : String , placeholder : String , value : Binding < String > , provider : APITokenKey ) -> some View {
2026-02-12 09:15:40 +00:00
Group {
if isCompactSettingsLayout {
2026-02-13 14:03:43 +00:00
VStack ( alignment : . leading , spacing : UI . space8 ) {
2026-02-12 09:15:40 +00:00
Text ( title )
SecureField ( placeholder , text : value )
. textFieldStyle ( . plain )
2026-02-13 14:03:43 +00:00
. padding ( . vertical , UI . space6 )
. padding ( . horizontal , UI . space8 )
2026-02-12 09:15:40 +00:00
. background ( inputFieldBackground )
. overlay (
2026-02-13 14:03:43 +00:00
RoundedRectangle ( cornerRadius : UI . fieldCorner )
2026-02-12 09:15:40 +00:00
. stroke ( Color . secondary . opacity ( 0.35 ) , lineWidth : 1 )
)
2026-02-13 14:03:43 +00:00
. cornerRadius ( UI . fieldCorner )
2026-02-12 09:15:40 +00:00
. onChange ( of : value . wrappedValue ) { _ , new in
SecureTokenStore . setToken ( new , for : provider )
}
2026-02-11 10:20:17 +00:00
}
2026-02-12 09:15:40 +00:00
} else {
2026-02-13 14:03:43 +00:00
HStack ( spacing : UI . space12 ) {
2026-02-12 09:15:40 +00:00
Text ( title )
2026-02-19 08:09:35 +00:00
. frame ( width : standardLabelWidth , alignment : . leading )
2026-02-12 09:15:40 +00:00
SecureField ( placeholder , text : value )
. textFieldStyle ( . plain )
2026-02-13 14:03:43 +00:00
. padding ( . vertical , UI . space6 )
. padding ( . horizontal , UI . space8 )
2026-02-12 09:15:40 +00:00
. background ( inputFieldBackground )
. overlay (
2026-02-13 14:03:43 +00:00
RoundedRectangle ( cornerRadius : UI . fieldCorner )
2026-02-12 09:15:40 +00:00
. stroke ( Color . secondary . opacity ( 0.35 ) , lineWidth : 1 )
)
2026-02-13 14:03:43 +00:00
. cornerRadius ( UI . fieldCorner )
2026-02-19 08:09:35 +00:00
. frame ( maxWidth : 360 )
2026-02-12 09:15:40 +00:00
. onChange ( of : value . wrappedValue ) { _ , new in
SecureTokenStore . setToken ( new , for : provider )
}
}
}
2026-02-11 10:20:17 +00:00
}
2026-02-12 09:15:40 +00:00
. frame ( maxWidth : . infinity , alignment : isCompactSettingsLayout ? . leading : . center )
2026-02-11 10:20:17 +00:00
}
private func languageLabel ( for lang : String ) -> String {
switch lang {
case " php " : return " PHP "
case " cobol " : return " COBOL "
case " dotenv " : return " Dotenv "
case " proto " : return " Proto "
case " graphql " : return " GraphQL "
case " rst " : return " reStructuredText "
case " nginx " : return " Nginx "
case " objective-c " : return " Objective-C "
case " csharp " : return " C# "
case " c " : return " C "
case " cpp " : return " C++ "
case " json " : return " JSON "
case " xml " : return " XML "
case " yaml " : return " YAML "
case " toml " : return " TOML "
case " csv " : return " CSV "
case " ini " : return " INI "
case " sql " : return " SQL "
case " vim " : return " Vim "
case " log " : return " Log "
case " ipynb " : return " Jupyter Notebook "
case " html " : return " HTML "
2026-02-13 11:02:39 +00:00
case " expressionengine " : return " ExpressionEngine "
2026-02-11 10:20:17 +00:00
case " css " : return " CSS "
case " standard " : return " Standard "
default : return lang . capitalized
}
}
2026-02-19 08:09:35 +00:00
private var settingsShouldUseLeadingAlignment : Bool {
#if os ( iOS )
true
#else
isCompactSettingsLayout
#endif
}
private func migrateLegacyPinkSettingsIfNeeded ( ) {
if themeStringHex . uppercased ( ) = = " #FF7AD9 " {
themeStringHex = " #4EA4FF "
}
}
2026-02-11 10:20:17 +00:00
private func templateOverrideKey ( for language : String ) -> String {
" TemplateOverride_ \( language ) "
}
private func templateBinding ( for language : String ) -> Binding < String > {
Binding < String > (
get : { UserDefaults . standard . string ( forKey : templateOverrideKey ( for : language ) ) ? ? defaultTemplate ( for : language ) ? ? " " } ,
set : { newValue in UserDefaults . standard . set ( newValue , forKey : templateOverrideKey ( for : language ) ) }
)
}
private func defaultTemplate ( for language : String ) -> String ? {
switch language {
case " swift " :
return " import Foundation \n \n // TODO: Add code here \n "
case " python " :
return " def main(): \n pass \n \n \n if __name__ == \" __main__ \" : \n main() \n "
case " javascript " :
return " \" use strict \" ; \n \n function main() { \n // TODO: Add code here \n } \n \n main(); \n "
case " typescript " :
return " function main(): void { \n // TODO: Add code here \n } \n \n main(); \n "
case " java " :
return " public class Main { \n public static void main(String[] args) { \n // TODO: Add code here \n } \n } \n "
case " kotlin " :
return " fun main() { \n // TODO: Add code here \n } \n "
case " go " :
return " package main \n \n import \" fmt \" \n \n func main() { \n fmt.Println( \" Hello \" ) \n } \n "
case " ruby " :
return " def main \n # TODO: Add code here \n end \n \n main \n "
case " rust " :
return " fn main() { \n println!( \" Hello \" ); \n } \n "
case " c " :
return " #include <stdio.h> \n \n int main(void) { \n printf( \" Hello \\ n \" ); \n return 0; \n } \n "
case " cpp " :
return " #include <iostream> \n \n int main() { \n std::cout << \" Hello \" << std::endl; \n return 0; \n } \n "
case " csharp " :
return " using System; \n \n class Program { \n static void Main() { \n Console.WriteLine( \" Hello \" ); \n } \n } \n "
case " objective-c " :
return " #import <Foundation/Foundation.h> \n \n int main(int argc, const char * argv[]) { \n @autoreleasepool { \n NSLog(@ \" Hello \" ); \n } \n return 0; \n } \n "
case " php " :
return " <?php \n \n function main() { \n // TODO: Add code here \n } \n \n main(); \n "
case " html " :
return " <!doctype html> \n <html> \n <head> \n <meta charset= \" utf-8 \" /> \n <title>Document</title> \n </head> \n <body> \n \n </body> \n </html> \n "
2026-02-13 11:02:39 +00:00
case " expressionengine " :
return " {exp:channel:entries channel= \" news \" limit= \" 10 \" } \n <article> \n <h2>{title}</h2> \n <p>{summary}</p> \n </article> \n {/exp:channel:entries} \n "
2026-02-11 10:20:17 +00:00
case " css " :
return " body { \n margin: 0; \n font-family: system-ui, sans-serif; \n } \n "
case " json " :
return " { \n \" key \" : \" value \" \n } \n "
case " yaml " :
return " key: value \n "
case " toml " :
return " key = \" value \" \n "
case " sql " :
return " SELECT * \n FROM table_name; \n "
case " bash " , " zsh " :
return " #!/usr/bin/env \( language ) \n \n "
case " markdown " :
return " # Title \n \n "
case " plain " :
return " "
default :
return " TODO \n "
}
}
private func hexBinding ( _ hex : Binding < String > , fallback : Color ) -> Binding < Color > {
Binding < Color > (
get : { colorFromHex ( hex . wrappedValue , fallback : fallback ) } ,
set : { newColor in hex . wrappedValue = colorToHex ( newColor ) }
)
}
2026-02-19 08:09:35 +00:00
2026-02-19 08:44:24 +00:00
private func themePreviewSnippet ( previewTheme : EditorTheme ) -> some View {
2026-02-19 08:09:35 +00:00
VStack ( alignment : . leading , spacing : 6 ) {
Text ( " Preview " )
. font ( . caption . weight ( . semibold ) )
. foregroundStyle ( . secondary )
VStack ( alignment : . leading , spacing : 3 ) {
Text ( " func computeTotal(_ values: [Int]) -> Int { " )
2026-02-19 08:44:24 +00:00
. foregroundStyle ( previewTheme . syntax . keyword )
2026-02-19 08:09:35 +00:00
Text ( " let sum = values.reduce(0, +) " )
2026-02-19 08:44:24 +00:00
. foregroundStyle ( previewTheme . text )
2026-02-19 08:09:35 +00:00
Text ( " // tax adjustment " )
2026-02-19 08:44:24 +00:00
. foregroundStyle ( previewTheme . syntax . comment )
2026-02-19 08:09:35 +00:00
Text ( " return sum + 42 " )
2026-02-19 08:44:24 +00:00
. foregroundStyle ( previewTheme . syntax . number )
2026-02-19 08:09:35 +00:00
Text ( " } " )
2026-02-19 08:44:24 +00:00
. foregroundStyle ( previewTheme . syntax . keyword )
2026-02-19 08:09:35 +00:00
}
. font ( . system ( size : 12 , weight : . regular , design : . monospaced ) )
. padding ( UI . space10 )
. frame ( maxWidth : . infinity , alignment : . leading )
. background (
RoundedRectangle ( cornerRadius : 10 , style : . continuous )
2026-02-19 08:44:24 +00:00
. fill ( previewTheme . background )
2026-02-19 08:09:35 +00:00
. overlay (
RoundedRectangle ( cornerRadius : 10 , style : . continuous )
2026-02-19 08:44:24 +00:00
. stroke ( previewTheme . selection . opacity ( 0.7 ) , lineWidth : 1 )
2026-02-19 08:09:35 +00:00
)
)
}
}
2026-02-11 10:20:17 +00:00
}
#if os ( macOS )
2026-02-13 14:03:43 +00:00
struct SettingsWindowConfigurator : NSViewRepresentable {
let minSize : NSSize
let idealSize : NSSize
2026-02-14 22:15:22 +00:00
let translucentEnabled : Bool
2026-02-13 14:03:43 +00:00
func makeNSView ( context : Context ) -> NSView {
let view = NSView ( frame : . zero )
DispatchQueue . main . async {
apply ( to : view . window )
}
return view
}
func updateNSView ( _ nsView : NSView , context : Context ) {
DispatchQueue . main . async {
apply ( to : nsView . window )
}
}
private func apply ( to window : NSWindow ? ) {
guard let window else { return }
window . minSize = NSSize (
width : max ( window . minSize . width , minSize . width ) ,
height : max ( window . minSize . height , minSize . height )
)
2026-02-14 22:15:22 +00:00
// M a t c h n a t i v e m a c O S S e t t i n g s l a y o u t : c e n t e r e d p r e f e r e n c e t a b s a n d h i d d e n t i t l e t e x t .
window . toolbarStyle = . preference
window . titleVisibility = . hidden
2026-02-13 14:03:43 +00:00
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 {
2026-02-19 08:09:35 +00:00
// K e e p s i z i n g i n f r a m e - s p a c e ; m i x i n g f r a m e c h e c k s w i t h c o n t e n t - s i z e a s s i g n m e n t
// c a u s e s g r o w t h w h e n t i t l e b a r / t r a n s l u c e n c y s t y l e c h a n g e s .
var frame = window . frame
frame . size = NSSize ( width : targetWidth , height : targetHeight )
window . setFrame ( frame , display : true )
2026-02-13 14:03:43 +00:00
}
2026-02-14 22:15:22 +00:00
// K e e p s e t t i n g s - w i n d o w t r a n s l u c e n c y i n s y n c w i t h o u t r e l y i n g o n e d i t o r v i e w e v e n t s .
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
}
2026-02-13 14:03:43 +00:00
}
}
2026-02-11 10:20:17 +00:00
#endif
#if DEBUG && canImport ( SwiftUI ) && canImport ( PreviewsMacros )
# Preview {
NeonSettingsView (
supportsOpenInTabs : true ,
supportsTranslucency : true
)
}
#endif