Skip to content
GitHub

Displays and presentation

Two user-facing preferences decide where and in what shape the chrome appears: presentation (notch-fused vs free-floating) and display (which screen). Both ship with a Settings UI and persist across launches; a host usually leaves them alone and reads the resolved result.

A notch app’s premise is the physical notch - but plenty of Macs don’t have one (a Mac mini or Studio on an external display, pre-2021 MacBooks, any desktop display). NookPresentation is the knob that makes the chrome work anywhere:

public enum NookPresentation: String, Codable, Sendable, CaseIterable, Equatable {
case auto // notch layout on a notched display, floating otherwise
case notch // always the eared notch shape (mostly useful for testing)
case floating // always a free-floating rounded panel below the menu bar
}
  • .auto (the default) renders the eared, notch-fused shape on a notched display and a free-floating rounded panel just below the menu bar everywhere else. This is the right choice for almost every app - the same binary adapts to whatever Mac it runs on.
  • .notch forces the notch shape even where there is no notch (it then hangs from the bare menu bar).
  • .floating forces the free-floating panel even on a notched MacBook.

presentation lives on NookAppearancePreferences (persisted under opennook.appearance.v1, the same record as the palette and surface style) and is exposed in the Settings “Layout” picker. The resolution is a pure function:

NookPresentation.auto.isFloating(screenHasNotch: false) // -> true
NookPresentation.auto.isFloating(screenHasNotch: true) // -> false

You don’t normally set this in code - the user picks it in Settings. To pin it programmatically, write through AppState.replaceAppearancePreferences(_:) so the choice persists (see Theming).

On a multi-display Mac, “which screen” is a real choice. NookDisplayPreference expresses it in a form that survives reboots and replugging:

public struct NookDisplayPreference: Equatable, Codable, Sendable {
public enum Mode { case builtIn, main, specific }
public var mode: Mode
public var displayUUID: String? // only used when mode is .specific
}
  • .builtIn (the default) - the laptop’s built-in (notched) panel, where the physical notch is.
  • .main - whichever display currently hosts the active menu bar (NSScreen.main); follows the user’s focus across screens.
  • .specific(uuid) - one named display, pinned by its stable display UUID.

The UUID matters: CGDirectDisplayID and NSScreen ordering both shuffle as displays connect and disconnect, so a saved preference can’t reference them directly. NookDisplayPreference.specific(_:) persists the display UUID (CGDisplayCreateUUIDFromDisplayID), which is stable across reconnects.

The preference lives on AppState.displayPreference (persisted separately, under opennook.display.v1) and is exposed in the Settings “Display” picker. Write it through AppState.replaceDisplayPreference(_:) to persist; the coordinator re-places the chrome on the new display immediately.

NookScreenLocator resolves a preference to a concrete NSScreen, and the fallback chain keeps the chrome on-screen even when the chosen display is gone:

  • .specific that isn’t attached -> built-in -> main -> first attached.
  • .builtIn on a Mac with no built-in panel -> main -> first attached.
  • .main -> built-in -> first attached.

It returns no screen only when no display is attached at all. So a .builtIn default on a Mac mini, or a saved .specific display that has been unplugged, both degrade to a sensible screen rather than the chrome vanishing.

NookScreenLocator.connectedDisplays() enumerates the attached displays (DisplayInfo: uuid, name, isBuiltIn) for building your own picker if you replace the Settings UI.

presentation and displayPreference persist to different keys

Section titled “presentation and displayPreference persist to different keys”

presentation is part of NookAppearancePreferences (opennook.appearance.v1) and is written through replaceAppearancePreferences(_:). The display choice is separate - AppState.displayPreference, key opennook.display.v1, written through replaceDisplayPreference(_:). Two different preferences, two different write paths.

Test the floating layout without unplugging

Section titled “Test the floating layout without unplugging”

Set presentation to .floating to see the non-notch layout on a notched MacBook, or .notch to force the eared shape on an external display. .auto only ever shows you whichever matches the current screen.

  • Theming - the appearance-preferences record presentation is part of.
  • Settings chrome - the chrome that hosts the Layout and Display pickers.