Presenting from a Mac without constantly turning around

Presenting from a laptop often means craning your neck to check what the audience sees. This guide configures an extended desktop so selected apps launch maximized on the projector, then mirrors that output back to a MacBook with click-through support; notes and tools stay on your screen and presentation content stays on theirs.

1. Set the projector as an extended display

System SettingsDisplays → select the projector/external display → Use as: Extended display

2. Install and configure Hammerspoon

  1. Install Hammerspoon
  2. Grant Accessibility permission when prompted
  3. Hammerspoon menu bar icon → Open Config
  4. Paste the script below into init.lua and save
  5. Hammerspoon menu bar icon → Reload Config
-- Auto-move and maximize QuickTime Player and Preview windows
-- on the first external display.

hs.window.animationDuration = 0

local wf = hs.window.filter.new({
  "QuickTime Player",
  "Preview",
})

local function externalScreen()
  local primary = hs.screen.primaryScreen()

  for _, screen in ipairs(hs.screen.allScreens()) do
    if screen:id() ~= primary:id() then
      return screen
    end
  end

  -- Fallback if no external display is connected.
  return primary
end

local function moveAndMaximizeWindow(win)
  if not win then return end

  local winID = win:id()
  if not winID then return end

  local function apply()
    local currentWin = hs.window.get(winID)

    if not currentWin or not currentWin:isStandard() then
      return
    end

    local targetScreen = externalScreen()

    if targetScreen and currentWin:screen():id() ~= targetScreen:id() then
      currentWin:moveToScreen(targetScreen, false, true, 0)
    end

    currentWin:maximize()
  end

  -- First pass after the window is created.
  hs.timer.doAfter(0.25, apply)

  -- Second pass catches apps that restore/resize shortly after opening.
  hs.timer.doAfter(0.75, apply)
end

-- Only handle newly-created windows.
-- This avoids fighting manual resizing during the presentation.
wf:subscribe(hs.window.filter.windowCreated, moveAndMaximizeWindow)

-- Handle existing matching windows when Hammerspoon loads/reloads.
hs.timer.doAfter(0.5, function()
  for _, win in ipairs(wf:getWindows()) do
    moveAndMaximizeWindow(win)
  end
end)

hs.alert.show("Auto-move/maximize QuickTime/Preview loaded")

Add or remove managed apps using their exact names in the Hammerspoon window filter.

3. Install and start Side Mirror

Install Side Mirror, select the projector/external display from drop down menu at top right, then click Start.

4. Presenting

Opening QuickTime Player or Preview-associated files will display them in full screen on the projector. Use Side Mirror on your MacBook to see and control the projected content. Clicking through moves focus and your cursor to the external display; recover via Opt+Shift+Return.

If you have multiple external displays, replace:

local function externalScreen()
  local primary = hs.screen.primaryScreen()

  for _, screen in ipairs(hs.screen.allScreens()) do
    if screen:id() ~= primary:id() then
      return screen
    end
  end

  return primary
end

with:

local function externalScreen()
  return hs.screen.find("BenQ") or hs.screen.primaryScreen()
end

Replace "BenQ" with a substring matching your projector's display name. You can list connected display names in the Hammerspoon console with:

hs.fnutils.each(hs.screen.allScreens(), function(s) print(s:name()) end)

❧ 2026-04-27