Fullscreen apps above the MacBook notch
NOTE: This is not a tutorial. If you don't understand this article in detail, please don't attempt to do what's written in it.
On MacBook laptops with a notch, the area above the notch is reserved for the menu bar for most applications. That is good for normal window usage, as the menubar no longer takes precious vertical space that could be used for the content.
Going fullscreen though, only makes that area black (so as to hide the notch) but apps will still render below the notch, leaving about 100k pixels useless.
So here are some instructions on how to fullscreen a window above the notch in the most hackish way possible.
By the way, if you prefer that old MacBook without a notch design, my Lunar app can disable the notch by applying a hidden resolution.
# Old-style fullscreen
Every macOS window is actually a NSWindow
, and that class has a toggleFullScreen()
method.
But what that does is move the window to its own special Mission Control Space and render it fullscreen below the notch. Not what I want.
There's a thing called old-style fullscreen which some apps support natively (Sublime Text, kitty, IINA etc.).
Instead of calling toggleFullScreen()
, this method does the following:
- makes the window borderless
- sets up the app to auto-hide the menubar and dock
- sets the window frame equal to the screen frame
Here's how IINA does it for example: iina/MainWindowController.swift · iina/iina · GitHub
The problem is that, while apps can change their own windows as they please, there's no way for an app to change those attributes on windows of other apps.
# Injecting code
Using Frida we can inject and run code inside the context of any running process.
We first need to disable SIP though, as code injection is restricted by default for obvious security reasons.
Below we define our Frida script, which we can save in ~/Documents/OverNotch.js for later usage.
OverNotch.js
var NSApplicationPresentationAutoHideMenuBar = 1 << 2
var NSApplicationPresentationAutoHideDock = 1 << 0
var NSWindowStyleMaskBorderless = 0
var NSApplicationPresentationDefault = 0
global.app = ObjC.classes.NSApplication.sharedApplication()
global.arrayFromNSArray = (nsArray) => {
var jsArray = []
var count = nsArray.count()
for (var i = 0; i < count; i++) {
jsArray[i] = nsArray.objectAtIndex_(i)
}
return jsArray
}
global.mainWindow = () => {
if (global.app.mainWindow()) { return global.app.mainWindow() }
if (global.app.keyWindow()) { return global.app.keyWindow() }
var windows = arrayFromNSArray(global.app.windows())
var index = Math.min(...windows.map((w) => w.orderedIndex()))
return windows.find((w) => w.orderedIndex() == index)
}
global.w = mainWindow()
global.toggleFullScreen = (windowNum) => {
if (!windowNum) { windowNum = global.w.windowNumber() }
var dict = ObjC.classes.NSThread.mainThread().threadDictionary()
if (dict.valueForKey_(windowNum.toString())) {
stopFullScreen(windowNum)
} else {
makeFullScreen(windowNum)
}
}
global.stopFullScreen = (windowNum) => {
Interceptor.revert(app.setPresentationOptions_.implementation)
ObjC.schedule(ObjC.mainQueue, () => {
global.app.setPresentationOptions_(NSApplicationPresentationDefault)
})
var window = windowNum ? global.app.windowWithWindowNumber_(windowNum) : global.w
if (!window) {
return console.log('Window not found')
}
ObjC.schedule(ObjC.mainQueue, () => {
var key = window.windowNumber().toString()
var dict = ObjC.classes.NSThread.mainThread().threadDictionary()
if (dict.valueForKey_(key)) {
window.setStyleMask_(dict.valueForKey_(key).unsignedLongLongValue())
dict.removeObjectForKey_(key)
}
window.setFrameUsingName_(key)
window.setIsMovable_(true)
Interceptor.revert(app.setPresentationOptions_.implementation)
})
}
global.makeFullScreen = (windowNum) => {
var window = windowNum ? global.app.windowWithWindowNumber_(windowNum) : global.w
if (!window) {
return console.log('Window not found')
}
ObjC.schedule(ObjC.mainQueue, () => {
var key = window.windowNumber().toString()
var dict = ObjC.classes.NSThread.mainThread().threadDictionary()
var value = ObjC.classes.NSNumber.numberWithUnsignedLongLong_(window.styleMask())
dict.setValue_forKey_(value, key)
window.saveFrameUsingName_(key)
global.app.setPresentationOptions_(NSApplicationPresentationAutoHideDock | NSApplicationPresentationAutoHideMenuBar)
window.setStyleMask_(NSWindowStyleMaskBorderless)
window.setFrame_display_(window.screen().frame(), true)
window.setIsMovable_(false)
Interceptor.replace(app.setPresentationOptions_.implementation, new NativeCallback((mask) => {}))
})
}
Then as a test, have a Safari window open, and run the following to make it fullscreen:
sudo frida -q -l ~/Documents/OverNotch.js -e "toggleFullScreen()" Safari
Note that this doesn't work with all windows, and some apps will crash when their window styleMask
is changed.
# Hotkey
I prefer to bind fn - a
to fullscreen the current window using skhd.
I would usually bind
rcmd - a
as it's a bit easier to press, but since I surrendered the⌘ Right Command
key to my rcmd app switcher, I'm usingfn
for these system-wide hotkeys.
Here's what I added in ~/.skhdrc
to do that:
fn - a : sudo frida -q -l ~/Documents/OverNotch.js -e "toggleFullScreen()" $(osascript -e 'tell application "System Events" to get unix id of first application process whose frontmost is true')
# Why would you need such a thing?
I personally wanted it for having Windows 11 look as natively as possible through Parallels Desktop. I still need it from time to time for hardware that only provides a Windows configuration program.
You can also use it to get an immersive view of specific webpages. Here's NightDrive for example:
Or maybe have a more focused zen-mode on note taking apps. Here's NotePlan in my case:
# SIP disabled? How about no?
Frida and other debuggers like lldb and gdb use the task_for_pid
API for attaching to a running process.
That doesn't necessarily need SIP being disabled. For task_for_pid
to succeed on a process, that process needs to meet one of the following requirements:
- be unsigned
- be signed without Hardened Runtime
- include the
com.apple.security.get-task-allow
entitlement
Fortunately, we can alter the signature of any non-system app without disabling SIP.
Unfortunately that's not very straightforward because we need to ensure that we sign all the additional bundles inside the app, and we need to take care of not removing existing entitlements.
Because I'm using fish shell, here's how I approach this task:
function resign-bundle -a bundle
# Dump existing entitlements as a PLIST
codesign -d --entitlements - --xml "$bundle" >/tmp/entitlements.xml
if not test -s /tmp/entitlements.xml
# Sign without Hardened Runtime
codesign -fs $CODESIGN_CERT "$bundle"
return
end
# Add `get-task-allow` to the entitlements to allow debuggers
/usr/libexec/PlistBuddy -c "Add :com.apple.security.get-task-allow bool true" /tmp/entitlements.xml
# Sign with Hardened Runtime and `get-task-allow`
codesign -fs $CODESIGN_CERT -o runtime --timestamp --entitlements /tmp/entitlements.xml "$bundle"
end
function resign-app -a app
# Backup existing app
mkdir -p ~/.cache/resign-app-backups/
if not test -d "~/.cache/resign-app-backups/$(basename "$app")"
rsync -avz "$app" ~/.cache/resign-app-backups/
end
# Resign bundles inside the app
fd -uu '\.(app|framework|dylib|xpc|appex)$' "$app" -j 1 -x fish -c 'resign-bundle {}'
# Resign the app itself
resign-bundle "$app"
end
Testing this on RealVNC Viewer:
> resign-app "/Applications/VNC Viewer.app"
Executable=/Applications/VNC Viewer.app/Contents/MacOS/vncviewer
/Applications/VNC Viewer.app: replacing existing signature
I also tried testing this on VirtualBuddy but because it has a required entitlement (com.apple.vm.networking
) and an embedded provisioning profile tied to the developer certificate, this got too complicated to warrant the effort.
System apps also can't work with this approach because some of them live in immutable volumes called Cryptexes. For example here's where Safari can be found on macOS Ventura:
# Safari lives at /System/Volumes/Preboot/Cryptexes/App/System/Applications/Safari.app
> codesign --remove-signature '/System/Volumes/Preboot/Cryptexes/App/System/Applications/Safari.app'
/System/Volumes/Preboot/Cryptexes/App/System/Applications/Safari.app: internal error in Code Signing subsystem