@@ -81,6 +81,12 @@ class LaunchAtLoginManager: ObservableObject {
8181class AppDelegate : NSObject , NSApplicationDelegate {
8282 var keyRemapper : KeyRemapper ?
8383 var statusCheckTimer : Timer ?
84+ var statusItem : NSStatusItem ?
85+
86+ // Debug logging is now optional - only enabled if user explicitly turns it on in Settings.
87+ // This reduces SSD wear and avoids logging sensitive keystroke patterns by default.
88+ @AppStorage ( " bc64keys.debugLogging " ) var debugLoggingEnabled : Bool = false
89+
8490 // Prefer a per-user log location instead of /tmp to reduce information exposure and
8591 // avoid symlink-related file clobbering risks.
8692 private lazy var logFileURL : URL = {
@@ -95,14 +101,20 @@ class AppDelegate: NSObject, NSApplicationDelegate {
95101 let mappingManager = KeyMappingManager ( )
96102
97103 // Writes operational status to both stdout and a per-user log file under ~/Library/Logs.
98- // This avoids /tmp exposure and is a more standard location for macOS app logs .
104+ // File logging is now OPTIONAL (controlled by debugLoggingEnabled toggle) to reduce SSD wear .
99105
100106 func log( _ message: String ) {
101107 let timestamp = DateFormatter . localizedString ( from: Date ( ) , dateStyle: . none, timeStyle: . medium)
102108 let logMessage = " [ \( timestamp) ] \( message) \n "
109+
110+ // Always print to console for development/debugging
111+ #if DEBUG
103112 print ( logMessage, terminator: " " )
113+ #endif
114+
115+ // Only write to file if debug logging is explicitly enabled by user
116+ guard debugLoggingEnabled else { return }
104117
105- // Also write to file
106118 if let data = logMessage. data ( using: . utf8) {
107119 let path = logFileURL. path
108120 if FileManager . default. fileExists ( atPath: path) {
@@ -118,14 +130,21 @@ class AppDelegate: NSObject, NSApplicationDelegate {
118130 }
119131
120132 func applicationDidFinishLaunching( _ notification: Notification ) {
121- // Clear old log
122- try ? FileManager . default. removeItem ( at: logFileURL)
133+ // Clear old log (only if debug logging is enabled)
134+ if debugLoggingEnabled {
135+ try ? FileManager . default. removeItem ( at: logFileURL)
136+ }
123137
124138 log ( " ================================================== " )
125139 log ( " 🚀 BC64Keys App Started! " )
126- log ( " 📝 Status log: \( logFileURL. path) " )
140+ if debugLoggingEnabled {
141+ log ( " 📝 Status log: \( logFileURL. path) " )
142+ }
127143 log ( " ================================================== " )
128144
145+ // Setup menu bar icon (like Karabiner Elements)
146+ setupMenuBar ( )
147+
129148 // Start periodic status check (1 second interval).
130149 // Reason: Accessibility permission can be granted/revoked while the app is running.
131150 statusCheckTimer = Timer . scheduledTimer ( withTimeInterval: 1.0 , repeats: true ) { [ weak self] _ in
@@ -136,11 +155,109 @@ class AppDelegate: NSObject, NSApplicationDelegate {
136155 checkAndReportStatus ( )
137156 }
138157
158+ // MARK: - Menu Bar Icon (like Karabiner Elements)
159+ // Creates a status bar icon that shows remapper state and allows quick access to the app window.
160+ func setupMenuBar( ) {
161+ statusItem = NSStatusBar . system. statusItem ( withLength: NSStatusItem . squareLength)
162+
163+ if let button = statusItem? . button {
164+ // Create simple square icon with "B" letter
165+ let icon = createMenuBarIcon ( )
166+ icon. isTemplate = true // Makes it white in menu bar
167+ button. image = icon
168+ button. action = #selector( statusItemClicked)
169+ button. target = self
170+ }
171+
172+ updateMenuBarIcon ( )
173+ }
174+
175+ // Create custom menu bar icon - simple square with "B" letter
176+ private func createMenuBarIcon( ) -> NSImage {
177+ let size = NSSize ( width: 18 , height: 18 )
178+ let image = NSImage ( size: size)
179+
180+ image. lockFocus ( )
181+
182+ // Draw rounded square background
183+ let rect = NSRect ( x: 2 , y: 2 , width: 14 , height: 14 )
184+ let path = NSBezierPath ( roundedRect: rect, xRadius: 2 , yRadius: 2 )
185+ NSColor . white. setStroke ( )
186+ path. lineWidth = 1.5
187+ path. stroke ( )
188+
189+ // Draw "B" letter in center
190+ let attrs : [ NSAttributedString . Key : Any ] = [
191+ . font: NSFont . systemFont ( ofSize: 11 , weight: . semibold) ,
192+ . foregroundColor: NSColor . white
193+ ]
194+ let letter = " B "
195+ let letterSize = letter. size ( withAttributes: attrs)
196+ let letterRect = NSRect (
197+ x: ( size. width - letterSize. width) / 2 ,
198+ y: ( size. height - letterSize. height) / 2 - 1 ,
199+ width: letterSize. width,
200+ height: letterSize. height
201+ )
202+ letter. draw ( in: letterRect, withAttributes: attrs)
203+
204+ image. unlockFocus ( )
205+ return image
206+ }
207+
208+ // Update menu bar icon based on remapper state (no color change needed with template)
209+ func updateMenuBarIcon( ) {
210+ // Template icons automatically match menu bar appearance (white/dark)
211+ // Status is shown by the icon itself being present
212+ }
213+
214+ // When status item is clicked, bring the app window to foreground
215+ // Simplified approach that works consistently with SwiftUI WindowGroup
216+ @objc func statusItemClicked( ) {
217+ // Activate app first
218+ NSApp . activate ( ignoringOtherApps: true )
219+
220+ // Try to show ALL windows (brute force approach - most reliable)
221+ var didShowWindow = false
222+ for window in NSApp . windows {
223+ // Handle minimized windows
224+ if window. isMiniaturized {
225+ window. deminiaturize ( nil )
226+ didShowWindow = true
227+ }
228+
229+ // Bring all non-panel windows to front
230+ if !window. className. contains ( " Panel " ) && !window. className. contains ( " Alert " ) {
231+ window. makeKeyAndOrderFront ( nil )
232+ window. orderFrontRegardless ( )
233+ didShowWindow = true
234+ }
235+ }
236+
237+ // If no windows were shown, the user closed them all
238+ // Force app to recreate main window by toggling activation policy
239+ if !didShowWindow || NSApp . windows. isEmpty {
240+ NSApp . unhide ( nil )
241+ NSApp . setActivationPolicy ( . regular)
242+
243+ // Give SwiftUI time to recreate the window
244+ DispatchQueue . main. asyncAfter ( deadline: . now( ) + 0.15 ) {
245+ NSApp . activate ( ignoringOtherApps: true )
246+ // Show any new windows that appeared
247+ for window in NSApp . windows {
248+ window. makeKeyAndOrderFront ( nil )
249+ window. orderFrontRegardless ( )
250+ }
251+ }
252+ }
253+ }
254+
139255 func checkAndReportStatus( ) {
140256 let hasAccessibility = AXIsProcessTrusted ( )
141257
142- // Update UI
258+ // Update UI and menu bar icon
143259 statusManager. update ( accessibility: hasAccessibility, remapperRunning: keyRemapper != nil )
260+ updateMenuBarIcon ( )
144261
145262 log ( " " )
146263 log ( " ⏰ STATUS CHECK: " )
@@ -676,6 +793,7 @@ struct SettingsView: View {
676793 @ObservedObject var l10n = L10n . shared
677794 @StateObject private var launchManager = LaunchAtLoginManager ( )
678795 @AppStorage ( " selectedLanguage " ) private var selectedLanguageRaw : String = AppLanguage . system. rawValue
796+ @AppStorage ( " bc64keys.debugLogging " ) private var debugLoggingEnabled : Bool = false
679797
680798 private var selectedLanguage : AppLanguage {
681799 get { AppLanguage ( rawValue: selectedLanguageRaw) ?? . system }
@@ -687,123 +805,101 @@ struct SettingsView: View {
687805 // Header
688806 HStack {
689807 Text ( L10n . current. settingsTitle)
690- . font ( . title )
808+ . font ( . title2 )
691809 . fontWeight ( . bold)
692810
693811 Spacer ( )
694812 }
695- . padding ( )
813+ . padding ( . horizontal, 16 )
814+ . padding ( . vertical, 12 )
696815 . background ( Color ( NSColor . controlBackgroundColor) )
697816
698817 Divider ( )
699818
700- // Settings Content
701- ScrollView {
702- VStack ( alignment: . leading, spacing: 24 ) {
703- // Launch at Login Section
704- VStack ( alignment: . leading, spacing: 12 ) {
705- Label ( L10n . current. launchAtLogin, systemImage: " power " )
706- . font ( . headline)
707-
708- Toggle ( isOn: Binding (
709- get: { launchManager. isEnabled } ,
710- set: { _ in launchManager. toggle ( ) }
711- ) ) {
712- Text ( L10n . current. launchAtLoginDescription)
713- }
714- . toggleStyle ( . switch)
715-
716- Text ( L10n . current. launchAtLoginHint)
717- . font ( . caption)
718- . foregroundColor ( . secondary)
719- }
720- . padding ( )
721- . background ( Color ( NSColor . controlBackgroundColor) )
722- . cornerRadius ( 12 )
819+ // Settings Content - Compact but readable
820+ VStack ( alignment: . leading, spacing: 16 ) {
821+ // Launch at Login + Language - Single row
822+ HStack ( spacing: 20 ) {
823+ // Launch at Login
824+ Label ( L10n . current. launchAtLogin, systemImage: " power " )
825+ . font ( . headline)
723826
724- // Language Section
725- VStack ( alignment: . leading, spacing: 12 ) {
726- Label ( L10n . current. language, systemImage: " globe " )
727- . font ( . headline)
728-
729- Picker ( " " , selection: Binding (
730- get: { selectedLanguage } ,
731- set: { newValue in
732- selectedLanguageRaw = newValue. rawValue
733- L10n . shared. setLanguage ( newValue)
734- }
735- ) ) {
736- ForEach ( AppLanguage . allCases, id: \. self) { lang in
737- Text ( lang. displayName) . tag ( lang)
738- }
827+ Toggle ( " " , isOn: Binding (
828+ get: { launchManager. isEnabled } ,
829+ set: { _ in launchManager. toggle ( ) }
830+ ) )
831+ . toggleStyle ( . switch)
832+ . labelsHidden ( )
833+
834+ Spacer ( )
835+
836+ // Language
837+ Label ( L10n . current. language, systemImage: " globe " )
838+ . font ( . headline)
839+
840+ Picker ( " " , selection: Binding (
841+ get: { selectedLanguage } ,
842+ set: { newValue in
843+ selectedLanguageRaw = newValue. rawValue
844+ L10n . shared. setLanguage ( newValue)
845+ }
846+ ) ) {
847+ ForEach ( AppLanguage . allCases, id: \. self) { lang in
848+ Text ( lang. displayName) . tag ( lang)
739849 }
740- . pickerStyle ( . segmented)
741- . frame ( maxWidth: 400 )
742-
743- Text ( L10n . current. languageHint)
744- . font ( . caption)
745- . foregroundColor ( . secondary)
746850 }
747- . padding ( )
748- . background ( Color ( NSColor . controlBackgroundColor) )
749- . cornerRadius ( 12 )
750-
751- // Support Section
752- VStack ( alignment: . leading, spacing: 12 ) {
753- Label ( L10n . current. support, systemImage: " heart.fill " )
754- . font ( . headline)
755- . foregroundColor ( . pink)
756-
757- Text ( L10n . current. supportDescription)
758- . font ( . caption)
759- . foregroundColor ( . secondary)
760-
761- Button ( action: {
762- if let url = URL ( string: " https://buymeacoffee.com/badcode64 " ) {
763- NSWorkspace . shared. open ( url)
764- }
765- } ) {
766- HStack {
767- Image ( systemName: " cup.and.saucer.fill " )
768- Text ( L10n . current. supportButton)
769- }
770- . frame ( maxWidth: . infinity)
771- . padding ( . vertical, 8 )
851+ . pickerStyle ( . menu)
852+ . frame ( width: 150 )
853+ }
854+ . padding ( 12 )
855+ . background ( Color ( NSColor . controlBackgroundColor) )
856+ . cornerRadius ( 8 )
857+
858+ // Support Section - Centered, prominent, no extra text
859+ VStack ( spacing: 12 ) {
860+ Button ( action: {
861+ if let url = URL ( string: " https://buymeacoffee.com/badcode64 " ) {
862+ NSWorkspace . shared. open ( url)
772863 }
773- . buttonStyle ( . borderedProminent)
774- . tint ( . pink)
864+ } ) {
865+ HStack ( spacing: 8 ) {
866+ Image ( systemName: " cup.and.saucer.fill " )
867+ Text ( L10n . current. supportButton)
868+ }
869+ . font ( . body)
870+ . frame ( maxWidth: 300 )
871+ . padding ( . vertical, 10 )
775872 }
776- . padding ( )
777- . background ( Color ( NSColor . controlBackgroundColor) )
778- . cornerRadius ( 12 )
873+ . buttonStyle ( . borderedProminent)
874+ . tint ( . pink)
875+ }
876+ . frame ( maxWidth: . infinity)
877+ . padding ( 12 )
878+ . background ( Color ( NSColor . controlBackgroundColor) )
879+ . cornerRadius ( 8 )
880+
881+ Spacer ( )
882+
883+ // Debug Logging Section - At the bottom (rarely used)
884+ VStack ( alignment: . leading, spacing: 8 ) {
885+ Label ( L10n . current. debugLogging, systemImage: " ladybug.fill " )
886+ . font ( . headline)
887+ . foregroundColor ( . orange)
779888
780- // About Section
781- VStack ( alignment: . leading, spacing: 12 ) {
782- Label ( L10n . current. about, systemImage: " info.circle " )
783- . font ( . headline)
784-
785- VStack ( alignment: . leading, spacing: 8 ) {
786- HStack {
787- Text ( " BC64Keys " )
788- . fontWeight ( . semibold)
789- Spacer ( )
790- Text ( " v1.0 " )
791- . foregroundColor ( . secondary)
792- }
793-
794- Text ( L10n . current. aboutDescription)
795- . font ( . caption)
796- . foregroundColor ( . secondary)
797- }
889+ Toggle ( isOn: $debugLoggingEnabled) {
890+ Text ( L10n . current. debugLoggingDescription)
798891 }
799- . padding ( )
800- . background ( Color ( NSColor . controlBackgroundColor) )
801- . cornerRadius ( 12 )
892+ . toggleStyle ( . switch)
802893
803- Spacer ( )
894+ Text ( L10n . current. debugLoggingHint)
895+ . font ( . caption)
896+ . foregroundColor ( . secondary)
804897 }
805- . padding ( )
898+ . padding ( 12 )
899+ . background ( Color ( NSColor . controlBackgroundColor) )
900+ . cornerRadius ( 8 )
806901 }
902+ . padding ( 16 )
807903 }
808904 }
809905}
0 commit comments