#include #include #include #include #include #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:."), 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; }