Skip to content

Repository files navigation

McMonad

Have You Ever Wondered What Would Happen if XMonad Wore Clown Shoes

v0.999999999999

Abstract

We bridge the gap between macOS sucking and Linux rocking by introducing McMonad — a bundle of

  • a Swift 6 daemon (mcmonad-core) which is a minimal bus to the macOS native window server via brittle private APIs; and
  • a Haskell process (mcmonad) which runs all window management logic.

They communicate over a Unix socket and the latter is configured with a regular compiled configuraion.

i3-style binary-tree-of-window-splits is enabled with the confusingly-named withSway combinator, which enables feature-parity with i3 except for the fact that holding Option key is required when the WM enters "Resize mode" (denoted by the unicode dagger in the menu bar).

macOS Tahoe+ only.

Getting it running

Prerequisites: grant Accessibility permissions (System Settings > Privacy & Security > Accessibility), and unmap Option+Space (System Settings > Keyboard > Keyboard Shortcuts > Input Sources — disable input source switching). McMonad uses Option as mod.

Install via Nix (recommended): nix build github:cognivore/mcmonad. With home-manager (the proper way for daily driving): add mcmonad.url = "github:cognivore/mcmonad" to your flake inputs, include mcmonad.homeManagerModules.default in your modules, then set services.mcmonad.enable = true and optionally services.mcmonad.configFile with your Haskell config. This gives you a launchd agent that manages both processes, auto-restarts on crash, and sets MCMONAD_GHC for recompilation.

From the .app bundle: grab a release from GitHub Releases. This is very under-tested, so please fix it if it doesn't work and write human-described pull requests to this repo should you undertake this undertaking.

Switching configs

