load script after initial registry burst

This makes the logic of installing idle handlers and layout handlers a bit
cleaner and allows us to send wayland request from the script in the future.
This commit is contained in:
Leon Henrik Plickat
2024-01-07 06:24:23 +01:00
parent 732c1adb55
commit 153b5e0858
11 changed files with 304 additions and 152 deletions

View File

@@ -1,8 +1,10 @@
# riverguile
Layout generator for the [river](https://github.com/riverwm/river) Wayland
server which uses [Guile Scheme](https://www.gnu.org/software/guile/) for
layouts. Its layout namespace is `riverguile`.
Scripting layer for the [river](https://github.com/riverwm/river) Wayland
server using [Guile Scheme](https://www.gnu.org/software/guile/).
Send commands to river and install handlers for various events, inclusing
layout events.
Uppon launch, riverguile tries to load a scheme script and checks the following
paths for it in the given order:
@@ -17,9 +19,15 @@ is available, which can be used to install handlers to certain events.
Check the man page for more information.
Here is an example `layout.scm` file replicating rivertiles behavior of having
a main window and a stack of windows aside:
a main window and a stack of windows aside while also automatically locking
the screen after five minutes of inactivity.
```scheme
(install-handler 'idle:300 (lambda (event)
(cond ((eq? event 'idle) (system "swaylock &")
(system "backlight.sh set-to 40"))
((eq? event 'resume) (system "backlight.sh set-to 100")))))
(define split 0.55)
(define (layout:rows n x y w h)

View File

@@ -2,7 +2,7 @@
.
.SH NAME
.P
riverguile \- scheme powered layout generator for river
riverguile \- scheme powered scripting layer for river
.
.
.SH SYNOPSIS
@@ -12,16 +12,17 @@ riverguile \- scheme powered layout generator for river
.
.SH DESCRIPTION
.P
Layout generator for the
Scripting layer for the
.BR river (1)
Wayland server which allows users to define layouts using guile scheme functions.
.
Wayland server.
Allows the user to send commands to the Wayland server (probably river) and
install handlers for events from a scheme script.
.P
The layout namespace is \fBriverguile\fB.
.
By default, riverguile will exit after the script has been loaded and evaluated.
If certain handlers are installed it will run continously.
.P
Uppon launch, riverguile tries to eval a scheme script and checks the following
paths for it in the given order:
Uppon launch, riverguile tries to load the script from the following paths
in the given order:
.IP \(bu 2
\fBlayout.scm\fR
.IP \(bu 2
@@ -30,80 +31,153 @@ paths for it in the given order:
\fB$HOME/.config/river/layout.scm\fR
.IP \(bu 2
\fB/etc/riverguile/layout.scm\fR
.
.P
In the context of this script, a special procedure
.
.
.SH EVENT HANDLERS
.P
In the context of the script, a special procedure
\fB(install-handler \fR\fIkey\fR \fIproc\fR\fB)\fR is available, which can be
used to install handlers to certain events.
used to install event handlers.
The parameter \fIkey\fR is a symbol indicating for which event to install
the procedure \fIproc\fR.
The following keys are currently evailable:
.
.P
The key \fBlayout-demand\fR installs a handler for layout demands, which must
accept five required arguments, which are, in order: The amount of views in the
layout (integer), the available width (integer), the available height (integer),
the currently active tag set (integer representing a bitfield of size 32)
and the global name of the output the layout is needed for (integer).
\fBlayout-demand\fR
.P
.RS
Installing a handler for this key allows the user to provide window layouts.
All limitations of the river-layout-v3 protocol apply.
The server will trigger this event when a new layout is required ("demanded").
.P
Installing a layout-demand handler will cause riverguile to run continously.
.P
The handler procedure must accept five required arguments, which are, in order:
The amount of views in the layout (integer), the available width (integer), the
available height (integer), the currently active tag set (integer representing a
bitfield of size 32) and the global name of the output the layout is needed for
(integer).
The procedure must return a list containing exactly as many lists as there are
views in the layout.
Each of those sublists must contains exactly four numerical values, which are
the x and y coordinates of the window as well as its width and height.
Window positions and dimensions get applied to rivers window list top to bottom.
Window positions and dimensions get applied to the window list top to bottom.
.P
Note that the numerical values do not need to be exact, riverguile takes care
of rounding and casting for you.
A layout demand handler must be installed, otherwise riverguile will exit.
.P
The key \fBuser-command\fR install a handler for user commands, which are the
strings a user can send to layout generators.
This handler procedure must accept three arguments, which are, in order:
The command (string), the currently active tags (integer representing a
bitfield of size 32) and the global name of the output (integer).
This event can be used to change paramters of the layout.
A new layout demand will be send right after this event.
.P
The key \fBidle:X\fR installs a handler which is called when the system has been
idle for \fIX\fR seconds.
As an example, use \fBidle:300\fR if you wish to be notified of your system
being idle for five minutes.
Idle in this case relates to user interaction;
The system being idle for five minutes usually means there has been no input
from the user in five minutes.
Idle status however can also be influenced by other factors, for example by
other programs inhibiting it (like video players), special sensors (user
presence sensors) and is generally server-specific policy.
The handler procedure must accept one argument, a symbol indicating the type
of idle event.
This symbol is either \fBidle\fR, indicating the system has been idle for the
configured amount of time, or \fBresume\fR, indicating that the time intervall
of the system being idle is over.
Multiple idle handlers can be installed.
Note: All idle events relate to the first advetised seat.
As of now, river only supports a single seat anyway.
.P
The key \fBexit\fR installs a handler which is called when riverguile exits.
The procedure takes no arguments.
.
.
.SH EXAMPLE
.P
This is an example configuration, which installs a layout that simply assigns
each window all usable space and a user command handler that tries to evaluate
all send commands as scheme code.
.
Here is an example of a simple layout-demand handler which simply makes all
windows use all available space:
.P
.RS
.EX
(install-handler 'user-command (lambda (cmd tags output)
(eval-string cmd)))
(install-handler 'layout-demand (lambda (view-count width height tags output)
(letrec ((iter (lambda (n)
(if (eq? 0 n)
'()
(append (list (list 0 0 width height))
(iter (1- n)))))))
(iter view-count))))
(\fBinstall-handler\fR 'layout-demand
(\fBlambda\fR (view-count width height tags output)
(\fBletrec\fR ((iter (\fBlambda\fR (n)
(if (eq? 0 n)
'()
(\fBappend\fR (\fBlist\fR (\fBlist\fR 0 0 width height))
(iter (1- n)))))))
(iter view-count))))
.EE
.RE
.RE
.
.P
\fBuser-command\fR
.P
.RS
User commands are intended to send commands to layout generators, allowing the
user to update parameters of the layout live.
Of course, nothing is stopping you from (ab-)using this event to trigger
arbitrary scheme code on keypresses or on outside events, or from simply not
using it at all.
After a user-command has been received, the server can will trigger a
layout-demand if there are visible windows.
.P
Installing a user-command handler will \fInot\fR cause riverguile to run continously.
This event is an extension to the layout-demand event and as such it is invalid
to install a user-command handler without also installing a layout-demant
handler.
.P
The handler procedure must accept three arguments, which are, in order:
The command (string), the currently active tags (integer representing a
bitfield of size 32) and the global name of the output (integer).
.P
Here is an example of a simple user-command handler which simply evaluates the
string as scheme code:
.P
.RS
.EX
(\fBinstall-handler\fR 'user-command
(\fBlambda\fR (cmd tags output)
(\fBeval-string\fR cmd)))
.EE
.RE
.P
Note that this is not necessarily good practice, but serves as a decent example.
.RE
.
.P
\fBidle:X\fR
.P
.RS
A handler installed for this key will be triggered after the system has been
idle for \fIX\fR seconds and once more once the system is no longer idle.
.P
Installing a layout-demand handler will cause riverguile to run continously.
Multiple idle handlers can be installed.
.P
Idle state is server policy and may depend on a multitude of factors, but
usually maps directly to the usage activity of input devices by the user.
Certain programs may inhibit idle state, like for example video players.
.P
The handler procedure must accept one argument, a symbol indicating the type
of idle event.
This symbol is either \fBidle\fR, indicating the system has been idle for the
configured amount of time, or \fBresume\fR, indicating that the system is no
longer idle.
.P
Here is an example which will dim the screen after two minutes of inactiviy
and lock it after fice:
.P
.RS
.EX
(\fBinstall-handler\fR 'idle:120
(\fBlambda\fR (event)
(\fBcond\fR ((\fBeq?\fR event 'idle) (\fBsystem\fR "light -S 20"))
((\fBeq?\fR event 'resume) (\fBsystem\fR "light -S 100")))))
(\fBinstall-handler\fR 'idle:300
(\fBlambda\fR (event)
(if (\fBeq?\fR event 'idle) (\fBsystem\fR "swaylock &"))))
.EE
.RE
.P
Note: All idle events relate to the first advetised seat.
As of now, river only supports a single seat anyway.
.RE
.
.P
\fBexit\fR
.RE
.RS
This key allows you to installs a handler which is called when riverguile exits.
.P
The procedure takes no arguments.
.P
Here is an example which adds a message to the system log on exit:
.P
.RS
.EX
(\fBinstall-handler\fR 'exit
(\fBlambda\fR ()
(\fBsystem\fR "logger 'goodbye from riverguile'")))
.EE
.RE
.RE
.
.
.SH SEE ALSO

View File

@@ -9,7 +9,7 @@ static void *call_exit_handler_inner (void *data)
return scm_call_0(context.exit_handler);
}
void *call_exit_handler (void)
void *call_exit_handler (void *data)
{
assert(context.exit_handler != NULL);

View File

@@ -1,6 +1,6 @@
#ifndef RIVERGUILE_CALL_EXIT_HANDLER_H
#define RIVERGUILE_CALL_EXIT_HANDLER_H
void *call_exit_handler (void);
void *call_exit_handler (void* data);
#endif

View File

@@ -5,6 +5,7 @@
#include <string.h>
#include "riverguile.h"
#include "output.h"
#include "seat.h"
/**
@@ -75,19 +76,58 @@ static SCM install_handler (SCM key, SCM proc)
}
if ( scm_is_eq(scm_from_utf8_symbol("layout-demand"), key) == 1 )
{
if ( context.layout_manager == NULL )
{
fputs("ERROR: Trying to install layout-demand handler but server does not support river-layout-v3.\n", stderr);
fputs("INFO: This error is not fatal, but means riverguile will not provide any layout.\n", stderr);
return SCM_BOOL_F;
}
context.mode = CONTINOUS;
context.layout_demand_handler = proc;
/* Configure all outputs to expose layouts. */
struct Output *output;
wl_list_for_each(output, &context.outputs, link)
output_configure_layout(output);
}
else if ( scm_is_eq(scm_from_utf8_symbol("user-command"), key) == 1)
{
/* No need to check if the interface exists since it's only
* used when a layout-demand handler is configured.
*/
context.user_command_handler = proc;
}
else if ( scm_is_eq(scm_from_utf8_symbol("exit"), key) == 1)
context.exit_handler = proc;
else if ( scm_is_true(scm_string_prefix_p(scm_from_utf8_string("idle:"), scm_symbol_to_string(key),
scm_from_int(0), scm_from_int(5),
scm_from_int(0), scm_string_length(scm_symbol_to_string(key)))) == 1 )
{
if ( context.idle_notifier == NULL )
{
fputs("ERROR: Trying to install idle handler but server does not support ext-idle-notify-v1.\n", stderr);
fputs("INFO: This error is not fatal, but means riverguile will not be able to call any idle handler.\n", stderr);
return SCM_BOOL_F;
}
if ( wl_list_length(&context.seats) == 0 )
{
fputs("ERROR: Trying to install idle handler but server did not advertise any seats.\n", stderr);
fputs("INFO: This error is not fatal, but means riverguile will not be able to call any idle handler.\n", stderr);
return SCM_BOOL_F;
}
context.mode = CONTINOUS;
/* Just use the first seat. River only supports a single one anyway. */
struct Seat *seat;
wl_list_for_each(seat, &context.seats, link)
break;
uint32_t ms = extract_ms_from_idle_key(key);
struct Idle *idle = calloc(1, sizeof(struct Idle));
if ( idle == NULL )
if (!seat_add_idle(seat, proc, ms))
{
scm_error_scm(
scm_from_utf8_symbol("memory-allocation-error"),
@@ -98,10 +138,6 @@ static SCM install_handler (SCM key, SCM proc)
);
return SCM_UNSPECIFIED;
}
idle->ms = ms;
idle->handler = proc;
wl_list_insert(&context.unconfigured_idles, &idle->link);
}
else
{
@@ -148,13 +184,13 @@ void *load_script (void *data)
if ( call_result == NULL )
return (void *)"ERROR: Fatal error while loading layout script.\n";
if ( context.layout_demand_handler == NULL )
return (void *)"ERROR: No layout demand handler installed.\n";
/* Checked in the installer functions. */
assert(scm_is_true(scm_procedure_p(context.layout_demand_handler)) == 1);
if ( context.layout_demand_handler != NULL )
assert(scm_is_true(scm_procedure_p(context.layout_demand_handler)) == 1);
if ( context.user_command_handler != NULL )
assert(scm_is_true(scm_procedure_p(context.user_command_handler)) == 1);
if ( context.exit_handler != NULL )
assert(scm_is_true(scm_procedure_p(context.exit_handler)) == 1);
return NULL;
}

View File

@@ -80,18 +80,18 @@ static const struct river_layout_v3_listener layout_listener = {
.user_command_tags = layout_handle_user_command_tags,
};
void output_configure (struct Output *output)
void output_configure_layout (struct Output *output)
{
if ( context.layout_manager == NULL )
return;
if (output->configured)
return;
assert(context.layout_manager != NULL);
assert(context.layout_demand_handler != NULL);
if ( output->layout != NULL )
return;
output->layout = river_layout_manager_v3_get_layout(
context.layout_manager, output->wl_output, "riverguile"
);
river_layout_v3_add_listener(output->layout, &layout_listener, output);
output->configured = true;
}
struct Output *output_create (struct wl_output *wl_output, uint32_t name)
@@ -105,7 +105,13 @@ struct Output *output_create (struct wl_output *wl_output, uint32_t name)
output->name = name;
output->wl_output = wl_output;
output_configure(output);
/* Only bind layout if we need it. Outputs advertised in the initial
* registry burst, before the script is loaded, will always skip this,
* however it is necessary for outputs added later.
*/
if ( context.layout_manager != NULL && context.layout_demand_handler != NULL )
output_configure_layout(output);
return output;
}

View File

@@ -12,7 +12,6 @@ struct Output
struct wl_output *wl_output;
struct river_layout_v3 *layout;
uint32_t name;
bool configured;
/* Tags for the next user command. Due to backwards compatability, the
* layout protocol sends us the currently active tags for a user command
@@ -24,6 +23,6 @@ struct Output
struct Output *output_create (struct wl_output *wl_output, uint32_t name);
void output_destroy (struct Output *output);
void output_configure (struct Output *output);
void output_configure_layout (struct Output *output);
#endif

View File

@@ -118,54 +118,66 @@ static const struct wl_registry_listener registry_listener = {
.global_remove = registry_handle_global_remove,
};
static void sync_handle_done (void *, struct wl_callback *, uint32_t);
static const struct wl_callback_listener sync_callback_listener = {
.done = sync_handle_done,
};
static void sync_handle_done (void *data, struct wl_callback *wl_callback, uint32_t other_data)
{
static int i = 0;
if ( i == 1 )
{
assert(context.mode == ONESHOT);
context.loop = false;
return;
}
wl_callback_destroy(wl_callback);
context.sync_callback = NULL;
if ( context.layout_manager == NULL )
/* Load the script after connecting to the server and binding interfaces
* to allow calling Wayland requests from it.
*/
assert(context.path != NULL);
void *res = scm_with_guile(load_script, (void *)context.path);
if ( res != NULL )
{
fputs("ERROR: Wayland server does not support river-layout-v3.\n", stderr);
fputs((char *)res, stderr);
context.ret = EXIT_FAILURE;
context.loop = false;
return;
}
if ( context.idle_notifier == NULL )
if ( context.layout_demand_handler == NULL
&& context.user_command_handler != NULL )
{
if ( wl_list_length(&context.unconfigured_idles) > 0 )
{
fputs("ERROR: Wayland server does not support ext-idle-notify-v1.\n", stderr);
fputs("INFO: This error is not fatal.\n", stderr);
}
fputs("ERROR: Installing user-command handler without installing a layout-demand handler is not allowed.\n", stderr);
fputs("INFO: This error is not fatal, but means riverguile will not provide any layout.\n", stderr);
}
else
switch (context.mode)
{
struct Seat *seat;
wl_list_for_each(seat, &context.seats, link)
case ONESHOT:
/* Oneshot mode. Sync again so we are sure that all commands
* have been send, then exit.
*/
assert(i == 0);
i++;
context.sync_callback = wl_display_sync(context.wl_display);
wl_callback_add_listener(context.sync_callback, &sync_callback_listener, NULL);
fputs("INFO: No handlers installed: Riverguile will exit.\n", stderr);
break;
struct Idle *idle, *tmp_i;
wl_list_for_each_safe(idle, tmp_i, &context.unconfigured_idles, link)
idle_configure(idle, seat);
case CONTINOUS:
fputs("INFO: At least one handler installed: Riverguile will run continously.\n", stderr);
break;
}
/* If outputs were registered before the river_layout_manager is
* available, they won't have a river_layout, so we need to create
* those here.
*/
struct Output *output;
wl_list_for_each(output, &context.outputs, link)
output_configure(output);
}
static const struct wl_callback_listener sync_callback_listener = {
.done = sync_handle_done,
};
static void handle_interrupt (int signum)
{
fputs("Killed 💀\n", stderr);
fputs("INFO: Killed 💀\n", stderr);
context.loop = false;
longjmp(skip_main_loop, 1);
}
@@ -211,7 +223,6 @@ static void handle_error (int signum)
kill(getpid(), signum);
}
static char *formatted_buffer (const char *fmt, ...)
{
/* Determine length of formatted text. */
@@ -286,36 +297,29 @@ static char *get_script_path (void)
if ( path != NULL )
free(path);
fputs("ERROR: No layout script found.\n", stderr);
fputs("ERROR: No script found.\n", stderr);
return NULL;
}
int main(int argc, char *argv[])
{
fputs("INFO: Welcome to riverguile!\n", stderr);
signal(SIGSEGV, handle_error);
signal(SIGFPE, handle_error);
signal(SIGINT, handle_interrupt);
wl_list_init(&context.outputs);
wl_list_init(&context.seats);
wl_list_init(&context.unconfigured_idles);
// TODO use argv[1] if present
char *path = get_script_path();
if ( path == NULL )
context.path = get_script_path();
if ( context.path == NULL )
{
context.ret = EXIT_FAILURE;
goto early_exit;
}
void *res = scm_with_guile(load_script, (void *)path);
if ( res != NULL )
{
fputs((char *)res, stderr);
context.ret = EXIT_FAILURE;
context.loop = false;
goto early_exit;
}
fprintf(stderr, "INFO: Found script: %s\n", context.path);
/* We query the display name here instead of letting wl_display_connect()
* figure it out itself, because libwayland (for legacy reasons) falls
@@ -348,13 +352,14 @@ int main(int argc, char *argv[])
while ( context.loop && wl_display_dispatch(context.wl_display) > 0 );
if ( context.exit_handler != NULL )
call_exit_handler();
struct Idle *idle, *tmp_i;
wl_list_for_each_safe(idle, tmp_i, &context.unconfigured_idles, link)
{
wl_list_remove(&idle->link);
idle_destroy(idle);
void *res = scm_with_guile(call_exit_handler, NULL);
if ( res != NULL )
{
fputs((char *)res, stderr);
context.ret = EXIT_FAILURE;
context.loop = false;
}
}
struct Output *output, *tmp_o;
@@ -382,9 +387,10 @@ int main(int argc, char *argv[])
wl_display_disconnect(context.wl_display);
early_exit:
if ( path != NULL )
free(path);
if ( context.path != NULL )
free(context.path);
fputs("INFO: Exiting.\n", stderr);
return context.ret;
}

View File

@@ -7,12 +7,26 @@
#include "seat.h"
enum Riverguile_mode
{
/* Riverguile is used only for configuring river and will exit after
* the init script has been loaded.
*/
ONESHOT = 0,
/* Riverguile need to run continous, because we have handlers installed
* f.e. for layouts or idle.
*/
CONTINOUS,
};
struct Context
{
SCM layout_demand_handler;
SCM user_command_handler;
SCM exit_handler;
enum Riverguile_mode mode;
bool loop;
int ret;
@@ -25,8 +39,7 @@ struct Context
struct wl_list seats;
struct wl_list outputs;
/* Idles are created before we connect to the server and bind seats. */
struct wl_list unconfigured_idles;
char *path;
};
extern struct Context context;

View File

@@ -1,5 +1,6 @@
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <errno.h>
#include <assert.h>
@@ -20,7 +21,9 @@ static void idle_notification_handle_idled (void *data,
.idle = idle,
.event = IDLE,
};
call_idle_handler(&params);
void *res = scm_with_guile(call_idle_handler, &params);
if ( res != NULL )
fputs((char *)res, stderr);
}
static void idle_notification_handle_resumed (void *data,
@@ -33,7 +36,9 @@ static void idle_notification_handle_resumed (void *data,
.idle = idle,
.event = RESUME,
};
call_idle_handler(&params);
void *res = scm_with_guile(call_idle_handler, &params);
if ( res != NULL )
fputs((char *)res, stderr);
}
static const struct ext_idle_notification_v1_listener idle_notification_listener = {
@@ -41,28 +46,34 @@ static const struct ext_idle_notification_v1_listener idle_notification_listener
.resumed = idle_notification_handle_resumed,
};
void idle_configure (struct Idle *idle, struct Seat *seat)
bool seat_add_idle (struct Seat *seat, SCM proc, uint32_t ms)
{
assert(idle->seat == NULL);
assert(context.idle_notifier != NULL);
struct Idle *idle = calloc(1, sizeof(struct Idle));
if ( idle == NULL )
return false;
idle->ms = ms;
idle->seat = seat;
idle->handler = proc;
idle->idle_notification = ext_idle_notifier_v1_get_idle_notification(
context.idle_notifier,
idle->ms,
seat->wl_seat
);
ext_idle_notification_v1_add_listener(
idle->idle_notification,
&idle_notification_listener,
idle
);
wl_list_remove(&idle->link);
wl_list_insert(&seat->idles, &idle->link);
idle->seat = seat;
return true;
}
void idle_destroy (struct Idle *idle)
static void idle_destroy (struct Idle *idle)
{
// TODO XXX how do we tell guile it is allowed to clean up the handler?
// maybe by entering guile mode one more time at exit?

View File

@@ -1,6 +1,7 @@
#ifndef RIVERGUILE_SEAT_H
#define RIVERGUILE_SEAT_H
#include <stdbool.h>
#include <wayland-client.h>
#include <libguile.h>
@@ -24,8 +25,6 @@ struct Idle
struct Seat *seat_create (struct wl_seat *wl_seat, uint32_t name);
void seat_destroy (struct Seat *seat);
void idle_configure (struct Idle *idle, struct Seat *seat);
void idle_destroy (struct Idle *idle);
bool seat_add_idle (struct Seat *seat, SCM proc, uint32_t ms);
#endif