Browse Source

feat: smart per-app window state memory with default full-screen and manual desktop override persistence

main
Ali Alavi 2 hours ago
parent
commit
1190abb698
  1. 16
      src/StatusBarController.swift
  2. 87
      src/VoiceCommandExecutor.swift
  3. 101
      src/WindowStateManager.swift

16
src/StatusBarController.swift

@ -222,6 +222,12 @@ public final class StatusBarController: NSObject, NSMenuDelegate {
clearLogItem.target = self clearLogItem.target = self
advancedMenu.addItem(clearLogItem) advancedMenu.addItem(clearLogItem)
advancedMenu.addItem(NSMenuItem.separator())
let resetWindowMemItem = NSMenuItem(title: "🖥️ Reset Window Memory (Default Full-Screen)", action: #selector(resetWindowMemoryAction), keyEquivalent: "")
resetWindowMemItem.target = self
advancedMenu.addItem(resetWindowMemItem)
let advancedMenuItem = NSMenuItem(title: "🛠️ Advanced Options", action: nil, keyEquivalent: "") let advancedMenuItem = NSMenuItem(title: "🛠️ Advanced Options", action: nil, keyEquivalent: "")
advancedMenuItem.submenu = advancedMenu advancedMenuItem.submenu = advancedMenu
menu.addItem(advancedMenuItem) menu.addItem(advancedMenuItem)
@ -509,6 +515,16 @@ public final class StatusBarController: NSObject, NSMenuDelegate {
alert.addButton(withTitle: "OK") alert.addButton(withTitle: "OK")
alert.runModal() alert.runModal()
} }
@objc private func resetWindowMemoryAction() {
WindowStateManager.shared.resetAllPreferences()
let alert = NSAlert()
alert.messageText = "Window Memory Reset"
alert.informativeText = "All apps will now open in Full Screen by default unless manually exited."
alert.alertStyle = .informational
alert.addButton(withTitle: "OK")
alert.runModal()
}
@objc private func toggleLaunchAtLoginAction(_ sender: NSMenuItem) { @objc private func toggleLaunchAtLoginAction(_ sender: NSMenuItem) {
SonioxSettings.shared.launchAtLogin.toggle() SonioxSettings.shared.launchAtLogin.toggle()

87
src/VoiceCommandExecutor.swift

@ -558,9 +558,22 @@ public final class VoiceCommandExecutor {
} }
if normalized.contains("تمام صفحه") || normalized.contains("فول اسکرین") || normalized.contains("ماکزیمایز") || normalized == "maximize" || normalized == "full screen" { if normalized.contains("تمام صفحه") || normalized.contains("فول اسکرین") || normalized.contains("ماکزیمایز") || normalized == "maximize" || normalized == "full screen" {
tileFrontmostWindow(action: .maximize) tileFrontmostWindow(action: .maximize)
if let front = NSWorkspace.shared.frontmostApplication {
if let bid = front.bundleIdentifier { WindowStateManager.shared.setPrefersFullScreen(true, for: bid) }
if let name = front.localizedName { WindowStateManager.shared.setPrefersFullScreen(true, for: name) }
}
HistoryManager.shared.addEntry("⚡️ [Window] Maximize Window") HistoryManager.shared.addEntry("⚡️ [Window] Maximize Window")
return "Maximize Window" return "Maximize Window"
} }
if normalized.contains("خروج از تمام صفحه") || normalized.contains("پنجره عادی") || normalized == "دسکتاپ" || normalized == "پنجره" || normalized == "exit full screen" || normalized == "restore window" {
exitFrontmostWindowFullScreen()
if let front = NSWorkspace.shared.frontmostApplication {
if let bid = front.bundleIdentifier { WindowStateManager.shared.setPrefersFullScreen(false, for: bid) }
if let name = front.localizedName { WindowStateManager.shared.setPrefersFullScreen(false, for: name) }
}
HistoryManager.shared.addEntry("⚡️ [Window] Exit Full Screen (Desktop Mode)")
return "Exit Full Screen"
}
if normalized == "وسط" || normalized == "مرکز" || normalized.contains("پنجره وسط") || normalized == "center" || normalized == "window center" { if normalized == "وسط" || normalized == "مرکز" || normalized.contains("پنجره وسط") || normalized == "center" || normalized == "window center" {
tileFrontmostWindow(action: .center) tileFrontmostWindow(action: .center)
HistoryManager.shared.addEntry("⚡️ [Window] Center Window") HistoryManager.shared.addEntry("⚡️ [Window] Center Window")
@ -1046,16 +1059,25 @@ public final class VoiceCommandExecutor {
private func launchApp(at url: URL, name: String) { private func launchApp(at url: URL, name: String) {
let ws = NSWorkspace.shared let ws = NSWorkspace.shared
let shouldBeFullScreen = WindowStateManager.shared.prefersFullScreen(for: name)
if let running = ws.runningApplications.first(where: { $0.bundleURL == url }) { if let running = ws.runningApplications.first(where: { $0.bundleURL == url }) {
running.activate(options: [.activateIgnoringOtherApps]) running.activate(options: [.activateIgnoringOtherApps])
ensureWindowFullScreen(app: running)
if shouldBeFullScreen {
ensureWindowFullScreen(app: running)
}
} else { } else {
let conf = NSWorkspace.OpenConfiguration() let conf = NSWorkspace.OpenConfiguration()
conf.activates = true conf.activates = true
ws.openApplication(at: url, configuration: conf) { app, _ in ws.openApplication(at: url, configuration: conf) { app, _ in
if let launched = app { if let launched = app {
DispatchQueue.main.async { DispatchQueue.main.async {
self.pollAndMakeFullScreen(app: launched, targetBid: launched.bundleIdentifier, appName: name)
self.pollAndManageWindow(
app: launched,
targetBid: launched.bundleIdentifier,
appName: name,
shouldBeFullScreen: shouldBeFullScreen
)
} }
} }
} }
@ -1129,6 +1151,8 @@ public final class VoiceCommandExecutor {
] ]
let targetBid = bundleIDMap[appName] let targetBid = bundleIDMap[appName]
let appPrefKey = targetBid ?? appName
let shouldBeFullScreen = WindowStateManager.shared.prefersFullScreen(for: appPrefKey)
// Check if currently running // Check if currently running
let runningApp = ws.runningApplications.first(where: { let runningApp = ws.runningApplications.first(where: {
@ -1137,9 +1161,11 @@ public final class VoiceCommandExecutor {
}) })
if let running = runningApp { if let running = runningApp {
// Instant path: if the app is ALREADY frontmost (e.g. user is in Telegram switching accounts)
// Instant path: if the app is ALREADY frontmost
if running.isActive { if running.isActive {
ensureWindowFullScreen(app: running)
if shouldBeFullScreen {
ensureWindowFullScreen(app: running)
}
completion?() completion?()
return return
} }
@ -1161,7 +1187,9 @@ public final class VoiceCommandExecutor {
try? task.run() try? task.run()
} }
ensureWindowFullScreen(app: running)
if shouldBeFullScreen {
ensureWindowFullScreen(app: running)
}
if let comp = completion { if let comp = completion {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.03) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.03) {
@ -1179,7 +1207,7 @@ public final class VoiceCommandExecutor {
try? task.run() try? task.run()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.08) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.08) {
self.pollAndMakeFullScreen(app: running, targetBid: targetBid, appName: appName, completion: completion)
self.pollAndManageWindow(app: running, targetBid: targetBid, appName: appName, shouldBeFullScreen: shouldBeFullScreen, completion: completion)
} }
return return
} }
@ -1191,7 +1219,7 @@ public final class VoiceCommandExecutor {
conf.activates = true conf.activates = true
ws.openApplication(at: appURL, configuration: conf) { app, _ in ws.openApplication(at: appURL, configuration: conf) { app, _ in
DispatchQueue.main.async { DispatchQueue.main.async {
self.pollAndMakeFullScreen(app: app, targetBid: bid, appName: appName, completion: completion)
self.pollAndManageWindow(app: app, targetBid: bid, appName: appName, shouldBeFullScreen: shouldBeFullScreen, completion: completion)
} }
} }
return return
@ -1204,7 +1232,7 @@ public final class VoiceCommandExecutor {
try? task.run() try? task.run()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.pollAndMakeFullScreen(app: nil, targetBid: targetBid, appName: appName, completion: completion)
self.pollAndManageWindow(app: nil, targetBid: targetBid, appName: appName, shouldBeFullScreen: shouldBeFullScreen, completion: completion)
} }
} }
@ -1259,10 +1287,33 @@ public final class VoiceCommandExecutor {
return true return true
} }
private func pollAndMakeFullScreen(
private func exitFrontmostWindowFullScreen() {
guard let front = NSWorkspace.shared.frontmostApplication else { return }
let appElem = AXUIElementCreateApplication(front.processIdentifier)
var windowObj: CFTypeRef?
let hasWin = (AXUIElementCopyAttributeValue(appElem, kAXFocusedWindowAttribute as CFString, &windowObj) == .success && windowObj != nil) ||
(AXUIElementCopyAttributeValue(appElem, kAXMainWindowAttribute as CFString, &windowObj) == .success && windowObj != nil)
if hasWin, let winElem = windowObj {
let axWin = winElem as! AXUIElement
var isFullObj: CFTypeRef?
if AXUIElementCopyAttributeValue(axWin, "AXFullScreen" as CFString, &isFullObj) == .success,
let isFull = isFullObj as? Bool, isFull {
let res = AXUIElementSetAttributeValue(axWin, "AXFullScreen" as CFString, kCFBooleanFalse)
if res != .success {
pressHotkey(keyCode: 3, modifiers: [.maskCommand, .maskControl])
}
return
}
}
pressHotkey(keyCode: 3, modifiers: [.maskCommand, .maskControl])
}
private func pollAndManageWindow(
app: NSRunningApplication?, app: NSRunningApplication?,
targetBid: String?, targetBid: String?,
appName: String, appName: String,
shouldBeFullScreen: Bool,
attempt: Int = 1, attempt: Int = 1,
completion: (() -> Void)? = nil completion: (() -> Void)? = nil
) { ) {
@ -1274,8 +1325,17 @@ public final class VoiceCommandExecutor {
if let target = target { if let target = target {
target.activate(options: [.activateIgnoringOtherApps]) target.activate(options: [.activateIgnoringOtherApps])
if ensureWindowFullScreen(app: target) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
if shouldBeFullScreen {
if ensureWindowFullScreen(app: target) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
target.activate(options: [.activateIgnoringOtherApps])
completion?()
}
return
}
} else {
// Desktop mode preferred
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
target.activate(options: [.activateIgnoringOtherApps]) target.activate(options: [.activateIgnoringOtherApps])
completion?() completion?()
} }
@ -1284,11 +1344,12 @@ public final class VoiceCommandExecutor {
} }
if attempt < 18 { if attempt < 18 {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.pollAndMakeFullScreen(
DispatchQueue.main.asyncAfter(deadline: .now() + 0.08) {
self.pollAndManageWindow(
app: target, app: target,
targetBid: targetBid, targetBid: targetBid,
appName: appName, appName: appName,
shouldBeFullScreen: shouldBeFullScreen,
attempt: attempt + 1, attempt: attempt + 1,
completion: completion completion: completion
) )

101
src/WindowStateManager.swift

@ -0,0 +1,101 @@
import Cocoa
import ApplicationServices
public final class WindowStateManager {
public static let shared = WindowStateManager()
private let defaults = UserDefaults.standard
private let prefKey = "SonioxAppFullScreenPreferences"
private var observer: NSObjectProtocol?
private init() {
startObservingAppSwitches()
}
deinit {
if let obs = observer {
NSWorkspace.shared.notificationCenter.removeObserver(obs)
}
}
// MARK: - Preference Query
/// Returns true by default unless the user has explicitly exited full screen for this app.
public func prefersFullScreen(for identifier: String) -> Bool {
let prefs = defaults.dictionary(forKey: prefKey) as? [String: Bool] ?? [:]
let cleanId = identifier.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
return prefs[cleanId] ?? true // DEFAULT IS FULL SCREEN!
}
public func setPrefersFullScreen(_ fullScreen: Bool, for identifier: String) {
let cleanId = identifier.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
guard !cleanId.isEmpty else { return }
var prefs = defaults.dictionary(forKey: prefKey) as? [String: Bool] ?? [:]
prefs[cleanId] = fullScreen
defaults.set(prefs, forKey: prefKey)
print("WindowStateManager: Saved full screen preference for '\(cleanId)': \(fullScreen)")
}
public func resetAllPreferences() {
defaults.removeObject(forKey: prefKey)
print("WindowStateManager: Reset all app window preferences to default (Full Screen).")
}
// MARK: - Automatic Learning via Deactivation
private func startObservingAppSwitches() {
let center = NSWorkspace.shared.notificationCenter
observer = center.addObserver(
forName: NSWorkspace.didDeactivateApplicationNotification,
object: nil,
queue: .main
) { [weak self] notif in
guard let self = self,
let app = notif.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication,
app.activationPolicy == .regular else {
return
}
self.captureCurrentWindowState(app: app)
}
}
public func captureCurrentWindowState(app: NSRunningApplication) {
guard let bid = app.bundleIdentifier ?? app.localizedName, !bid.isEmpty else { return }
if let isFull = isWindowFullScreen(app: app) {
setPrefersFullScreen(isFull, for: bid)
if let name = app.localizedName {
setPrefersFullScreen(isFull, for: name)
}
}
}
// MARK: - Window Inspection Helper
public func isWindowFullScreen(app: NSRunningApplication) -> Bool? {
let appElem = AXUIElementCreateApplication(app.processIdentifier)
var windowObj: CFTypeRef?
let hasWin = (AXUIElementCopyAttributeValue(appElem, kAXFocusedWindowAttribute as CFString, &windowObj) == .success && windowObj != nil) ||
(AXUIElementCopyAttributeValue(appElem, kAXMainWindowAttribute as CFString, &windowObj) == .success && windowObj != nil)
if hasWin, let winElem = windowObj {
var fsObj: CFTypeRef?
if AXUIElementCopyAttributeValue(winElem as! AXUIElement, "AXFullScreen" as CFString, &fsObj) == .success,
let isFull = fsObj as? Bool {
return isFull
}
}
var winListObj: CFTypeRef?
if AXUIElementCopyAttributeValue(appElem, kAXWindowsAttribute as CFString, &winListObj) == .success,
let wins = winListObj as? [AXUIElement], let firstWin = wins.first {
var fsObj: CFTypeRef?
if AXUIElementCopyAttributeValue(firstWin, "AXFullScreen" as CFString, &fsObj) == .success,
let isFull = fsObj as? Bool {
return isFull
}
}
return nil
}
}
Loading…
Cancel
Save