Skip to main content

pleamar

Build anything on the desktop with pleamar — shells (bars, docks, launchers, panels, notification centres, widgets, lock screens), apps and windows, animations and effects, and the pleamar-wm window manager (its look, layouts, key bindings, actions) — from the user's own ~/.config/pleamar. Use when asked to make, change or fix a bar, a widget, a shell, a panel, a launcher, a desktop app, a key binding, a window-manager behaviour or a theme with pleamar, or when touching any .plm or .luau file.

설치로 이동

소스 정보

저장소
k4ditano/pleamar
최근 소스 활동
2026년 9월 27일 15:13
감지된 SKILL.md 언어
영어
스타
56
포크
2

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

파일 탐색기
2 개 파일

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
pleamar
description
Build anything on the desktop with pleamar — shells (bars, docks, launchers, panels, notification centres, widgets, lock screens), apps and windows, animations and effects, and the pleamar-wm window manager (its look, layouts, key bindings, actions) — from the user's own ~/.config/pleamar. Use when asked to make, change or fix a bar, a widget, a shell, a panel, a launcher, a desktop app, a key binding, a window-manager behaviour or a theme with pleamar, or when touching any .plm or .luau file.
<!-- pleamar skill {VERSION}. Written by `pleamar --install-skill` and rewritten when pleamar updates: put your own notes in another skill. --> # Writing pleamar scenes pleamar is a desktop runtime in Rust with its own declarative language. A `.plm` file is a scene: what is drawn and how it reacts. Everything declared there is run by the **renderer** — springs, rules, layers, gestures — so it keeps moving even if the logic stalls. An optional `.luau` file next to it is the logic, and it only reports facts; it never draws. **It is not QML.** Do not carry over QtQuick names, properties or idioms. There is no `Rectangle`, no `anchors.fill`, no `Behavior`, no JavaScript in the scene. ## The rule that matters **Never guess a keyword or a property. Check it.** ```sh pleamar --grammar # every word the compiler accepts, by category pleamar --check x.plm # reads the scene, says whether it is fine, exits ``` `--grammar` prints the real vocabulary: statements, the properties of each element, functions, triggers, effects, springs, units, services and what each service reports. If a word is not in that list, it does not exist. The compiler suggests the closest one ("did you mean…?"), so a failed `--check` usually tells you the right word. After writing or editing any scene, run `--check` on it. Always. It is instant and it catches everything: unknown names, wrong types, a property that belongs to another element, a service asked for something it does not report. ## Where the answers are The documentation travels with the binary, so it always matches the pleamar that is installed: ```sh pleamar --docs reference # the reference: grammar, every element and property, rules, layers, gestures, services, files pleamar --docs guide # from zero to a bar, six steps pleamar --docs recipes # whole scenes that work: lists, grids, gradients, paths, saved settings, windows, popups pleamar --docs logic # what the .luau logic can and cannot do pleamar --docs measuring # measuring cost and animations pleamar --grammar # every word the compiler accepts ``` Read the reference before writing anything non-trivial; search it (`pleamar --docs reference | grep -n -i "shadow"`) instead of guessing. ## Where the user's things go Everything of the user's lives in **one folder, `~/.config/pleamar/`** —the one they keep in their dotfiles—. Put what you make there, never in a repository or a system path: ```text ~/.config/pleamar/ shells/NAME/NAME.plm a shell or app of theirs (+ NAME.luau for its logic, + anything it imports, relative to it) autostart what starts with the desktop, one command a line theme.plm a palette their scenes can import, if they want one session.conf pleamar-wm only: monitors, keyboard, pointer, idle keys.conf pleamar-wm only: key bindings and touchpad gestures wm/session.plm pleamar-wm only: their own window manager ``` A shell works on **any Wayland compositor** (Hyprland, Sway, niri, KDE, pleamar-wm): it is a layer-shell surface. To start it with the desktop, add `pleamar --scene ~/.config/pleamar/shells/NAME/NAME.plm --no-hud` to `autostart`; on Hyprland, `exec-once = pleamar --autostart` runs that file (lines starting with `wm:` only run in pleamar-wm's own session). A scene reloads itself when its file is saved, so the user sees each change live. ### pleamar-wm, the window manager When the user runs pleamar-wm (their session is `pleamar-wm session`, and `pleamar-wm init` makes the folder above), the window manager itself is a pleamar scene: layouts, window decorations, animations, what happens on drag. - **Key bindings** go in `keys.conf`, never in the scene: `bind Super+b launch zen-browser`, `bind Super+q minimize`, `unbind Super+t`, `gesture swipe3_down close`. Start the file with `defaults` to keep pleamar-wm's; `pleamar-wm keys` prints them with the list of actions (`close`, `minimize`, `restore_last`, `fullscreen`, `toggle_free`, `overview`, `focus_next`, `focus_previous`, `move_left/right/up/down`, `narrower`, `wider`). - **An action is an event the window manager's scene declares**, so a new behaviour is: declare `event my_action` in the scene, write `on my_action { … }`, and bind it in `keys.conf`. Anything can also fire it: `pleamar-wm --say wm "emit my_action"`. - **Changing the window manager** means their own `wm/session.plm`: start from a copy of the one that comes with it (`pleamar-wm scene > ~/.config/pleamar/wm/session.plm`) and change that; pleamar-wm uses theirs when it exists. - `pleamar-wm config` shows how it understood `session.conf`. ## The shape of a scene ``` language 0.1 // optional, first line: which language version it needs import "common/palette.plm" // libraries, relative to this file scene Name { surface { size: full, 44; anchor: top } // the window it asks for permissions { services: "clock" } // without this, the logic can do nothing service clock as now { time: text } // system services, by name fact open = false // what the logic may report text title = "…" model rows max 14 { label: text } event chosen -> // `->` means the logic hears it too prop x = 360 ~calm // something that moves: a spring let mint = #9ed6bd // a name for an expression or colour box { from: 0, 0; size: 200, 44; color: mint } // what is drawn text title { at: 10, 22; anchor: left center; size: 12; color: #f5f7f5 } zone box hit { from: 0, 0; size: 200, 44; cursor: pointer } on press hit { toggle open } // rules, run by the renderer follow x = if(open, 420, 360) // movement that carries itself } ``` ## Things that are easy to get wrong - **Ranges are exclusive at the end.** `repeat i in 1..10` gives 1 to 9. A model with `max 16` has records 0 to 15, and its logic fills `1..16` in Lua terms. - **A `let` is a name for an expression, not a variable**: a small one is written out wherever it is named. A big one is computed once a frame instead, so chaining them (`let b = a * (1 - t) + 60 * t`) is fine — but keep each link naming the one before it **once**, which is what `a * (1 - t) + k * t` does and `a + (k - a) * t` does not. - **`prop` is a spring, not a variable.** Do not set it every frame from the logic; declare where it goes (`follow`, a rule, `impulse`) and let it travel. **The spring is the property's**: `prop lid = 0 ~150ms` is what `lid: 1` uses, and a rule only travels differently if it says so (`lid: 1 ~40ms`). - **A property belongs to its element.** `corner` is a `box` thing, `radius` an `ellipse` thing, `width`/`lines` are `text` things. `--check` lists the valid ones when you miss. - **Inside a layout (`row`/`column`) a child must say how much room it takes.** Wrap loose shapes in `group { size: w, h; … }`. - **`if()` and `mix()` work with colours too**: `if(urgent, amber, mint)`, `mix(ink, mint, open)` —a comparison works as `t`—. - **Text with holes** is `text "{a} · {b}"`, where `a` and `b` are live texts or facts. For a number with decimals, `text number(expr, 2, " %")`. - **A surface does not grow with what it holds, and a shadow needs room.** `shadow: 0, 18, 44` reaches 62 px past its shape: leave it in `surface { size: … }` or the shadow is cut into a straight line. A panel that grows is the usual way to find this out: the renderer warns once, both for the shadow ("a shadow is cut: it needs 30 px below…") and for the drawing itself ("a drawing is cut: it needs 36 px below…"), once it has been cut for three seconds and never against an edge the surface is glued to. Only while it runs, though, never in `--check`: where a card ends is a sum that exists only while the scene is alive. - **Zones are what catch the mouse.** A named shape only becomes a zone if a rule names it, if it carries `active`, or if it is declared with `zone`. - **A press goes to the zone declared LAST**, not to the smallest one. So a grace zone —the big invisible rectangle that keeps a panel open while the pointer crosses a gap— goes **before** what it wraps, or it swallows every click inside it. This is the mistake that repeats: in marea-plm it happened five times, always the same way, and every time it looked like "the button does nothing". What is **hidden** does not catch it, though: a `show:` that is false, or an `opacity:` that has reached zero —on a group, on a layout— turns off the zones inside it. No need to repeat `active: open > 0.9` on every zone of a panel that fades in; keep `active:` for what is visible and still must not be pressed (a button mid-transition, a confirm that arms late). - **Two rules in the same frame: the one declared LAST sets the value, and a `while` reads the frame BEFORE them.** A pointer that jumps from one row to another gives `leave` on the old one and `enter` on the new one in the same frame, so whatever clears has to be declared **before** whatever sets, or the row just entered is cleared by the row just left. Guarding it (`while thing == what_it_set`) does not save it: the guard is reading the old value. - **A loose `clip` reaches further than it looks**: it clips everything after it until the next named `surface`, not until the end of the block it seems to belong to. Something written further down comes out clipped to a shape that may be closed —that is, it does not come out at all—. Inside a `group`, it ends with the group. - **A shadow is black unless you say otherwise** (`shadow: dx, dy, blur, alpha, colour`), and on a desktop of dark windows a black shadow has nothing to darken: it reads as a dirty ring around the thing it was meant to lift. What a small shape usually needs there is its own `rim`, which is light **inside** the silhouette and touches nothing behind it. Everything about a shadow is an expression, so it can show up only when there is something to cast one: `shadow: 0, 2 * open, 12 * open, 32% * open`. - **An svg is `figure`, not `image`.** `image` rasterises it into an atlas —a sticker: it melts into nothing, it cannot be tinted by parts nor animated by layers—. `figure hat = file "hat.svg"` reads the same file as paths, and `figure hat.brim { … }` draws one layer (the `id` of its group in the file) in its place inside the piece, so two layers drawn apart still fit together. - **Permissions are per service and listening is not commanding**: `services: "audio"` lets the logic know the volume; changing it needs `"audio.volume"` or `"audio.*"`. ## When the language falls short: your own shader Before faking an effect with a hundred shapes, write it. `shader aurora = file "aurora.wgsl"` declares one, `shader aurora { at: …; size: …; values: …; colors: … }` paints a box with it. The file has ONE function, `fn shade(s: Shader) -> vec4<f32>` —straight colour and coverage for each point—, and reads `s.pos`, `s.size`, `s.uv`, `s.time`, `s.pointer`, `s.hovered`, `s.a`/`s.b` (the eight `values:`), `s.color`/`s.color2`, and `behind(s, at)` / `behind_frosted(s, at)` for what is behind the surface. Only what it reads costs: without `s.time` it does not keep the scene painting. `--check` validates the WGSL with its line. Contract and rules: §8.1 of `pleamar --docs reference`. A `group` can also treat what it holds as one thing: `blur: 6`, `glow: 14, 90%, mint` (or without a colour, a bloom), `saturation`, `brightness`, `contrast`, `hue: 120deg`, `mask: x1, y1 to x2, y2` / `mask: radial x, y radius r1 to r2`, `mode: add | screen | multiply` (multiply only darkens what the scene painted). All animatable; §8.2. Two groups with effects cannot nest. Particles are an element: `particles { at: x, y; count: 400; life: 0.8s .. 1.6s; speed: 120 .. 220; direction: -90deg; spread: 40deg; gravity: 0, 260; size: 3, 1; colors: a, b; shape: dot | square | spark; emit: cond }` (or `burst: event`). Worked out on the card, thousands are cheap; §8.3. A `text` can have `gradient:` (like a body's), `outline: w, color`, `shadow: dx, dy, blur, alpha[, color]`, and per letter `letter_move: dx, dy`, `letter_opacity`, `letter_scale`, where `letter` is its index and `letters` the count; §8.4. An `image … = file "x.gif"` (or an animated PNG/WebP) plays by itself. For smaller things there is maths: `noise(x)`, `noise(x, y)`, `random(k)`, `sqrt`, `pow`, `fract`, `mod`, `atan2`, `length`… and `time`, the seconds since the scene started (naming it keeps the scene painting). Gesture frames take `bezier(x1, y1, x2, y2)`, CSS's cubic-bezier. ## The logic, if there is any `scene.luau` next to `scene.plm`. Sandboxed Luau, own thread, cut off if a handler runs longer than two seconds. It can only cross the boundary the scene declares: ```lua fact.open = true -- a declared fact text.title = "hello" -- a declared live text model.rows = { { label = "a" } } -- the whole list at once, atomically emit("chosen", 3) -- a declared event on("chosen", function(n) end) -- listen: events, "fact:x", "text:x", "press:zone" after(200, f) every(1000, f) -- timers run("date", {"+%H:%M"}, f) -- a command, if `permissions { run: … }` allows it sys.watch("audio", f) -- a service; sys.ask(…) asks, sys.call(…) commands ``` Everything else is a mistake: the logic does not draw, does not move properties and does not know about coordinates. ## Before saying it is done Check it, open it, and look at it: ```sh pleamar --check scene.plm # always, after every edit pleamar --scene scene.plm --seconds 8 # opens, closes by itself pleamar --scene scene.plm --record open,x # what a fact or spring is worth, frame by frame pleamar --say NAME "fact open true" # talk to a running scene (NAME: its scene name, lower case) pleamar --say NAME "get open" # ask it ``` To **see** it without taking over the user's screen, run it inside a screenless pleamar-wm and read the picture it writes: ```sh echo "pleamar --scene $PWD/scene.plm --no-hud" > /tmp/autostart PLEAMAR_WM_AUTOSTART=/tmp/autostart PLEAMAR_HEADLESS_AT=4 PLEAMAR_HEADLESS_PNG=/tmp/look.png \ pleamar-wm headless --seconds 6 ``` `PLEAMAR_HEADLESS_INPUT="900,40@2000 down@2500 up@2600 key:Super+q:16@3000"` adds a mouse and keys there. Never drive the real mouse or keyboard. Do not say an animation lasts what it was asked to last without measuring it (`--record`, and `pleamar --docs measuring`).
GitHub에서 보기