Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
906f45b156 | ||
|
|
a8dfe6fb15 | ||
|
|
dd84561293 | ||
|
|
d2eac0f0d2 | ||
|
|
f94a56ec58 | ||
|
|
60d2a15f0f | ||
|
|
d26169f8b9 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
/*.app
|
/*.app
|
||||||
/*.tar.gz
|
/*.tar.gz
|
||||||
|
*.xcodeproj
|
||||||
|
|||||||
28
Makefile
Executable file → Normal file
28
Makefile
Executable file → Normal file
@@ -1,35 +1,51 @@
|
|||||||
# usage: make [CONFIG=debug|release]
|
# usage: make [CONFIG=debug|release]
|
||||||
|
|
||||||
ifeq ($(CONFIG), debug)
|
ifeq ($(CONFIG), debug)
|
||||||
CFLAGS=-Onone -g
|
CFLAGS=-Onone -g
|
||||||
else
|
else
|
||||||
CFLAGS=-O
|
CFLAGS=-O
|
||||||
endif
|
endif
|
||||||
|
|
||||||
PLIST=$(shell grep -A1 $(1) src/Info.plist | tail -1 | cut -d'>' -f2 | cut -d'<' -f1)
|
PLIST=$(shell grep -A1 $(1) src/Info.plist | tail -1 | cut -d'>' -f2 | cut -d'<' -f1)
|
||||||
|
HAS_SIGN_IDENTITY=$(shell security find-identity -v -p codesigning | grep -q "Apple Development" && echo 1 || echo 0)
|
||||||
|
|
||||||
|
|
||||||
Memmon.app: SDK_PATH=$(shell xcrun --show-sdk-path --sdk macosx)
|
Memmon.app: SDK_PATH=$(shell xcrun --show-sdk-path --sdk macosx)
|
||||||
Memmon.app: src/*
|
Memmon.app: src/*
|
||||||
@mkdir -p Memmon.app/Contents/MacOS/
|
@mkdir -p Memmon.app/Contents/MacOS/
|
||||||
swiftc ${CFLAGS} src/main.swift \
|
swiftc ${CFLAGS} src/main.swift -target x86_64-apple-macos10.10 \
|
||||||
-target arm64-apple-macos10.10 -target x86_64-apple-macos10.10 \
|
-emit-executable -sdk ${SDK_PATH} -o bin_x64
|
||||||
-emit-executable -sdk ${SDK_PATH} -o Memmon.app/Contents/MacOS/Memmon
|
swiftc ${CFLAGS} src/main.swift -target arm64-apple-macos10.10 \
|
||||||
|
-emit-executable -sdk ${SDK_PATH} -o bin_arm64
|
||||||
|
lipo -create bin_x64 bin_arm64 -o Memmon.app/Contents/MacOS/Memmon
|
||||||
|
@rm bin_x64 bin_arm64
|
||||||
@echo 'APPL????' > Memmon.app/Contents/PkgInfo
|
@echo 'APPL????' > Memmon.app/Contents/PkgInfo
|
||||||
@mkdir -p Memmon.app/Contents/Resources/
|
@mkdir -p Memmon.app/Contents/Resources/
|
||||||
@cp src/AppIcon.icns Memmon.app/Contents/Resources/AppIcon.icns
|
@cp src/AppIcon.icns Memmon.app/Contents/Resources/AppIcon.icns
|
||||||
@cp src/Info.plist Memmon.app/Contents/Info.plist
|
@cp src/Info.plist Memmon.app/Contents/Info.plist
|
||||||
@touch Memmon.app
|
@touch Memmon.app
|
||||||
@echo
|
@echo
|
||||||
|
ifeq ($(HAS_SIGN_IDENTITY),1)
|
||||||
codesign -v -s 'Apple Development' --options=runtime --timestamp Memmon.app
|
codesign -v -s 'Apple Development' --options=runtime --timestamp Memmon.app
|
||||||
|
else
|
||||||
|
codesign -v -s - Memmon.app
|
||||||
|
endif
|
||||||
@echo
|
@echo
|
||||||
@echo 'Verify Signature...'
|
@echo 'Verify Signature...'
|
||||||
@echo
|
@echo
|
||||||
codesign -dvv Memmon.app
|
codesign -dvv Memmon.app
|
||||||
@echo
|
@echo
|
||||||
codesign -vvv --deep --strict Memmon.app
|
codesign -vvv --deep --strict Memmon.app
|
||||||
|
ifeq ($(HAS_SIGN_IDENTITY),1)
|
||||||
@echo
|
@echo
|
||||||
spctl -vvv --assess --type exec Memmon.app
|
-spctl -vvv --assess --type exec Memmon.app
|
||||||
|
endif
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
|
clean:
|
||||||
|
rm -rf Memmon.app bin_x64 bin_arm64
|
||||||
|
|
||||||
|
|
||||||
.PHONY: release
|
.PHONY: release
|
||||||
release: VERSION=$(call PLIST,CFBundleShortVersionString)
|
release: VERSION=$(call PLIST,CFBundleShortVersionString)
|
||||||
|
|||||||
56
README.md
56
README.md
@@ -1,5 +1,6 @@
|
|||||||
[](#install)
|
[](#install)
|
||||||
[](https://github.com/relikd/Memmon/releases)
|
[](https://github.com/relikd/Memmon/releases)
|
||||||
|
[](https://github.com/relikd/Memmon/releases)
|
||||||
|
|
||||||
<img src="img/icon.svg" width="180" height="180">
|
<img src="img/icon.svg" width="180" height="180">
|
||||||
|
|
||||||
@@ -7,57 +8,82 @@
|
|||||||
|
|
||||||
Memmon remembers what your Mac forgets – A simple deamon that restores your window positions on external monitors.
|
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.
|
||||||
|
- Support for the Misson Control config option “Displays have separate Spaces” is not tested.
|
||||||
|
I will add support for this as soon as I have access to an external monitor again (issue [#5](https://github.com/relikd/Memmon/issues/5#issuecomment-1040611494)).
|
||||||
|
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
1. You will need macOS 10.10 or newer.
|
1. You will need macOS 10.10 or newer.
|
||||||
2. Grant Memmon the Accessibility privilege. Go to "System Preference" > "Security & Privacy" > "Accessibility" and add Memmon to that list. Otherwise, you can't move other application windows around and the app has no purpose.
|
Download and unzip the tar.gz from [latest release](https://github.com/relikd/Memmon/releases/latest).
|
||||||
3. Thats it. The app runs in your status bar.
|
2. Grant Memmon the Accessibility privilege.
|
||||||
|
Go to "System Preference" > "Security & Privacy" > "Accessibility" and add Memmon to that list.
|
||||||
|
(Otherwise, the app has no purpose as it can't move application windows around.)
|
||||||
|
3. Thats it. The app runs in your menu bar.
|
||||||
|
|
||||||
|
Alternatively, you can compile Memmon from source by running `make`, or call the script directly (`swift src/main.swift`) without building an app bundle.
|
||||||
|
|
||||||
|
|
||||||
### Status Icon
|
### Menu Bar Icon
|
||||||
|
|
||||||
You can hide the status icon either via `defaults` or the same-titled menu entry. If you do so, the only way to quit the app is by killing the process (with Activity.app or `killall Memmon`).
|
You can hide the menu bar icon either via `defaults` or the same-titled menu entry.
|
||||||
|
If you do so, the only way to quit the app is by killing the process (with Activity.app or `killall Memmon`).
|
||||||
|
The menu bar icon stays hidden during this execution only. If you restart the OS or app it will reappear (unless you hide the icon with `defaults`).
|
||||||
|
|
||||||
Memmon has exactly one app-setting, the status icon. You can manipulate the display of the icon, or hide the icon completely:
|
Memmon has exactly one app-setting, the menu bar icon.
|
||||||
|
You can manipulate the display of the icon, or hide the icon completely:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# disable status icon completely
|
# disable menu bar icon completely
|
||||||
defaults write de.relikd.Memmon icon -int 0
|
defaults write de.relikd.Memmon icon -int 0
|
||||||
# Use window-dots-icon
|
# Use window-dots-icon
|
||||||
defaults write de.relikd.Memmon icon -int 1
|
defaults write de.relikd.Memmon icon -int 1
|
||||||
# Use monitor-with-windows icon (default)
|
# Use monitor-with-windows icon (default)
|
||||||
defaults write de.relikd.Memmon icon -int 2
|
defaults write de.relikd.Memmon icon -int 2
|
||||||
# re-enable status icon and use default icon
|
# re-enable menu bar icon and use default icon
|
||||||
defaults delete de.relikd.Memmon icon
|
defaults delete de.relikd.Memmon icon
|
||||||
```
|
```
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
### Why‽
|
### Why‽
|
||||||
|
|
||||||
I am frustrated! Why does my Mac forget all window positions which I moved to a second screen? Every time I unplug the monitor. Every time I close my Macbook lid. Every time I lock my Mac.
|
I am frustrated!
|
||||||
|
Why does my Mac forget all window positions which I moved to a second screen?
|
||||||
|
Every time I unplug the monitor.
|
||||||
|
Every time I close my Macbook lid.
|
||||||
|
Every time I lock my Mac.
|
||||||
|
|
||||||
Is it macOS 11? Is it the USB-C-to-HDMI converter dongle (notably one made by Apple)? Why do I have to fix things that Apple should have fixed long ago? …
|
Is it macOS 11?
|
||||||
|
Is it the USB-C-to-HDMI converter dongle (notably one made by Apple)?
|
||||||
|
Why do I have to fix things that Apple should have fixed long ago? …
|
||||||
|
|
||||||
|
|
||||||
### Aren't there other solutions?
|
### Aren't there other solutions?
|
||||||
|
|
||||||
Yes, for example [Mjolnir](https://github.com/mjolnirapp/mjolnir) or [Hammerspoon](https://github.com/Hammerspoon/hammerspoon) (and some comercial ones). But I do not need a full-fledged window manager. Nor the dependencies they rely on. I just need to fix this damn bug.
|
Yes, for example, you can use [Mjolnir](https://github.com/mjolnirapp/mjolnir) or [Hammerspoon](https://github.com/Hammerspoon/hammerspoon) (and some comercial ones) to restore your perfect window setup on a button press.
|
||||||
|
But I do not need a full-fledged window manager or the dependencies it relies on.
|
||||||
|
Nor do I want to constantly adjust for new windows.
|
||||||
|
Actually, I don't want to think about this problem at all – I just want to fix this damn bug.
|
||||||
|
|
||||||
|
|
||||||
### What is it good for?
|
### 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.
|
Secondly, it does one thing and one thing only:
|
||||||
|
Save and restore window positions whenever your monitor setup changes.
|
||||||
|
|
||||||
|
|
||||||
### Develop
|
### Develop
|
||||||
|
|
||||||
You can either run the `main.swift` file directly with `swift main.swift`, via Terminal `./main.swift` (`chmod 755 main.swift`), or create a new Xcode project. Select the Command-Line template and after creation replace the existing `main.swift` with the bundled one.
|
You can either run the `main.swift` file directly with `swift main.swift`, via Terminal `./main.swift` (`chmod 755 main.swift`), or create a new Xcode project.
|
||||||
|
In Xcode, select the Command-Line template and replace the template provided `main.swift` with this one.
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.3</string>
|
<string>1.5</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>42</string>
|
<string>42</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
private var numScreens: Int = NSScreen.screens.count
|
private var numScreens: Int = NSScreen.screens.count
|
||||||
private var state: [Int: WinConf] = [:] // [screencount: [pid: [windows]]]
|
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 spacesAll: [SpaceId] = [] // keep forever (and keep order)
|
||||||
|
private var spacesVisited: Set<WinNum> = [] // fill-up on space-switch
|
||||||
private var spacesNeedRestore: Set<SpaceId> = [] // dropped after restore
|
private var spacesNeedRestore: Set<SpaceId> = [] // dropped after restore
|
||||||
|
|
||||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||||
@@ -23,6 +23,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
// track space changes
|
// track space changes
|
||||||
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(self.activeSpaceChanged), name: NSWorkspace.activeSpaceDidChangeNotification, object: nil)
|
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(self.activeSpaceChanged), name: NSWorkspace.activeSpaceDidChangeNotification, object: nil)
|
||||||
_ = self.currentSpace() // create space-id win for current space
|
_ = self.currentSpace() // create space-id win for current space
|
||||||
|
self.spacesVisited = Set(self.getWinIds())
|
||||||
// create status menu icon
|
// create status menu icon
|
||||||
UserDefaults.standard.register(defaults: ["icon": 2])
|
UserDefaults.standard.register(defaults: ["icon": 2])
|
||||||
let icon = UserDefaults.standard.integer(forKey: "icon")
|
let icon = UserDefaults.standard.integer(forKey: "icon")
|
||||||
@@ -36,7 +37,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.statusItem.menu = NSMenu(title: "")
|
self.statusItem.menu = NSMenu(title: "")
|
||||||
self.statusItem.menu!.addItem(withTitle: "Memmon (v1.3)", action: nil, keyEquivalent: "")
|
self.statusItem.menu!.addItem(withTitle: "Memmon (v1.5)", action: nil, keyEquivalent: "")
|
||||||
self.statusItem.menu!.addItem(withTitle: "Hide Status Icon", action: #selector(self.enableInvisbleMode), 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")
|
self.statusItem.menu!.addItem(withTitle: "Quit", action: #selector(NSApp.terminate), keyEquivalent: "q")
|
||||||
}
|
}
|
||||||
@@ -46,9 +47,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func applicationDidChangeScreenParameters(_ notification: Notification) {
|
func applicationDidChangeScreenParameters(_ notification: Notification) {
|
||||||
if numScreens != NSScreen.screens.count {
|
if self.numScreens != NSScreen.screens.count {
|
||||||
self.saveState()
|
self.saveState()
|
||||||
numScreens = NSScreen.screens.count
|
self.numScreens = NSScreen.screens.count
|
||||||
|
self.spacesVisited.removeAll(keepingCapacity: true)
|
||||||
self.restoreState()
|
self.restoreState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,31 +63,36 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
|
|
||||||
private func saveState() {
|
private func saveState() {
|
||||||
self.spacesNeedRestore = Set(self.spacesAll)
|
self.spacesNeedRestore = Set(self.spacesAll)
|
||||||
guard self.time.timeIntervalSinceNow < -5 else {
|
if self.state[self.numScreens] == nil {
|
||||||
// Last save is less than 5 sec ago.
|
self.state[self.numScreens] = [:] // otherwise state.keys wont run
|
||||||
// Do not override a (probably) still correct state.
|
|
||||||
// Otherwise a monitor flicker will forget win-positions in other spaces.
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
let newState = self.getState()
|
let newState = self.getState()
|
||||||
self.state[numScreens] = newState
|
|
||||||
// update existing
|
|
||||||
let dummy: WinPos = (0, CGRect.zero)
|
let dummy: WinPos = (0, CGRect.zero)
|
||||||
for kNum in self.state.keys {
|
for kNum in self.state.keys {
|
||||||
if kNum == numScreens { continue } // current state, already set above
|
let isCurrent = kNum == self.numScreens
|
||||||
var tmp_state: WinConf = [:]
|
var tmp_state: WinConf = [:]
|
||||||
for (n_app, new_val) in newState {
|
for (n_app, n_windows) in newState {
|
||||||
if let old_val = self.state[kNum]![n_app] {
|
if let old_windows = self.state[kNum]![n_app] {
|
||||||
tmp_state[n_app] = []
|
var win_arr: [WinPos] = []
|
||||||
for (n_win, _) in new_val {
|
for n_win in n_windows {
|
||||||
let old_pos = old_val.first { $0.0 == n_win }
|
// In theory, every space that was visited, was also restored.
|
||||||
tmp_state[n_app]!.append(old_pos ?? dummy)
|
// 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.state[kNum] = tmp_state
|
||||||
}
|
}
|
||||||
self.time = Date(timeIntervalSinceNow: 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getState() -> WinConf {
|
private func getState() -> WinConf {
|
||||||
@@ -125,7 +132,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
if let space = currentSpace(), self.spacesNeedRestore.contains(space) {
|
if let space = currentSpace(), self.spacesNeedRestore.contains(space) {
|
||||||
self.spacesNeedRestore.remove(space)
|
self.spacesNeedRestore.remove(space)
|
||||||
let spaceWinNums = self.getWinIds()
|
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) })
|
self.setWindowSizes(pid, bounds.filter{ spaceWinNums.contains($0.0) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user