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-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 "
#if os ( macOS )
@ State private var fontPicker = FontPickerController ( )
#endif
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-12 22:20:39 +00:00
@ State private var availableEditorFonts : [ String ] = [ ]
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 "
@ AppStorage ( " SettingsThemeStringColor " ) private var themeStringHex : String = " #FF7AD9 "
@ AppStorage ( " SettingsThemeNumberColor " ) private var themeNumberHex : String = " #FFB86C "
@ AppStorage ( " SettingsThemeCommentColor " ) private var themeCommentHex : String = " #7F8C98 "
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-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
static let topPadding : CGFloat = 18
static let bottomPadding : CGFloat = 24
}
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-11 10:20:17 +00:00
init (
supportsOpenInTabs : Bool = true ,
supportsTranslucency : Bool = true
) {
self . supportsOpenInTabs = supportsOpenInTabs
self . supportsTranslucency = supportsTranslucency
}
var body : some View {
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 " )
aiTab
. tabItem { Label ( " AI " , systemImage : " brain.head.profile " ) }
. tag ( " ai " )
supportTab
. tabItem { Label ( " Support " , systemImage : " heart " ) }
. tag ( " support " )
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-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-14 20:57:32 +00:00
selectedTheme = canonicalThemeName ( selectedTheme )
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 )
fontPicker . onChange = { selected in
2026-02-12 22:20:39 +00:00
useSystemFont = false
2026-02-11 10:20:17 +00:00
editorFontName = selected . fontName
editorFontSize = Double ( selected . pointSize )
}
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
if newValue = = " ai " {
loadAPITokensIfNeeded ( )
}
}
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 ( " Restore Purchases " ) {
Task { await supportPurchaseManager . restorePurchases ( ) }
}
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-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 {
GroupBox ( " Window " ) {
2026-02-13 14:03:43 +00:00
VStack ( alignment : . leading , spacing : UI . space12 ) {
2026-02-11 10:20:17 +00:00
if supportsOpenInTabs {
2026-02-13 14:03:43 +00:00
HStack ( alignment : . center , spacing : UI . space12 ) {
2026-02-11 10:20:17 +00:00
Text ( " Open in Tabs " )
2026-02-12 09:15:40 +00:00
. frame ( width : isCompactSettingsLayout ? nil : 140 , alignment : . leading )
2026-02-11 10:20:17 +00:00
Picker ( " " , selection : $ openInTabs ) {
Text ( " Follow System " ) . tag ( " system " )
Text ( " Always " ) . tag ( " always " )
Text ( " Never " ) . tag ( " never " )
}
. pickerStyle ( . segmented )
}
}
2026-02-13 14:03:43 +00:00
HStack ( alignment : . center , spacing : UI . space12 ) {
2026-02-11 10:20:17 +00:00
Text ( " Appearance " )
2026-02-12 09:15:40 +00:00
. frame ( width : isCompactSettingsLayout ? nil : 140 , alignment : . leading )
2026-02-11 10:20:17 +00:00
Picker ( " " , selection : $ appearance ) {
Text ( " System " ) . tag ( " system " )
Text ( " Light " ) . tag ( " light " )
Text ( " Dark " ) . tag ( " dark " )
}
. pickerStyle ( . segmented )
}
if supportsTranslucency {
Toggle ( " Translucent Window " , isOn : $ translucentWindow )
. frame ( maxWidth : . infinity , alignment : . leading )
}
}
2026-02-13 14:03:43 +00:00
. padding ( UI . groupPadding )
2026-02-13 17:53:09 +00:00
. onChange ( of : selectedFontValue ) { _ , _ in
useSystemFont = ( selectedFontValue = = systemFontSentinel )
if ! useSystemFont && ! selectedFontValue . isEmpty {
editorFontName = selectedFontValue
}
}
. onChange ( of : useSystemFont ) { _ , isSystem in
if isSystem {
selectedFontValue = systemFontSentinel
} else if ! editorFontName . isEmpty {
selectedFontValue = editorFontName
}
}
. onChange ( of : editorFontName ) { _ , newValue in
guard ! useSystemFont else { return }
if ! newValue . isEmpty {
selectedFontValue = newValue
}
}
2026-02-11 10:20:17 +00:00
}
GroupBox ( " Editor Font " ) {
2026-02-13 14:03:43 +00:00
VStack ( alignment : . leading , spacing : UI . space12 ) {
2026-02-12 22:20:39 +00:00
Toggle ( " Use System Font " , isOn : $ useSystemFont )
. frame ( maxWidth : . infinity , alignment : . leading )
2026-02-13 14:03:43 +00:00
HStack ( alignment : . center , spacing : UI . space12 ) {
2026-02-12 22:20:39 +00:00
Text ( " Font " )
2026-02-12 09:15:40 +00:00
. frame ( width : isCompactSettingsLayout ? nil : 140 , alignment : . leading )
2026-02-13 17:53:09 +00:00
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 ( )
}
. buttonStyle ( . borderless )
2026-02-12 22:20:39 +00:00
}
2026-02-13 17:53:09 +00:00
if showFontList {
Picker ( " " , selection : selectedFontBinding ) {
Text ( " System " ) . tag ( systemFontSentinel )
ForEach ( availableEditorFonts , id : \ . self ) { fontName in
Text ( fontName ) . tag ( fontName )
}
}
. 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-13 17:53:09 +00:00
. frame ( maxWidth : isCompactSettingsLayout ? . infinity : 240 , alignment : . leading )
2026-02-11 10:20:17 +00:00
#if os ( macOS )
Button ( " Choose… " ) {
2026-02-12 22:20:39 +00:00
useSystemFont = false
2026-02-11 10:20:17 +00:00
fontPicker . open ( currentName : editorFontName , size : editorFontSize )
}
2026-02-12 22:20:39 +00:00
. disabled ( useSystemFont )
2026-02-11 10:20:17 +00:00
#endif
2026-02-12 22:20:39 +00:00
}
2026-02-13 14:03:43 +00:00
HStack ( alignment : . center , spacing : UI . space12 ) {
2026-02-12 22:20:39 +00:00
Text ( " Font Size " )
. frame ( width : isCompactSettingsLayout ? nil : 140 , alignment : . leading )
2026-02-11 10:20:17 +00:00
Stepper ( value : $ editorFontSize , in : 10. . . 28 , step : 1 ) {
Text ( " \( Int ( editorFontSize ) ) pt " )
}
2026-02-12 22:20:39 +00:00
. frame ( maxWidth : isCompactSettingsLayout ? . infinity : 220 , alignment : . leading )
2026-02-11 10:20:17 +00:00
}
2026-02-13 14:03:43 +00:00
HStack ( alignment : . center , spacing : UI . space12 ) {
2026-02-11 10:20:17 +00:00
Text ( " Line Height " )
2026-02-12 09:15:40 +00:00
. frame ( width : isCompactSettingsLayout ? nil : 140 , alignment : . leading )
2026-02-11 10:20:17 +00:00
Slider ( value : $ lineHeight , in : 1.0 . . . 1.8 , step : 0.05 )
2026-02-12 09:15:40 +00:00
. frame ( maxWidth : isCompactSettingsLayout ? . infinity : 240 )
2026-02-11 10:20:17 +00:00
Text ( String ( format : " %.2fx " , lineHeight ) )
. frame ( width : 54 , alignment : . trailing )
}
}
2026-02-13 14:03:43 +00:00
. padding ( UI . groupPadding )
2026-02-11 10:20:17 +00:00
}
2026-02-12 22:20:39 +00:00
GroupBox ( " Startup " ) {
2026-02-13 14:03:43 +00:00
VStack ( alignment : . leading , spacing : UI . space12 ) {
2026-02-12 22:20:39 +00:00
Toggle ( " Open with Blank Document " , isOn : $ openWithBlankDocument )
Toggle ( " Reopen Last Session " , isOn : $ reopenLastSession )
. disabled ( openWithBlankDocument )
2026-02-13 14:03:43 +00:00
HStack ( alignment : . center , spacing : UI . space12 ) {
2026-02-12 22:20:39 +00:00
Text ( " Default New File Language " )
. frame ( width : isCompactSettingsLayout ? nil : 180 , alignment : . leading )
Picker ( " " , selection : $ defaultNewFileLanguage ) {
ForEach ( templateLanguages , id : \ . self ) { lang in
Text ( languageLabel ( for : lang ) ) . tag ( lang )
}
}
. pickerStyle ( . menu )
}
}
2026-02-13 14:03:43 +00:00
. padding ( UI . groupPadding )
2026-02-12 22:20:39 +00:00
}
GroupBox ( " Confirmations " ) {
2026-02-13 14:03:43 +00:00
VStack ( alignment : . leading , spacing : UI . space12 ) {
2026-02-12 22:20:39 +00:00
Toggle ( " Confirm Before Closing Dirty Tab " , isOn : $ confirmCloseDirtyTab )
Toggle ( " Confirm Before Clearing Editor " , isOn : $ confirmClearEditor )
}
2026-02-13 14:03:43 +00:00
. padding ( UI . groupPadding )
2026-02-12 22:20:39 +00:00
}
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 }
return editorFontName
} ,
set : { selectedFontValue = $0 }
)
}
2026-02-13 00:19:31 +00:00
private func loadAvailableEditorFontsIfNeeded ( ) {
if ! availableEditorFonts . isEmpty {
selectedFontValue = useSystemFont ? systemFontSentinel : ( editorFontName . isEmpty ? systemFontSentinel : editorFontName )
return
}
if ! Self . cachedEditorFonts . isEmpty {
availableEditorFonts = Self . cachedEditorFonts
selectedFontValue = useSystemFont ? systemFontSentinel : ( editorFontName . isEmpty ? systemFontSentinel : editorFontName )
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
selectedFontValue = useSystemFont ? systemFontSentinel : ( editorFontName . isEmpty ? systemFontSentinel : editorFontName )
}
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 ) {
GroupBox ( " Editor " ) {
VStack ( 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 )
}
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 ) " )
}
}
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 )
}
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-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 ) {
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-12 09:15:40 +00:00
. frame ( width : isCompactSettingsLayout ? nil : 140 , 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-11 10:20:17 +00:00
Button ( " Reset to Default " ) {
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 ) )
}
}
}
. 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 )
return settingsContainer ( maxWidth : 760 ) {
2026-02-13 14:03:43 +00:00
HStack ( spacing : UI . space16 ) {
2026-02-11 10:20:17 +00:00
#if os ( macOS )
let listView = List ( themes , id : \ . self , selection : $ selectedTheme ) { theme in
Text ( theme )
. listRowBackground ( Color . clear )
}
. 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 )
}
}
2026-02-12 09:15:40 +00:00
. frame ( minWidth : isCompactSettingsLayout ? nil : 200 )
2026-02-11 10:20:17 +00:00
. listStyle ( . plain )
. background ( Color . clear )
if #available ( iOS 16.0 , * ) {
listView . scrollContentBackground ( . hidden )
} else {
listView
}
#endif
2026-02-13 14:03:43 +00:00
VStack ( alignment : . leading , spacing : UI . space16 ) {
2026-02-11 10:20:17 +00:00
Text ( " Theme Colors " )
2026-02-13 14:03:43 +00:00
. font ( Typography . sectionHeadline )
2026-02-12 23:28:35 +00:00
Spacer ( minLength : 6 )
2026-02-11 10:20:17 +00:00
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 )
Divider ( )
Text ( " Syntax " )
2026-02-13 14:03:43 +00:00
. font ( Typography . sectionSubheadline )
2026-02-11 10:20:17 +00:00
. foregroundStyle ( . secondary )
colorRow ( title : " Keywords " , color : isCustom ? hexBinding ( $ themeKeywordHex , fallback : . yellow ) : . constant ( palette . keyword ) )
. disabled ( ! isCustom )
colorRow ( title : " Strings " , color : isCustom ? hexBinding ( $ themeStringHex , fallback : . pink ) : . 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 : . constant ( palette . type ) )
. disabled ( true )
colorRow ( title : " Builtins " , color : . constant ( palette . builtin ) )
. disabled ( true )
Spacer ( )
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-12 23:28:35 +00:00
#if os ( iOS )
. padding ( . top , 20 )
#endif
2026-02-11 10:20:17 +00:00
}
}
private var aiTab : some View {
settingsContainer ( maxWidth : 520 ) {
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 )
Text ( " Choose the default model used by editor AI actions. " )
2026-02-13 14:03:43 +00:00
. font ( Typography . footnote )
2026-02-12 22:20:39 +00:00
. foregroundStyle ( . secondary )
}
2026-02-13 14:03:43 +00:00
. padding ( UI . groupPadding )
2026-02-12 22:20:39 +00:00
}
. frame ( maxWidth : 420 )
. frame ( maxWidth : . infinity , alignment : . center )
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
}
. frame ( maxWidth : 420 )
. frame ( maxWidth : . infinity , alignment : . center )
}
}
2026-02-12 22:20:39 +00:00
private var selectedAIModelBinding : Binding < AIModel > {
Binding (
get : { AIModel ( rawValue : selectedAIModelRaw ) ? ? . appleIntelligence } ,
set : { selectedAIModelRaw = $0 . rawValue }
)
}
2026-02-11 10:20:17 +00:00
private var supportTab : some View {
settingsContainer ( maxWidth : 520 ) {
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
}
. disabled ( supportPurchaseManager . isPurchasing || supportPurchaseManager . isLoadingProducts )
2026-02-12 15:55:14 +00:00
Button ( " Restore Purchases " ) {
Task { await supportPurchaseManager . restorePurchases ( ) }
}
. disabled ( supportPurchaseManager . isLoadingProducts )
2026-02-11 10:20:17 +00:00
Button ( " Refresh Price " ) {
Task { await supportPurchaseManager . refreshProducts ( ) }
}
. 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 " )
. frame ( width : isCompactSettingsLayout ? nil : 140 , alignment : . leading )
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-11 10:20:17 +00:00
private func settingsContainer < Content : View > ( maxWidth : CGFloat = 560 , @ ViewBuilder _ content : ( ) -> Content ) -> some View {
ScrollView {
2026-02-13 14:03:43 +00:00
VStack ( alignment : isCompactSettingsLayout ? . leading : . center , spacing : UI . space20 ) {
2026-02-11 10:20:17 +00:00
content ( )
}
. frame ( maxWidth : maxWidth , alignment : . center )
2026-02-12 09:15:40 +00:00
. frame ( maxWidth : . infinity , maxHeight : . infinity , alignment : isCompactSettingsLayout ? . topLeading : . top )
2026-02-13 14:03:43 +00:00
. padding ( . top , UI . topPadding )
. padding ( . bottom , UI . bottomPadding )
. padding ( . horizontal , isCompactSettingsLayout ? UI . sidePaddingCompact : UI . sidePaddingRegular )
2026-02-11 10:20:17 +00:00
}
. background ( . ultraThinMaterial )
}
private func colorRow ( title : String , color : Binding < Color > ) -> some View {
HStack {
Text ( title )
2026-02-12 09:15:40 +00:00
. frame ( width : isCompactSettingsLayout ? nil : 120 , 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 )
. frame ( width : 120 , alignment : . leading )
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
. frame ( width : 200 )
. 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
}
}
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 ) }
)
}
}
#if os ( macOS )
final class FontPickerController : NSObject , NSFontChanging {
private var currentFont : NSFont = NSFont . monospacedSystemFont ( ofSize : 14 , weight : . regular )
var onChange : ( ( NSFont ) -> Void ) ?
func open ( currentName : String , size : Double ) {
let base = NSFont ( name : currentName , size : CGFloat ( size ) ) ? ? NSFont . monospacedSystemFont ( ofSize : CGFloat ( size ) , weight : . regular )
currentFont = base
let manager = NSFontManager . shared
manager . target = self
manager . action = #selector ( changeFont ( _ : ) )
manager . setSelectedFont ( base , isMultiple : false )
NSFontPanel . shared . orderFront ( nil )
}
@objc func changeFont ( _ sender : NSFontManager ? ) {
let manager = sender ? ? NSFontManager . shared
let converted = manager . convert ( currentFont )
currentFont = converted
onChange ? ( converted )
}
}
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 {
window . setContentSize ( NSSize ( width : targetWidth , height : targetHeight ) )
}
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