thomasadam/cow
3
13
Fork
You've already forked cow
4
Page: Scripting
1 Scripting
Thomas Adam edited this page 2026年05月28日 17:52:42 +01:00

Some Thoughts on Scripting

FVWM has always had a Domain Specific Language (DSL) to describe its config and behaviour. Plenty of other projects have something similar to a lesser or greater extent, even down to providing a configuration file is a DSL to some extent.

CoW is making use of tmux(1)'s command parsing as a basis for its own commands. This means different commands in Cow can have their own options, and indeed, this is what tmux(1) itself does.

tmux(1) takes this a stage further by also embedding a RPN (Reverse Polish Notation) means to provide scripting capabilities -- a very tight-knit DSL which anyone using tmux has used implicitly, or has used explicitly to some degree.

This is actually very powerful and allows for a lot of customisations, but it is still DSL one has to learn and use if one wishes to customise tmux to any great extent.

That said, this DSL hasn't stopped various frameworks and bindings from popping up, such as:

So it's possible to still compose commands specific to a DSL, even if it ends up being a compositor of string commands -- which in the case of tmux, it will be.

So DSLs, even if they don't expose a fine-grained API, can still allow a language to bind to various aspects of the DSL to to expose that in a programmatic way.

What Does CoW Need?

I'm a firm believer in DSLs that are easy to understand, and can be controlled via scripts (in any language). This might mean the scripting engine (programming language) providing its own APIs on top of the DSL, that's OK.

This is not a weakness but a feature, whereby it allows language-bindings to behave however they need to.

CoW needs a means of presenting concrete types which the user can iterate on. These include:

  • Outputs
  • Desks
  • Windows

In doing so, such a type should therefore expose properties which can be used.

Iteration and Filtering

Being able to iterate over items and at the same time filter them based on conditions allows for target window(s) to be selected for a specific command.

Conditionals

if would be helpful for conditional branching.


Prototype

On the ta/dsl branch in the CoW repository, lives the code which has prototype code to bring the above considerations to life.

Here's an example:

script {
 for w in (windows where w.focused != true and w,iconified != true) {
 winops -t '#{w.id}' -i
 }
}

In this example the script keyword starts a scope of what will be a DSL operation. Scripts do not have names, and a re-form, instead they can be attached to bindings, or rules, for example.

w is the object which is exposed based on the filter: (windows where w.focused != true and w,iconified != true). Here, windows are filtered based on if they're not focused and not iconified.

Then, for all windows which match those conditions, the command winops -t '#{w.id}' -i is run, which iconfies those windows.

It's important to ensure that -t #{w.id} so that the correct window is used by the winops command. Note as well that '#{...}' performs the expansion of that window ID.

Here's another example:

rule -g -n top-tint -T on-layer-change script {
 if self.layer >=8
 set titlebar.active_colour 0xC04030
 } else {
 set titlebar.active_colour 0x303030
 }
 }

This script is bound to a rule for when a window changes layer, and will check if the window is in a layer greater than or equal to 8, and apply a titlebar colour of one colour, and a different colour for all other windows.

Note here, that self is referencing the window affected by the rule command.

The following attributes are exposed...

Window Attributes

Attrib Meaning
id The River ID of the window
app_id The applicatioon ID of the window
title The window title
desk The desk the window is on
output The output the window is on
layer The layer the window is in

Desk Attributes

Attrib Meaning
nr The desk number
output The output the desk is attached to
current Whether the desk is the one being used
window_count How many windows are on that desk

Output Attributes

Attrib Meaning
name The name of the output
number The assigned number the output has
current True if the output is the current one
current_desk The number of the desk which is displayed on the output

Implementation Details

Bison/Flex can define the grammar of the script command. Augmenting the current parser to support this would be difficult.

Each grammar definition can convert to an AST which defines things such as:

  • Blocks
  • Let
  • For
  • If
  • Not / And / Or
  • Command

Scopes are therefore their own and create children.

Three structures during run:

struct dsl_env { name  dsl_value*, parent }; // lexical scope chain
struct dsl_eval_ctx { cmd_q*, dsl_error*, failed, implicit_target_id };
struct dsl_ast { root, arena };

Walk:

  • DSL_N_LET adds to current scope.
  • DSL_N_FOR snapshots source, optionally filters via the where expression evaluated per element, pushes a child scope binding the loop variable, recurses into the body.
  • DSL_N_IF evaluates condition, recurses into the chosen branch.
  • DSL_N_CMD builds argv (raw pieces verbatim, interp pieces evaluated and stringified), parses via cow's existing cmd_parse, wraps in a cmd_list, and cmdq_appends onto the calling cmd_q.
  • Expressions evaluate recursively; comparisons require matching types (cross-type compare = eval error). Boolean ops short-circuit. Field access (a.b.c) walks records; missing field is an eval error.

Errors carry a line:column from the originating AST node and abort the script (skip remaining statements) but don't abort the cmd_q.