mirror of
https://github.com/cage-kiosk/cage.git
synced 2025-11-26 07:00:00 -05:00
Start Cage 0.2 rewrite
This commit is contained in:
parent
d097393732
commit
248f4847df
5 changed files with 577 additions and 25 deletions
328
cageng.c
Normal file
328
cageng.c
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
/*
|
||||
* Cage: A Wayland kiosk.
|
||||
*
|
||||
* Copyright (C) 2018-2020 Jente Hidskes
|
||||
*
|
||||
* See the LICENSE file accompanying this file.
|
||||
*/
|
||||
|
||||
#define _POSIX_C_SOURCE 200112L
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <getopt.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#include <wayland-server-core.h>
|
||||
#include <wlr/backend.h>
|
||||
#include <wlr/render/wlr_renderer.h>
|
||||
#include <wlr/types/wlr_compositor.h>
|
||||
#include <wlr/util/log.h>
|
||||
|
||||
#include "desktop/output.h"
|
||||
#include "serverng.h"
|
||||
|
||||
static int
|
||||
sigchld_handler(int fd, uint32_t mask, void *user_data)
|
||||
{
|
||||
struct wl_display *display = user_data;
|
||||
|
||||
/* Close Cage's read pipe. */
|
||||
close(fd);
|
||||
|
||||
if (mask & WL_EVENT_HANGUP) {
|
||||
wlr_log(WLR_DEBUG, "Child process closed normally");
|
||||
} else if (mask & WL_EVENT_ERROR) {
|
||||
wlr_log(WLR_DEBUG, "Connection closed by server");
|
||||
}
|
||||
|
||||
wl_display_terminate(display);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool
|
||||
set_cloexec(int fd)
|
||||
{
|
||||
int flags = fcntl(fd, F_GETFD);
|
||||
|
||||
if (flags == -1) {
|
||||
wlr_log(WLR_ERROR, "Unable to set the CLOEXEC flag: fnctl failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
flags = flags | FD_CLOEXEC;
|
||||
if (fcntl(fd, F_SETFD, flags) == -1) {
|
||||
wlr_log(WLR_ERROR, "Unable to set the CLOEXEC flag: fnctl failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
spawn_primary_client(struct wl_display *display, char *argv[], pid_t *pid_out, struct wl_event_source **sigchld_source)
|
||||
{
|
||||
int fd[2];
|
||||
if (pipe(fd) != 0) {
|
||||
wlr_log(WLR_ERROR, "Unable to create pipe");
|
||||
return false;
|
||||
}
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
sigset_t set;
|
||||
sigemptyset(&set);
|
||||
sigprocmask(SIG_SETMASK, &set, NULL);
|
||||
/* Close read, we only need write in the primary client process. */
|
||||
close(fd[0]);
|
||||
execvp(argv[0], argv);
|
||||
_exit(1);
|
||||
} else if (pid == -1) {
|
||||
wlr_log_errno(WLR_ERROR, "Unable to fork");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Set this early so that if we fail, the client process will be cleaned up properly. */
|
||||
*pid_out = pid;
|
||||
|
||||
if (!set_cloexec(fd[0]) || !set_cloexec(fd[1])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Close write, we only need read in Cage. */
|
||||
close(fd[1]);
|
||||
|
||||
struct wl_event_loop *event_loop = wl_display_get_event_loop(display);
|
||||
uint32_t mask = WL_EVENT_HANGUP | WL_EVENT_ERROR;
|
||||
*sigchld_source = wl_event_loop_add_fd(event_loop, fd[0], mask, sigchld_handler, display);
|
||||
|
||||
wlr_log(WLR_DEBUG, "Child process created with pid %d", pid);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
cleanup_primary_client(pid_t pid)
|
||||
{
|
||||
int status;
|
||||
|
||||
waitpid(pid, &status, 0);
|
||||
|
||||
if (WIFEXITED(status)) {
|
||||
wlr_log(WLR_DEBUG, "Child exited normally with exit status %d", WEXITSTATUS(status));
|
||||
} else if (WIFSIGNALED(status)) {
|
||||
wlr_log(WLR_DEBUG, "Child was terminated by a signal (%d)", WTERMSIG(status));
|
||||
}
|
||||
}
|
||||
|
||||
static bool
|
||||
drop_permissions(void)
|
||||
{
|
||||
if (getuid() != geteuid() || getgid() != getegid()) {
|
||||
// Set the gid and uid in the correct order.
|
||||
if (setgid(getgid()) != 0 || setuid(getuid()) != 0) {
|
||||
wlr_log(WLR_ERROR, "Unable to drop root, refusing to start");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (setgid(0) != -1 || setuid(0) != -1) {
|
||||
wlr_log(WLR_ERROR,
|
||||
"Unable to drop root (we shouldn't be able to restore it after setuid), refusing to start");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static int
|
||||
handle_signal(int signal, void *user_data)
|
||||
{
|
||||
struct wl_display *display = user_data;
|
||||
|
||||
switch (signal) {
|
||||
case SIGINT:
|
||||
/* Fallthrough */
|
||||
case SIGTERM:
|
||||
wl_display_terminate(display);
|
||||
return 0;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
handle_new_output(struct wl_listener *listener, void *user_data)
|
||||
{
|
||||
struct cg_server *server = wl_container_of(listener, server, new_output);
|
||||
struct wlr_output *wlr_output = user_data;
|
||||
|
||||
struct cg_output *output = calloc(1, sizeof(struct cg_output));
|
||||
if (!output) {
|
||||
wlr_log(WLR_ERROR, "Failed to allocate output");
|
||||
return;
|
||||
}
|
||||
|
||||
wlr_output_layout_add_auto(server->output_layout, wlr_output);
|
||||
|
||||
// TODO: do this before or after init?
|
||||
wl_list_insert(&server->outputs, &output->link);
|
||||
cage_output_init(output, wlr_output);
|
||||
}
|
||||
|
||||
static void
|
||||
usage(FILE *file, const char *cage)
|
||||
{
|
||||
fprintf(file,
|
||||
"Usage: %s [OPTIONS] [--] APPLICATION\n"
|
||||
"\n"
|
||||
" -h\t Display this help message\n"
|
||||
" -v\t Show the version number and exit\n"
|
||||
"\n"
|
||||
" Use -- when you want to pass arguments to APPLICATION\n",
|
||||
cage);
|
||||
}
|
||||
|
||||
static bool
|
||||
parse_args(struct cg_server *server, int argc, char *argv[])
|
||||
{
|
||||
int c;
|
||||
while ((c = getopt(argc, argv, "hv")) != -1) {
|
||||
switch (c) {
|
||||
case 'h':
|
||||
usage(stdout, argv[0]);
|
||||
return false;
|
||||
case 'v':
|
||||
fprintf(stdout, "Cage version " CAGE_VERSION "\n");
|
||||
exit(0);
|
||||
default:
|
||||
usage(stderr, argv[0]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (optind >= argc) {
|
||||
usage(stderr, argv[0]);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char *argv[])
|
||||
{
|
||||
struct cg_server server = {0};
|
||||
struct wl_event_loop *event_loop = NULL;
|
||||
struct wl_event_source *sigint_source = NULL;
|
||||
struct wl_event_source *sigterm_source = NULL;
|
||||
struct wl_event_source *sigchld_source = NULL;
|
||||
struct wlr_backend *backend = NULL;
|
||||
struct wlr_renderer *renderer = NULL;
|
||||
struct wlr_compositor *compositor = NULL;
|
||||
pid_t pid = 0;
|
||||
int ret = 0;
|
||||
|
||||
if (!parse_args(&server, argc, argv)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
#ifdef DEBUG
|
||||
wlr_log_init(WLR_DEBUG, NULL);
|
||||
#else
|
||||
wlr_log_init(WLR_ERROR, NULL);
|
||||
#endif
|
||||
|
||||
/* Wayland requires XDG_RUNTIME_DIR to be set. */
|
||||
if (!getenv("XDG_RUNTIME_DIR")) {
|
||||
wlr_log(WLR_ERROR, "XDG_RUNTIME_DIR is not set in the environment");
|
||||
return 1;
|
||||
}
|
||||
|
||||
server.wl_display = wl_display_create();
|
||||
if (!server.wl_display) {
|
||||
wlr_log(WLR_ERROR, "Cannot allocate a Wayland display");
|
||||
return 1;
|
||||
}
|
||||
|
||||
event_loop = wl_display_get_event_loop(server.wl_display);
|
||||
sigint_source = wl_event_loop_add_signal(event_loop, SIGINT, handle_signal, &server.wl_display);
|
||||
sigterm_source = wl_event_loop_add_signal(event_loop, SIGTERM, handle_signal, &server.wl_display);
|
||||
|
||||
backend = wlr_backend_autocreate(server.wl_display, NULL);
|
||||
if (!backend) {
|
||||
wlr_log(WLR_ERROR, "Unable to create the wlroots backend");
|
||||
ret = 1;
|
||||
goto end;
|
||||
}
|
||||
|
||||
if (!drop_permissions()) {
|
||||
ret = 1;
|
||||
goto end;
|
||||
}
|
||||
|
||||
renderer = wlr_backend_get_renderer(backend);
|
||||
wlr_renderer_init_wl_display(renderer, server.wl_display);
|
||||
|
||||
compositor = wlr_compositor_create(server.wl_display, renderer);
|
||||
if (!compositor) {
|
||||
wlr_log(WLR_ERROR, "Unable to create the wlroots compositor");
|
||||
ret = 1;
|
||||
goto end;
|
||||
}
|
||||
|
||||
server.output_layout = wlr_output_layout_create();
|
||||
if (!server.output_layout) {
|
||||
wlr_log(WLR_ERROR, "Unable to create output layout");
|
||||
ret = 1;
|
||||
goto end;
|
||||
}
|
||||
|
||||
wl_list_init(&server.outputs);
|
||||
server.new_output.notify = handle_new_output;
|
||||
wl_signal_add(&backend->events.new_output, &server.new_output);
|
||||
|
||||
const char *socket = wl_display_add_socket_auto(server.wl_display);
|
||||
if (!socket) {
|
||||
wlr_log_errno(WLR_ERROR, "Unable to open Wayland socket");
|
||||
ret = 1;
|
||||
goto end;
|
||||
}
|
||||
|
||||
if (!wlr_backend_start(backend)) {
|
||||
wlr_log(WLR_ERROR, "Unable to start the wlroots backend");
|
||||
ret = 1;
|
||||
goto end;
|
||||
}
|
||||
|
||||
if (setenv("WAYLAND_DISPLAY", socket, true) < 0) {
|
||||
wlr_log_errno(WLR_ERROR, "Unable to set WAYLAND_DISPLAY. Clients may not be able to connect");
|
||||
} else {
|
||||
wlr_log(WLR_DEBUG, "Cage " CAGE_VERSION " is running on Wayland display %s", socket);
|
||||
}
|
||||
|
||||
if (!spawn_primary_client(server.wl_display, argv + optind, &pid, &sigchld_source)) {
|
||||
ret = 1;
|
||||
goto end;
|
||||
}
|
||||
|
||||
wl_display_run(server.wl_display);
|
||||
wl_display_destroy_clients(server.wl_display);
|
||||
|
||||
end:
|
||||
cleanup_primary_client(pid);
|
||||
|
||||
wl_event_source_remove(sigint_source);
|
||||
wl_event_source_remove(sigterm_source);
|
||||
if (sigchld_source) {
|
||||
wl_event_source_remove(sigchld_source);
|
||||
}
|
||||
/* This function is not null-safe, but we only ever get here
|
||||
with a proper wl_display. */
|
||||
wl_display_destroy(server.wl_display);
|
||||
wlr_output_layout_destroy(server.output_layout);
|
||||
return ret;
|
||||
}
|
||||
184
desktop/output.c
Normal file
184
desktop/output.c
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
* Cage: A Wayland kiosk.
|
||||
*
|
||||
* Copyright (C) 2018-2020 Jente Hidskes
|
||||
* Copyright (C) 2019 The Sway authors
|
||||
*
|
||||
* See the LICENSE file accompanying this file.
|
||||
*/
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdlib.h>
|
||||
#include <wayland-server-core.h>
|
||||
#include <wlr/backend.h>
|
||||
#include <wlr/backend/wayland.h>
|
||||
#if WLR_HAS_X11_BACKEND
|
||||
#include <wlr/backend/x11.h>
|
||||
#endif
|
||||
#include <wlr/types/wlr_box.h>
|
||||
#include <wlr/types/wlr_output.h>
|
||||
#include <wlr/types/wlr_output_damage.h>
|
||||
#include <wlr/util/log.h>
|
||||
#include <wlr/util/region.h>
|
||||
|
||||
#include "output.h"
|
||||
|
||||
static void
|
||||
handle_output_damage_destroy(struct wl_listener *listener, void *user_data)
|
||||
{
|
||||
struct cg_output *output = wl_container_of(listener, output, damage_destroy);
|
||||
|
||||
if (output->wlr_output->enabled) {
|
||||
cage_output_disable(output);
|
||||
}
|
||||
|
||||
wl_list_remove(&output->damage_destroy.link);
|
||||
}
|
||||
|
||||
static void
|
||||
handle_output_transform(struct wl_listener *listener, void *user_data)
|
||||
{
|
||||
struct cg_output *output = wl_container_of(listener, output, transform);
|
||||
|
||||
assert(!output->wlr_output->enabled);
|
||||
|
||||
// no-op
|
||||
}
|
||||
|
||||
static void
|
||||
handle_output_mode(struct wl_listener *listener, void *user_data)
|
||||
{
|
||||
struct cg_output *output = wl_container_of(listener, output, mode);
|
||||
|
||||
assert(!output->wlr_output->enabled);
|
||||
|
||||
// no-op
|
||||
}
|
||||
|
||||
static void
|
||||
handle_output_destroy(struct wl_listener *listener, void *user_data)
|
||||
{
|
||||
struct cg_output *output = wl_container_of(listener, output, destroy);
|
||||
cage_output_fini(output);
|
||||
}
|
||||
|
||||
void
|
||||
cage_output_get_geometry(struct cg_output *output, struct wlr_box *geometry)
|
||||
{
|
||||
assert(output != NULL);
|
||||
assert(geometry != NULL);
|
||||
|
||||
wlr_output_effective_resolution(output->wlr_output, &geometry->width, &geometry->height);
|
||||
}
|
||||
|
||||
void
|
||||
cage_output_disable(struct cg_output *output)
|
||||
{
|
||||
assert(output != NULL);
|
||||
assert(output->wlr_output->enabled);
|
||||
|
||||
struct wlr_output *wlr_output = output->wlr_output;
|
||||
|
||||
wlr_log(WLR_DEBUG, "Disabling output %s", wlr_output->name);
|
||||
|
||||
wl_list_remove(&output->mode.link);
|
||||
wl_list_init(&output->mode.link);
|
||||
wl_list_remove(&output->transform.link);
|
||||
wl_list_init(&output->transform.link);
|
||||
wl_list_remove(&output->damage_frame.link);
|
||||
wl_list_init(&output->damage_frame.link);
|
||||
|
||||
wlr_output_rollback(wlr_output);
|
||||
wlr_output_enable(wlr_output, false);
|
||||
wlr_output_commit(wlr_output);
|
||||
}
|
||||
|
||||
void
|
||||
cage_output_enable(struct cg_output *output)
|
||||
{
|
||||
assert(output != NULL);
|
||||
/* Outputs get enabled by the backend before firing the new_output event,
|
||||
* so we can't do a check for already enabled outputs here unless we
|
||||
* duplicate the enabled property in cg_output. */
|
||||
|
||||
struct wlr_output *wlr_output = output->wlr_output;
|
||||
|
||||
wlr_log(WLR_DEBUG, "Enabling output %s", wlr_output->name);
|
||||
|
||||
wl_list_remove(&output->mode.link);
|
||||
output->mode.notify = handle_output_mode;
|
||||
wl_signal_add(&wlr_output->events.mode, &output->mode);
|
||||
wl_list_remove(&output->transform.link);
|
||||
output->transform.notify = handle_output_transform;
|
||||
wl_signal_add(&wlr_output->events.transform, &output->transform);
|
||||
wl_list_remove(&output->damage_frame.link);
|
||||
// output->damage_frame.notify = handle_output_damage_frame;
|
||||
wl_signal_add(&output->damage->events.frame, &output->damage_frame);
|
||||
|
||||
wlr_output_enable(wlr_output, true);
|
||||
wlr_output_commit(wlr_output);
|
||||
wlr_output_damage_add_whole(output->damage);
|
||||
}
|
||||
|
||||
void
|
||||
cage_output_init(struct cg_output *output, struct wlr_output *wlr_output)
|
||||
{
|
||||
assert(output != NULL);
|
||||
assert(wlr_output != NULL);
|
||||
|
||||
output->wlr_output = wlr_output;
|
||||
wlr_output->data = output;
|
||||
output->damage = wlr_output_damage_create(wlr_output);
|
||||
wl_list_init(&output->views);
|
||||
|
||||
/* We need to init the lists here because cage_output_enable calls
|
||||
* `wl_list_remove` on these. */
|
||||
wl_list_init(&output->mode.link);
|
||||
wl_list_init(&output->transform.link);
|
||||
wl_list_init(&output->damage_frame.link);
|
||||
|
||||
output->destroy.notify = handle_output_destroy;
|
||||
wl_signal_add(&wlr_output->events.destroy, &output->destroy);
|
||||
output->damage_destroy.notify = handle_output_damage_destroy;
|
||||
wl_signal_add(&output->damage->events.destroy, &output->damage_destroy);
|
||||
|
||||
struct wlr_output_mode *preferred_mode = wlr_output_preferred_mode(wlr_output);
|
||||
if (preferred_mode) {
|
||||
wlr_output_set_mode(wlr_output, preferred_mode);
|
||||
}
|
||||
|
||||
cage_output_enable(output);
|
||||
}
|
||||
|
||||
void
|
||||
cage_output_fini(struct cg_output *output)
|
||||
{
|
||||
assert(output != NULL);
|
||||
|
||||
if (output->wlr_output->enabled) {
|
||||
cage_output_disable(output);
|
||||
}
|
||||
|
||||
wl_list_remove(&output->destroy.link);
|
||||
wl_list_remove(&output->link);
|
||||
|
||||
free(output);
|
||||
}
|
||||
|
||||
void
|
||||
cage_output_set_window_title(struct cg_output *output, const char *title)
|
||||
{
|
||||
assert(output != NULL);
|
||||
assert(title != NULL);
|
||||
assert(output->wlr_output->enabled);
|
||||
|
||||
struct wlr_output *wlr_output = output->wlr_output;
|
||||
|
||||
if (wlr_output_is_wl(wlr_output)) {
|
||||
wlr_wl_output_set_title(wlr_output, title);
|
||||
#if WLR_HAS_X11_BACKEND
|
||||
} else if (wlr_output_is_x11(wlr_output)) {
|
||||
wlr_x11_output_set_title(wlr_output, title);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
34
desktop/output.h
Normal file
34
desktop/output.h
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
#ifndef CG_OUTPUT_H
|
||||
#define CG_OUTPUT_H
|
||||
|
||||
#include <wayland-server-core.h>
|
||||
#include <wlr/types/wlr_box.h>
|
||||
#include <wlr/types/wlr_output.h>
|
||||
#include <wlr/types/wlr_output_damage.h>
|
||||
|
||||
struct cg_output {
|
||||
struct wlr_output *wlr_output;
|
||||
struct wlr_output_damage *damage;
|
||||
|
||||
/**
|
||||
* The views on this output. Ordered from top to bottom.
|
||||
*/
|
||||
struct wl_list views;
|
||||
|
||||
struct wl_listener mode;
|
||||
struct wl_listener transform;
|
||||
struct wl_listener destroy;
|
||||
struct wl_listener damage_frame;
|
||||
struct wl_listener damage_destroy;
|
||||
|
||||
struct wl_list link; // cg_server::outputs
|
||||
};
|
||||
|
||||
void cage_output_get_geometry(struct cg_output *output, struct wlr_box *geometry);
|
||||
void cage_output_disable(struct cg_output *output);
|
||||
void cage_output_enable(struct cg_output *output);
|
||||
void cage_output_init(struct cg_output *output, struct wlr_output *wlr_output);
|
||||
void cage_output_fini(struct cg_output *output);
|
||||
void cage_output_set_window_title(struct cg_output *output, const char *title);
|
||||
|
||||
#endif
|
||||
38
meson.build
38
meson.build
|
|
@ -1,5 +1,5 @@
|
|||
project('cage', 'c',
|
||||
version: '0.1.2',
|
||||
version: '0.2.0',
|
||||
license: 'MIT',
|
||||
default_options: [
|
||||
'c_std=c11',
|
||||
|
|
@ -119,39 +119,27 @@ if scdoc.found()
|
|||
endforeach
|
||||
endif
|
||||
|
||||
cage_sources = [
|
||||
'cage.c',
|
||||
'idle_inhibit_v1.c',
|
||||
'output.c',
|
||||
'render.c',
|
||||
'seat.c',
|
||||
'util.c',
|
||||
'view.c',
|
||||
'xdg_shell.c',
|
||||
cageng_sources = [
|
||||
'desktop/output.c',
|
||||
'cageng.c',
|
||||
]
|
||||
|
||||
cage_headers = [
|
||||
cageng_headers = [
|
||||
configure_file(input: 'config.h.in',
|
||||
output: 'config.h',
|
||||
configuration: conf_data),
|
||||
'idle_inhibit_v1.h',
|
||||
'output.h',
|
||||
'render.h',
|
||||
'seat.h',
|
||||
'server.h',
|
||||
'util.h',
|
||||
'view.h',
|
||||
'xdg_shell.h',
|
||||
'desktop/output.h',
|
||||
'serverng.h',
|
||||
]
|
||||
|
||||
if conf_data.get('CAGE_HAS_XWAYLAND', 0) == 1
|
||||
cage_sources += 'xwayland.c'
|
||||
cage_headers += 'xwayland.h'
|
||||
endif
|
||||
# if conf_data.get('CAGE_HAS_XWAYLAND', 0) == 1
|
||||
# cage_sources += 'xwayland.c'
|
||||
# cage_headers += 'xwayland.h'
|
||||
# endif
|
||||
|
||||
executable(
|
||||
meson.project_name(),
|
||||
cage_sources + cage_headers,
|
||||
meson.project_name() + 'ng',
|
||||
cageng_sources + cageng_headers,
|
||||
dependencies: [
|
||||
server_protos,
|
||||
wayland_server,
|
||||
|
|
|
|||
18
serverng.h
Normal file
18
serverng.h
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#ifndef CG_SERVER_H
|
||||
#define CG_SERVER_H
|
||||
|
||||
#include <wayland-server-core.h>
|
||||
#include <wlr/types/wlr_output_layout.h>
|
||||
|
||||
#include "desktop/output.h"
|
||||
|
||||
struct cg_server {
|
||||
struct wl_display *wl_display;
|
||||
|
||||
/* Includes disabled outputs. */
|
||||
struct wl_list outputs; // cg_output::link
|
||||
struct wlr_output_layout *output_layout;
|
||||
struct wl_listener new_output;
|
||||
};
|
||||
|
||||
#endif
|
||||
Loading…
Add table
Add a link
Reference in a new issue