408 lines
12 KiB
C
408 lines
12 KiB
C
#include <assert.h>
|
|
#include <stdio.h>
|
|
#include <stdint.h>
|
|
#include <libguile.h>
|
|
#include <string.h>
|
|
|
|
#include "libguile/bitvectors.h"
|
|
#include "libguile/boolean.h"
|
|
#include "libguile/foreign-object.h"
|
|
#include "libguile/list.h"
|
|
#include "libguile/modules.h"
|
|
#include "libguile/numbers.h"
|
|
#include "libguile/pairs.h"
|
|
#include "libguile/scm.h"
|
|
#include "libguile/strings.h"
|
|
#include "libguile/symbols.h"
|
|
#include "riverguile.h"
|
|
#include "output.h"
|
|
#include "seat.h"
|
|
|
|
#include "river-control-unstable-v1.h"
|
|
#include "call-new-output-handler.h"
|
|
#include "wayland-util.h"
|
|
|
|
/**
|
|
* ISO C forbids casting a function pointer to a void pointer because on some
|
|
* architectures they have different sizes. However scm_c_define_gsubr() wants
|
|
* a function pointer as a void pointer, which trips -Wpedantic. Compiling C
|
|
* without pedantic errors is not a reasonable option in my opinion, so instead
|
|
* we'll have to resort to this hack.
|
|
*/
|
|
#define scm_c_define_gsubr_fix(NAME, REQ, OPT, RST, FN) \
|
|
{ const long int ptr = (long int)FN; scm_c_define_gsubr(NAME, REQ, OPT, RST, (void *)ptr); }
|
|
|
|
static uint32_t extract_ms_from_idle_key (SCM key)
|
|
{
|
|
SCM key_split = scm_string_split(
|
|
scm_symbol_to_string(key),
|
|
scm_to_char_set(scm_from_utf8_string(":"))
|
|
);
|
|
if ( scm_to_uint32(scm_length(key_split)) != 2 )
|
|
goto error;
|
|
|
|
SCM maybe_number = scm_string_to_number(scm_cadr(key_split), scm_from_int(10));
|
|
if ( scm_is_false(maybe_number) == 1 )
|
|
goto error;
|
|
|
|
int32_t ms = scm_to_int32(maybe_number);
|
|
if ( ms < 0 )
|
|
goto error;
|
|
|
|
return (uint32_t)ms * 1000;
|
|
|
|
error:
|
|
scm_error_scm(
|
|
scm_from_utf8_symbol("wrong-type-arg"),
|
|
scm_from_utf8_string("install-handler"),
|
|
scm_from_utf8_string("First argument: Excpected 'idle:<positive integer>."),
|
|
SCM_BOOL_F,
|
|
scm_list_1(key)
|
|
);
|
|
return 0;
|
|
}
|
|
|
|
static SCM install_handler (SCM key, SCM proc)
|
|
{
|
|
if ( scm_is_false(scm_symbol_p(key)) == 1 )
|
|
{
|
|
scm_error_scm(
|
|
scm_from_utf8_symbol("wrong-type-arg"),
|
|
scm_from_utf8_string("install-handler"),
|
|
scm_from_utf8_string("First argument must be a symbol."),
|
|
SCM_BOOL_F,
|
|
scm_list_1(key)
|
|
);
|
|
return SCM_UNSPECIFIED;
|
|
}
|
|
|
|
// TODO check if the procedure has the right amount of arguments.
|
|
if ( scm_is_false(scm_procedure_p(proc)) == 1 )
|
|
{
|
|
scm_error_scm(
|
|
scm_from_utf8_symbol("wrong-type-arg"),
|
|
scm_from_utf8_string("install-handler"),
|
|
scm_from_utf8_string("Second argument must be a procedure."),
|
|
SCM_BOOL_F,
|
|
scm_list_1(proc)
|
|
);
|
|
return SCM_UNSPECIFIED;
|
|
}
|
|
|
|
if ( scm_is_eq(scm_from_utf8_symbol("new-output"), key) == 1 )
|
|
{
|
|
context.mode = CONTINOUS;
|
|
context.new_output_handler = proc;
|
|
|
|
/* Call handler for all already existing outputs. */
|
|
struct Output *output;
|
|
wl_list_for_each(output, &context.outputs, link)
|
|
{
|
|
struct Call_new_output_handler_parameters params = {
|
|
.output = output,
|
|
};
|
|
|
|
void *res = scm_with_guile(call_new_output_handler, (void *)¶ms);
|
|
if ( res != NULL )
|
|
{
|
|
fputs(res, stderr);
|
|
fputs("INFO: This error is not fatal.\n", stderr);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
else 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);
|
|
|
|
if (!seat_add_idle(seat, proc, ms))
|
|
{
|
|
scm_error_scm(
|
|
scm_from_utf8_symbol("memory-allocation-error"),
|
|
scm_from_utf8_string("install-handler"),
|
|
SCM_BOOL_F,
|
|
SCM_BOOL_F,
|
|
SCM_BOOL_F
|
|
);
|
|
return SCM_UNSPECIFIED;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
scm_error_scm(
|
|
// TODO should this be 'misc-error instead?
|
|
scm_from_utf8_symbol("out-of-range"),
|
|
scm_from_utf8_string("install-handler"),
|
|
scm_from_utf8_string("Unknown key: ~A"),
|
|
scm_list_1(key),
|
|
scm_list_1(key)
|
|
);
|
|
return SCM_UNSPECIFIED;
|
|
}
|
|
|
|
return SCM_BOOL_T;
|
|
}
|
|
|
|
static SCM riverctl (SCM first, SCM rest)
|
|
{
|
|
if ( context.river_control == NULL )
|
|
{
|
|
fputs("ERROR: User script attempts to send river command but river-control-unstable-v1 not available.\n", stderr);
|
|
fputs("INFO: This error is not fatal, however riverguile will not be able to send any commands to river.\n", stderr);
|
|
return SCM_BOOL_F;
|
|
}
|
|
if ( wl_list_length(&context.seats) == 0 )
|
|
{
|
|
fputs("ERROR: User script attempts to send river command but server did not advertise any seats.\n", stderr);
|
|
fputs("INFO: This error is not fatal, however riverguile will not be able to send any commands to river.\n", stderr);
|
|
return SCM_BOOL_F;
|
|
}
|
|
|
|
if ( scm_is_string(first) != 1 )
|
|
goto error;
|
|
|
|
assert(scm_is_true(scm_list_p(rest)) == 1);
|
|
const uint32_t rest_len = scm_to_uint32(scm_length(rest));
|
|
for (uint32_t i = 0; i < rest_len; i++)
|
|
if ( scm_is_string(scm_list_ref(rest, scm_from_uint32(i))) != 1 )
|
|
goto error;
|
|
|
|
char *first_owned = scm_to_utf8_stringn(first, 0);
|
|
zriver_control_v1_add_argument(context.river_control, first_owned);
|
|
free(first_owned);
|
|
|
|
for (uint32_t i = 0; i < rest_len; i++)
|
|
{
|
|
char *owned = scm_to_utf8_stringn(scm_list_ref(rest, scm_from_uint32(i)), 0);
|
|
zriver_control_v1_add_argument(context.river_control, owned);
|
|
free(owned);
|
|
}
|
|
|
|
/* Just use the first seat. River only supports a single one anyway. */
|
|
struct Seat *seat;
|
|
wl_list_for_each(seat, &context.seats, link)
|
|
break;
|
|
|
|
/* While river does tell us about the status of the command, I do not
|
|
* want to make use of that in riverguile for multiple reasons.
|
|
* For one, the reporting is of course async, meaning we could not raise
|
|
* a guile error in response to failed commands, even if we wanted to.
|
|
* Riverguile could allow to install a handler for the callback, however
|
|
* I do not sett the use. There are two riverctl commands that return
|
|
* useful data as a string, however using that for scripting would be
|
|
* very hacky and I prefer dedicated well designed interfaces. So we
|
|
* just destroy the callback before it even fires.
|
|
*/
|
|
zriver_command_callback_v1_destroy(
|
|
zriver_control_v1_run_command(context.river_control, seat->wl_seat)
|
|
);
|
|
|
|
return SCM_BOOL_T;
|
|
error:
|
|
scm_error_scm(
|
|
scm_from_utf8_symbol("wrong-type-arg"),
|
|
scm_from_utf8_string("riverctl"),
|
|
scm_from_utf8_string("All arguments must be strings."),
|
|
SCM_BOOL_F,
|
|
SCM_BOOL_F
|
|
);
|
|
return SCM_UNSPECIFIED;
|
|
}
|
|
|
|
static SCM output_t;
|
|
|
|
static SCM scm_str(const char *chars) {
|
|
if (chars != NULL)
|
|
return scm_from_utf8_string(chars);
|
|
return scm_from_utf8_string("");
|
|
}
|
|
|
|
static SCM bitvector_tags(uint32_t tags) {
|
|
SCM bitvector = scm_c_make_bitvector(32, SCM_BOOL_F);
|
|
for (int i = 0; i < 32; i++) {
|
|
if ((tags & (1 << i)) != 0)
|
|
scm_c_bitvector_set_bit_x(bitvector, i);
|
|
}
|
|
return bitvector;
|
|
}
|
|
|
|
static SCM output_for(struct Output *output) {
|
|
SCM output_scm = scm_make_foreign_object_0(output_t);
|
|
scm_foreign_object_set_x(output_scm, 0, scm_from_int(output->name));
|
|
scm_foreign_object_set_x(output_scm, 1,
|
|
bitvector_tags(output->tags.focused_tags));
|
|
SCM view_tags = SCM_EOL;
|
|
for (ssize_t i = output->tags.view_tags_len - 1; i >= 0; i--)
|
|
view_tags = scm_cons(bitvector_tags(*(output->tags.view_tags + i)), view_tags);
|
|
scm_foreign_object_set_x(output_scm, 2, view_tags);
|
|
scm_foreign_object_set_x(output_scm, 3,
|
|
bitvector_tags(output->tags.urgent_tags));
|
|
scm_foreign_object_set_x(output_scm, 4, scm_str(output->tags.layout_name));
|
|
return output_scm;
|
|
}
|
|
|
|
static SCM outputs(void) {
|
|
SCM lst = SCM_EOL;
|
|
struct Output *output, *tmp_o;
|
|
wl_list_for_each_safe(output, tmp_o, &context.outputs, link)
|
|
lst = scm_cons(output_for(output), lst);
|
|
|
|
return scm_reverse_x(lst, SCM_EOL);
|
|
}
|
|
|
|
static SCM seat_t;
|
|
|
|
// Assumes there's only one seat available
|
|
static SCM seat(void) {
|
|
struct Seat *seat;
|
|
// Take the first seat
|
|
wl_list_for_each(seat, &context.seats, link)
|
|
break;
|
|
struct Output *output = NULL;
|
|
if ( seat->status.focused_output != NULL )
|
|
wl_list_for_each(output, &context.outputs, link)
|
|
if ( output->wl_output == seat->status.focused_output )
|
|
break;
|
|
|
|
|
|
return scm_make_foreign_object_3(
|
|
seat_t,
|
|
output != NULL ? output_for(output) : SCM_BOOL_F,
|
|
scm_str(seat->status.focused_view_title),
|
|
scm_str(seat->status.mode));
|
|
}
|
|
|
|
#define getters(X) \
|
|
X(output_t, output_name, "output-name", 0) \
|
|
X(output_t, output_focused_tags, "output-focused-tags", 1) \
|
|
X(output_t, output_view_tags, "output-view-tags", 2) \
|
|
X(output_t, output_urgent_tags, "output-urgent-tags", 3) \
|
|
X(output_t, output_layout_name, "output-layout-name", 4) \
|
|
X(seat_t, seat_focused_output, "seat-focused-output", 0) \
|
|
X(seat_t, seat_focused_view_title, "seat-focused-view-title", 1) \
|
|
X(seat_t, seat_mode, "seat-mode", 2)
|
|
|
|
#define X(t, c, scm, i) static SCM c (SCM output) { scm_assert_foreign_object_type(t, output); return scm_foreign_object_ref(output, i); }
|
|
getters(X)
|
|
#undef X
|
|
|
|
|
|
|
|
static void *load_script_inner (void *data)
|
|
{
|
|
const char *path = (char *)data;
|
|
|
|
/* scm_primitive_load_path() searches guiles load-path when encountering
|
|
* a relative path. That should never happen here though.
|
|
*/
|
|
assert(path[0] == '/');
|
|
|
|
return scm_primitive_load_path(scm_from_utf8_string(path));
|
|
}
|
|
|
|
SCM init_module(void *data) {
|
|
/* Note: All guile objects are garbage collected. */
|
|
scm_c_define_gsubr_fix("install-handler", 2, 0, 0, install_handler);
|
|
scm_c_define_gsubr_fix("riverctl", 1, 0, 1, riverctl);
|
|
|
|
output_t = scm_make_foreign_object_type(
|
|
scm_from_utf8_symbol("output"),
|
|
scm_list_5(scm_from_utf8_symbol("name"),
|
|
scm_from_utf8_symbol("focused-tags"),
|
|
scm_from_utf8_symbol("view-tags"),
|
|
scm_from_utf8_symbol("urgent-tags"),
|
|
scm_from_utf8_symbol("layout")),
|
|
NULL);
|
|
|
|
seat_t = scm_make_foreign_object_type(
|
|
scm_from_utf8_symbol("seat"),
|
|
scm_list_3(scm_from_utf8_symbol("focused-output"),
|
|
scm_from_utf8_symbol("focused-view-title"),
|
|
scm_from_utf8_symbol("mode")),
|
|
NULL);
|
|
|
|
scm_c_define_gsubr_fix("outputs", 0, 0, 0, outputs);
|
|
scm_c_define_gsubr_fix("seat", 0, 0, 0, seat);
|
|
|
|
#define X(t, c, scm, i) scm_c_define_gsubr_fix(scm, 1, 0, 0, c);
|
|
getters(X)
|
|
#undef X
|
|
return SCM_UNSPECIFIED;
|
|
}
|
|
|
|
void *load_script (void *data)
|
|
{
|
|
SCM module = scm_c_resolve_module("riverguile");
|
|
scm_c_call_with_current_module(module, init_module, NULL);
|
|
|
|
/* Continuation barrier causes stack unwind on exceptions to stop here.
|
|
* Otherwise the entire stack created by scm_with_guile() would be
|
|
* unwound. This makes responding to exceptions nicer.
|
|
*/
|
|
SCM call_result = scm_c_with_continuation_barrier(
|
|
load_script_inner, data
|
|
);
|
|
|
|
if ( call_result == NULL )
|
|
return (void *)"ERROR: Fatal error while loading layout script.\n";
|
|
|
|
/* Checked in the installer functions. */
|
|
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;
|
|
}
|
|
|