knstrktr (as in “constructor,” but with Slavic vibes and harder to read; or as “constrictor,” because it’s a fun coincidence) is a lightweight UI construction tool. The goal is to make creation of simple UIs as easy as possible. Especially so—for shell command wrapper UIs. Thus knstrktr’s extensive shell integration.
knstrktr is made to be portable across front-ends (HTML, GTK, Qt, ImGUI, X11, Plan9—you pick!) That’s why it’s important to have a detailed specification for its commands and elements. This document is such a specification.
Don’t expect too exact of a formulation from this specification. Contribute a fix or report a problem when you see one!
knstrktr is inspired by .INI files, Gemtext, and mould by Alexander Cobleigh, thanks to them!
The most representative knstrktr program introducing the most concepts is:
#!knstrktr -f
@title Hello!
@section #greetings Let me greet yourself!
@input #name Your name
@button #greet command='echo "Hello ${name}!"' output=k
Unpacking this:
knstrktr commands.
#) and stretch until the end of the line. Convenient for shell interop.
Out of these all lines, one can leave just @input and @button.
knstrktr tries to be minimalist and get out programmer’s way, not requiring more input than necessary.
Programmer should be as merciful to the user of the UI (likely themselves) and not clutter it with too many elements.
knstrktr is an interactive tool. Meaning: REPL is non-negotiable. All implementations of knstrktr must include a REPL allowing one to construct UIs interactively. And all implementations should start in REPL, whether right in user’s terminal or in a separate window.
When called with a script file (-f,) knstrktr implementation must open the REPL after evaluating the script.
So that the user can amend the resulting UI however they want.
knstrktr can be driven from a REPL, but it can also be driven by scripts.
Scripts are .kns files containing commands and elements.
Every implementation of knstrktr should accept scripts via:
cat script.kns | knstrktr
knstrktr < script.kns
-f CLI option: knstrktr -f script.kns
While driving knstrktr via scripts is less optimal than REPL, it might be useful. For people preferring compiled code or production deployments. I’ll leave out my judgment of such people.
knstrktr can be configured via a number of files. Most of them are knstrktr scripts themselves. Which means that they can abandon their initial purpose and run any command user deems necessary.
All configuration files are located in OS-specific configuration locations.
On XDG-compliant systems, this is $HOME/.config/knstrktr/
This file should include @style commands defining initial element styling. All implementations supporting styles should read this file and apply its commands at startup (see below for the exact startup order.)
Example style.kns for Hot Dog Stand vibes:
@style root background-color=red color=white @style section background-color=yellow color=black border-color=black border-width=1px @style button background-color=#c6c6c6 color=black border-color=black @style file background-color=#c6c6c6 color=black border-color=black @style select background-color=#c6c6c6 color=black border-color=black @style input background-color=black color=white border-color=white @style password background-color=black color=white border-color=white @style multiline background-color=black color=white border-color=white @style date background-color=black color=white border-color=white @style number background-color=black color=white border-color=white @style checkbox color=black
Elements and commands from this file are implied to concern the header of the UI. Implementations should read this file at startup (see below) and prepend its contents to the UI that user / script runs. Elements from header.kns should be deletable with @clear and @delete.
A simple custom header could be:
@image #header-image header.png A welcome message for delightful UI use! @code #header-date !date
Another header for quick shell evaluation could be:
@input #command Shell command
@button #run command='sh -c "${command}"' output=t Run!
There’s no matching footer.kns, and there won’t be. knstrktr GUIs are implied to grow downwards, with the most recent element being the last. Nothing should occupy the bottom of the screen but the elements user added.
These files are ran at startup before / between / after styles.kns and header.kns. They can contain arbitrary commands and elements the user considers necessary for startup. The order is:
It is suggested that commands go into 0–1 and elements go into 8–9, after header.kns. Future (unlikely) configuration files go between 2—7. Which restricts the number of possible named files to 4 (style, header, and 2 future files.)
Any other file names are not accepted. For simplicity of implementation and radical anti-bloat position. Implementations are not recommended to extend this list of files.
knstrktr is a programming system. Which, unfortunately, means: it has input syntax. The syntax is simple though: there are freeform text, comments, shell passthrough, and special lines (commands and elements.)
Commands and elements
are lines prefixed with an at-sign (@) followed by command / element name and arguments.
Commands perform actions, elements create UI parts.
The choice of hiding them behind the same character might be a mistake.
Comments are lines starting with a hash sign (#).
They don’t influence the UI in any way and are intended for the fellow knstrktr reader to learn from.
# Why should a sequence of words be anything but a pleasure? ― Gertrude Stein
Shell passthroughs are lines prefixed with a bang (!)
They pass their text to an underlying shell and print output.
It’s intended that shell passthroughs are used for experimentation with commands without leaving knstrktr.
Because you shall not leave knstrktr.
!date Thu May 14 16:17:05 +04 2026
It is possible to use knstrktr’s special shell command syntax with input values. However, it is not guaranteed that inserted input values will be consistent with the current UI state or provided at all.
Freeform text is anything that isn’t the other three. Like this last sentence or “Later in their living they liked it that they had had such a mixing of being rich and poor, together, in them.” (go read The Making of Americans!)
Freeform text is equivalent to @text command. In case you want to start a text with an at-sign reserved for commands and elements, just use @text.
@text @-prefixed text
In many (almost all of them really!) elements and commands of knstrktr, it is allowed to pass bang-prefixed (!) strings to attributes and descriptive text.
These bangs execute shell commands and replace values with outputs of these commands.
So these look like shell commands.
Use your favorite tools and pipes and preprocessors—knstrktr will be happy to consume that!
In addition to regular shell syntax, it can interpolate values of inputs (only from the same section!) and variables into the command using a ${...} syntax.
The full syntax for this is:
${var|separator}
var can be, in the order of priority:
“multi-value var” in separator and indexing interpolation above means either an input with multiple values; a @multiline with multiple lines; or environment variable separated by colons.
Implementations are not required to make this into a fully recursive construct with indices nested in ternaries and stuff. One level is enough.
Idioms considered but neither included nor planned to keep the language minimal:
${var}${var?:fallback}
pattern-s and min / max.
sed or whatever.
Some inputs include a pattern attribute, with regex validating / restricting the values. The regex dialect choice is up to implementors, but Perl/JavaScript-compatible regex syntax is recommended.
Commands (generally) don’t create new elements, working on a whole GUI level or existing elements instead.
Quits knstrktr and closes the created UI (whenever open.)
Don’t @quit—just hack up a new UI alongside the old one!
Stops accepting new elements and most commands. Still accepts inputs and runs shell commands. A kind of “production mode” for GUI distribution, if you wish.
It is not recommended to use @freeze.
User is supposed to be able to override something in the running GUI.
knstrktr respects the user and their autonomy.
Make the current / last section repeat on each reload. Preserves the previous state of the section. Appends a copy of the section to the end of UI every time a section is activated.
Note that @loop does not @freeze the UI.
New elements can be added and existing elements deleted to the current instance of the last section.
@title Shell
@section prompt Your friendly shell prompt
@input command placeholder="rm -rf /" Command to run
@button run command="sh -c '${command}'" output=t Run!
@loop
Removes all the elements from the GUI. Does not cancel effects of other commands, like @style or @title.
Set UI title from freeform title.
For actual GUIs, like GTK, sets window title.
For HTML, sets page <title>.
Meaning for other implementations unspecified.
title-text (like most other text arguments) can start with a bang (!) to fetch text from a shell commands instead of literal text.
Open the UI in its current state in the most sensible program. Some implementations can ignore the program and open the UI in their preferred way, like a separate window or terminal tab.
Notice that starting knstrktr implementation (especially in REPL mode) should always open UI at startup. So that the changes can be easily previewed right after inputting commands or elements.
Outputs all the currently existing elements and metadata (like @title and @style.) Either to a file or to the REPL. The dumped format should be then suitable as input to another knstrktr instance. And reproduce the current UI faithfully.
Whether the current state of inputs is dumped is up to implementation.
Delete the element with ID id. The element might be situated anywhere in the UI. The element might be a section, in which case it’s removed together with all its child elements.
id can be a bang-prefixed (and likely quoted) shell command, in which case element ID is taken from this command output.
Set variables that can be reused in commands later on. Potentially, these variables can be implementation-specific. And used for e.g. styling or special behavior dispatch.
Variable with empty value (name= or name='') effectively un-sets the variable and makes it false for ternary interpolation.
Change style for a given element type (see below for element types.) Attributes are added to element-specific stylesheet incrementally. Which means that
@style command is invoked.
Implementations are free to add their own initial styles.
@style multiple times for the same element type retains the old styles whenever not modified by @style.
Removal of style attributes is not supported, only overwrites.
Suggested attributes:
It is recommended that implementations support full CSS color syntax for color attributes. And all CSS sizing syntaxes for width / radius attributes.
@style button background-color=#feddf4 color=#2e0415 border-width=0.1em border-color=#E4007C border-radius=0.2em
Attribute values can start with a bang. In such case attribute values are taken from the bang-prefixed command output. Implementations are free to re-run these commands as often as they wish to. Including: once.
@style root background-color="!bgcolor-of-the-day" color="!color-of-the-day"
In section styling @style behaves peculiarly:
This is inconsistent and might be fixed. Or might not 🤷
A special command @style root sets style for UI canvas all the elements appear on.
In case some elements inherit non-overriden styles from it, they change too.
For example, text color might depend on the color of root.
This is not a hard requirement and implementations are free to follow their own styling heuristics.
Load the new elements and commands from file-or-command. In case file-or-command is a raw string, interpret it as file and read commands from there. In case file-or-command is a bang-prefixed command, read its output and interpret that.
Most of the elements have a (preferably) unique ID.
Usually following element name.
They can be @delete-d, outputted to, and otherwise acted on using this ID.
ID-leading hash is optional in both cases.
Most elements also contain free-form text in some form or another. This text can be literal or be a bang-prefixed shell command (even in input labels!) In case it is one, it’s recomputed every time the element is rendered.
You will probably be able to see that most of these were scraped from HTML forms. Because forms are nice and are a good operational model. Additions of elements not present in HTML forms are welcome, though!
Creates a text label, equivalent to an HTML paragraph.
It is recommended that implementations “collapse” the subsequent @text elements together into one paragraph.
@text and ID can be safely omitted and still interpreted as a text label.
So any freeform text not prefixed by an at-sign is a @text element.
With the downside being: it cannot be deleted due to absence of ID.
It is not recommended to support any form of markup in @text and @code.
Because it’s noisy and distracting.
And not minimalist at all.
Care about the users, not the inner DeSigNEr.
Blank line or @text without value means paragraph / block break.
Any text “collapsing” stops at this line.
Piece of text set in monospace, likely computer code (in lang.)
But, a Gemini / Gemtext practice shows, it’s likely to be exploited to display ASCII art, maps, or ASCII-fied PNGs.
(Use @image instead!)
But who am I to judge.
Subsequent @code elements can be collapsed into code blocks.
Blank lines and other elements break these sequences too.
For HTML-supporting implementations only: a way to inject a line of literal HTML into the UI. Can contain bangs. Not recommended for use.
Creates a label-ed hyperlink pointing to destination.
Clicking this link should open the destination in a suitable program.
Which means: gemini:// etc. links are supported as long as there’s a program on your machine to open these.
Create an image from source with alt text alt.
Both source and alt can be bangs, though I have no idea why you might need that. Do not use LLMs to get alt text for images, please. They’re bad at it and hurt the environment.
Create an audio player widget playing audio from source.
Create a video player widget playing video from source.
Invokes an action expressed by command or @section command when not set.
output specifies how to interpret the result of command execution:
@text or @code element
Plain input field for freeform text. Input can be restricted by pattern regex and min and max length checks. value is used to set the default / example value, while placeholder is used to suggest the shape / purpose of the input. Not the other way around!
list, when provided, should be an existing file name or a bang-prefixed command. Lines from these should be used as suggestions in the created input field.
label, as mentioned here and in many elements below, is a descriptive string displayed alongside the element.
required mandates that the element must be filled in, and won’t run commands otherwise.
Creates a label-ed checkbox. It’s unchecked by default, but can be checked.
Input allowing to pick date and time.
Refer to HTML <input type=date> documentation for date format to follow.
Allow choosing (and possibly sending) a file from a local filesystem.
accept takes a unique file type specifier or a close approximation of that.
multiple allows to choose multiple files.
Allows entering text hidden from the prying eyes. Restrictions the same as in @input.
This element allows inputting numbers matching certain requirements (min, min, step.) With suggested values from list (file or bang.)
I might be implemented as a text input, a dial, a slider, or a combination of these. To implementation’s discretion. Same concerns other input elements: can all be text boxes if you wish.
Multiline text input. Restrictions the same as in @input.
@select allows picking one or multiple of the options.
Option values are represented as key-s, with their visual display as display.
In case the label is a bang, the output of the command—whenever matching—can be interpreted as attributes + label.
This is to allow dynamic generation of select options.
Other inputs have this too, with bang attributes.
Just that @select needs a special treatment due to its multi-option nature.
@section-s group other elements.
Element belong to either one section or none (before any section was introduced.)
It is recommended all elements belong to section in a knstrktr UI, for easier handling and composition.
Sections might be rendered as tabs, or delimited areas on one canvas, or something else altogether. Clay tablets with widgets left in random places around Paris work too.
Sections are not nestable.
Creation of new @section closes the previous one.
It is impossible to add new elements to previous sections, only @delete.
command is invoked when some of the elements in the section has no command itself, but initiates execution. Happens when HTML form gets submitted by pressing Enter inside a text input. Or by clicking a plain command-less @button, for example. In this case, output is interpreted like in button output format description.
Because what use is this knowledge if you don’t want to scratch an itch?