{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://dougfessler.com/projects/windowlayoutmanager/profile.schema.json",
  "title": "WLM profile",
  "description": "Schema for Window Layout Manager profile JSON files stored under %APPDATA%/window-layout-manager/profiles/. Profiles are human-editable but most users should let WLM write them. See https://dougfessler.com/projects/windowlayoutmanager/ for the app and its tutorial.",
  "type": "object",
  "required": ["name", "savedAt", "windows"],
  "additionalProperties": false,
  "properties": {
    "name": {
      "type": "string",
      "description": "Display name of the profile. Used as the file name (with forbidden characters substituted)."
    },
    "savedAt": {
      "type": "string",
      "format": "date-time",
      "description": "ISO 8601 timestamp when this profile was last saved."
    },
    "hotkey": {
      "type": "string",
      "description": "Optional Electron accelerator string (e.g. 'Ctrl+Alt+1'). Empty string disables the hotkey."
    },
    "$schema": {
      "type": "string",
      "description": "Pointer back to this schema so editors offer autocomplete on a hand-edited profile. WLM writes it on every save and otherwise ignores it."
    },
    "live": {
      "type": "boolean",
      "description": "Legacy Hold-mode flag. Hold mode was removed on 2026-09-20 and WLM neither reads nor writes this; it stays declared only so pre-removal profiles with live:true still validate against this schema (the root object disallows unknown properties)."
    },
    "displayMode": { "$ref": "#/$defs/displayMode" },
    "postLoad": {
      "description": "Optional shell command fired by WLM after the profile finishes restoring. Useful for switching audio, opening tabs, starting OBS, etc. Either a string command line or an object with explicit cwd/env.",
      "oneOf": [
        { "type": "string", "description": "Shell command line. Goes through cmd.exe (Windows) so you can chain with && / |." },
        {
          "type": "object",
          "required": ["command"],
          "additionalProperties": false,
          "properties": {
            "command": { "type": "string" },
            "cwd": { "type": "string", "description": "Working directory for the spawned command." },
            "env": {
              "type": "object",
              "description": "Extra environment variables. Merged onto process.env; values must be strings.",
              "additionalProperties": { "type": "string" }
            }
          }
        }
      ]
    },
    "monitors": {
      "type": "array",
      "description": "The monitor layout at save time. Used to derive saved coordinates and DPI on restore.",
      "items": { "$ref": "#/$defs/monitorRecord" }
    },
    "virtualDesktops": {
      "type": ["object", "null"],
      "description": "Virtual desktop snapshot. null on hosts where WLM couldn't read the COM bridge.",
      "required": ["count", "currentIndex"],
      "additionalProperties": false,
      "properties": {
        "count": { "type": "integer", "minimum": 0 },
        "currentIndex": { "type": "integer", "description": "Active desktop index at save time. -1 when unknown." }
      }
    },
    "windows": {
      "type": "array",
      "description": "Saved windows. Each entry is matched against live windows at restore time by process+title and friends.",
      "items": { "$ref": "#/$defs/window" }
    },
    "virtualWindows": {
      "type": "array",
      "description": "WLM-owned containers other windows can dock into. A container is a real window WLM creates, positions and moves on the desk. Docking never reparents a member, so a member stays a top-level window that WLM moves alongside its container. A container with fullDock also hides its members' title bars and borders, and puts them back when it lets go. Containers shared across profiles are not stored here; they live in virtual-windows.json beside the profiles folder. Absent means this profile has never used one.",
      "items": { "$ref": "#/$defs/virtualWindow" }
    }
  },
  "$defs": {
    "displayMode": {
      "type": "object",
      "description": "Optional display configuration applied before this profile restores its windows: per-monitor scaling (the Windows 'zoom' percentage) and/or resolution. Setting the scaling percentage uses an undocumented Windows call, so it can report unavailable on some builds; resolution uses the documented ChangeDisplaySettingsEx path.",
      "required": ["targets"],
      "properties": {
        "enabled": {
          "type": "boolean",
          "description": "Defaults to true when the block is present. Read as 'enabled !== false', so omitting it leaves the block active."
        },
        "displayOnly": {
          "type": "boolean",
          "description": "When true this profile is purely a zoom/resolution preset: window restore is skipped and only the display step runs."
        },
        "applyOn": {
          "type": "array",
          "description": "Which load sources apply this block. 'load' covers the Load button, tray, hotkey and CLI, and is the only value the editor writes.",
          "items": { "type": "string", "enum": ["load"] }
        },
        "appliedAt": { "type": "string", "description": "ISO timestamp of the last successful apply." },
        "targets": {
          "type": "array",
          "minItems": 1,
          "items": { "$ref": "#/$defs/displayModeTarget" }
        }
      }
    },
    "displayModeTarget": {
      "type": "object",
      "description": "One monitor's desired display state. At least one of scalePercent, scaleStep, mode or position must be set, and scalePercent/scaleStep are mutually exclusive.",
      "required": ["role"],
      "properties": {
        "role": {
          "type": "string",
          "description": "Either a saved monitor role ('primary', 'secondary', …) or a virtual role resolved live: '@focused' (the monitor holding the foreground window), '@primary', or '@all'."
        },
        "deviceId": {
          "type": ["string", "null"],
          "description": "Stable hardware path for physical monitors. Virtual displays (Parsec, Apollo, spacedesk, any IddCx driver) regenerate it each time the display is created, so matching falls back to the EDID hardware id and then to deviceString."
        },
        "deviceString": { "type": ["string", "null"], "description": "Driver/model name, e.g. 'ParsecVDA'. Stable where deviceId is not, though most displays report the useless constant 'Generic PnP Monitor'." },
        "deviceName": { "type": ["string", "null"], "description": "GDI adapter name (\\\\.\\DISPLAY1). A hint only — it renumbers when displays attach, and is always re-resolved at apply time." },
        "scalePercent": {
          "type": ["integer", "null"],
          "description": "Absolute scaling percentage. Must be one of the fixed Windows steps (100, 125, 150, 175, 200, 225, 250, 300, 350, 400, 450, 500)."
        },
        "scaleStep": {
          "type": ["integer", "null"],
          "description": "Relative move along the scaling ladder (+1 / -1), resolved against the live value at apply time. Mutually exclusive with scalePercent."
        },
        "position": {
          "type": ["object", "null"],
          "description": "Where this monitor sits on the desktop, in physical pixels, relative to the primary at (0,0). Negative values are normal — a monitor above or left of the primary has them. An arrangement is only meaningful as a whole, so the editor writes a position onto EVERY target when it writes any. Not allowed on the virtual roles: '@all' names several monitors, '@focused' names a different one each time, and '@primary' is the origin by definition. Applied after resolution and orientation, because those change the very sizes an arrangement is made of, and applied to all monitors in one atomic commit.",
          "required": ["x", "y"],
          "additionalProperties": false,
          "properties": {
            "x": { "type": "integer", "description": "Left edge, in physical pixels, relative to the primary monitor's top-left." },
            "y": { "type": "integer", "description": "Top edge, in physical pixels, relative to the primary monitor's top-left." }
          }
        },
        "mode": {
          "type": ["object", "null"],
          "description": "Target resolution. Width and height must match a mode the driver offers; refresh rate is a preference and falls back to the nearest available.",
          "required": ["width", "height"],
          "properties": {
            "width": { "type": "integer", "minimum": 1, "description": "Desktop width AFTER any rotation, matching what Windows reports once applied." },
            "height": { "type": "integer", "minimum": 1, "description": "Desktop height AFTER any rotation." },
            "refreshHz": { "type": ["integer", "null"], "minimum": 1 },
            "orientation": {
              "type": "string",
              "enum": ["landscape", "portrait", "landscape-flipped", "portrait-flipped"],
              "description": "Display rotation, mapping to DEVMODEW.dmDisplayOrientation DMDO_DEFAULT/90/180/270. Defaults to landscape. Width and height above are expressed in this orientation's space, so a portrait 1080x1920 is stored as width 1080."
            }
          }
        }
      }
    },
    "rect": {
      "type": "object",
      "required": ["left", "top", "right", "bottom"],
      "additionalProperties": false,
      "properties": {
        "left":   { "type": "integer" },
        "top":    { "type": "integer" },
        "right":  { "type": "integer" },
        "bottom": { "type": "integer" }
      }
    },
    "monitorRecord": {
      "type": "object",
      "required": ["role", "bounds", "workArea"],
      "additionalProperties": true,
      "properties": {
        "role": {
          "type": "string",
          "anyOf": [
            { "enum": ["primary", "secondary", "tertiary", "quaternary", "quinary", "senary"] },
            { "type": "string", "pattern": "^monitor-[0-9]+$" }
          ],
          "description": "Stable role assigned by WLM. Primary is always first; the rest are ordered left-to-right, top-to-bottom. The named roles run out at six displays; from the seventh on, the role is monitor-7, monitor-8, and so on."
        },
        "isPrimary": { "type": "boolean" },
        "bounds":   { "$ref": "#/$defs/rect", "description": "Monitor pixel bounds in virtual-screen coordinates." },
        "workArea": { "$ref": "#/$defs/rect", "description": "Work area = bounds minus reserved space (taskbars, etc.)." },
        "dpi":      { "type": "integer", "minimum": 1, "description": "Effective DPI at save time. 96 = 100% scale." },
        "deviceId":     { "type": ["string", "null"], "description": "Hardware identifier from EnumDisplayDevicesW (stable across reboots for physical monitors; regenerates per session on virtual displays)." },
        "hardwareId":   { "type": ["string", "null"], "description": "The EDID hardware id inside deviceId (e.g. PSCCDD0). Stable across virtual-display sessions where deviceId is not." }
      }
    },
    "virtualWindow": {
      "type": "object",
      "required": ["id", "title", "monitorRole", "relX", "relY", "width", "height"],
      "additionalProperties": false,
      "description": "One WLM-owned container. Its geometry speaks the same vocabulary a saved window does -- a monitorRole plus a position relative to that monitor -- so restore does the same arithmetic for both, and a profile lands correctly on a desk whose monitors sit at different offsets.",
      "properties": {
        "id": {
          "type": "string",
          "pattern": "^vw-[0-9a-f]{8}$",
          "description": "Identifies this container so a window's dockedInto can name it. Same shape as the bayId this collection replaces: short enough to read in a profile file, wide enough that a collision inside one profile is not a real concern."
        },
        "title": { "type": "string", "description": "Shown in the container's own title bar (when chromeHidden is not set) and used to label it in the editor." },
        "monitorRole": {
          "type": ["string", "null"],
          "anyOf": [
            { "enum": ["primary", "secondary", "tertiary", "quaternary", "quinary", "senary", null] },
            { "type": "string", "pattern": "^monitor-[0-9]+$" }
          ],
          "description": "Which monitor relX/relY are measured from, in the same vocabulary a saved window's monitorRole uses. Null when the container hasn't been placed on one yet. Desks with more than six displays use monitor-N roles."
        },
        "relX": { "type": "integer", "description": "Container left edge relative to the named monitor's work area." },
        "relY": { "type": "integer", "description": "Container top edge relative to the named monitor's work area." },
        "width":  { "type": "integer", "minimum": 1 },
        "height": { "type": "integer", "minimum": 1 },
        "chromeHidden": {
          "type": "boolean",
          "description": "When true, this container's own title bar and border are hidden. Absent means the title bar shows. Only ever affects the container's own chrome. Hiding a docked window's title bar is fullDock's job, not this one's."
        },
        "fullDock": {
          "type": "boolean",
          "description": "When true, members are fully docked: each window's client area is sized to exactly its pane and shown through this container, with its own title bar and borders hidden. The real window stays on top, made transparent, so it keeps its input, and everything is put back on undock, on closing the container and on quit. Absent means members dock as ordinary windows over their panes. Experimental."
        },
        "layout": {
          "$ref": "#/$defs/layoutNode",
          "description": "The container's split tree. A docked window names its leaf in its own dockSlot rather than the tree naming the window, which keeps the tree pure geometry. Absent means the container is empty. It's reconciled against the docked windows on every write. A pane with no window, or a dockSlot naming no pane, never sits on disk for long."
        }
      }
    },
    "layoutNode": {
      "description": "One node of a Virtual Window's split tree, either a 'split' (its two children side by side or stacked) or a 'pane' (a leaf a window can dock into by naming this id in its own dockSlot). A split's a and b are each a layoutNode in turn, which is how the tree nests to more than one level.",
      "oneOf": [
        {
          "type": "object",
          "required": ["kind", "id", "dir", "ratio", "a", "b"],
          "additionalProperties": false,
          "properties": {
            "kind": { "const": "split" },
            "id": {
              "type": "string",
              "pattern": "^sp-[0-9a-f]{8}$",
              "description": "Identifies this split so a drag on its splitter can find and resize it without walking the whole tree."
            },
            "dir": {
              "type": "string",
              "enum": ["row", "col"],
              "description": "'row' lays a beside b. 'col' stacks a above b."
            },
            "ratio": {
              "type": "number",
              "minimum": 0.05,
              "maximum": 0.95,
              "description": "Child a's share of the space left after the gutter. Clamped well short of 0 or 1 so a dragged splitter can never strand a pane at zero size, which would be a pane the user has no way back to."
            },
            "a": { "$ref": "#/$defs/layoutNode" },
            "b": { "$ref": "#/$defs/layoutNode" }
          }
        },
        {
          "type": "object",
          "required": ["kind", "id"],
          "additionalProperties": false,
          "properties": {
            "kind": { "const": "pane" },
            "id": {
              "type": "string",
              "pattern": "^pn-[0-9a-f]{8}$",
              "description": "Named so a docked window's dockSlot can point at exactly this leaf rather than just at the container."
            }
          }
        }
      ]
    },
    "ownerInfo": {
      "type": ["object", "null"],
      "additionalProperties": false,
      "properties": {
        "processName": { "type": "string" },
        "className":   { "type": "string" },
        "title":       { "type": "string" }
      }
    },
    "window": {
      "type": "object",
      "required": ["processName", "title", "className", "monitorRole", "relX", "relY", "width", "height", "showCmd"],
      "additionalProperties": true,
      "properties": {
        "processName": { "type": "string", "description": "Process basename (e.g. 'chrome.exe')." },
        "displayName": { "type": "string", "description": "Pretty name from the exe's FileDescription resource (e.g. 'Google Chrome')." },
        "exePath":     { "type": "string", "description": "Absolute path to the exe at save time." },
        "title":       { "type": "string", "description": "Full window title at save time." },
        "className":   { "type": "string", "description": "Win32 window class name (e.g. 'Chrome_WidgetWin_1')." },
        "isOwned":        { "type": "boolean" },
        "isToolWindow":   { "type": "boolean" },
        "onOtherDesktop": { "type": "boolean", "description": "True when this window lived on a non-current virtual desktop at save time." },
        "desktopIndex":   { "type": "integer", "description": "Virtual desktop index. -1 when unknown." },
        "ownerInfo":      { "$ref": "#/$defs/ownerInfo", "description": "Identity of the owner top-level window, when this is an owned/tool window." },
        "monitorRole":    { "type": "string", "anyOf": [ { "enum": ["primary", "secondary", "tertiary", "quaternary", "quinary", "senary"] }, { "type": "string", "pattern": "^monitor-[0-9]+$" } ] },
        "windowDpi":      { "type": "integer", "minimum": 0, "description": "Per-window effective DPI at save time, from GetDpiForWindow. 0 = unknown." },
        "dpiAwareness": {
          "type": "integer",
          "enum": [-1, 0, 1, 2],
          "description": "-1 = unknown, 0 = unaware, 1 = system-aware, 2 = per-monitor-aware (PMA / PMv2)."
        },
        "relX":   { "type": "integer", "description": "Window left edge relative to the saved monitor's work area." },
        "relY":   { "type": "integer", "description": "Window top edge relative to the saved monitor's work area." },
        "width":  { "type": "integer", "minimum": 1 },
        "height": { "type": "integer", "minimum": 1 },
        "color": {
          "type": ["string", "null"],
          "pattern": "^#[0-9a-fA-F]{6}$",
          "description": "Optional per-window colour override, as #rrggbb. Set from the window editor's Colour control. Absent means the colour is generated from the process name, which is what the Visual tab and the windows list have always done. Validated on write because it is applied as an inline style."
        },
        "showCmd": {
          "type": "integer",
          "enum": [1, 2, 3],
          "description": "Restore-state from WINDOWPLACEMENT.showCmd. 1 = SW_SHOWNORMAL, 2 = SW_SHOWMINIMIZED, 3 = SW_SHOWMAXIMIZED."
        },
        "dockedInto": {
          "type": "string",
          "pattern": "^vw-[0-9a-f]{8}$",
          "description": "Which virtualWindows entry this window is docked into. Absent means not docked. A docked window is still saved and restored exactly like any other window -- docking only changes what the layout view shows, hiding it there in favour of its container so the two are never drawn twice. A dockedInto naming a container that no longer exists is cleared on the next write rather than left dangling."
        },
        "dockSlot": {
          "type": "string",
          "pattern": "^pn-[0-9a-f]{8}$",
          "description": "Which pane of dockedInto's layout this window fills. Absent means not docked, or docked into a container with no split tree yet. A dockSlot naming no pane in the tree is repaired on the next write with a fresh pane rather than left unreachable, the same way a dangling dockedInto is cleared."
        },
        "preDock": {
          "type": "object",
          "required": ["width", "height"],
          "additionalProperties": false,
          "description": "This window's size from just before docking imposed one, so undocking can hand it back instead of leaving it at whatever size its pane happened to be. Absent means never docked, or already handed back by an undock.",
          "properties": {
            "width":  { "type": "integer", "minimum": 1 },
            "height": { "type": "integer", "minimum": 1 }
          }
        },
        "match": {
          "type": ["object", "null"],
          "description": "How this window is recognised on the next load. Absent means the title must match exactly. Set from the window editor's 'Recognise by' control.",
          "additionalProperties": false,
          "properties": {
            "title": { "type": "string", "enum": ["prefix", "ignore"], "description": "'prefix' accepts a live title that starts with pattern; 'ignore' does not consult the title at all. Both still require the window class and kind to match." },
            "pattern": { "type": "string", "maxLength": 200, "description": "The title prefix, for 'prefix'. An empty prefix is stored as 'ignore'." }
          }
        },
        "launch": {
          "type": "object",
          "description": "How to start this window again when a profile loads without it. Written by the capture for Explorer folder windows and PowerShell, pwsh and cmd consoles, never by the editor. It is validated before anything is run: a console only ever starts as one of those three shells, and an Explorer target must be a shell folder or an existing directory.",
          "additionalProperties": false,
          "required": ["kind"],
          "properties": {
            "kind": { "type": "string", "enum": ["explorer", "console"] },
            "path": { "type": "string", "maxLength": 2000, "description": "For 'explorer', the folder. A '::{GUID}' path is a shell folder such as Home." },
            "cwd": { "type": "string", "maxLength": 2000, "description": "For 'console', the working directory it was started in. For PowerShell that is where the window started, since Set-Location doesn't change it." }
          }
        },
        "recreate": {
          "enum": [true, false, "fresh"],
          "description": "What a Load does about this window. false uses the open window only. true uses the open window and starts it when none is open. \"fresh\" always starts a new one and places that. Absent means the default: an app-wide default set in Settings answers first when one exists for this app, and otherwise it is true for Explorer folders and PowerShell, pwsh and cmd consoles, and false for everything else. An app other than those starts from exePath, which must be an absolute .exe named for processName. Set from the window editor."
        }
      }
    }
  }
}
