├── .gitignore ├── LICENSE ├── README.md ├── main.c ├── meson.build └── protocol ├── meson.build └── wlr-output-management-unstable-v1.xml /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /build-* 3 | *.log 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Purism SPC 2 | Copyright (c) 2019 The wlr-randr Contributors 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wlr-randr 2 | 3 | Utility to manage outputs of a Wayland compositor. 4 | 5 | > **Heads up!** This project has moved over [to SourceHut](https://sr.ht/~emersion/wlr-randr/). 6 | 7 | ## Building 8 | 9 | Install dependencies: 10 | 11 | * meson (compile-time dependency) 12 | * wayland 13 | 14 | Then run: 15 | 16 | meson build 17 | ninja -C build 18 | build/wlr-randr 19 | 20 | ## License 21 | 22 | MIT 23 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | #define _POSIX_C_SOURCE 200809L 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "wlr-output-management-unstable-v1-client-protocol.h" 12 | 13 | struct randr_state; 14 | struct randr_head; 15 | 16 | struct randr_mode { 17 | struct randr_head *head; 18 | struct zwlr_output_mode_v1 *wlr_mode; 19 | struct wl_list link; 20 | 21 | int32_t width, height; 22 | int32_t refresh; // mHz 23 | bool preferred; 24 | }; 25 | 26 | struct randr_head { 27 | struct randr_state *state; 28 | struct zwlr_output_head_v1 *wlr_head; 29 | struct wl_list link; 30 | 31 | char *name, *description; 32 | int32_t phys_width, phys_height; // mm 33 | struct wl_list modes; 34 | 35 | bool enabled; 36 | struct randr_mode *mode; 37 | struct { 38 | int32_t width, height; 39 | int32_t refresh; 40 | } custom_mode; 41 | int32_t x, y; 42 | enum wl_output_transform transform; 43 | double scale; 44 | }; 45 | 46 | struct randr_state { 47 | struct zwlr_output_manager_v1 *output_manager; 48 | 49 | struct wl_list heads; 50 | uint32_t serial; 51 | bool running; 52 | bool failed; 53 | }; 54 | 55 | static const char *output_transform_map[] = { 56 | [WL_OUTPUT_TRANSFORM_NORMAL] = "normal", 57 | [WL_OUTPUT_TRANSFORM_90] = "90", 58 | [WL_OUTPUT_TRANSFORM_180] = "180", 59 | [WL_OUTPUT_TRANSFORM_270] = "270", 60 | [WL_OUTPUT_TRANSFORM_FLIPPED] = "flipped", 61 | [WL_OUTPUT_TRANSFORM_FLIPPED_90] = "flipped-90", 62 | [WL_OUTPUT_TRANSFORM_FLIPPED_180] = "flipped-180", 63 | [WL_OUTPUT_TRANSFORM_FLIPPED_270] = "flipped-270", 64 | }; 65 | 66 | static void print_state(struct randr_state *state) { 67 | struct randr_head *head; 68 | wl_list_for_each(head, &state->heads, link) { 69 | printf("%s \"%s\"\n", head->name, head->description); 70 | if (head->phys_width > 0 && head->phys_height > 0) { 71 | printf(" Physical size: %dx%d mm\n", 72 | head->phys_width, head->phys_height); 73 | } 74 | printf(" Enabled: %s\n", head->enabled ? "yes" : "no"); 75 | if (!wl_list_empty(&head->modes)) { 76 | printf(" Modes:\n"); 77 | struct randr_mode *mode; 78 | wl_list_for_each(mode, &head->modes, link) { 79 | printf(" %dx%d px", mode->width, mode->height); 80 | if (mode->refresh > 0) { 81 | printf(", %f Hz", (float)mode->refresh / 1000); 82 | } 83 | bool current = head->mode == mode; 84 | if (current || mode->preferred) { 85 | printf(" ("); 86 | if (mode->preferred) { 87 | printf("preferred"); 88 | } 89 | if (current && mode->preferred) { 90 | printf(", "); 91 | } 92 | if (current) { 93 | printf("current"); 94 | } 95 | printf(")"); 96 | } 97 | printf("\n"); 98 | } 99 | } 100 | 101 | if (!head->enabled) { 102 | continue; 103 | } 104 | 105 | printf(" Position: %d,%d\n", head->x, head->y); 106 | printf(" Transform: %s\n", output_transform_map[head->transform]); 107 | printf(" Scale: %f\n", head->scale); 108 | } 109 | 110 | state->running = false; 111 | } 112 | 113 | static void config_handle_succeeded(void *data, 114 | struct zwlr_output_configuration_v1 *config) { 115 | struct randr_state *state = data; 116 | zwlr_output_configuration_v1_destroy(config); 117 | state->running = false; 118 | } 119 | 120 | static void config_handle_failed(void *data, 121 | struct zwlr_output_configuration_v1 *config) { 122 | struct randr_state *state = data; 123 | zwlr_output_configuration_v1_destroy(config); 124 | state->running = false; 125 | state->failed = true; 126 | 127 | fprintf(stderr, "failed to apply configuration\n"); 128 | } 129 | 130 | static void config_handle_cancelled(void *data, 131 | struct zwlr_output_configuration_v1 *config) { 132 | struct randr_state *state = data; 133 | zwlr_output_configuration_v1_destroy(config); 134 | state->running = false; 135 | state->failed = true; 136 | 137 | fprintf(stderr, "configuration cancelled, please try again\n"); 138 | } 139 | 140 | static const struct zwlr_output_configuration_v1_listener config_listener = { 141 | .succeeded = config_handle_succeeded, 142 | .failed = config_handle_failed, 143 | .cancelled = config_handle_cancelled, 144 | }; 145 | 146 | static void apply_state(struct randr_state *state, bool dry_run) { 147 | struct zwlr_output_configuration_v1 *config = 148 | zwlr_output_manager_v1_create_configuration(state->output_manager, 149 | state->serial); 150 | zwlr_output_configuration_v1_add_listener(config, &config_listener, state); 151 | 152 | struct randr_head *head; 153 | wl_list_for_each(head, &state->heads, link) { 154 | if (!head->enabled) { 155 | zwlr_output_configuration_v1_disable_head(config, head->wlr_head); 156 | continue; 157 | } 158 | 159 | struct zwlr_output_configuration_head_v1 *config_head = 160 | zwlr_output_configuration_v1_enable_head(config, head->wlr_head); 161 | if (head->mode != NULL) { 162 | zwlr_output_configuration_head_v1_set_mode(config_head, 163 | head->mode->wlr_mode); 164 | } else { 165 | zwlr_output_configuration_head_v1_set_custom_mode(config_head, 166 | head->custom_mode.width, head->custom_mode.height, 167 | head->custom_mode.refresh); 168 | } 169 | zwlr_output_configuration_head_v1_set_position(config_head, 170 | head->x, head->y); 171 | zwlr_output_configuration_head_v1_set_transform(config_head, 172 | head->transform); 173 | zwlr_output_configuration_head_v1_set_scale(config_head, 174 | wl_fixed_from_double(head->scale)); 175 | } 176 | 177 | if (dry_run) { 178 | zwlr_output_configuration_v1_test(config); 179 | } else { 180 | zwlr_output_configuration_v1_apply(config); 181 | } 182 | } 183 | 184 | static void mode_handle_size(void *data, struct zwlr_output_mode_v1 *wlr_mode, 185 | int32_t width, int32_t height) { 186 | struct randr_mode *mode = data; 187 | mode->width = width; 188 | mode->height = height; 189 | } 190 | 191 | static void mode_handle_refresh(void *data, 192 | struct zwlr_output_mode_v1 *wlr_mode, int32_t refresh) { 193 | struct randr_mode *mode = data; 194 | mode->refresh = refresh; 195 | } 196 | 197 | static void mode_handle_preferred(void *data, 198 | struct zwlr_output_mode_v1 *wlr_mode) { 199 | struct randr_mode *mode = data; 200 | mode->preferred = true; 201 | } 202 | 203 | static void mode_handle_finished(void *data, 204 | struct zwlr_output_mode_v1 *wlr_mode) { 205 | struct randr_mode *mode = data; 206 | wl_list_remove(&mode->link); 207 | zwlr_output_mode_v1_destroy(mode->wlr_mode); 208 | free(mode); 209 | } 210 | 211 | static const struct zwlr_output_mode_v1_listener mode_listener = { 212 | .size = mode_handle_size, 213 | .refresh = mode_handle_refresh, 214 | .preferred = mode_handle_preferred, 215 | .finished = mode_handle_finished, 216 | }; 217 | 218 | static void head_handle_name(void *data, 219 | struct zwlr_output_head_v1 *wlr_head, const char *name) { 220 | struct randr_head *head = data; 221 | head->name = strdup(name); 222 | } 223 | 224 | static void head_handle_description(void *data, 225 | struct zwlr_output_head_v1 *wlr_head, const char *description) { 226 | struct randr_head *head = data; 227 | head->description = strdup(description); 228 | } 229 | 230 | static void head_handle_physical_size(void *data, 231 | struct zwlr_output_head_v1 *wlr_head, int32_t width, int32_t height) { 232 | struct randr_head *head = data; 233 | head->phys_width = width; 234 | head->phys_height = height; 235 | } 236 | 237 | static void head_handle_mode(void *data, 238 | struct zwlr_output_head_v1 *wlr_head, 239 | struct zwlr_output_mode_v1 *wlr_mode) { 240 | struct randr_head *head = data; 241 | 242 | struct randr_mode *mode = calloc(1, sizeof(*mode)); 243 | mode->head = head; 244 | mode->wlr_mode = wlr_mode; 245 | wl_list_insert(&head->modes, &mode->link); 246 | 247 | zwlr_output_mode_v1_add_listener(wlr_mode, &mode_listener, mode); 248 | } 249 | 250 | static void head_handle_enabled(void *data, 251 | struct zwlr_output_head_v1 *wlr_head, int32_t enabled) { 252 | struct randr_head *head = data; 253 | head->enabled = !!enabled; 254 | if (!enabled) { 255 | head->mode = NULL; 256 | } 257 | } 258 | 259 | static void head_handle_current_mode(void *data, 260 | struct zwlr_output_head_v1 *wlr_head, 261 | struct zwlr_output_mode_v1 *wlr_mode) { 262 | struct randr_head *head = data; 263 | struct randr_mode *mode; 264 | wl_list_for_each(mode, &head->modes, link) { 265 | if (mode->wlr_mode == wlr_mode) { 266 | head->mode = mode; 267 | return; 268 | } 269 | } 270 | fprintf(stderr, "received unknown current_mode\n"); 271 | head->mode = NULL; 272 | } 273 | 274 | static void head_handle_position(void *data, 275 | struct zwlr_output_head_v1 *wlr_head, int32_t x, int32_t y) { 276 | struct randr_head *head = data; 277 | head->x = x; 278 | head->y = y; 279 | } 280 | 281 | static void head_handle_transform(void *data, 282 | struct zwlr_output_head_v1 *wlr_head, int32_t transform) { 283 | struct randr_head *head = data; 284 | head->transform = transform; 285 | } 286 | 287 | static void head_handle_scale(void *data, 288 | struct zwlr_output_head_v1 *wlr_head, wl_fixed_t scale) { 289 | struct randr_head *head = data; 290 | head->scale = wl_fixed_to_double(scale); 291 | } 292 | 293 | static void head_handle_finished(void *data, 294 | struct zwlr_output_head_v1 *wlr_head) { 295 | struct randr_head *head = data; 296 | wl_list_remove(&head->link); 297 | zwlr_output_head_v1_destroy(head->wlr_head); 298 | free(head->name); 299 | free(head->description); 300 | free(head); 301 | } 302 | 303 | static const struct zwlr_output_head_v1_listener head_listener = { 304 | .name = head_handle_name, 305 | .description = head_handle_description, 306 | .physical_size = head_handle_physical_size, 307 | .mode = head_handle_mode, 308 | .enabled = head_handle_enabled, 309 | .current_mode = head_handle_current_mode, 310 | .position = head_handle_position, 311 | .transform = head_handle_transform, 312 | .scale = head_handle_scale, 313 | .finished = head_handle_finished, 314 | }; 315 | 316 | static void output_manager_handle_head(void *data, 317 | struct zwlr_output_manager_v1 *manager, 318 | struct zwlr_output_head_v1 *wlr_head) { 319 | struct randr_state *state = data; 320 | 321 | struct randr_head *head = calloc(1, sizeof(*head)); 322 | head->state = state; 323 | head->wlr_head = wlr_head; 324 | head->scale = 1.0; 325 | wl_list_init(&head->modes); 326 | wl_list_insert(&state->heads, &head->link); 327 | 328 | zwlr_output_head_v1_add_listener(wlr_head, &head_listener, head); 329 | } 330 | 331 | static void output_manager_handle_done(void *data, 332 | struct zwlr_output_manager_v1 *manager, uint32_t serial) { 333 | struct randr_state *state = data; 334 | state->serial = serial; 335 | } 336 | 337 | static void output_manager_handle_finished(void *data, 338 | struct zwlr_output_manager_v1 *manager) { 339 | // This space is intentionally left blank 340 | } 341 | 342 | static const struct zwlr_output_manager_v1_listener output_manager_listener = { 343 | .head = output_manager_handle_head, 344 | .done = output_manager_handle_done, 345 | .finished = output_manager_handle_finished, 346 | }; 347 | 348 | static void registry_handle_global(void *data, struct wl_registry *registry, 349 | uint32_t name, const char *interface, uint32_t version) { 350 | struct randr_state *state = data; 351 | 352 | if (strcmp(interface, zwlr_output_manager_v1_interface.name) == 0) { 353 | state->output_manager = wl_registry_bind(registry, name, 354 | &zwlr_output_manager_v1_interface, 1); 355 | zwlr_output_manager_v1_add_listener(state->output_manager, 356 | &output_manager_listener, state); 357 | } 358 | } 359 | 360 | static void registry_handle_global_remove(void *data, 361 | struct wl_registry *registry, uint32_t name) { 362 | // This space is intentionally left blank 363 | } 364 | 365 | static const struct wl_registry_listener registry_listener = { 366 | .global = registry_handle_global, 367 | .global_remove = registry_handle_global_remove, 368 | }; 369 | 370 | static const struct option long_options[] = { 371 | {"help", no_argument, 0, 'h'}, 372 | {"dryrun", no_argument, 0, 0}, 373 | {"output", required_argument, 0, 0}, 374 | {"on", no_argument, 0, 0}, 375 | {"off", no_argument, 0, 0}, 376 | {"mode", required_argument, 0, 0}, 377 | {"preferred", no_argument, 0, 0}, 378 | {"custom-mode", required_argument, 0, 0}, 379 | {"pos", required_argument, 0, 0}, 380 | {"transform", required_argument, 0, 0}, 381 | {"scale", required_argument, 0, 0}, 382 | {0}, 383 | }; 384 | 385 | static bool parse_mode(const char *value, int *width, int *height, 386 | int *refresh) { 387 | *refresh = 0; 388 | 389 | // width + "x" + height 390 | char *cur = (char *)value; 391 | char *end; 392 | *width = strtol(cur, &end, 10); 393 | if (end[0] != 'x' || cur == end) { 394 | fprintf(stderr, "invalid mode: invalid width: %s\n", value); 395 | return false; 396 | } 397 | 398 | cur = end + 1; 399 | *height = strtol(cur, &end, 10); 400 | if (cur == end) { 401 | fprintf(stderr, "invalid mode: invalid height: %s\n", value); 402 | return false; 403 | } 404 | if (end[0] != '\0') { 405 | // whitespace + "px" 406 | cur = end; 407 | while (cur[0] == ' ') { 408 | cur++; 409 | } 410 | if (strncmp(cur, "px", 2) == 0) { 411 | cur += 2; 412 | } 413 | 414 | if (cur[0] != '\0') { 415 | // ("," or "@") + whitespace + refresh 416 | if (cur[0] == ',' || cur[0] == '@') { 417 | cur++; 418 | } else { 419 | fprintf(stderr, "invalid mode: expected refresh rate: %s\n", 420 | value); 421 | return false; 422 | } 423 | while (cur[0] == ' ') { 424 | cur++; 425 | } 426 | double refresh_hz = strtod(cur, &end); 427 | if ((end[0] != '\0' && strcmp(end, "Hz") != 0) || 428 | cur == end || refresh_hz <= 0) { 429 | fprintf(stderr, "invalid mode: invalid refresh rate: %s\n", 430 | value); 431 | return false; 432 | } 433 | 434 | *refresh = round(refresh_hz * 1000); // Hz → mHz 435 | } 436 | } 437 | 438 | return true; 439 | } 440 | 441 | static void fixup_disabled_head(struct randr_head *head) { 442 | if (!head->mode && head->custom_mode.refresh == 0 && 443 | head->custom_mode.width == 0 && 444 | head->custom_mode.height == 0) { 445 | struct randr_mode *mode; 446 | wl_list_for_each(mode, &head->modes, link) { 447 | if (mode->preferred) { 448 | head->mode = mode; 449 | return; 450 | } 451 | } 452 | /* Pick first element if when there's no preferred mode */ 453 | if (!wl_list_empty(&head->modes)) { 454 | head->mode = wl_container_of(head->modes.next, 455 | mode, link); 456 | } 457 | } 458 | } 459 | 460 | static bool parse_output_arg(struct randr_head *head, 461 | const char *name, const char *value) { 462 | if (strcmp(name, "on") == 0) { 463 | if (!head->enabled) { 464 | fixup_disabled_head(head); 465 | } 466 | head->enabled = true; 467 | } else if (strcmp(name, "off") == 0) { 468 | head->enabled = false; 469 | } else if (strcmp(name, "mode") == 0) { 470 | int width, height, refresh; 471 | if (!parse_mode(value, &width, &height, &refresh)) { 472 | return false; 473 | } 474 | 475 | bool found = false; 476 | struct randr_mode *mode; 477 | wl_list_for_each(mode, &head->modes, link) { 478 | if (mode->width == width && mode->height == height && 479 | (refresh == 0 || mode->refresh == refresh)) { 480 | found = true; 481 | break; 482 | } 483 | } 484 | 485 | if (!found) { 486 | fprintf(stderr, "unknown mode: %s\n", value); 487 | return false; 488 | } 489 | 490 | head->mode = mode; 491 | head->custom_mode.width = 0; 492 | head->custom_mode.height = 0; 493 | head->custom_mode.refresh = 0; 494 | } else if (strcmp(name, "preferred") == 0) { 495 | bool found = false; 496 | struct randr_mode *mode; 497 | wl_list_for_each(mode, &head->modes, link) { 498 | if (mode->preferred) { 499 | found = true; 500 | break; 501 | } 502 | } 503 | 504 | if (!found) { 505 | fprintf(stderr, "no preferred mode found\n"); 506 | return false; 507 | } 508 | 509 | head->mode = mode; 510 | head->custom_mode.width = 0; 511 | head->custom_mode.height = 0; 512 | head->custom_mode.refresh = 0; 513 | } else if (strcmp(name, "custom-mode") == 0) { 514 | int width, height, refresh; 515 | if (!parse_mode(value, &width, &height, &refresh)) { 516 | return false; 517 | } 518 | 519 | head->mode = NULL; 520 | head->custom_mode.width = width; 521 | head->custom_mode.height = height; 522 | head->custom_mode.refresh = refresh; 523 | } else if (strcmp(name, "pos") == 0) { 524 | char *cur = (char *)value; 525 | char *end; 526 | int x = strtol(cur, &end, 10); 527 | if (end[0] != ',' || cur == end) { 528 | fprintf(stderr, "invalid position: %s\n", value); 529 | return false; 530 | } 531 | 532 | cur = end + 1; 533 | int y = strtol(cur, &end, 10); 534 | if (end[0] != '\0') { 535 | fprintf(stderr, "invalid position: %s\n", value); 536 | return false; 537 | } 538 | 539 | head->x = x; 540 | head->y = y; 541 | } else if (strcmp(name, "transform") == 0) { 542 | bool found = false; 543 | size_t len = 544 | sizeof(output_transform_map) / sizeof(output_transform_map[0]); 545 | for (size_t i = 0; i < len; ++i) { 546 | if (strcmp(output_transform_map[i], value) == 0) { 547 | found = true; 548 | head->transform = i; 549 | break; 550 | } 551 | } 552 | 553 | if (!found) { 554 | fprintf(stderr, "invalid transform: %s\n", value); 555 | return false; 556 | } 557 | } else if (strcmp(name, "scale") == 0) { 558 | char *end; 559 | double scale = strtod(value, &end); 560 | if (end[0] != '\0' || value == end) { 561 | fprintf(stderr, "invalid scale: %s\n", value); 562 | return false; 563 | } 564 | 565 | head->scale = scale; 566 | } else { 567 | fprintf(stderr, "invalid option: %s\n", name); 568 | return false; 569 | } 570 | 571 | return true; 572 | } 573 | 574 | static const char usage[] = 575 | "usage: wlr-randr [options…]\n" 576 | "--help\n" 577 | "--dryrun\n" 578 | "--output \n" 579 | " --on\n" 580 | " --off\n" 581 | " --mode|--custom-mode x[@Hz]\n" 582 | " --preferred\n" 583 | " --pos ,\n" 584 | " --transform normal|90|180|270|flipped|flipped-90|flipped-180|flipped-270\n" 585 | " --scale \n"; 586 | 587 | int main(int argc, char *argv[]) { 588 | struct randr_state state = { .running = true }; 589 | wl_list_init(&state.heads); 590 | 591 | struct wl_display *display = wl_display_connect(NULL); 592 | if (display == NULL) { 593 | fprintf(stderr, "failed to connect to display\n"); 594 | return EXIT_FAILURE; 595 | } 596 | 597 | struct wl_registry *registry = wl_display_get_registry(display); 598 | wl_registry_add_listener(registry, ®istry_listener, &state); 599 | wl_display_dispatch(display); 600 | wl_display_roundtrip(display); 601 | 602 | if (state.output_manager == NULL) { 603 | fprintf(stderr, "compositor doesn't support " 604 | "wlr-output-management-unstable-v1\n"); 605 | return EXIT_FAILURE; 606 | } 607 | 608 | while (state.serial == 0) { 609 | if (wl_display_dispatch(display) < 0) { 610 | fprintf(stderr, "wl_display_dispatch failed\n"); 611 | return EXIT_FAILURE; 612 | } 613 | } 614 | 615 | bool changed = false, dry_run = false; 616 | struct randr_head *current_head = NULL; 617 | while (1) { 618 | int option_index = -1; 619 | int c = getopt_long(argc, argv, "h", long_options, &option_index); 620 | if (c < 0) { 621 | break; 622 | } else if (c == '?') { 623 | return EXIT_FAILURE; 624 | } else if (c == 'h') { 625 | fprintf(stderr, "%s", usage); 626 | return EXIT_SUCCESS; 627 | } 628 | 629 | const char *name = long_options[option_index].name; 630 | const char *value = optarg; 631 | if (strcmp(name, "output") == 0) { 632 | bool found = false; 633 | wl_list_for_each(current_head, &state.heads, link) { 634 | if (strcmp(current_head->name, value) == 0) { 635 | found = true; 636 | break; 637 | } 638 | } 639 | if (!found) { 640 | fprintf(stderr, "unknown output %s\n", value); 641 | return EXIT_FAILURE; 642 | } 643 | } else if (strcmp(name, "dryrun") == 0) { 644 | dry_run = true; 645 | } else { // output sub-option 646 | if (current_head == NULL) { 647 | fprintf(stderr, "no --output specified before --%s\n", name); 648 | return EXIT_FAILURE; 649 | } 650 | 651 | if (!parse_output_arg(current_head, name, value)) { 652 | return EXIT_FAILURE; 653 | } 654 | 655 | changed = true; 656 | } 657 | } 658 | 659 | if (changed) { 660 | apply_state(&state, dry_run); 661 | } else { 662 | print_state(&state); 663 | } 664 | 665 | while (state.running && wl_display_dispatch(display) != -1) { 666 | // This space intentionally left blank 667 | } 668 | 669 | // TODO: destroy heads 670 | zwlr_output_manager_v1_destroy(state.output_manager); 671 | wl_registry_destroy(registry); 672 | wl_display_disconnect(display); 673 | 674 | return state.failed ? EXIT_FAILURE : EXIT_SUCCESS; 675 | } 676 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'wlr-randr', 3 | 'c', 4 | version : '0.2.0', 5 | license : 'MIT', 6 | meson_version : '>=0.47.0', 7 | default_options : ['c_std=c99', 'warning_level=3', 'werror=true'] 8 | ) 9 | 10 | cc = meson.get_compiler('c') 11 | 12 | add_project_arguments(cc.get_supported_arguments([ 13 | '-Wundef', 14 | '-Wlogical-op', 15 | '-Wmissing-include-dirs', 16 | '-Wold-style-definition', 17 | '-Wpointer-arith', 18 | '-Winit-self', 19 | '-Wfloat-equal', 20 | '-Wstrict-prototypes', 21 | '-Wredundant-decls', 22 | '-Wimplicit-fallthrough=2', 23 | '-Wendif-labels', 24 | '-Wstrict-aliasing=2', 25 | '-Woverflow', 26 | '-Wformat=2', 27 | 28 | '-Wno-missing-braces', 29 | '-Wno-missing-field-initializers', 30 | '-Wno-unused-parameter', 31 | ]), language: 'c') 32 | 33 | wayland_client = dependency('wayland-client') 34 | 35 | math = cc.find_library('m', required: false) 36 | 37 | subdir('protocol') 38 | 39 | wlr_randr_exe = executable( 40 | meson.project_name(), 41 | files(['main.c']), 42 | dependencies: [wayland_client, client_protos, math], 43 | install: true, 44 | ) 45 | -------------------------------------------------------------------------------- /protocol/meson.build: -------------------------------------------------------------------------------- 1 | wayland_scanner = find_program('wayland-scanner') 2 | 3 | # should check wayland_scanner's version, but it is hard to get 4 | if wayland_client.version().version_compare('>=1.14.91') 5 | code_type = 'private-code' 6 | else 7 | code_type = 'code' 8 | endif 9 | 10 | wayland_scanner_code = generator( 11 | wayland_scanner, 12 | output: '@BASENAME@-protocol.c', 13 | arguments: [code_type, '@INPUT@', '@OUTPUT@'], 14 | ) 15 | 16 | wayland_scanner_client = generator( 17 | wayland_scanner, 18 | output: '@BASENAME@-client-protocol.h', 19 | arguments: ['client-header', '@INPUT@', '@OUTPUT@'], 20 | ) 21 | 22 | client_protocols = [ 23 | ['wlr-output-management-unstable-v1.xml'], 24 | ] 25 | 26 | client_protos_src = [] 27 | client_protos_headers = [] 28 | 29 | foreach p : client_protocols 30 | xml = join_paths(p) 31 | client_protos_src += wayland_scanner_code.process(xml) 32 | client_protos_headers += wayland_scanner_client.process(xml) 33 | endforeach 34 | 35 | lib_client_protos = static_library( 36 | 'client_protos', 37 | client_protos_src + client_protos_headers, 38 | dependencies: [wayland_client] 39 | ) # for the include directory 40 | 41 | client_protos = declare_dependency( 42 | link_with: lib_client_protos, 43 | sources: client_protos_headers, 44 | ) 45 | -------------------------------------------------------------------------------- /protocol/wlr-output-management-unstable-v1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright © 2019 Purism SPC 5 | 6 | Permission to use, copy, modify, distribute, and sell this 7 | software and its documentation for any purpose is hereby granted 8 | without fee, provided that the above copyright notice appear in 9 | all copies and that both that copyright notice and this permission 10 | notice appear in supporting documentation, and that the name of 11 | the copyright holders not be used in advertising or publicity 12 | pertaining to distribution of the software without specific, 13 | written prior permission. The copyright holders make no 14 | representations about the suitability of this software for any 15 | purpose. It is provided "as is" without express or implied 16 | warranty. 17 | 18 | THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS 19 | SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 20 | FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 22 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 23 | AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, 24 | ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 25 | THIS SOFTWARE. 26 | 27 | 28 | 29 | This protocol exposes interfaces to obtain and modify output device 30 | configuration. 31 | 32 | Warning! The protocol described in this file is experimental and 33 | backward incompatible changes may be made. Backward compatible changes 34 | may be added together with the corresponding interface version bump. 35 | Backward incompatible changes are done by bumping the version number in 36 | the protocol and interface names and resetting the interface version. 37 | Once the protocol is to be declared stable, the 'z' prefix and the 38 | version number in the protocol and interface names are removed and the 39 | interface version number is reset. 40 | 41 | 42 | 43 | 44 | This interface is a manager that allows reading and writing the current 45 | output device configuration. 46 | 47 | Output devices that display pixels (e.g. a physical monitor or a virtual 48 | output in a window) are represented as heads. Heads cannot be created nor 49 | destroyed by the client, but they can be enabled or disabled and their 50 | properties can be changed. Each head may have one or more available modes. 51 | 52 | Whenever a head appears (e.g. a monitor is plugged in), it will be 53 | advertised via the head event. Immediately after the output manager is 54 | bound, all current heads are advertised. 55 | 56 | Whenever a head's properties change, the relevant wlr_output_head events 57 | will be sent. Not all head properties will be sent: only properties that 58 | have changed need to. 59 | 60 | Whenever a head disappears (e.g. a monitor is unplugged), a 61 | wlr_output_head.finished event will be sent. 62 | 63 | After one or more heads appear, change or disappear, the done event will 64 | be sent. It carries a serial which can be used in a create_configuration 65 | request to update heads properties. 66 | 67 | The information obtained from this protocol should only be used for output 68 | configuration purposes. This protocol is not designed to be a generic 69 | output property advertisement protocol for regular clients. Instead, 70 | protocols such as xdg-output should be used. 71 | 72 | 73 | 74 | 75 | This event introduces a new head. This happens whenever a new head 76 | appears (e.g. a monitor is plugged in) or after the output manager is 77 | bound. 78 | 79 | 80 | 81 | 82 | 83 | 84 | This event is sent after all information has been sent after binding to 85 | the output manager object and after any subsequent changes. This applies 86 | to child head and mode objects as well. In other words, this event is 87 | sent whenever a head or mode is created or destroyed and whenever one of 88 | their properties has been changed. Not all state is re-sent each time 89 | the current configuration changes: only the actual changes are sent. 90 | 91 | This allows changes to the output configuration to be seen as atomic, 92 | even if they happen via multiple events. 93 | 94 | A serial is sent to be used in a future create_configuration request. 95 | 96 | 97 | 98 | 99 | 100 | 101 | Create a new output configuration object. This allows to update head 102 | properties. 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | Indicates the client no longer wishes to receive events for output 111 | configuration changes. However the compositor may emit further events, 112 | until the finished event is emitted. 113 | 114 | The client must not send any more requests after this one. 115 | 116 | 117 | 118 | 119 | 120 | This event indicates that the compositor is done sending manager events. 121 | The compositor will destroy the object immediately after sending this 122 | event, so it will become invalid and the client should release any 123 | resources associated with it. 124 | 125 | 126 | 127 | 128 | 129 | 130 | A head is an output device. The difference between a wl_output object and 131 | a head is that heads are advertised even if they are turned off. A head 132 | object only advertises properties and cannot be used directly to change 133 | them. 134 | 135 | A head has some read-only properties: modes, name, description and 136 | physical_size. These cannot be changed by clients. 137 | 138 | Other properties can be updated via a wlr_output_configuration object. 139 | 140 | Properties sent via this interface are applied atomically via the 141 | wlr_output_manager.done event. No guarantees are made regarding the order 142 | in which properties are sent. 143 | 144 | 145 | 146 | 147 | This event describes the head name. 148 | 149 | The naming convention is compositor defined, but limited to alphanumeric 150 | characters and dashes (-). Each name is unique among all wlr_output_head 151 | objects, but if a wlr_output_head object is destroyed the same name may 152 | be reused later. The names will also remain consistent across sessions 153 | with the same hardware and software configuration. 154 | 155 | Examples of names include 'HDMI-A-1', 'WL-1', 'X11-1', etc. However, do 156 | not assume that the name is a reflection of an underlying DRM 157 | connector, X11 connection, etc. 158 | 159 | If the compositor implements the xdg-output protocol and this head is 160 | enabled, the xdg_output.name event must report the same name. 161 | 162 | The name event is sent after a wlr_output_head object is created. This 163 | event is only sent once per object, and the name does not change over 164 | the lifetime of the wlr_output_head object. 165 | 166 | 167 | 168 | 169 | 170 | 171 | This event describes a human-readable description of the head. 172 | 173 | The description is a UTF-8 string with no convention defined for its 174 | contents. Examples might include 'Foocorp 11" Display' or 'Virtual X11 175 | output via :1'. However, do not assume that the name is a reflection of 176 | the make, model, serial of the underlying DRM connector or the display 177 | name of the underlying X11 connection, etc. 178 | 179 | If the compositor implements xdg-output and this head is enabled, 180 | the xdg_output.description must report the same description. 181 | 182 | The description event is sent after a wlr_output_head object is created. 183 | This event is only sent once per object, and the description does not 184 | change over the lifetime of the wlr_output_head object. 185 | 186 | 187 | 188 | 189 | 190 | 191 | This event describes the physical size of the head. This event is only 192 | sent if the head has a physical size (e.g. is not a projector or a 193 | virtual device). 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | This event introduces a mode for this head. It is sent once per 202 | supported mode. 203 | 204 | 205 | 206 | 207 | 208 | 209 | This event describes whether the head is enabled. A disabled head is not 210 | mapped to a region of the global compositor space. 211 | 212 | When a head is disabled, some properties (current_mode, position, 213 | transform and scale) are irrelevant. 214 | 215 | 216 | 217 | 218 | 219 | 220 | This event describes the mode currently in use for this head. It is only 221 | sent if the output is enabled. 222 | 223 | 224 | 225 | 226 | 227 | 228 | This events describes the position of the head in the global compositor 229 | space. It is only sent if the output is enabled. 230 | 231 | 233 | 235 | 236 | 237 | 238 | 239 | This event describes the transformation currently applied to the head. 240 | It is only sent if the output is enabled. 241 | 242 | 243 | 244 | 245 | 246 | 247 | This events describes the scale of the head in the global compositor 248 | space. It is only sent if the output is enabled. 249 | 250 | 251 | 252 | 253 | 254 | 255 | The compositor will destroy the object immediately after sending this 256 | event, so it will become invalid and the client should release any 257 | resources associated with it. 258 | 259 | 260 | 261 | 262 | 263 | 264 | This object describes an output mode. 265 | 266 | Some heads don't support output modes, in which case modes won't be 267 | advertised. 268 | 269 | Properties sent via this interface are applied atomically via the 270 | wlr_output_manager.done event. No guarantees are made regarding the order 271 | in which properties are sent. 272 | 273 | 274 | 275 | 276 | This event describes the mode size. The size is given in physical 277 | hardware units of the output device. This is not necessarily the same as 278 | the output size in the global compositor space. For instance, the output 279 | may be scaled or transformed. 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | This event describes the mode's fixed vertical refresh rate. It is only 288 | sent if the mode has a fixed refresh rate. 289 | 290 | 291 | 292 | 293 | 294 | 295 | This event advertises this mode as preferred. 296 | 297 | 298 | 299 | 300 | 301 | The compositor will destroy the object immediately after sending this 302 | event, so it will become invalid and the client should release any 303 | resources associated with it. 304 | 305 | 306 | 307 | 308 | 309 | 310 | This object is used by the client to describe a full output configuration. 311 | 312 | First, the client needs to setup the output configuration. Each head can 313 | be either enabled (and configured) or disabled. It is a protocol error to 314 | send two enable_head or disable_head requests with the same head. It is a 315 | protocol error to omit a head in a configuration. 316 | 317 | Then, the client can apply or test the configuration. The compositor will 318 | then reply with a succeeded, failed or cancelled event. Finally the client 319 | should destroy the configuration object. 320 | 321 | 322 | 323 | 325 | 327 | 329 | 330 | 331 | 332 | 333 | Enable a head. This request creates a head configuration object that can 334 | be used to change the head's properties. 335 | 336 | 338 | 340 | 341 | 342 | 343 | 344 | Disable a head. 345 | 346 | 348 | 349 | 350 | 351 | 352 | Apply the new output configuration. 353 | 354 | In case the configuration is successfully applied, there is no guarantee 355 | that the new output state matches completely the requested 356 | configuration. For instance, a compositor might round the scale if it 357 | doesn't support fractional scaling. 358 | 359 | After this request has been sent, the compositor must respond with an 360 | succeeded, failed or cancelled event. Sending a request that isn't the 361 | destructor is a protocol error. 362 | 363 | 364 | 365 | 366 | 367 | Test the new output configuration. The configuration won't be applied, 368 | but will only be validated. 369 | 370 | Even if the compositor succeeds to test a configuration, applying it may 371 | fail. 372 | 373 | After this request has been sent, the compositor must respond with an 374 | succeeded, failed or cancelled event. Sending a request that isn't the 375 | destructor is a protocol error. 376 | 377 | 378 | 379 | 380 | 381 | Sent after the compositor has successfully applied the changes or 382 | tested them. 383 | 384 | Upon receiving this event, the client should destroy this object. 385 | 386 | If the current configuration has changed, events to describe the changes 387 | will be sent followed by a wlr_output_manager.done event. 388 | 389 | 390 | 391 | 392 | 393 | Sent if the compositor rejects the changes or failed to apply them. The 394 | compositor should revert any changes made by the apply request that 395 | triggered this event. 396 | 397 | Upon receiving this event, the client should destroy this object. 398 | 399 | 400 | 401 | 402 | 403 | Sent if the compositor cancels the configuration because the state of an 404 | output changed and the client has outdated information (e.g. after an 405 | output has been hotplugged). 406 | 407 | The client can create a new configuration with a newer serial and try 408 | again. 409 | 410 | Upon receiving this event, the client should destroy this object. 411 | 412 | 413 | 414 | 415 | 416 | Using this request a client can tell the compositor that it is not going 417 | to use the configuration object anymore. Any changes to the outputs 418 | that have not been applied will be discarded. 419 | 420 | This request also destroys wlr_output_configuration_head objects created 421 | via this object. 422 | 423 | 424 | 425 | 426 | 427 | 428 | This object is used by the client to update a single head's configuration. 429 | 430 | It is a protocol error to set the same property twice. 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | This request sets the head's mode. 444 | 445 | 446 | 447 | 448 | 449 | 450 | This request assigns a custom mode to the head. The size is given in 451 | physical hardware units of the output device. If set to zero, the 452 | refresh rate is unspecified. 453 | 454 | It is a protocol error to set both a mode and a custom mode. 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | This request sets the head's position in the global compositor space. 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | This request sets the head's transform. 472 | 473 | 474 | 475 | 476 | 477 | 478 | This request sets the head's scale. 479 | 480 | 481 | 482 | 483 | 484 | --------------------------------------------------------------------------------