Skip to main content

pleamar

Write or edit pleamar scenes (.plm files) and their Luau logic (.luau) — bars, launchers, panels, notifications, tray, popups, windows. Use when touching any .plm file, when building a desktop shell with pleamar, or when asked about the pleamar language, its services, springs, layers, gestures, components or permissions.

Zur Installation springen

Quellinformationen

Repository
k4ditano/pleamar
Letzte Quellaktivität
25. September 2026 um 22:55
Erkannte Sprache von SKILL.md
Englisch
Sterne
56
Forks
2

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

Datei-Explorer
2 Dateien

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
pleamar
description
Write or edit pleamar scenes (.plm files) and their Luau logic (.luau) — bars, launchers, panels, notifications, tray, popups, windows. Use when touching any .plm file, when building a desktop shell with pleamar, or when asked about the pleamar language, its services, springs, layers, gestures, components or permissions.
# 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 | | | | --- | --- | | `docs/11-language-reference.md` | The reference. Grammar, every element, every property, types, rules, layers, gestures, services, files. Read this before writing anything non-trivial | | `docs/guide.md` | From zero to a bar, six steps | | `docs/recipes.md` | Whole scenes that work: long lists, grids, melting shapes, gradients, paths, saved settings, normal windows, popups | | `docs/10-luau-logic.md` | What the `.luau` logic can and cannot do | | `examples/*.plm` | Real scenes that run: `bar.plm` (a bar), `long-list.plm` (5000 rows), `icons.plm` (tray with menus), `launcher.plm` (launcher), `paths.plm` (paths and gradients) | | `tests/*.plm` | One file per language feature, each with the expected result on its first line | ## 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 the reference; working examples in `examples/effects.plm` and `examples/shaders/`. 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. ## How to try a scene without bothering anyone ```sh pleamar --check x.plm # no window: just says if it is fine pleamar --scene x.plm --seconds 8 # closes itself after 8 seconds pleamar --scene x.plm --screen HDMI-A-1 # on a chosen monitor pleamar --scene x.plm --mouse "160,20@800 click@1400 up@1500" # a pretend mouse pleamar --say x "fact open true" # talk to a running scene pleamar --say x "emit arrives" # …or fire one of its events pleamar --scene x.plm --stall 2000 # stall the logic on purpose pleamar --scene x.plm --record open,card # what those are worth on every frame pleamar --scene x.plm --reduced-motion # springs settle, nothing loops PLEAMAR_DEBUG_ZONES=1 pleamar --scene x.plm --mouse "…" # which zones are under the pointer, as it moves ``` **Do not say an animation lasts what it was asked to last without measuring it.** `--record name,other > log.tsv` prints `ms<tab>value…` once per frame, and that is how a duration is checked against its contract. It says at the start which names it could not find, with the ones that look like them: inside a component's copy they carry their mark (`px#Hat2`). How to read those logs, in `measuring.md`. A press that does nothing is almost always a zone that is not the one on top, or a rule that names another zone than the one under the pointer: `PLEAMAR_DEBUG_ZONES=1` prints the zones under the pointer every time that changes, with their real names (`drop.4#screen0` in a copy per monitor). Never drive the real mouse or keyboard to test a scene: `--mouse` exists for that, and `--say` reaches anything the logic can hear. ## Measuring it against Quickshell If the job is about cost — CPU, memory, frames, how long it takes to appear — read `measuring.md` next to this file. It has the commands, what each number means, how to restart someone's bar without losing it, and the traps that make a comparison worthless. ## Before saying it is done ```sh ./run-tests.sh # every test scene, plus the examples inside the documentation ./portable.sh # still builds for Windows and macOS ``` If the language changed, `run-tests.sh` also checks that the reference's vocabulary still matches `--grammar` and that the highlighters in `editor/` are the current ones. Regenerate them with `pleamar --highlight vim|vscode`.
Auf GitHub ansehen