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.

Informations de source

Dépôt
k4ditano/pleamar
Dernière activité de la source
25 septembre 2026 à 22:55
Langue détectée de SKILL.md
anglais
Étoiles
56
Forks
2

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Explorateur de fichiers
2 fichiers

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
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`.
Voir sur GitHub