Closed anantnrg closed 1 year ago
I'll put my vote down for lua.
Nickel is great, but it's only useful for producing a static configuration. There's no runtime coming with it, so it can't help implement dynamic behavior. Something like lua could enable very handy stuff like this:
bindings = {
{
{"CTRL", "SHIFT", "Q"},
function()
for window in strata.current_workspace.get_windows() do
window.close()
done
end,
)
}
},
animations = {
window_in = function()
-- My custom bezier curve
end
}
Not only does it let the user implement any behavior very easily without resorting to external scripts and remote cli utilities (as is used for every other WM I've used), it also allows you to distribute some builtin behaviors as a library directly in lua (as a library) by using the same API, and these functions can be implemented both in rust and lua.
I'm sure Haskell could manage the same thing but it's not nearly as popular and much less easy to learn. It also has to be compiled, if I'm not mistaken, and can't really challenge lua as far as performance goes.
I'll go even further and say that lua could be the lone language of configuration for the WM. If you don't use any fancy construct, a simple configuration file full of options could look like this:
local strata = require("strata") -- You could even make this line optional
return {
vsync = true,
shadows = true,
borders = {
color = "red",
thickness = 2,
},
keybindings = {
{ { "SUPER", "LEFT" }, strata.builtin.move_window_left }
},
}
It honestly doesn't look any more complex than any random yaml configuration to me, and keeps users able to copy/paste any snippet for complex stuff they might find online without having to change configuration formats.
For inspiration, you can look at how wezterm handles this. As an example, here's how my own configuration file looks like: https://github.com/calops/nix/blob/main/programs/gui/wezterm/config.lua
Looks great and the syntax is also pretty good. I'll look into implementing it soon after I fix some critical bugs. Also, I want your opinion about the whole modularity thing. Is it something that you're interested in?
Good suggestions tho. Cheers :)
Modularity sounds interesting to me, but what do you have in mind exactly? Something like plugins? If so, yes definitely. It would make it much easier to work with plugins compared to something like hyprland, although I'm not sure it'd be possible to achieve low-level graphical plugins that way.
It does require a somewhat stable API though, but I guess that's not really a concern in the early stages of the project.
Sure, plugins sound great. What I meant by 'modularity' is the current design principle, i.e.,
╭───────────────╮ ╭───────────╮ ╭──────────╮
│ Hotkey Daemon │ ────> │ StrataCTL │ ────> │ StrataWM │
╰───────────────╯ ╰───────────╯ ╰──────────╯
Or would doing everything inside the WM itself be better?
I'm not really sure. I know this kind of modularity is the traditional way of doing things, but it's probably a bit limited for "analog" input. For example, you want the WM itself to be handling touchpad events, otherwise you can't expect smooth scrolling and gestures.
A hotkey deamon is so lightweight anyway, it doesn't strike me as superfluous if you embed it in the WM itself, even if it's not used by all users. As for the CTL utility, that can just be the role of the lua runtime itself, with the freedom afterwards to implement a CLI interface on top of it for integration with simple tools. You will probably want additional ways to integrate for full functionality though, such as pipes (for example, to enable reactive buttons in bars).
You're right. I've decided to scrap the whole hotkey daemon thing and just implement everything in the WM. Mostly because I'm having errors communicating with WM and calling functions, and I'm too lazy to make it work.
Also I started working on implementing Lua and getting rid of TOML. Decided to use mlua
which seems to be the one used by Wezterm and is pretty good. Still tryna understand and wrap my head around the whole syntax thing. Pretty new stuff to me even though I've configured NeoVim a lot. Will update once I make a base implementation. Cheers :)
Hey @calops, I created a more advanced config. Could you let me know what you think
local strata = require("strata")
return {
autostart {
"kitty --tile Terminal",
"kagi"
},
general {
workspaces = 1,
gaps_in = 8,
gaps_out = 12,
kb_repeat = {500, 250}
},
decorations {
border {
width = 2,
active = "#FFF",
inactive = "#131418",
radius = 5,
},
window {
opacity = 0.9
},
blur {
enabled = true,
size = 2,
passes = 3,
optimize = true,
},
shadow {
enabled = true,
size = 2,
blur = 3,
color = "#FFF"
}
},
tiling {
layout = "dwindle"
},
animations {
enabled = true,
},
rules {
workspaces {
{
workspace = 1,
class_name = "kitty"
},
{
workspace = 2,
class_name = "Brave-browser"
}
},
floating {
{
class_name = "pavucontrol"
}
}
},
bindings {
{
{"CTRL", "SHIFT", "Q"},
function()
for window in strata.current_workspace.get_windows()
do
window.close()
end
end,
},
{
{"WIN", "RETURN"},
strata.spawn("kitty --title Terminal");
}
}
}
I'm currently trying to parse this. mlua
's documentation is a bit sparse so kinda tricky. I found rlua
which seems to have a bit better documentation. Just experimenting with stuff rn. If you have any resources that's worth checking out, please do let me know. Cheers :)
Awesome! This looks great. I'll be sure to play with it this week and report back.
Some preliminary thoughts:
You're probably looking for a syntax that looks like this instead:
return {
autostart = {
"kitty --tile Terminal",
"kagi"
},
general = {
workspaces = 1,
gaps_in = 8,
gaps_out = 12,
kb_repeat = {500, 250}
},
}
Otherwise, autostart { ... }
and general { ... }
are function calls, which would need to be brought in scope just for this (and also, it looks a bit weird among the rest of the lua conf).
I know you took it from my example before, but there's probably better to be done for the bindings. Something more verbose (which for configurations means more intuitive) like:
bindings = {
{
keys = {"CTRL", "SHIFT", "Q"},
action = function()
for window in strata.current_workspace.get_windows()
do
window.close()
end
end,
},
}
I think it's the best way to go about this, especially since there will probably be more (optional) arguments later. This makes sure that nothing is ambiguous.
To be discussed as I'm not sure how annoying this can be, but the consensus everywhere I've looked for spawning programs seems to be to take arguments as a list, rather than a full-string command line. Something like {"kitty", "--title", "Terminal"}
instead of "kitty --title Terminal
}. This makes it easier to provide multi-words arguments (which, especially for this example, are common).
However, the magic of lua means we don't have to choose! You can accept both a string or a list of strings, and adjust the behavior with simple introspection. But since the backend libs that will spawn processes take lists of arguments anyway, I advise to first implement my suggestion and the string one on top of it.
About this:
rules {
workspaces {
{
workspace = 1,
class_name = "kitty"
},
{
workspace = 2,
class_name = "Brave-browser"
}
},
floating {
{
class_name = "pavucontrol"
}
}
},
I get the idea and that's pretty much how it's done in other WMs, but maybe there are improvements that could be made here thanks to lua? First, I would reverse the logic, which lets us create hooks for pretty much anything. Something like this (I tried to come up with simple use-cases that could actually make sense):
rules = {
-- Always bind firefox to the workspace 1, before the window is opened
{
triggers = { event = "win_open_pre", class_name = "firefox" },
action = function(window) window.send_to_workspace(1) end,
},
-- A single action that can be ran from multiple triggers.
{
triggers = {
-- Set mpv to floating, always
{ event = "win_open_pre", class_name = "mpv" },
-- Set terminals to floating only if on the workspace 1 (where we put firefox previously)
{ event = "win_open_pre", workspace = 1, class_name = { "kitty", "wezterm" },
}
action = function(window) window.set_floating() end,
},
-- Here comes the nice stuff: helper functions! This one does the same thing as the first rule
strata.rules.bind_to_workspace(1, "firefox"),
-- Nothing preventing us from some more sugar:
strata.rules.bind_to_workspace {
{ 1, "firefox" },
{ 10, "slack" },
},
-- This does the same thing as the first part of the second rule
strata.rules.set_floating("mpv"),
}
I think we should consider a more imperative approach for all of this, rather than the declarative way of returning a dict that serves as the whole configuration.
-- this:
return {
rules = {
...
}
}
-- Could instead be done this way:
local strata = require("strata")
strata.create_rules {
...
}
It doesn't change much for a simple config file, but it's much more powerful when you consider plugins. It leverages the fact that you actually have a runtime, which means configuration can be altered in real time during the WM's life. It's basically the exact same approach as neovim: provide API functions and let people configure whatever they want with this. This means a plugin (or even the user directly) could create and delete rules depending on arbitrary things, making it able to deeply extend the WM's functionality very easily.
I think there's actually a lot of inspiration to take from neovim here, and some thinking to do to see what should stick and what should be improved. The concept of autocmds is pretty much the same thing as rules in strata, but it also comes with the concept of augroups, which I think is a useful addition if you want to delete or toggle a bunch of rules at once.
Anyway, this is all a bunch of complexity that can be added later on without necessarily breaking (just deprecating) the first draft. I'm mostly brainstorming with myself right now. It's also likely that I'm getting overeager and this is all overkill for a WM, who knows. I still really like point 4.
though.
These are really great suggestions! Lua is pretty new stuff to me and parsing it in Rust is even newer. I'm just researching on how its parsed. The docs for mlua
and rlua
are a bit sparse, so I'm looking at projects that use them and then reverse engineer it. Wezterm seems to do a fair bit of processing and some advanced stuff. I also have my exams coming up next week so I have study a lot. I'll look into tho. Cheers :)
Hey @calops, I've been thinking about a new way of doing stuff. I came across KDL, and its a pretty human readable language. I though of having a design where all the static config is done in KDL and then the scriptable parts are done in Lua, kinda like Wezterm does it, instead KDL for TOML. This way, it'll also be a bit easier and not overwhelming for new users when they just get started. I thought of having a directory structure like this:
strata
|- config.kdl
|- scripts
|- custom_layout.lua
|- window_function.lua
This way, its the best of both worlds imo. Also I've been pulling my hair out for the past day or so trying to figure out how to parse the desired Lua config and this way, I think it'll be easier for us devs and the users. Do let me know what you think!
Cheers :)
KDL is great and I understand the motivations, but I'm still iffy about having two different ways to achieve some things. It hurts discoverability, and adds unnecessary friction when somebody suddenly wants to do something that requires to move to lua.
I think there are other ways to make it not overwhelming for newcomers:
Anyway, as the project is young, I suggest going with the more powerful option (lua) first, and then later implement stuff to make things easier for users.
Also I've been pulling my hair out for the past day or so trying to figure out how to parse the desired Lua config
I'm a bit confused by this, as I don't think there's any parsing to be done at all. This should be done by mlua entirely, you only need to provide a context (the strata
module) and then load a lua file to let the runtime interpret it.
Oh okay, all this stuff is a bit new to me since I haven't played around with Lua that much and only started taking it seriously after I worked with this project. You're right, implementing too many languages has its draw backs and the suggestions you provided are really great. I'll work on it today and hopefully make a simple practical implementation.
Cheers :)
Hey @calops, I'm also a bit confused by this (lol, recursion):
I don't think there's any parsing to be done at all. This should be done by mlua entirely, you only need to provide a context (the
strata
module) and then load a lua file to let the runtime interpret it.
(I haven't done anything like this before so please bear with me)
So where I come from, when you parse a TOML file, you just store it to a variable and access the data. Now since Lua obviously doesn't work in the exact same way, I'm a bit confused as to how we access variables and at the same time run functions. For example, what I would do to handle keybindings previously, was to get all the keybinds, their keys and commands and then when any key is pressed, check if the pressed key(s) match a certain keybind and if they do, then just spawn a new child process with the command. Now with Lua and its functions and stuff, I'm assuming that we'd have to get the keybinds, watch for the key presses and if any keypresses match the bindings, then call back to the Lua runtime for the function to be executed.
Also, since this config is used pretty much everywhere, I previously stored it in a once_cell
instance so that it can be accessed in all the files. Now I'm not exactly sure how to implement that.
It would be great if you could shine some light on this. Cheers :)
What you're doing with lua is actually embedding a runtime within your program. What this means is that this runtime will be executing stuff independently, and you get to provide it with functions that can be called from within (the strata
module that you can get with require()
), as well as the ability to call functions within it yourself (you shouldn't have any need for this, but who know).
Disclaimer: I haven't used mlua much, so I'm far from an expert, but this is what I understand.
For configuration, there are two ways to go about it:
struct
by deserializing the result. I'm sure mlua offers plenty of utilities to do this. If there are lua functions in this dict, you should be able to keep calling them and it'll be hooked into the runtime by mlua.strata
module that will directly call rust code that will update a global state for the WM. Like before, you get the arguments passed to the function as rust structs deserialized by mlua (and I'm sure serde plays a role).Both approaches should have roughly the same level of complexity, I think.
Also, since this config is used pretty much everywhere, I previously stored it in a once_cell instance so that it can be accessed in all the files. Now I'm not exactly sure how to implement that.
First of all, I'm not sure once_cell
is the way forward, since you will probably want to be able to perform hot reloads of the configuration whenever the user changes it (this is very handy when tweaking things without having to force reload everything). But you can use some other synchronization method and it'll keep working just fine, so let's not focus on this (although I strongly recommend putting an abstraction over this so that you're free to change the synchronization method without changing all the call sites too much).
Virtually, you can keep doing it that way with both methods:
once_cell
method.strata
module can access this global state exactly the same as the rest of your code. However, they would need to be able to modify it as well, so this method disquilifies once_cell
right away.Hey, FWIW I'm gonna have some time to work on random stuff soon, and I'd love to help implement this (or other things in the WM). Where are you at on this?
I created a separate repo so that I can work on it a bit more easily. Its here. Currently, I've been able to execute the script and get the config tables. I changed the config quite a bit, more like what your suggested (or so I think). From what I've seen from one of the mlua
examples for serializing data, the goto way seems to be just parsing them as JSON and then destructuring that data into a struct. This works for the config
table where all the values are constant but for the bindings
table, you can't parse a LuaFunction
into a JSON struct. That's where I'm at currently. Also my term exams start on 16th so I have to study and I probably won't get much time to work on this. I can start working on it after the 25th. If you could make any changes and create a pull request, I'll try to check it out and merge it.
Cheers :)
Thanks for the update!
I do have a few comments on the new format, so I'll work on it on a branch and show you what I was thinking of.
As for getting back functions, you're right, you can't just parse the table as JSON. However, there are serde
utilities in mlua
, I think? And I haven't tried it, but the crate mlua_serde
might be useful.
I'll play around with all this and report back in a PR somewhere.
Hmm... that sounds about right. Last time I checked, the mlua_serde
crate wasn't updated in a while so I thought that it might be abandoned but I guess if it works, then its okay. Well, I'm going to get back to studying now but I'll keep an eye out for an update. Cheers :)
Hey @calops, I think I've found a problem with the mlua_serde
crate. Since it hasn't been updated in a while, its required version of mlua
is pretty old so the install fails. Might have to fork it and update it quite a bit since the mlua
APIs have undergone pretty much a whole redesign especially with version 0.9 . I'd love to know where you're at on this. I have a few hours of free time tomorrow so I could work on this a bit. Cheers :)
I think we can forget that crate entirely, it seems mlua itself provides serde modules.
I'll be able to put an example together by this weekend, probably.
That's what I thought too. Well, I'll keep researching and let you know how it goes. Also if you could give me the config with your changes, then maybe I can work on parsing that.
For the record, it's there: https://docs.rs/mlua/latest/mlua/serde/index.html. It needs the serialize
feature flag.
Also if you could give me the config with your changes, then maybe I can work on parsing that.
Sure thing, I'll open a PR with my changes se we can discuss it there.
I've been able to execute the script and get the config tables. I changed the config quite a bit, more like what your suggested (or so I think). From what I've seen from one of the
mlua
examples for serializing data, the goto way seems to be just parsing them as JSON and then destructuring that data into a struct. This works for theconfig
table where all the values are constant but for thebindings
table, you can't parse aLuaFunction
into a JSON struct.
Instead of trying to serialize the LuaFunction
directly, can we represent it as a String
in Rust struct?. This way, you can later evaluate this string as a LuaFunction
when needed.
Instead of trying to serialize the LuaFunction directly, can we represent it as a String in Rust struct?. This way, you can later evaluate this string as a LuaFunction when needed.
The thing is that you're not just getting a function, but a context as well (they're more akin to closures). Just the body of the function can't be enough since it may be capturing stuff outside of its scope.
Anyway, it's unnecessary since mlua provides a way to directly manipulate (and call) the Function
objects, and it integrates perfectly with their serde framework.
Instead of trying to serialize the LuaFunction directly, can we represent it as a String in Rust struct?. This way, you can later evaluate this string as a LuaFunction when needed.
The thing is that you're not just getting a function, but a context as well (they're more akin to closures). Just the body of the function can't be enough since it may be capturing stuff outside of its scope.
Anyway, it's unnecessary since mlua provides a way to directly manipulate (and call) the
Function
objects, and it integrates perfectly with their serde framework.
I'll wait for your config file then maybe I can figure out what to do.
There you go: https://github.com/calops/stratawm/blob/luaconf/strata.default.lua
The PR is there: #18
As a basic config has been achieved, I'll be closing this issue. For any further feature requests, please create a new issue.
Currently all the configuration is done using TOML. Would love to implement support for extending functionality like custom layouts and window management stuff eg., grouping together windows of different programs like PekWM. Language of choice should be something like Lua, Nickel, Haskell or the like.