diff --git a/.gitignore b/.gitignore index 53eea1f..0e1b3d6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .DS_Store /*.app /*.tar.gz +*.xcodeproj diff --git a/README.md b/README.md index 474b4cb..8dad794 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ [![macOS 10.10+](https://img.shields.io/badge/macOS-10.10+-888)](#install) [![Current release](https://img.shields.io/github/release/relikd/Memmon)](https://github.com/relikd/Memmon/releases) +[![All downloads](https://img.shields.io/github/downloads/relikd/Memmon/total)](https://github.com/relikd/Memmon/releases) @@ -7,7 +8,7 @@ Memmon remembers what your Mac forgets – A simple deamon that restores your window positions on external monitors. -**Limitations:** Currently, Memmon can not restore windows in other spaces, only the currently active space. If you know a way to access the accessibility settings of a different space, let me know. +**Limitations:** Currently, Memmon restores windows in other spaces only if the space is activated. If you know a way to access the accessibility settings of a different space, let me know. ## Install @@ -53,7 +54,7 @@ Yes, for example [Mjolnir](https://github.com/mjolnirapp/mjolnir) or [Hammerspoo ### What is it good for? -First off, Memmon is just 140 lines of code – no dependencies. You can audit it in 5 minutes and build it from scratch – just run `make`. +First off, Memmon is less than 300 lines of code – no dependencies. You can audit it in 10 minutes... And build it from scratch – just run `make`. Secondly, it does one thing and one thing only: Save and restore window positions whenever your monitor setup changes. diff --git a/src/Info.plist b/src/Info.plist index 8916d53..c298e8b 100644 --- a/src/Info.plist +++ b/src/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.3 + 1.4 CFBundleVersion 42 LSMinimumSystemVersion diff --git a/src/main.swift b/src/main.swift index c9262af..af2acb7 100755 --- a/src/main.swift +++ b/src/main.swift @@ -13,8 +13,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { private var numScreens: Int = NSScreen.screens.count private var state: [Int: WinConf] = [:] // [screencount: [pid: [windows]]] - private var time: Date = Date.distantPast private var spacesAll: [SpaceId] = [] // keep forever (and keep order) + private var spacesVisited: Set = [] // fill-up on space-switch private var spacesNeedRestore: Set = [] // dropped after restore func applicationDidFinishLaunching(_ aNotification: Notification) { @@ -23,6 +23,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { // track space changes NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(self.activeSpaceChanged), name: NSWorkspace.activeSpaceDidChangeNotification, object: nil) _ = self.currentSpace() // create space-id win for current space + self.spacesVisited = Set(self.getWinIds()) // create status menu icon UserDefaults.standard.register(defaults: ["icon": 2]) let icon = UserDefaults.standard.integer(forKey: "icon") @@ -36,7 +37,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } self.statusItem.menu = NSMenu(title: "") - self.statusItem.menu!.addItem(withTitle: "Memmon (v1.3)", action: nil, keyEquivalent: "") + self.statusItem.menu!.addItem(withTitle: "Memmon (v1.4)", action: nil, keyEquivalent: "") self.statusItem.menu!.addItem(withTitle: "Hide Status Icon", action: #selector(self.enableInvisbleMode), keyEquivalent: "") self.statusItem.menu!.addItem(withTitle: "Quit", action: #selector(NSApp.terminate), keyEquivalent: "q") } @@ -46,9 +47,10 @@ class AppDelegate: NSObject, NSApplicationDelegate { } func applicationDidChangeScreenParameters(_ notification: Notification) { - if numScreens != NSScreen.screens.count { + if self.numScreens != NSScreen.screens.count { self.saveState() - numScreens = NSScreen.screens.count + self.numScreens = NSScreen.screens.count + self.spacesVisited.removeAll(keepingCapacity: true) self.restoreState() } } @@ -61,31 +63,36 @@ class AppDelegate: NSObject, NSApplicationDelegate { private func saveState() { self.spacesNeedRestore = Set(self.spacesAll) - guard self.time.timeIntervalSinceNow < -5 else { - // Last save is less than 5 sec ago. - // Do not override a (probably) still correct state. - // Otherwise a monitor flicker will forget win-positions in other spaces. - return + if self.state[self.numScreens] == nil { + self.state[self.numScreens] = [:] // otherwise state.keys wont run } let newState = self.getState() - self.state[numScreens] = newState - // update existing let dummy: WinPos = (0, CGRect.zero) for kNum in self.state.keys { - if kNum == numScreens { continue } // current state, already set above + let isCurrent = kNum == self.numScreens var tmp_state: WinConf = [:] - for (n_app, new_val) in newState { - if let old_val = self.state[kNum]![n_app] { - tmp_state[n_app] = [] - for (n_win, _) in new_val { - let old_pos = old_val.first { $0.0 == n_win } - tmp_state[n_app]!.append(old_pos ?? dummy) + for (n_app, n_windows) in newState { + if let old_windows = self.state[kNum]![n_app] { + var win_arr: [WinPos] = [] + for n_win in n_windows { + // In theory, every space that was visited, was also restored. + // If not visited (and not restored) then windows may still appear minimized, + // so we rather copy the old value, assuming windows weren't moved while in an unvisited space. + if isCurrent && self.spacesVisited.contains(n_win.0) { + win_arr.append(n_win) + } else { + // caution! the positions of all other states are updated as well. + let old_win = old_windows.first { $0.0 == n_win.0 } + win_arr.append(old_win ?? dummy) + } } + tmp_state[n_app] = win_arr + } else if isCurrent { // and not saved yet + tmp_state[n_app] = n_windows // TODO: or only add if visited? } } self.state[kNum] = tmp_state } - self.time = Date(timeIntervalSinceNow: 0) } private func getState() -> WinConf { @@ -125,7 +132,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { if let space = currentSpace(), self.spacesNeedRestore.contains(space) { self.spacesNeedRestore.remove(space) let spaceWinNums = self.getWinIds() - for (pid, bounds) in self.state[numScreens] ?? [:] { + self.spacesVisited.formUnion(spaceWinNums) + for (pid, bounds) in self.state[self.numScreens] ?? [:] { self.setWindowSizes(pid, bounds.filter{ spaceWinNums.contains($0.0) }) } }