// Name: Toggle Monitor Config // Description: Toggle between vertical/work and horizontal/default display, Dock, and Stage Manager configurations with state persistence. // Author: dallascrilley // GitHub: dallascrilley import "@johnlindquist/kit" // ─────────────────────────────────────────────────────────────────────────────── // Setup Instructions (read-only) // - Requires: macOS Sequoia+ // - Dependencies: // 1) Homebrew (https://brew.sh) // 2) displayplacer (the script can install it via Homebrew on first run) // - First Run: // • You'll be prompted to select which display(s) rotate when toggling. // • The script persists state and selected displays in ~/.kenv/db via Keyv store. // - Re-run: The script toggles between two states and restarts Dock as needed. // ─────────────────────────────────────────────────────────────────────────────── // Helper types type ModeState = "work" | "default" type Settings = { state: ModeState displayIds: string[] // displayplacer "Persistent screen id" values to rotate } // Constants for Dock/Stage Manager settings const WORK_MODE = { name: "Vertical/Work Mode", rotation: 90, dock: { orientation: "left", // left | bottom | right autohide: true, // automatically hide/show tilesize: 32, // small }, stageManagerEnabled: true, } const DEFAULT_MODE = { name: "Horizontal/Default Mode", rotation: 0, dock: { orientation: "bottom", autohide: false, // always visible tilesize: 56, // medium }, stageManagerEnabled: false, } // ─────────────────────────────────────────────────────────────────────────────── // Utility Helpers // ─────────────────────────────────────────────────────────────────────────────── const boolStr = (b: boolean) => (b ? "true" : "false") async function ensureDisplayplacer(): Promise<string> { // Find displayplacer binary or install using Homebrew let bin = which("displayplacer") if (bin?.stdout?.trim()) return bin.stdout.trim() const hasBrew = which("brew")?.stdout?.trim() const choice = await arg( "displayplacer is required to rotate displays. Choose an option:", [ { name: "Install displayplacer via Homebrew", value: "install" }, { name: "Open displayplacer GitHub page", value: "open" }, { name: "Cancel", value: "cancel" }, ] ) if (choice === "install") { if (!hasBrew) { await div( md(` ### Homebrew not found Install Homebrew from https://brew.sh then re-run this script. `) ) exit() } try { await $`brew install displayplacer` } catch (e) { await notify("Failed to install displayplacer") console.warn(e) exit() } const post = which("displayplacer")?.stdout?.trim() if (!post) { await notify("displayplacer not detected after installation") exit() } return post } else if (choice === "open") { await browse("https://github.com/jakehilborn/displayplacer") exit() } else { exit() } return "" // unreachable } type DisplayInfo = { id: string degree?: number } async function listDisplays(): Promise<DisplayInfo[]> { const displayplacerBin = await ensureDisplayplacer() const { stdout } = await $`${displayplacerBin} list` const lines = stdout.split("\n") const displays: DisplayInfo[] = [] let current: DisplayInfo | null = null for (const line of lines) { const idMatch = line.match(/Persistent\s+screen\s+id:\s*([A-Za-z0-9\-]+)/i) if (idMatch) { if (current) displays.push(current) current = { id: idMatch[1] } continue } const degMatch = line.match(/\bdegree:\s*(\d+)/i) if (degMatch && current) { current.degree = Number(degMatch[1]) } } if (current) displays.push(current) return displays } async function rotateDisplays(targetIds: string[], degree: number): Promise<void> { const displayplacerBin = await ensureDisplayplacer() // Apply rotation to only the selected displays; leave others untouched for (const id of targetIds) { try { await $`${displayplacerBin} "id:${id} degree:${degree}"` } catch (e) { console.warn(`Error rotating display ${id}:`, e) throw new Error(`Failed to rotate display ${id}`) } } } async function setDockSettings(orientation: "left" | "bottom" | "right", autohide: boolean, tilesize: number) { try { await $`/usr/bin/defaults write com.apple.dock orientation -string ${orientation}` await $`/usr/bin/defaults write com.apple.dock autohide -bool ${boolStr(autohide)}` await $`/usr/bin/defaults write com.apple.dock tilesize -int ${String(tilesize)}` } catch (e) { console.warn("Error writing Dock defaults:", e) throw new Error("Failed to write Dock settings") } try { await $`/usr/bin/killall Dock` } catch (e) { // ignore if Dock not running; warn otherwise console.warn("Dock restart error:", e) } } async function setStageManager(enabled: boolean) { // Stage Manager: com.apple.WindowManager GloballyEnabled -bool true/false try { await $`/usr/bin/defaults write com.apple.WindowManager GloballyEnabled -bool ${boolStr(enabled)}` } catch (e) { console.warn("Error setting Stage Manager:", e) throw new Error("Failed to set Stage Manager") } // Nudge UI services (not strictly required, but helps apply changes) try { await $`/usr/bin/killall ControlCenter` } catch { // not fatal } } async function getOrSetupSelection(): Promise<Settings> { const kv = await store("toggle-monitor-config", { settings: { state: "default", displayIds: [] as string[] } }) let settings = (await kv.get("settings")) as Settings | undefined if (!settings) { settings = { state: "default", displayIds: [] } } // If no displays stored, prompt selection if (!settings.displayIds || settings.displayIds.length === 0) { const displays = await listDisplays() if (displays.length === 0) { await div(md(`No displays detected via displayplacer.`)) exit() } let selectedIds: string[] = [] if (displays.length === 1) { selectedIds = [displays[0].id] } else { const choices = displays.map(d => ({ name: `Display ${d.id}${typeof d.degree === "number" ? ` (deg ${d.degree})` : ""}`, value: d.id, })) selectedIds = await select<string[]>( { placeholder: "Select displays to rotate when toggling", enter: "Save Selection", }, choices ) if (!selectedIds || selectedIds.length === 0) { await div(md(`No displays selected. Exiting.`)) exit() } } settings.displayIds = selectedIds // Derive initial state if possible based on first selected display's degree try { const currentDisplays = await listDisplays() const first = currentDisplays.find(d => d.id === selectedIds[0]) if (first && first.degree === 90) { settings.state = "work" } else { settings.state = "default" } } catch { settings.state = "default" } await kv.set("settings", settings) } return settings } async function persistState(nextState: ModeState, displayIds: string[]) { const kv = await store("toggle-monitor-config") await kv.set("settings", { state: nextState, displayIds }) } // ─────────────────────────────────────────────────────────────────────────────── // Main Toggle Logic // ─────────────────────────────────────────────────────────────────────────────── let errors: string[] = [] try { // Ensure dependency and collect config await ensureDisplayplacer() const settings = await getOrSetupSelection() const currentState: ModeState = settings.state || "default" const targetState: ModeState = currentState === "work" ? "default" : "work" const targetConfig = targetState === "work" ? WORK_MODE : DEFAULT_MODE // Apply Rotations try { await rotateDisplays(settings.displayIds, targetConfig.rotation) } catch (e) { errors.push((e as Error).message || "Rotation failed") } // Apply Dock settings try { await setDockSettings( targetConfig.dock.orientation as "left" | "bottom" | "right", targetConfig.dock.autohide, targetConfig.dock.tilesize ) } catch (e) { errors.push((e as Error).message || "Dock settings failed") } // Apply Stage Manager try { await setStageManager(targetConfig.stageManagerEnabled) } catch (e) { errors.push((e as Error).message || "Stage Manager update failed") } // Persist new state await persistState(targetState, settings.displayIds) // Feedback if (errors.length === 0) { await notify(`Switched: ${targetConfig.name}`) await toast(`Success: ${targetConfig.name}`, { autoClose: 3000 }) } else { await notify(`Partial Success: ${targetConfig.name}`) await div( md( `# Toggle Result - Target: ${targetConfig.name} - Rotated Displays: ${settings.displayIds.join(", ") || "None"} ## Issues ${errors.map(e => `- ${e}`).join("\n")} ` ) ) } } catch (fatal) { console.warn("Fatal error:", fatal) await notify("Toggle failed") await div( md( `# Toggle Failed \`\`\` ${String((fatal as Error).message || fatal)} \`\`\` ` ) ) }