Mod-q is not bound by mcmonad and you should not bind it — on Tahoe, in-process ghc --make triggers a code-signing kill (see Troubleshooting). Recompile out-of-band; the GHC needs the mcmonad library in its package DB (the home-manager module sets MCMONAD_GHC for you; otherwise:

# 1. Get a GHC with mcmonad
GHC=$(nix build .#mcmonad-ghc --no-link --print-out-paths)/bin/ghc

# 2. Edit ~/.config/mcmonad/mcmonad.hs (or copy one from example-configs/)

# 3. Compile and restart
$GHC --make ~/.config/mcmonad/mcmonad.hs \
     -o ~/.config/mcmonad/mcmonad-aarch64-darwin -v0
kill $(pgrep -f mcmonad)
~/.config/mcmonad/mcmonad-aarch64-darwin &

Troubleshooting

"Nothing happens when I press hotkeys." Accessibility permission was not granted, or you need to restart MCMonadCore and mcmonad after granting it. Add whichever binary is running — MCMonadCore + mcmonad if from Nix. On McMonad.app if from the bundle.

"I edited mcmonad.hs but nothing changed." If services.mcmonad.configFile is set in home-manager, it manages ~/.config/mcmonad/mcmonad.hs declaratively — your local edits get overwritten on home-manager switch. Manage it in home-manager!

"Two instances are running." kill $(pgrep -f mcmonad) then start one. If launchd keeps respawning: launchctl bootout gui/$(id -u)/com.mcmonad.agent first.

"The app won't open — 'damaged or incomplete'." (.app bundle only) xattr -cr /Applications/McMonad.app then try again. macOS quarantines unsigned apps.

"Windows tile but focus doesn't follow." Check ~/Library/Logs/mcmonad-core.log. The focus ritual involves four separate private APIs; if one fails, the log says which.

"Librewolf / Firefox windows won't shrink — swaps look like they did nothing." Firefox-family browsers enforce an internal minimum window width in their parent process. When McMonad's layout asks Librewolf to be (say) 576 px wide via kAXSize, the AX call succeeds but Librewolf immediately resizes itself back to its preferred minimum. In ~/Library/Logs/mcmonad-core.log you'll see result=DEFIED delta=(0,0,N,0) for these — McMonad notices, but doesn't fight back. Practical effect: in a 3-column layout on a 1728-px-wide screen, two Librewolfs swapping positions will end up overlapping in the wider half because the "going-narrow" one refuses to actually become narrow. Chrome and Ghostty don't do this; they accept any size. There is no McMonad-side fix that doesn't either (a) read AXMinSize and rewrite the layout to honour it, or (b) use a private NSWindow API to override the in-app resize hook. Until then: don't put more than one Librewolf window on a workspace that gives any of them less than ~800 px of width.

"My workspace switches sometimes go silent for a few seconds." Don't bind Mod-q to restart. Tahoe keys Accessibility grants by cdhash, and ghc --make produces a fresh one each time, so macOS SIGKILLs the recompiled mcmonad ~125 ms in (Launch Constraint Violation). While the launcher respawns it, macOS's text layer eats Option+2/3/5 as ™/£/∞. Deploy via home-manager switch, or recompile out-of-band and launchctl kickstart -k "gui/$(id -u)/org.nix-community.home.mcmonad".


User guide

Before you start

Unmap Option+Space. macOS binds Option+Space to input source switching by default. McMonad uses Option as the mod key (a la xmonad's mod1), so Option+Space is NextLayout. Go to System Settings > Keyboard > Keyboard Shortcuts > Input Sources and disable it. You will not regret this.

Grant Accessibility permissions. McMonad will prompt you on first launch. Without this, mcmonad-core cannot move or resize windows. There is no workaround.

Default keybindings

McMonad defaults to Option as the mod key. If you are a proper Linux / XMonad user who is accustomed to the keybinds and wants the full experience on macOS, you will find my Karabiner configuration to be a nice starter pack for your keybinds. It remaps PC-style shortcuts to macOS equivalents, swaps Fn and Ctrl, adds Right Command + HJKL as arrow keys, and a few other XMonad-flavoured niceties (Option+Shift+Enter spawns a terminal, Option+P opens Spotlight, Option+Shift+C closes a window).

Keys Action
Opt-j / Opt-k Focus down / up
Opt-Return Swap focused window with main
Opt-Shift-j / Opt-Shift-k Swap down / up
Opt-h / Opt-l Shrink / expand main area
Opt-Space Next layout
Opt-Shift-Return Spawn terminal
Opt-Shift-c Close focused window
Opt-t Push floating window back into tiling
Opt-p Spotlight launcher — command runner + app launcher
Opt-Shift-p Spotlight launcher — window search
Opt-1..Opt-9 Switch to workspace
Opt-Shift-1..Opt-Shift-9 Move window to workspace
Opt-w / Opt-e / Opt-r Focus screen 1 / 2 / 3

The Spotlight launcher

McMonad ships a Spotlight-style overlay — one floating panel with switchable modes, cycled with Tab:

  • Command mode (Opt+P) — a command runner and app launcher. Type an app name and hit Return to launch it ("chrome", "librewolf"). Type timer to be asked for minutes, or set one inline: timer 15 check on agents starts a 15-minute countdown labelled "check on agents".
  • Window mode (Opt+Shift+P) — the fuzzy window search across every workspace. "I lost Google Chrome" → type chr → Return jumps to it. (This is the old Opt+Shift+P window picker; it now lives inside the launcher, and Tab flips to command mode.)

/ move the selection, Return activates it, Esc cancels (restoring the window you came from).

Screenshots work in either mode, including the Opt+Shift+P menu. Type screenshot (or scr) and press Return to open the native macOS capture tools. Prefix the name with seconds — 3 scr or 3 screenshot — to select a region, then capture it 3 seconds after releasing the mouse. Press Esc during selection to cancel. The selection overlay closes and restores the previous window before the countdown starts. Captures use macOS's native timer, thumbnail, and configured save destination; delays are non-negative whole seconds and do not block the WM. On the first timed capture, macOS may ask for Screen & System Audio Recording access for MCMonadCore.app. Enable it, then retry the command. This permission is separate from Accessibility and from any grant to the Haskell process.

Running timers live in the menu bar. While any timer is counting down, a ⏱ 14:32 item appears in the menu bar showing the soonest one; its dropdown lists every running timer and lets you cancel them. When a timer finishes it chimes and a "time's up" banner appears. No timers running → no menu-bar clutter.

The reminder offers Snooze 5 min, Jump, Peek, and Dismiss. Peek jumps to the timer's origin workspace and restarts the interval that just fired, keeping its description. For example, Peek on timer 10 check build starts another 10-minute countdown; Peek after a 5-minute Snooze restarts those 5 minutes. Existing state files remain readable; timers saved before duration tracking can still fire, Snooze, and Jump, but Peek is disabled for those timers.

Workspaces belong to screens. Each attached display carries a role — primary, secondary, tertiary, or one of three auxiliary ones — and a workspace belongs to a role, never to "the left" or "the right". Viewing a workspace shows it on its role's screen and moves focus there (and the pointer, when focus follows the mouse or the config warps on switch); it is never pulled onto the screen you happen to be on. While a role's screen is unplugged its workspaces live on the primary; plug it back and they return, each screen getting the workspace it showed last. After every view the other screens are tidied too: a workspace displaced onto a screen it does not belong to gives way to one that does. A screen with none of its own workspaces free keeps whatever it has — some workspace must be shown. The default rules:

affinity = [ Pin Tertiary ["a"], Pin Secondary ["o"]
           , SplitAcross [Tertiary, Secondary] ["7", "8", "9", "0"] ]

Pin is what it says. SplitAcross cuts the list into as many chunks as roles and deals them out in order — 7 8 to the tertiary, 9 0 to the secondary — and with only one of those screens attached the last chunk goes there and the first returns to the primary. Every workspace no rule names belongs to the primary. mcmonad (xmonadClassic defaultConfig) keeps xmonad's greedy behaviour instead.

Roles are assigned by mcmonad-core from the arrangement — the main display is primary, the nearest display to its right secondary, the nearest to its left tertiary, displays above or below auxiliary — and remembered per display (by its hardware UUID) once you choose otherwise: type screen in the launcher to see every attached display with its role and pick "make Secondary" or any other; the choice is kept in the daemon's defaults, and a role is never held by two displays.

Permissions, audited from the inside. The menubar dropdown's Permissions section shows what the daemon itself sees: Accessibility, Screen Recording, Microphone and Speech Recognition as granted or not, the identifier and team the running code is signed with (what macOS keys every grant to), the bundle path, and whether the executable on disk has been replaced since this process launched. That last line explains most phantom prompts: a deploy replaces MCMonadCore.app while the old daemon is still running, macOS stops recognising that process until the launcher restarts it, and a naive check would ask for a grant that was never lost. The daemon now recognises that state and asks for nothing. "Copy audit" puts the whole list on the clipboard; the same lines are logged as PERMS: at startup.

Every list is most-recently-used first. Windows, apps and builtin commands all sort by when you last focused or ran them; typing re-ranks by match, with recency breaking ties.

Window search reads inside windows. mcmonad-core keeps an OCR index of every window it manages, displayed or parked on a hidden workspace: each is captured through ScreenCaptureKit (the window's own buffer, so a covered or parked window reads like a visible one) and read with Vision whenever its pixels change — displayed windows are checked every six seconds and on every layout or focus change, hidden ones every thirty seconds, at most six recognitions per cycle so a big sweep spreads out — and again each time the launcher opens. A query that matches no title is then matched against that text; such rows show the matching line beneath the title with the words highlighted. The index is on by default and needs the Screen & System Audio Recording permission for MCMonadCore.app — the first run lists the app in that pane and opens it; tick it and the index starts on the next cycle. Opt out with ocrIndex = False in mcmonad.hs.

The index reads when you step away. Nothing is captured while you are at the keyboard. After three minutes without input the index makes one pass over every window (displayed first, then parked; a window is re-read only if its picture changed), and "what's up" refreshes the workspaces that changed, so the recap is ready when you come back. Opening "what's up" shows the cache and when it was made; ! what's up (or ! where is …) re-reads every window first. One accurate read of a big window costs about three CPU-seconds, which is why it is no longer done every six seconds.

Everything read off the framebuffer stays in mcmonad-core's memory. It is never written to disk, never logged, and never sent to the Haskell brain.

"where is …" asks Claude. Type or say where is the deploy terminal (where's, where are, where did I put work too) in either mode and press Return. The launcher hands the question, together with every window (workspace, app, title, focus, and an excerpt of its recognised text) to the Claude Code CLI already installed on the Mac — claude -p, model claude-fable-5-1 at low effort, no tools, session persistence off — and shows the whole exchange while it thinks: the exact command line, the prompt as sent, and the answer as it streams, over a muted spinner. The result is a list of windows with their workspace and the model's one-line reason, the question's words highlighted in it; Return focuses one, Esc goes back to the query. The CLI must be logged in; it is looked for in ~/.local/bin, /opt/homebrew/bin, /usr/local/bin and PATH.

"what's up" summarises every workspace. Type or say what's up (or pick the builtin row in command mode) and the same call goes out with a different contract: for every workspace that has windows, one terse sentence on what is going on there and one on the evidence — the titles or recognised text — that makes it right. The instruction asks for caveman brevity: substance only, structured output only. Rows come in workspace order, on-screen ones first, each with its reason underneath. Return views that workspace.

The answers are cached per workspace and kept current by what the window manager itself reports, so opening "what's up" only reads the cache — instant, no call — and a workspace that did not change is never asked about again. Every layout snapshot and every OCR re-read updates a per-workspace fingerprint. A change to a workspace's window set (windows added, removed, moved) refreshes just the changed workspaces in the background after a three-second quiet period. A change only to titles or recognised text also refreshes in the background, but only once that workspace's summary is ten minutes old, so a terminal repainting all day costs a call every ten minutes at most. Calls are batched and at least twenty seconds apart. Rows being refreshed while the panel is open say "(refreshing…)" and update in place.

Voice input — on by default. The launcher starts listening the moment it opens: just speak. "timer 15 check on agents", "chrome", "librewolf". The first keystroke hands off to the keyboard (so dictation never fights your typing); ⌘L or the mic button toggles listening back on. Transcription uses Apple's Speech.framework on-device (free; nothing leaves your Mac). First use prompts for Microphone and Speech Recognition permission; if you decline, the mic affordance hides and everything else keeps working.

Note on Opt+P and Karabiner. If your Karabiner config remaps Opt+P (the starter pack maps it to macOS Spotlight), Karabiner eats the key before McMonad's Carbon hotkey sees it. Remove that rule to let Opt+P reach the McMonad launcher, or bind the launcher to a different key with showSpotlight "command" in your config.

Default terminal

McMonad defaults to Ghostty. If you are using Ghostty (and you should), be aware that its default UX is pretty horrible for a tiling WM setup. Specifically, Ghostty keeps running after you close the last window, which means you will have phantom Ghostty processes cluttering your workspace. The fix is one line in your Ghostty config:

quit-after-last-window-closed = true

A complete Ghostty configuration that works well with McMonad is also available in my configuration starter pack.

Versioning

The versioning policy is to just keep adding 9s to the minor version after 0. until we find a maintainer for this. Current version: 0.999999999999. Next: 0.9999999999999. Then 0.99999999999999. This is called "ClownVer".

Who made this and why?

McMonad was developed to enable performance among the engineering team at geoSurge. If you want to join our hip company, you are most welcome to.


Deep dive

The big idea

image

From X to M

In 2007, Stewart and Janssen published a lengthy and detailed paper xmonad: A Tiling Window Manager, which demonstrated that a window manager could be structured as a pure function from events to window configurations, with all mutable state confined to a well-typed monad stack. Full text of the paper is available in the Implementation and Application of Functional Languages, 2008. The core insight was that the X monad — ReaderT XConf (StateT XState IO) — cleanly separated pure layout computation from X11herc side effects.

We asked a simple question: what if we replace the X monad with an M monad, and replace the X11 Display* with a Unix socket to a macOS Accessibility server?

-- xmonad
newtype X a = X (ReaderT XConf (StateT XState IO) a)

-- mcmonad
newtype M a = M (ReaderT MConf (StateT MState IO) a)

The monad stack is the same shape. MConf holds a socket connection where XConf held an X11 display. MState holds a WindowSet — the exact same XMonad.StackSet type, instantiated with macOS window references instead of X11 window IDs:

-- xmonad
type WindowSet = StackSet WorkspaceId (Layout Window) Window ScreenId ScreenDetail

-- mcmonad
type WindowSet = StackSet String (Layout WindowRef) WindowRef ScreenId ScreenDetail

Boy, do I love strings.

Where xmonad calls XSync, XSetInputFocus, and XMoveResizeWindow, mcmonad sends JSON commands over a Unix socket: SetFrames, FocusWindow, HideWindows. Where xmonad reads X11 events, mcmonad reads events from mcmonad-core: WindowCreated, WindowDestroyed, HotkeyPressed, ScreensChanged.

The pure core is the single source of truth for correct window placements, we inherit the invariants and tests verbatim from XMonad.

mcmonad-core: the Effectful backend

mcmonad-core is a small Swift 6 daemon that serves as the effectful backend for the M monad. It performs all I/O — talking to the window server, observing events, registering hotkeys, managing displays — and exposes a clean command/event protocol over a Unix socket at ~/.config/mcmonad/core.sock.

The primitives it provides, and their implementation status:

Window enumeration and observation. We use the SkyLight private framework to enumerate windows and observe creation/destruction/move/resize events. SkyLight is Apple's private interface to the window server (WindowServer process) — the layer beneath AppKit that actually composites and manages windows on screen. It is not documented, not stable across macOS versions, and not supposed to be used by third-party applications but every macOS tiling window manager (yabai, Amethyst's lower layers, AeroSpace's experimental paths) uses it anyway.

We load it via dlopen("/System/Library/PrivateFrameworks/SkyLight.framework/SkyLight") and resolve symbols with dlsym at runtime. Window filtering applies the same heuristics proven in OmniWM: top-level windows only, correct window levels (Normal, Floating, ModalPanel), proper visibility attributes and tag bits. Event coalescing deduplicates frameChanged per window ID and drains on the main runloop.

Window metadata. AXUIElement APIs provide structured metadata: title, app name, bundle ID, subrole, dialog classification, fixed-size detection, button presence. This feeds directly into the WindowInfo record that the Haskell Query monad reads. The defaultManageHook classifies windows using this metadata (dialogs and fixed-size windows float, everything else tiles).

Frame writes. Writing window frames uses AXUIElement position/size attributes. The write ordering is deliberate: when growing a window, we set position first then size (to avoid clipping at screen edges); when shrinking, size first then position (to avoid overlap). Batch writes are wrapped in SkyLight.disableUpdate/reenableUpdate to suppress redraws.

Window focus. Focusing a window on macOS is a three-step ritual that requires three separate private APIs:

  1. NSRunningApplication.activate to bring the app forward,
  2. _SLPSSetFrontProcessWithOptions via a ProcessSerialNumber to tell the window server which specific window to front,
  3. posting synthetic key-window event records via SLPSPostEventRecordTo — a 248-byte event structure with magic constants at specific offsets. Finally
  4. AXUIElementPerformAction(kAXRaiseAction) to raise via Accessibility.

This is the ugliest code in the project. It works.

The CGWindowID-to-AXUIElement bridge. The private function _AXUIElementGetWindow bridges SkyLight's CGWindowID namespace to the Accessibility API's AXUIElement namespace. Without it, we would have no way to correlate the windows we observe (via SkyLight) with the windows we manipulate (via AX). Apple provides no public API for this. Please note that other window managers report Tahoe-related bugs when window IDs are not stable between screen locks. I haven't hit this with McMonad yet, but, you know, if you suffer send PRs.

Hotkey registration. We use Carbon's RegisterEventHotKey because it is the only macOS API that provides global hotkey registration without requiring an event tap (which would need additional permissions). The API is deprecated but stable — it has worked since Mac OS X 10.0 and Apple has not removed it. If Apple removes it, I will switch to Framework Pro and SSH to my Mac Book.

Clownordinate system. macOS uses CGFloat (doubles) where X11 uses integers. AppKit has origin at bottom-left, SkyLight at top-left, and some AX calls return yet another coordinate space :clown:. mcmonad-core normalises everything to top-left-origin doubles before sending to Haskell.

Closing the loop

How we integrated mcmonad-core with xmonad

The haskell/ directory contains ~5,500 lines of Haskell that wire xmonad's pure logic to mcmonad-core's effectful backend. We shall walk through the key modules.

MCMonad.Core defines the M monad, the WindowRef type (a CGWindowID + pid pair), ScreenId, ScreenDetail, Rectangle (using doubles, not X11 integers), and the LayoutClass typeclass. The layout typeclass is pattern-matched to xmonad's — same methods (runLayout, doLayout, pureLayout, handleMessage, pureMessage, description), same SomeMessage system — but with M instead of X as the effect monad. Exception isolation follows the same pattern: catchM wraps SomeException, userCodeDef provides a default value on failure.

MCMonad.IPC defines the wire protocol. Command is an ADT of everything Haskell can ask Swift to do (SetFrames, FocusWindow, HideWindows, ShowWindows, QueryWindows, QueryScreens, RegisterHotkeys, CloseWindow, SetWorkspaceIndicator, WarpMouse). Event is everything Swift can report (WindowCreated, WindowDestroyed, FrontAppChanged, HotkeyPressed, ScreensChanged, etc.). Connection management includes exponential backoff (500ms to 30s) — if mcmonad-core is not running yet, the Haskell process waits.

MCMonad.Operations contains the windows function — the single point of truth for all state transitions, directly mirroring xmonad's architecture. When you call windows W.focusDown, here is what happens: (1) apply the pure WindowSet transformation, (2) diff old and new visible sets, (3) send HideWindows for windows that left, (4) send ShowWindows for windows that arrived, (5) run layouts for each visible screen, (6) resolve floating windows from RationalRect to absolute coordinates, (7) send SetFrames with all frame assignments, (8) send FocusWindow if focus changed, (9) update the workspace indicator, (10) warp the mouse if the screen or workspace changed. All window lifecycle operations — manage, unmanage, kill — ultimately call windows.

MCMonad.Layout provides Tall, Full, Mirror, and Choose (the ||| combinator). These are pure geometric algorithms copied from xmonad: tile, splitVertically, splitHorizontally, mirrorRect. No I/O, no platform specifics. A Tall 1 0.03 0.5 on macOS produces the exact same rectangle list as on X11 for the same input.

MCMonad.ManageHook provides the Query monad over WindowInfo metadata and the standard combinators: —>, =?, <&&>, <||>, composeAll, composeOne, doFloat, doShift, doIgnore. Predicates include title, appName, bundleId, isDialog, isFixedSize, hasCloseButton, hasFullscreenButton. The defaultManageHook floats dialogs, fixed-size windows, and windows without a fullscreen button.

MCMonad.Config defines the configuration record (same fields as xmonad's XConfig: terminal, layoutHook, manageHook, modMask, keys, borderWidth, focusFollowsMouse, logHook, startupHook) and the default keybindings, which follow xmonad conventions exactly.

MCMonad.Main ties it together: connect to core, wait for Ready, query screens, build initial StackSet, register hotkeys, query existing windows, batch-insert them via manageSilent (to avoid N layout passes during startup), run one layout pass, run the startup hook, enter the event loop. The event loop dispatches on event type and calls the appropriate M action.

The process boundary as a reliability mechanism

If the Haskell process crashes, mcmonad-core keeps running. launchd restarts Haskell, which reconnects, queries current state, and resumes layout. If mcmonad-core crashes, launchd restarts it, and Haskell reconnects on the next event. Probably there are bugs here because launchd is a joke, but I suffered 0 crashes so far.

Appendix A: Shimming for xmonad-contrib

One of our goals was to let users use their elongated 21st century screens properly.

For that, they need ThreeCol layout, which is trivially importable from xmonad-contrib. Thus we figured — heck, let's just import it! This will also allow fellow XMonad enjoyers bring their full configs to macOS without modification.

The problem: xmonad-contrib layouts implement XMonad.LayoutClass, which lives in the X monad and uses X11's integer Rectangle. McMonad has its own LayoutClass in the M monad with double-precision Rectangle. Thus, we are shimming it!

The shim lives in MCMonad.Compat.XMonadContrib:

newtype XMonadWrapper l a = XW (l a)

fromXMonad :: (XMonad.LayoutClass l a, ...) => l a -> Layout a
fromXMonad = Layout . XW

XMonadWrapper implements McMonad's LayoutClass by delegating to xmonad's LayoutClass, converting rectangles at the boundary (toX11Rect truncates doubles to integers, fromX11Rect promotes integers to doubles). Since xmonad-contrib layouts are overwhelmingly pure — they implement pureLayout and pureMessage, not the effectful variants — the X monad is never actually entered. The wrapper calls XMonad.pureLayout directly.

This means ThreeColMid, and the other less useful layouts nobody I know cares about, work out of the box:

import MCMonad.Compat.XMonadContrib (XMonadWrapper(..))
import qualified XMonad.Layout.ThreeColumns as XMonad

main = mcmonad defaultConfig
    { layoutHook = Layout (XW (XMonad.ThreeColMid 1 0.03 (1/3))
                       ||| Tall 1 0.03 0.5
                       ||| Full)
    }

The integer-to-double conversion introduces sub-pixel rounding. In practice this is invisible — macOS windows snap to integer coordinates anyway.

Layouts that perform effects in the X monad (reading X11 atoms, spawning processes, querying window properties) will not work through the shim. These are rare in xmonad-contrib, but they exist. We have not built an effectful bridge and currently have no plans to. Pure layouts cover the overwhelming majority of use cases.


McMonad builds on the work of Stewart, Janssen, and the xmonad community. We just put clown shoes on it and made it honk on macOS.

About

Have you ever wondered what would happen if XMonad wore clown shoes?

Resources

Stars

15 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages