commit a57e3f2c4d88f258c2f038343a398c6d5be1fb59 Author: fy59 Date: Sat Oct 10 06:26:32 2026 +0200 chore: import SwayFX 0.6 base diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a1c73fe --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +# For the full list of code style requirements, see CONTRIBUTING.md + +root = true + +[*] +charset = utf-8 +end_of_line = lf + +[*.{c,h,cmake,txt}] +indent_style = tab +indent_size = 4 + +[*.{xml,yml}] +indent_style = space +indent_size = 2 + +[config] +indent_style = space +indent_size = 4 + +[*.md] +trim_trailing_whitespace = false diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..e1a63b9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,34 @@ +--- +name: Bugs +about: Crashes and other bugs +labels: 'bug' + +--- + +### Please read the following before submitting: +- Proprietary graphics drivers, including nvidia, are not supported. Please use the open source equivalents, such as nouveau, if you would like to use Sway. +- Please do NOT submit issues for information from the github wiki. The github wiki is community maintained and therefore may contain outdated information, scripts that don't work or obsolete workarounds. + If you fix a script or find outdated information, don't hesitate to adjust the wiki page. + +### Please fill out the following: +- **Swayfx Version:** + - `sway -v` + +- **Debug Log:** + - Run `sway -d 2> ~/sway.log` from a TTY and upload it to a pastebin, such as gist.github.com. + - This will record information about sway's activity. Please try to keep the reproduction as brief as possible and exit sway. + - Attach the **full** file, do not truncate it. + +- **Configuration File:** + - Please try to produce with the default configuration. + - If you cannot reproduce with the default configuration, please try to find the minimal configuration to reproduce. + - Upload the config to a pastebin such as gist.github.com. + +- **Stack Trace:** + - This is only needed if sway crashes. + - If you use systemd, you should be able to open the coredump of the most recent crash with gdb with + `coredumpctl gdb sway` and then `bt full` to obtain the stack trace. + - If the lines mentioning sway or wlroots have `??` for the location, your binaries were built without debug symbols. Please compile both sway and wlroots from source and try to reproduce. + +- **Description:** + - The steps you took in plain English to reproduce the problem. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..99d680b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,2 @@ +blank_issues_enabled: false + diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/enhancement.md new file mode 100644 index 0000000..46ebb73 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enhancement.md @@ -0,0 +1,10 @@ +--- +name: Enhancements +about: New functionality +labels: 'enhancement' + +--- + +### Please fill out the following: +- **Description:** +Please describe in plain English what the enhancement is and what the use case is. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..446beea --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +install_manifest.txt +*.swp +*.o +*.a +bin/ +test/ +build/ +build-*/ +.cache/ +.lvimrc +config-debug +wayland-*-protocol.* +/subprojects/wlroots +subprojects +result + +# ignore the build output from build.sh +opt/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4f043f7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,245 @@ +# Contributing to sway + +Contributing just involves sending a pull request. You will probably be more +successful with your contribution if you visit #sway-devel on Libera Chat +upfront and discuss your plans. + +Note: rules are made to be broken. Adjust or ignore any/all of these as you see +fit, but be prepared to justify it to your peers. + +## Scope of future changes to sway + +**Important**: Sway has completed its core value proposition: it is a fully +featured Wayland-compatible replacement for i3. It is not our intention to +expand on the scope of what i3 aims to accomplish. Our priorities now are +increasing the stability, reliability, and performance of sway within its +current scope. For this reason, most new window management feature requests are +not accepted, even if accompanied by a patch. + +## Pull Requests + +If you already have your own pull request habits, feel free to use them. If you +don't, however, allow me to make a suggestion: feature branches pulled from +upstream. Try this: + +1. Fork sway +2. `git clone https://github.com/username/sway && cd sway` +3. `git remote add upstream https://github.com/swaywm/sway` + +You only need to do this once. You're never going to use your fork's master +branch. Instead, when you start working on a feature, do this: + +1. `git fetch upstream` +2. `git checkout -b add-so-and-so-feature upstream/master` +3. Add and commit your changes +4. `git push -u origin add-so-and-so-feature` +5. Make a pull request from your feature branch + +When you submit your pull request, your commit log should do most of the talking +when it comes to describing your changes and their motivation. In addition to +this, your pull request's comments will ideally include a test plan that the +reviewers can use to (1) demonstrate the problem on master, if applicable and +(2) verify that the problem no longer exists with your changes applied (or that +your new features work correctly). Document all of the edge cases you're aware +of so we can adequately test them - then verify the test plan yourself before +submitting. + +## Commit Messages + +Please strive to write good commit messages. Here's some guidelines to follow: + +The first line should be limited to 50 characters and should be a sentence that +completes the thought [When applied, this commit will...] *"Implement +cmd_move"* or *"Fix #742"* or *"Improve performance of arrange_windows on ARM"* +or similar. + +The subsequent lines should be separated from the subject line by a single +blank line, and include optional details. In this you can give justification +for the change, [reference Github +issues](https://help.github.com/articles/closing-issues-via-commit-messages/), +or explain some of the subtler details of your patch. This is important because +when someone finds a line of code they don't understand later, they can use the +`git blame` command to find out what the author was thinking when they wrote +it. It's also easier to review your pull requests if they're separated into +logical commits that have good commit messages and justify themselves in the +extended commit description. + +As a good rule of thumb, anything you might put into the pull request +description on Github is probably fair game for going into the extended commit +message as well. + +See [here](https://chris.beams.io/posts/git-commit/) for more details. + +## Code Review + +When your changes are submitted for review, one or more core committers will +look over them. Smaller changes might be merged with little fanfare, but larger +changes will typically see review from several people. Be prepared to receive +some feedback - you may be asked to make changes to your work. Our code review +process is: + +1. **Triage** the pull request. Do the commit messages make sense? Is a test + plan necessary and/or present? Add anyone as reviewers that you think should + be there (using the relevant GitHub feature, if you have the permissions, or + with an @mention if necessary). +2. **Review** the code. Look for code style violations, naming convention + violations, buffer overflows, memory leaks, logic errors, non-portable code + (including GNU-isms), etc. For significant changes to the public API, loop in + a couple more people for discussion. +3. **Execute** the test plan, if present. +4. **Merge** the pull request when all reviewers approve. +5. **File** follow-up tickets if appropriate. + +## Style Reference + +Sway is written in C with a style similar to the [kernel +style](https://www.kernel.org/doc/Documentation/process/coding-style.rst), but +with a few notable differences. + +Try to keep your code conforming to C11 and POSIX as much as possible, and do +not use GNU extensions. + +### Brackets + +Brackets always go on the same line, including in functions. +Always include brackets for if/while/for, even if it's a single statement. +```c +void function(void) { + if (condition1) { + do_thing1(); + } + + if (condition2) { + do_thing2(); + } else { + do_thing3(); + } +} +``` + +### Indentation + +Indentations are a single tab. + +For long lines that need to be broken, the continuation line should be indented +with an additional tab. + +If the line being broken is opening a new block (functions, if, while, etc.), +the continuation line should be indented with two tabs, so they can't be +misread as being part of the block. + +```c +really_long_function(argument1, argument2, ..., + argument3, argument4); + +if (condition1 && condition2 && ... + condition3 && condition4) { + do_thing(); +} +``` + +Try to break the line in the place which you think is the most appropriate to +balance the lines. + +### Line Length + +Try to keep your lines under 80 columns, assuming a tab width equal to 4 spaces, +but you can go up to 100 if it improves readability. Don't break lines +indiscriminately, try to find nice breaking points so your code is easy to read. + +### Names + +Global function and type names should be prefixed with `sway_submodule_` (e.g. +`struct sway_output`, `sway_output_destroy`). For static functions and +types local to a file, the names chosen aren't as important. Static functions +shouldn't have a `sway_` prefix. + +For include guards, use the header's filename relative to include. Uppercase +all of the characters, and replace any invalid characters with an underscore. + +### Construction/Destruction Functions + +For functions that are responsible for constructing and destructing an object, +they should be written as a pair of one of two forms: + +* `init`/`finish`: These initialize/deinitialize a type, but are **NOT** + responsible for allocating it. They should accept a pointer to some + pre-allocated memory (e.g. a member of a struct). +* `create`/`destroy`: These also initialize/deinitialize, but will return a + pointer to a `malloc`ed chunk of memory, and will `free` it in `destroy`. + +A destruction function should always be able to accept a NULL pointer or a +zeroed value and exit cleanly; this simplifies error handling a lot. + +### Error Codes + +For functions not returning a value, they should return a (stdbool.h) bool to +indicated if they succeeded or not. + +### Macros + +Keep the use of macros to a minimum, especially if a function can do the job. If +you do need to use them, try to keep them close to where they're being used and +`#undef` them after. + +### Example + +```c +struct wlr_backend *wlr_backend_autocreate(struct wl_display *display) { + struct wlr_backend *backend; + if (getenv("WAYLAND_DISPLAY") || getenv("_WAYLAND_DISPLAY")) { + backend = attempt_wl_backend(display); + if (backend) { + return backend; + } + } + + const char *x11_display = getenv("DISPLAY"); + if (x11_display) { + return wlr_x11_backend_create(display, x11_display); + } + + // Attempt DRM+libinput + + struct wlr_session *session = wlr_session_create(display); + if (!session) { + wlr_log(WLR_ERROR, "Failed to start a DRM session"); + return NULL; + } + + int gpu = wlr_session_find_gpu(session); + if (gpu == -1) { + wlr_log(WLR_ERROR, "Failed to open DRM device"); + goto error_session; + } + + backend = wlr_multi_backend_create(session); + if (!backend) { + goto error_gpu; + } + + struct wlr_backend *libinput = wlr_libinput_backend_create(display, session); + if (!libinput) { + goto error_multi; + } + + struct wlr_backend *drm = wlr_drm_backend_create(display, session, gpu); + if (!drm) { + goto error_libinput; + } + + wlr_multi_backend_add(backend, libinput); + wlr_multi_backend_add(backend, drm); + return backend; + +error_libinput: + wlr_backend_destroy(libinput); +error_multi: + wlr_backend_destroy(backend); +error_gpu: + wlr_session_close_file(session, gpu); +error_session: + wlr_session_destroy(session); + return NULL; +} +``` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e5caa47 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,95 @@ +# syntax=docker.io/docker/dockerfile:1 + +ARG BASE_IMAGE=docker.io/debian:trixie +FROM ${BASE_IMAGE} + +SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"] + +ENV LANG=C.UTF-8 + +RUN < **Note:** This guide targets **SwayFX 0.5.3** (based on Sway 1.11). It manually builds the required dependencies (**wlroots 0.19.0** and **scenefx 0.4.1**) using Meson subprojects to ensure compatibility and stability. + +## 1\. Install Dependencies + +Debian Trixie requires a specific set of development headers, particularly for the newer `wlroots` 0.19 backend (which needs `liblcms2`, `libliftoff`, and `libdisplay-info`). + +```bash +sudo apt update && sudo apt install -y \ + meson pkg-config cmake git scdoc \ + wayland-protocols libwayland-dev libpcre2-dev libjson-c-dev \ + libpango1.0-dev libcairo2-dev libgdk-pixbuf-2.0-dev \ + libdrm-dev libgbm-dev libinput-dev libseat-dev libxkbcommon-dev \ + libxcb-dri3-dev libxcb-present-dev libxcb-res0-dev \ + libxcb-render-util0-dev libxcb-ewmh-dev libxcb-icccm4-dev \ + libliftoff-dev libdisplay-info-dev liblcms2-dev libpixman-1-dev \ + libgles2-mesa-dev hwdata libudev-dev +``` + +## 2\. Setup Build Environment + +We will use the **Meson Subprojects** method. This places the specific versions of `wlroots` and `scenefx` directly inside the build folder, preventing conflicts with system libraries. + +```bash +# Create a clean build directory +mkdir -p ~/build +cd ~/build + +# 1. Clone SwayFX 0.5.3 (The Window Manager) +git clone https://github.com/WillPower3309/swayfx.git +cd swayfx +git checkout 0.5.3 + +# 2. Setup Subprojects Directory +mkdir subprojects +cd subprojects + +# 3. Clone SceneFX 0.4.1 (The Rendering FX Library) +# Note: 0.4.1 is required for wlroots 0.19 support +git clone https://github.com/wlrfx/scenefx.git +cd scenefx +git checkout 0.4.1 +cd .. + +# 4. Clone Wlroots 0.19.0 (The Wayland Compositor Backend) +git clone https://gitlab.freedesktop.org/wlroots/wlroots.git +cd wlroots +git checkout 0.19.0 + +# Return to source root +cd ../.. +``` + +Your directory structure should now look like this: + +```text +~/build/swayfx +└── subprojects + ├── scenefx (v0.4.1) + └── wlroots (v0.19.0) +``` + +## 3\. Compile and Install + +Now we build everything in one go. Meson will detect the code in `subprojects/` and build it automatically. + +```bash +# Configure the build +meson setup build/ + +# Compile +ninja -C build/ + +# Install +sudo ninja -C build/ install +``` + +## 4\. Finalize + +Debian does not automatically link new libraries in `/usr/local/lib` immediately. You must update the linker cache to avoid "shared object not found" errors. + +```bash +sudo ldconfig +``` + +## 5\. Verification + +Check that the installation was successful and is using the correct version base. + +```bash +sway --version +# Expected Output: +# swayfx version 0.5.3... (based on sway 1.11.0) +``` + + + Guide updated for Debian 13 / SwayFX 0.5.3 with ♥️ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3e0cacc --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2016-2017 Drew DeVault + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..21e8000 --- /dev/null +++ b/README.md @@ -0,0 +1,166 @@ +

+ swayfx logo +

+ + +Sway is an incredible window manager, and certainly one of the most well established wayland window managers. However, it is restricted to only include the functionality that existed in i3. This fork ditches the simple wlr_renderer, and replaces it with our fx_renderer, capable of rendering with fancy GLES2 effects. This, along with a couple of minor changes, expands sway's featureset to include the following: + ++ **Blur** ++ **Anti-aliased rounded corners, borders, and titlebars** ++ **Shadows** ++ **Dim unfocused windows** ++ **Scratchpad treated as minimize**: Allows docks, or panels with a taskbar, to correctly interpret minimize / unminimize requests ([thanks to LCBCrion](https://github.com/swaywm/sway/issues/6457)) ++ **nixify the repo**: Allows nixos users to easily contribute to and test this project + +

+ + +

+ +For Fedora users, SwayFX is also available in [copr](https://copr.fedorainfracloud.org/coprs/swayfx/swayfx/) or in the [Terra repository](https://terra.fyralabs.com). + +[Join our Discord](https://discord.gg/qsSx397rkh) + +## New Configuration Options + ++ Window blur: + - `blur enable|disable` + - `blur_xray enable|disable`: this will set floating windows to blur based on the background, not the windows below. You probably want to set this to `disable` :) + - `blur_passes ` + - `blur_radius ` + - `blur_noise ` + - `blur_brightness ` + - `blur_contrast ` + - `blur_saturation ` ++ Corner radius: `corner_radius ` ++ Window shadows: + - `shadows enable|disable` + - `shadows_on_csd enable|disable` (**Note**: The shadow might not fit some windows) + - `shadow_blur_radius ` + - `shadow_color ex, #0000007F` + - `shadow_offset ` + - `shadow_inactive_color ex, #0000007F` ++ LayerShell effects (to blur panels / notifications etc): + - `layer_effects ` + - The current layer namespaces can be shown with `swaymsg -r -t get_outputs | jq '.[0].layer_shell_surfaces | .[] | .namespace'` + - Example: `layer_effects "waybar" blur enable; shadows enable; corner_radius 6` + - Note: If an application uses gtk, its namespace is likely to be "gtk-layer-shell" + - SwayIPC Example: `swaymsg layer_effects "waybar" "blur enable"` (you can only set one effect at a time through `swaymsg`) + - Config Example: + ``` + layer_effects "waybar" { + blur enable; + blur_xray enable; + blur_ignore_transparent enable; + shadows enable; + corner_radius 20; + } + ``` + - Available Effects: + - `blur ` + - `blur_xray ` + - `blur_ignore_transparent ` + - `shadows ` + - `corner_radius ` + - `reset`: To reset/disable all previously applied effects to the layer application ++ Dim unfocused windows: + - `default_dim_inactive ` + - `for_window [CRITERIA_HERE] dim_inactive ` + - `dim_inactive_colors.unfocused ex, #000000FF` + - `dim_inactive_colors.urgent ex, #900000FF` ++ Keep/remove separator border between titlebar and content: `titlebar_separator enable|disable` ++ Treat Scratchpad as minimized: `scratchpad_minimize enable|disable`: **we recommend keeping this setting off, as there are many kinks to iron out here** + +## Roadmap + ++ fade in / out animations ++ window movement animations + +## Compiling From Source + +### Nix + +If you have Nix installed, you can build and run SwayFX easily: + +``` +nix build +./result/bin/sway +``` + +You can also bring up a development shell and follow the build instructions below, without installing all of the dependencies manually: + +``` +nix develop +``` +### Debian + +Check [INSTALL-deb.md](/INSTALL-deb.md) + +### Container (Docker) + +To build SwayFX inside a container, install Docker and run the following commands from inside this repository: + +```bash +# build the container and run it (which builds SwayFX) +docker compose up --build + +# become root to install the previously built files +sudo bash + +# copy all files into the host file system at /usr/local/ +while read -r FILE; do + NEW_FILE=/usr/local${FILE#opt} + NEW_DIR=$(dirname "${NEW_FILE}") + [[ -d ${NEW_DIR} ]] || mkdir -p "${NEW_DIR}" + + echo "Creating '${NEW_FILE}'" + cp "${FILE}" "${NEW_FILE}" +done < <(command find opt/ -type f) +``` + +### Manual Steps + +Install dependencies: + +* meson \* +* wlroots +* wayland +* wayland-protocols \* +* pcre2 +* json-c +* pango +* cairo +* [scenefx](https://github.com/wlrfx/scenefx) +* gdk-pixbuf2 (optional: additional image formats for system tray) +* [swaybg] (optional: wallpaper) +* [scdoc] (optional: man pages) \* +* git (optional: version info) \* + +_\* Compile-time dep_ + +Run these commands: + + meson build/ + ninja -C build/ + sudo ninja -C build/ install + +On systems without logind nor seatd, you need to suid the sway binary: + + sudo chmod a+s /usr/local/bin/sway + +SwayFX will drop root permissions shortly after startup. + +## Contributing + +SwayFX would love to receive any new features that you're willing to build! Generally, we'd like to focus on eye-candy type improvements to keep our scope appropriate. If you'd like to build something that you think may be out of that focus, please raise an issue and we can discuss whether or not it will fit within this project. + +## Acknowledgements + +The SwayFX team would like to first and foremost thank the maintainers and contributors of the Sway window manager. We are but a humble group of Sway enthusiasts who wanted to expand upon your creation. + +We would also like to thank the talented artists in our community for contibuting the visual assets that give this project charm: ++ pkdesuwu and honchokomodo for creating the swayfx mascot: https://twitter.com/pkdesuwu/status/1664517033017368576 ++ spooky_skeleton for the swayfx logo, and [Basil](https://basil.cafe) for making some fine adjustments to it + +Lastly, we would like to thank you, the community, for enjoying and using window manager that we have spent so much time maintaining. + diff --git a/assets/LICENSE b/assets/LICENSE new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/assets/LICENSE @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/assets/Sway_Wallpaper_Blue_1136x640.png b/assets/Sway_Wallpaper_Blue_1136x640.png new file mode 100644 index 0000000..fdab997 Binary files /dev/null and b/assets/Sway_Wallpaper_Blue_1136x640.png differ diff --git a/assets/Sway_Wallpaper_Blue_1136x640_Portrait.png b/assets/Sway_Wallpaper_Blue_1136x640_Portrait.png new file mode 100644 index 0000000..669e76f Binary files /dev/null and b/assets/Sway_Wallpaper_Blue_1136x640_Portrait.png differ diff --git a/assets/Sway_Wallpaper_Blue_1366x768.png b/assets/Sway_Wallpaper_Blue_1366x768.png new file mode 100644 index 0000000..9128406 Binary files /dev/null and b/assets/Sway_Wallpaper_Blue_1366x768.png differ diff --git a/assets/Sway_Wallpaper_Blue_1920x1080.png b/assets/Sway_Wallpaper_Blue_1920x1080.png new file mode 100644 index 0000000..034f004 Binary files /dev/null and b/assets/Sway_Wallpaper_Blue_1920x1080.png differ diff --git a/assets/Sway_Wallpaper_Blue_2048x1536.png b/assets/Sway_Wallpaper_Blue_2048x1536.png new file mode 100644 index 0000000..bcb36c0 Binary files /dev/null and b/assets/Sway_Wallpaper_Blue_2048x1536.png differ diff --git a/assets/Sway_Wallpaper_Blue_2048x1536_Portrait.png b/assets/Sway_Wallpaper_Blue_2048x1536_Portrait.png new file mode 100644 index 0000000..087d5d7 Binary files /dev/null and b/assets/Sway_Wallpaper_Blue_2048x1536_Portrait.png differ diff --git a/assets/Sway_Wallpaper_Blue_768x1024.png b/assets/Sway_Wallpaper_Blue_768x1024.png new file mode 100644 index 0000000..cc05bb9 Binary files /dev/null and b/assets/Sway_Wallpaper_Blue_768x1024.png differ diff --git a/assets/Sway_Wallpaper_Blue_768x1024_Portrait.png b/assets/Sway_Wallpaper_Blue_768x1024_Portrait.png new file mode 100644 index 0000000..6af9053 Binary files /dev/null and b/assets/Sway_Wallpaper_Blue_768x1024_Portrait.png differ diff --git a/assets/swayfx_legacy_mascot_1066x600.png b/assets/swayfx_legacy_mascot_1066x600.png new file mode 100644 index 0000000..717a05a Binary files /dev/null and b/assets/swayfx_legacy_mascot_1066x600.png differ diff --git a/assets/swayfx_logo.svg b/assets/swayfx_logo.svg new file mode 100644 index 0000000..c4d5024 --- /dev/null +++ b/assets/swayfx_logo.svg @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/swayfx_mascot.jpg b/assets/swayfx_mascot.jpg new file mode 100644 index 0000000..da90fb5 Binary files /dev/null and b/assets/swayfx_mascot.jpg differ diff --git a/assets/swayfx_mascot_1366x768.png b/assets/swayfx_mascot_1366x768.png new file mode 100644 index 0000000..778d3bc Binary files /dev/null and b/assets/swayfx_mascot_1366x768.png differ diff --git a/assets/swayfx_mascot_1920x1080.png b/assets/swayfx_mascot_1920x1080.png new file mode 100644 index 0000000..25df92c Binary files /dev/null and b/assets/swayfx_mascot_1920x1080.png differ diff --git a/assets/swayfx_mascot_3840x2160.png b/assets/swayfx_mascot_3840x2160.png new file mode 100644 index 0000000..9e5192f Binary files /dev/null and b/assets/swayfx_mascot_3840x2160.png differ diff --git a/assets/swayfx_mascot_4096x4096.png b/assets/swayfx_mascot_4096x4096.png new file mode 100644 index 0000000..3cf5355 Binary files /dev/null and b/assets/swayfx_mascot_4096x4096.png differ diff --git a/assets/swayfx_screenshot.jpg b/assets/swayfx_screenshot.jpg new file mode 100644 index 0000000..7e90489 Binary files /dev/null and b/assets/swayfx_screenshot.jpg differ diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..276d3c3 --- /dev/null +++ b/build.sh @@ -0,0 +1,40 @@ +#! /bin/bash + +# shellcheck disable=SC2154 + +set -eE -u -o pipefail +shopt -s inherit_errexit + +DEB_BUILD_GNU_TYPE=$(dpkg-architecture -q DEB_BUILD_GNU_TYPE) + +meson setup --prefix ${OPT_SWAYFX} "wlroots-${WLROOTSVERSION}/build/" "wlroots-${WLROOTSVERSION}" +ninja -C "wlroots-${WLROOTSVERSION}/build/" +ninja -C "wlroots-${WLROOTSVERSION}/build/" install +ls -l ${OPT_SWAYFX}/lib/${DEB_BUILD_GNU_TYPE}/pkgconfig +echo "wlroots DONE" + +PKG_CONFIG_PATH="${OPT_SWAYFX}/lib/${DEB_BUILD_GNU_TYPE}/pkgconfig/${PKG_CONFIG_PATH+:${PKG_CONFIG_PATH}}" \ +meson setup --prefix ${OPT_SWAYFX} --cmake-prefix-path ${OPT_SWAYFX} "scenefx-${SCENEFXVERSION}/build/" "scenefx-${SCENEFXVERSION}" +ninja -C "scenefx-${SCENEFXVERSION}/build/" +ninja -C "scenefx-${SCENEFXVERSION}/build/" install +echo "scenefx DONE" + +PKG_CONFIG_PATH="${OPT_SWAYFX}/lib/${DEB_BUILD_GNU_TYPE}/pkgconfig/${PKG_CONFIG_PATH+:${PKG_CONFIG_PATH}}" \ +meson setup --prefix ${OPT_SWAYFX} --cmake-prefix-path ${OPT_SWAYFX} "swayfx-${SWAYFXVERSION}/build/" "swayfx-${SWAYFXVERSION}" +ninja -C "swayfx-${SWAYFXVERSION}/build/" +ninja -C "swayfx-${SWAYFXVERSION}/build/" install +echo "swayfx DONE" + +mv ${OPT_SWAYFX}/bin/sway ${OPT_SWAYFX}/bin/swayfx + +cat >${OPT_SWAYFX}/bin/swayfx.sh < +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "config.h" +#include "pool-buffer.h" +#include "util.h" + +static int anonymous_shm_open(void) { + int retries = 100; + + do { + // try a probably-unique name + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + pid_t pid = getpid(); + char name[50]; + snprintf(name, sizeof(name), "/sway-%x-%x", + (unsigned int)pid, (unsigned int)ts.tv_nsec); + + // shm_open guarantees that O_CLOEXEC is set + int fd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600); + if (fd >= 0) { + shm_unlink(name); + return fd; + } + + --retries; + } while (retries > 0 && errno == EEXIST); + + return -1; +} + +static void buffer_release(void *data, struct wl_buffer *wl_buffer) { + struct pool_buffer *buffer = data; + buffer->busy = false; +} + +static const struct wl_buffer_listener buffer_listener = { + .release = buffer_release +}; + +static struct pool_buffer *create_buffer(struct wl_shm *shm, + struct pool_buffer *buf, int32_t width, int32_t height, + uint32_t format) { + uint32_t stride = width * 4; + size_t size = stride * height; + + int fd = anonymous_shm_open(); + if (fd == -1) { + return NULL; + } + if (ftruncate(fd, size) < 0) { + close(fd); + return NULL; + } + void *data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, size); + buf->buffer = wl_shm_pool_create_buffer(pool, 0, + width, height, stride, format); + wl_shm_pool_destroy(pool); + close(fd); + + buf->size = size; + buf->width = width; + buf->height = height; + buf->data = data; + buf->surface = cairo_image_surface_create_for_data(data, + CAIRO_FORMAT_ARGB32, width, height, stride); + buf->cairo = cairo_create(buf->surface); + buf->pango = pango_cairo_create_context(buf->cairo); + + wl_buffer_add_listener(buf->buffer, &buffer_listener, buf); + return buf; +} + +void destroy_buffer(struct pool_buffer *buffer) { + if (buffer->buffer) { + wl_buffer_destroy(buffer->buffer); + } + if (buffer->cairo) { + cairo_destroy(buffer->cairo); + } + if (buffer->surface) { + cairo_surface_destroy(buffer->surface); + } + if (buffer->pango) { + g_object_unref(buffer->pango); + } + if (buffer->data) { + munmap(buffer->data, buffer->size); + } + memset(buffer, 0, sizeof(struct pool_buffer)); +} + +struct pool_buffer *get_next_buffer(struct wl_shm *shm, + struct pool_buffer pool[static 2], uint32_t width, uint32_t height) { + struct pool_buffer *buffer = NULL; + + for (size_t i = 0; i < 2; ++i) { + if (pool[i].busy) { + continue; + } + buffer = &pool[i]; + } + + if (!buffer) { + return NULL; + } + + if (buffer->width != width || buffer->height != height) { + destroy_buffer(buffer); + } + + if (!buffer->buffer) { + if (!create_buffer(shm, buffer, width, height, + WL_SHM_FORMAT_ARGB8888)) { + return NULL; + } + } + buffer->busy = true; + return buffer; +} diff --git a/common/cairo.c b/common/cairo.c new file mode 100644 index 0000000..7c59d48 --- /dev/null +++ b/common/cairo.c @@ -0,0 +1,44 @@ +#include +#include +#include "cairo_util.h" + +void cairo_set_source_u32(cairo_t *cairo, uint32_t color) { + cairo_set_source_rgba(cairo, + (color >> (3*8) & 0xFF) / 255.0, + (color >> (2*8) & 0xFF) / 255.0, + (color >> (1*8) & 0xFF) / 255.0, + (color >> (0*8) & 0xFF) / 255.0); +} + +cairo_subpixel_order_t to_cairo_subpixel_order(enum wl_output_subpixel subpixel) { + switch (subpixel) { + case WL_OUTPUT_SUBPIXEL_HORIZONTAL_RGB: + return CAIRO_SUBPIXEL_ORDER_RGB; + case WL_OUTPUT_SUBPIXEL_HORIZONTAL_BGR: + return CAIRO_SUBPIXEL_ORDER_BGR; + case WL_OUTPUT_SUBPIXEL_VERTICAL_RGB: + return CAIRO_SUBPIXEL_ORDER_VRGB; + case WL_OUTPUT_SUBPIXEL_VERTICAL_BGR: + return CAIRO_SUBPIXEL_ORDER_VBGR; + default: + return CAIRO_SUBPIXEL_ORDER_DEFAULT; + } + return CAIRO_SUBPIXEL_ORDER_DEFAULT; +} + +cairo_surface_t *cairo_image_surface_scale(cairo_surface_t *image, + int width, int height) { + int image_width = cairo_image_surface_get_width(image); + int image_height = cairo_image_surface_get_height(image); + + cairo_surface_t *new = + cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + cairo_t *cairo = cairo_create(new); + cairo_scale(cairo, (double)width / image_width, + (double)height / image_height); + cairo_set_source_surface(cairo, image, 0, 0); + + cairo_paint(cairo); + cairo_destroy(cairo); + return new; +} diff --git a/common/gesture.c b/common/gesture.c new file mode 100644 index 0000000..272aa83 --- /dev/null +++ b/common/gesture.c @@ -0,0 +1,332 @@ +#include "gesture.h" + +#include +#include +#include +#include +#include +#include "list.h" +#include "log.h" +#include "stringop.h" + +const uint8_t GESTURE_FINGERS_ANY = 0; + +char *gesture_parse(const char *input, struct gesture *output) { + // Clear output in case of failure + output->type = GESTURE_TYPE_NONE; + output->fingers = GESTURE_FINGERS_ANY; + output->directions = GESTURE_DIRECTION_NONE; + + // Split input type, fingers and directions + list_t *split = split_string(input, ":"); + if (split->length < 1 || split->length > 3) { + return format_str( + "expected [:][:direction], got %s", + input); + } + + // Parse gesture type + if (strcmp(split->items[0], "hold") == 0) { + output->type = GESTURE_TYPE_HOLD; + } else if (strcmp(split->items[0], "pinch") == 0) { + output->type = GESTURE_TYPE_PINCH; + } else if (strcmp(split->items[0], "swipe") == 0) { + output->type = GESTURE_TYPE_SWIPE; + } else { + return format_str("expected hold|pinch|swipe, got %s", + (const char *)split->items[0]); + } + + // Parse optional arguments + if (split->length > 1) { + char *next = split->items[1]; + + // Try to parse as finger count (1-9) + if (strlen(next) == 1 && '1' <= next[0] && next[0] <= '9') { + output->fingers = atoi(next); + + // Move to next if available + next = split->length == 3 ? split->items[2] : NULL; + } else if (split->length == 3) { + // Fail here if argument can only be finger count + return format_str("expected 1-9, got %s", next); + } + + // If there is an argument left, try to parse as direction + if (next) { + list_t *directions = split_string(next, "+"); + + for (int i = 0; i < directions->length; ++i) { + const char *item = directions->items[i]; + if (strcmp(item, "any") == 0) { + continue; + } else if (strcmp(item, "up") == 0) { + output->directions |= GESTURE_DIRECTION_UP; + } else if (strcmp(item, "down") == 0) { + output->directions |= GESTURE_DIRECTION_DOWN; + } else if (strcmp(item, "left") == 0) { + output->directions |= GESTURE_DIRECTION_LEFT; + } else if (strcmp(item, "right") == 0) { + output->directions |= GESTURE_DIRECTION_RIGHT; + } else if (strcmp(item, "inward") == 0) { + output->directions |= GESTURE_DIRECTION_INWARD; + } else if (strcmp(item, "outward") == 0) { + output->directions |= GESTURE_DIRECTION_OUTWARD; + } else if (strcmp(item, "clockwise") == 0) { + output->directions |= GESTURE_DIRECTION_CLOCKWISE; + } else if (strcmp(item, "counterclockwise") == 0) { + output->directions |= GESTURE_DIRECTION_COUNTERCLOCKWISE; + } else { + return format_str("expected direction, got %s", item); + } + } + list_free_items_and_destroy(directions); + } + } // if optional args + + list_free_items_and_destroy(split); + + return NULL; +} + +const char *gesture_type_string(enum gesture_type type) { + switch (type) { + case GESTURE_TYPE_NONE: + return "none"; + case GESTURE_TYPE_HOLD: + return "hold"; + case GESTURE_TYPE_PINCH: + return "pinch"; + case GESTURE_TYPE_SWIPE: + return "swipe"; + } + + return NULL; +} + +const char *gesture_direction_string(enum gesture_direction direction) { + switch (direction) { + case GESTURE_DIRECTION_NONE: + return "none"; + case GESTURE_DIRECTION_UP: + return "up"; + case GESTURE_DIRECTION_DOWN: + return "down"; + case GESTURE_DIRECTION_LEFT: + return "left"; + case GESTURE_DIRECTION_RIGHT: + return "right"; + case GESTURE_DIRECTION_INWARD: + return "inward"; + case GESTURE_DIRECTION_OUTWARD: + return "outward"; + case GESTURE_DIRECTION_CLOCKWISE: + return "clockwise"; + case GESTURE_DIRECTION_COUNTERCLOCKWISE: + return "counterclockwise"; + } + + return NULL; +} + +// Helper to turn combination of directions flags into string representation. +static char *gesture_directions_to_string(uint32_t directions) { + char *result = NULL; + + for (uint8_t bit = 0; bit < 32; bit++) { + uint32_t masked = directions & (1 << bit); + if (masked) { + const char *name = gesture_direction_string(masked); + + if (!name) { + name = "unknown"; + } + + if (!result) { + result = strdup(name); + } else { + char *new = format_str("%s+%s", result, name); + free(result); + result = new; + } + } + } + + if(!result) { + return strdup("any"); + } + + return result; +} + +char *gesture_to_string(struct gesture *gesture) { + char *directions = gesture_directions_to_string(gesture->directions); + char *result = format_str("%s:%u:%s", + gesture_type_string(gesture->type), + gesture->fingers, directions); + free(directions); + return result; +} + +bool gesture_check(struct gesture *target, enum gesture_type type, uint8_t fingers) { + // Check that gesture type matches + if (target->type != type) { + return false; + } + + // Check that finger count matches + if (target->fingers != GESTURE_FINGERS_ANY && target->fingers != fingers) { + return false; + } + + return true; +} + +bool gesture_match(struct gesture *target, struct gesture *to_match, bool exact) { + // Check type and fingers + if (!gesture_check(target, to_match->type, to_match->fingers)) { + return false; + } + + // Enforce exact matches ... + if (exact && target->directions != to_match->directions) { + return false; + } + + // ... or ensure all target directions are matched + return (target->directions & to_match->directions) == target->directions; +} + +bool gesture_equal(struct gesture *a, struct gesture *b) { + return a->type == b->type + && a->fingers == b->fingers + && a->directions == b->directions; +} + +// Return count of set bits in directions bit field. +static uint8_t gesture_directions_count(uint32_t directions) { + uint8_t count = 0; + for (; directions; directions >>= 1) { + count += directions & 1; + } + return count; +} + +// Compare direction bit count of two direction. +static int8_t gesture_directions_compare(uint32_t a, uint32_t b) { + return gesture_directions_count(a) - gesture_directions_count(b); +} + +// Compare two direction based on their direction bit count +int8_t gesture_compare(struct gesture *a, struct gesture *b) { + return gesture_directions_compare(a->directions, b->directions); +} + +void gesture_tracker_begin(struct gesture_tracker *tracker, + enum gesture_type type, uint8_t fingers) { + tracker->type = type; + tracker->fingers = fingers; + + tracker->dx = 0.0; + tracker->dy = 0.0; + tracker->scale = 1.0; + tracker->rotation = 0.0; + + sway_log(SWAY_DEBUG, "begin tracking %s:%u:? gesture", + gesture_type_string(type), fingers); +} + +bool gesture_tracker_check(struct gesture_tracker *tracker, enum gesture_type type) { + return tracker->type == type; +} + +void gesture_tracker_update(struct gesture_tracker *tracker, + double dx, double dy, double scale, double rotation) { + if (tracker->type == GESTURE_TYPE_HOLD) { + sway_assert(false, "hold does not update."); + return; + } + + tracker->dx += dx; + tracker->dy += dy; + + if (tracker->type == GESTURE_TYPE_PINCH) { + tracker->scale = scale; + tracker->rotation += rotation; + } + + sway_log(SWAY_DEBUG, "update tracking %s:%u:? gesture: %f %f %f %f", + gesture_type_string(tracker->type), + tracker->fingers, + tracker->dx, tracker->dy, + tracker->scale, tracker->rotation); +} + +void gesture_tracker_cancel(struct gesture_tracker *tracker) { + sway_log(SWAY_DEBUG, "cancel tracking %s:%u:? gesture", + gesture_type_string(tracker->type), tracker->fingers); + + tracker->type = GESTURE_TYPE_NONE; +} + +struct gesture *gesture_tracker_end(struct gesture_tracker *tracker) { + struct gesture *result = calloc(1, sizeof(struct gesture)); + + // Ignore gesture under some threshold + // TODO: Make configurable + const double min_rotation = 5; + const double min_scale_delta = 0.1; + + // Determine direction + switch(tracker->type) { + // Gestures with scale and rotation + case GESTURE_TYPE_PINCH: + if (tracker->rotation > min_rotation) { + result->directions |= GESTURE_DIRECTION_CLOCKWISE; + } + if (tracker->rotation < -min_rotation) { + result->directions |= GESTURE_DIRECTION_COUNTERCLOCKWISE; + } + + if (tracker->scale > (1.0 + min_scale_delta)) { + result->directions |= GESTURE_DIRECTION_OUTWARD; + } + if (tracker->scale < (1.0 - min_scale_delta)) { + result->directions |= GESTURE_DIRECTION_INWARD; + } + __attribute__ ((fallthrough)); + // Gestures with dx and dy + case GESTURE_TYPE_SWIPE: + if (fabs(tracker->dx) > fabs(tracker->dy)) { + if (tracker->dx > 0) { + result->directions |= GESTURE_DIRECTION_RIGHT; + } else { + result->directions |= GESTURE_DIRECTION_LEFT; + } + } else { + if (tracker->dy > 0) { + result->directions |= GESTURE_DIRECTION_DOWN; + } else { + result->directions |= GESTURE_DIRECTION_UP; + } + } + // Gesture without any direction + case GESTURE_TYPE_HOLD: + break; + // Not tracking any gesture + case GESTURE_TYPE_NONE: + sway_assert(false, "Not tracking any gesture."); + return result; + } + + result->type = tracker->type; + result->fingers = tracker->fingers; + + char *description = gesture_to_string(result); + sway_log(SWAY_DEBUG, "end tracking gesture: %s", description); + free(description); + + tracker->type = GESTURE_TYPE_NONE; + + return result; +} diff --git a/common/ipc-client.c b/common/ipc-client.c new file mode 100644 index 0000000..a0be2b2 --- /dev/null +++ b/common/ipc-client.c @@ -0,0 +1,148 @@ +#include +#include +#include +#include +#include +#include +#include +#include "ipc-client.h" +#include "log.h" + +static const char ipc_magic[] = {'i', '3', '-', 'i', 'p', 'c'}; + +#define IPC_HEADER_SIZE (sizeof(ipc_magic) + 8) + +char *get_socketpath(void) { + const char *swaysock = getenv("SWAYSOCK"); + if (swaysock) { + return strdup(swaysock); + } + char *line = NULL; + size_t line_size = 0; + FILE *fp = popen("sway --get-socketpath 2>/dev/null", "r"); + if (fp) { + ssize_t nret = getline(&line, &line_size, fp); + pclose(fp); + if (nret > 0) { + // remove trailing newline, if there is one + if (line[nret - 1] == '\n') { + line[nret - 1] = '\0'; + } + return line; + } + } + const char *i3sock = getenv("I3SOCK"); + if (i3sock) { + free(line); + return strdup(i3sock); + } + fp = popen("i3 --get-socketpath 2>/dev/null", "r"); + if (fp) { + ssize_t nret = getline(&line, &line_size, fp); + pclose(fp); + if (nret > 0) { + // remove trailing newline, if there is one + if (line[nret - 1] == '\n') { + line[nret - 1] = '\0'; + } + return line; + } + } + free(line); + return NULL; +} + +int ipc_open_socket(const char *socket_path) { + struct sockaddr_un addr; + int socketfd; + if ((socketfd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) { + sway_abort("Unable to open Unix socket"); + } + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1); + addr.sun_path[sizeof(addr.sun_path) - 1] = 0; + int l = sizeof(struct sockaddr_un); + if (connect(socketfd, (struct sockaddr *)&addr, l) == -1) { + sway_abort("Unable to connect to %s", socket_path); + } + return socketfd; +} + +bool ipc_set_recv_timeout(int socketfd, struct timeval tv) { + if (setsockopt(socketfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) == -1) { + sway_log_errno(SWAY_ERROR, "Failed to set ipc recv timeout"); + return false; + } + return true; +} + +struct ipc_response *ipc_recv_response(int socketfd) { + char data[IPC_HEADER_SIZE]; + + size_t total = 0; + while (total < IPC_HEADER_SIZE) { + ssize_t received = recv(socketfd, data + total, IPC_HEADER_SIZE - total, 0); + if (received <= 0) { + sway_abort("Unable to receive IPC response"); + } + total += received; + } + + struct ipc_response *response = malloc(sizeof(struct ipc_response)); + if (!response) { + goto error_1; + } + + memcpy(&response->size, data + sizeof(ipc_magic), sizeof(uint32_t)); + memcpy(&response->type, data + sizeof(ipc_magic) + sizeof(uint32_t), sizeof(uint32_t)); + + char *payload = malloc(response->size + 1); + if (!payload) { + goto error_2; + } + + total = 0; + while (total < response->size) { + ssize_t received = recv(socketfd, payload + total, response->size - total, 0); + if (received < 0) { + sway_abort("Unable to receive IPC response"); + } + total += received; + } + payload[response->size] = '\0'; + response->payload = payload; + + return response; +error_2: + free(response); +error_1: + sway_log(SWAY_ERROR, "Unable to allocate memory for IPC response"); + return NULL; +} + +void free_ipc_response(struct ipc_response *response) { + free(response->payload); + free(response); +} + +char *ipc_single_command(int socketfd, uint32_t type, const char *payload, uint32_t *len) { + char data[IPC_HEADER_SIZE]; + memcpy(data, ipc_magic, sizeof(ipc_magic)); + memcpy(data + sizeof(ipc_magic), len, sizeof(*len)); + memcpy(data + sizeof(ipc_magic) + sizeof(*len), &type, sizeof(type)); + + if (write(socketfd, data, IPC_HEADER_SIZE) == -1) { + sway_abort("Unable to send IPC header"); + } + + if (write(socketfd, payload, *len) == -1) { + sway_abort("Unable to send IPC payload"); + } + + struct ipc_response *resp = ipc_recv_response(socketfd); + char *response = resp->payload; + *len = resp->size; + free(resp); + + return response; +} diff --git a/common/list.c b/common/list.c new file mode 100644 index 0000000..c3e2fe2 --- /dev/null +++ b/common/list.c @@ -0,0 +1,160 @@ +#include "list.h" +#include +#include +#include +#include "log.h" + +list_t *create_list(void) { + list_t *list = malloc(sizeof(list_t)); + if (!list) { + return NULL; + } + list->capacity = 10; + list->length = 0; + list->items = malloc(sizeof(void*) * list->capacity); + return list; +} + +static void list_resize(list_t *list) { + if (list->length == list->capacity) { + list->capacity *= 2; + list->items = realloc(list->items, sizeof(void*) * list->capacity); + } +} + +void list_free(list_t *list) { + if (list == NULL) { + return; + } + free(list->items); + free(list); +} + +void list_add(list_t *list, void *item) { + list_resize(list); + list->items[list->length++] = item; +} + +void list_insert(list_t *list, int index, void *item) { + list_resize(list); + memmove(&list->items[index + 1], &list->items[index], sizeof(void*) * (list->length - index)); + list->length++; + list->items[index] = item; +} + +void list_del(list_t *list, int index) { + list->length--; + memmove(&list->items[index], &list->items[index + 1], sizeof(void*) * (list->length - index)); +} + +void list_cat(list_t *list, list_t *source) { + for (int i = 0; i < source->length; ++i) { + list_add(list, source->items[i]); + } +} + +void list_qsort(list_t *list, int compare(const void *left, const void *right)) { + qsort(list->items, list->length, sizeof(void *), compare); +} + +int list_seq_find(list_t *list, int compare(const void *item, const void *data), const void *data) { + for (int i = 0; i < list->length; i++) { + void *item = list->items[i]; + if (compare(item, data) == 0) { + return i; + } + } + return -1; +} + +int list_find(list_t *list, const void *item) { + for (int i = 0; i < list->length; i++) { + if (list->items[i] == item) { + return i; + } + } + return -1; +} + +void list_swap(list_t *list, int src, int dest) { + void *tmp = list->items[src]; + list->items[src] = list->items[dest]; + list->items[dest] = tmp; +} + +void list_move_to_end(list_t *list, void *item) { + int i; + for (i = 0; i < list->length; ++i) { + if (list->items[i] == item) { + break; + } + } + if (!sway_assert(i < list->length, "Item not found in list")) { + return; + } + list_del(list, i); + list_add(list, item); +} + +static void list_rotate(list_t *list, int from, int to) { + void *tmp = list->items[to]; + + while (to > from) { + list->items[to] = list->items[to - 1]; + to--; + } + + list->items[from] = tmp; +} + +static void list_inplace_merge(list_t *list, int left, int last, int mid, int compare(const void *a, const void *b)) { + int right = mid + 1; + + if (compare(&list->items[mid], &list->items[right]) <= 0) { + return; + } + + while (left <= mid && right <= last) { + if (compare(&list->items[left], &list->items[right]) <= 0) { + left++; + } else { + list_rotate(list, left, right); + left++; + mid++; + right++; + } + } +} + +static void list_inplace_sort(list_t *list, int first, int last, int compare(const void *a, const void *b)) { + if (first >= last) { + return; + } else if ((last - first) == 1) { + if (compare(&list->items[first], &list->items[last]) > 0) { + list_swap(list, first, last); + } + } else { + int mid = (int)((last + first) / 2); + list_inplace_sort(list, first, mid, compare); + list_inplace_sort(list, mid + 1, last, compare); + list_inplace_merge(list, first, last, mid, compare); + } +} + +void list_stable_sort(list_t *list, int compare(const void *a, const void *b)) { + if (list->length > 1) { + list_inplace_sort(list, 0, list->length - 1, compare); + } +} + +void list_free_items_and_destroy(list_t *list) { + if (!list) { + return; + } + + for (int i = 0; i < list->length; ++i) { + free(list->items[i]); + } + list_free(list); +} + diff --git a/common/log.c b/common/log.c new file mode 100644 index 0000000..3eacdb3 --- /dev/null +++ b/common/log.c @@ -0,0 +1,125 @@ +#include +#include +#include +#include +#include +#include +#include "log.h" + +static terminate_callback_t log_terminate = exit; + +void _sway_abort(const char *format, ...) { + va_list args; + va_start(args, format); + _sway_vlog(SWAY_ERROR, format, args); + va_end(args); + log_terminate(EXIT_FAILURE); +} + +bool _sway_assert(bool condition, const char *format, ...) { + if (condition) { + return true; + } + + va_list args; + va_start(args, format); + _sway_vlog(SWAY_ERROR, format, args); + va_end(args); + +#ifndef NDEBUG + raise(SIGABRT); +#endif + + return false; +} + +static bool colored = true; +static sway_log_importance_t log_importance = SWAY_ERROR; +static struct timespec start_time = {-1, -1}; + +static const char *verbosity_colors[] = { + [SWAY_SILENT] = "", + [SWAY_ERROR ] = "\x1B[1;31m", + [SWAY_INFO ] = "\x1B[1;34m", + [SWAY_DEBUG ] = "\x1B[1;90m", +}; + +static const char *verbosity_headers[] = { + [SWAY_SILENT] = "", + [SWAY_ERROR] = "[ERROR]", + [SWAY_INFO] = "[INFO]", + [SWAY_DEBUG] = "[DEBUG]", +}; + +static void timespec_sub(struct timespec *r, const struct timespec *a, + const struct timespec *b) { + const long NSEC_PER_SEC = 1000000000; + r->tv_sec = a->tv_sec - b->tv_sec; + r->tv_nsec = a->tv_nsec - b->tv_nsec; + if (r->tv_nsec < 0) { + r->tv_sec--; + r->tv_nsec += NSEC_PER_SEC; + } +} + +static void init_start_time(void) { + if (start_time.tv_sec >= 0) { + return; + } + clock_gettime(CLOCK_MONOTONIC, &start_time); +} + +static void sway_log_stderr(sway_log_importance_t verbosity, const char *fmt, + va_list args) { + init_start_time(); + + if (verbosity > log_importance) { + return; + } + + struct timespec ts = {0}; + clock_gettime(CLOCK_MONOTONIC, &ts); + timespec_sub(&ts, &ts, &start_time); + + fprintf(stderr, "%02d:%02d:%02d.%03ld ", (int)(ts.tv_sec / 60 / 60), + (int)(ts.tv_sec / 60 % 60), (int)(ts.tv_sec % 60), + ts.tv_nsec / 1000000); + + unsigned c = (verbosity < SWAY_LOG_IMPORTANCE_LAST) ? verbosity : + SWAY_LOG_IMPORTANCE_LAST - 1; + + if (colored && isatty(STDERR_FILENO)) { + fprintf(stderr, "%s", verbosity_colors[c]); + } else { + fprintf(stderr, "%s ", verbosity_headers[c]); + } + + vfprintf(stderr, fmt, args); + + if (colored && isatty(STDERR_FILENO)) { + fprintf(stderr, "\x1B[0m"); + } + fprintf(stderr, "\n"); +} + +void sway_log_init(sway_log_importance_t verbosity, terminate_callback_t callback) { + init_start_time(); + + if (verbosity < SWAY_LOG_IMPORTANCE_LAST) { + log_importance = verbosity; + } + if (callback) { + log_terminate = callback; + } +} + +void _sway_vlog(sway_log_importance_t verbosity, const char *fmt, va_list args) { + sway_log_stderr(verbosity, fmt, args); +} + +void _sway_log(sway_log_importance_t verbosity, const char *fmt, ...) { + va_list args; + va_start(args, fmt); + sway_log_stderr(verbosity, fmt, args); + va_end(args); +} diff --git a/common/loop.c b/common/loop.c new file mode 100644 index 0000000..b99c6d5 --- /dev/null +++ b/common/loop.c @@ -0,0 +1,183 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "list.h" +#include "log.h" +#include "loop.h" + +struct loop_fd_event { + void (*callback)(int fd, short mask, void *data); + void *data; +}; + +struct loop_timer { + void (*callback)(void *data); + void *data; + struct timespec expiry; +}; + +struct loop { + struct pollfd *fds; + int fd_length; + int fd_capacity; + + list_t *fd_events; // struct loop_fd_event + list_t *timers; // struct loop_timer +}; + +struct loop *loop_create(void) { + struct loop *loop = calloc(1, sizeof(struct loop)); + if (!loop) { + sway_log(SWAY_ERROR, "Unable to allocate memory for loop"); + return NULL; + } + loop->fd_capacity = 10; + loop->fds = malloc(sizeof(struct pollfd) * loop->fd_capacity); + loop->fd_events = create_list(); + loop->timers = create_list(); + return loop; +} + +void loop_destroy(struct loop *loop) { + list_free_items_and_destroy(loop->fd_events); + list_free_items_and_destroy(loop->timers); + free(loop->fds); + free(loop); +} + +void loop_poll(struct loop *loop) { + // Calculate next timer in ms + int ms = INT_MAX; + if (loop->timers->length) { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + for (int i = 0; i < loop->timers->length; ++i) { + struct loop_timer *timer = loop->timers->items[i]; + int timer_ms = (timer->expiry.tv_sec - now.tv_sec) * 1000; + timer_ms += (timer->expiry.tv_nsec - now.tv_nsec) / 1000000; + if (timer_ms < ms) { + ms = timer_ms; + } + } + } + if (ms < 0) { + ms = 0; + } + + poll(loop->fds, loop->fd_length, ms); + + // Dispatch fds + for (int i = 0; i < loop->fd_length; ++i) { + struct pollfd pfd = loop->fds[i]; + struct loop_fd_event *event = loop->fd_events->items[i]; + + // Always send these events + unsigned events = pfd.events | POLLHUP | POLLERR; + + if (pfd.revents & events) { + event->callback(pfd.fd, pfd.revents, event->data); + } + } + + // Dispatch timers + if (loop->timers->length) { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + for (int i = 0; i < loop->timers->length; ++i) { + struct loop_timer *timer = loop->timers->items[i]; + bool expired = timer->expiry.tv_sec < now.tv_sec || + (timer->expiry.tv_sec == now.tv_sec && + timer->expiry.tv_nsec < now.tv_nsec); + if (expired) { + timer->callback(timer->data); + loop_remove_timer(loop, timer); + --i; + } + } + } +} + +void loop_add_fd(struct loop *loop, int fd, short mask, + void (*callback)(int fd, short mask, void *data), void *data) { + struct loop_fd_event *event = calloc(1, sizeof(struct loop_fd_event)); + if (!event) { + sway_log(SWAY_ERROR, "Unable to allocate memory for event"); + return; + } + event->callback = callback; + event->data = data; + list_add(loop->fd_events, event); + + struct pollfd pfd = {fd, mask, 0}; + + if (loop->fd_length == loop->fd_capacity) { + int capacity = loop->fd_capacity + 10; + struct pollfd *tmp = realloc(loop->fds, + sizeof(struct pollfd) * capacity); + if (!tmp) { + sway_log(SWAY_ERROR, "Unable to allocate memory for pollfd"); + return; + } + loop->fds = tmp; + loop->fd_capacity = capacity; + } + + loop->fds[loop->fd_length++] = pfd; +} + +struct loop_timer *loop_add_timer(struct loop *loop, int ms, + void (*callback)(void *data), void *data) { + struct loop_timer *timer = calloc(1, sizeof(struct loop_timer)); + if (!timer) { + sway_log(SWAY_ERROR, "Unable to allocate memory for timer"); + return NULL; + } + timer->callback = callback; + timer->data = data; + + clock_gettime(CLOCK_MONOTONIC, &timer->expiry); + timer->expiry.tv_sec += ms / 1000; + + long int nsec = (ms % 1000) * 1000000; + if (timer->expiry.tv_nsec + nsec >= 1000000000) { + timer->expiry.tv_sec++; + nsec -= 1000000000; + } + timer->expiry.tv_nsec += nsec; + + list_add(loop->timers, timer); + + return timer; +} + +bool loop_remove_fd(struct loop *loop, int fd) { + for (int i = 0; i < loop->fd_length; ++i) { + if (loop->fds[i].fd == fd) { + free(loop->fd_events->items[i]); + list_del(loop->fd_events, i); + + loop->fd_length--; + memmove(&loop->fds[i], &loop->fds[i + 1], + sizeof(struct pollfd) * (loop->fd_length - i)); + + return true; + } + } + return false; +} + +bool loop_remove_timer(struct loop *loop, struct loop_timer *timer) { + for (int i = 0; i < loop->timers->length; ++i) { + if (loop->timers->items[i] == timer) { + list_del(loop->timers, i); + free(timer); + return true; + } + } + return false; +} diff --git a/common/meson.build b/common/meson.build new file mode 100644 index 0000000..c0ce1f6 --- /dev/null +++ b/common/meson.build @@ -0,0 +1,21 @@ +lib_sway_common = static_library( + 'sway-common', + files( + 'cairo.c', + 'gesture.c', + 'ipc-client.c', + 'log.c', + 'loop.c', + 'list.c', + 'pango.c', + 'stringop.c', + 'util.c' + ), + dependencies: [ + cairo, + pango, + pangocairo, + wayland_client.partial_dependency(compile_args: true) + ], + include_directories: sway_inc +) diff --git a/common/pango.c b/common/pango.c new file mode 100644 index 0000000..41f8b76 --- /dev/null +++ b/common/pango.c @@ -0,0 +1,171 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "cairo_util.h" +#include "log.h" +#include "stringop.h" + +size_t escape_markup_text(const char *src, char *dest) { + size_t length = 0; + if (dest) { + dest[0] = '\0'; + } + + while (src[0]) { + switch (src[0]) { + case '&': + length += 5; + lenient_strcat(dest, "&"); + break; + case '<': + length += 4; + lenient_strcat(dest, "<"); + break; + case '>': + length += 4; + lenient_strcat(dest, ">"); + break; + case '\'': + length += 6; + lenient_strcat(dest, "'"); + break; + case '"': + length += 6; + lenient_strcat(dest, """); + break; + default: + if (dest) { + dest[length] = *src; + dest[length + 1] = '\0'; + } + length += 1; + } + src++; + } + return length; +} + +PangoLayout *get_pango_layout(cairo_t *cairo, const PangoFontDescription *desc, + const char *text, double scale, bool markup) { + PangoLayout *layout = pango_cairo_create_layout(cairo); + pango_context_set_round_glyph_positions(pango_layout_get_context(layout), false); + + PangoAttrList *attrs; + if (markup) { + char *buf; + GError *error = NULL; + if (pango_parse_markup(text, -1, 0, &attrs, &buf, NULL, &error)) { + pango_layout_set_text(layout, buf, -1); + free(buf); + } else { + sway_log(SWAY_ERROR, "pango_parse_markup '%s' -> error %s", text, + error->message); + g_error_free(error); + markup = false; // fallback to plain text + } + } + if (!markup) { + attrs = pango_attr_list_new(); + pango_layout_set_text(layout, text, -1); + } + + pango_attr_list_insert(attrs, pango_attr_scale_new(scale)); + pango_layout_set_font_description(layout, desc); + pango_layout_set_single_paragraph_mode(layout, 1); + pango_layout_set_attributes(layout, attrs); + pango_attr_list_unref(attrs); + return layout; +} + +void get_text_size(cairo_t *cairo, const PangoFontDescription *desc, int *width, int *height, + int *baseline, double scale, bool markup, const char *fmt, ...) { + if (width) { + *width = 0; + } + if (height) { + *height = 0; + } + if (baseline) { + *baseline = 0; + } + + va_list args; + va_start(args, fmt); + char *buf = vformat_str(fmt, args); + va_end(args); + if (buf == NULL) { + sway_log(SWAY_ERROR, "Failed to format string"); + return; + } + + PangoLayout *layout = get_pango_layout(cairo, desc, buf, scale, markup); + pango_cairo_update_layout(cairo, layout); + cairo_status_t status = cairo_status(cairo); + if (status != CAIRO_STATUS_SUCCESS) { + sway_log(SWAY_ERROR, "pango_cairo_update_layout() failed: %s", + cairo_status_to_string(status)); + goto out; + } + + pango_layout_get_pixel_size(layout, width, height); + if (baseline) { + *baseline = pango_layout_get_baseline(layout) / PANGO_SCALE; + } + +out: + g_object_unref(layout); + + free(buf); +} + +void get_text_metrics(const PangoFontDescription *description, int *height, int *baseline) { + PangoFontMap *fontmap = pango_cairo_font_map_get_default(); + PangoContext *pango = pango_font_map_create_context(fontmap); + pango_context_set_round_glyph_positions(pango, false); + // When passing NULL as a language, pango uses the current locale. + PangoFontMetrics *metrics = pango_context_get_metrics(pango, description, NULL); + + *baseline = pango_font_metrics_get_ascent(metrics) / PANGO_SCALE; + *height = *baseline + pango_font_metrics_get_descent(metrics) / PANGO_SCALE; + + pango_font_metrics_unref(metrics); + g_object_unref(pango); +} + +void render_text(cairo_t *cairo, const PangoFontDescription *desc, + double scale, bool markup, const char *fmt, ...) { + va_list args; + va_start(args, fmt); + char *buf = vformat_str(fmt, args); + va_end(args); + if (buf == NULL) { + sway_log(SWAY_ERROR, "Failed to format string"); + return; + } + + PangoLayout *layout = get_pango_layout(cairo, desc, buf, scale, markup); + cairo_font_options_t *fo = cairo_font_options_create(); + cairo_get_font_options(cairo, fo); + pango_cairo_context_set_font_options(pango_layout_get_context(layout), fo); + cairo_font_options_destroy(fo); + + pango_cairo_update_layout(cairo, layout); + cairo_status_t status = cairo_status(cairo); + if (status != CAIRO_STATUS_SUCCESS) { + sway_log(SWAY_ERROR, "pango_cairo_update_layout() failed: %s", + cairo_status_to_string(status)); + goto out; + } + + pango_cairo_show_layout(cairo, layout); + +out: + g_object_unref(layout); + + free(buf); +} diff --git a/common/stringop.c b/common/stringop.c new file mode 100644 index 0000000..dcbd165 --- /dev/null +++ b/common/stringop.c @@ -0,0 +1,366 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "list.h" +#include "log.h" +#include "stringop.h" + +static const char whitespace[] = " \f\n\r\t\v"; + +void strip_whitespace(char *str) { + size_t len = strlen(str); + size_t start = strspn(str, whitespace); + memmove(str, &str[start], len + 1 - start); + + if (*str) { + for (len -= start + 1; isspace(str[len]); --len) {} + str[len + 1] = '\0'; + } +} + +void strip_quotes(char *str) { + bool in_str = false; + bool in_chr = false; + bool escaped = false; + char *end = strchr(str,0); + while (*str) { + if (*str == '\'' && !in_str && !escaped) { + in_chr = !in_chr; + goto shift_over; + } else if (*str == '\"' && !in_chr && !escaped) { + in_str = !in_str; + goto shift_over; + } else if (*str == '\\') { + escaped = !escaped; + ++str; + continue; + } + escaped = false; + ++str; + continue; + shift_over: + memmove(str, str+1, end-- - str); + } + *end = '\0'; +} + +char *lenient_strcat(char *dest, const char *src) { + if (dest && src) { + return strcat(dest, src); + } + return dest; +} + +char *lenient_strncat(char *dest, const char *src, size_t len) { + if (dest && src) { + return strncat(dest, src, len); + } + return dest; +} + +// strcmp that also handles null pointers. +int lenient_strcmp(const char *a, const char *b) { + if (a == b) { + return 0; + } else if (!a) { + return -1; + } else if (!b) { + return 1; + } else { + return strcmp(a, b); + } +} + +list_t *split_string(const char *str, const char *delims) { + list_t *res = create_list(); + char *copy = strdup(str); + + char *token = strtok(copy, delims); + while (token) { + list_add(res, strdup(token)); + token = strtok(NULL, delims); + } + free(copy); + return res; +} + +char **split_args(const char *start, int *argc) { + *argc = 0; + int alloc = 2; + char **argv = malloc(sizeof(char *) * alloc); + bool in_token = false; + bool in_string = false; + bool in_char = false; + bool in_brackets = false; // brackets are used for criteria + bool escaped = false; + const char *end = start; + if (start) { + while (*start) { + if (!in_token) { + start = (end += strspn(end, whitespace)); + in_token = true; + } + if (*end == '"' && !in_char && !escaped) { + in_string = !in_string; + } else if (*end == '\'' && !in_string && !escaped) { + in_char = !in_char; + } else if (*end == '[' && !in_string && !in_char && !in_brackets && !escaped) { + in_brackets = true; + } else if (*end == ']' && !in_string && !in_char && in_brackets && !escaped) { + in_brackets = false; + } else if (*end == '\\') { + escaped = !escaped; + } else if (*end == '\0' || (!in_string && !in_char && !in_brackets + && !escaped && strchr(whitespace, *end))) { + goto add_token; + } + if (*end != '\\') { + escaped = false; + } + ++end; + continue; + add_token: + if (end - start > 0) { + char *token = malloc(end - start + 1); + strncpy(token, start, end - start + 1); + token[end - start] = '\0'; + argv[*argc] = token; + if (++*argc + 1 == alloc) { + argv = realloc(argv, (alloc *= 2) * sizeof(char *)); + } + } + in_token = false; + escaped = false; + } + } + argv[*argc] = NULL; + return argv; +} + +void free_argv(int argc, char **argv) { + while (argc-- > 0) { + free(argv[argc]); + } + free(argv); +} + +int unescape_string(char *string) { + /* TODO: More C string escapes */ + int len = strlen(string); + int i; + for (i = 0; string[i]; ++i) { + if (string[i] == '\\') { + switch (string[++i]) { + case '0': + string[i - 1] = '\0'; + return i - 1; + case 'a': + string[i - 1] = '\a'; + string[i] = '\0'; + break; + case 'b': + string[i - 1] = '\b'; + string[i] = '\0'; + break; + case 'f': + string[i - 1] = '\f'; + string[i] = '\0'; + break; + case 'n': + string[i - 1] = '\n'; + string[i] = '\0'; + break; + case 'r': + string[i - 1] = '\r'; + string[i] = '\0'; + break; + case 't': + string[i - 1] = '\t'; + string[i] = '\0'; + break; + case 'v': + string[i - 1] = '\v'; + string[i] = '\0'; + break; + case '\\': + string[i] = '\0'; + break; + case '\'': + string[i - 1] = '\''; + string[i] = '\0'; + break; + case '\"': + string[i - 1] = '\"'; + string[i] = '\0'; + break; + case '?': + string[i - 1] = '?'; + string[i] = '\0'; + break; + case 'x': + { + unsigned char c = 0; + if (string[i+1] >= '0' && string[i+1] <= '9') { + c = string[i+1] - '0'; + if (string[i+2] >= '0' && string[i+2] <= '9') { + c *= 0x10; + c += string[i+2] - '0'; + string[i+2] = '\0'; + } + string[i+1] = '\0'; + } + string[i] = '\0'; + string[i - 1] = c; + } + } + } + } + // Shift characters over nullspaces + int shift = 0; + for (i = 0; i < len; ++i) { + if (string[i] == 0) { + shift++; + continue; + } + string[i-shift] = string[i]; + } + string[len - shift] = 0; + return len - shift; +} + +char *join_args(char **argv, int argc) { + if (!sway_assert(argc > 0, "argc should be positive")) { + return NULL; + } + int len = 0, i; + for (i = 0; i < argc; ++i) { + len += strlen(argv[i]) + 1; + } + char *res = malloc(len); + len = 0; + for (i = 0; i < argc; ++i) { + strcpy(res + len, argv[i]); + len += strlen(argv[i]); + res[len++] = ' '; + } + res[len - 1] = '\0'; + return res; +} + +static inline char *argsep_next_interesting(const char *src, const char *delim) { + char *special = strpbrk(src, "\"'\\"); + char *next_delim = strpbrk(src, delim); + if (!special) { + return next_delim; + } + if (!next_delim) { + return special; + } + return (next_delim < special) ? next_delim : special; +} + +char *argsep(char **stringp, const char *delim, char *matched) { + char *start = *stringp; + char *end = start; + bool in_string = false; + bool in_char = false; + bool escaped = false; + char *interesting = NULL; + + while ((interesting = argsep_next_interesting(end, delim))) { + if (escaped && interesting != end) { + escaped = false; + } + if (*interesting == '"' && !in_char && !escaped) { + in_string = !in_string; + end = interesting + 1; + } else if (*interesting == '\'' && !in_string && !escaped) { + in_char = !in_char; + end = interesting + 1; + } else if (*interesting == '\\') { + escaped = !escaped; + end = interesting + 1; + } else if (!in_string && !in_char && !escaped) { + // We must have matched a separator + end = interesting; + if (matched) { + *matched = *end; + } + if (end - start) { + *(end++) = 0; + *stringp = end; + break; + } else { + end = ++start; + } + } else { + end++; + } + } + if (!interesting) { + *stringp = NULL; + if (matched) { + *matched = '\0'; + } + } + return start; +} + +bool expand_path(char **path) { + wordexp_t p = {0}; + while (strstr(*path, " ")) { + *path = realloc(*path, strlen(*path) + 2); + char *ptr = strstr(*path, " ") + 1; + memmove(ptr + 1, ptr, strlen(ptr) + 1); + *ptr = '\\'; + } + if (wordexp(*path, &p, 0) != 0 || p.we_wordv[0] == NULL) { + wordfree(&p); + return false; + } + free(*path); + *path = join_args(p.we_wordv, p.we_wordc); + wordfree(&p); + return true; +} + +char *vformat_str(const char *fmt, va_list args) { + char *str = NULL; + va_list args_copy; + va_copy(args_copy, args); + + int len = vsnprintf(NULL, 0, fmt, args); + if (len < 0) { + sway_log_errno(SWAY_ERROR, "vsnprintf(\"%s\") failed", fmt); + goto out; + } + + str = malloc(len + 1); + if (str == NULL) { + sway_log_errno(SWAY_ERROR, "malloc() failed"); + goto out; + } + + vsnprintf(str, len + 1, fmt, args_copy); + +out: + va_end(args_copy); + return str; +} + +char *format_str(const char *fmt, ...) { + va_list args; + va_start(args, fmt); + char *str = vformat_str(fmt, args); + va_end(args); + return str; +} + +bool has_prefix(const char *str, const char *prefix) { + return strncmp(str, prefix, strlen(prefix)) == 0; +} diff --git a/common/util.c b/common/util.c new file mode 100644 index 0000000..ae1fe44 --- /dev/null +++ b/common/util.c @@ -0,0 +1,149 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "log.h" +#include "util.h" + +int wrap(int i, int max) { + return ((i % max) + max) % max; +} + +bool parse_color(const char *color, uint32_t *result) { + if (color[0] == '#') { + ++color; + } + int len = strlen(color); + if ((len != 6 && len != 8) || !isxdigit(color[0]) || !isxdigit(color[1])) { + return false; + } + char *ptr; + uint32_t parsed = strtoul(color, &ptr, 16); + if (*ptr != '\0') { + return false; + } + *result = len == 6 ? ((parsed << 8) | 0xFF) : parsed; + return true; +} + +void color_to_rgba(float dest[static 4], uint32_t color) { + dest[0] = ((color >> 24) & 0xff) / 255.0; + dest[1] = ((color >> 16) & 0xff) / 255.0; + dest[2] = ((color >> 8) & 0xff) / 255.0; + dest[3] = (color & 0xff) / 255.0; +} + +bool parse_boolean(const char *boolean, bool current) { + if (strcasecmp(boolean, "1") == 0 + || strcasecmp(boolean, "yes") == 0 + || strcasecmp(boolean, "on") == 0 + || strcasecmp(boolean, "true") == 0 + || strcasecmp(boolean, "enable") == 0 + || strcasecmp(boolean, "enabled") == 0 + || strcasecmp(boolean, "active") == 0) { + return true; + } else if (strcasecmp(boolean, "toggle") == 0) { + return !current; + } + // All other values are false to match i3 + return false; +} + +float parse_float(const char *value) { + errno = 0; + char *end; + float flt = strtof(value, &end); + if (*end || errno) { + sway_log(SWAY_DEBUG, "Invalid float value '%s', defaulting to NAN", value); + return NAN; + } + return flt; +} + +enum movement_unit parse_movement_unit(const char *unit) { + if (strcasecmp(unit, "px") == 0) { + return MOVEMENT_UNIT_PX; + } + if (strcasecmp(unit, "ppt") == 0) { + return MOVEMENT_UNIT_PPT; + } + if (strcasecmp(unit, "default") == 0) { + return MOVEMENT_UNIT_DEFAULT; + } + return MOVEMENT_UNIT_INVALID; +} + +int parse_movement_amount(int argc, char **argv, + struct movement_amount *amount) { + if (!sway_assert(argc > 0, "Expected args in parse_movement_amount")) { + amount->amount = 0; + amount->unit = MOVEMENT_UNIT_INVALID; + return 0; + } + + char *err; + amount->amount = (int)strtol(argv[0], &err, 10); + if (*err) { + // e.g. 10px + amount->unit = parse_movement_unit(err); + return 1; + } + if (argc == 1) { + amount->unit = MOVEMENT_UNIT_DEFAULT; + return 1; + } + // Try the second argument + amount->unit = parse_movement_unit(argv[1]); + if (amount->unit == MOVEMENT_UNIT_INVALID) { + amount->unit = MOVEMENT_UNIT_DEFAULT; + return 1; + } + return 2; +} + +const char *sway_wl_output_subpixel_to_string(enum wl_output_subpixel subpixel) { + switch (subpixel) { + case WL_OUTPUT_SUBPIXEL_UNKNOWN: + return "unknown"; + case WL_OUTPUT_SUBPIXEL_NONE: + return "none"; + case WL_OUTPUT_SUBPIXEL_HORIZONTAL_RGB: + return "rgb"; + case WL_OUTPUT_SUBPIXEL_HORIZONTAL_BGR: + return "bgr"; + case WL_OUTPUT_SUBPIXEL_VERTICAL_RGB: + return "vrgb"; + case WL_OUTPUT_SUBPIXEL_VERTICAL_BGR: + return "vbgr"; + } + sway_assert(false, "Unknown value for wl_output_subpixel."); + return NULL; +} + +bool sway_set_cloexec(int fd, bool cloexec) { + int flags = fcntl(fd, F_GETFD); + if (flags == -1) { + sway_log_errno(SWAY_ERROR, "fcntl failed"); + return false; + } + if (cloexec) { + flags = flags | FD_CLOEXEC; + } else { + flags = flags & ~FD_CLOEXEC; + } + if (fcntl(fd, F_SETFD, flags) == -1) { + sway_log_errno(SWAY_ERROR, "fcntl failed"); + return false; + } + return true; +} + +uint32_t get_current_time_in_msec(void) { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + return now.tv_sec * 1000 + now.tv_nsec / 1000000; +} diff --git a/completions/bash/sway b/completions/bash/sway new file mode 100644 index 0000000..01b2007 --- /dev/null +++ b/completions/bash/sway @@ -0,0 +1,46 @@ +# sway(1) completion + +_sway() +{ + local cur prev short long + _get_comp_words_by_ref cur prev + + short=( + -h + -c + -C + -d + -v + -V + ) + + long=( + --help + --config + --validate + --debug + --version + --verbose + --get-socketpath + ) + + case $prev in + -c|--config) + _filedir + return + ;; + esac + + if [[ $cur == --* ]]; then + COMPREPLY=($(compgen -W "${long[*]}" -- "$cur")) + elif [[ $cur == -* ]]; then + COMPREPLY=($(compgen -W "${short[*]}" -- "$cur")) + COMPREPLY+=($(compgen -W "${long[*]}" -- "$cur")) + else + COMPREPLY=($(compgen -W "${short[*]}" -- "$cur")) + COMPREPLY+=($(compgen -W "${long[*]}" -- "$cur")) + COMPREPLY+=($(compgen -c -- "$cur")) + fi + +} && +complete -F _sway sway diff --git a/completions/bash/swaybar b/completions/bash/swaybar new file mode 100644 index 0000000..3709d4f --- /dev/null +++ b/completions/bash/swaybar @@ -0,0 +1,44 @@ +# swaybar(1) completion + +_swaybar() +{ + local cur prev short long + _get_comp_words_by_ref cur prev + + short=( + -h + -v + -s + -b + -d + ) + + long=( + --help + --version + --socket + --bar_id + --debug + ) + + case $prev in + -s|--socket) + _filedir + return + ;; + -b|--bar_id) + bars=($(swaymsg -t get_bar_config | jq -r '.[]')) + COMPREPLY=($(compgen -W "${bars[*]}" -- "$cur")) + return + ;; + esac + + if [[ $cur == --* ]]; then + COMPREPLY=($(compgen -W "${long[*]}" -- "$cur")) + else + COMPREPLY=($(compgen -W "${short[*]}" -- "$cur")) + COMPREPLY+=($(compgen -W "${long[*]}" -- "$cur")) + fi + +} && +complete -F _swaybar swaybar diff --git a/completions/bash/swaymsg b/completions/bash/swaymsg new file mode 100644 index 0000000..3045775 --- /dev/null +++ b/completions/bash/swaymsg @@ -0,0 +1,65 @@ +# swaymsg(1) completion + +_swaymsg() +{ + local cur prev types short long + _get_comp_words_by_ref cur prev + + types=( + 'get_workspaces' + 'get_seats' + 'get_inputs' + 'get_outputs' + 'get_tree' + 'get_marks' + 'get_bar_config' + 'get_version' + 'get_binding_modes' + 'get_binding_state' + 'get_config' + 'send_tick' + 'subscribe' + ) + + short=( + -h + -m + -p + -q + -r + -s + -t + -v + ) + + long=( + --help + --monitor + --pretty + --quiet + --raw + --socket + --type + --version + ) + + case $prev in + -s|--socket) + _filedir + return + ;; + -t|--type) + COMPREPLY=($(compgen -W "${types[*]}" -- "$cur")) + return + ;; + esac + + if [[ $cur == --* ]]; then + COMPREPLY=($(compgen -W "${long[*]}" -- "$cur")) + else + COMPREPLY=($(compgen -W "${short[*]}" -- "$cur")) + COMPREPLY+=($(compgen -W "${long[*]}" -- "$cur")) + fi + +} && +complete -F _swaymsg swaymsg diff --git a/completions/fish/sway.fish b/completions/fish/sway.fish new file mode 100644 index 0000000..444fd27 --- /dev/null +++ b/completions/fish/sway.fish @@ -0,0 +1,11 @@ +# sway(1) completion + +complete -f -c sway +complete -c sway -s h -l help --description "Show help message and quit." +complete -c sway -s c -l config --description "Specifies a config file." -r +complete -c sway -s C -l validate --description "Check the validity of the config file, then exit." +complete -c sway -s d -l debug --description "Enables full logging, including debug information." +complete -c sway -s v -l version --description "Show the version number and quit." +complete -c sway -s V -l verbose --description "Enables more verbose logging." +complete -c sway -l get-socketpath --description "Gets the IPC socket path and prints it, then exits." + diff --git a/completions/fish/swaymsg.fish b/completions/fish/swaymsg.fish new file mode 100644 index 0000000..5ee7dcc --- /dev/null +++ b/completions/fish/swaymsg.fish @@ -0,0 +1,25 @@ +# swaymsg(1) completion + +complete -f -c swaymsg +complete -c swaymsg -s h -l help --description "Show help message and quit." +complete -c swaymsg -s m -l monitor --description "Monitor subscribed events until killed." +complete -c swaymsg -s p -l pretty --description "Use pretty output even when not using a tty." +complete -c swaymsg -s q -l quiet --description "Sends the IPC message but does not print the response from sway." +complete -c swaymsg -s r -l raw --description "Use raw output even if using tty." +complete -c swaymsg -s s -l socket -r --description "Use the specified socket path. Otherwise, swaymsg will ask where the socket is (which is the value of $SWAYSOCK, then of $I3SOCK)." +complete -c swaymsg -s v -l version --description "Print the version (of swaymsg) and quit." + +complete -c swaymsg -s t -l type -fr --description "Specify the type of IPC message." +complete -c swaymsg -s t -l type -fra 'get_workspaces' --description "Gets a JSON-encoded list of workspaces and their status." +complete -c swaymsg -s t -l type -fra 'get_inputs' --description "Gets a JSON-encoded list of current inputs." +complete -c swaymsg -s t -l type -fra 'get_outputs' --description "Gets a JSON-encoded list of current outputs." +complete -c swaymsg -s t -l type -fra 'get_tree' --description "Gets a JSON-encoded layout tree of all open windows, containers, outputs, workspaces, and so on." +complete -c swaymsg -s t -l type -fra 'get_marks' --description "Get a JSON-encoded list of marks." +complete -c swaymsg -s t -l type -fra 'get_bar_config' --description "Get a JSON-encoded configuration for swaybar." +complete -c swaymsg -s t -l type -fra 'get_version' --description "Get JSON-encoded version information for the running instance of sway." +complete -c swaymsg -s t -l type -fra 'get_binding_modes' --description "Gets a JSON-encoded list of currently configured binding modes." +complete -c swaymsg -s t -l type -fra 'get_binding_state' --description "Get JSON-encoded info about the current binding state." +complete -c swaymsg -s t -l type -fra 'get_config' --description "Gets a JSON-encoded copy of the current configuration." +complete -c swaymsg -s t -l type -fra 'get_seats' --description "Gets a JSON-encoded list of all seats, its properties and all assigned devices." +complete -c swaymsg -s t -l type -fra 'send_tick' --description "Sends a tick event to all subscribed clients." +complete -c swaymsg -s t -l type -fra 'subscribe' --description "Subscribe to a list of event types." diff --git a/completions/fish/swaynag.fish b/completions/fish/swaynag.fish new file mode 100644 index 0000000..7015d5e --- /dev/null +++ b/completions/fish/swaynag.fish @@ -0,0 +1,30 @@ +# swaynag +complete -f -c swaynag +complete -c swaynag -s C -l config -r --description 'The config file to use. Default: $HOME/.swaynag/config, $XDG_CONFIG_HOME/swaynag/config, and SYSCONFDIR/swaynag/config.' +complete -c swaynag -s d -l debug --description 'Enable debugging.' +complete -c swaynag -s e -l edge -fr --description 'Set the edge to use: top or bottom' +complete -c swaynag -s f -l font -r --description 'Set the font to use.' +complete -c swaynag -s h -l help --description 'Show help message and quit.' +complete -c swaynag -s b -l button -fr --description 'Create a button with a text and an action which is executed when pressed. Multiple buttons can be defined by providing the flag multiple times.' +complete -c swaynag -s l -l detailed-message --description 'Read a detailed message from stdin. A button to toggle details will be added. Details are shown in a scrollable multi-line text area.' +complete -c swaynag -s L -l detailed-button -fr --description 'Set the text for the button that toggles details. This has no effect if there is not a detailed message. The default is "Toggle details".' +complete -c swaynag -s m -l message -fr --description 'Set the message text.' +complete -c swaynag -s o -l output -fr --description 'Set the output to use.' +complete -c swaynag -s s -l dismiss-button -fr --description 'Sets the text for the dismiss nagbar button. The default is "X".' +complete -c swaynag -s t -l type -fr --description 'Set the message type. Two types are created by default "error" and "warning". Custom types can be defined in the config file.' +complete -c swaynag -s v -l version --description 'Show the version number and quit.' + +# Appearance +complete -c swaynag -l background -fr --description 'Set the color of the background.' +complete -c swaynag -l border -fr --description 'Set the color of the border.' +complete -c swaynag -l border-bottom -fr --description 'Set the color of the bottom border.' +complete -c swaynag -l button-background -fr --description 'Set the color for the background for buttons.' +complete -c swaynag -l text -fr --description 'Set the text color.' +complete -c swaynag -l border-bottom-size -fr --description 'Set the thickness of the bottom border.' +complete -c swaynag -l message-padding -fr --description 'Set the padding for the message.' +complete -c swaynag -l details-border-size -fr --description 'Set the thickness for the details border.' +complete -c swaynag -l button-border-size -fr --description 'Set the thickness for the button border.' +complete -c swaynag -l button-gap -fr --description 'Set the size of the gap between buttons.' +complete -c swaynag -l button-dismiss-gap -fr --description 'Set the size of the gap between the dismiss button and another button.' +complete -c swaynag -l button-margin-right -fr --description 'Set the margin from the right of the dismiss button to edge.' +complete -c swaynag -l button-padding -fr --description 'Set the padding for the button text.' diff --git a/completions/meson.build b/completions/meson.build new file mode 100644 index 0000000..6bca939 --- /dev/null +++ b/completions/meson.build @@ -0,0 +1,57 @@ +if get_option('zsh-completions') + zsh_files = files( + 'zsh/_sway', + 'zsh/_swaymsg', + ) + zsh_install_dir = join_paths(datadir, 'zsh', 'site-functions') + + install_data(zsh_files, install_dir: zsh_install_dir) +endif + +if get_option('bash-completions') + bash_comp = dependency('bash-completion', required: false) + + bash_files = files( + 'bash/sway', + 'bash/swaymsg', + ) + + if get_option('swaybar') + bash_files += files('bash/swaybar') + endif + + if bash_comp.found() + bash_install_dir = bash_comp.get_variable( + pkgconfig: 'completionsdir', + pkgconfig_define: ['datadir', datadir] + ) + else + bash_install_dir = join_paths(datadir, 'bash-completion', 'completions') + endif + + install_data(bash_files, install_dir: bash_install_dir) +endif + +if get_option('fish-completions') + fish_comp = dependency('fish', required: false) + + fish_files = files( + 'fish/sway.fish', + 'fish/swaymsg.fish', + ) + + if get_option('swaynag') + fish_files += files('fish/swaynag.fish') + endif + + if fish_comp.found() + fish_install_dir = fish_comp.get_variable( + pkgconfig: 'completionsdir', + pkgconfig_define: ['datadir', datadir] + ) + else + fish_install_dir = join_paths(datadir, 'fish', 'vendor_completions.d') + endif + + install_data(fish_files, install_dir: fish_install_dir) +endif diff --git a/completions/zsh/_sway b/completions/zsh/_sway new file mode 100644 index 0000000..a7f55cc --- /dev/null +++ b/completions/zsh/_sway @@ -0,0 +1,22 @@ +#compdef sway +#------------ +# Description +# ----------- +# +# Completion script for the sway window manager (http://swaywm.org) +# +# --------------------------------------------- +# Author +# ------- +# +# * Seth Barberee +# +# ------------------------------- +_arguments -s \ + '(-v --version)'{-v,--version}'[Show the version number and quit]' \ + '(-h --help)'{-h,--help}'[Show help message and quit]' \ + '(-c --config)'{-c,--config}'[Specify a config file]:files:_files' \ + '(-C --validate)'{-C,--validate}'[Check validity of the config file, then exit]' \ + '(-d --debug)'{-d,--debug}'[Enables full logging, including debug information]' \ + '(-V --verbose)'{-V,--verbose}'[Enables more verbose logging]' \ + '(--get-socketpath)'--get-socketpath'[Gets the IPC socket path and prints it, then exits]' diff --git a/completions/zsh/_swaybar b/completions/zsh/_swaybar new file mode 100644 index 0000000..af2cee9 --- /dev/null +++ b/completions/zsh/_swaybar @@ -0,0 +1,13 @@ +#compdef swaybar +# +# Completion script for swaybar +# + +local bars=($(swaymsg -t get_bar_config | jq -r '.[]')) + +_arguments -s \ + '(-h --help)'{-h,--help}'[Show help message and quit]' \ + '(-v --version)'{-v,--version}'[Show version and quit]' \ + '(-s --socket)'{-s,--socket}'[Connect to sway via socket]:filename:_files' \ + '(-b --bar_id)'{-b,--bar-id}'[Bar ID for which to get the configuration]:filename:($bars)'\ + '(-d --debug)'{-d,--debug}'[Enable debugging]' diff --git a/completions/zsh/_swaymsg b/completions/zsh/_swaymsg new file mode 100644 index 0000000..106f3d9 --- /dev/null +++ b/completions/zsh/_swaymsg @@ -0,0 +1,40 @@ +#compdef swaymsg +#----------------- +# Description +# ----------- +# +# Completion script for swaymsg in sway wm (http://swaywm.org) +# +# ------------------------------------------------------ +# Author +# -------- +# +# * Seth Barberee +# +# ------------------------------------------- + +types=( +'get_workspaces' +'get_seats' +'get_inputs' +'get_outputs' +'get_tree' +'get_marks' +'get_bar_config' +'get_version' +'get_binding_modes' +'get_binding_state' +'get_config' +'send_tick' +'subscribe' +) + +_arguments -s \ + '(-h --help)'{-h,--help}'[Show help message and quit]' \ + '(-m --monitor)'{-m,--monitor}'[Monitor until killed (-t SUBSCRIBE only)]' \ + '(-p --pretty)'{-p,--pretty}'[Use pretty output even when not using a tty]' \ + '(-q --quiet)'{-q,--quiet}'[Be quiet]' \ + '(-r --raw)'{-r,--raw}'[Use raw output even if using a tty]' \ + '(-s --socket)'{-s,--socket}'[Use the specified socket path]:files:_files' \ + '(-t --type)'{-t,--type}'[Specify the message type]:type:{_describe "type" types}' \ + '(-v --version)'{-v,--version}'[Show the version number and quit]' diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..96faf06 --- /dev/null +++ b/compose.yml @@ -0,0 +1,13 @@ +services: + swayfxbuild: + container_name: swayfxbuild + build: + args: + WLROOTSVERSION: 0.19.0 + WLROOTSLIBVERSION: 0.19 + SWAYFXVERSION: 0.5.3 + SCENEFXVERSION: 0.4.1 + SCENEFXLIBVERSION: 0.4 + OPT_SWAYFX: /opt + volumes: + - ./opt:/opt diff --git a/config.in b/config.in new file mode 100644 index 0000000..c944b15 --- /dev/null +++ b/config.in @@ -0,0 +1,281 @@ +# SwayFX settings + +animation_duration_ms 250 + +blur disable +# blur_xray enables wallpaper blurring without blurring windows below +blur_xray disable +blur_passes 4 +blur_radius 4 +# blur_noise +# blur_brightness +# blur_contrast +# blur_saturation + +# Window corners can be rounded in SwayFX +# corner_radius can be any number of pixels +corner_radius 15 +gaps inner 15 +# corner radius needs gaps to be visible + +# SwayFX also features window drop shadows, which, of course, can be enabled at any time +shadows disable +shadows_on_csd enable + +# You can also offset the shadow's position, make unfocused windows have different shadows, give shadows a nondefault color, and blur shadows +# shadow_inactive_color #0000007F +# shadow_offset +# shadow_blur_radius +# shadow_color + +# Dimming +# default_dim_inactive +# for_window [CRITERIA_HERE] dim_inactive +# dim_inactive_colors.unfocused +# dim_inactive_colors.urgent + +# Default config for Sway +# +# Copy this to ~/.config/sway/config and edit it to your liking. +# +# Read `man 5 sway` for a complete reference. + +### Variables +# +# Logo key. Use Mod1 for Alt. +set $mod Mod1 +# Home row direction keys, like vim +set $left h +set $down j +set $up k +set $right l +# Your preferred terminal emulator +set $term foot +# Your preferred application launcher +set $menu wmenu-run + +### Output configuration +# +# Default wallpaper (more resolutions are available in @datadir@/backgrounds/sway/) +output * bg @datadir@/backgrounds/sway/Sway_Wallpaper_Blue_1920x1080.png fill +# +# Example configuration: +# +# output HDMI-A-1 resolution 1920x1080 position 1920,0 +# +# You can get the names of your outputs by running: swaymsg -t get_outputs + +### Idle configuration +# +# Example configuration: +# +# exec swayidle -w \ +# timeout 300 'swaylock -f -c 000000' \ +# timeout 600 'swaymsg "output * power off"' resume 'swaymsg "output * power on"' \ +# before-sleep 'swaylock -f -c 000000' +# +# This will lock your screen after 300 seconds of inactivity, then turn off +# your displays after another 300 seconds, and turn your screens back on when +# resumed. It will also lock your screen before your computer goes to sleep. + +### Input configuration +# +# Example configuration: +# +# input type:touchpad { +# dwt enabled +# tap enabled +# natural_scroll enabled +# middle_emulation enabled +# } +# +# input type:keyboard { +# xkb_layout "eu" +# } +# +# You can also configure each device individually. +# Read `man 5 sway-input` for more information about this section. + +### Key bindings +# +# Basics: +# + # Start a terminal + bindsym $mod+Return exec $term + + # Kill focused window + bindsym $mod+Shift+q kill + + # Start your launcher + bindsym $mod+d exec $menu + + # Drag floating windows by holding down $mod and left mouse button. + # Resize them with right mouse button + $mod. + # Despite the name, also works for non-floating windows. + # Change normal to inverse to use left mouse button for resizing and right + # mouse button for dragging. + floating_modifier $mod normal + + # Reload the configuration file + bindsym $mod+Shift+c reload + + # Exit SwayFX (logs you out of your Wayland session) + bindsym $mod+Shift+e exec swaynag -t warning -m 'You pressed the exit shortcut. Do you really want to exit SwayFX? This will end your Wayland session.' -B 'Yes, exit SwayFX' 'swaymsg exit' +# +# Moving around: +# + # Move your focus around + bindsym $mod+$left focus left + bindsym $mod+$down focus down + bindsym $mod+$up focus up + bindsym $mod+$right focus right + # Or use $mod+[up|down|left|right] + bindsym $mod+Left focus left + bindsym $mod+Down focus down + bindsym $mod+Up focus up + bindsym $mod+Right focus right + + # Move the focused window with the same, but add Shift + bindsym $mod+Shift+$left move left + bindsym $mod+Shift+$down move down + bindsym $mod+Shift+$up move up + bindsym $mod+Shift+$right move right + # Ditto, with arrow keys + bindsym $mod+Shift+Left move left + bindsym $mod+Shift+Down move down + bindsym $mod+Shift+Up move up + bindsym $mod+Shift+Right move right +# +# Workspaces: +# + # Switch to workspace + bindsym $mod+1 workspace number 1 + bindsym $mod+2 workspace number 2 + bindsym $mod+3 workspace number 3 + bindsym $mod+4 workspace number 4 + bindsym $mod+5 workspace number 5 + bindsym $mod+6 workspace number 6 + bindsym $mod+7 workspace number 7 + bindsym $mod+8 workspace number 8 + bindsym $mod+9 workspace number 9 + bindsym $mod+0 workspace number 10 + # Move focused container to workspace + bindsym $mod+Shift+1 move container to workspace number 1 + bindsym $mod+Shift+2 move container to workspace number 2 + bindsym $mod+Shift+3 move container to workspace number 3 + bindsym $mod+Shift+4 move container to workspace number 4 + bindsym $mod+Shift+5 move container to workspace number 5 + bindsym $mod+Shift+6 move container to workspace number 6 + bindsym $mod+Shift+7 move container to workspace number 7 + bindsym $mod+Shift+8 move container to workspace number 8 + bindsym $mod+Shift+9 move container to workspace number 9 + bindsym $mod+Shift+0 move container to workspace number 10 + # Note: workspaces can have any name you want, not just numbers. + # We just use 1-10 as the default. +# +# Layout stuff: +# + # You can "split" the current object of your focus with + # $mod+b or $mod+v, for horizontal and vertical splits + # respectively. + bindsym $mod+b splith + bindsym $mod+v splitv + + # Switch the current container between different layout styles + bindsym $mod+s layout stacking + bindsym $mod+w layout tabbed + bindsym $mod+e layout toggle split + + # Make the current focus fullscreen + bindsym $mod+f fullscreen + + # Toggle the current focus between tiling and floating mode + bindsym $mod+Shift+space floating toggle + + # Swap focus between the tiling area and the floating area + bindsym $mod+space focus mode_toggle + + # Move focus to the parent container + bindsym $mod+a focus parent +# +# Scratchpad: +# + # SwayFX has a "scratchpad", which is a bag of holding for windows. + # You can send windows there and get them back later. + + # Move the currently focused window to the scratchpad + bindsym $mod+Shift+minus move scratchpad + + # Show the next scratchpad window or hide the focused scratchpad window. + # If there are multiple scratchpad windows, this command cycles through them. + bindsym $mod+minus scratchpad show + +# +# Resizing containers: +# +mode "resize" { + # left will shrink the containers width + # right will grow the containers width + # up will shrink the containers height + # down will grow the containers height + bindsym $left resize shrink width 10px + bindsym $down resize grow height 10px + bindsym $up resize shrink height 10px + bindsym $right resize grow width 10px + + # Ditto, with arrow keys + bindsym Left resize shrink width 10px + bindsym Down resize grow height 10px + bindsym Up resize shrink height 10px + bindsym Right resize grow width 10px + + # Return to default mode + bindsym Return mode "default" + bindsym Escape mode "default" +} +bindsym $mod+r mode "resize" +# +# Utilities: +# + # Special keys to adjust volume via PulseAudio + bindsym --locked XF86AudioMute exec pactl set-sink-mute \@DEFAULT_SINK@ toggle + bindsym --locked XF86AudioLowerVolume exec pactl set-sink-volume \@DEFAULT_SINK@ -5% + bindsym --locked XF86AudioRaiseVolume exec pactl set-sink-volume \@DEFAULT_SINK@ +5% + bindsym --locked XF86AudioMicMute exec pactl set-source-mute \@DEFAULT_SOURCE@ toggle + # Special keys to adjust brightness via brightnessctl + bindsym --locked XF86MonBrightnessDown exec brightnessctl set 5%- + bindsym --locked XF86MonBrightnessUp exec brightnessctl set 5%+ + + # Special keys to control media via playerctl + bindsym --locked XF86AudioPlay exec playerctl play-pause + bindsym --locked XF86AudioPause exec playerctl play-pause + bindsym --locked XF86AudioPrev exec playerctl previous + bindsym --locked XF86AudioNext exec playerctl next + bindsym --locked XF86AudioStop exec playerctl stop + + # Special keys to adjust brightness via brightnessctl + bindsym --locked XF86MonBrightnessDown exec brightnessctl set 5%- + bindsym --locked XF86MonBrightnessUp exec brightnessctl set 5%+ + + # Special key to take a screenshot with grim + bindsym Print exec grim + +# +# Status Bar: +# +# Read `man 5 sway-bar` for more information about this section. +bar { + position top + + # When the status_command prints a new line to stdout, swaybar updates. + # The default just shows the current date and time. + status_command while date +'%Y-%m-%d %X'; do sleep 1; done + + colors { + statusline #ffffff + background #323232 + inactive_workspace #32323200 #32323200 #5c5c5c + } +} + +include @sysconfdir@/sway/config.d/* diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..5ff0402 --- /dev/null +++ b/flake.lock @@ -0,0 +1,104 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": [ + "systems" + ] + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1783791668, + "narHash": "sha256-zbcZ1dmBTPfJ7Mlqh/yLEPGpgJnwuv4Xr1xucy2WqMA=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "716c7a2664ca8325617b8a7fbb609273f2c4cae7", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "scenefx": "scenefx", + "systems": "systems_2" + } + }, + "scenefx": { + "inputs": { + "flake-utils": [ + "flake-utils" + ], + "nixpkgs": [ + "nixpkgs" + ], + "systems": "systems" + }, + "locked": { + "lastModified": 1782624992, + "narHash": "sha256-vUjLG6eubEhJJVa9LPygIcVmNoHwYbSUTJcWEcbxnU4=", + "owner": "wlrfx", + "repo": "scenefx", + "rev": "37ccd723bef49e6891156ffafce8f549f01446cc", + "type": "github" + }, + "original": { + "owner": "wlrfx", + "repo": "scenefx", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1689347949, + "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", + "owner": "nix-systems", + "repo": "default-linux", + "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default-linux", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1689347949, + "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", + "owner": "nix-systems", + "repo": "default-linux", + "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default-linux", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..d3691a4 --- /dev/null +++ b/flake.nix @@ -0,0 +1,85 @@ +{ + description = "Swayfx development environment"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; + + systems.url = "github:nix-systems/default-linux"; + + flake-utils = { + url = "github:numtide/flake-utils"; + inputs.systems.follows = "systems"; + }; + + scenefx = { + url = "github:wlrfx/scenefx"; + inputs.nixpkgs.follows = "nixpkgs"; + inputs.flake-utils.follows = "flake-utils"; + }; + }; + + outputs = { self, nixpkgs, flake-utils, scenefx, ... }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { inherit system; }; + scenefx-git = scenefx.packages.${system}.scenefx-git; + + in { + packages = rec { + swayfx-unwrapped-git = pkgs.swayfx-unwrapped.overrideAttrs(old: { + version = "git"; + src = pkgs.lib.cleanSource ./.; + nativeBuildInputs = with pkgs; [ + meson + ninja + pkg-config + wayland-scanner + scdoc + ]; + buildInputs = with pkgs; [ + libGL + wayland + libxkbcommon + pcre2 + json_c + libevdev + pango + cairo + libinput + gdk-pixbuf + librsvg + wayland-protocols + libdrm + libxcb-wm + wlroots_0_20 + ] ++ [ scenefx-git ]; + }); + + default = swayfx-unwrapped-git; + }; + + devShells.default = pkgs.mkShell { + name = "swayfx-shell"; + inputsFrom = [ + self.packages.${system}.swayfx-unwrapped-git + scenefx-git + ]; + packages = [ pkgs.gdb ]; # add debugging packages + shellHook = '' + ( + mkdir -p "$PWD/subprojects" && cd "$PWD/subprojects" + if [ ! -d scenefx ]; then + echo "Copying scenefx to ./subprojects..." + cp -R --no-preserve=mode,ownership ${scenefx-git.src} scenefx + fi + cd "$OLDPWD" + ) || echo "Failed to copy subproject sources." + ''; + hardeningDisable = [ "fortify" ]; + }; + + formatter = pkgs.nixfmt-rfc-style; + } + ); +} + diff --git a/include/cairo_util.h b/include/cairo_util.h new file mode 100644 index 0000000..dc049c6 --- /dev/null +++ b/include/cairo_util.h @@ -0,0 +1,14 @@ +#ifndef _SWAY_CAIRO_UTIL_H +#define _SWAY_CAIRO_UTIL_H +#include "config.h" +#include +#include +#include + +void cairo_set_source_u32(cairo_t *cairo, uint32_t color); +cairo_subpixel_order_t to_cairo_subpixel_order(enum wl_output_subpixel subpixel); + +cairo_surface_t *cairo_image_surface_scale(cairo_surface_t *image, + int width, int height); + +#endif diff --git a/include/gesture.h b/include/gesture.h new file mode 100644 index 0000000..9c6b0f9 --- /dev/null +++ b/include/gesture.h @@ -0,0 +1,104 @@ +#ifndef _SWAY_GESTURE_H +#define _SWAY_GESTURE_H + +#include +#include + +/** + * A gesture type used in binding. + */ +enum gesture_type { + GESTURE_TYPE_NONE = 0, + GESTURE_TYPE_HOLD, + GESTURE_TYPE_PINCH, + GESTURE_TYPE_SWIPE, +}; + +// Turns single type enum value to constant string representation. +const char *gesture_type_string(enum gesture_type direction); + +// Value to use to accept any finger count +extern const uint8_t GESTURE_FINGERS_ANY; + +/** + * A gesture direction used in binding. + */ +enum gesture_direction { + GESTURE_DIRECTION_NONE = 0, + // Directions based on delta x and y + GESTURE_DIRECTION_UP = 1 << 0, + GESTURE_DIRECTION_DOWN = 1 << 1, + GESTURE_DIRECTION_LEFT = 1 << 2, + GESTURE_DIRECTION_RIGHT = 1 << 3, + // Directions based on scale + GESTURE_DIRECTION_INWARD = 1 << 4, + GESTURE_DIRECTION_OUTWARD = 1 << 5, + // Directions based on rotation + GESTURE_DIRECTION_CLOCKWISE = 1 << 6, + GESTURE_DIRECTION_COUNTERCLOCKWISE = 1 << 7, +}; + +// Turns single direction enum value to constant string representation. +const char *gesture_direction_string(enum gesture_direction direction); + +/** + * Struct representing a pointer gesture + */ +struct gesture { + enum gesture_type type; + uint8_t fingers; + uint32_t directions; +}; + +/** + * Parses gesture from [:][:] string. + * + * Return NULL on success, otherwise error message string + */ +char *gesture_parse(const char *input, struct gesture *output); + +// Turns gesture into string representation +char *gesture_to_string(struct gesture *gesture); + +// Check if gesture is of certain type and finger count. +bool gesture_check(struct gesture *target, + enum gesture_type type, uint8_t fingers); + +// Check if a gesture target/binding is match by other gesture/input +bool gesture_match(struct gesture *target, + struct gesture *to_match, bool exact); + +// Returns true if gesture are exactly the same +bool gesture_equal(struct gesture *a, struct gesture *b); + +// Compare distance between two matched target gestures. +int8_t gesture_compare(struct gesture *a, struct gesture *b); + +// Small helper struct to track gestures over time +struct gesture_tracker { + enum gesture_type type; + uint8_t fingers; + double dx, dy; + double scale; + double rotation; +}; + +// Begin gesture tracking +void gesture_tracker_begin(struct gesture_tracker *tracker, + enum gesture_type type, uint8_t fingers); + +// Check if the provides type is currently being tracked +bool gesture_tracker_check(struct gesture_tracker *tracker, + enum gesture_type type); + +// Update gesture track with new data point +void gesture_tracker_update(struct gesture_tracker *tracker, double dx, + double dy, double scale, double rotation); + +// Reset tracker +void gesture_tracker_cancel(struct gesture_tracker *tracker); + +// Reset tracker and return gesture tracked +struct gesture *gesture_tracker_end(struct gesture_tracker *tracker); + +#endif diff --git a/include/ipc-client.h b/include/ipc-client.h new file mode 100644 index 0000000..9c5712d --- /dev/null +++ b/include/ipc-client.h @@ -0,0 +1,49 @@ +#ifndef _SWAY_IPC_CLIENT_H +#define _SWAY_IPC_CLIENT_H + +// arbitrary number, it's probably sufficient, higher number = more memory usage +#define JSON_MAX_DEPTH 512 + +#include +#include +#include + +#include "ipc.h" + +/** + * IPC response including type of IPC response, size of payload and the json + * encoded payload string. + */ +struct ipc_response { + uint32_t size; + uint32_t type; + char *payload; +}; + +/** + * Gets the path to the IPC socket from sway. + */ +char *get_socketpath(void); +/** + * Opens the sway socket. + */ +int ipc_open_socket(const char *socket_path); +/** + * Issues a single IPC command and returns the buffer. len will be updated with + * the length of the buffer returned from sway. + */ +char *ipc_single_command(int socketfd, uint32_t type, const char *payload, uint32_t *len); +/** + * Receives a single IPC response and returns an ipc_response. + */ +struct ipc_response *ipc_recv_response(int socketfd); +/** + * Free ipc_response struct + */ +void free_ipc_response(struct ipc_response *response); +/** + * Sets the receive timeout for the IPC socket + */ +bool ipc_set_recv_timeout(int socketfd, struct timeval tv); + +#endif diff --git a/include/ipc.h b/include/ipc.h new file mode 100644 index 0000000..ff01175 --- /dev/null +++ b/include/ipc.h @@ -0,0 +1,41 @@ +#ifndef _SWAY_IPC_H +#define _SWAY_IPC_H + +#define event_mask(ev) (1 << (ev & 0x7F)) + +enum ipc_command_type { + // i3 command types - see i3's I3_REPLY_TYPE constants + IPC_COMMAND = 0, + IPC_GET_WORKSPACES = 1, + IPC_SUBSCRIBE = 2, + IPC_GET_OUTPUTS = 3, + IPC_GET_TREE = 4, + IPC_GET_MARKS = 5, + IPC_GET_BAR_CONFIG = 6, + IPC_GET_VERSION = 7, + IPC_GET_BINDING_MODES = 8, + IPC_GET_CONFIG = 9, + IPC_SEND_TICK = 10, + IPC_SYNC = 11, + IPC_GET_BINDING_STATE = 12, + + // sway-specific command types + IPC_GET_INPUTS = 100, + IPC_GET_SEATS = 101, + + // Events sent from sway to clients. Events have the highest bits set. + IPC_EVENT_WORKSPACE = ((1<<31) | 0), + IPC_EVENT_OUTPUT = ((1<<31) | 1), + IPC_EVENT_MODE = ((1<<31) | 2), + IPC_EVENT_WINDOW = ((1<<31) | 3), + IPC_EVENT_BARCONFIG_UPDATE = ((1<<31) | 4), + IPC_EVENT_BINDING = ((1<<31) | 5), + IPC_EVENT_SHUTDOWN = ((1<<31) | 6), + IPC_EVENT_TICK = ((1<<31) | 7), + + // sway-specific event types + IPC_EVENT_BAR_STATE_UPDATE = ((1<<31) | 20), + IPC_EVENT_INPUT = ((1<<31) | 21), +}; + +#endif diff --git a/include/list.h b/include/list.h new file mode 100644 index 0000000..895f6cc --- /dev/null +++ b/include/list.h @@ -0,0 +1,35 @@ +#ifndef _SWAY_LIST_H +#define _SWAY_LIST_H + +typedef struct { + int capacity; + int length; + void **items; +} list_t; + +list_t *create_list(void); +void list_free(list_t *list); +void list_add(list_t *list, void *item); +void list_insert(list_t *list, int index, void *item); +void list_del(list_t *list, int index); +void list_cat(list_t *list, list_t *source); +// See qsort. Remember to use *_qsort functions as compare functions, +// because they dereference the left and right arguments first! +void list_qsort(list_t *list, int compare(const void *left, const void *right)); +// Return index for first item in list that returns 0 for given compare +// function or -1 if none matches. +int list_seq_find(list_t *list, int compare(const void *item, const void *cmp_to), const void *cmp_to); +int list_find(list_t *list, const void *item); +// stable sort since qsort is not guaranteed to be stable +void list_stable_sort(list_t *list, int compare(const void *a, const void *b)); +// swap two elements in a list +void list_swap(list_t *list, int src, int dest); +// move item to end of list +void list_move_to_end(list_t *list, void *item); + +/* Calls `free` for each item in the list, then frees the list. + * Do not use this to free lists of primitives or items that require more + * complicated deallocation code. + */ +void list_free_items_and_destroy(list_t *list); +#endif diff --git a/include/log.h b/include/log.h new file mode 100644 index 0000000..41399a9 --- /dev/null +++ b/include/log.h @@ -0,0 +1,58 @@ +#ifndef _SWAY_LOG_H +#define _SWAY_LOG_H + +#include +#include +#include +#include + +typedef enum { + SWAY_SILENT = 0, + SWAY_ERROR = 1, + SWAY_INFO = 2, + SWAY_DEBUG = 3, + SWAY_LOG_IMPORTANCE_LAST, +} sway_log_importance_t; + +#ifdef __GNUC__ +#define ATTRIB_PRINTF(start, end) __attribute__((format(printf, start, end))) +#else +#define ATTRIB_PRINTF(start, end) +#endif + +void error_handler(int sig); + +typedef void (*terminate_callback_t)(int exit_code); + +// Will log all messages less than or equal to `verbosity` +// The `terminate` callback is called by `sway_abort` +void sway_log_init(sway_log_importance_t verbosity, terminate_callback_t terminate); + +void _sway_log(sway_log_importance_t verbosity, const char *format, ...) ATTRIB_PRINTF(2, 3); +void _sway_vlog(sway_log_importance_t verbosity, const char *format, va_list args) ATTRIB_PRINTF(2, 0); +void _sway_abort(const char *filename, ...) ATTRIB_PRINTF(1, 2); +bool _sway_assert(bool condition, const char* format, ...) ATTRIB_PRINTF(2, 3); + +#ifdef SWAY_REL_SRC_DIR +// strip prefix from __FILE__, leaving the path relative to the project root +#define _SWAY_FILENAME ((const char *)__FILE__ + sizeof(SWAY_REL_SRC_DIR) - 1) +#else +#define _SWAY_FILENAME __FILE__ +#endif + +#define sway_log(verb, fmt, ...) \ + _sway_log(verb, "[%s:%d] " fmt, _SWAY_FILENAME, __LINE__, ##__VA_ARGS__) + +#define sway_vlog(verb, fmt, args) \ + _sway_vlog(verb, "[%s:%d] " fmt, _SWAY_FILENAME, __LINE__, args) + +#define sway_log_errno(verb, fmt, ...) \ + sway_log(verb, fmt ": %s", ##__VA_ARGS__, strerror(errno)) + +#define sway_abort(FMT, ...) \ + _sway_abort("[%s:%d] " FMT, _SWAY_FILENAME, __LINE__, ##__VA_ARGS__) + +#define sway_assert(COND, FMT, ...) \ + _sway_assert(COND, "[%s:%d] %s:" FMT, _SWAY_FILENAME, __LINE__, __PRETTY_FUNCTION__, ##__VA_ARGS__) + +#endif diff --git a/include/loop.h b/include/loop.h new file mode 100644 index 0000000..2f608ed --- /dev/null +++ b/include/loop.h @@ -0,0 +1,54 @@ +#ifndef _SWAY_LOOP_H +#define _SWAY_LOOP_H +#include + +/** + * This is an event loop system designed for sway clients, not sway itself. + * + * The loop consists of file descriptors and timers. Typically the Wayland + * display's file descriptor will be one of the fds in the loop. + */ + +struct loop; +struct loop_timer; + +/** + * Create an event loop. + */ +struct loop *loop_create(void); + +/** + * Destroy the event loop (eg. on program termination). + */ +void loop_destroy(struct loop *loop); + +/** + * Poll the event loop. This will block until one of the fds has data. + */ +void loop_poll(struct loop *loop); + +/** + * Add a file descriptor to the loop. + */ +void loop_add_fd(struct loop *loop, int fd, short mask, + void (*func)(int fd, short mask, void *data), void *data); + +/** + * Add a timer to the loop. + * + * When the timer expires, the timer will be removed from the loop and freed. + */ +struct loop_timer *loop_add_timer(struct loop *loop, int ms, + void (*callback)(void *data), void *data); + +/** + * Remove a file descriptor from the loop. + */ +bool loop_remove_fd(struct loop *loop, int fd); + +/** + * Remove a timer from the loop. + */ +bool loop_remove_timer(struct loop *loop, struct loop_timer *timer); + +#endif diff --git a/include/meson.build b/include/meson.build new file mode 100644 index 0000000..65ed027 --- /dev/null +++ b/include/meson.build @@ -0,0 +1 @@ +configure_file(output: 'config.h', configuration: conf_data) diff --git a/include/pango.h b/include/pango.h new file mode 100644 index 0000000..228e39c --- /dev/null +++ b/include/pango.h @@ -0,0 +1,25 @@ +#ifndef _SWAY_PANGO_H +#define _SWAY_PANGO_H +#include +#include +#include +#include +#include +#include "stringop.h" + +/** + * Utility function which escape characters a & < > ' ". + * + * The function returns the length of the escaped string, optionally writing the + * escaped string to dest if provided. + */ +size_t escape_markup_text(const char *src, char *dest); +PangoLayout *get_pango_layout(cairo_t *cairo, const PangoFontDescription *desc, + const char *text, double scale, bool markup); +void get_text_size(cairo_t *cairo, const PangoFontDescription *desc, int *width, int *height, + int *baseline, double scale, bool markup, const char *fmt, ...) _SWAY_ATTRIB_PRINTF(8, 9); +void get_text_metrics(const PangoFontDescription *desc, int *height, int *baseline); +void render_text(cairo_t *cairo, PangoFontDescription *desc, + double scale, bool markup, const char *fmt, ...) _SWAY_ATTRIB_PRINTF(5, 6); + +#endif diff --git a/include/pool-buffer.h b/include/pool-buffer.h new file mode 100644 index 0000000..b7a95af --- /dev/null +++ b/include/pool-buffer.h @@ -0,0 +1,24 @@ +#ifndef _SWAY_BUFFERS_H +#define _SWAY_BUFFERS_H +#include +#include +#include +#include +#include + +struct pool_buffer { + struct wl_buffer *buffer; + cairo_surface_t *surface; + cairo_t *cairo; + PangoContext *pango; + uint32_t width, height; + void *data; + size_t size; + bool busy; +}; + +struct pool_buffer *get_next_buffer(struct wl_shm *shm, + struct pool_buffer pool[static 2], uint32_t width, uint32_t height); +void destroy_buffer(struct pool_buffer *buffer); + +#endif diff --git a/include/stringop.h b/include/stringop.h new file mode 100644 index 0000000..ffc355c --- /dev/null +++ b/include/stringop.h @@ -0,0 +1,45 @@ +#ifndef _SWAY_STRINGOP_H +#define _SWAY_STRINGOP_H + +#include +#include +#include "list.h" + +#ifdef __GNUC__ +#define _SWAY_ATTRIB_PRINTF(start, end) __attribute__((format(printf, start, end))) +#else +#define _SWAY_ATTRIB_PRINTF(start, end) +#endif + +void strip_whitespace(char *str); +void strip_quotes(char *str); + +// strcat that does nothing if dest or src is NULL +char *lenient_strcat(char *dest, const char *src); +char *lenient_strncat(char *dest, const char *src, size_t len); + +// strcmp that also handles null pointers. +int lenient_strcmp(const char *a, const char *b); + +// Simply split a string with delims, free with `list_free_items_and_destroy` +list_t *split_string(const char *str, const char *delims); + +// Splits an argument string, keeping quotes intact +char **split_args(const char *str, int *argc); +void free_argv(int argc, char **argv); + +int unescape_string(char *string); +char *join_args(char **argv, int argc); + +// Split string into 2 by delim, handle quotes +char *argsep(char **stringp, const char *delim, char *matched_delim); + +// Expand a path using shell replacements such as $HOME and ~ +bool expand_path(char **path); + +char *vformat_str(const char *fmt, va_list args) _SWAY_ATTRIB_PRINTF(1, 0); +char *format_str(const char *fmt, ...) _SWAY_ATTRIB_PRINTF(1, 2); + +bool has_prefix(const char *str, const char *prefix); + +#endif diff --git a/include/sway/animation_manager.h b/include/sway/animation_manager.h new file mode 100644 index 0000000..98f1829 --- /dev/null +++ b/include/sway/animation_manager.h @@ -0,0 +1,37 @@ +#ifndef _SWAY_ANIMATION_MANAGER_H +#define _SWAY_ANIMATION_MANAGER_H + +#include +#include + +struct sway_container; +struct sway_server; + +// TODO: make animation just a pointer to progress, make multiplier and callback private +struct animation { + struct wl_list link; + float progress; + void *data; + float multiplier; + bool initialized; + void (*update)(void *); + void (*complete)(void *); +}; + +void animation_manager_init(struct sway_server *server); + +struct animation init_animation(void *data); + +void refresh_animation_manager_timing(); + +void add_animation(struct animation *animation, void (*update_callback)(void *), + void (*complete_callback)(void *)); + +void finish_animation(struct animation *animation); + +void start_animations(); + +float get_animated_value(float from, float to, const struct animation *animation); + +#endif + diff --git a/include/sway/commands.h b/include/sway/commands.h new file mode 100644 index 0000000..d3be916 --- /dev/null +++ b/include/sway/commands.h @@ -0,0 +1,350 @@ +#ifndef _SWAY_COMMANDS_H +#define _SWAY_COMMANDS_H + +#include +#include "config.h" +#include "stringop.h" + +struct sway_container; + +typedef struct cmd_results *sway_cmd(int argc, char **argv); + +struct cmd_handler { + const char *command; + sway_cmd *handle; +}; + +/** + * Indicates the result of a command's execution. + */ +enum cmd_status { + CMD_SUCCESS, /**< The command was successful */ + CMD_FAILURE, /**< The command resulted in an error */ + CMD_INVALID, /**< Unknown command or parser error */ + CMD_DEFER, /**< Command execution deferred */ + CMD_BLOCK, + CMD_BLOCK_COMMANDS, + CMD_BLOCK_END +}; + +/** + * Stores the result of executing a command. + */ +struct cmd_results { + enum cmd_status status; + /** + * Human friendly error message, or NULL on success + */ + char *error; +}; + +enum expected_args { + EXPECTED_AT_LEAST, + EXPECTED_AT_MOST, + EXPECTED_EQUAL_TO +}; + +struct cmd_results *checkarg(int argc, const char *name, + enum expected_args type, int val); + +const struct cmd_handler *find_handler(const char *line, + const struct cmd_handler *cmd_handlers, size_t handlers_size); + +/** + * Parse and executes a command. + * + * If the command string contains criteria then the command will be executed on + * all matching containers. Otherwise, it'll run on the `con` container. If + * `con` is NULL then it'll run on the currently focused container. + */ +list_t *execute_command(char *command, struct sway_seat *seat, + struct sway_container *con); +/** + * Parse and handles a command during config file loading. + * + * Do not use this under normal conditions. + */ +struct cmd_results *config_command(char *command, char **new_block); +/** + * Parse and handle a sub command + */ +struct cmd_results *config_subcommand(char **argv, int argc, + const struct cmd_handler *handlers, size_t handlers_size); +/* + * Parses a command policy rule. + */ +struct cmd_results *config_commands_command(char *exec); +/** + * Allocates a cmd_results object. + */ +struct cmd_results *cmd_results_new(enum cmd_status status, const char *error, ...) _SWAY_ATTRIB_PRINTF(2, 3); +/** + * Frees a cmd_results object. + */ +void free_cmd_results(struct cmd_results *results); +/** + * Serializes a list of cmd_results to a JSON string. + * + * Free the JSON string later on. + */ +char *cmd_results_to_json(list_t *res_list); + +/** + * TODO: Move this function and its dependent functions to container.c. + */ +void container_resize_tiled(struct sway_container *parent, uint32_t axis, + int amount); + +struct sway_container *container_find_resize_parent(struct sway_container *con, + uint32_t edge); +/** + * Effect handlers value parsers + */ +bool cmd_corner_radius_parse_value(char *arg, int* result); + +/** + * Handlers shared by exec and exec_always. + */ +sway_cmd cmd_exec_validate; +sway_cmd cmd_exec_process; + +sway_cmd cmd_allow_tearing; +sway_cmd cmd_animation_duration_ms; +sway_cmd cmd_assign; +sway_cmd cmd_bar; +sway_cmd cmd_bindcode; +sway_cmd cmd_bindgesture; +sway_cmd cmd_bindswitch; +sway_cmd cmd_bindsym; +sway_cmd cmd_blur; +sway_cmd cmd_blur_brightness; +sway_cmd cmd_blur_contrast; +sway_cmd cmd_blur_noise; +sway_cmd cmd_blur_passes; +sway_cmd cmd_blur_radius; +sway_cmd cmd_blur_saturation; +sway_cmd cmd_blur_xray; +sway_cmd cmd_border; +sway_cmd cmd_client_noop; +sway_cmd cmd_client_focused; +sway_cmd cmd_client_focused_inactive; +sway_cmd cmd_client_focused_tab_title; +sway_cmd cmd_client_unfocused; +sway_cmd cmd_client_urgent; +sway_cmd cmd_client_placeholder; +sway_cmd cmd_client_background; +sway_cmd cmd_commands; +sway_cmd cmd_corner_radius; +sway_cmd cmd_create_output; +sway_cmd cmd_default_border; +sway_cmd cmd_default_dim_inactive; +sway_cmd cmd_default_floating_border; +sway_cmd cmd_default_orientation; +sway_cmd cmd_dim_inactive; +sway_cmd cmd_dim_inactive_colors_unfocused; +sway_cmd cmd_dim_inactive_colors_urgent; +sway_cmd cmd_exec; +sway_cmd cmd_exec_always; +sway_cmd cmd_exit; +sway_cmd cmd_floating; +sway_cmd cmd_floating_maximum_size; +sway_cmd cmd_floating_minimum_size; +sway_cmd cmd_floating_modifier; +sway_cmd cmd_floating_scroll; +sway_cmd cmd_focus; +sway_cmd cmd_focus_follows_mouse; +sway_cmd cmd_focus_on_window_activation; +sway_cmd cmd_focus_wrapping; +sway_cmd cmd_font; +sway_cmd cmd_for_window; +sway_cmd cmd_force_display_urgency_hint; +sway_cmd cmd_force_focus_wrapping; +sway_cmd cmd_fullscreen; +sway_cmd cmd_gaps; +sway_cmd cmd_hide_edge_borders; +sway_cmd cmd_include; +sway_cmd cmd_inhibit_idle; +sway_cmd cmd_input; +sway_cmd cmd_seat; +sway_cmd cmd_ipc; +sway_cmd cmd_kill; +sway_cmd cmd_layer_effects; +sway_cmd cmd_layout; +sway_cmd cmd_log_colors; +sway_cmd cmd_mark; +sway_cmd cmd_max_render_time; +sway_cmd cmd_mode; +sway_cmd cmd_mouse_warping; +sway_cmd cmd_move; +sway_cmd cmd_new_float; +sway_cmd cmd_new_window; +sway_cmd cmd_nop; +sway_cmd cmd_opacity; +sway_cmd cmd_no_focus; +sway_cmd cmd_output; +sway_cmd cmd_permit; +sway_cmd cmd_popup_during_fullscreen; +sway_cmd cmd_primary_selection; +sway_cmd cmd_reject; +sway_cmd cmd_reload; +sway_cmd cmd_rename; +sway_cmd cmd_resize; +sway_cmd cmd_scratchpad; +sway_cmd cmd_scratchpad_minimize; +sway_cmd cmd_seamless_mouse; +sway_cmd cmd_set; +sway_cmd cmd_shadow_blur_radius; +sway_cmd cmd_shadow_color; +sway_cmd cmd_shadow_offset; +sway_cmd cmd_shadow_inactive_color; +sway_cmd cmd_shadows; +sway_cmd cmd_shadows_on_csd; +sway_cmd cmd_shortcuts_inhibitor; +sway_cmd cmd_show_marks; +sway_cmd cmd_smart_borders; +sway_cmd cmd_smart_corner_radius; +sway_cmd cmd_smart_gaps; +sway_cmd cmd_split; +sway_cmd cmd_splith; +sway_cmd cmd_splitt; +sway_cmd cmd_splitv; +sway_cmd cmd_sticky; +sway_cmd cmd_swaybg_command; +sway_cmd cmd_swaynag_command; +sway_cmd cmd_swap; +sway_cmd cmd_tiling_drag; +sway_cmd cmd_tiling_drag_threshold; +sway_cmd cmd_title_align; +sway_cmd cmd_title_format; +sway_cmd cmd_titlebar_border_thickness; +sway_cmd cmd_titlebar_padding; +sway_cmd cmd_titlebar_separator; +sway_cmd cmd_unbindcode; +sway_cmd cmd_unbindswitch; +sway_cmd cmd_unbindgesture; +sway_cmd cmd_unbindsym; +sway_cmd cmd_unmark; +sway_cmd cmd_urgent; +sway_cmd cmd_workspace; +sway_cmd cmd_workspace_layout; +sway_cmd cmd_ws_auto_back_and_forth; +sway_cmd cmd_xwayland; + +sway_cmd bar_cmd_bindcode; +sway_cmd bar_cmd_binding_mode_indicator; +sway_cmd bar_cmd_bindsym; +sway_cmd bar_cmd_colors; +sway_cmd bar_cmd_font; +sway_cmd bar_cmd_gaps; +sway_cmd bar_cmd_mode; +sway_cmd bar_cmd_modifier; +sway_cmd bar_cmd_output; +sway_cmd bar_cmd_height; +sway_cmd bar_cmd_hidden_state; +sway_cmd bar_cmd_icon_theme; +sway_cmd bar_cmd_id; +sway_cmd bar_cmd_position; +sway_cmd bar_cmd_separator_symbol; +sway_cmd bar_cmd_status_command; +sway_cmd bar_cmd_status_edge_padding; +sway_cmd bar_cmd_status_padding; +sway_cmd bar_cmd_pango_markup; +sway_cmd bar_cmd_strip_workspace_numbers; +sway_cmd bar_cmd_strip_workspace_name; +sway_cmd bar_cmd_swaybar_command; +sway_cmd bar_cmd_tray_bindcode; +sway_cmd bar_cmd_tray_bindsym; +sway_cmd bar_cmd_tray_output; +sway_cmd bar_cmd_tray_padding; +sway_cmd bar_cmd_unbindcode; +sway_cmd bar_cmd_unbindsym; +sway_cmd bar_cmd_wrap_scroll; +sway_cmd bar_cmd_workspace_buttons; +sway_cmd bar_cmd_workspace_min_width; + +sway_cmd bar_colors_cmd_active_workspace; +sway_cmd bar_colors_cmd_background; +sway_cmd bar_colors_cmd_focused_background; +sway_cmd bar_colors_cmd_binding_mode; +sway_cmd bar_colors_cmd_focused_workspace; +sway_cmd bar_colors_cmd_inactive_workspace; +sway_cmd bar_colors_cmd_separator; +sway_cmd bar_colors_cmd_focused_separator; +sway_cmd bar_colors_cmd_statusline; +sway_cmd bar_colors_cmd_focused_statusline; +sway_cmd bar_colors_cmd_urgent_workspace; + +sway_cmd input_cmd_seat; +sway_cmd input_cmd_accel_profile; +sway_cmd input_cmd_calibration_matrix; +sway_cmd input_cmd_click_method; +sway_cmd input_cmd_clickfinger_button_map; +sway_cmd input_cmd_drag; +sway_cmd input_cmd_drag_lock; +sway_cmd input_cmd_dwt; +sway_cmd input_cmd_dwtp; +sway_cmd input_cmd_events; +sway_cmd input_cmd_left_handed; +sway_cmd input_cmd_map_from_region; +sway_cmd input_cmd_map_to_output; +sway_cmd input_cmd_map_to_region; +sway_cmd input_cmd_middle_emulation; +sway_cmd input_cmd_natural_scroll; +sway_cmd input_cmd_pointer_accel; +sway_cmd input_cmd_rotation_angle; +sway_cmd input_cmd_scroll_factor; +sway_cmd input_cmd_repeat_delay; +sway_cmd input_cmd_repeat_rate; +sway_cmd input_cmd_scroll_button; +sway_cmd input_cmd_scroll_button_lock; +sway_cmd input_cmd_scroll_method; +sway_cmd input_cmd_tap; +sway_cmd input_cmd_tap_button_map; +sway_cmd input_cmd_tool_mode; +sway_cmd input_cmd_xkb_capslock; +sway_cmd input_cmd_xkb_file; +sway_cmd input_cmd_xkb_layout; +sway_cmd input_cmd_xkb_model; +sway_cmd input_cmd_xkb_numlock; +sway_cmd input_cmd_xkb_options; +sway_cmd input_cmd_xkb_rules; +sway_cmd input_cmd_xkb_switch_layout; +sway_cmd input_cmd_xkb_variant; + +sway_cmd output_cmd_adaptive_sync; +sway_cmd output_cmd_allow_tearing; +sway_cmd output_cmd_background; +sway_cmd output_cmd_color_profile; +sway_cmd output_cmd_disable; +sway_cmd output_cmd_dpms; +sway_cmd output_cmd_enable; +sway_cmd output_cmd_hdr; +sway_cmd output_cmd_max_render_time; +sway_cmd output_cmd_mode; +sway_cmd output_cmd_modeline; +sway_cmd output_cmd_position; +sway_cmd output_cmd_power; +sway_cmd output_cmd_render_bit_depth; +sway_cmd output_cmd_scale; +sway_cmd output_cmd_scale_filter; +sway_cmd output_cmd_subpixel; +sway_cmd output_cmd_toggle; +sway_cmd output_cmd_transform; +sway_cmd output_cmd_unplug; + +sway_cmd seat_cmd_attach; +sway_cmd seat_cmd_cursor; +sway_cmd seat_cmd_fallback; +sway_cmd seat_cmd_hide_cursor; +sway_cmd seat_cmd_idle_inhibit; +sway_cmd seat_cmd_idle_wake; +sway_cmd seat_cmd_keyboard_grouping; +sway_cmd seat_cmd_pointer_constraint; +sway_cmd seat_cmd_shortcuts_inhibitor; +sway_cmd seat_cmd_xcursor_theme; + +sway_cmd cmd_ipc_cmd; +sway_cmd cmd_ipc_events; +sway_cmd cmd_ipc_event_cmd; + +#endif diff --git a/include/sway/config.h b/include/sway/config.h new file mode 100644 index 0000000..849411b --- /dev/null +++ b/include/sway/config.h @@ -0,0 +1,790 @@ +#ifndef _SWAY_CONFIG_H +#define _SWAY_CONFIG_H +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../include/config.h" +#include "gesture.h" +#include "list.h" +#include "stringop.h" +#include "swaynag.h" +#include "tree/container.h" +#include "sway/input/tablet.h" +#include "sway/tree/root.h" +#include "wlr-layer-shell-unstable-v1-protocol.h" +#include + +// TODO: Refactor this shit + +/** + * Describes a variable created via the `set` command. + */ +struct sway_variable { + char *name; + char *value; +}; + +enum binding_input_type { + BINDING_KEYCODE, + BINDING_KEYSYM, + BINDING_MOUSECODE, + BINDING_MOUSESYM, + BINDING_SWITCH, // dummy, only used to call seat_execute_command + BINDING_GESTURE // dummy, only used to call seat_execute_command +}; + +enum binding_flags { + BINDING_RELEASE = 1 << 0, + BINDING_LOCKED = 1 << 1, // keyboard only + BINDING_BORDER = 1 << 2, // mouse only; trigger on container border + BINDING_CONTENTS = 1 << 3, // mouse only; trigger on container contents + BINDING_TITLEBAR = 1 << 4, // mouse only; trigger on container titlebar + BINDING_CODE = 1 << 5, // keyboard only; convert keysyms into keycodes + BINDING_RELOAD = 1 << 6, // switch only; (re)trigger binding on reload + BINDING_INHIBITED = 1 << 7, // keyboard only: ignore shortcut inhibitor + BINDING_NOREPEAT = 1 << 8, // keyboard only; do not trigger when repeating a held key + BINDING_EXACT = 1 << 9, // gesture only; only trigger on exact match +}; + +/** + * A key (or mouse) binding and an associated command. + */ +struct sway_binding { + enum binding_input_type type; + int order; + char *input; + uint32_t flags; + list_t *keys; // sorted in ascending order + list_t *syms; // sorted in ascending order; NULL if BINDING_CODE is not set + uint32_t modifiers; + xkb_layout_index_t group; + char *command; +}; + +enum sway_switch_trigger { + SWAY_SWITCH_TRIGGER_OFF, + SWAY_SWITCH_TRIGGER_ON, + SWAY_SWITCH_TRIGGER_TOGGLE, +}; + +/** + * A laptop switch binding and an associated command. + */ +struct sway_switch_binding { + enum wlr_switch_type type; + enum sway_switch_trigger trigger; + uint32_t flags; + char *command; +}; + +/** + * A gesture binding and an associated command. + */ +struct sway_gesture_binding { + char *input; + uint32_t flags; + struct gesture gesture; + char *command; +}; + +/** + * Focus on window activation. + */ +enum sway_fowa { + FOWA_SMART, + FOWA_URGENT, + FOWA_FOCUS, + FOWA_NONE, +}; + +/** + * A "mode" of keybindings created via the `mode` command. + */ +struct sway_mode { + char *name; + list_t *keysym_bindings; + list_t *keycode_bindings; + list_t *mouse_bindings; + list_t *switch_bindings; + list_t *gesture_bindings; + bool pango; +}; + +struct input_config_mapped_from_region { + double x1, y1; + double x2, y2; + bool mm; +}; + +struct calibration_matrix { + bool configured; + float matrix[6]; +}; + +enum input_config_mapped_to { + MAPPED_TO_DEFAULT, + MAPPED_TO_OUTPUT, + MAPPED_TO_REGION, +}; + +struct input_config_tool { + enum wlr_tablet_tool_type type; + enum sway_tablet_tool_mode mode; +}; + +/** + * options for input devices + */ +struct input_config { + char *identifier; + const char *input_type; + + int accel_profile; + struct calibration_matrix calibration_matrix; + int click_method; + int clickfinger_button_map; + int drag; + int drag_lock; + int dwt; + int dwtp; + int left_handed; + int middle_emulation; + int natural_scroll; + float pointer_accel; + float rotation_angle; + float scroll_factor; + int repeat_delay; + int repeat_rate; + int scroll_button; + int scroll_button_lock; + int scroll_method; + int send_events; + int tap; + int tap_button_map; + + char *xkb_layout; + char *xkb_model; + char *xkb_options; + char *xkb_rules; + char *xkb_variant; + char *xkb_file; + + bool xkb_file_is_set; + + int xkb_numlock; + int xkb_capslock; + + struct input_config_mapped_from_region *mapped_from_region; + + enum input_config_mapped_to mapped_to; + char *mapped_to_output; + struct wlr_box *mapped_to_region; + + list_t *tools; + + bool capturable; + struct wlr_box region; +}; + +/** + * Options for misc device configurations that happen in the seat block + */ +struct seat_attachment_config { + char *identifier; + // TODO other things are configured here for some reason +}; + +enum seat_config_hide_cursor_when_typing { + HIDE_WHEN_TYPING_DEFAULT, // the default is currently disabled + HIDE_WHEN_TYPING_ENABLE, + HIDE_WHEN_TYPING_DISABLE, +}; + +enum seat_config_allow_constrain { + CONSTRAIN_DEFAULT, // the default is currently enabled + CONSTRAIN_ENABLE, + CONSTRAIN_DISABLE, +}; + +enum seat_config_shortcuts_inhibit { + SHORTCUTS_INHIBIT_DEFAULT, // the default is currently enabled + SHORTCUTS_INHIBIT_ENABLE, + SHORTCUTS_INHIBIT_DISABLE, +}; + +enum seat_keyboard_grouping { + KEYBOARD_GROUP_DEFAULT, // the default is currently smart + KEYBOARD_GROUP_NONE, + KEYBOARD_GROUP_SMART, // keymap and repeat info +}; + +enum sway_input_idle_source { + IDLE_SOURCE_KEYBOARD = 1 << 0, + IDLE_SOURCE_POINTER = 1 << 1, + IDLE_SOURCE_TOUCH = 1 << 2, + IDLE_SOURCE_TABLET_PAD = 1 << 3, + IDLE_SOURCE_TABLET_TOOL = 1 << 4, + IDLE_SOURCE_SWITCH = 1 << 5, +}; + +/** + * Options for multiseat and other misc device configurations + */ +struct seat_config { + char *name; + int fallback; // -1 means not set + list_t *attachments; // list of seat_attachment configs + int hide_cursor_timeout; + enum seat_config_hide_cursor_when_typing hide_cursor_when_typing; + enum seat_config_allow_constrain allow_constrain; + enum seat_config_shortcuts_inhibit shortcuts_inhibit; + enum seat_keyboard_grouping keyboard_grouping; + uint32_t idle_inhibit_sources, idle_wake_sources; + struct { + char *name; + int size; + } xcursor_theme; +}; + +enum scale_filter_mode { + SCALE_FILTER_DEFAULT, // the default is currently smart + SCALE_FILTER_LINEAR, + SCALE_FILTER_NEAREST, + SCALE_FILTER_SMART, +}; + +enum render_bit_depth { + RENDER_BIT_DEPTH_DEFAULT, // the default is currently 8 for SDR, 10 for HDR + RENDER_BIT_DEPTH_6, + RENDER_BIT_DEPTH_8, + RENDER_BIT_DEPTH_10, +}; + +enum color_profile { + COLOR_PROFILE_DEFAULT, // default is Transform with NULL color_transform + COLOR_PROFILE_TRANSFORM, // use color_transform from output_config + COLOR_PROFILE_TRANSFORM_WITH_DEVICE_PRIMARIES, // create transform from wlr_output +}; + +/** + * Size and position configuration for a particular output. + * + * This is set via the `output` command. + */ +struct output_config { + char *name; + int enabled; + int power; + int width, height; + float refresh_rate; + int custom_mode; + drmModeModeInfo drm_mode; + int x, y; + float scale; + enum scale_filter_mode scale_filter; + int32_t transform; + enum wl_output_subpixel subpixel; + int max_render_time; // In milliseconds + int adaptive_sync; + enum render_bit_depth render_bit_depth; + enum color_profile color_profile; + struct wlr_color_transform *color_transform; + int allow_tearing; + int hdr; + + char *background; + char *background_option; + char *background_fallback; +}; + +/** + * Stores size of gaps for each side + */ +struct side_gaps { + int top; + int right; + int bottom; + int left; +}; + +enum smart_gaps_mode { + SMART_GAPS_OFF, + SMART_GAPS_ON, + SMART_GAPS_INVERSE_OUTER, +}; + +/** + * Stores configuration for a workspace, regardless of whether the workspace + * exists. + */ +struct workspace_config { + char *workspace; + list_t *outputs; + int gaps_inner; + struct side_gaps gaps_outer; +}; + +enum pango_markup_config { + PANGO_MARKUP_DISABLED = false, + PANGO_MARKUP_ENABLED = true, + PANGO_MARKUP_DEFAULT // The default is font dependent ("pango:" prefix) +}; + +struct bar_config { + char *swaybar_command; + struct wl_client *client; + struct wl_listener client_destroy; + + /** + * One of "dock", "hide", "invisible" + * + * Always visible in dock mode. Visible only when modifier key is held in hide mode. + * Never visible in invisible mode. + */ + char *mode; + /** + * One of "show" or "hide". + * + * In "show" mode, it will always be shown on top of the active workspace. + */ + char *hidden_state; + bool visible_by_modifier; // only relevant in "hide" mode + /** + * Id name used to identify the bar through IPC. + * + * Defaults to bar-x, where x corresponds to the position of the + * embedding bar block in the config file (bar-0, bar-1, ...). + */ + char *id; + uint32_t modifier; + list_t *outputs; + char *position; + list_t *bindings; + char *status_command; + enum pango_markup_config pango_markup; + char *font; + int height; // -1 not defined + bool workspace_buttons; + bool wrap_scroll; + char *separator_symbol; + bool strip_workspace_numbers; + bool strip_workspace_name; + bool binding_mode_indicator; + bool verbose; + struct side_gaps gaps; + int status_padding; + int status_edge_padding; + uint32_t workspace_min_width; + struct { + char *background; + char *statusline; + char *separator; + char *focused_background; + char *focused_statusline; + char *focused_separator; + char *focused_workspace_border; + char *focused_workspace_bg; + char *focused_workspace_text; + char *active_workspace_border; + char *active_workspace_bg; + char *active_workspace_text; + char *inactive_workspace_border; + char *inactive_workspace_bg; + char *inactive_workspace_text; + char *urgent_workspace_border; + char *urgent_workspace_bg; + char *urgent_workspace_text; + char *binding_mode_border; + char *binding_mode_bg; + char *binding_mode_text; + } colors; + +#if HAVE_TRAY + char *icon_theme; + struct wl_list tray_bindings; // struct tray_binding::link + list_t *tray_outputs; // char * + int tray_padding; +#endif +}; + +struct bar_binding { + uint32_t button; + bool release; + char *command; +}; + +#if HAVE_TRAY +struct tray_binding { + uint32_t button; + const char *command; + struct wl_list link; // struct tray_binding::link +}; +#endif + +struct border_colors { + float border[4]; + float background[4]; + float text[4]; + float indicator[4]; + float child_border[4]; +}; + +enum edge_border_types { + E_NONE, /**< Don't hide edge borders */ + E_VERTICAL, /**< hide vertical edge borders */ + E_HORIZONTAL, /**< hide horizontal edge borders */ + E_BOTH, /**< hide vertical and horizontal edge borders */ +}; + +enum edge_border_smart_types { + ESMART_OFF, + ESMART_ON, /**< hide edges if precisely one window is present in workspace */ + ESMART_NO_GAPS, /**< hide edges if one window and gaps to edge is zero */ +}; + +enum sway_popup_during_fullscreen { + POPUP_SMART, + POPUP_IGNORE, + POPUP_LEAVE, +}; + +enum focus_follows_mouse_mode { + FOLLOWS_NO, + FOLLOWS_YES, + FOLLOWS_ALWAYS, +}; + +enum focus_wrapping_mode { + WRAP_NO, + WRAP_YES, + WRAP_FORCE, + WRAP_WORKSPACE, +}; + +enum mouse_warping_mode { + WARP_NO, + WARP_OUTPUT, + WARP_CONTAINER, +}; + +enum alignment { + ALIGN_LEFT, + ALIGN_CENTER, + ALIGN_RIGHT, +}; + +enum xwayland_mode { + XWAYLAND_MODE_DISABLED, + XWAYLAND_MODE_LAZY, + XWAYLAND_MODE_IMMEDIATE, +}; + +/** + * The configuration struct. The result of loading a config file. + */ +struct sway_config { + float animation_duration_ms; + int corner_radius; + bool smart_corner_radius; + + float default_dim_inactive; + struct { + float unfocused[4]; + float urgent[4]; + } dim_inactive_colors; + + bool blur_enabled; + bool blur_xray; + struct blur_data blur_data; + + bool shadow_enabled; + bool shadows_on_csd_enabled; + int shadow_blur_sigma; + float shadow_color[4]; + float shadow_inactive_color[4]; + float shadow_offset_x, shadow_offset_y; + + bool titlebar_separator; + bool scratchpad_minimize; + + list_t *layer_criteria; + + char *swaynag_command; + struct swaynag_instance swaynag_config_errors; + list_t *symbols; + list_t *modes; + list_t *bars; + list_t *cmd_queue; + list_t *workspace_configs; + list_t *output_configs; + list_t *input_configs; + list_t *input_type_configs; + list_t *seat_configs; + list_t *criteria; + list_t *no_focus; + list_t *active_bar_modifiers; + struct sway_mode *current_mode; + struct bar_config *current_bar; + uint32_t floating_mod; + bool floating_mod_inverse; + uint32_t dragging_key; + uint32_t resizing_key; + char *floating_scroll_up_cmd; + char *floating_scroll_down_cmd; + char *floating_scroll_left_cmd; + char *floating_scroll_right_cmd; + enum sway_container_layout default_orientation; + enum sway_container_layout default_layout; + char *font; // Used for IPC. + PangoFontDescription *font_description; // Used internally for rendering and validating. + int font_height; + int font_baseline; + bool pango_markup; + int titlebar_border_thickness; + int titlebar_h_padding; + int titlebar_v_padding; + size_t urgent_timeout; + enum sway_fowa focus_on_window_activation; + enum sway_popup_during_fullscreen popup_during_fullscreen; + enum xwayland_mode xwayland; + + // swaybg + char *swaybg_command; + struct wl_client *swaybg_client; + struct wl_listener swaybg_client_destroy; + + // Flags + enum focus_follows_mouse_mode focus_follows_mouse; + enum mouse_warping_mode mouse_warping; + enum focus_wrapping_mode focus_wrapping; + bool active; + bool failed; + bool reloading; + bool reading; + bool validating; + bool auto_back_and_forth; + bool show_marks; + enum alignment title_align; + bool primary_selection; + + bool tiling_drag; + int tiling_drag_threshold; + + enum smart_gaps_mode smart_gaps; + int gaps_inner; + struct side_gaps gaps_outer; + + list_t *config_chain; + bool user_config_path; + const char *current_config_path; + const char *current_config; + int current_config_line_number; + char *current_config_line; + + enum sway_container_border border; + enum sway_container_border floating_border; + int border_thickness; + int floating_border_thickness; + enum edge_border_types hide_edge_borders; + enum edge_border_smart_types hide_edge_borders_smart; + bool hide_lone_tab; + + // border colors + struct { + struct border_colors focused; + struct border_colors focused_inactive; + struct border_colors focused_tab_title; + struct border_colors unfocused; + struct border_colors urgent; + struct border_colors placeholder; + float background[4]; + } border_colors; + + bool has_focused_tab_title; + + // floating view + int32_t floating_maximum_width; + int32_t floating_maximum_height; + int32_t floating_minimum_width; + int32_t floating_minimum_height; + + // The keysym to keycode translation + struct xkb_state *keysym_translation_state; + + // Context for command handlers + struct { + struct input_config *input_config; + struct output_config *output_config; + struct seat_config *seat_config; + struct sway_seat *seat; + struct sway_node *node; + struct sway_container *container; + struct sway_workspace *workspace; + bool node_overridden; // True if the node is selected by means other than focus + struct { + int argc; + char **argv; + } leftovers; + } handler_context; +}; + +/** + * Loads the main config from the given path. is_active should be true when + * reloading the config. + */ +bool load_main_config(const char *path, bool is_active, bool validating); + +/** + * Loads an included config. Can only be used after load_main_config. + */ +void load_include_configs(const char *path, struct sway_config *config, + struct swaynag_instance *swaynag); + +/** + * Reads the config from the given FILE. + */ +bool read_config(FILE *file, struct sway_config *config, + struct swaynag_instance *swaynag); + +/** + * Run the commands that were deferred when reading the config file. + */ +void run_deferred_commands(void); + +/** + * Run the binding commands that were deferred when initializing the inputs + */ +void run_deferred_bindings(void); + +/** + * Adds a warning entry to the swaynag instance used for errors. + */ +void config_add_swaynag_warning(char *fmt, ...) _SWAY_ATTRIB_PRINTF(1, 2); + +/** + * Free config struct + */ +void free_config(struct sway_config *config); + +void free_sway_variable(struct sway_variable *var); + +/** + * Does variable replacement for a string based on the config's currently loaded variables. + */ +char *do_var_replacement(char *str); + +int input_identifier_cmp(const void *item, const void *data); + +struct input_config *new_input_config(const char* identifier); + +void merge_input_config(struct input_config *dst, struct input_config *src); + +struct input_config *store_input_config(struct input_config *ic, char **error); + +void input_config_fill_rule_names(struct input_config *ic, + struct xkb_rule_names *rules); + +void free_input_config(struct input_config *ic); + +int seat_name_cmp(const void *item, const void *data); + +struct seat_config *new_seat_config(const char* name); + +void merge_seat_config(struct seat_config *dst, struct seat_config *src); + +struct seat_config *copy_seat_config(struct seat_config *seat); + +void free_seat_config(struct seat_config *ic); + +struct seat_attachment_config *seat_attachment_config_new(void); + +struct seat_attachment_config *seat_config_get_attachment( + struct seat_config *seat_config, char *identifier); + +struct seat_config *store_seat_config(struct seat_config *seat); + +int output_name_cmp(const void *item, const void *data); + +void output_get_identifier(char *identifier, size_t len, + struct sway_output *output); + +const char *sway_output_scale_filter_to_string(enum scale_filter_mode scale_filter); + +struct output_config *new_output_config(const char *name); + +bool apply_output_configs(struct output_config **ocs, size_t ocs_len, + bool test_only, bool degrade_to_off); + +void apply_stored_output_configs(void); + +/** + * store_output_config stores a new output config. An output may be matched by + * three different config types, in order of precedence: Identifier, name and + * wildcard. When storing a config type of lower precedence, assume that the + * user wants the config to take immediate effect by superseding (clearing) the + * same values from higher presedence configuration. + */ +void store_output_config(struct output_config *oc); + +struct output_config *find_output_config(struct sway_output *output); + +void free_output_config(struct output_config *oc); + +void request_modeset(void); +void force_modeset(void); +bool modeset_is_pending(void); + +bool spawn_swaybg(void); + +int workspace_output_cmp_workspace(const void *a, const void *b); + +void free_sway_binding(struct sway_binding *sb); + +void free_switch_binding(struct sway_switch_binding *binding); + +void free_gesture_binding(struct sway_gesture_binding *binding); + +void seat_execute_command(struct sway_seat *seat, struct sway_binding *binding); + +void load_swaybar(struct bar_config *bar); + +void load_swaybars(void); + +struct bar_config *default_bar_config(void); + +void free_bar_config(struct bar_config *bar); + +void free_bar_binding(struct bar_binding *binding); + +void free_workspace_config(struct workspace_config *wsc); + +/** + * Updates the value of config->font_height based on the metrics for title's + * font as reported by pango. + * + * If the height has changed, all containers will be rearranged to take on the + * new size. + */ +void config_update_font_height(void); + +/** + * Convert bindsym into bindcode using the first configured layout. + * Return false in case the conversion is unsuccessful. + */ +bool translate_binding(struct sway_binding *binding); + +void translate_keysyms(struct input_config *input_config); + +void binding_add_translated(struct sway_binding *binding, list_t *bindings); + +/* Global config singleton. */ +extern struct sway_config *config; + +#endif diff --git a/include/sway/criteria.h b/include/sway/criteria.h new file mode 100644 index 0000000..fad278e --- /dev/null +++ b/include/sway/criteria.h @@ -0,0 +1,91 @@ +#ifndef _SWAY_CRITERIA_H +#define _SWAY_CRITERIA_H + +#define PCRE2_CODE_UNIT_WIDTH 8 +#include +#include "config.h" +#include "list.h" +#include "tree/view.h" + +#if WLR_HAS_XWAYLAND +#include "sway/xwayland.h" +#endif + +enum criteria_type { + CT_COMMAND = 1 << 0, + CT_ASSIGN_OUTPUT = 1 << 1, + CT_ASSIGN_WORKSPACE = 1 << 2, + CT_ASSIGN_WORKSPACE_NUMBER = 1 << 3, + CT_NO_FOCUS = 1 << 4, +}; + +enum pattern_type { + PATTERN_PCRE2, + PATTERN_FOCUSED, +}; + +struct pattern { + enum pattern_type match_type; + pcre2_code *regex; +}; + +struct criteria { + enum criteria_type type; + char *raw; // entire criteria string (for logging) + char *cmdlist; + char *target; // workspace or output name for `assign` criteria + + struct pattern *title; + struct pattern *shell; + struct pattern *app_id; + struct pattern *con_mark; + uint32_t con_id; // internal ID +#if WLR_HAS_XWAYLAND + struct pattern *class; + uint32_t id; // X11 window ID + struct pattern *instance; + struct pattern *window_role; + enum atom_name window_type; +#endif + bool all; + bool floating; + bool tiling; + char urgent; // 'l' for latest or 'o' for oldest + struct pattern *workspace; + pid_t pid; + struct pattern *sandbox_engine; + struct pattern *sandbox_app_id; + struct pattern *sandbox_instance_id; + struct pattern *tag; +}; + +bool criteria_is_empty(struct criteria *criteria); +bool criteria_is_equal(struct criteria *left, struct criteria *right); + +bool criteria_already_exists(struct criteria *criteria); + +void criteria_destroy(struct criteria *criteria); + +/** + * Generate a criteria struct from a raw criteria string such as + * [class="foo" instance="bar"] (brackets inclusive). + * + * The error argument is expected to be an address of a null pointer. If an + * error is encountered, the function will return NULL and the pointer will be + * changed to point to the error string. This string should be freed afterwards. + */ +struct criteria *criteria_parse(char *raw, char **error); + +/** + * Compile a list of criterias matching the given view. + * + * Criteria types can be bitwise ORed. + */ +list_t *criteria_for_view(struct sway_view *view, enum criteria_type types); + +/** + * Compile a list of containers matching the given criteria. + */ +list_t *criteria_get_containers(struct criteria *criteria); + +#endif diff --git a/include/sway/decoration.h b/include/sway/decoration.h new file mode 100644 index 0000000..7916746 --- /dev/null +++ b/include/sway/decoration.h @@ -0,0 +1,17 @@ +#ifndef _SWAY_DECORATION_H +#define _SWAY_DECORATION_H + +#include + +struct sway_server_decoration { + struct wlr_server_decoration *wlr_server_decoration; + struct wl_list link; + + struct wl_listener destroy; + struct wl_listener mode; +}; + +struct sway_server_decoration *decoration_from_surface( + struct wlr_surface *surface); + +#endif diff --git a/include/sway/desktop/idle_inhibit_v1.h b/include/sway/desktop/idle_inhibit_v1.h new file mode 100644 index 0000000..447ac87 --- /dev/null +++ b/include/sway/desktop/idle_inhibit_v1.h @@ -0,0 +1,48 @@ +#ifndef _SWAY_DESKTOP_IDLE_INHIBIT_V1_H +#define _SWAY_DESKTOP_IDLE_INHIBIT_V1_H +#include + +enum sway_idle_inhibit_mode { + INHIBIT_IDLE_APPLICATION, // Application set inhibitor (when visible) + INHIBIT_IDLE_FOCUS, // User set inhibitor when focused + INHIBIT_IDLE_FULLSCREEN, // User set inhibitor when fullscreen + visible + INHIBIT_IDLE_OPEN, // User set inhibitor while open + INHIBIT_IDLE_VISIBLE // User set inhibitor when visible +}; + +struct sway_idle_inhibit_manager_v1 { + struct wlr_idle_inhibit_manager_v1 *wlr_manager; + struct wl_listener new_idle_inhibitor_v1; + struct wl_listener manager_destroy; + struct wl_list inhibitors; +}; + +struct sway_idle_inhibitor_v1 { + struct wlr_idle_inhibitor_v1 *wlr_inhibitor; + struct sway_view *view; + enum sway_idle_inhibit_mode mode; + + struct wl_list link; + struct wl_listener destroy; +}; + +bool sway_idle_inhibit_v1_is_active( + struct sway_idle_inhibitor_v1 *inhibitor); + +void sway_idle_inhibit_v1_check_active(void); + +void sway_idle_inhibit_v1_user_inhibitor_register(struct sway_view *view, + enum sway_idle_inhibit_mode mode); + +struct sway_idle_inhibitor_v1 *sway_idle_inhibit_v1_user_inhibitor_for_view( + struct sway_view *view); + +struct sway_idle_inhibitor_v1 *sway_idle_inhibit_v1_application_inhibitor_for_view( + struct sway_view *view); + +void sway_idle_inhibit_v1_user_inhibitor_destroy( + struct sway_idle_inhibitor_v1 *inhibitor); + +bool sway_idle_inhibit_manager_v1_init(void); + +#endif diff --git a/include/sway/desktop/launcher.h b/include/sway/desktop/launcher.h new file mode 100644 index 0000000..412068a --- /dev/null +++ b/include/sway/desktop/launcher.h @@ -0,0 +1,40 @@ +#ifndef _SWAY_LAUNCHER_H +#define _SWAY_LAUNCHER_H + +#include +#include +#include "sway/input/seat.h" + +struct launcher_ctx { + pid_t pid; + char *fallback_name; + struct wlr_xdg_activation_token_v1 *token; + struct wl_listener token_destroy; + struct sway_seat *seat; + struct wl_listener seat_destroy; + + bool activated; + bool had_focused_surface; + + struct sway_node *node; + struct wl_listener node_destroy; + + struct wl_list link; // sway_server::pending_launcher_ctxs +}; + +struct launcher_ctx *launcher_ctx_find_pid(pid_t pid); + +struct sway_workspace *launcher_ctx_get_workspace(struct launcher_ctx *ctx); + +void launcher_ctx_consume(struct launcher_ctx *ctx); + +void launcher_ctx_destroy(struct launcher_ctx *ctx); + +struct launcher_ctx *launcher_ctx_create_internal(void); + +struct launcher_ctx *launcher_ctx_create( + struct wlr_xdg_activation_token_v1 *token, struct sway_node *node); + +const char *launcher_ctx_get_token_name(struct launcher_ctx *ctx); + +#endif diff --git a/include/sway/desktop/transaction.h b/include/sway/desktop/transaction.h new file mode 100644 index 0000000..d878a23 --- /dev/null +++ b/include/sway/desktop/transaction.h @@ -0,0 +1,65 @@ +#ifndef _SWAY_TRANSACTION_H +#define _SWAY_TRANSACTION_H +#include +#include +#include +#include + +/** + * Transactions enable us to perform atomic layout updates. + * + * A transaction contains a list of containers and their new state. + * A state might contain a new size, or new border settings, or new parent/child + * relationships. + * + * Committing a transaction makes sway notify of all the affected clients with + * their new sizes. We then wait for all the views to respond with their new + * surface sizes. When all are ready, or when a timeout has passed, we apply the + * updates all at the same time. + * + * When we want to make adjustments to the layout, we change the pending state + * in containers, mark them as dirty and call transaction_commit_dirty(). This + * create and commits a transaction from the dirty containers. + */ + +struct sway_transaction_instruction; +struct sway_view; + +/** + * Find all dirty containers, create and commit a transaction containing them, + * and unmark them as dirty. + */ +void transaction_commit_dirty(void); + +/* + * Same as transaction_commit_dirty, but signalling that this is a + * client-initiated change has already taken effect. + */ +void transaction_commit_dirty_client(void); + +/** + * Notify the transaction system that a view is ready for the new layout. + * + * When all views in the transaction are ready, the layout will be applied. + * + * A success boolean is returned denoting that this part of the transaction is + * ready. + */ +bool transaction_notify_view_ready_by_serial(struct sway_view *view, + uint32_t serial); + +/** + * Notify the transaction system that a view is ready for the new layout, but + * identifying the instruction by geometry rather than by serial. + * + * This is used by xwayland views, as they don't have serials. + * + * A success boolean is returned denoting that this part of the transaction is + * ready. + */ +bool transaction_notify_view_ready_by_geometry(struct sway_view *view, + double x, double y, int width, int height); + +void arrange_popups(struct wlr_scene_tree *popups); + +#endif diff --git a/include/sway/input/cursor.h b/include/sway/input/cursor.h new file mode 100644 index 0000000..527d035 --- /dev/null +++ b/include/sway/input/cursor.h @@ -0,0 +1,150 @@ +#ifndef _SWAY_INPUT_CURSOR_H +#define _SWAY_INPUT_CURSOR_H +#include +#include +#include +#include +#include +#include "sway/input/seat.h" +#include "config.h" + +#define SWAY_CURSOR_PRESSED_BUTTONS_CAP 32 + +#define SWAY_SCROLL_UP KEY_MAX + 1 +#define SWAY_SCROLL_DOWN KEY_MAX + 2 +#define SWAY_SCROLL_LEFT KEY_MAX + 3 +#define SWAY_SCROLL_RIGHT KEY_MAX + 4 + +struct sway_cursor { + struct sway_seat *seat; + struct wlr_cursor *cursor; + struct { + double x, y; + struct sway_node *node; + } previous; + struct wlr_xcursor_manager *xcursor_manager; + struct wl_list tablets; + struct wl_list tablet_pads; + + const char *image; + struct wl_client *image_client; + struct wlr_surface *image_surface; + int hotspot_x, hotspot_y; + + struct wlr_pointer_constraint_v1 *active_constraint; + pixman_region32_t confine; // invalid if active_constraint == NULL + bool active_confine_requires_warp; + + struct wl_listener hold_begin; + struct wl_listener hold_end; + struct wl_listener pinch_begin; + struct wl_listener pinch_update; + struct wl_listener pinch_end; + struct wl_listener swipe_begin; + struct wl_listener swipe_update; + struct wl_listener swipe_end; + + struct wl_listener motion; + struct wl_listener motion_absolute; + struct wl_listener button; + struct wl_listener axis; + struct wl_listener frame; + + struct wl_listener touch_down; + struct wl_listener touch_up; + struct wl_listener touch_cancel; + struct wl_listener touch_motion; + struct wl_listener touch_frame; + bool simulating_pointer_from_touch; + bool pointer_touch_up; + int32_t pointer_touch_id; + + struct wl_listener tool_axis; + struct wl_listener tool_tip; + struct wl_listener tool_proximity; + struct wl_listener tool_button; + bool simulating_pointer_from_tool_tip; + bool simulating_pointer_from_tool_button; + uint32_t tool_buttons; + + struct wl_listener request_set_cursor; + struct wl_listener image_surface_destroy; + + struct wl_listener constraint_commit; + + struct wl_event_source *hide_source; + bool hidden; + // This field is just a cache of the field in seat_config in order to avoid + // costly seat_config lookups on every keypress. HIDE_WHEN_TYPING_DEFAULT + // indicates that there is no cached value. + enum seat_config_hide_cursor_when_typing hide_when_typing; + + size_t pressed_button_count; +}; + +struct sway_node; + +struct sway_node *node_at_coords( + struct sway_seat *seat, double lx, double ly, + struct wlr_surface **surface, double *sx, double *sy); + +void sway_cursor_destroy(struct sway_cursor *cursor); +struct sway_cursor *sway_cursor_create(struct sway_seat *seat); + +/** + * "Rebase" a cursor on top of whatever view is underneath it. + * + * This chooses a cursor icon and sends a motion event to the surface. + */ +void cursor_rebase(struct sway_cursor *cursor); +void cursor_rebase_all(void); +void cursor_update_image(struct sway_cursor *cursor, struct sway_node *node); + +void cursor_handle_activity_from_idle_source(struct sway_cursor *cursor, + enum sway_input_idle_source idle_source); +void cursor_handle_activity_from_device(struct sway_cursor *cursor, + struct wlr_input_device *device); +void cursor_unhide(struct sway_cursor *cursor); +int cursor_get_timeout(struct sway_cursor *cursor); +void cursor_notify_key_press(struct sway_cursor *cursor); + +void pointer_motion(struct sway_cursor *cursor, uint32_t time_msec, + struct wlr_input_device *device, double dx, double dy, + double dx_unaccel, double dy_unaccel); + +void dispatch_cursor_button(struct sway_cursor *cursor, + struct wlr_input_device *device, uint32_t time_msec, uint32_t button, + enum wl_pointer_button_state state); + +void dispatch_cursor_axis(struct sway_cursor *cursor, + struct wlr_pointer_axis_event *event); + +void cursor_set_image(struct sway_cursor *cursor, const char *image, + struct wl_client *client); + +void cursor_set_image_surface(struct sway_cursor *cursor, + struct wlr_surface *surface, int32_t hotspot_x, int32_t hotspot_y, + struct wl_client *client); + +void cursor_warp_to_container(struct sway_cursor *cursor, + struct sway_container *container, bool force); + +void cursor_warp_to_workspace(struct sway_cursor *cursor, + struct sway_workspace *workspace); + + +void sway_cursor_constrain(struct sway_cursor *cursor, + struct wlr_pointer_constraint_v1 *constraint); + +uint32_t get_mouse_bindsym(const char *name, char **error); + +uint32_t get_mouse_bindcode(const char *name, char **error); + +// Considers both bindsym and bindcode +uint32_t get_mouse_button(const char *name, char **error); + +const char *get_mouse_button_name(uint32_t button); + +void handle_request_set_cursor_shape(struct wl_listener *listener, void *data); + +#endif diff --git a/include/sway/input/input-manager.h b/include/sway/input/input-manager.h new file mode 100644 index 0000000..5113844 --- /dev/null +++ b/include/sway/input/input-manager.h @@ -0,0 +1,81 @@ +#ifndef _SWAY_INPUT_INPUT_MANAGER_H +#define _SWAY_INPUT_INPUT_MANAGER_H +#include +#include +#include +#include +#include +#include "sway/config.h" +#include "list.h" + +struct sway_server; + +struct sway_input_device { + char *identifier; + struct wlr_input_device *wlr_device; + struct wl_list link; + struct wl_listener device_destroy; + bool is_virtual; +}; + +struct sway_input_manager { + struct wl_list devices; + struct wl_list seats; + + struct wlr_keyboard_shortcuts_inhibit_manager_v1 *keyboard_shortcuts_inhibit; + struct wlr_virtual_keyboard_manager_v1 *virtual_keyboard; + struct wlr_virtual_pointer_manager_v1 *virtual_pointer; + struct wlr_pointer_gestures_v1 *pointer_gestures; + struct wlr_transient_seat_manager_v1 *transient_seat_manager; + + struct wl_listener new_input; + struct wl_listener inhibit_activate; + struct wl_listener inhibit_deactivate; + struct wl_listener keyboard_shortcuts_inhibit_new_inhibitor; + struct wl_listener virtual_keyboard_new; + struct wl_listener virtual_pointer_new; + struct wl_listener transient_seat_create; +}; + +struct sway_input_manager *input_manager_create(struct sway_server *server); + +void input_manager_finish(struct sway_input_manager *input); + +bool input_manager_has_focus(struct sway_node *node); + +void input_manager_set_focus(struct sway_node *node); + +void input_manager_configure_xcursor(void); + +void input_manager_apply_input_config(struct input_config *input_config); + +void input_manager_configure_all_input_mappings(void); + +void input_manager_reset_input(struct sway_input_device *input_device); + +void input_manager_reset_all_inputs(void); + +void input_manager_apply_seat_config(struct seat_config *seat_config); + +struct sway_seat *input_manager_get_default_seat(void); + +struct sway_seat *input_manager_get_seat(const char *seat_name, bool create); + +/** + * If none of the seat configs have a fallback setting (either true or false), + * create the default seat (if needed) and set it as the fallback + */ +void input_manager_verify_fallback_seat(void); + +/** + * Gets the last seat the user interacted with + */ +struct sway_seat *input_manager_current_seat(void); + +struct input_config *input_device_get_config(struct sway_input_device *device); + +char *input_device_get_identifier(struct wlr_input_device *device); + +const char *input_device_get_type(struct sway_input_device *device); + +#endif diff --git a/include/sway/input/keyboard.h b/include/sway/input/keyboard.h new file mode 100644 index 0000000..571d9e6 --- /dev/null +++ b/include/sway/input/keyboard.h @@ -0,0 +1,95 @@ +#ifndef _SWAY_INPUT_KEYBOARD_H +#define _SWAY_INPUT_KEYBOARD_H + +#include "sway/input/seat.h" + +#define SWAY_KEYBOARD_PRESSED_KEYS_CAP 32 + +/** + * Get modifier mask from modifier name. + * + * Returns the modifier mask or 0 if the name isn't found. + */ +uint32_t get_modifier_mask_by_name(const char *name); + +/** + * Get modifier name from modifier mask. + * + * Returns the modifier name or NULL if it isn't found. + */ +const char *get_modifier_name_by_mask(uint32_t modifier); + +/** + * Get an array of modifier names from modifier_masks + * + * Populates the names array and return the number of names added. + */ +int get_modifier_names(const char **names, uint32_t modifier_masks); + +struct sway_shortcut_state { + /** + * A list of pressed key ids (either keysyms or keycodes), + * including duplicates when different keycodes produce the same key id. + * + * Each key id is associated with the keycode (in `pressed_keycodes`) + * whose press generated it, so that the key id can be removed on + * keycode release without recalculating the transient link between + * keycode and key id at the time of the key press. + */ + uint32_t pressed_keys[SWAY_KEYBOARD_PRESSED_KEYS_CAP]; + /** + * The list of keycodes associated to currently pressed key ids, + * including duplicates when a keycode generates multiple key ids. + */ + uint32_t pressed_keycodes[SWAY_KEYBOARD_PRESSED_KEYS_CAP]; + uint32_t last_keycode; + uint32_t last_raw_modifiers; + size_t npressed; + uint32_t current_key; +}; + +struct sway_keyboard { + struct sway_seat_device *seat_device; + struct wlr_keyboard *wlr; + + struct xkb_keymap *keymap; + xkb_layout_index_t effective_layout; + + int32_t repeat_rate; + int32_t repeat_delay; + + struct wl_listener keyboard_key; + struct wl_listener keyboard_modifiers; + + struct sway_shortcut_state state_keysyms_translated; + struct sway_shortcut_state state_keysyms_raw; + struct sway_shortcut_state state_keycodes; + struct sway_shortcut_state state_pressed_sent; + struct sway_binding *held_binding; + + struct wl_event_source *key_repeat_source; + struct sway_binding *repeat_binding; +}; + +struct sway_keyboard_group { + struct wlr_keyboard_group *wlr_group; + struct sway_seat_device *seat_device; + struct wl_listener keyboard_key; + struct wl_listener keyboard_modifiers; + struct wl_listener enter; + struct wl_listener leave; + struct wl_list link; // sway_seat::keyboard_groups +}; + +struct xkb_keymap *sway_keyboard_compile_keymap(struct input_config *ic, + char **error); + +struct sway_keyboard *sway_keyboard_create(struct sway_seat *seat, + struct sway_seat_device *device); + +void sway_keyboard_configure(struct sway_keyboard *keyboard); + +void sway_keyboard_destroy(struct sway_keyboard *keyboard); + +void sway_keyboard_disarm_key_repeat(struct sway_keyboard *keyboard); +#endif diff --git a/include/sway/input/libinput.h b/include/sway/input/libinput.h new file mode 100644 index 0000000..1f84a8e --- /dev/null +++ b/include/sway/input/libinput.h @@ -0,0 +1,14 @@ +#ifndef _SWAY_INPUT_LIBINPUT_H +#define _SWAY_INPUT_LIBINPUT_H +#include "sway/input/input-manager.h" + +bool sway_input_configure_libinput_device(struct sway_input_device *device); + +void sway_input_configure_libinput_device_send_events( + struct sway_input_device *device); + +void sway_input_reset_libinput_device(struct sway_input_device *device); + +bool sway_libinput_device_is_builtin(struct sway_input_device *device); + +#endif diff --git a/include/sway/input/seat.h b/include/sway/input/seat.h new file mode 100644 index 0000000..0766e71 --- /dev/null +++ b/include/sway/input/seat.h @@ -0,0 +1,373 @@ +#ifndef _SWAY_INPUT_SEAT_H +#define _SWAY_INPUT_SEAT_H + +#include +#include +#include +#include +#include +#include +#include +#include "sway/config.h" +#include "sway/input/input-manager.h" +#include "sway/input/tablet.h" +#include "sway/input/text_input.h" + +struct sway_seat; + +struct sway_seatop_impl { + void (*button)(struct sway_seat *seat, uint32_t time_msec, + struct wlr_input_device *device, uint32_t button, + enum wl_pointer_button_state state); + void (*pointer_motion)(struct sway_seat *seat, uint32_t time_msec); + void (*pointer_axis)(struct sway_seat *seat, + struct wlr_pointer_axis_event *event); + void (*hold_begin)(struct sway_seat *seat, + struct wlr_pointer_hold_begin_event *event); + void (*hold_end)(struct sway_seat *seat, + struct wlr_pointer_hold_end_event *event); + void (*pinch_begin)(struct sway_seat *seat, + struct wlr_pointer_pinch_begin_event *event); + void (*pinch_update)(struct sway_seat *seat, + struct wlr_pointer_pinch_update_event *event); + void (*pinch_end)(struct sway_seat *seat, + struct wlr_pointer_pinch_end_event *event); + void (*swipe_begin)(struct sway_seat *seat, + struct wlr_pointer_swipe_begin_event *event); + void (*swipe_update)(struct sway_seat *seat, + struct wlr_pointer_swipe_update_event *event); + void (*swipe_end)(struct sway_seat *seat, + struct wlr_pointer_swipe_end_event *event); + void (*rebase)(struct sway_seat *seat, uint32_t time_msec); + void (*touch_motion)(struct sway_seat *seat, + struct wlr_touch_motion_event *event, double lx, double ly); + void (*touch_up)(struct sway_seat *seat, + struct wlr_touch_up_event *event); + void (*touch_down)(struct sway_seat *seat, + struct wlr_touch_down_event *event, double lx, double ly); + void (*touch_cancel)(struct sway_seat *seat, + struct wlr_touch_cancel_event *event); + void (*tablet_tool_motion)(struct sway_seat *seat, + struct sway_tablet_tool *tool, uint32_t time_msec); + void (*tablet_tool_tip)(struct sway_seat *seat, struct sway_tablet_tool *tool, + uint32_t time_msec, enum wlr_tablet_tool_tip_state state); + void (*end)(struct sway_seat *seat); + void (*unref)(struct sway_seat *seat, struct sway_container *con); + bool allow_set_cursor; +}; + +struct sway_seat_device { + struct sway_seat *sway_seat; + struct sway_input_device *input_device; + struct sway_keyboard *keyboard; + struct sway_switch *switch_device; + struct sway_tablet *tablet; + struct sway_tablet_pad *tablet_pad; + struct wl_list link; // sway_seat::devices +}; + +struct sway_seat_node { + struct sway_seat *seat; + struct sway_node *node; + + struct wl_list link; // sway_seat::focus_stack + + struct wl_listener destroy; +}; + +struct sway_drag { + struct sway_seat *seat; + struct wlr_drag *wlr_drag; + struct wl_listener destroy; +}; + +struct sway_seat { + struct wlr_seat *wlr_seat; + struct sway_cursor *cursor; + + // Seat scene tree structure + // - scene_tree + // - drag icons + // - drag icon 1 + // - drag icon 2 + // - seatop specific stuff + struct wlr_scene_tree *scene_tree; + struct wlr_scene_tree *drag_icons; + + bool has_focus; + struct wl_list focus_stack; // list of containers in focus order + struct sway_workspace *workspace; + char *prev_workspace_name; // for workspace back_and_forth + + struct wlr_layer_surface_v1 *focused_layer; + // If the exclusive layer is set, views cannot receive keyboard focus + bool has_exclusive_layer; + + // Last touch point + int32_t touch_id; + double touch_x, touch_y; + + // Seat operations (drag and resize) + const struct sway_seatop_impl *seatop_impl; + void *seatop_data; + + uint32_t last_button_serial; + + uint32_t idle_inhibit_sources, idle_wake_sources; + + list_t *deferred_bindings; // struct sway_binding + + struct sway_input_method_relay im_relay; + + struct wl_listener focus_destroy; + struct wl_listener new_node; + struct wl_listener request_start_drag; + struct wl_listener start_drag; + struct wl_listener request_set_selection; + struct wl_listener request_set_primary_selection; + struct wl_listener destroy; + + struct wl_list devices; // sway_seat_device::link + struct wl_list keyboard_groups; // sway_keyboard_group::link + struct wl_list keyboard_shortcuts_inhibitors; + // sway_keyboard_shortcuts_inhibitor::link + + struct wl_list link; // input_manager::seats +}; + +struct sway_pointer_constraint { + struct sway_cursor *cursor; + struct wlr_pointer_constraint_v1 *constraint; + + struct wl_listener set_region; + struct wl_listener destroy; +}; + +struct sway_keyboard_shortcuts_inhibitor { + struct wlr_keyboard_shortcuts_inhibitor_v1 *inhibitor; + + struct wl_listener destroy; + + struct wl_list link; // sway_seat::keyboard_shortcuts_inhibitors +}; + +struct sway_seat *seat_create(const char *seat_name); + +void seat_destroy(struct sway_seat *seat); + +void seat_add_device(struct sway_seat *seat, + struct sway_input_device *device); + +void seat_configure_device(struct sway_seat *seat, + struct sway_input_device *device); + +void seat_configure_device_mapping(struct sway_seat *seat, + struct sway_input_device *input_device); + +void seat_reset_device(struct sway_seat *seat, + struct sway_input_device *input_device); + +void seat_remove_device(struct sway_seat *seat, + struct sway_input_device *device); + +void seat_configure_xcursor(struct sway_seat *seat); + +void seat_set_focus(struct sway_seat *seat, struct sway_node *node); + +void seat_set_focus_container(struct sway_seat *seat, + struct sway_container *con); + +void seat_set_focus_workspace(struct sway_seat *seat, + struct sway_workspace *ws); + +/** + * Manipulate the focus stack without triggering any other behaviour. + * + * This can be used to set focus_inactive by calling the function a second time + * with the real focus. + */ +void seat_set_raw_focus(struct sway_seat *seat, struct sway_node *node); + +void seat_set_focus_surface(struct sway_seat *seat, + struct wlr_surface *surface, bool unfocus); + +void seat_set_focus_layer(struct sway_seat *seat, + struct wlr_layer_surface_v1 *layer); + +void seat_unfocus_unless_client(struct sway_seat *seat, struct wl_client *client); + +struct sway_node *seat_get_focus(struct sway_seat *seat); + +struct sway_workspace *seat_get_focused_workspace(struct sway_seat *seat); + +// If a scratchpad container is fullscreen global, this can be used to try to +// determine the last focused workspace. Otherwise, this should yield the same +// results as seat_get_focused_workspace. +struct sway_workspace *seat_get_last_known_workspace(struct sway_seat *seat); + +struct sway_container *seat_get_focused_container(struct sway_seat *seat); + +/** + * Return the last container to be focused for the seat (or the most recently + * opened if no container has received focused) that is a child of the given + * container. The focus-inactive container of the root window is the focused + * container for the seat (if the seat does have focus). This function can be + * used to determine what container gets focused next if the focused container + * is destroyed, or focus moves to a container with children and we need to + * descend into the next leaf in focus order. + */ +struct sway_node *seat_get_focus_inactive(struct sway_seat *seat, + struct sway_node *node); + +struct sway_container *seat_get_focus_inactive_tiling(struct sway_seat *seat, + struct sway_workspace *workspace); + +/** + * Descend into the focus stack to find the focus-inactive view. Useful for + * container placement when they change position in the tree. + */ +struct sway_container *seat_get_focus_inactive_view(struct sway_seat *seat, + struct sway_node *ancestor); + +/** + * Return the immediate child of container which was most recently focused. + */ +struct sway_node *seat_get_active_tiling_child(struct sway_seat *seat, + struct sway_node *parent); + +/** + * Iterate over the focus-inactive children of the container calling the + * function on each. + */ +void seat_for_each_node(struct sway_seat *seat, + void (*f)(struct sway_node *node, void *data), void *data); + +void seat_apply_config(struct sway_seat *seat, struct seat_config *seat_config); + +struct seat_config *seat_get_config(struct sway_seat *seat); + +struct seat_config *seat_get_config_by_name(const char *name); + +void seat_idle_notify_activity(struct sway_seat *seat, + enum sway_input_idle_source source); + +bool seat_is_input_allowed(struct sway_seat *seat, struct wlr_surface *surface); + +void drag_icons_update_position(struct sway_seat *seat); + +enum wlr_edges find_resize_edge(struct sway_container *cont, + struct wlr_surface *surface, struct sway_cursor *cursor); + +void seatop_begin_default(struct sway_seat *seat); + +void seatop_begin_down(struct sway_seat *seat, struct sway_container *con, + double sx, double sy); + +void seatop_begin_down_on_surface(struct sway_seat *seat, + struct wlr_surface *surface, double sx, double sy); + +void seatop_begin_touch_down(struct sway_seat *seat, struct wlr_surface *surface, + struct wlr_touch_down_event *event, double sx, double sy, double lx, double ly); + +void seatop_begin_move_floating(struct sway_seat *seat, + struct sway_container *con); + +void seatop_begin_move_tiling_threshold(struct sway_seat *seat, + struct sway_container *con); + +void seatop_begin_move_tiling(struct sway_seat *seat, + struct sway_container *con); + +void seatop_begin_resize_floating(struct sway_seat *seat, + struct sway_container *con, enum wlr_edges edge); + +void seatop_begin_resize_tiling(struct sway_seat *seat, + struct sway_container *con, enum wlr_edges edge); + +struct sway_container *seat_get_focus_inactive_floating(struct sway_seat *seat, + struct sway_workspace *workspace); + +void seat_pointer_notify_button(struct sway_seat *seat, uint32_t time_msec, + uint32_t button, enum wl_pointer_button_state state); + +void seat_consider_warp_to_focus(struct sway_seat *seat); + +void seatop_button(struct sway_seat *seat, uint32_t time_msec, + struct wlr_input_device *device, uint32_t button, + enum wl_pointer_button_state state); + +void seatop_pointer_motion(struct sway_seat *seat, uint32_t time_msec); + +void seatop_pointer_axis(struct sway_seat *seat, + struct wlr_pointer_axis_event *event); + +void seatop_tablet_tool_tip(struct sway_seat *seat, + struct sway_tablet_tool *tool, uint32_t time_msec, + enum wlr_tablet_tool_tip_state state); + +void seatop_tablet_tool_motion(struct sway_seat *seat, + struct sway_tablet_tool *tool, uint32_t time_msec); + +void seatop_hold_begin(struct sway_seat *seat, + struct wlr_pointer_hold_begin_event *event); +void seatop_hold_end(struct sway_seat *seat, + struct wlr_pointer_hold_end_event *event); + +void seatop_pinch_begin(struct sway_seat *seat, + struct wlr_pointer_pinch_begin_event *event); +void seatop_pinch_update(struct sway_seat *seat, + struct wlr_pointer_pinch_update_event *event); +void seatop_pinch_end(struct sway_seat *seat, + struct wlr_pointer_pinch_end_event *event); + +void seatop_swipe_begin(struct sway_seat *seat, + struct wlr_pointer_swipe_begin_event *event); +void seatop_swipe_update(struct sway_seat *seat, + struct wlr_pointer_swipe_update_event *event); +void seatop_swipe_end(struct sway_seat *seat, + struct wlr_pointer_swipe_end_event *event); + +void seatop_touch_motion(struct sway_seat *seat, + struct wlr_touch_motion_event *event, double lx, double ly); + +void seatop_touch_up(struct sway_seat *seat, + struct wlr_touch_up_event *event); + +void seatop_touch_down(struct sway_seat *seat, + struct wlr_touch_down_event *event, double lx, double ly); + +void seatop_touch_cancel(struct sway_seat *seat, + struct wlr_touch_cancel_event *event); + +void seatop_rebase(struct sway_seat *seat, uint32_t time_msec); + +/** + * End a seatop (ie. free any seatop specific resources). + */ +void seatop_end(struct sway_seat *seat); + +/** + * Instructs the seatop implementation to drop any references to the given + * container (eg. because the container is destroying). + * The seatop may choose to abort itself in response to this. + */ +void seatop_unref(struct sway_seat *seat, struct sway_container *con); + +bool seatop_allows_set_cursor(struct sway_seat *seat); + +/** + * Returns the keyboard shortcuts inhibitor that applies to the given surface + * or NULL if none exists. + */ +struct sway_keyboard_shortcuts_inhibitor * +keyboard_shortcuts_inhibitor_get_for_surface(const struct sway_seat *seat, + const struct wlr_surface *surface); + +/** + * Returns the keyboard shortcuts inhibitor that applies to the currently + * focused surface of a seat or NULL if none exists. + */ +struct sway_keyboard_shortcuts_inhibitor * +keyboard_shortcuts_inhibitor_get_for_focused_surface(const struct sway_seat *seat); + +#endif diff --git a/include/sway/input/switch.h b/include/sway/input/switch.h new file mode 100644 index 0000000..de6787b --- /dev/null +++ b/include/sway/input/switch.h @@ -0,0 +1,24 @@ +#ifndef _SWAY_INPUT_SWITCH_H +#define _SWAY_INPUT_SWITCH_H + +#include "sway/input/seat.h" + +struct sway_switch { + struct sway_seat_device *seat_device; + struct wlr_switch *wlr; + enum wlr_switch_state state; + enum wlr_switch_type type; + + struct wl_listener switch_toggle; +}; + +struct sway_switch *sway_switch_create(struct sway_seat *seat, + struct sway_seat_device *device); + +void sway_switch_configure(struct sway_switch *sway_switch); + +void sway_switch_destroy(struct sway_switch *sway_switch); + +void sway_switch_retrigger_bindings_for_all(void); + +#endif diff --git a/include/sway/input/tablet.h b/include/sway/input/tablet.h new file mode 100644 index 0000000..2fa5db6 --- /dev/null +++ b/include/sway/input/tablet.h @@ -0,0 +1,69 @@ +#ifndef _SWAY_INPUT_TABLET_H +#define _SWAY_INPUT_TABLET_H +#include + +struct sway_seat; +struct wlr_tablet_tool; + +struct sway_tablet { + struct wl_list link; + struct sway_seat_device *seat_device; + struct wlr_tablet_v2_tablet *tablet_v2; +}; + +enum sway_tablet_tool_mode { + SWAY_TABLET_TOOL_MODE_ABSOLUTE, + SWAY_TABLET_TOOL_MODE_RELATIVE, +}; + +struct sway_tablet_tool { + struct sway_seat *seat; + struct sway_tablet *tablet; + struct wlr_tablet_v2_tablet_tool *tablet_v2_tool; + + enum sway_tablet_tool_mode mode; + double tilt_x, tilt_y; + + struct wl_listener set_cursor; + struct wl_listener tool_destroy; +}; + +struct sway_tablet_pad { + struct wl_list link; + struct sway_seat_device *seat_device; + struct sway_tablet *tablet; + struct wlr_tablet_pad *wlr; + struct wlr_tablet_v2_tablet_pad *tablet_v2_pad; + + struct wl_listener attach; + struct wl_listener button; + struct wl_listener ring; + struct wl_listener strip; + + struct wlr_surface *current_surface; + struct wl_listener surface_destroy; + + struct wl_listener tablet_destroy; +}; + +struct sway_tablet *sway_tablet_create(struct sway_seat *seat, + struct sway_seat_device *device); + +void sway_configure_tablet(struct sway_tablet *tablet); + +void sway_tablet_destroy(struct sway_tablet *tablet); + +void sway_tablet_tool_configure(struct sway_tablet *tablet, + struct wlr_tablet_tool *wlr_tool); + +struct sway_tablet_pad *sway_tablet_pad_create(struct sway_seat *seat, + struct sway_seat_device *device); + +void sway_configure_tablet_pad(struct sway_tablet_pad *tablet_pad); + +void sway_tablet_pad_destroy(struct sway_tablet_pad *tablet_pad); + +void sway_tablet_pad_set_focus(struct sway_tablet_pad *tablet_pad, + struct wlr_surface *surface); + +#endif diff --git a/include/sway/input/text_input.h b/include/sway/input/text_input.h new file mode 100644 index 0000000..1818749 --- /dev/null +++ b/include/sway/input/text_input.h @@ -0,0 +1,73 @@ +#ifndef _SWAY_INPUT_TEXT_INPUT_H +#define _SWAY_INPUT_TEXT_INPUT_H + +#include +#include +#include + +/** + * The relay structure manages the relationship between text-input and + * input_method interfaces on a given seat. Multiple text-input interfaces may + * be bound to a relay, but at most one will be focused (receiving events) at + * a time. At most one input-method interface may be bound to the seat. The + * relay manages life cycle of both sides. When both sides are present and + * focused, the relay passes messages between them. + * + * Text input focus is a subset of keyboard focus - if the text-input is + * in the focused state, wl_keyboard sent an enter as well. However, having + * wl_keyboard focused doesn't mean that text-input will be focused. + */ +struct sway_input_method_relay { + struct sway_seat *seat; + + struct wl_list text_inputs; // sway_text_input::link + struct wl_list input_popups; // sway_input_popup::link + struct wlr_input_method_v2 *input_method; // doesn't have to be present + + struct wl_listener text_input_new; + struct wl_listener text_input_manager_destroy; + + struct wl_listener input_method_new; + struct wl_listener input_method_manager_destroy; + struct wl_listener input_method_commit; + struct wl_listener input_method_new_popup_surface; + struct wl_listener input_method_grab_keyboard; + struct wl_listener input_method_destroy; + + struct wl_listener input_method_keyboard_grab_destroy; +}; + + +struct sway_text_input { + struct sway_input_method_relay *relay; + + struct wlr_text_input_v3 *input; + // The surface getting seat's focus. Stored for when text-input cannot + // be sent an enter event immediately after getting focus, e.g. when + // there's no input method available. Cleared once text-input is entered. + struct wlr_surface *pending_focused_surface; + + struct wl_list link; + + struct wl_listener pending_focused_surface_destroy; + + struct wl_listener text_input_enable; + struct wl_listener text_input_commit; + struct wl_listener text_input_disable; + struct wl_listener text_input_destroy; +}; + +void sway_input_method_relay_init(struct sway_seat *seat, + struct sway_input_method_relay *relay); + +void sway_input_method_relay_finish(struct sway_input_method_relay *relay); + +// Updates currently focused surface. Surface must belong to the same seat. +void sway_input_method_relay_set_focus(struct sway_input_method_relay *relay, + struct wlr_surface *surface); + +struct sway_text_input *sway_text_input_create( + struct sway_input_method_relay *relay, + struct wlr_text_input_v3 *text_input); + +#endif diff --git a/include/sway/input/text_input_popup.h b/include/sway/input/text_input_popup.h new file mode 100644 index 0000000..7e838ed --- /dev/null +++ b/include/sway/input/text_input_popup.h @@ -0,0 +1,23 @@ +#ifndef _SWAY_INPUT_TEXT_INPUT_POPUP_H +#define _SWAY_INPUT_TEXT_INPUT_POPUP_H + +#include "sway/tree/view.h" + +struct sway_input_popup { + struct sway_input_method_relay *relay; + + struct wlr_scene_tree *scene_tree; + struct sway_popup_desc desc; + struct wlr_input_popup_surface_v2 *popup_surface; + struct wlr_output *fixed_output; + + struct wl_list link; + + struct wl_listener popup_destroy; + struct wl_listener popup_surface_commit; + struct wl_listener popup_surface_map; + struct wl_listener popup_surface_unmap; + + struct wl_listener focused_surface_unmap; +}; +#endif diff --git a/include/sway/ipc-json.h b/include/sway/ipc-json.h new file mode 100644 index 0000000..bc9f498 --- /dev/null +++ b/include/sway/ipc-json.h @@ -0,0 +1,20 @@ +#ifndef _SWAY_IPC_JSON_H +#define _SWAY_IPC_JSON_H +#include +#include "sway/output.h" +#include "sway/tree/container.h" +#include "sway/input/input-manager.h" + +json_object *ipc_json_get_version(void); + +json_object *ipc_json_get_binding_mode(void); + +json_object *ipc_json_describe_disabled_output(struct sway_output *o); +json_object *ipc_json_describe_non_desktop_output(struct sway_output_non_desktop *o); +json_object *ipc_json_describe_node(struct sway_node *node); +json_object *ipc_json_describe_node_recursive(struct sway_node *node); +json_object *ipc_json_describe_input(struct sway_input_device *device); +json_object *ipc_json_describe_seat(struct sway_seat *seat); +json_object *ipc_json_describe_bar_config(struct bar_config *bar); + +#endif diff --git a/include/sway/ipc-server.h b/include/sway/ipc-server.h new file mode 100644 index 0000000..d4c0094 --- /dev/null +++ b/include/sway/ipc-server.h @@ -0,0 +1,26 @@ +#ifndef _SWAY_IPC_SERVER_H +#define _SWAY_IPC_SERVER_H +#include +#include "sway/config.h" +#include "sway/input/input-manager.h" +#include "sway/tree/container.h" +#include "ipc.h" + +struct sway_server; + +void ipc_init(struct sway_server *server); + +struct sockaddr_un *ipc_user_sockaddr(void); + +void ipc_event_workspace(struct sway_workspace *old, + struct sway_workspace *new, const char *change); +void ipc_event_window(struct sway_container *window, const char *change); +void ipc_event_barconfig_update(struct bar_config *bar); +void ipc_event_bar_state_update(struct bar_config *bar); +void ipc_event_mode(const char *mode, bool pango); +void ipc_event_shutdown(const char *reason); +void ipc_event_binding(struct sway_binding *binding); +void ipc_event_input(const char *change, struct sway_input_device *device); +void ipc_event_output(void); + +#endif diff --git a/include/sway/layer_criteria.h b/include/sway/layer_criteria.h new file mode 100644 index 0000000..554a482 --- /dev/null +++ b/include/sway/layer_criteria.h @@ -0,0 +1,19 @@ +#include + +struct layer_criteria { + char *namespace; + char *cmdlist; + + bool shadow_enabled; + bool blur_enabled; + bool blur_xray; + bool blur_ignore_transparent; + int corner_radius; +}; + +void layer_criteria_destroy(struct layer_criteria *criteria); + +struct layer_criteria *layer_criteria_add(char *namespace, char *cmdlist); + +// Get the matching criteria for a specified `sway_layer_surface` +struct layer_criteria *layer_criteria_for_namespace(char *namespace); diff --git a/include/sway/layers.h b/include/sway/layers.h new file mode 100644 index 0000000..b845749 --- /dev/null +++ b/include/sway/layers.h @@ -0,0 +1,61 @@ +#ifndef _SWAY_LAYERS_H +#define _SWAY_LAYERS_H +#include +#include +#include +#include "sway/layer_criteria.h" +#include "sway/tree/view.h" + +struct sway_layer_surface { + struct wl_listener map; + struct wl_listener unmap; + struct wl_listener surface_commit; + struct wl_listener node_destroy; + struct wl_listener new_popup; + + bool mapped; + + struct wlr_scene_tree *popups; + struct sway_popup_desc desc; + + struct sway_output *output; + + struct wlr_scene_blur *blur_node; + struct wlr_scene_shadow *shadow_node; + + bool shadow_enabled; + bool blur_enabled; + bool blur_xray; + bool blur_ignore_transparent; + int corner_radius; + + struct wl_list link; // sway_output.layer_surfaces + + struct wlr_scene_layer_surface_v1 *scene; + struct wlr_scene_tree *tree; + struct wlr_layer_surface_v1 *layer_surface; +}; + +struct sway_layer_popup { + struct wlr_xdg_popup *wlr_popup; + struct wlr_scene_tree *scene; + struct sway_layer_surface *toplevel; + + struct wl_listener destroy; + struct wl_listener new_popup; + struct wl_listener commit; + struct wl_listener reposition; +}; + +struct sway_output; + +struct wlr_layer_surface_v1 *toplevel_layer_surface_from_surface( + struct wlr_surface *surface); + +void arrange_layers(struct sway_output *output); + +void layer_apply_criteria(struct sway_layer_surface *surface, struct layer_criteria *criteria); + +void destroy_layers(struct sway_output *output); + +#endif diff --git a/include/sway/lock.h b/include/sway/lock.h new file mode 100644 index 0000000..5be0f96 --- /dev/null +++ b/include/sway/lock.h @@ -0,0 +1,6 @@ +#ifndef _SWAY_LOCK_H +#define _SWAY_LOCK_H + +void arrange_locks(void); + +#endif diff --git a/include/sway/output.h b/include/sway/output.h new file mode 100644 index 0000000..3d1d1ad --- /dev/null +++ b/include/sway/output.h @@ -0,0 +1,159 @@ +#ifndef _SWAY_OUTPUT_H +#define _SWAY_OUTPUT_H +#include +#include +#include +#include +#include +#include +#include +#include "config.h" +#include "sway/tree/node.h" +#include "sway/tree/view.h" + +struct sway_server; +struct sway_container; + +struct sway_output_state { + list_t *workspaces; + struct sway_workspace *active_workspace; +}; + +struct sway_output { + struct sway_node node; + + struct { + struct wlr_scene_tree *shell_background; + struct wlr_scene_tree *shell_bottom; + // Used for optimized blur. Everything exclusively below gets blurred + struct wlr_scene_optimized_blur *blur_layer; + struct wlr_scene_tree *tiling; + struct wlr_scene_tree *fullscreen; + struct wlr_scene_tree *shell_top; + struct wlr_scene_tree *shell_overlay; + struct wlr_scene_tree *session_lock; + } layers; + + // when a container is fullscreen, in case the fullscreen surface is + // translucent (can see behind) we must make sure that the background is a + // solid color in order to conform to the wayland protocol. This rect + // ensures that when looking through a surface, all that will be seen + // is black. + struct wlr_scene_rect *fullscreen_background; + + struct wlr_output *wlr_output; + struct wlr_scene_output *scene_output; + struct sway_server *server; + struct wl_list link; + + struct wlr_box usable_area; + + int lx, ly; // layout coords + int width, height; // transformed buffer size + enum wl_output_subpixel detected_subpixel; + enum scale_filter_mode scale_filter; + + bool enabled; + list_t *workspaces; + struct wl_list layer_surfaces; // sway_layer_surface.link + + struct sway_output_state current; + + struct sway_workspace *prev_active_workspace; + + struct wl_listener layout_destroy; + struct wl_listener destroy; + struct wl_listener present; + struct wl_listener frame; + struct wl_listener request_state; + + struct wlr_color_transform *color_transform; + struct wlr_ext_workspace_group_handle_v1 *ext_workspace_group; + + struct timespec last_presentation; + uint32_t refresh_nsec; + int max_render_time; // In milliseconds + struct wl_event_source *repaint_timer; + + bool allow_tearing; + bool hdr; +}; + +struct sway_output_non_desktop { + struct wlr_output *wlr_output; + + struct wl_listener destroy; +}; + +struct sway_output *output_create(struct wlr_output *wlr_output); + +void output_destroy(struct sway_output *output); + +void output_begin_destroy(struct sway_output *output); + +struct sway_output *output_from_wlr_output(struct wlr_output *output); + +struct sway_output *output_get_in_direction(struct sway_output *reference, + enum wlr_direction direction); + +void output_configure_scene(struct sway_output *output, + struct wlr_scene_node *node, bool has_titlebar, + struct sway_container *closest_con); + +void output_add_workspace(struct sway_output *output, + struct sway_workspace *workspace); + +typedef void (*sway_surface_iterator_func_t)(struct sway_output *output, + struct sway_view *view, struct wlr_surface *surface, struct wlr_box *box, + void *user_data); + +bool output_match_name_or_id(struct sway_output *output, + const char *name_or_id); + +// this ONLY includes the enabled outputs +struct sway_output *output_by_name_or_id(const char *name_or_id); + +// this includes all the outputs, including disabled ones +struct sway_output *all_output_by_name_or_id(const char *name_or_id); + +void output_sort_workspaces(struct sway_output *output); + +void output_enable(struct sway_output *output); + +void output_disable(struct sway_output *output); + +struct sway_workspace *output_get_active_workspace(struct sway_output *output); + +void output_for_each_workspace(struct sway_output *output, + void (*f)(struct sway_workspace *ws, void *data), void *data); + +void output_for_each_container(struct sway_output *output, + void (*f)(struct sway_container *con, void *data), void *data); + +struct sway_workspace *output_find_workspace(struct sway_output *output, + bool (*test)(struct sway_workspace *ws, void *data), void *data); + +struct sway_container *output_find_container(struct sway_output *output, + bool (*test)(struct sway_container *con, void *data), void *data); + +void output_get_box(struct sway_output *output, struct wlr_box *box); + +bool output_supports_hdr(struct wlr_output *output, const char **unsupported_reason_ptr); + +enum sway_container_layout output_get_default_layout( + struct sway_output *output); + +enum wlr_direction opposite_direction(enum wlr_direction d); + +void handle_output_manager_apply(struct wl_listener *listener, void *data); + +void handle_output_manager_test(struct wl_listener *listener, void *data); + +void handle_output_power_manager_set_mode(struct wl_listener *listener, + void *data); + +struct sway_output_non_desktop *output_non_desktop_create(struct wlr_output *wlr_output); + +void update_output_manager_config(struct sway_server *server); + +#endif diff --git a/include/sway/scene_descriptor.h b/include/sway/scene_descriptor.h new file mode 100644 index 0000000..4f6de11 --- /dev/null +++ b/include/sway/scene_descriptor.h @@ -0,0 +1,34 @@ +/** + * Across a wayland compositor, there are multiple shells: It can be + * a toplevel, or a layer_shell, or even something more meta like a drag + * icon or highlight indicators when dragging windows around. + * + * This object lets us store values that represent these modes of operation + * and keep track of what object is being represented. + */ +#ifndef _SWAY_SCENE_DESCRIPTOR_H +#define _SWAY_SCENE_DESCRIPTOR_H +#include + +enum sway_scene_descriptor_type { + SWAY_SCENE_DESC_BUFFER_TIMER, + SWAY_SCENE_DESC_NON_INTERACTIVE, + SWAY_SCENE_DESC_CONTAINER, + SWAY_SCENE_DESC_VIEW, + SWAY_SCENE_DESC_LAYER_SHELL, + SWAY_SCENE_DESC_XWAYLAND_UNMANAGED, + SWAY_SCENE_DESC_POPUP, + SWAY_SCENE_DESC_DRAG_ICON, + SWAY_SCENE_DESC_SAVED_BUFFER, +}; + +bool scene_descriptor_assign(struct wlr_scene_node *node, + enum sway_scene_descriptor_type type, void *data); + +void *scene_descriptor_try_get(struct wlr_scene_node *node, + enum sway_scene_descriptor_type type); + +void scene_descriptor_destroy(struct wlr_scene_node *node, + enum sway_scene_descriptor_type type); + +#endif diff --git a/include/sway/server.h b/include/sway/server.h new file mode 100644 index 0000000..8c81148 --- /dev/null +++ b/include/sway/server.h @@ -0,0 +1,200 @@ +#ifndef _SWAY_SERVER_H +#define _SWAY_SERVER_H +#include +#include +#include "config.h" +#include "list.h" +#include "sway/desktop/idle_inhibit_v1.h" +#if WLR_HAS_XWAYLAND +#include "sway/xwayland.h" +#endif + +struct sway_transaction; + +struct sway_session_lock { + struct wlr_session_lock_v1 *lock; + struct wlr_surface *focused; + bool abandoned; + + struct wl_list outputs; // struct sway_session_lock_output + + // invalid if the session is abandoned + struct wl_listener new_surface; + struct wl_listener unlock; + struct wl_listener destroy; +}; + +struct sway_server { + struct wl_display *wl_display; + struct wl_event_loop *wl_event_loop; + char *socket; + + struct wlr_backend *backend; + struct wlr_session *session; + // secondary headless backend used for creating virtual outputs on-the-fly + struct wlr_backend *headless_backend; + struct wlr_renderer *renderer; + struct wlr_allocator *allocator; + + struct wlr_compositor *compositor; + + struct wlr_linux_dmabuf_v1 *linux_dmabuf_v1; + + struct wlr_data_device_manager *data_device_manager; + + struct sway_input_manager *input; + + struct wl_listener new_output; + struct wl_listener renderer_lost; + struct wl_event_source *recreating_renderer; + + struct wlr_idle_notifier_v1 *idle_notifier_v1; + struct sway_idle_inhibit_manager_v1 idle_inhibit_manager_v1; + + struct wlr_layer_shell_v1 *layer_shell; + struct wl_listener layer_shell_surface; + + struct wlr_xdg_shell *xdg_shell; + struct wl_listener xdg_shell_toplevel; + + struct wlr_tablet_manager_v2 *tablet_v2; + +#if WLR_HAS_XWAYLAND + struct sway_xwayland xwayland; + struct wl_listener xwayland_surface; + struct wl_listener xwayland_ready; +#endif + + struct wlr_relative_pointer_manager_v1 *relative_pointer_manager; + + struct wlr_server_decoration_manager *server_decoration_manager; + struct wl_listener server_decoration; + struct wl_list decorations; // sway_server_decoration::link + + struct wlr_xdg_decoration_manager_v1 *xdg_decoration_manager; + struct wl_listener xdg_decoration; + struct wl_list xdg_decorations; // sway_xdg_decoration::link + + struct wlr_drm_lease_v1_manager *drm_lease_manager; + struct wl_listener drm_lease_request; + + struct wlr_pointer_constraints_v1 *pointer_constraints; + struct wl_listener pointer_constraint; + + struct wlr_xdg_output_manager_v1 *xdg_output_manager_v1; + + struct wlr_output_manager_v1 *output_manager_v1; + struct wl_listener output_manager_apply; + struct wl_listener output_manager_test; + + struct wlr_gamma_control_manager_v1 *gamma_control_manager_v1; + struct wl_listener gamma_control_set_gamma; + + struct { + struct sway_session_lock *lock; + struct wlr_session_lock_manager_v1 *manager; + + struct wl_listener new_lock; + struct wl_listener manager_destroy; + } session_lock; + + struct wlr_output_power_manager_v1 *output_power_manager_v1; + struct wl_listener output_power_manager_set_mode; + struct wlr_input_method_manager_v2 *input_method; + struct wlr_text_input_manager_v3 *text_input; + struct wlr_ext_foreign_toplevel_list_v1 *foreign_toplevel_list; + struct wlr_foreign_toplevel_manager_v1 *foreign_toplevel_manager; + struct wlr_content_type_manager_v1 *content_type_manager_v1; + struct wlr_data_control_manager_v1 *wlr_data_control_manager_v1; + struct wlr_ext_data_control_manager_v1 *ext_data_control_manager_v1; + struct wlr_screencopy_manager_v1 *screencopy_manager_v1; + struct wlr_ext_image_copy_capture_manager_v1 *ext_image_copy_capture_manager_v1; + struct wlr_export_dmabuf_manager_v1 *export_dmabuf_manager_v1; + struct wlr_security_context_manager_v1 *security_context_manager_v1; + + struct wlr_ext_foreign_toplevel_image_capture_source_manager_v1 *ext_foreign_toplevel_image_capture_source_manager_v1; + struct wl_listener new_foreign_toplevel_capture_request; + + struct wlr_xdg_activation_v1 *xdg_activation_v1; + struct wl_listener xdg_activation_v1_request_activate; + struct wl_listener xdg_activation_v1_new_token; + + struct wl_listener xdg_toplevel_tag_manager_v1_set_tag; + + struct wl_listener request_set_cursor_shape; + + struct wlr_tearing_control_manager_v1 *tearing_control_v1; + struct wl_listener tearing_control_new_object; + struct wl_list tearing_controllers; // sway_tearing_controller::link + + struct wlr_ext_workspace_manager_v1 *workspace_manager_v1; + struct wl_listener workspace_manager_v1_commit; + + struct wl_list pending_launcher_ctxs; // launcher_ctx::link + + // The timeout for transactions, after which a transaction is applied + // regardless of readiness. + size_t txn_timeout_ms; + + // Stores a transaction after it has been committed, but is waiting for + // views to ack the new dimensions before being applied. A queued + // transaction is frozen and must not have new instructions added to it. + struct sway_transaction *queued_transaction; + + // Stores a pending transaction that will be committed once the existing + // queued transaction is applied and freed. The pending transaction can be + // updated with new instructions as needed. + struct sway_transaction *pending_transaction; + + // Stores the nodes that have been marked as "dirty" and will be put into + // the pending transaction. + list_t *dirty_nodes; + + struct wl_event_source *delayed_modeset; +}; + +extern struct sway_server server; + +struct sway_debug { + bool noatomic; // Ignore atomic layout updates + bool txn_timings; // Log verbose messages about transactions + bool txn_wait; // Always wait for the timeout before applying +}; + +extern struct sway_debug debug; + +extern bool unsupported_gpu_detected; + +void sway_terminate(int exit_code); + +bool server_init(struct sway_server *server); +void server_fini(struct sway_server *server); +bool server_start(struct sway_server *server); +void server_run(struct sway_server *server); + +void handle_new_output(struct wl_listener *listener, void *data); + +void handle_idle_inhibitor_v1(struct wl_listener *listener, void *data); +void handle_layer_shell_surface(struct wl_listener *listener, void *data); +bool sway_session_lock_init(void); +void sway_session_lock_add_output(struct sway_session_lock *lock, + struct sway_output *output); +bool sway_session_lock_has_surface(struct sway_session_lock *lock, + struct wlr_surface *surface); +void handle_xdg_shell_toplevel(struct wl_listener *listener, void *data); +#if WLR_HAS_XWAYLAND +void handle_xwayland_surface(struct wl_listener *listener, void *data); +#endif +void handle_server_decoration(struct wl_listener *listener, void *data); +void handle_xdg_decoration(struct wl_listener *listener, void *data); +void handle_pointer_constraint(struct wl_listener *listener, void *data); +void xdg_activation_v1_handle_request_activate(struct wl_listener *listener, + void *data); +void xdg_activation_v1_handle_new_token(struct wl_listener *listener, + void *data); + +void set_rr_scheduling(void); + +void handle_new_tearing_hint(struct wl_listener *listener, void *data); + +#endif diff --git a/include/sway/sway_text_node.h b/include/sway/sway_text_node.h new file mode 100644 index 0000000..621cee1 --- /dev/null +++ b/include/sway/sway_text_node.h @@ -0,0 +1,28 @@ +#ifndef _SWAY_BUFFER_H +#define _SWAY_BUFFER_H +#include + +struct sway_text_node { + int width; + int max_width; + int height; + int baseline; + bool pango_markup; + float color[4]; + float background[4]; + + struct wlr_scene_node *node; +}; + +struct sway_text_node *sway_text_node_create(struct wlr_scene_tree *parent, + char *text, float color[4], bool pango_markup); + +void sway_text_node_set_color(struct sway_text_node *node, float color[4]); + +void sway_text_node_set_text(struct sway_text_node *node, char *text); + +void sway_text_node_set_max_width(struct sway_text_node *node, int max_width); + +void sway_text_node_set_background(struct sway_text_node *node, float background[4]); + +#endif diff --git a/include/sway/swaynag.h b/include/sway/swaynag.h new file mode 100644 index 0000000..03bd52c --- /dev/null +++ b/include/sway/swaynag.h @@ -0,0 +1,30 @@ +#ifndef _SWAY_SWAYNAG_H +#define _SWAY_SWAYNAG_H +#include +#include "stringop.h" + +struct swaynag_instance { + struct wl_client *client; + struct wl_listener client_destroy; + + const char *args; + int fd[2]; + bool detailed; +}; + +// Spawn swaynag. If swaynag->detailed, then swaynag->fd[1] will left open +// so it can be written to. Call swaynag_show when done writing. This will +// be automatically called by swaynag_log if the instance is not spawned and +// swaynag->detailed is true. +bool swaynag_spawn(const char *swaynag_command, + struct swaynag_instance *swaynag); + +// Write a log message to swaynag->fd[1]. This will fail when swaynag->detailed +// is false. +void swaynag_log(const char *swaynag_command, struct swaynag_instance *swaynag, + const char *fmt, ...) _SWAY_ATTRIB_PRINTF(3, 4); + +// If swaynag->detailed, close swaynag->fd[1] so swaynag displays +void swaynag_show(struct swaynag_instance *swaynag); + +#endif diff --git a/include/sway/tree/arrange.h b/include/sway/tree/arrange.h new file mode 100644 index 0000000..06a2279 --- /dev/null +++ b/include/sway/tree/arrange.h @@ -0,0 +1,19 @@ +#ifndef _SWAY_ARRANGE_H +#define _SWAY_ARRANGE_H + +struct sway_output; +struct sway_workspace; +struct sway_container; +struct sway_node; + +void arrange_container(struct sway_container *container); + +void arrange_workspace(struct sway_workspace *workspace); + +void arrange_output(struct sway_output *output); + +void arrange_root(void); + +void arrange_node(struct sway_node *node); + +#endif diff --git a/include/sway/tree/container.h b/include/sway/tree/container.h new file mode 100644 index 0000000..5a36df8 --- /dev/null +++ b/include/sway/tree/container.h @@ -0,0 +1,404 @@ +#ifndef _SWAY_CONTAINER_H +#define _SWAY_CONTAINER_H +#include +#include +#include +#include +#include +#include "list.h" +#include "sway/animation_manager.h" +#include "sway/tree/node.h" + +struct sway_view; +struct sway_seat; + +enum sway_container_layout { + L_NONE, + L_HORIZ, + L_VERT, + L_STACKED, + L_TABBED, +}; + +enum sway_container_border { + B_NONE, + B_PIXEL, + B_NORMAL, + B_CSD, +}; + +enum sway_fullscreen_mode { + FULLSCREEN_NONE, + FULLSCREEN_WORKSPACE, + FULLSCREEN_GLOBAL, +}; + +struct sway_root; +struct sway_output; +struct sway_workspace; +struct sway_view; + +enum wlr_direction; + +struct sway_container_state { + // Container properties + enum sway_container_layout layout; + double x, y; + double width, height; + + enum sway_fullscreen_mode fullscreen_mode; + + struct sway_workspace *workspace; // NULL when hidden in the scratchpad + struct sway_container *parent; // NULL if container in root of workspace + list_t *children; // struct sway_container + + struct sway_container *focused_inactive_child; + bool focused; + + enum sway_container_border border; + int border_thickness; + bool border_top; + bool border_bottom; + bool border_left; + bool border_right; + + // These are in layout coordinates. + double content_x, content_y; + double content_width, content_height; +}; + +struct sway_container { + struct sway_node node; + struct sway_view *view; + + struct wlr_scene_tree *scene_tree; + + struct { + struct wlr_scene_tree *tree; + + struct wlr_scene_rect *border; + struct wlr_scene_rect *background_left; + struct wlr_scene_rect *background_right; + + struct sway_text_node *title_text; + struct sway_text_node *marks_text; + } title_bar; + + struct { + struct wlr_scene_tree *tree; + + struct wlr_scene_rect *top; + struct wlr_scene_rect *bottom; + struct wlr_scene_rect *left; + struct wlr_scene_rect *right; + } border; + + struct wlr_scene_blur *blur; + struct wlr_scene_shadow *shadow; + + struct wlr_scene_rect *dim_rect; + + struct wlr_scene_tree *content_tree; + + struct sway_container_state current; + struct sway_container_state pending; + + char *title; // The view's title (unformatted) + char *formatted_title; // The title displayed in the title bar + int title_width; + + char *title_format; + + enum sway_container_layout prev_split_layout; + + // Whether stickiness has been enabled on this container. Use + // `container_is_sticky_[or_child]` rather than accessing this field + // directly; it'll also check that the container is floating. + bool is_sticky; + + // For C_ROOT, this has no meaning + // For other types, this is the position in layout coordinates + // Includes borders + double saved_x, saved_y; + double saved_width, saved_height; + + // Used when the view changes to CSD unexpectedly. This will be a non-B_CSD + // border which we use to restore when the view returns to SSD. + enum sway_container_border saved_border; + + // The share of the space of parent container this container occupies + double width_fraction; + double height_fraction; + + // The share of space of the parent container that all children occupy + // Used for doing the resize calculations + double child_total_width; + double child_total_height; + + // Indicates that the container is a scratchpad container. + // Both hidden and visible scratchpad containers have scratchpad=true. + // Hidden scratchpad containers have a NULL parent. + bool scratchpad; + + // Stores last output size and position for adjusting coordinates of + // scratchpad windows. + // Unused for non-scratchpad windows. + struct wlr_box transform; + + float alpha; + + int corner_radius; + bool blur_enabled; + bool shadow_enabled; + float dim; + + struct { + struct animation animation; + float from_alpha; + float to_alpha; + int from_x; + int from_y; + int to_x; + int to_y; + int from_width; + int to_width; + int from_height; + int to_height; + int current_global_x; + int current_global_y; + int current_width; + int current_height; + int current_content_width; // needed for output.c + int current_content_height; // needed for output.c + + bool seat_is_resizing; + bool seat_is_moving_float; + } animation_state; + + list_t *marks; // char * + + struct { + struct wl_signal destroy; + } events; +}; + +struct sway_container *container_create(struct sway_view *view); + +void container_destroy(struct sway_container *con); + +void container_begin_destroy(struct sway_container *con); + +/** + * Search a container's descendants a container based on test criteria. Returns + * the first container that passes the test. + */ +struct sway_container *container_find_child(struct sway_container *container, + bool (*test)(struct sway_container *view, void *data), void *data); + +void container_for_each_child(struct sway_container *container, + void (*f)(struct sway_container *container, void *data), void *data); + +/** + * Returns the fullscreen container obstructing this container if it exists. + */ +struct sway_container *container_obstructing_fullscreen_container(struct sway_container *container); + +/** + * Returns true if the given container is an ancestor of this container. + */ +bool container_has_ancestor(struct sway_container *container, + struct sway_container *ancestor); + +void container_reap_empty(struct sway_container *con); + +struct sway_container *container_flatten(struct sway_container *container); + +void container_update_title_bar(struct sway_container *container); + +void container_update_marks(struct sway_container *container); + +size_t parse_title_format(struct sway_container *container, char *buffer); + +size_t container_build_representation(enum sway_container_layout layout, + list_t *children, char *buffer); + +void container_update_representation(struct sway_container *container); + +/** + * Return the height of a regular title bar. + */ +size_t container_titlebar_height(void); + +void floating_calculate_constraints(int *min_width, int *max_width, + int *min_height, int *max_height); + +void floating_fix_coordinates(struct sway_container *con, + struct wlr_box *old, struct wlr_box *new); + +void container_floating_resize_and_center(struct sway_container *con); + +void container_floating_set_default_size(struct sway_container *con); + +void container_set_resizing(struct sway_container *con, bool resizing); + +void container_set_floating(struct sway_container *container, bool enable); + +void container_set_geometry_from_content(struct sway_container *con); + +/** + * Determine if the given container is itself floating. + * This will return false for any descendants of a floating container. + * + * Uses pending container state. + */ +bool container_is_floating(struct sway_container *container); + +/** + * Get a container's box in layout coordinates. + */ +void container_get_box(struct sway_container *container, struct wlr_box *box); + +/** + * Move a floating container by the specified amount. + */ +void container_floating_translate(struct sway_container *con, + double x_amount, double y_amount); + +/** + * Choose an output for the floating container's new position. + */ +struct sway_output *container_floating_find_output(struct sway_container *con); + +/** + * Move a floating container to a new layout-local position. + */ +void container_floating_move_to(struct sway_container *con, + double lx, double ly); + +/** + * Move a floating container to the center of the workspace. + */ +void container_floating_move_to_center(struct sway_container *con); + +bool container_has_urgent_child(struct sway_container *container); + +/** + * If the container is involved in a drag or resize operation via a mouse, this + * ends the operation. + */ +void container_end_mouse_operation(struct sway_container *container); + +void container_set_fullscreen(struct sway_container *con, + enum sway_fullscreen_mode mode); + +/** + * Convenience function. + */ +void container_fullscreen_disable(struct sway_container *con); + +/** + * Walk up the container tree branch starting at the given container, and return + * its earliest ancestor. + */ +struct sway_container *container_toplevel_ancestor( + struct sway_container *container); + +/** + * Return true if the container is floating, or a child of a floating split + * container. + */ +bool container_is_floating_or_child(struct sway_container *container); + +/** + * Return true if the container is fullscreen, or a child of a fullscreen split + * container. + */ +bool container_is_fullscreen_or_child(struct sway_container *container); + +enum sway_container_layout container_parent_layout(struct sway_container *con); + +list_t *container_get_siblings(struct sway_container *container); + +int container_sibling_index(struct sway_container *child); + +void container_handle_fullscreen_reparent(struct sway_container *con); + +void container_add_child(struct sway_container *parent, + struct sway_container *child); + +void container_insert_child(struct sway_container *parent, + struct sway_container *child, int i); + +/** + * Side should be 0 to add before, or 1 to add after. + */ +void container_add_sibling(struct sway_container *parent, + struct sway_container *child, bool after); + +void container_detach(struct sway_container *child); + +void container_replace(struct sway_container *container, + struct sway_container *replacement); + +void container_swap(struct sway_container *con1, struct sway_container *con2); + +struct sway_container *container_split(struct sway_container *child, + enum sway_container_layout layout); + +bool container_is_transient_for(struct sway_container *child, + struct sway_container *ancestor); + +/** + * Find any container that has the given mark and return it. + */ +struct sway_container *container_find_mark(char *mark); + +/** + * Find any container that has the given mark and remove the mark from the + * container. Returns true if it matched a container. + */ +bool container_find_and_unmark(char *mark); + +/** + * Remove all marks from the container. + */ +void container_clear_marks(struct sway_container *container); + +bool container_has_mark(struct sway_container *container, char *mark); + +void container_add_mark(struct sway_container *container, char *mark); + +void container_raise_floating(struct sway_container *con); + +bool container_is_scratchpad_hidden(struct sway_container *con); + +bool container_is_scratchpad_hidden_or_child(struct sway_container *con); + +bool container_is_sticky(struct sway_container *con); + +bool container_is_sticky_or_child(struct sway_container *con); + +/** + * This will destroy pairs of redundant H/V splits + * e.g. H[V[H[app app]] app] -> H[app app app] + * The middle "V[H[" are eliminated by a call to container_squash + * on the V[ con. It's grandchildren are added to its parent. + * + * This function is roughly equivalent to i3's tree_flatten here: + * https://github.com/i3/i3/blob/1f0c628cde40cf87371481041b7197344e0417c6/src/tree.c#L651 + * + * Returns the number of new containers added to the parent + */ +int container_squash(struct sway_container *con); + +void container_arrange_title_bar(struct sway_container *con); + +void container_update(struct sway_container *con); + +void container_update_itself_and_parents(struct sway_container *con); + +bool container_has_shadow(struct sway_container *con); + +bool container_has_corner_radius(struct sway_container *con); + +#endif diff --git a/include/sway/tree/node.h b/include/sway/tree/node.h new file mode 100644 index 0000000..966da65 --- /dev/null +++ b/include/sway/tree/node.h @@ -0,0 +1,97 @@ +#ifndef _SWAY_NODE_H +#define _SWAY_NODE_H +#include +#include +#include +#include "list.h" + +#define MIN_SANE_W 100 +#define MIN_SANE_H 60 + +struct sway_root; +struct sway_output; +struct sway_workspace; +struct sway_container; +struct sway_transaction_instruction; +struct wlr_box; + +enum sway_node_type { + N_ROOT, + N_OUTPUT, + N_WORKSPACE, + N_CONTAINER, +}; + +struct sway_node { + enum sway_node_type type; + union { + struct sway_root *sway_root; + struct sway_output *sway_output; + struct sway_workspace *sway_workspace; + struct sway_container *sway_container; + }; + + /** + * A unique ID to identify this node. + * Primarily used in the get_tree JSON output. + */ + size_t id; + + struct sway_transaction_instruction *instruction; + size_t ntxnrefs; + bool destroying; + + // If true, indicates that the container has pending state that differs from + // the current. + bool dirty; + + struct { + struct wl_signal destroy; + } events; +}; + +void node_init(struct sway_node *node, enum sway_node_type type, void *thing); + +const char *node_type_to_str(enum sway_node_type type); + +/** + * Mark a node as dirty if it isn't already. Dirty nodes will be included in the + * next transaction then unmarked as dirty. + */ +void node_set_dirty(struct sway_node *node); + +bool node_is_view(struct sway_node *node); + +char *node_get_name(struct sway_node *node); + +void node_get_box(struct sway_node *node, struct wlr_box *box); + +struct sway_output *node_get_output(struct sway_node *node); + +enum sway_container_layout node_get_layout(struct sway_node *node); + +struct sway_node *node_get_parent(struct sway_node *node); + +list_t *node_get_children(struct sway_node *node); + +bool node_has_ancestor(struct sway_node *node, struct sway_node *ancestor); + +// when destroying a sway tree, it's not known which order the tree will be +// destroyed. To prevent freeing of scene_nodes recursing up the tree, +// let's use this helper function to disown them to the staging node. +void scene_node_disown_children(struct wlr_scene_tree *tree); + +// a helper function used to allocate tree nodes. If an allocation failure +// occurs a flag is flipped that can be checked later to destroy a parent +// of this scene node preventing memory leaks. +struct wlr_scene_tree *alloc_scene_tree(struct wlr_scene_tree *parent, + bool *failed); + +struct wlr_scene_shadow *alloc_scene_shadow(struct wlr_scene_tree *parent, + int width, int height, int corner_radius, float blur_sigma, + const float color [static 4], bool *failed); + +struct wlr_scene_blur *alloc_scene_blur(struct wlr_scene_tree *parent, + int width, int height, bool *failed); + +#endif diff --git a/include/sway/tree/root.h b/include/sway/tree/root.h new file mode 100644 index 0000000..68c4ece --- /dev/null +++ b/include/sway/tree/root.h @@ -0,0 +1,124 @@ +#ifndef _SWAY_ROOT_H +#define _SWAY_ROOT_H +#include +#include +#include +#include +#include +#include +#include +#include "sway/tree/container.h" +#include "sway/tree/node.h" +#include "list.h" + +extern struct sway_root *root; + +struct sway_root { + struct sway_node node; + struct wlr_output_layout *output_layout; + + // scene node layout: + // - root + // - staging + // - layer shell stuff + // - tiling + // - floating + // - fullscreen stuff + // - seat stuff + // - ext_session_lock + struct wlr_scene *root_scene; + + // since wlr_scene nodes can't be orphaned and must always + // have a parent, use this staging scene_tree so that a + // node always have a valid parent. Nothing in this + // staging node will be visible. + struct wlr_scene_tree *staging; + + // tree containing all layers the compositor will render. Cursor handling + // will end up iterating this tree. + struct wlr_scene_tree *layer_tree; + + struct { + struct wlr_scene_tree *shell_background; + struct wlr_scene_tree *shell_bottom; + struct wlr_scene_tree *blur_tree; + struct wlr_scene_tree *tiling; + struct wlr_scene_tree *floating; + struct wlr_scene_tree *shell_top; + struct wlr_scene_tree *fullscreen; + struct wlr_scene_tree *fullscreen_global; +#if WLR_HAS_XWAYLAND + struct wlr_scene_tree *unmanaged; +#endif + struct wlr_scene_tree *shell_overlay; + struct wlr_scene_tree *popup; + struct wlr_scene_tree *seat; + struct wlr_scene_tree *session_lock; + } layers; + + // Includes disabled outputs + struct wl_list all_outputs; // sway_output::link + + double x, y; + double width, height; + + list_t *outputs; // struct sway_output + list_t *non_desktop_outputs; // struct sway_output_non_desktop + list_t *scratchpad; // struct sway_container + + // For when there's no connected outputs + struct sway_output *fallback_output; + + struct sway_container *fullscreen_global; + + struct { + struct wl_signal new_node; + } events; +}; + +struct sway_root *root_create(struct wl_display *display); + +void root_destroy(struct sway_root *root); + +/** + * Move a container to the scratchpad. + * If a workspace is passed, the container is assumed to have been in + * the scratchpad before and is shown on the workspace. + * The ws parameter can safely be NULL. + */ +void root_scratchpad_add_container(struct sway_container *con, + struct sway_workspace *ws); + +/** + * Remove a container from the scratchpad. + */ +void root_scratchpad_remove_container(struct sway_container *con); + +/** + * Show a single scratchpad container. + */ +void root_scratchpad_show(struct sway_container *con); + +/** + * Hide a single scratchpad container. + */ +void root_scratchpad_hide(struct sway_container *con); + +void root_for_each_workspace(void (*f)(struct sway_workspace *ws, void *data), + void *data); + +void root_for_each_container(void (*f)(struct sway_container *con, void *data), + void *data); + +struct sway_output *root_find_output( + bool (*test)(struct sway_output *output, void *data), void *data); + +struct sway_workspace *root_find_workspace( + bool (*test)(struct sway_workspace *ws, void *data), void *data); + +struct sway_container *root_find_container( + bool (*test)(struct sway_container *con, void *data), void *data); + +void root_get_box(struct sway_root *root, struct wlr_box *box); + +#endif diff --git a/include/sway/tree/view.h b/include/sway/tree/view.h new file mode 100644 index 0000000..420a437 --- /dev/null +++ b/include/sway/tree/view.h @@ -0,0 +1,374 @@ +#ifndef _SWAY_VIEW_H +#define _SWAY_VIEW_H +#include +#include +#include +#include +#include +#include "sway/config.h" +#if WLR_HAS_XWAYLAND +#include +#endif +#include "sway/input/input-manager.h" +#include "sway/input/seat.h" + +struct sway_container; +struct sway_xdg_decoration; + +enum sway_view_type { + SWAY_VIEW_XDG_SHELL, +#if WLR_HAS_XWAYLAND + SWAY_VIEW_XWAYLAND, +#endif +}; + +enum sway_view_prop { + VIEW_PROP_TITLE, + VIEW_PROP_APP_ID, + VIEW_PROP_TAG, + VIEW_PROP_CLASS, + VIEW_PROP_INSTANCE, + VIEW_PROP_WINDOW_TYPE, + VIEW_PROP_WINDOW_ROLE, +#if WLR_HAS_XWAYLAND + VIEW_PROP_X11_WINDOW_ID, + VIEW_PROP_X11_PARENT_ID, +#endif +}; + +enum sway_view_tearing_mode { + TEARING_OVERRIDE_FALSE, + TEARING_OVERRIDE_TRUE, + TEARING_WINDOW_HINT, +}; + +struct sway_view_impl { + void (*get_constraints)(struct sway_view *view, double *min_width, + double *max_width, double *min_height, double *max_height); + const char *(*get_string_prop)(struct sway_view *view, + enum sway_view_prop prop); + uint32_t (*get_int_prop)(struct sway_view *view, enum sway_view_prop prop); + uint32_t (*configure)(struct sway_view *view, double lx, double ly, + int width, int height); + void (*set_activated)(struct sway_view *view, bool activated); + void (*set_tiled)(struct sway_view *view, bool tiled); + void (*set_fullscreen)(struct sway_view *view, bool fullscreen); + void (*set_resizing)(struct sway_view *view, bool resizing); + bool (*wants_floating)(struct sway_view *view); + bool (*is_transient_for)(struct sway_view *child, + struct sway_view *ancestor); + void (*close)(struct sway_view *view); + void (*close_popups)(struct sway_view *view); + void (*destroy)(struct sway_view *view); +}; + +struct sway_view { + enum sway_view_type type; + const struct sway_view_impl *impl; + + struct wlr_scene_tree *scene_tree; + struct wlr_scene_tree *content_tree; + struct wlr_scene_tree *saved_surface_tree; + struct wlr_scene_buffer *output_handler; + + struct wl_listener outputs_update; + + struct wlr_scene *image_capture_scene; + struct wlr_ext_image_capture_source_v1 *image_capture_source; + + struct sway_container *container; // NULL if unmapped and transactions finished + struct wlr_surface *surface; // NULL for unmapped views + struct sway_xdg_decoration *xdg_decoration; + + pid_t pid; + struct launcher_ctx *ctx; + + // The size the view would want to be if it weren't tiled. + // Used when changing a view from tiled to floating. + int natural_width, natural_height; + + bool using_csd; + + struct timespec urgent; + bool allow_request_urgent; + struct wl_event_source *urgent_timer; + + // The geometry for whatever the client is committing, regardless of + // transaction state. Updated on every commit. + struct wlr_box geometry; + + struct wlr_ext_foreign_toplevel_handle_v1 *ext_foreign_toplevel; + + struct wlr_foreign_toplevel_handle_v1 *foreign_toplevel; + struct wl_listener foreign_activate_request; + struct wl_listener foreign_fullscreen_request; + struct wl_listener foreign_close_request; + struct wl_listener foreign_minimize; + struct wl_listener foreign_destroy; + + bool destroying; + + list_t *executed_criteria; // struct criteria * + + union { + struct wlr_xdg_toplevel *wlr_xdg_toplevel; +#if WLR_HAS_XWAYLAND + struct wlr_xwayland_surface *wlr_xwayland_surface; +#endif + }; + + struct { + struct wl_signal unmap; + } events; + + int max_render_time; // In milliseconds + + enum seat_config_shortcuts_inhibit shortcuts_inhibit; + + enum sway_view_tearing_mode tearing_mode; + enum wp_tearing_control_v1_presentation_hint tearing_hint; +}; + +struct sway_xdg_shell_view { + struct sway_view view; + + struct wlr_scene_tree *image_capture_tree; + char *tag; + + struct wl_listener commit; + struct wl_listener request_move; + struct wl_listener request_resize; + struct wl_listener request_maximize; + struct wl_listener request_minimize; + struct wl_listener request_fullscreen; + struct wl_listener set_title; + struct wl_listener set_app_id; + struct wl_listener new_popup; + struct wl_listener map; + struct wl_listener unmap; + struct wl_listener destroy; +}; +#if WLR_HAS_XWAYLAND +struct sway_xwayland_view { + struct sway_view view; + + struct wlr_scene_tree *surface_tree; + + struct wlr_scene_surface *image_capture_scene_surface; + + struct wl_listener commit; + struct wl_listener request_move; + struct wl_listener request_resize; + struct wl_listener request_maximize; + struct wl_listener request_minimize; + struct wl_listener request_configure; + struct wl_listener request_fullscreen; + struct wl_listener request_activate; + struct wl_listener set_title; + struct wl_listener set_class; + struct wl_listener set_role; + struct wl_listener set_startup_id; + struct wl_listener set_window_type; + struct wl_listener set_hints; + struct wl_listener set_decorations; + struct wl_listener associate; + struct wl_listener dissociate; + struct wl_listener map; + struct wl_listener unmap; + struct wl_listener destroy; + struct wl_listener override_redirect; + + struct wl_listener surface_tree_destroy; +}; + +struct sway_xwayland_unmanaged { + struct wlr_xwayland_surface *wlr_xwayland_surface; + + struct wlr_scene_surface *surface_scene; + + struct wl_listener request_activate; + struct wl_listener request_configure; + struct wl_listener request_fullscreen; + struct wl_listener set_geometry; + struct wl_listener associate; + struct wl_listener dissociate; + struct wl_listener map; + struct wl_listener unmap; + struct wl_listener destroy; + struct wl_listener override_redirect; +}; +#endif + +struct sway_popup_desc { + struct wlr_scene_node *relative; + struct sway_view *view; +}; + +struct sway_xdg_popup { + struct sway_view *view; + struct wlr_xdg_popup *wlr_xdg_popup; + + struct wlr_scene_tree *scene_tree; + struct wlr_scene_tree *xdg_surface_tree; + + struct wlr_scene_tree *image_capture_tree; + + struct sway_popup_desc desc; + + struct wl_listener surface_commit; + struct wl_listener new_popup; + struct wl_listener reposition; + struct wl_listener destroy; +}; + +const char *view_get_title(struct sway_view *view); + +const char *view_get_app_id(struct sway_view *view); + +const char *view_get_class(struct sway_view *view); + +const char *view_get_instance(struct sway_view *view); + +uint32_t view_get_x11_window_id(struct sway_view *view); + +uint32_t view_get_x11_parent_id(struct sway_view *view); + +const char *view_get_window_role(struct sway_view *view); + +uint32_t view_get_window_type(struct sway_view *view); + +const char *view_get_sandbox_engine(struct sway_view *view); + +const char *view_get_sandbox_app_id(struct sway_view *view); + +const char *view_get_sandbox_instance_id(struct sway_view *view); + +const char *view_get_tag(struct sway_view *view); + +const char *view_get_shell(struct sway_view *view); + +void view_get_constraints(struct sway_view *view, double *min_width, + double *max_width, double *min_height, double *max_height); + +uint32_t view_configure(struct sway_view *view, double lx, double ly, int width, + int height); + +bool view_inhibit_idle(struct sway_view *view); + +/** + * Whether or not this view's most distant ancestor (possibly itself) is the + * only visible node in its tree. If the view is tiling, there may be floating + * views. If the view is floating, there may be tiling views or views in a + * different floating container. + */ +bool view_ancestor_is_only_visible(struct sway_view *view); + +/** + * Configure the view's position and size based on the container's position and + * size, taking borders into consideration. + */ +void view_autoconfigure(struct sway_view *view); + +void view_set_activated(struct sway_view *view, bool activated); + +/** + * Called when the view requests to be focused. + */ +void view_request_activate(struct sway_view *view, struct sway_seat *seat); + +/* + * Called when the view requests urgent state + */ +void view_request_urgent(struct sway_view *view); + +/** + * If possible, instructs the client to change their decoration mode. + */ +void view_set_csd_from_server(struct sway_view *view, bool enabled); + +/** + * Updates the view's border setting when the client unexpectedly changes their + * decoration mode. + */ +void view_update_csd_from_client(struct sway_view *view, bool enabled); + +void view_set_tiled(struct sway_view *view, bool tiled); + +void view_close(struct sway_view *view); + +void view_close_popups(struct sway_view *view); + +// view implementation + +bool view_init(struct sway_view *view, enum sway_view_type type, + const struct sway_view_impl *impl); + +void view_destroy(struct sway_view *view); + +void view_begin_destroy(struct sway_view *view); + +/** + * Map a view, ie. make it visible in the tree. + * + * `fullscreen` should be set to true (and optionally `fullscreen_output` + * should be populated) if the view should be made fullscreen immediately. + * + * `decoration` should be set to true if the client prefers CSD. The client's + * preference may be ignored. + */ +void view_map(struct sway_view *view, struct wlr_surface *wlr_surface, + bool fullscreen, struct wlr_output *fullscreen_output, bool decoration); + +void view_unmap(struct sway_view *view); + +void view_update_size(struct sway_view *view); +void view_center_and_clip_surface(struct sway_view *view); + +struct sway_view *view_from_wlr_xdg_surface( + struct wlr_xdg_surface *xdg_surface); +#if WLR_HAS_XWAYLAND +struct sway_view *view_from_wlr_xwayland_surface( + struct wlr_xwayland_surface *xsurface); +#endif +struct sway_view *view_from_wlr_surface(struct wlr_surface *surface); + +void view_update_app_id(struct sway_view *view); + +/** + * Re-read the view's title property and update any relevant title bars. + * The force argument makes it recreate the title bars even if the title hasn't + * changed. + */ +void view_update_title(struct sway_view *view, bool force); + +/** + * Run any criteria that match the view and haven't been run on this view + * before. + */ +void view_execute_criteria(struct sway_view *view); + +/** + * Returns true if there's a possibility the view may be rendered on screen. + * Intended for damage tracking. + */ +bool view_is_visible(struct sway_view *view); + +void view_set_urgent(struct sway_view *view, bool enable); + +bool view_is_urgent(struct sway_view *view); + +void view_remove_saved_buffer(struct sway_view *view); + +void view_save_buffer(struct sway_view *view); + +bool view_is_transient_for(struct sway_view *child, struct sway_view *ancestor); + +void view_assign_ctx(struct sway_view *view, struct launcher_ctx *ctx); + +void view_send_frame_done(struct sway_view *view); + +bool view_can_tear(struct sway_view *view); + +void xdg_toplevel_tag_manager_v1_handle_set_tag(struct wl_listener *listener, void *data); + +#endif diff --git a/include/sway/tree/workspace.h b/include/sway/tree/workspace.h new file mode 100644 index 0000000..a7da622 --- /dev/null +++ b/include/sway/tree/workspace.h @@ -0,0 +1,176 @@ +#ifndef _SWAY_WORKSPACE_H +#define _SWAY_WORKSPACE_H + +#include +#include +#include +#include "sway/animation_manager.h" +#include "sway/config.h" +#include "sway/tree/container.h" +#include "sway/tree/node.h" + +struct sway_view; + +struct sway_workspace_state { + struct sway_container *fullscreen; + double x, y; + int width, height; + enum sway_container_layout layout; + struct sway_output *output; + list_t *floating; + list_t *tiling; + + struct sway_container *focused_inactive_child; + bool focused; +}; + +struct sway_workspace { + struct sway_node node; + + struct { + struct wlr_scene_tree *tiling; + struct wlr_scene_tree *fullscreen; + } layers; + + struct sway_container *fullscreen; + + char *name; + char *representation; + + double x, y; + int width, height; + enum sway_container_layout layout; + enum sway_container_layout prev_split_layout; + + struct side_gaps current_gaps; + int gaps_inner; + struct side_gaps gaps_outer; + + struct sway_output *output; // NULL if no outputs are connected + list_t *floating; // struct sway_container + list_t *tiling; // struct sway_container + list_t *output_priority; + bool urgent; + + struct sway_workspace_state current; + struct wlr_ext_workspace_handle_v1 *ext_workspace; // Always set. + + struct { + struct animation animation; + float from_alpha; + float to_alpha; + } animation_state; +}; + +struct workspace_config *workspace_find_config(const char *ws_name); + +struct sway_output *workspace_get_initial_output(const char *name); + +struct sway_workspace *workspace_create(struct sway_output *output, + const char *name); + +void workspace_destroy(struct sway_workspace *workspace); + +void workspace_begin_destroy(struct sway_workspace *workspace); + +void workspace_consider_destroy(struct sway_workspace *ws); + +char *workspace_next_name(const char *output_name); + +struct sway_workspace *workspace_auto_back_and_forth( + struct sway_workspace *workspace); + +bool workspace_switch(struct sway_workspace *workspace); + +struct sway_workspace *workspace_by_number(const char* name); + +struct sway_workspace *workspace_by_name(const char*); + +struct sway_workspace *workspace_output_next(struct sway_workspace *current); + +struct sway_workspace *workspace_next(struct sway_workspace *current); + +struct sway_workspace *workspace_output_prev(struct sway_workspace *current); + +struct sway_workspace *workspace_prev(struct sway_workspace *current); + +bool workspace_is_visible(struct sway_workspace *ws); + +bool workspace_is_empty(struct sway_workspace *ws); + +void workspace_output_raise_priority(struct sway_workspace *workspace, + struct sway_output *old_output, struct sway_output *new_output); + +void workspace_output_add_priority(struct sway_workspace *workspace, + struct sway_output *output); + +struct sway_output *workspace_output_get_highest_available( + struct sway_workspace *ws); + +void workspace_detect_urgent(struct sway_workspace *workspace); + +void workspace_for_each_container(struct sway_workspace *ws, + void (*f)(struct sway_container *con, void *data), void *data); + +struct sway_container *workspace_find_container(struct sway_workspace *ws, + bool (*test)(struct sway_container *con, void *data), void *data); + +/** + * Wrap the workspace's tiling children in a new container. + * The new container will be the only direct tiling child of the workspace. + * The new container is returned. + */ +struct sway_container *workspace_wrap_children(struct sway_workspace *ws); + +void workspace_unwrap_children(struct sway_workspace *ws, + struct sway_container *wrap); + +void workspace_detach(struct sway_workspace *workspace); + +struct sway_container *workspace_add_tiling(struct sway_workspace *workspace, + struct sway_container *con); + +void workspace_add_floating(struct sway_workspace *workspace, + struct sway_container *con); + +/** + * Adds a tiling container to the workspace without considering + * the workspace_layout, so the con will not be split. + */ +void workspace_insert_tiling_direct(struct sway_workspace *workspace, + struct sway_container *con, int index); + +struct sway_container *workspace_insert_tiling(struct sway_workspace *workspace, + struct sway_container *con, int index); + +void workspace_remove_gaps(struct sway_workspace *ws); + +void workspace_add_gaps(struct sway_workspace *ws); + +struct sway_container *workspace_split(struct sway_workspace *workspace, + enum sway_container_layout layout); + +void workspace_update_representation(struct sway_workspace *ws); + +void workspace_get_box(struct sway_workspace *workspace, struct wlr_box *box); + +size_t workspace_num_tiling_views(struct sway_workspace *ws); + +size_t workspace_num_sticky_containers(struct sway_workspace *ws); + +/** + * workspace_squash is container_flatten in the reverse + * direction. Instead of eliminating redundant splits that are + * parents of the target container, it eliminates pairs of + * redundant H/V splits that are children of the workspace. + */ +void workspace_squash(struct sway_workspace *workspace); + +void workspace_move_to_output(struct sway_workspace *workspace, + struct sway_output *output); + +bool sway_ext_workspace_init(void); +void sway_ext_workspace_output_enable(struct sway_output *output); +void sway_ext_workspace_output_disable(struct sway_output *output); + +#endif diff --git a/include/sway/xdg_decoration.h b/include/sway/xdg_decoration.h new file mode 100644 index 0000000..2388ebc --- /dev/null +++ b/include/sway/xdg_decoration.h @@ -0,0 +1,21 @@ +#ifndef _SWAY_XDG_DECORATION_H +#define _SWAY_XDG_DECORATION_H + +#include + +struct sway_xdg_decoration { + struct wlr_xdg_toplevel_decoration_v1 *wlr_xdg_decoration; + struct wl_list link; + + struct sway_view *view; + + struct wl_listener destroy; + struct wl_listener request_mode; +}; + +struct sway_xdg_decoration *xdg_decoration_from_surface( + struct wlr_surface *surface); + +void set_xdg_decoration_mode(struct sway_xdg_decoration *deco); + +#endif diff --git a/include/sway/xwayland.h b/include/sway/xwayland.h new file mode 100644 index 0000000..93055c4 --- /dev/null +++ b/include/sway/xwayland.h @@ -0,0 +1,31 @@ +#ifndef SWAY_XWAYLAND_H +#define SWAY_XWAYLAND_H + +#include +#include + +enum atom_name { + NET_WM_WINDOW_TYPE_NORMAL, + NET_WM_WINDOW_TYPE_DIALOG, + NET_WM_WINDOW_TYPE_UTILITY, + NET_WM_WINDOW_TYPE_TOOLBAR, + NET_WM_WINDOW_TYPE_SPLASH, + NET_WM_WINDOW_TYPE_MENU, + NET_WM_WINDOW_TYPE_DROPDOWN_MENU, + NET_WM_WINDOW_TYPE_POPUP_MENU, + NET_WM_WINDOW_TYPE_TOOLTIP, + NET_WM_WINDOW_TYPE_NOTIFICATION, + NET_WM_STATE_MODAL, + ATOM_LAST, +}; + +struct sway_xwayland { + struct wlr_xwayland *wlr_xwayland; + struct wlr_xcursor_manager *xcursor_manager; + + xcb_atom_t atoms[ATOM_LAST]; +}; + +void handle_xwayland_ready(struct wl_listener *listener, void *data); + +#endif diff --git a/include/swaybar/bar.h b/include/swaybar/bar.h new file mode 100644 index 0000000..197d219 --- /dev/null +++ b/include/swaybar/bar.h @@ -0,0 +1,118 @@ +#ifndef _SWAYBAR_BAR_H +#define _SWAYBAR_BAR_H +#include +#include "config.h" +#include "input.h" +#include "pool-buffer.h" +#include "cursor-shape-v1-client-protocol.h" +#include "wlr-layer-shell-unstable-v1-client-protocol.h" +#include "xdg-output-unstable-v1-client-protocol.h" + +struct swaybar_config; +struct swaybar_output; +#if HAVE_TRAY +struct swaybar_tray; +#endif +struct swaybar_workspace; +struct loop; + +struct swaybar { + char *id; + char *mode; + bool mode_pango_markup; + + // only relevant when bar is in "hide" mode + bool visible_by_modifier; + bool visible_by_urgency; + bool visible_by_mode; + bool visible; + + struct wl_display *display; + struct wl_compositor *compositor; + struct zwlr_layer_shell_v1 *layer_shell; + struct zxdg_output_manager_v1 *xdg_output_manager; + struct wp_cursor_shape_manager_v1 *cursor_shape_manager; + struct wl_shm *shm; + + struct swaybar_config *config; + struct status_line *status; + + struct loop *eventloop; + + int ipc_event_socketfd; + int ipc_socketfd; + + struct wl_list outputs; // swaybar_output::link + struct wl_list unused_outputs; // swaybar_output::link + struct wl_list seats; // swaybar_seat::link + +#if HAVE_TRAY + struct swaybar_tray *tray; +#endif + + bool running; +}; + +struct swaybar_output { + struct wl_list link; // swaybar::outputs + struct swaybar *bar; + struct wl_output *output; + struct zxdg_output_v1 *xdg_output; + struct wl_surface *surface; + struct zwlr_layer_surface_v1 *layer_surface; + uint32_t wl_name; + + struct wl_list workspaces; // swaybar_workspace::link + struct wl_list hotspots; // swaybar_hotspot::link + + char *name; + char *identifier; + bool focused; + + uint32_t width, height; + int32_t scale; + enum wl_output_subpixel subpixel; + struct pool_buffer buffers[2]; + struct pool_buffer *current_buffer; + bool dirty; + bool frame_scheduled; + + uint32_t output_height, output_width, output_x, output_y; +}; + +struct swaybar_workspace { + struct wl_list link; // swaybar_output::workspaces + int num; + char *name; + char *label; + bool focused; + bool visible; + bool urgent; +}; + +bool bar_setup(struct swaybar *bar, const char *socket_path); +void bar_run(struct swaybar *bar); +void bar_teardown(struct swaybar *bar); + +void set_bar_dirty(struct swaybar *bar); + +/* + * Determines whether the bar should be visible and changes it to be so. + * If the current visibility of the bar is the different to what it should be, + * then it adds or destroys the layer surface as required, + * as well as sending the cont or stop signal to the status command. + * If the current visibility of the bar is already what it should be, + * then this function is a no-op, unless moving_layer is true, which occurs + * when the bar changes from "hide" to "dock" mode or vice versa, and the bar + * needs to be destroyed and re-added in order to change its layer. + * + * Returns true if the bar is now visible, otherwise false. + */ +bool determine_bar_visibility(struct swaybar *bar, bool moving_layer); +void free_workspaces(struct wl_list *list); + +void status_in(int fd, short mask, void *data); + +void destroy_layer_surface(struct swaybar_output *output); + +#endif diff --git a/include/swaybar/config.h b/include/swaybar/config.h new file mode 100644 index 0000000..ad58b3c --- /dev/null +++ b/include/swaybar/config.h @@ -0,0 +1,100 @@ +#ifndef _SWAYBAR_CONFIG_H +#define _SWAYBAR_CONFIG_H +#include +#include +#include +#include "../include/config.h" +#include "list.h" +#include "util.h" +#include + +struct box_colors { + uint32_t border; + uint32_t background; + uint32_t text; +}; + +struct box_size { + uint32_t width; + uint32_t height; +}; + +struct config_output { + struct wl_list link; // swaybar_config::outputs + char *name; +}; + +struct swaybar_binding { + uint32_t button; + char *command; + bool release; +}; + +struct swaybar_config { + char *status_command; + bool pango_markup; + uint32_t position; // zwlr_layer_surface_v1_anchor + PangoFontDescription *font_description; + char *sep_symbol; + char *mode; + char *hidden_state; + char *modifier; + bool strip_workspace_numbers; + bool strip_workspace_name; + bool binding_mode_indicator; + bool wrap_scroll; + bool workspace_buttons; + uint32_t workspace_min_width; + list_t *bindings; + struct wl_list outputs; // config_output::link + int height; + int status_padding; + int status_edge_padding; + struct { + int top; + int right; + int bottom; + int left; + } gaps; + + struct { + uint32_t background; + uint32_t statusline; + uint32_t separator; + + uint32_t focused_background; + uint32_t focused_statusline; + uint32_t focused_separator; + + struct box_colors focused_workspace; + struct box_colors active_workspace; + struct box_colors inactive_workspace; + struct box_colors urgent_workspace; + struct box_colors binding_mode; + } colors; + +#if HAVE_TRAY + char *icon_theme; + struct wl_list tray_bindings; // struct tray_binding::link + bool tray_hidden; + list_t *tray_outputs; // char * + int tray_padding; +#endif +}; + +#if HAVE_TRAY +struct tray_binding { + uint32_t button; + char *command; + struct wl_list link; // struct tray_binding::link +}; + +void free_tray_binding(struct tray_binding *binding); +#endif + +struct swaybar_config *init_config(void); +void free_config(struct swaybar_config *config); +uint32_t parse_position(const char *position); +void free_binding(struct swaybar_binding *binding); + +#endif diff --git a/include/swaybar/i3bar.h b/include/swaybar/i3bar.h new file mode 100644 index 0000000..dced2a6 --- /dev/null +++ b/include/swaybar/i3bar.h @@ -0,0 +1,35 @@ +#ifndef _SWAYBAR_I3BAR_H +#define _SWAYBAR_I3BAR_H + +#include "input.h" +#include "status_line.h" + +struct i3bar_block { + struct wl_list link; // status_link::blocks + int ref_count; + char *full_text, *short_text, *align, *min_width_str; + bool urgent; + uint32_t color; + bool color_set; + int min_width; + char *name, *instance; + bool separator; + int separator_block_width; + bool markup; + // Airblader features + uint32_t background; + uint32_t border; + bool border_set; + int border_top; + int border_bottom; + int border_left; + int border_right; +}; + +void i3bar_block_unref(struct i3bar_block *block); +bool i3bar_handle_readable(struct status_line *status); +enum hotspot_event_handling i3bar_block_send_click(struct status_line *status, + struct i3bar_block *block, double x, double y, double rx, double ry, + double w, double h, int scale, uint32_t button, bool released); + +#endif diff --git a/include/swaybar/image.h b/include/swaybar/image.h new file mode 100644 index 0000000..53a210d --- /dev/null +++ b/include/swaybar/image.h @@ -0,0 +1,7 @@ +#ifndef _SWAYBAR_IMAGE_H +#define _SWAYBAR_IMAGE_H +#include + +cairo_surface_t *load_image(const char *path); + +#endif diff --git a/include/swaybar/input.h b/include/swaybar/input.h new file mode 100644 index 0000000..8ea88a6 --- /dev/null +++ b/include/swaybar/input.h @@ -0,0 +1,83 @@ +#ifndef _SWAYBAR_INPUT_H +#define _SWAYBAR_INPUT_H + +#include +#include +#include "list.h" + +#define SWAY_SCROLL_UP KEY_MAX + 1 +#define SWAY_SCROLL_DOWN KEY_MAX + 2 +#define SWAY_SCROLL_LEFT KEY_MAX + 3 +#define SWAY_SCROLL_RIGHT KEY_MAX + 4 + +#define SWAY_CONTINUOUS_SCROLL_TIMEOUT 1000 +#define SWAY_CONTINUOUS_SCROLL_THRESHOLD 10000 + +struct swaybar; +struct swaybar_output; + +struct swaybar_pointer { + struct wl_pointer *pointer; + struct wl_cursor_theme *cursor_theme; + struct wl_cursor_image *cursor_image; + struct wl_surface *cursor_surface; + struct swaybar_output *current; + double x, y; + uint32_t serial; +}; + +struct touch_slot { + int32_t id; + uint32_t time; + struct swaybar_output *output; + double start_x, start_y; + double x, y; +}; + +struct swaybar_touch { + struct wl_touch *touch; + struct touch_slot slots[16]; +}; + +enum hotspot_event_handling { + HOTSPOT_IGNORE, + HOTSPOT_PROCESS, +}; + +struct swaybar_hotspot { + struct wl_list link; // swaybar_output::hotspots + int x, y, width, height; + enum hotspot_event_handling (*callback)(struct swaybar_output *output, + struct swaybar_hotspot *hotspot, double x, double y, uint32_t button, + bool released, void *data); + void (*destroy)(void *data); + void *data; +}; + +struct swaybar_scroll_axis { + wl_fixed_t value; + uint32_t discrete_steps; + uint32_t update_time; +}; + +struct swaybar_seat { + struct swaybar *bar; + uint32_t wl_name; + struct wl_seat *wl_seat; + struct swaybar_pointer pointer; + struct swaybar_touch touch; + struct wl_list link; // swaybar_seat:link + struct swaybar_scroll_axis axis[2]; +}; + +extern const struct wl_seat_listener seat_listener; + +void update_cursor(struct swaybar_seat *seat); + +uint32_t event_to_x11_button(uint32_t event); + +void free_hotspots(struct wl_list *list); + +void swaybar_seat_free(struct swaybar_seat *seat); + +#endif diff --git a/include/swaybar/ipc.h b/include/swaybar/ipc.h new file mode 100644 index 0000000..d8cd0c7 --- /dev/null +++ b/include/swaybar/ipc.h @@ -0,0 +1,12 @@ +#ifndef _SWAYBAR_IPC_H +#define _SWAYBAR_IPC_H +#include +#include "swaybar/bar.h" + +bool ipc_initialize(struct swaybar *bar); +bool handle_ipc_readable(struct swaybar *bar); +bool ipc_get_workspaces(struct swaybar *bar); +void ipc_send_workspace_command(struct swaybar *bar, const char *ws); +void ipc_execute_binding(struct swaybar *bar, struct swaybar_binding *bind); + +#endif diff --git a/include/swaybar/render.h b/include/swaybar/render.h new file mode 100644 index 0000000..ebdc69e --- /dev/null +++ b/include/swaybar/render.h @@ -0,0 +1,8 @@ +#ifndef _SWAYBAR_RENDER_H +#define _SWAYBAR_RENDER_H + +struct swaybar_output; + +void render_frame(struct swaybar_output *output); + +#endif diff --git a/include/swaybar/status_line.h b/include/swaybar/status_line.h new file mode 100644 index 0000000..65c3a79 --- /dev/null +++ b/include/swaybar/status_line.h @@ -0,0 +1,46 @@ +#ifndef _SWAYBAR_STATUS_LINE_H +#define _SWAYBAR_STATUS_LINE_H +#include +#include +#include +#include +#include "bar.h" + +enum status_protocol { + PROTOCOL_UNDEF, + PROTOCOL_ERROR, + PROTOCOL_TEXT, + PROTOCOL_I3BAR, +}; + +struct status_line { + struct swaybar *bar; + + pid_t pid; + int read_fd, write_fd; + FILE *read, *write; + + enum status_protocol protocol; + const char *text; + struct wl_list blocks; // i3bar_block::link + + int stop_signal; + int cont_signal; + + bool click_events; + bool float_event_coords; + bool clicked; + char *buffer; + size_t buffer_size; + size_t buffer_index; + bool started; + bool expecting_comma; + json_tokener *tokener; +}; + +struct status_line *status_line_init(char *cmd); +void status_error(struct status_line *status, const char *text); +bool status_handle_readable(struct status_line *status); +void status_line_free(struct status_line *status); + +#endif diff --git a/include/swaybar/tray/host.h b/include/swaybar/tray/host.h new file mode 100644 index 0000000..2d4cf82 --- /dev/null +++ b/include/swaybar/tray/host.h @@ -0,0 +1,17 @@ +#ifndef _SWAYBAR_TRAY_HOST_H +#define _SWAYBAR_TRAY_HOST_H + +#include + +struct swaybar_tray; + +struct swaybar_host { + struct swaybar_tray *tray; + char *service; + char *watcher_interface; +}; + +bool init_host(struct swaybar_host *host, char *protocol, struct swaybar_tray *tray); +void finish_host(struct swaybar_host *host); + +#endif diff --git a/include/swaybar/tray/icon.h b/include/swaybar/tray/icon.h new file mode 100644 index 0000000..3673674 --- /dev/null +++ b/include/swaybar/tray/icon.h @@ -0,0 +1,43 @@ +#ifndef _SWAYBAR_TRAY_ICON_H +#define _SWAYBAR_TRAY_ICON_H + +#include "list.h" + +struct icon_theme_subdir { + char *name; + int size; + + enum { + THRESHOLD, + SCALABLE, + FIXED + } type; + + int max_size; + int min_size; + int threshold; +}; + +struct icon_theme { + char *name; + char *comment; + list_t *inherits; // char * + list_t *directories; // char * + + char *dir; + list_t *subdirs; // struct icon_theme_subdir * +}; + +void init_themes(list_t **themes, list_t **basedirs); +void finish_themes(list_t *themes, list_t *basedirs); + +/* + * Finds an icon of a specified size given a list of themes and base directories. + * If the icon is found, the pointers min_size & max_size are set to minimum & + * maximum size that the icon can be scaled to, respectively. + * Returns: path of icon (which should be freed), or NULL if the icon is not found. + */ +char *find_icon(list_t *themes, list_t *basedirs, char *name, int size, + char *theme, int *min_size, int *max_size); + +#endif diff --git a/include/swaybar/tray/item.h b/include/swaybar/tray/item.h new file mode 100644 index 0000000..73937a0 --- /dev/null +++ b/include/swaybar/tray/item.h @@ -0,0 +1,58 @@ +#ifndef _SWAYBAR_TRAY_ITEM_H +#define _SWAYBAR_TRAY_ITEM_H + +#include +#include +#include +#include +#include "swaybar/tray/tray.h" +#include "list.h" + +struct swaybar_output; + +struct swaybar_pixmap { + int size; + unsigned char pixels[]; +}; + +struct swaybar_sni_slot { + struct wl_list link; // swaybar_sni::slots + struct swaybar_sni *sni; + const char *prop; + const char *type; + void *dest; + sd_bus_slot *slot; +}; + +struct swaybar_sni { + // icon properties + struct swaybar_tray *tray; + cairo_surface_t *icon; + int min_size; + int max_size; + int target_size; + + // dbus properties + char *watcher_id; + char *service; + char *path; + char *interface; + + char *status; + char *icon_name; + list_t *icon_pixmap; // struct swaybar_pixmap * + char *attention_icon_name; + list_t *attention_icon_pixmap; // struct swaybar_pixmap * + bool item_is_menu; + char *menu; + char *icon_theme_path; // non-standard KDE property + + struct wl_list slots; // swaybar_sni_slot::link +}; + +struct swaybar_sni *create_sni(char *id, struct swaybar_tray *tray); +void destroy_sni(struct swaybar_sni *sni); +uint32_t render_sni(cairo_t *cairo, struct swaybar_output *output, double *x, + struct swaybar_sni *sni); + +#endif diff --git a/include/swaybar/tray/tray.h b/include/swaybar/tray/tray.h new file mode 100644 index 0000000..d2e80a6 --- /dev/null +++ b/include/swaybar/tray/tray.h @@ -0,0 +1,42 @@ +#ifndef _SWAYBAR_TRAY_TRAY_H +#define _SWAYBAR_TRAY_TRAY_H + +#include "config.h" +#if HAVE_LIBSYSTEMD +#include +#elif HAVE_LIBELOGIND +#include +#elif HAVE_BASU +#include +#endif +#include +#include +#include "swaybar/tray/host.h" +#include "list.h" + +struct swaybar; +struct swaybar_output; +struct swaybar_watcher; + +struct swaybar_tray { + struct swaybar *bar; + + int fd; + sd_bus *bus; + + struct swaybar_host host_xdg; + struct swaybar_host host_kde; + list_t *items; // struct swaybar_sni * + struct swaybar_watcher *watcher_xdg; + struct swaybar_watcher *watcher_kde; + + list_t *basedirs; // char * + list_t *themes; // struct swaybar_theme * +}; + +struct swaybar_tray *create_tray(struct swaybar *bar); +void destroy_tray(struct swaybar_tray *tray); +void tray_in(int fd, short mask, void *data); +uint32_t render_tray(cairo_t *cairo, struct swaybar_output *output, double *x); + +#endif diff --git a/include/swaybar/tray/watcher.h b/include/swaybar/tray/watcher.h new file mode 100644 index 0000000..8f276da --- /dev/null +++ b/include/swaybar/tray/watcher.h @@ -0,0 +1,18 @@ +#ifndef _SWAYBAR_TRAY_WATCHER_H +#define _SWAYBAR_TRAY_WATCHER_H + +#include "swaybar/tray/tray.h" +#include "list.h" + +struct swaybar_watcher { + char *interface; + sd_bus *bus; + list_t *hosts; + list_t *items; + int version; +}; + +struct swaybar_watcher *create_watcher(char *protocol, sd_bus *bus); +void destroy_watcher(struct swaybar_watcher *watcher); + +#endif diff --git a/include/swaynag/config.h b/include/swaynag/config.h new file mode 100644 index 0000000..0d8889d --- /dev/null +++ b/include/swaynag/config.h @@ -0,0 +1,13 @@ +#ifndef _SWAYNAG_CONFIG_H +#define _SWAYNAG_CONFIG_H +#include "swaynag/swaynag.h" +#include "list.h" + +int swaynag_parse_options(int argc, char **argv, struct swaynag *swaynag, + list_t *types, struct swaynag_type *type, char **config, bool *debug); + +char *swaynag_get_config_path(void); + +int swaynag_load_config(char *path, struct swaynag *swaynag, list_t *types); + +#endif diff --git a/include/swaynag/render.h b/include/swaynag/render.h new file mode 100644 index 0000000..d09e592 --- /dev/null +++ b/include/swaynag/render.h @@ -0,0 +1,7 @@ +#ifndef _SWAYNAG_RENDER_H +#define _SWAYNAG_RENDER_H +#include "swaynag/swaynag.h" + +void render_frame(struct swaynag *swaynag); + +#endif diff --git a/include/swaynag/swaynag.h b/include/swaynag/swaynag.h new file mode 100644 index 0000000..fb9e9c2 --- /dev/null +++ b/include/swaynag/swaynag.h @@ -0,0 +1,111 @@ +#ifndef _SWAYNAG_SWAYNAG_H +#define _SWAYNAG_SWAYNAG_H +#include +#include +#include "list.h" +#include "pool-buffer.h" +#include "cursor-shape-v1-client-protocol.h" + +#include "swaynag/types.h" + +#define SWAYNAG_MAX_HEIGHT 500 + +struct swaynag; + +enum swaynag_action_type { + SWAYNAG_ACTION_DISMISS, + SWAYNAG_ACTION_EXPAND, + SWAYNAG_ACTION_COMMAND, +}; + +struct swaynag_pointer { + struct wl_pointer *pointer; + uint32_t serial; + struct wl_cursor_theme *cursor_theme; + struct wl_cursor_image *cursor_image; + struct wl_surface *cursor_surface; + int x; + int y; +}; + +struct swaynag_seat { + struct wl_seat *wl_seat; + uint32_t wl_name; + struct swaynag *swaynag; + struct swaynag_pointer pointer; + struct wl_list link; +}; + +struct swaynag_output { + char *name; + struct wl_output *wl_output; + uint32_t wl_name; + uint32_t scale; + struct swaynag *swaynag; + struct wl_list link; +}; + +struct swaynag_button { + char *text; + enum swaynag_action_type type; + char *action; + int x; + int y; + int width; + int height; + bool terminal; + bool dismiss; +}; + +struct swaynag_details { + bool visible; + char *message; + char *details_text; + + int x; + int y; + int width; + int height; + + int offset; + int visible_lines; + int total_lines; + struct swaynag_button *button_details; + struct swaynag_button button_up; + struct swaynag_button button_down; +}; + +struct swaynag { + bool run_display; + + struct wl_display *display; + struct wl_compositor *compositor; + struct wl_seat *seat; + struct wl_shm *shm; + struct wl_list outputs; // swaynag_output::link + struct wl_list seats; // swaynag_seat::link + struct swaynag_output *output; + struct zwlr_layer_shell_v1 *layer_shell; + struct zwlr_layer_surface_v1 *layer_surface; + struct wp_cursor_shape_manager_v1 *cursor_shape_manager; + struct wl_surface *surface; + + uint32_t width; + uint32_t height; + int32_t scale; + struct pool_buffer buffers[2]; + struct pool_buffer *current_buffer; + + struct swaynag_type *type; + char *message; + list_t *buttons; + struct swaynag_details details; +}; + +void swaynag_setup(struct swaynag *swaynag); + +void swaynag_run(struct swaynag *swaynag); + +void swaynag_destroy(struct swaynag *swaynag); + +#endif diff --git a/include/swaynag/types.h b/include/swaynag/types.h new file mode 100644 index 0000000..9c3c50d --- /dev/null +++ b/include/swaynag/types.h @@ -0,0 +1,50 @@ +#ifndef _SWAYNAG_TYPES_H +#define _SWAYNAG_TYPES_H + +#include +#include +#include "list.h" + +struct swaynag_type { + char *name; + + PangoFontDescription *font_description; + char *output; + uint32_t anchors; + int32_t layer; // enum zwlr_layer_shell_v1_layer or -1 if unset + + // Colors + uint32_t button_text; + uint32_t button_background; + uint32_t details_background; + uint32_t background; + uint32_t text; + uint32_t border; + uint32_t border_bottom; + + // Sizing + ssize_t bar_border_thickness; + ssize_t message_padding; + ssize_t details_border_thickness; + ssize_t button_border_thickness; + ssize_t button_gap; + ssize_t button_gap_close; + ssize_t button_margin_right; + ssize_t button_padding; +}; + +struct swaynag_type *swaynag_type_new(const char *name); + +void swaynag_types_add_default(list_t *types); + +struct swaynag_type *swaynag_type_get(list_t *types, char *name); + +struct swaynag_type *swaynag_type_clone(struct swaynag_type *type); + +void swaynag_type_merge(struct swaynag_type *dest, struct swaynag_type *src); + +void swaynag_type_free(struct swaynag_type *type); + +void swaynag_types_free(list_t *types); + +#endif diff --git a/include/util.h b/include/util.h new file mode 100644 index 0000000..92f5916 --- /dev/null +++ b/include/util.h @@ -0,0 +1,66 @@ +#ifndef _SWAY_UTIL_H +#define _SWAY_UTIL_H + +#include +#include +#include + +enum movement_unit { + MOVEMENT_UNIT_PX, + MOVEMENT_UNIT_PPT, + MOVEMENT_UNIT_DEFAULT, + MOVEMENT_UNIT_INVALID, +}; + +struct movement_amount { + int amount; + enum movement_unit unit; +}; + +/* + * Parse units such as "px" or "ppt" + */ +enum movement_unit parse_movement_unit(const char *unit); + +/* + * Parse arguments such as "10", "10px" or "10 px". + * Returns the number of arguments consumed. + */ +int parse_movement_amount(int argc, char **argv, + struct movement_amount *amount); + +/** + * Wrap i into the range [0, max] + */ +int wrap(int i, int max); + +/** + * Given a string that represents an RGB(A) color, result will be set to a + * uint32_t version of the color, as long as it is valid. If it is invalid, + * then false will be returned and result will be untouched. + */ +bool parse_color(const char *color, uint32_t *result); + +void color_to_rgba(float dest[static 4], uint32_t color); + +/** + * Given a string that represents a boolean, return the boolean value. This + * function also takes in the current boolean value to support toggling. If + * toggling is not desired, pass in true for current so that toggling values + * get parsed as not true. + */ +bool parse_boolean(const char *boolean, bool current); + +/** + * Given a string that represents a floating point value, return a float. + * Returns NAN on error. + */ +float parse_float(const char *value); + +const char *sway_wl_output_subpixel_to_string(enum wl_output_subpixel subpixel); + +bool sway_set_cloexec(int fd, bool cloexec); + +uint32_t get_current_time_in_msec(void); + +#endif diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..dad186a --- /dev/null +++ b/meson.build @@ -0,0 +1,259 @@ +project( + 'sway', + 'c', + version: '0.6', + license: 'MIT', + meson_version: '>=1.3', + default_options: [ + 'c_std=c11', + 'warning_level=2', + 'werror=true', + 'wrap_mode=nodownload', + ], +) + +original_version = '1.12.0' + +add_project_arguments( + [ + '-DWLR_USE_UNSTABLE', + '-D_POSIX_C_SOURCE=200809L', + + '-Wno-unused-parameter', + '-Wno-unused-result', + '-Wno-missing-braces', + '-Wno-format-zero-length', + '-Wundef', + '-Wvla', + ], + language: 'c', +) + +cc = meson.get_compiler('c') + +is_freebsd = host_machine.system().startswith('freebsd') +datadir = get_option('datadir') +sysconfdir = get_option('sysconfdir') +prefix = get_option('prefix') + +if is_freebsd + add_project_arguments('-D_C11_SOURCE', language: 'c') +endif + +# Execute the scenefx subproject, if any +subproject( + 'scenefx', + required: false, +) +scenefx = dependency('scenefx-0.5', fallback: 'scenefx') + +# Execute the wlroots subproject, if any +wlroots_version = ['>=0.20.0', '<0.21.0'] +subproject( + 'wlroots', + default_options: ['examples=false'], + required: false, + version: wlroots_version, +) +wlroots = dependency('wlroots-0.20', version: wlroots_version, fallback: 'wlroots') +wlroots_features = { + 'xwayland': false, + 'libinput_backend': false, + 'session': false, +} +foreach name, _ : wlroots_features + var_name = 'have_' + name.underscorify() + have = wlroots.get_variable(pkgconfig: var_name, internal: var_name) == 'true' + wlroots_features += { name: have } +endforeach + +null_dep = dependency('', required: false) + +jsonc = dependency('json-c', version: '>=0.13') +pcre2 = dependency('libpcre2-8') +wayland_server = dependency('wayland-server', version: '>=1.21.0') +wayland_client = dependency('wayland-client') +wayland_cursor = dependency('wayland-cursor') +wayland_protos = dependency('wayland-protocols', version: '>=1.41', default_options: ['tests=false']) +xkbcommon = dependency('xkbcommon', version: '>=1.5.0') +cairo = dependency('cairo') +pango = dependency('pango') +pangocairo = dependency('pangocairo') +gdk_pixbuf = dependency('gdk-pixbuf-2.0', required: get_option('gdk-pixbuf')) +pixman = dependency('pixman-1') +libevdev = dependency('libevdev') +libinput = wlroots_features['libinput_backend'] ? dependency('libinput', version: '>=1.26.0') : null_dep +xcb = wlroots_features['xwayland'] ? dependency('xcb') : null_dep +drm = dependency('libdrm') +libudev = wlroots_features['libinput_backend'] ? dependency('libudev') : null_dep +math = cc.find_library('m') +rt = cc.find_library('rt') +xcb_icccm = wlroots_features['xwayland'] ? dependency('xcb-icccm') : null_dep +threads = dependency('threads') # for pthread_setschedparam and pthread_atfork + +if get_option('sd-bus-provider') == 'auto' + if not get_option('tray').disabled() + assert(get_option('auto_features').auto(), 'sd-bus-provider must not be set to auto since auto_features != auto') + endif + sdbus = dependency(['libsystemd', 'libelogind'], + required: false, + version: '>=239', + ) + if not sdbus.found() + sdbus = dependency('basu', required: false) + endif +else + sdbus = dependency(get_option('sd-bus-provider'), required: get_option('tray')) +endif + +tray_deps_found = sdbus.found() +if get_option('tray').enabled() and not tray_deps_found + error('Building with -Dtray=enabled, but sd-bus has not been not found') +endif +have_tray = (not get_option('tray').disabled()) and tray_deps_found + +conf_data = configuration_data() + +conf_data.set10('HAVE_GDK_PIXBUF', gdk_pixbuf.found()) +conf_data.set10('HAVE_LIBSYSTEMD', sdbus.found() and sdbus.name() == 'libsystemd') +conf_data.set10('HAVE_LIBELOGIND', sdbus.found() and sdbus.name() == 'libelogind') +conf_data.set10('HAVE_BASU', sdbus.found() and sdbus.name() == 'basu') +conf_data.set10('HAVE_TRAY', have_tray) +foreach sym : ['LIBINPUT_CONFIG_ACCEL_PROFILE_CUSTOM', 'LIBINPUT_CONFIG_DRAG_LOCK_ENABLED_STICKY', 'LIBINPUT_SWITCH_KEYPAD_SLIDE'] + conf_data.set10('HAVE_' + sym, cc.has_header_symbol('libinput.h', sym, dependencies: libinput)) +endforeach + +scdoc = dependency('scdoc', version: '>=1.9.2', native: true, required: get_option('man-pages')) +if scdoc.found() + scdoc_prog = find_program(scdoc.get_variable(pkgconfig: 'scdoc'), native: true) + mandir = get_option('mandir') + man_files = [ + 'sway/sway.1.scd', + 'sway/sway.5.scd', + 'sway/sway-bar.5.scd', + 'sway/sway-input.5.scd', + 'sway/sway-ipc.7.scd', + 'sway/sway-output.5.scd', + 'swaybar/swaybar-protocol.7.scd', + 'swaymsg/swaymsg.1.scd', + ] + + if get_option('swaynag') + man_files += [ + 'swaynag/swaynag.1.scd', + 'swaynag/swaynag.5.scd', + ] + endif + + foreach filename : man_files + topic = filename.split('.')[-3].split('/')[-1] + section = filename.split('.')[-2] + output = '@0@.@1@'.format(topic, section) + + custom_target( + output, + input: filename, + output: output, + command: scdoc_prog, + install: true, + feed: true, + capture: true, + install_dir: '@0@/man@1@'.format(mandir, section) + ) + endforeach +endif + +add_project_arguments('-DSYSCONFDIR="/@0@"'.format(join_paths(prefix, sysconfdir)), language : 'c') + +version = '"@0@"'.format(meson.project_version()) +git = find_program('git', native: true, required: false) +if git.found() + git_commit = run_command([git, '--git-dir=.git', 'rev-parse', '--short', 'HEAD'], check: false) + git_branch = run_command([git, '--git-dir=.git', 'rev-parse', '--abbrev-ref', 'HEAD'], check: false) + if git_commit.returncode() == 0 and git_branch.returncode() == 0 + version = '"@0@-@1@ (" __DATE__ ", branch \'@2@\')"'.format( + meson.project_version(), + git_commit.stdout().strip(), + git_branch.stdout().strip(), + ) + endif +endif +add_project_arguments('-DSWAY_VERSION=@0@'.format(version), language: 'c') +add_project_arguments('-DSWAY_ORIGINAL_VERSION="@0@"'.format(original_version), language: 'c') +add_project_arguments('-DSCENEFX_VERSION="@0@"'.format(scenefx.version()), language: 'c') + +fs = import('fs') + +# Strip relative path prefixes from the code if possible, otherwise hide them. +relative_dir = fs.relative_to(meson.current_source_dir(), meson.global_build_root()) + '/' +if cc.has_argument('-fmacro-prefix-map=/prefix/to/hide=') + add_project_arguments( + '-fmacro-prefix-map=@0@='.format(relative_dir), + language: 'c', + ) +else + add_project_arguments( + '-DSWAY_REL_SRC_DIR="@0@"'.format(relative_dir), + language: 'c', + ) +endif + + +sway_inc = include_directories('include') + +subdir('include') +subdir('protocols') +subdir('common') +subdir('sway') +subdir('swaymsg') + +if get_option('swaybar') or get_option('swaynag') + subdir('client') +endif +if get_option('swaybar') + subdir('swaybar') +endif +if get_option('swaynag') + subdir('swaynag') +endif + +config = configuration_data() +config.set('datadir', join_paths(prefix, datadir)) +config.set('prefix', prefix) +config.set('sysconfdir', join_paths(prefix, sysconfdir)) + +configure_file( + configuration: config, + input: 'config.in', + output: '@BASENAME@', + install_dir: join_paths(sysconfdir, 'sway') +) + +install_data( + 'sway.desktop', + install_dir: join_paths(datadir, 'wayland-sessions') +) + +if get_option('default-wallpaper') + wallpaper_files = files( + 'assets/Sway_Wallpaper_Blue_768x1024.png', + 'assets/Sway_Wallpaper_Blue_768x1024_Portrait.png', + 'assets/Sway_Wallpaper_Blue_1136x640.png', + 'assets/Sway_Wallpaper_Blue_1136x640_Portrait.png', + 'assets/Sway_Wallpaper_Blue_1366x768.png', + 'assets/Sway_Wallpaper_Blue_1920x1080.png', + 'assets/Sway_Wallpaper_Blue_2048x1536.png', + 'assets/Sway_Wallpaper_Blue_2048x1536_Portrait.png', + ) + wallpaper_install_dir = join_paths(datadir, 'backgrounds', 'sway') + + install_data(wallpaper_files, install_dir: wallpaper_install_dir) +endif + +subdir('completions') + +summary({ + 'gdk-pixbuf': gdk_pixbuf.found(), + 'tray': have_tray, + 'man-pages': scdoc.found(), +}, bool_yn: true) diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..506ecc9 --- /dev/null +++ b/meson_options.txt @@ -0,0 +1,10 @@ +option('default-wallpaper', type: 'boolean', value: true, description: 'Install the default wallpaper.') +option('zsh-completions', type: 'boolean', value: true, description: 'Install zsh shell completions.') +option('bash-completions', type: 'boolean', value: true, description: 'Install bash shell completions.') +option('fish-completions', type: 'boolean', value: true, description: 'Install fish shell completions.') +option('swaybar', type: 'boolean', value: true, description: 'Enable support for swaybar') +option('swaynag', type: 'boolean', value: true, description: 'Enable support for swaynag') +option('tray', type: 'feature', value: 'auto', description: 'Enable support for swaybar tray') +option('gdk-pixbuf', type: 'feature', value: 'auto', description: 'Enable support for more image formats in swaybar tray') +option('man-pages', type: 'feature', value: 'auto', description: 'Generate and install man pages') +option('sd-bus-provider', type: 'combo', choices: ['auto', 'libsystemd', 'libelogind', 'basu'], value: 'auto', description: 'Provider of the sd-bus library') diff --git a/protocols/meson.build b/protocols/meson.build new file mode 100644 index 0000000..a1cb353 --- /dev/null +++ b/protocols/meson.build @@ -0,0 +1,39 @@ +wl_protocol_dir = wayland_protos.get_variable('pkgdatadir') + +wayland_scanner_dep = dependency('wayland-scanner', native: true) +wayland_scanner = find_program( + wayland_scanner_dep.get_variable('wayland_scanner'), + native: true, +) + +protocols = [ + wl_protocol_dir / 'stable/tablet/tablet-v2.xml', + wl_protocol_dir / 'stable/xdg-shell/xdg-shell.xml', + wl_protocol_dir / 'staging/cursor-shape/cursor-shape-v1.xml', + wl_protocol_dir / 'unstable/xdg-output/xdg-output-unstable-v1.xml', + 'wlr-layer-shell-unstable-v1.xml', + 'wlr-output-power-management-unstable-v1.xml', +] + +wl_protos_src = [] + +foreach xml : protocols + wl_protos_src += custom_target( + xml.underscorify() + '_c', + input: xml, + output: '@BASENAME@-protocol.c', + command: [wayland_scanner, 'private-code', '@INPUT@', '@OUTPUT@'], + ) + wl_protos_src += custom_target( + xml.underscorify() + '_server_h', + input: xml, + output: '@BASENAME@-protocol.h', + command: [wayland_scanner, 'server-header', '@INPUT@', '@OUTPUT@'], + ) + wl_protos_src += custom_target( + xml.underscorify() + '_client_h', + input: xml, + output: '@BASENAME@-client-protocol.h', + command: [wayland_scanner, 'client-header', '@INPUT@', '@OUTPUT@'], + ) +endforeach diff --git a/protocols/wlr-layer-shell-unstable-v1.xml b/protocols/wlr-layer-shell-unstable-v1.xml new file mode 100644 index 0000000..e9f27e4 --- /dev/null +++ b/protocols/wlr-layer-shell-unstable-v1.xml @@ -0,0 +1,407 @@ + + + + Copyright © 2017 Drew DeVault + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + Clients can use this interface to assign the surface_layer role to + wl_surfaces. Such surfaces are assigned to a "layer" of the output and + rendered with a defined z-depth respective to each other. They may also be + anchored to the edges and corners of a screen and specify input handling + semantics. This interface should be suitable for the implementation of + many desktop shell components, and a broad number of other applications + that interact with the desktop. + + + + + Create a layer surface for an existing surface. This assigns the role of + layer_surface, or raises a protocol error if another role is already + assigned. + + Creating a layer surface from a wl_surface which has a buffer attached + or committed is a client error, and any attempts by a client to attach + or manipulate a buffer prior to the first layer_surface.configure call + must also be treated as errors. + + After creating a layer_surface object and setting it up, the client + must perform an initial commit without any buffer attached. + The compositor will reply with a layer_surface.configure event. + The client must acknowledge it and is then allowed to attach a buffer + to map the surface. + + You may pass NULL for output to allow the compositor to decide which + output to use. Generally this will be the one that the user most + recently interacted with. + + Clients can specify a namespace that defines the purpose of the layer + surface. + + + + + + + + + + + + + + + + + These values indicate which layers a surface can be rendered in. They + are ordered by z depth, bottom-most first. Traditional shell surfaces + will typically be rendered between the bottom and top layers. + Fullscreen shell surfaces are typically rendered at the top layer. + Multiple surfaces can share a single layer, and ordering within a + single layer is undefined. + + + + + + + + + + + + + This request indicates that the client will not use the layer_shell + object any more. Objects that have been created through this instance + are not affected. + + + + + + + An interface that may be implemented by a wl_surface, for surfaces that + are designed to be rendered as a layer of a stacked desktop-like + environment. + + Layer surface state (layer, size, anchor, exclusive zone, + margin, interactivity) is double-buffered, and will be applied at the + time wl_surface.commit of the corresponding wl_surface is called. + + Attaching a null buffer to a layer surface unmaps it. + + Unmapping a layer_surface means that the surface cannot be shown by the + compositor until it is explicitly mapped again. The layer_surface + returns to the state it had right after layer_shell.get_layer_surface. + The client can re-map the surface by performing a commit without any + buffer attached, waiting for a configure event and handling it as usual. + + + + + Sets the size of the surface in surface-local coordinates. The + compositor will display the surface centered with respect to its + anchors. + + If you pass 0 for either value, the compositor will assign it and + inform you of the assignment in the configure event. You must set your + anchor to opposite edges in the dimensions you omit; not doing so is a + protocol error. Both values are 0 by default. + + Size is double-buffered, see wl_surface.commit. + + + + + + + + Requests that the compositor anchor the surface to the specified edges + and corners. If two orthogonal edges are specified (e.g. 'top' and + 'left'), then the anchor point will be the intersection of the edges + (e.g. the top left corner of the output); otherwise the anchor point + will be centered on that edge, or in the center if none is specified. + + Anchor is double-buffered, see wl_surface.commit. + + + + + + + Requests that the compositor avoids occluding an area with other + surfaces. The compositor's use of this information is + implementation-dependent - do not assume that this region will not + actually be occluded. + + A positive value is only meaningful if the surface is anchored to one + edge or an edge and both perpendicular edges. If the surface is not + anchored, anchored to only two perpendicular edges (a corner), anchored + to only two parallel edges or anchored to all edges, a positive value + will be treated the same as zero. + + A positive zone is the distance from the edge in surface-local + coordinates to consider exclusive. + + Surfaces that do not wish to have an exclusive zone may instead specify + how they should interact with surfaces that do. If set to zero, the + surface indicates that it would like to be moved to avoid occluding + surfaces with a positive exclusive zone. If set to -1, the surface + indicates that it would not like to be moved to accommodate for other + surfaces, and the compositor should extend it all the way to the edges + it is anchored to. + + For example, a panel might set its exclusive zone to 10, so that + maximized shell surfaces are not shown on top of it. A notification + might set its exclusive zone to 0, so that it is moved to avoid + occluding the panel, but shell surfaces are shown underneath it. A + wallpaper or lock screen might set their exclusive zone to -1, so that + they stretch below or over the panel. + + The default value is 0. + + Exclusive zone is double-buffered, see wl_surface.commit. + + + + + + + Requests that the surface be placed some distance away from the anchor + point on the output, in surface-local coordinates. Setting this value + for edges you are not anchored to has no effect. + + The exclusive zone includes the margin. + + Margin is double-buffered, see wl_surface.commit. + + + + + + + + + + Types of keyboard interaction possible for layer shell surfaces. The + rationale for this is twofold: (1) some applications are not interested + in keyboard events and not allowing them to be focused can improve the + desktop experience; (2) some applications will want to take exclusive + keyboard focus. + + + + + This value indicates that this surface is not interested in keyboard + events and the compositor should never assign it the keyboard focus. + + This is the default value, set for newly created layer shell surfaces. + + This is useful for e.g. desktop widgets that display information or + only have interaction with non-keyboard input devices. + + + + + Request exclusive keyboard focus if this surface is above the shell surface layer. + + For the top and overlay layers, the seat will always give + exclusive keyboard focus to the top-most layer which has keyboard + interactivity set to exclusive. If this layer contains multiple + surfaces with keyboard interactivity set to exclusive, the compositor + determines the one receiving keyboard events in an implementation- + defined manner. In this case, no guarantee is made when this surface + will receive keyboard focus (if ever). + + For the bottom and background layers, the compositor is allowed to use + normal focus semantics. + + This setting is mainly intended for applications that need to ensure + they receive all keyboard events, such as a lock screen or a password + prompt. + + + + + This requests the compositor to allow this surface to be focused and + unfocused by the user in an implementation-defined manner. The user + should be able to unfocus this surface even regardless of the layer + it is on. + + Typically, the compositor will want to use its normal mechanism to + manage keyboard focus between layer shell surfaces with this setting + and regular toplevels on the desktop layer (e.g. click to focus). + Nevertheless, it is possible for a compositor to require a special + interaction to focus or unfocus layer shell surfaces (e.g. requiring + a click even if focus follows the mouse normally, or providing a + keybinding to switch focus between layers). + + This setting is mainly intended for desktop shell components (e.g. + panels) that allow keyboard interaction. Using this option can allow + implementing a desktop shell that can be fully usable without the + mouse. + + + + + + + Set how keyboard events are delivered to this surface. By default, + layer shell surfaces do not receive keyboard events; this request can + be used to change this. + + This setting is inherited by child surfaces set by the get_popup + request. + + Layer surfaces receive pointer, touch, and tablet events normally. If + you do not want to receive them, set the input region on your surface + to an empty region. + + Keyboard interactivity is double-buffered, see wl_surface.commit. + + + + + + + This assigns an xdg_popup's parent to this layer_surface. This popup + should have been created via xdg_surface::get_popup with the parent set + to NULL, and this request must be invoked before committing the popup's + initial state. + + See the documentation of xdg_popup for more details about what an + xdg_popup is and how it is used. + + + + + + + When a configure event is received, if a client commits the + surface in response to the configure event, then the client + must make an ack_configure request sometime before the commit + request, passing along the serial of the configure event. + + If the client receives multiple configure events before it + can respond to one, it only has to ack the last configure event. + + A client is not required to commit immediately after sending + an ack_configure request - it may even ack_configure several times + before its next surface commit. + + A client may send multiple ack_configure requests before committing, but + only the last request sent before a commit indicates which configure + event the client really is responding to. + + + + + + + This request destroys the layer surface. + + + + + + The configure event asks the client to resize its surface. + + Clients should arrange their surface for the new states, and then send + an ack_configure request with the serial sent in this configure event at + some point before committing the new surface. + + The client is free to dismiss all but the last configure event it + received. + + The width and height arguments specify the size of the window in + surface-local coordinates. + + The size is a hint, in the sense that the client is free to ignore it if + it doesn't resize, pick a smaller size (to satisfy aspect ratio or + resize in steps of NxM pixels). If the client picks a smaller size and + is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the + surface will be centered on this axis. + + If the width or height arguments are zero, it means the client should + decide its own window dimension. + + + + + + + + + The closed event is sent by the compositor when the surface will no + longer be shown. The output may have been destroyed or the user may + have asked for it to be removed. Further changes to the surface will be + ignored. The client should destroy the resource after receiving this + event, and create a new surface if they so choose. + + + + + + + + + + + + + + + + + + + + + + + Change the layer that the surface is rendered on. + + Layer is double-buffered, see wl_surface.commit. + + + + + + + + + Requests an edge for the exclusive zone to apply. The exclusive + edge will be automatically deduced from anchor points when possible, + but when the surface is anchored to a corner, it will be necessary + to set it explicitly to disambiguate, as it is not possible to deduce + which one of the two corner edges should be used. + + The edge must be one the surface is anchored to, otherwise the + invalid_exclusive_edge protocol error will be raised. + + + + + diff --git a/protocols/wlr-output-power-management-unstable-v1.xml b/protocols/wlr-output-power-management-unstable-v1.xml new file mode 100644 index 0000000..20dbb77 --- /dev/null +++ b/protocols/wlr-output-power-management-unstable-v1.xml @@ -0,0 +1,128 @@ + + + + Copyright © 2019 Purism SPC + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + This protocol allows clients to control power management modes + of outputs that are currently part of the compositor space. The + intent is to allow special clients like desktop shells to power + down outputs when the system is idle. + + To modify outputs not currently part of the compositor space see + wlr-output-management. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible changes + may be added together with the corresponding interface version bump. + Backward incompatible changes are done by bumping the version number in + the protocol and interface names and resetting the interface version. + Once the protocol is to be declared stable, the 'z' prefix and the + version number in the protocol and interface names are removed and the + interface version number is reset. + + + + + This interface is a manager that allows creating per-output power + management mode controls. + + + + + Create an output power management mode control that can be used to + adjust the power management mode for a given output. + + + + + + + + All objects created by the manager will still remain valid, until their + appropriate destroy request has been called. + + + + + + + This object offers requests to set the power management mode of + an output. + + + + + + + + + + + + + + Set an output's power save mode to the given mode. The mode change + is effective immediately. If the output does not support the given + mode a failed event is sent. + + + + + + + Report the power management mode change of an output. + + The mode event is sent after an output changed its power + management mode. The reason can be a client using set_mode or the + compositor deciding to change an output's mode. + This event is also sent immediately when the object is created + so the client is informed about the current power management mode. + + + + + + + This event indicates that the output power management mode control + is no longer valid. This can happen for a number of reasons, + including: + - The output doesn't support power management + - Another client already has exclusive power management mode control + for this output + - The output disappeared + + Upon receiving this event, the client should destroy this object. + + + + + + Destroys the output power management mode control object. + + + + diff --git a/sway.desktop b/sway.desktop new file mode 100644 index 0000000..03d979d --- /dev/null +++ b/sway.desktop @@ -0,0 +1,6 @@ +[Desktop Entry] +Name=Sway +Comment=An i3-compatible Wayland compositor +Exec=sway +Type=Application +DesktopNames=sway;wlroots;swayfx diff --git a/sway/animation_manager.c b/sway/animation_manager.c new file mode 100644 index 0000000..e5adc5a --- /dev/null +++ b/sway/animation_manager.c @@ -0,0 +1,134 @@ +#include +#include "sway/animation_manager.h" +#include "sway/config.h" +#include "sway/output.h" +#include "sway/server.h" +#include "sway/tree/arrange.h" +#include "sway/tree/container.h" +#include "sway/tree/node.h" +#include "sway/tree/root.h" + +struct animation_manager { + float tick_time; + float progress_delta; + struct wl_event_source *tick; + struct wl_list animations; +} animation_manager; + +struct animation init_animation(void *data) { + return (struct animation){ + .data = data, + .progress = 0.0f, + .multiplier = 0.0f, + .initialized = false, + .update = NULL, + .complete = NULL, + }; +} + +static float ease_out_cubic(float p) { + return pow(p - 1, 3) + 1; +} + +static int animation_timer() { + struct animation *animation, *tmp; + wl_list_for_each_reverse_safe(animation, tmp, &animation_manager.animations, link) { + animation->progress = MIN(animation->progress + animation_manager.progress_delta, 1.0f); + animation->multiplier = ease_out_cubic(animation->progress); + + if (animation->update) { + animation->update(animation->data); + } + + if (animation->progress == 1.0f) { + finish_animation(animation); + if (animation->complete) { + animation->complete(animation->data); + } + } + } + + if (!wl_list_empty(&animation_manager.animations)) { + wl_event_source_timer_update(animation_manager.tick, + animation_manager.tick_time); + } + return 0; +} + +void add_animation(struct animation *animation, void (*update_callback)(void *), + void (*complete_callback)(void *)) { + if (!config->animation_duration_ms) { + if (complete_callback) { + complete_callback(animation->data); + } + return; + } + // remove previous instances of this animation + if (animation->initialized) { + wl_list_remove(&animation->link); + } + + animation->progress = 0.0f; + animation->multiplier = 0.0f; + animation->initialized = true; + animation->update = update_callback; + animation->complete = complete_callback; + wl_list_insert(&animation_manager.animations, &animation->link); +} + +void finish_animation(struct animation *animation) { + if (animation->initialized) { + wl_list_remove(&animation->link); + animation->initialized = false; + } + animation->progress = 1.0f; + animation->multiplier = 1.0f; +} + +void start_animations() { + if (!config->animation_duration_ms) { + return; + } + assert(animation_manager.tick); + + wl_event_source_timer_update(animation_manager.tick, 1); +} + +static float get_fastest_output_refresh_ms() { + float fastest_output_refresh_ms = 16.6667; // fallback to 60 Hz + for (int i = 0; i < root->outputs->length; ++i) { + struct sway_output *output = root->outputs->items[i]; + if (output->refresh_nsec > 0) { + float output_refresh_ms = output->refresh_nsec / 1000000.0; + fastest_output_refresh_ms = MIN(fastest_output_refresh_ms, output_refresh_ms); + } + } + return fastest_output_refresh_ms; +} + +void refresh_animation_manager_timing() { + if (!config || !root) { + return; + } + animation_manager.tick_time = get_fastest_output_refresh_ms(); + animation_manager.progress_delta = animation_manager.tick_time / config->animation_duration_ms; +} + +void animation_manager_init(struct sway_server *server) { + animation_manager.tick = wl_event_loop_add_timer(server->wl_event_loop, + animation_timer, NULL); + wl_list_init(&animation_manager.animations); + refresh_animation_manager_timing(); +} + +static float lerp(float a, float b, float t) { + return a * (1.0 - t) + b * t; +} + +float get_animated_value(float from, float to, const struct animation *animation) { + if (!config->animation_duration_ms || !animation->initialized) { + return to; + } + return lerp(from, to, animation->multiplier); +} + diff --git a/sway/commands.c b/sway/commands.c new file mode 100644 index 0000000..9e9adef --- /dev/null +++ b/sway/commands.c @@ -0,0 +1,552 @@ +#include +#include +#include +#include +#include +#include +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/criteria.h" +#include "sway/input/input-manager.h" +#include "sway/input/seat.h" +#include "sway/tree/view.h" +#include "stringop.h" +#include "log.h" + +// Returns error object, or NULL if check succeeds. +struct cmd_results *checkarg(int argc, const char *name, enum expected_args type, int val) { + const char *error_name = NULL; + switch (type) { + case EXPECTED_AT_LEAST: + if (argc < val) { + error_name = "at least "; + } + break; + case EXPECTED_AT_MOST: + if (argc > val) { + error_name = "at most "; + } + break; + case EXPECTED_EQUAL_TO: + if (argc != val) { + error_name = ""; + } + } + return error_name ? + cmd_results_new(CMD_INVALID, "Invalid %s command " + "(expected %s%d argument%s, got %d)", + name, error_name, val, val != 1 ? "s" : "", argc) + : NULL; +} + +/* Keep alphabetized */ +static const struct cmd_handler handlers[] = { + { "animation_duration_ms", cmd_animation_duration_ms }, + { "assign", cmd_assign }, + { "bar", cmd_bar }, + { "bindcode", cmd_bindcode }, + { "bindgesture", cmd_bindgesture }, + { "bindswitch", cmd_bindswitch }, + { "bindsym", cmd_bindsym }, + { "blur", cmd_blur }, + { "blur_brightness", cmd_blur_brightness }, + { "blur_contrast", cmd_blur_contrast }, + { "blur_noise", cmd_blur_noise }, + { "blur_passes", cmd_blur_passes }, + { "blur_radius", cmd_blur_radius }, + { "blur_saturation", cmd_blur_saturation }, + { "blur_xray", cmd_blur_xray }, + { "client.background", cmd_client_noop }, + { "client.focused", cmd_client_focused }, + { "client.focused_inactive", cmd_client_focused_inactive }, + { "client.focused_tab_title", cmd_client_focused_tab_title }, + { "client.placeholder", cmd_client_noop }, + { "client.unfocused", cmd_client_unfocused }, + { "client.urgent", cmd_client_urgent }, + { "corner_radius", cmd_corner_radius }, + { "default_border", cmd_default_border }, + { "default_dim_inactive", cmd_default_dim_inactive }, + { "default_floating_border", cmd_default_floating_border }, + { "dim_inactive", cmd_dim_inactive }, + { "dim_inactive_colors.unfocused", cmd_dim_inactive_colors_unfocused }, + { "dim_inactive_colors.urgent", cmd_dim_inactive_colors_urgent }, + { "exec", cmd_exec }, + { "exec_always", cmd_exec_always }, + { "floating_maximum_size", cmd_floating_maximum_size }, + { "floating_minimum_size", cmd_floating_minimum_size }, + { "floating_modifier", cmd_floating_modifier }, + { "focus", cmd_focus }, + { "focus_follows_mouse", cmd_focus_follows_mouse }, + { "focus_on_window_activation", cmd_focus_on_window_activation }, + { "focus_wrapping", cmd_focus_wrapping }, + { "font", cmd_font }, + { "for_window", cmd_for_window }, + { "force_display_urgency_hint", cmd_force_display_urgency_hint }, + { "force_focus_wrapping", cmd_force_focus_wrapping }, + { "fullscreen", cmd_fullscreen }, + { "gaps", cmd_gaps }, + { "hide_edge_borders", cmd_hide_edge_borders }, + { "input", cmd_input }, + { "layer_effects", cmd_layer_effects }, + { "mode", cmd_mode }, + { "mouse_warping", cmd_mouse_warping }, + { "new_float", cmd_new_float }, + { "new_window", cmd_new_window }, + { "no_focus", cmd_no_focus }, + { "output", cmd_output }, + { "popup_during_fullscreen", cmd_popup_during_fullscreen }, + { "seat", cmd_seat }, + { "set", cmd_set }, + { "shadow_blur_radius", cmd_shadow_blur_radius }, + { "shadow_color", cmd_shadow_color }, + { "shadow_inactive_color", cmd_shadow_inactive_color }, + { "shadow_offset", cmd_shadow_offset }, + { "shadows", cmd_shadows }, + { "shadows_on_csd", cmd_shadows_on_csd }, + { "show_marks", cmd_show_marks }, + { "smart_borders", cmd_smart_borders }, + { "smart_corner_radius", cmd_smart_corner_radius }, + { "smart_gaps", cmd_smart_gaps }, + { "tiling_drag", cmd_tiling_drag }, + { "tiling_drag_threshold", cmd_tiling_drag_threshold }, + { "title_align", cmd_title_align }, + { "titlebar_border_thickness", cmd_titlebar_border_thickness }, + { "titlebar_padding", cmd_titlebar_padding }, + { "titlebar_separator", cmd_titlebar_separator }, + { "unbindcode", cmd_unbindcode }, + { "unbindgesture", cmd_unbindgesture }, + { "unbindswitch", cmd_unbindswitch }, + { "unbindsym", cmd_unbindsym }, + { "workspace", cmd_workspace }, + { "workspace_auto_back_and_forth", cmd_ws_auto_back_and_forth }, +}; + +/* Config-time only commands. Keep alphabetized */ +static const struct cmd_handler config_handlers[] = { + { "default_orientation", cmd_default_orientation }, + { "include", cmd_include }, + { "primary_selection", cmd_primary_selection }, + { "scratchpad_minimize", cmd_scratchpad_minimize }, + { "swaybg_command", cmd_swaybg_command }, + { "swaynag_command", cmd_swaynag_command }, + { "workspace_layout", cmd_workspace_layout }, + { "xwayland", cmd_xwayland }, +}; + +/* Runtime-only commands. Keep alphabetized */ +static const struct cmd_handler command_handlers[] = { + { "allow_tearing", cmd_allow_tearing }, + { "border", cmd_border }, + { "create_output", cmd_create_output }, + { "exit", cmd_exit }, + { "floating", cmd_floating }, + { "fullscreen", cmd_fullscreen }, + { "inhibit_idle", cmd_inhibit_idle }, + { "kill", cmd_kill }, + { "layout", cmd_layout }, + { "mark", cmd_mark }, + { "max_render_time", cmd_max_render_time }, + { "move", cmd_move }, + { "nop", cmd_nop }, + { "opacity", cmd_opacity }, + { "reload", cmd_reload }, + { "rename", cmd_rename }, + { "resize", cmd_resize }, + { "scratchpad", cmd_scratchpad }, + { "shortcuts_inhibitor", cmd_shortcuts_inhibitor }, + { "split", cmd_split }, + { "splith", cmd_splith }, + { "splitt", cmd_splitt }, + { "splitv", cmd_splitv }, + { "sticky", cmd_sticky }, + { "swap", cmd_swap }, + { "title_format", cmd_title_format }, + { "unmark", cmd_unmark }, + { "urgent", cmd_urgent }, +}; + +static int handler_compare(const void *_a, const void *_b) { + const struct cmd_handler *a = _a; + const struct cmd_handler *b = _b; + return strcasecmp(a->command, b->command); +} + +const struct cmd_handler *find_handler(const char *line, + const struct cmd_handler *handlers, size_t handlers_size) { + if (!handlers || !handlers_size) { + return NULL; + } + const struct cmd_handler query = { .command = line }; + return bsearch(&query, handlers, + handlers_size / sizeof(struct cmd_handler), + sizeof(struct cmd_handler), handler_compare); +} + +static const struct cmd_handler *find_handler_ex(char *line, + const struct cmd_handler *config_handlers, size_t config_handlers_size, + const struct cmd_handler *command_handlers, size_t command_handlers_size, + const struct cmd_handler *handlers, size_t handlers_size) { + const struct cmd_handler *handler = NULL; + if (config->reading) { + handler = find_handler(line, config_handlers, config_handlers_size); + } else if (config->active) { + handler = find_handler(line, command_handlers, command_handlers_size); + } + return handler ? handler : find_handler(line, handlers, handlers_size); +} + +static const struct cmd_handler *find_core_handler(char *line) { + return find_handler_ex(line, config_handlers, sizeof(config_handlers), + command_handlers, sizeof(command_handlers), + handlers, sizeof(handlers)); +} + +static void set_config_node(struct sway_node *node, bool node_overridden) { + config->handler_context.node = node; + config->handler_context.container = NULL; + config->handler_context.workspace = NULL; + config->handler_context.node_overridden = node_overridden; + + if (node == NULL) { + return; + } + + switch (node->type) { + case N_CONTAINER: + config->handler_context.container = node->sway_container; + config->handler_context.workspace = node->sway_container->pending.workspace; + break; + case N_WORKSPACE: + config->handler_context.workspace = node->sway_workspace; + break; + case N_ROOT: + case N_OUTPUT: + break; + } +} + +list_t *execute_command(char *_exec, struct sway_seat *seat, + struct sway_container *con) { + char *cmd; + char matched_delim = ';'; + list_t *containers = NULL; + bool using_criteria = false; + + if (seat == NULL) { + // passing a NULL seat means we just pick the default seat + seat = input_manager_get_default_seat(); + if (!sway_assert(seat, "could not find a seat to run the command on")) { + return NULL; + } + } + + char *exec = strdup(_exec); + char *head = exec; + list_t *res_list = create_list(); + + if (!res_list || !exec) { + return NULL; + } + + config->handler_context.seat = seat; + + do { + for (; isspace(*head); ++head) {} + // Extract criteria (valid for this command list only). + if (matched_delim == ';') { + using_criteria = false; + if (*head == '[') { + char *error = NULL; + struct criteria *criteria = criteria_parse(head, &error); + if (!criteria) { + list_add(res_list, + cmd_results_new(CMD_INVALID, "%s", error)); + free(error); + goto cleanup; + } + list_free(containers); + containers = criteria_get_containers(criteria); + head += strlen(criteria->raw); + criteria_destroy(criteria); + using_criteria = true; + // Skip leading whitespace + for (; isspace(*head); ++head) {} + } + } + // Split command list + cmd = argsep(&head, ";,", &matched_delim); + for (; isspace(*cmd); ++cmd) {} + + if (strcmp(cmd, "") == 0) { + sway_log(SWAY_INFO, "Ignoring empty command."); + continue; + } + sway_log(SWAY_INFO, "Handling command '%s'", cmd); + //TODO better handling of argv + int argc; + char **argv = split_args(cmd, &argc); + if (strcmp(argv[0], "exec") != 0 && + strcmp(argv[0], "exec_always") != 0 && + strcmp(argv[0], "mode") != 0) { + for (int i = 1; i < argc; ++i) { + if (*argv[i] == '\"' || *argv[i] == '\'') { + strip_quotes(argv[i]); + } + } + } + const struct cmd_handler *handler = find_core_handler(argv[0]); + if (!handler) { + list_add(res_list, cmd_results_new(CMD_INVALID, + "Unknown/invalid command '%s'", argv[0])); + free_argv(argc, argv); + goto cleanup; + } + + // Var replacement, for all but first argument of set + for (int i = handler->handle == cmd_set ? 2 : 1; i < argc; ++i) { + argv[i] = do_var_replacement(argv[i]); + } + + + if (!using_criteria) { + if (con) { + set_config_node(&con->node, true); + } else { + set_config_node(seat_get_focus_inactive(seat, &root->node), + false); + } + struct cmd_results *res = handler->handle(argc-1, argv+1); + list_add(res_list, res); + if (res->status == CMD_INVALID) { + free_argv(argc, argv); + goto cleanup; + } + } else if (containers->length == 0) { + list_add(res_list, + cmd_results_new(CMD_FAILURE, "No matching node.")); + } else { + struct cmd_results *fail_res = NULL; + for (int i = 0; i < containers->length; ++i) { + struct sway_container *container = containers->items[i]; + set_config_node(&container->node, true); + struct cmd_results *res = handler->handle(argc-1, argv+1); + if (res->status == CMD_SUCCESS) { + free_cmd_results(res); + } else { + // last failure will take precedence + if (fail_res) { + free_cmd_results(fail_res); + } + fail_res = res; + if (res->status == CMD_INVALID) { + list_add(res_list, fail_res); + free_argv(argc, argv); + goto cleanup; + } + } + } + list_add(res_list, + fail_res ? fail_res : cmd_results_new(CMD_SUCCESS, NULL)); + } + free_argv(argc, argv); + } while(head); +cleanup: + free(exec); + list_free(containers); + return res_list; +} + +// this is like execute_command above, except: +// 1) it ignores empty commands (empty lines) +// 2) it does variable substitution +// 3) it doesn't split commands (because the multiple commands are supposed to +// be chained together) +// 4) execute_command handles all state internally while config_command has +// some state handled outside (notably the block mode, in read_config) +struct cmd_results *config_command(char *exec, char **new_block) { + struct cmd_results *results = NULL; + int argc; + char **argv = split_args(exec, &argc); + + // Check for empty lines + if (!argc) { + results = cmd_results_new(CMD_SUCCESS, NULL); + goto cleanup; + } + + // Check for the start of a block + if (argc > 1 && strcmp(argv[argc - 1], "{") == 0) { + *new_block = join_args(argv, argc - 1); + results = cmd_results_new(CMD_BLOCK, NULL); + goto cleanup; + } + + // Check for the end of a block + if (strcmp(argv[argc - 1], "}") == 0) { + results = cmd_results_new(CMD_BLOCK_END, NULL); + goto cleanup; + } + + // Make sure the command is not stored in a variable + if (*argv[0] == '$') { + argv[0] = do_var_replacement(argv[0]); + char *temp = join_args(argv, argc); + free_argv(argc, argv); + argv = split_args(temp, &argc); + free(temp); + if (!argc) { + results = cmd_results_new(CMD_SUCCESS, NULL); + goto cleanup; + } + } + + // Determine the command handler + sway_log(SWAY_INFO, "Config command: %s", exec); + const struct cmd_handler *handler = find_core_handler(argv[0]); + if (!handler || !handler->handle) { + if (handler) { + results = cmd_results_new(CMD_INVALID, + "Command '%s' is shimmed, but unimplemented", argv[0]); + } else { + results = cmd_results_new(CMD_INVALID, + "Unknown/invalid command '%s'", argv[0]); + } + goto cleanup; + } + + // Do variable replacement + if (handler->handle == cmd_set && argc > 1 && *argv[1] == '$') { + // Escape the variable name so it does not get replaced by one shorter + char *temp = calloc(1, strlen(argv[1]) + 2); + temp[0] = '$'; + strcpy(&temp[1], argv[1]); + free(argv[1]); + argv[1] = temp; + } + char *command = do_var_replacement(join_args(argv, argc)); + sway_log(SWAY_INFO, "After replacement: %s", command); + free_argv(argc, argv); + argv = split_args(command, &argc); + free(command); + + // Strip quotes and unescape the string + for (int i = handler->handle == cmd_set ? 2 : 1; i < argc; ++i) { + if (handler->handle != cmd_exec && handler->handle != cmd_exec_always + && handler->handle != cmd_mode + && handler->handle != cmd_bindsym + && handler->handle != cmd_bindcode + && handler->handle != cmd_bindswitch + && handler->handle != cmd_bindgesture + && handler->handle != cmd_set + && handler->handle != cmd_for_window + && (*argv[i] == '\"' || *argv[i] == '\'')) { + strip_quotes(argv[i]); + } + unescape_string(argv[i]); + } + + // Run command + results = handler->handle(argc - 1, argv + 1); + +cleanup: + free_argv(argc, argv); + return results; +} + +struct cmd_results *config_subcommand(char **argv, int argc, + const struct cmd_handler *handlers, size_t handlers_size) { + char *command = join_args(argv, argc); + sway_log(SWAY_DEBUG, "Subcommand: %s", command); + free(command); + + const struct cmd_handler *handler = find_handler(argv[0], handlers, + handlers_size); + if (!handler) { + return cmd_results_new(CMD_INVALID, + "Unknown/invalid command '%s'", argv[0]); + } + if (handler->handle) { + return handler->handle(argc - 1, argv + 1); + } + return cmd_results_new(CMD_INVALID, + "The command '%s' is shimmed, but unimplemented", argv[0]); +} + +struct cmd_results *config_commands_command(char *exec) { + struct cmd_results *results = NULL; + int argc; + char **argv = split_args(exec, &argc); + if (!argc) { + results = cmd_results_new(CMD_SUCCESS, NULL); + goto cleanup; + } + + // Find handler for the command this is setting a policy for + char *cmd = argv[0]; + + if (strcmp(cmd, "}") == 0) { + results = cmd_results_new(CMD_BLOCK_END, NULL); + goto cleanup; + } + + const struct cmd_handler *handler = find_handler(cmd, NULL, 0); + if (!handler && strcmp(cmd, "*") != 0) { + results = cmd_results_new(CMD_INVALID, + "Unknown/invalid command '%s'", cmd); + goto cleanup; + } + + results = cmd_results_new(CMD_SUCCESS, NULL); + +cleanup: + free_argv(argc, argv); + return results; +} + +struct cmd_results *cmd_results_new(enum cmd_status status, + const char *format, ...) { + struct cmd_results *results = malloc(sizeof(struct cmd_results)); + if (!results) { + sway_log(SWAY_ERROR, "Unable to allocate command results"); + return NULL; + } + results->status = status; + if (format) { + va_list args; + va_start(args, format); + results->error = vformat_str(format, args); + va_end(args); + } else { + results->error = NULL; + } + return results; +} + +void free_cmd_results(struct cmd_results *results) { + if (results->error) { + free(results->error); + } + free(results); +} + +char *cmd_results_to_json(list_t *res_list) { + json_object *result_array = json_object_new_array(); + for (int i = 0; i < res_list->length; ++i) { + struct cmd_results *results = res_list->items[i]; + json_object *root = json_object_new_object(); + json_object_object_add(root, "success", + json_object_new_boolean(results->status == CMD_SUCCESS)); + if (results->error) { + json_object_object_add(root, "parse_error", + json_object_new_boolean(results->status == CMD_INVALID)); + json_object_object_add( + root, "error", json_object_new_string(results->error)); + } + json_object_array_add(result_array, root); + } + const char *json = json_object_to_json_string(result_array); + char *res = strdup(json); + json_object_put(result_array); + return res; +} diff --git a/sway/commands/allow_tearing.c b/sway/commands/allow_tearing.c new file mode 100644 index 0000000..ee59413 --- /dev/null +++ b/sway/commands/allow_tearing.c @@ -0,0 +1,24 @@ +#include +#include "sway/config.h" +#include "sway/tree/view.h" +#include "util.h" + +struct cmd_results *cmd_allow_tearing(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "allow_tearing", EXPECTED_AT_LEAST, 1))) { + return error; + } + + struct sway_container *container = config->handler_context.container; + if (!container || !container->view) { + return cmd_results_new(CMD_INVALID, "Tearing can only be allowed on views"); + } + + bool wants_tearing = parse_boolean(argv[0], true); + + struct sway_view *view = container->view; + view->tearing_mode = wants_tearing ? TEARING_OVERRIDE_TRUE : + TEARING_OVERRIDE_FALSE; + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/animation_duration_ms.c b/sway/commands/animation_duration_ms.c new file mode 100644 index 0000000..a116d62 --- /dev/null +++ b/sway/commands/animation_duration_ms.c @@ -0,0 +1,27 @@ +#include +#include +#include +#include "sway/animation_manager.h" +#include "sway/commands.h" + +struct cmd_results *cmd_animation_duration_ms(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "animation_duration_ms", EXPECTED_AT_LEAST, 1))) { + return error; + } + + char *err; + float val = strtof(argc == 1 ? argv[0] : argv[1], &err); + if (*err) { + return cmd_results_new(CMD_INVALID, "animation_duration_ms float invalid"); + } + + if (val < 0 || val > 5000) { // surely no one wants an animation longer than 5 seconds + return cmd_results_new(CMD_FAILURE, "animation_duration_ms value out of bounds"); + } + + config->animation_duration_ms = val; + refresh_animation_manager_timing(); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/assign.c b/sway/commands/assign.c new file mode 100644 index 0000000..5bcbb16 --- /dev/null +++ b/sway/commands/assign.c @@ -0,0 +1,62 @@ +#include +#include +#include "sway/commands.h" +#include "sway/criteria.h" +#include "list.h" +#include "log.h" +#include "stringop.h" + +struct cmd_results *cmd_assign(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "assign", EXPECTED_AT_LEAST, 2))) { + return error; + } + + // Create criteria + char *err_str = NULL; + struct criteria *criteria = criteria_parse(argv[0], &err_str); + if (!criteria) { + error = cmd_results_new(CMD_INVALID, "%s", err_str); + free(err_str); + return error; + } + + --argc; ++argv; + + if (has_prefix(*argv, "→")) { + if (argc < 2) { + free(criteria); + return cmd_results_new(CMD_INVALID, "Missing workspace"); + } + --argc; + ++argv; + } + + if (strcmp(*argv, "output") == 0) { + criteria->type = CT_ASSIGN_OUTPUT; + --argc; ++argv; + } else { + if (strcmp(*argv, "workspace") == 0) { + --argc; ++argv; + } + if (strcmp(*argv, "number") == 0) { + --argc; ++argv; + if (argv[0][0] < '0' || argv[0][0] > '9') { + free(criteria); + return cmd_results_new(CMD_INVALID, + "Invalid workspace number '%s'", argv[0]); + } + criteria->type = CT_ASSIGN_WORKSPACE_NUMBER; + } else { + criteria->type = CT_ASSIGN_WORKSPACE; + } + } + + criteria->target = join_args(argv, argc); + + list_add(config->criteria, criteria); + sway_log(SWAY_DEBUG, "assign: '%s' -> '%s' added", criteria->raw, + criteria->target); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/bar.c b/sway/commands/bar.c new file mode 100644 index 0000000..635e895 --- /dev/null +++ b/sway/commands/bar.c @@ -0,0 +1,135 @@ +#include +#include +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/ipc-server.h" +#include "log.h" + +// Must be in alphabetical order for bsearch +static const struct cmd_handler bar_handlers[] = { + { "bindcode", bar_cmd_bindcode }, + { "binding_mode_indicator", bar_cmd_binding_mode_indicator }, + { "bindsym", bar_cmd_bindsym }, + { "colors", bar_cmd_colors }, + { "font", bar_cmd_font }, + { "gaps", bar_cmd_gaps }, + { "height", bar_cmd_height }, + { "hidden_state", bar_cmd_hidden_state }, + { "icon_theme", bar_cmd_icon_theme }, + { "mode", bar_cmd_mode }, + { "modifier", bar_cmd_modifier }, + { "output", bar_cmd_output }, + { "pango_markup", bar_cmd_pango_markup }, + { "position", bar_cmd_position }, + { "separator_symbol", bar_cmd_separator_symbol }, + { "status_command", bar_cmd_status_command }, + { "status_edge_padding", bar_cmd_status_edge_padding }, + { "status_padding", bar_cmd_status_padding }, + { "strip_workspace_name", bar_cmd_strip_workspace_name }, + { "strip_workspace_numbers", bar_cmd_strip_workspace_numbers }, + { "tray_bindcode", bar_cmd_tray_bindcode }, + { "tray_bindsym", bar_cmd_tray_bindsym }, + { "tray_output", bar_cmd_tray_output }, + { "tray_padding", bar_cmd_tray_padding }, + { "unbindcode", bar_cmd_unbindcode }, + { "unbindsym", bar_cmd_unbindsym }, + { "workspace_buttons", bar_cmd_workspace_buttons }, + { "workspace_min_width", bar_cmd_workspace_min_width }, + { "wrap_scroll", bar_cmd_wrap_scroll }, +}; + +// Must be in alphabetical order for bsearch +static const struct cmd_handler bar_config_handlers[] = { + { "id", bar_cmd_id }, + { "swaybar_command", bar_cmd_swaybar_command }, +}; + +// Determines whether the subcommand is valid in any bar handler struct +static bool is_subcommand(char *name) { + return find_handler(name, bar_handlers, sizeof(bar_handlers)) || + find_handler(name, bar_config_handlers, sizeof(bar_config_handlers)); +} + +struct cmd_results *cmd_bar(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "bar", EXPECTED_AT_LEAST, 2))) { + return error; + } + + char *id = NULL; + if (strcmp(argv[0], "id") != 0 && is_subcommand(argv[1])) { + for (int i = 0; i < config->bars->length; ++i) { + struct bar_config *item = config->bars->items[i]; + if (strcmp(item->id, argv[0]) == 0) { + sway_log(SWAY_DEBUG, "Selecting bar: %s", argv[0]); + config->current_bar = item; + break; + } + } + if (!config->current_bar) { + id = strdup(argv[0]); + } + ++argv; --argc; + } else if (config->reading && !config->current_bar) { + id = format_str("bar-%d", config->bars->length); + if (!id) { + return cmd_results_new(CMD_FAILURE, "Unable to allocate bar id"); + } + } else if (!config->reading && strcmp(argv[0], "mode") != 0 && + strcmp(argv[0], "hidden_state") != 0) { + if (is_subcommand(argv[0])) { + return cmd_results_new(CMD_INVALID, "No bar defined."); + } else { + return cmd_results_new(CMD_INVALID, + "Unknown/invalid command '%s'", argv[1]); + } + } + + if (id) { + sway_log(SWAY_DEBUG, "Creating bar: %s", id); + config->current_bar = default_bar_config(); + if (!config->current_bar) { + free(id); + return cmd_results_new(CMD_FAILURE, "Unable to allocate bar config"); + } + config->current_bar->id = id; + } + + struct cmd_results *res = NULL; + if (find_handler(argv[0], bar_config_handlers, + sizeof(bar_config_handlers))) { + if (config->reading) { + res = config_subcommand(argv, argc, bar_config_handlers, + sizeof(bar_config_handlers)); + } else { + res = cmd_results_new(CMD_INVALID, + "Can only be used in the config file"); + } + } else { + res = config_subcommand(argv, argc, bar_handlers, sizeof(bar_handlers)); + } + + if (res && res->status != CMD_SUCCESS) { + if (id) { + free_bar_config(config->current_bar); + config->current_bar = NULL; + id = NULL; + } + return res; + } + + if (id) { + list_add(config->bars, config->current_bar); + } + + if (!config->reading && config->current_bar) { + ipc_event_barconfig_update(config->current_bar); + if (id) { + load_swaybar(config->current_bar); + } + config->current_bar = NULL; + } + + return res; +} diff --git a/sway/commands/bar/bind.c b/sway/commands/bar/bind.c new file mode 100644 index 0000000..8a837e3 --- /dev/null +++ b/sway/commands/bar/bind.c @@ -0,0 +1,129 @@ +#include +#include +#include +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/input/cursor.h" +#include "list.h" +#include "log.h" +#include "stringop.h" + +static struct cmd_results *binding_add(struct bar_binding *binding, + list_t *mode_bindings) { + const char *name = get_mouse_button_name(binding->button); + bool overwritten = false; + for (int i = 0; i < mode_bindings->length; i++) { + struct bar_binding *other = mode_bindings->items[i]; + if (other->button == binding->button && + other->release == binding->release) { + overwritten = true; + mode_bindings->items[i] = binding; + free_bar_binding(other); + sway_log(SWAY_DEBUG, "[bar %s] Updated binding for %u (%s)%s", + config->current_bar->id, binding->button, name, + binding->release ? " - release" : ""); + break; + } + } + if (!overwritten) { + list_add(mode_bindings, binding); + sway_log(SWAY_DEBUG, "[bar %s] Added binding for %u (%s)%s", + config->current_bar->id, binding->button, name, + binding->release ? " - release" : ""); + } + return cmd_results_new(CMD_SUCCESS, NULL); +} + +static struct cmd_results *binding_remove(struct bar_binding *binding, + list_t *mode_bindings) { + const char *name = get_mouse_button_name(binding->button); + for (int i = 0; i < mode_bindings->length; i++) { + struct bar_binding *other = mode_bindings->items[i]; + if (other->button == binding->button && + other->release == binding->release) { + sway_log(SWAY_DEBUG, "[bar %s] Unbound binding for %u (%s)%s", + config->current_bar->id, binding->button, name, + binding->release ? " - release" : ""); + free_bar_binding(other); + free_bar_binding(binding); + list_del(mode_bindings, i); + return cmd_results_new(CMD_SUCCESS, NULL); + } + } + + struct cmd_results *error = cmd_results_new(CMD_FAILURE, "Could not " + "find binding for [bar %s]" " Button %u (%s)%s", + config->current_bar->id, binding->button, name, + binding->release ? " - release" : ""); + free_bar_binding(binding); + return error; +} + +static struct cmd_results *bar_cmd_bind(int argc, char **argv, bool code, + bool unbind) { + int minargs = 2; + const char *command; + if (unbind) { + minargs--; + command = code ? "bar unbindcode" : "bar unbindsym"; + } else { + command = code ? "bar bindcode" : "bar bindsym"; + } + + struct cmd_results *error = NULL; + if ((error = checkarg(argc, command, EXPECTED_AT_LEAST, minargs))) { + return error; + } + + struct bar_binding *binding = calloc(1, sizeof(struct bar_binding)); + if (!binding) { + return cmd_results_new(CMD_FAILURE, "Unable to allocate bar binding"); + } + + binding->release = false; + if (strcmp("--release", argv[0]) == 0) { + binding->release = true; + argv++; + argc--; + } + + char *message = NULL; + if (code) { + binding->button = get_mouse_bindcode(argv[0], &message); + } else { + binding->button = get_mouse_bindsym(argv[0], &message); + } + if (message) { + free_bar_binding(binding); + error = cmd_results_new(CMD_INVALID, "%s", message); + free(message); + return error; + } else if (!binding->button) { + free_bar_binding(binding); + return cmd_results_new(CMD_INVALID, "Unknown button %s", argv[0]); + } + list_t *bindings = config->current_bar->bindings; + if (unbind) { + return binding_remove(binding, bindings); + } + + binding->command = join_args(argv + 1, argc - 1); + return binding_add(binding, bindings); +} + +struct cmd_results *bar_cmd_bindcode(int argc, char **argv) { + return bar_cmd_bind(argc, argv, true, false); +} + +struct cmd_results *bar_cmd_bindsym(int argc, char **argv) { + return bar_cmd_bind(argc, argv, false, false); +} + +struct cmd_results *bar_cmd_unbindcode(int argc, char **argv) { + return bar_cmd_bind(argc, argv, true, true); +} + +struct cmd_results *bar_cmd_unbindsym(int argc, char **argv) { + return bar_cmd_bind(argc, argv, false, true); +} diff --git a/sway/commands/bar/binding_mode_indicator.c b/sway/commands/bar/binding_mode_indicator.c new file mode 100644 index 0000000..b58d8a8 --- /dev/null +++ b/sway/commands/bar/binding_mode_indicator.c @@ -0,0 +1,23 @@ +#include +#include +#include "sway/commands.h" +#include "log.h" +#include "util.h" + +struct cmd_results *bar_cmd_binding_mode_indicator(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, + "binding_mode_indicator", EXPECTED_EQUAL_TO, 1))) { + return error; + } + config->current_bar->binding_mode_indicator = + parse_boolean(argv[0], config->current_bar->binding_mode_indicator); + if (config->current_bar->binding_mode_indicator) { + sway_log(SWAY_DEBUG, "Enabling binding mode indicator on bar: %s", + config->current_bar->id); + } else { + sway_log(SWAY_DEBUG, "Disabling binding mode indicator on bar: %s", + config->current_bar->id); + } + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/bar/colors.c b/sway/commands/bar/colors.c new file mode 100644 index 0000000..da606c1 --- /dev/null +++ b/sway/commands/bar/colors.c @@ -0,0 +1,156 @@ +#include +#include "sway/commands.h" +#include "log.h" +#include "util.h" + +// Must be in alphabetical order for bsearch +static const struct cmd_handler bar_colors_handlers[] = { + { "active_workspace", bar_colors_cmd_active_workspace }, + { "background", bar_colors_cmd_background }, + { "binding_mode", bar_colors_cmd_binding_mode }, + { "focused_background", bar_colors_cmd_focused_background }, + { "focused_separator", bar_colors_cmd_focused_separator }, + { "focused_statusline", bar_colors_cmd_focused_statusline }, + { "focused_workspace", bar_colors_cmd_focused_workspace }, + { "inactive_workspace", bar_colors_cmd_inactive_workspace }, + { "separator", bar_colors_cmd_separator }, + { "statusline", bar_colors_cmd_statusline }, + { "urgent_workspace", bar_colors_cmd_urgent_workspace }, +}; + +static char *hex_to_rgba_hex(const char *hex) { + uint32_t color; + if (!parse_color(hex, &color)) { + return NULL; + } + char *rgba = malloc(10); + if (!rgba) { + return NULL; + } + snprintf(rgba, 10, "#%08x", color); + return rgba; +} + +static struct cmd_results *parse_single_color(char **color, + const char *cmd_name, int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, cmd_name, EXPECTED_EQUAL_TO, 1))) { + return error; + } + + char *rgba = hex_to_rgba_hex(argv[0]); + if (!rgba) { + return cmd_results_new(CMD_INVALID, "Invalid color: %s", argv[0]); + } + + free(*color); + *color = rgba; + return cmd_results_new(CMD_SUCCESS, NULL); +} + +static struct cmd_results *parse_three_colors(char ***colors, + const char *cmd_name, int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, cmd_name, EXPECTED_EQUAL_TO, 3))) { + return error; + } + + char *rgba[3] = {0}; + for (int i = 0; i < 3; i++) { + rgba[i] = hex_to_rgba_hex(argv[i]); + if (!rgba[i]) { + return cmd_results_new(CMD_INVALID, "Invalid color: %s", argv[i]); + } + } + + for (int i = 0; i < 3; i++) { + free(*colors[i]); + *colors[i] = rgba[i]; + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} + +struct cmd_results *bar_cmd_colors(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "colors", EXPECTED_AT_LEAST, 1))) { + return error; + } + return config_subcommand(argv, argc, bar_colors_handlers, + sizeof(bar_colors_handlers)); +} + +struct cmd_results *bar_colors_cmd_active_workspace(int argc, char **argv) { + char **colors[3] = { + &(config->current_bar->colors.active_workspace_border), + &(config->current_bar->colors.active_workspace_bg), + &(config->current_bar->colors.active_workspace_text) + }; + return parse_three_colors(colors, "active_workspace", argc, argv); +} + +struct cmd_results *bar_colors_cmd_background(int argc, char **argv) { + return parse_single_color(&(config->current_bar->colors.background), + "background", argc, argv); +} + +struct cmd_results *bar_colors_cmd_focused_background(int argc, char **argv) { + return parse_single_color(&(config->current_bar->colors.focused_background), + "focused_background", argc, argv); +} + +struct cmd_results *bar_colors_cmd_binding_mode(int argc, char **argv) { + char **colors[3] = { + &(config->current_bar->colors.binding_mode_border), + &(config->current_bar->colors.binding_mode_bg), + &(config->current_bar->colors.binding_mode_text) + }; + return parse_three_colors(colors, "binding_mode", argc, argv); +} + +struct cmd_results *bar_colors_cmd_focused_workspace(int argc, char **argv) { + char **colors[3] = { + &(config->current_bar->colors.focused_workspace_border), + &(config->current_bar->colors.focused_workspace_bg), + &(config->current_bar->colors.focused_workspace_text) + }; + return parse_three_colors(colors, "focused_workspace", argc, argv); +} + +struct cmd_results *bar_colors_cmd_inactive_workspace(int argc, char **argv) { + char **colors[3] = { + &(config->current_bar->colors.inactive_workspace_border), + &(config->current_bar->colors.inactive_workspace_bg), + &(config->current_bar->colors.inactive_workspace_text) + }; + return parse_three_colors(colors, "inactive_workspace", argc, argv); +} + +struct cmd_results *bar_colors_cmd_separator(int argc, char **argv) { + return parse_single_color(&(config->current_bar->colors.separator), + "separator", argc, argv); +} + +struct cmd_results *bar_colors_cmd_focused_separator(int argc, char **argv) { + return parse_single_color(&(config->current_bar->colors.focused_separator), + "focused_separator", argc, argv); +} + +struct cmd_results *bar_colors_cmd_statusline(int argc, char **argv) { + return parse_single_color(&(config->current_bar->colors.statusline), + "statusline", argc, argv); +} + +struct cmd_results *bar_colors_cmd_focused_statusline(int argc, char **argv) { + return parse_single_color(&(config->current_bar->colors.focused_statusline), + "focused_statusline", argc, argv); +} + +struct cmd_results *bar_colors_cmd_urgent_workspace(int argc, char **argv) { + char **colors[3] = { + &(config->current_bar->colors.urgent_workspace_border), + &(config->current_bar->colors.urgent_workspace_bg), + &(config->current_bar->colors.urgent_workspace_text) + }; + return parse_three_colors(colors, "urgent_workspace", argc, argv); +} diff --git a/sway/commands/bar/font.c b/sway/commands/bar/font.c new file mode 100644 index 0000000..51ca20e --- /dev/null +++ b/sway/commands/bar/font.c @@ -0,0 +1,30 @@ +#include +#include "sway/commands.h" +#include "log.h" +#include "stringop.h" + +struct cmd_results *bar_cmd_font(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "font", EXPECTED_AT_LEAST, 1))) { + return error; + } + char *font = join_args(argv, argc); + free(config->current_bar->font); + + if (has_prefix(font, "pango:")) { + if (config->current_bar->pango_markup == PANGO_MARKUP_DEFAULT) { + config->current_bar->pango_markup = true; + } + config->current_bar->font = strdup(font + 6); + } else { + if (config->current_bar->pango_markup == PANGO_MARKUP_DEFAULT) { + config->current_bar->pango_markup = false; + } + config->current_bar->font = strdup(font); + } + + free(font); + sway_log(SWAY_DEBUG, "Settings font '%s' for bar: %s", + config->current_bar->font, config->current_bar->id); + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/bar/gaps.c b/sway/commands/bar/gaps.c new file mode 100644 index 0000000..9fa3e75 --- /dev/null +++ b/sway/commands/bar/gaps.c @@ -0,0 +1,57 @@ +#include +#include +#include +#include "sway/commands.h" +#include "sway/ipc-server.h" +#include "log.h" + +struct cmd_results *bar_cmd_gaps(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "gaps", EXPECTED_AT_LEAST, 1))) { + return error; + } + if ((error = checkarg(argc, "gaps", EXPECTED_AT_MOST, 4))) { + return error; + } + + int top = 0, right = 0, bottom = 0, left = 0; + + for (int i = 0; i < argc; i++) { + char *end; + int amount = strtol(argv[i], &end, 10); + if (strlen(end) && strcasecmp(end, "px") != 0) { + return cmd_results_new(CMD_INVALID, + "Expected 'bar [] gaps | " + " | '"); + } + + if (i == 0) { + top = amount; + } + if (i == 0 || i == 1) { + right = amount; + } + if (i == 0 || i == 2) { + bottom = amount; + } + if (i == 0 || i == 1 || i == 3) { + left = amount; + } + } + + config->current_bar->gaps.top = top; + config->current_bar->gaps.right = right; + config->current_bar->gaps.bottom = bottom; + config->current_bar->gaps.left = left; + + sway_log(SWAY_DEBUG, "Setting bar gaps to %d %d %d %d on bar: %s", + config->current_bar->gaps.top, config->current_bar->gaps.right, + config->current_bar->gaps.bottom, config->current_bar->gaps.left, + config->current_bar->id); + + if (!config->reading) { + ipc_event_barconfig_update(config->current_bar); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/bar/height.c b/sway/commands/bar/height.c new file mode 100644 index 0000000..945eb70 --- /dev/null +++ b/sway/commands/bar/height.c @@ -0,0 +1,20 @@ +#include +#include +#include "sway/commands.h" +#include "log.h" + +struct cmd_results *bar_cmd_height(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "height", EXPECTED_EQUAL_TO, 1))) { + return error; + } + int height = atoi(argv[0]); + if (height < 0) { + return cmd_results_new(CMD_INVALID, + "Invalid height value: %s", argv[0]); + } + config->current_bar->height = height; + sway_log(SWAY_DEBUG, "Setting bar height to %d on bar: %s", + height, config->current_bar->id); + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/bar/hidden_state.c b/sway/commands/bar/hidden_state.c new file mode 100644 index 0000000..7b38831 --- /dev/null +++ b/sway/commands/bar/hidden_state.c @@ -0,0 +1,73 @@ +#include +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/ipc-server.h" +#include "log.h" + +static struct cmd_results *bar_set_hidden_state(struct bar_config *bar, + const char *hidden_state) { + char *old_state = bar->hidden_state; + if (strcasecmp("toggle", hidden_state) == 0 && !config->reading) { + if (strcasecmp("hide", bar->hidden_state) == 0) { + bar->hidden_state = strdup("show"); + } else if (strcasecmp("show", bar->hidden_state) == 0) { + bar->hidden_state = strdup("hide"); + } + } else if (strcasecmp("hide", hidden_state) == 0) { + bar->hidden_state = strdup("hide"); + } else if (strcasecmp("show", hidden_state) == 0) { + bar->hidden_state = strdup("show"); + } else { + return cmd_results_new(CMD_INVALID, "Invalid value %s", hidden_state); + } + if (strcmp(old_state, bar->hidden_state) != 0) { + if (!config->current_bar) { + ipc_event_barconfig_update(bar); + } + sway_log(SWAY_DEBUG, "Setting hidden_state: '%s' for bar: %s", + bar->hidden_state, bar->id); + } + // free old mode + free(old_state); + return NULL; +} + +struct cmd_results *bar_cmd_hidden_state(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "hidden_state", EXPECTED_AT_LEAST, 1))) { + return error; + } + if ((error = checkarg(argc, "hidden_state", EXPECTED_AT_MOST, 2))) { + return error; + } + if (config->reading && argc > 1) { + return cmd_results_new(CMD_INVALID, + "Unexpected value %s in config mode", argv[1]); + } + + if (config->current_bar && argc == 2 && + strcmp(config->current_bar->id, argv[1]) != 0) { + return cmd_results_new(CMD_INVALID, "Conflicting bar ids: %s and %s", + config->current_bar->id, argv[1]); + } + + const char *state = argv[0]; + if (config->current_bar) { + error = bar_set_hidden_state(config->current_bar, state); + } else { + const char *id = argc == 2 ? argv[1] : NULL; + for (int i = 0; i < config->bars->length; ++i) { + struct bar_config *bar = config->bars->items[i]; + if (id) { + if (strcmp(id, bar->id) == 0) { + error = bar_set_hidden_state(bar, state); + break; + } + } else if ((error = bar_set_hidden_state(bar, state))) { + break; + } + } + } + return error ? error : cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/bar/icon_theme.c b/sway/commands/bar/icon_theme.c new file mode 100644 index 0000000..fee2170 --- /dev/null +++ b/sway/commands/bar/icon_theme.c @@ -0,0 +1,23 @@ +#include +#include "config.h" +#include "sway/commands.h" +#include "sway/config.h" +#include "log.h" + +struct cmd_results *bar_cmd_icon_theme(int argc, char **argv) { +#if HAVE_TRAY + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "icon_theme", EXPECTED_EQUAL_TO, 1))) { + return error; + } + + sway_log(SWAY_DEBUG, "[Bar %s] Setting icon theme to %s", + config->current_bar->id, argv[0]); + free(config->current_bar->icon_theme); + config->current_bar->icon_theme = strdup(argv[0]); + return cmd_results_new(CMD_SUCCESS, NULL); +#else + return cmd_results_new(CMD_INVALID, + "Sway has been compiled without tray support"); +#endif +} diff --git a/sway/commands/bar/id.c b/sway/commands/bar/id.c new file mode 100644 index 0000000..46cf4ca --- /dev/null +++ b/sway/commands/bar/id.c @@ -0,0 +1,34 @@ +#include +#include "sway/commands.h" +#include "log.h" + +struct cmd_results *bar_cmd_id(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "id", EXPECTED_EQUAL_TO, 1))) { + return error; + } + + const char *name = argv[0]; + const char *oldname = config->current_bar->id; + if (strcmp(name, oldname) == 0) { + return cmd_results_new(CMD_SUCCESS, NULL); // NOP + } else if (strcmp(name, "id") == 0) { + return cmd_results_new(CMD_INVALID, "id cannot be 'id'"); + } + // check if id is used by a previously defined bar + for (int i = 0; i < config->bars->length; ++i) { + struct bar_config *find = config->bars->items[i]; + if (strcmp(name, find->id) == 0 && config->current_bar != find) { + return cmd_results_new(CMD_FAILURE, + "Id '%s' already defined for another bar. Id unchanged (%s).", + name, oldname); + } + } + + sway_log(SWAY_DEBUG, "Renaming bar: '%s' to '%s'", oldname, name); + + // free old bar id + free(config->current_bar->id); + config->current_bar->id = strdup(name); + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/bar/mode.c b/sway/commands/bar/mode.c new file mode 100644 index 0000000..d69e910 --- /dev/null +++ b/sway/commands/bar/mode.c @@ -0,0 +1,77 @@ +#include +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/ipc-server.h" +#include "log.h" + +static struct cmd_results *bar_set_mode(struct bar_config *bar, const char *mode) { + char *old_mode = bar->mode; + if (strcasecmp("toggle", mode) == 0 && !config->reading) { + if (strcasecmp("dock", bar->mode) == 0) { + bar->mode = strdup("hide"); + } else{ + bar->mode = strdup("dock"); + } + } else if (strcasecmp("dock", mode) == 0) { + bar->mode = strdup("dock"); + } else if (strcasecmp("hide", mode) == 0) { + bar->mode = strdup("hide"); + } else if (strcasecmp("invisible", mode) == 0) { + bar->mode = strdup("invisible"); + } else if (strcasecmp("overlay", mode) == 0) { + bar->mode = strdup("overlay"); + } else { + return cmd_results_new(CMD_INVALID, "Invalid value %s", mode); + } + + if (strcmp(old_mode, bar->mode) != 0) { + if (!config->current_bar) { + ipc_event_barconfig_update(bar); + } + sway_log(SWAY_DEBUG, "Setting mode: '%s' for bar: %s", bar->mode, bar->id); + } + + // free old mode + free(old_mode); + return NULL; +} + +struct cmd_results *bar_cmd_mode(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "mode", EXPECTED_AT_LEAST, 1))) { + return error; + } + if ((error = checkarg(argc, "mode", EXPECTED_AT_MOST, 2))) { + return error; + } + if (config->reading && argc > 1) { + return cmd_results_new(CMD_INVALID, + "Unexpected value %s in config mode", argv[1]); + } + + if (config->current_bar && argc == 2 && + strcmp(config->current_bar->id, argv[1]) != 0) { + return cmd_results_new(CMD_INVALID, "Conflicting bar ids: %s and %s", + config->current_bar->id, argv[1]); + } + + const char *mode = argv[0]; + if (config->current_bar) { + error = bar_set_mode(config->current_bar, mode); + } else { + const char *id = argc == 2 ? argv[1] : NULL; + for (int i = 0; i < config->bars->length; ++i) { + struct bar_config *bar = config->bars->items[i]; + if (id) { + if (strcmp(id, bar->id) == 0) { + error = bar_set_mode(bar, mode); + break; + } + } else if ((error = bar_set_mode(bar, mode))) { + break; + } + } + } + return error ? error : cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/bar/modifier.c b/sway/commands/bar/modifier.c new file mode 100644 index 0000000..983d217 --- /dev/null +++ b/sway/commands/bar/modifier.c @@ -0,0 +1,38 @@ +#include +#include "sway/commands.h" +#include "sway/input/keyboard.h" +#include "log.h" +#include "stringop.h" + +struct cmd_results *bar_cmd_modifier(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "modifier", EXPECTED_EQUAL_TO, 1))) { + return error; + } + + uint32_t mod = 0; + if (strcmp(argv[0], "none") != 0) { + list_t *split = split_string(argv[0], "+"); + for (int i = 0; i < split->length; ++i) { + uint32_t tmp_mod; + if ((tmp_mod = get_modifier_mask_by_name(split->items[i])) > 0) { + mod |= tmp_mod; + } else if (strcmp(split->items[i], "none") == 0) { + error = cmd_results_new(CMD_INVALID, + "none cannot be used along with other modifiers"); + list_free_items_and_destroy(split); + return error; + } else { + error = cmd_results_new(CMD_INVALID, + "Unknown modifier '%s'", (char *)split->items[i]); + list_free_items_and_destroy(split); + return error; + } + } + list_free_items_and_destroy(split); + } + config->current_bar->modifier = mod; + sway_log(SWAY_DEBUG, + "Show/Hide the bar when pressing '%s' in hide mode.", argv[0]); + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/bar/output.c b/sway/commands/bar/output.c new file mode 100644 index 0000000..5173017 --- /dev/null +++ b/sway/commands/bar/output.c @@ -0,0 +1,48 @@ +#include +#include +#include "sway/commands.h" +#include "list.h" +#include "log.h" + +struct cmd_results *bar_cmd_output(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "output", EXPECTED_EQUAL_TO, 1))) { + return error; + } + + const char *output = argv[0]; + list_t *outputs = config->current_bar->outputs; + if (!outputs) { + outputs = create_list(); + config->current_bar->outputs = outputs; + } + + bool add_output = true; + if (strcmp("*", output) == 0) { + // remove all previous defined outputs and replace with '*' + while (outputs->length) { + free(outputs->items[0]); + list_del(outputs, 0); + } + } else { + // only add output if not already defined, if the list has '*', remove + // it, in favor of a manual list + for (int i = 0; i < outputs->length; ++i) { + const char *find = outputs->items[i]; + if (strcmp("*", find) == 0) { + free(outputs->items[i]); + list_del(outputs, i); + } else if (strcmp(output, find) == 0) { + add_output = false; + break; + } + } + } + + if (add_output) { + list_add(outputs, strdup(output)); + sway_log(SWAY_DEBUG, "Adding bar: '%s' to output '%s'", + config->current_bar->id, output); + } + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/bar/pango_markup.c b/sway/commands/bar/pango_markup.c new file mode 100644 index 0000000..ee51390 --- /dev/null +++ b/sway/commands/bar/pango_markup.c @@ -0,0 +1,22 @@ +#include +#include +#include "sway/commands.h" +#include "log.h" +#include "util.h" + +struct cmd_results *bar_cmd_pango_markup(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "pango_markup", EXPECTED_EQUAL_TO, 1))) { + return error; + } + config->current_bar->pango_markup = + parse_boolean(argv[0], config->current_bar->pango_markup); + if (config->current_bar->pango_markup) { + sway_log(SWAY_DEBUG, "Enabling pango markup for bar: %s", + config->current_bar->id); + } else { + sway_log(SWAY_DEBUG, "Disabling pango markup for bar: %s", + config->current_bar->id); + } + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/bar/position.c b/sway/commands/bar/position.c new file mode 100644 index 0000000..94f530e --- /dev/null +++ b/sway/commands/bar/position.c @@ -0,0 +1,22 @@ +#include +#include +#include "sway/commands.h" +#include "log.h" + +struct cmd_results *bar_cmd_position(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "position", EXPECTED_EQUAL_TO, 1))) { + return error; + } + char *valid[] = { "top", "bottom" }; + for (size_t i = 0; i < sizeof(valid) / sizeof(valid[0]); ++i) { + if (strcasecmp(valid[i], argv[0]) == 0) { + sway_log(SWAY_DEBUG, "Setting bar position '%s' for bar: %s", + argv[0], config->current_bar->id); + free(config->current_bar->position); + config->current_bar->position = strdup(argv[0]); + return cmd_results_new(CMD_SUCCESS, NULL); + } + } + return cmd_results_new(CMD_INVALID, "Invalid value %s", argv[0]); +} diff --git a/sway/commands/bar/separator_symbol.c b/sway/commands/bar/separator_symbol.c new file mode 100644 index 0000000..50e9a87 --- /dev/null +++ b/sway/commands/bar/separator_symbol.c @@ -0,0 +1,15 @@ +#include +#include "sway/commands.h" +#include "log.h" + +struct cmd_results *bar_cmd_separator_symbol(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "separator_symbol", EXPECTED_EQUAL_TO, 1))) { + return error; + } + free(config->current_bar->separator_symbol); + config->current_bar->separator_symbol = strdup(argv[0]); + sway_log(SWAY_DEBUG, "Settings separator_symbol '%s' for bar: %s", + config->current_bar->separator_symbol, config->current_bar->id); + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/bar/status_command.c b/sway/commands/bar/status_command.c new file mode 100644 index 0000000..bb92e8e --- /dev/null +++ b/sway/commands/bar/status_command.c @@ -0,0 +1,23 @@ +#include +#include "sway/commands.h" +#include "log.h" +#include "stringop.h" + +struct cmd_results *bar_cmd_status_command(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "status_command", EXPECTED_AT_LEAST, 1))) { + return error; + } + free(config->current_bar->status_command); + config->current_bar->status_command = NULL; + + char *new_command = join_args(argv, argc); + if (strcmp(new_command, "-") != 0) { + config->current_bar->status_command = new_command; + sway_log(SWAY_DEBUG, "Feeding bar with status command: %s", + config->current_bar->status_command); + } else { + free(new_command); + } + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/bar/status_edge_padding.c b/sway/commands/bar/status_edge_padding.c new file mode 100644 index 0000000..75ef86f --- /dev/null +++ b/sway/commands/bar/status_edge_padding.c @@ -0,0 +1,21 @@ +#include +#include +#include "sway/commands.h" +#include "log.h" + +struct cmd_results *bar_cmd_status_edge_padding(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "status_edge_padding", EXPECTED_EQUAL_TO, 1))) { + return error; + } + char *end; + int padding = strtol(argv[0], &end, 10); + if (strlen(end) || padding < 0) { + return cmd_results_new(CMD_INVALID, + "Padding must be a positive integer"); + } + config->current_bar->status_edge_padding = padding; + sway_log(SWAY_DEBUG, "Status edge padding on bar %s: %d", + config->current_bar->id, config->current_bar->status_edge_padding); + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/bar/status_padding.c b/sway/commands/bar/status_padding.c new file mode 100644 index 0000000..0c65e51 --- /dev/null +++ b/sway/commands/bar/status_padding.c @@ -0,0 +1,21 @@ +#include +#include +#include "sway/commands.h" +#include "log.h" + +struct cmd_results *bar_cmd_status_padding(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "status_padding", EXPECTED_EQUAL_TO, 1))) { + return error; + } + char *end; + int padding = strtol(argv[0], &end, 10); + if (strlen(end) || padding < 0) { + return cmd_results_new(CMD_INVALID, + "Padding must be a positive integer"); + } + config->current_bar->status_padding = padding; + sway_log(SWAY_DEBUG, "Status padding on bar %s: %d", + config->current_bar->id, config->current_bar->status_padding); + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/bar/strip_workspace_name.c b/sway/commands/bar/strip_workspace_name.c new file mode 100644 index 0000000..764321a --- /dev/null +++ b/sway/commands/bar/strip_workspace_name.c @@ -0,0 +1,28 @@ +#include +#include +#include "sway/commands.h" +#include "log.h" +#include "util.h" + +struct cmd_results *bar_cmd_strip_workspace_name(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, + "strip_workspace_name", EXPECTED_EQUAL_TO, 1))) { + return error; + } + + config->current_bar->strip_workspace_name = + parse_boolean(argv[0], config->current_bar->strip_workspace_name); + + if (config->current_bar->strip_workspace_name) { + config->current_bar->strip_workspace_numbers = false; + + sway_log(SWAY_DEBUG, "Stripping workspace name on bar: %s", + config->current_bar->id); + } else { + sway_log(SWAY_DEBUG, "Enabling workspace name on bar: %s", + config->current_bar->id); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/bar/strip_workspace_numbers.c b/sway/commands/bar/strip_workspace_numbers.c new file mode 100644 index 0000000..2d7fe1a --- /dev/null +++ b/sway/commands/bar/strip_workspace_numbers.c @@ -0,0 +1,28 @@ +#include +#include +#include "sway/commands.h" +#include "log.h" +#include "util.h" + +struct cmd_results *bar_cmd_strip_workspace_numbers(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, + "strip_workspace_numbers", EXPECTED_EQUAL_TO, 1))) { + return error; + } + + config->current_bar->strip_workspace_numbers = + parse_boolean(argv[0], config->current_bar->strip_workspace_numbers); + + if (config->current_bar->strip_workspace_numbers) { + config->current_bar->strip_workspace_name = false; + + sway_log(SWAY_DEBUG, "Stripping workspace numbers on bar: %s", + config->current_bar->id); + } else { + sway_log(SWAY_DEBUG, "Enabling workspace numbers on bar: %s", + config->current_bar->id); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/bar/swaybar_command.c b/sway/commands/bar/swaybar_command.c new file mode 100644 index 0000000..0892a89 --- /dev/null +++ b/sway/commands/bar/swaybar_command.c @@ -0,0 +1,16 @@ +#include +#include "sway/commands.h" +#include "log.h" +#include "stringop.h" + +struct cmd_results *bar_cmd_swaybar_command(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "swaybar_command", EXPECTED_AT_LEAST, 1))) { + return error; + } + free(config->current_bar->swaybar_command); + config->current_bar->swaybar_command = join_args(argv, argc); + sway_log(SWAY_DEBUG, "Using custom swaybar command: %s", + config->current_bar->swaybar_command); + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/bar/tray_bind.c b/sway/commands/bar/tray_bind.c new file mode 100644 index 0000000..3dc9bc4 --- /dev/null +++ b/sway/commands/bar/tray_bind.c @@ -0,0 +1,95 @@ +#include +#include "config.h" +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/input/cursor.h" +#include "log.h" + +static struct cmd_results *tray_bind(int argc, char **argv, bool code) { +#if HAVE_TRAY + const char *command = code ? "bar tray_bindcode" : "bar tray_bindsym"; + struct cmd_results *error = NULL; + if ((error = checkarg(argc, command, EXPECTED_EQUAL_TO, 2))) { + return error; + } + + struct tray_binding *binding = calloc(1, sizeof(struct tray_binding)); + if (!binding) { + return cmd_results_new(CMD_FAILURE, "Unable to allocate tray binding"); + } + + char *message = NULL; + if (code) { + binding->button = get_mouse_bindcode(argv[0], &message); + } else { + binding->button = get_mouse_bindsym(argv[0], &message); + } + if (message) { + free(binding); + error = cmd_results_new(CMD_INVALID, "%s", message); + free(message); + return error; + } else if (!binding->button) { + free(binding); + return cmd_results_new(CMD_INVALID, "Unknown button %s", argv[0]); + } + const char *name = get_mouse_button_name(binding->button); + + static const char *commands[] = { + "ContextMenu", + "Activate", + "SecondaryActivate", + "ScrollDown", + "ScrollLeft", + "ScrollRight", + "ScrollUp", + "nop" + }; + + for (size_t i = 0; i < sizeof(commands) / sizeof(commands[0]); ++i) { + if (strcasecmp(argv[1], commands[i]) == 0) { + binding->command = commands[i]; + } + } + if (!binding->command) { + free(binding); + return cmd_results_new(CMD_INVALID, "[Bar %s] Invalid tray command %s", + config->current_bar->id, argv[1]); + } + + bool overwritten = false; + struct tray_binding *other = NULL; + wl_list_for_each(other, &config->current_bar->tray_bindings, link) { + if (other->button == binding->button) { + overwritten = true; + other->command = binding->command; + free(binding); + binding = other; + sway_log(SWAY_DEBUG, + "[bar %s] Updated tray binding for %u (%s) to %s", + config->current_bar->id, binding->button, name, + binding->command); + break; + } + } + if (!overwritten) { + wl_list_insert(&config->current_bar->tray_bindings, &binding->link); + sway_log(SWAY_DEBUG, "[bar %s] Added tray binding for %u (%s) to %s", + config->current_bar->id, binding->button, name, + binding->command); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +#else + return cmd_results_new(CMD_INVALID, + "Sway has been compiled without tray support"); +#endif +} + +struct cmd_results *bar_cmd_tray_bindcode(int argc, char **argv) { + return tray_bind(argc, argv, true); +} + +struct cmd_results *bar_cmd_tray_bindsym(int argc, char **argv) { + return tray_bind(argc, argv, false); +} diff --git a/sway/commands/bar/tray_output.c b/sway/commands/bar/tray_output.c new file mode 100644 index 0000000..679facf --- /dev/null +++ b/sway/commands/bar/tray_output.c @@ -0,0 +1,49 @@ +#include +#include "config.h" +#include "sway/commands.h" +#include "sway/config.h" +#include "list.h" +#include "log.h" + +struct cmd_results *bar_cmd_tray_output(int argc, char **argv) { +#if HAVE_TRAY + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "tray_output", EXPECTED_EQUAL_TO, 1))) { + return error; + } + + list_t *outputs = config->current_bar->tray_outputs; + if (!outputs) { + config->current_bar->tray_outputs = outputs = create_list(); + } + + if (strcmp(argv[0], "none") == 0) { + sway_log(SWAY_DEBUG, "Hiding tray on bar: %s", config->current_bar->id); + for (int i = 0; i < outputs->length; ++i) { + free(outputs->items[i]); + } + outputs->length = 0; + } else if (strcmp(argv[0], "*") == 0) { + sway_log(SWAY_DEBUG, "Showing tray on all outputs for bar: %s", + config->current_bar->id); + while (outputs->length) { + free(outputs->items[0]); + list_del(outputs, 0); + } + return cmd_results_new(CMD_SUCCESS, NULL); + } else { + sway_log(SWAY_DEBUG, "Showing tray on output '%s' for bar: %s", argv[0], + config->current_bar->id); + if (outputs->length == 1 && strcmp(outputs->items[0], "none") == 0) { + free(outputs->items[0]); + list_del(outputs, 0); + } + } + list_add(outputs, strdup(argv[0])); + + return cmd_results_new(CMD_SUCCESS, NULL); +#else + return cmd_results_new(CMD_INVALID, + "Sway has been compiled without tray support"); +#endif +} diff --git a/sway/commands/bar/tray_padding.c b/sway/commands/bar/tray_padding.c new file mode 100644 index 0000000..f90b600 --- /dev/null +++ b/sway/commands/bar/tray_padding.c @@ -0,0 +1,39 @@ +#include +#include +#include "config.h" +#include "sway/commands.h" +#include "sway/config.h" +#include "log.h" + +struct cmd_results *bar_cmd_tray_padding(int argc, char **argv) { +#if HAVE_TRAY + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "tray_padding", EXPECTED_AT_LEAST, 1))) { + return error; + } + if ((error = checkarg(argc, "tray_padding", EXPECTED_AT_MOST, 2))) { + return error; + } + + struct bar_config *bar = config->current_bar; + + char *end; + int padding = strtol(argv[0], &end, 10); + if (padding < 0 || (*end != '\0' && strcasecmp(end, "px") != 0)) { + return cmd_results_new(CMD_INVALID, + "[Bar %s] Invalid tray padding value: %s", bar->id, argv[0]); + } + + if (argc == 2 && strcasecmp(argv[1], "px") != 0) { + return cmd_results_new(CMD_INVALID, + "Expected 'tray_padding [px]'"); + } + + sway_log(SWAY_DEBUG, "[Bar %s] Setting tray padding to %d", bar->id, padding); + config->current_bar->tray_padding = padding; + return cmd_results_new(CMD_SUCCESS, NULL); +#else + return cmd_results_new(CMD_INVALID, + "Sway has been compiled without tray support"); +#endif +} diff --git a/sway/commands/bar/workspace_buttons.c b/sway/commands/bar/workspace_buttons.c new file mode 100644 index 0000000..6bfb161 --- /dev/null +++ b/sway/commands/bar/workspace_buttons.c @@ -0,0 +1,22 @@ +#include +#include +#include "sway/commands.h" +#include "log.h" +#include "util.h" + +struct cmd_results *bar_cmd_workspace_buttons(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "workspace_buttons", EXPECTED_EQUAL_TO, 1))) { + return error; + } + config->current_bar->workspace_buttons = + parse_boolean(argv[0], config->current_bar->workspace_buttons); + if (config->current_bar->workspace_buttons) { + sway_log(SWAY_DEBUG, "Enabling workspace buttons on bar: %s", + config->current_bar->id); + } else { + sway_log(SWAY_DEBUG, "Disabling workspace buttons on bar: %s", + config->current_bar->id); + } + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/bar/workspace_min_width.c b/sway/commands/bar/workspace_min_width.c new file mode 100644 index 0000000..8d65592 --- /dev/null +++ b/sway/commands/bar/workspace_min_width.c @@ -0,0 +1,33 @@ +#include +#include +#include "config.h" +#include "sway/commands.h" +#include "sway/config.h" +#include "log.h" + +struct cmd_results *bar_cmd_workspace_min_width(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "workspace_min_width", EXPECTED_AT_LEAST, 1))) { + return error; + } + + struct bar_config *bar = config->current_bar; + + char *end; + int min_width = strtol(argv[0], &end, 10); + if (min_width < 0 || (*end != '\0' && strcasecmp(end, "px") != 0)) { + return cmd_results_new(CMD_INVALID, + "[Bar %s] Invalid minimum workspace button width value: %s", + bar->id, argv[0]); + } + + if (argc == 2 && strcasecmp(argv[1], "px") != 0) { + return cmd_results_new(CMD_INVALID, + "Expected 'workspace_min_width [px]'"); + } + + sway_log(SWAY_DEBUG, "[Bar %s] Setting minimum workspace button width to %d", + bar->id, min_width); + config->current_bar->workspace_min_width = min_width; + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/bar/wrap_scroll.c b/sway/commands/bar/wrap_scroll.c new file mode 100644 index 0000000..f57e393 --- /dev/null +++ b/sway/commands/bar/wrap_scroll.c @@ -0,0 +1,22 @@ +#include +#include +#include "sway/commands.h" +#include "log.h" +#include "util.h" + +struct cmd_results *bar_cmd_wrap_scroll(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "wrap_scroll", EXPECTED_EQUAL_TO, 1))) { + return error; + } + config->current_bar->wrap_scroll = + parse_boolean(argv[0], config->current_bar->wrap_scroll); + if (config->current_bar->wrap_scroll) { + sway_log(SWAY_DEBUG, "Enabling wrap scroll on bar: %s", + config->current_bar->id); + } else { + sway_log(SWAY_DEBUG, "Disabling wrap scroll on bar: %s", + config->current_bar->id); + } + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/bind.c b/sway/commands/bind.c new file mode 100644 index 0000000..627d994 --- /dev/null +++ b/sway/commands/bind.c @@ -0,0 +1,764 @@ +#include +#include +#include +#include +#include +#include +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/desktop/transaction.h" +#include "sway/input/cursor.h" +#include "sway/input/keyboard.h" +#include "sway/ipc-server.h" +#include "list.h" +#include "log.h" +#include "stringop.h" +#include "util.h" + +int binding_order = 0; + +void free_sway_binding(struct sway_binding *binding) { + if (!binding) { + return; + } + + list_free_items_and_destroy(binding->keys); + list_free_items_and_destroy(binding->syms); + free(binding->input); + free(binding->command); + free(binding); +} + +void free_switch_binding(struct sway_switch_binding *binding) { + if (!binding) { + return; + } + free(binding->command); + free(binding); +} + +/** + * Returns true if the bindings have the same switch type and state combinations. + */ +static bool binding_switch_compare(struct sway_switch_binding *binding_a, + struct sway_switch_binding *binding_b) { + if (binding_a->type != binding_b->type) { + return false; + } + if (binding_a->trigger != binding_b->trigger) { + return false; + } + if ((binding_a->flags & BINDING_LOCKED) != + (binding_b->flags & BINDING_LOCKED)) { + return false; + } + return true; +} + +/** + * Returns true if the bindings have the same key and modifier combinations. + * Note that keyboard layout is not considered, so the bindings might actually + * not be equivalent on some layouts. + */ +static bool binding_key_compare(struct sway_binding *binding_a, + struct sway_binding *binding_b) { + if (strcmp(binding_a->input, binding_b->input) != 0) { + return false; + } + + if (binding_a->type != binding_b->type) { + return false; + } + + uint32_t conflict_generating_flags = BINDING_RELEASE | BINDING_BORDER + | BINDING_CONTENTS | BINDING_TITLEBAR | BINDING_LOCKED + | BINDING_INHIBITED; + if ((binding_a->flags & conflict_generating_flags) != + (binding_b->flags & conflict_generating_flags)) { + return false; + } + + if (binding_a->group != binding_b->group) { + return false; + } + + if (binding_a->modifiers ^ binding_b->modifiers) { + return false; + } + + if (binding_a->keys->length != binding_b->keys->length) { + return false; + } + + // Keys are sorted + int keys_len = binding_a->keys->length; + for (int i = 0; i < keys_len; ++i) { + uint32_t key_a = *(uint32_t *)binding_a->keys->items[i]; + uint32_t key_b = *(uint32_t *)binding_b->keys->items[i]; + if (key_a != key_b) { + return false; + } + } + + return true; +} + +static int key_qsort_cmp(const void *keyp_a, const void *keyp_b) { + uint32_t key_a = **(uint32_t **)keyp_a; + uint32_t key_b = **(uint32_t **)keyp_b; + return (key_a < key_b) ? -1 : ((key_a > key_b) ? 1 : 0); +} + +/** + * From a keycode, bindcode, or bindsym name and the most likely binding type, + * identify the appropriate numeric value corresponding to the key. Return NULL + * and set *key_val if successful, otherwise return a specific error. Change + * the value of *type if the initial type guess was incorrect and if this + * was the first identified key. + */ +static struct cmd_results *identify_key(const char* name, bool first_key, + uint32_t* key_val, enum binding_input_type* type) { + if (*type == BINDING_MOUSECODE) { + // check for mouse bindcodes + char *message = NULL; + uint32_t button = get_mouse_bindcode(name, &message); + if (!button) { + if (message) { + struct cmd_results *error = + cmd_results_new(CMD_INVALID, "%s", message); + free(message); + return error; + } else { + return cmd_results_new(CMD_INVALID, + "Unknown button code %s", name); + } + } + *key_val = button; + } else if (*type == BINDING_MOUSESYM) { + // check for mouse bindsyms (x11 buttons or event names) + char *message = NULL; + uint32_t button = get_mouse_bindsym(name, &message); + if (!button) { + if (message) { + struct cmd_results *error = + cmd_results_new(CMD_INVALID, "%s", message); + free(message); + return error; + } else { + return cmd_results_new(CMD_INVALID, "Unknown button %s", name); + } + } + *key_val = button; + } else if (*type == BINDING_KEYCODE) { + // check for keycode. If it is the first key, allow mouse bindcodes + if (first_key) { + char *message = NULL; + uint32_t button = get_mouse_bindcode(name, &message); + free(message); + if (button) { + *type = BINDING_MOUSECODE; + *key_val = button; + return NULL; + } + } + + xkb_keycode_t keycode = strtol(name, NULL, 10); + if (!xkb_keycode_is_legal_ext(keycode)) { + if (first_key) { + return cmd_results_new(CMD_INVALID, + "Invalid keycode or button code '%s'", name); + } else { + return cmd_results_new(CMD_INVALID, + "Invalid keycode '%s'", name); + } + } + *key_val = keycode; + } else { + // check for keysym. If it is the first key, allow mouse bindsyms + if (first_key) { + char *message = NULL; + uint32_t button = get_mouse_bindsym(name, &message); + if (message) { + struct cmd_results *error = + cmd_results_new(CMD_INVALID, "%s", message); + free(message); + return error; + } else if (button) { + *type = BINDING_MOUSESYM; + *key_val = button; + return NULL; + } + } + + xkb_keysym_t keysym = xkb_keysym_from_name(name, + XKB_KEYSYM_CASE_INSENSITIVE); + if (!keysym) { + if (first_key) { + return cmd_results_new(CMD_INVALID, + "Unknown key or button '%s'", name); + } else { + return cmd_results_new(CMD_INVALID, "Unknown key '%s'", name); + } + } + *key_val = keysym; + } + return NULL; +} + +static struct cmd_results *switch_binding_add( + struct sway_switch_binding *binding, const char *bindtype, + const char *switchcombo, bool warn) { + list_t *mode_bindings = config->current_mode->switch_bindings; + // overwrite the binding if it already exists + bool overwritten = false; + for (int i = 0; i < mode_bindings->length; ++i) { + struct sway_switch_binding *config_binding = mode_bindings->items[i]; + if (binding_switch_compare(binding, config_binding)) { + sway_log(SWAY_INFO, "Overwriting binding '%s' to `%s` from `%s`", + switchcombo, binding->command, config_binding->command); + if (warn) { + config_add_swaynag_warning("Overwriting binding" + "'%s' to `%s` from `%s`", + switchcombo, binding->command, + config_binding->command); + } + free_switch_binding(config_binding); + mode_bindings->items[i] = binding; + overwritten = true; + } + } + + if (!overwritten) { + list_add(mode_bindings, binding); + sway_log(SWAY_DEBUG, "%s - Bound %s to command `%s`", + bindtype, switchcombo, binding->command); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} + +static struct cmd_results *switch_binding_remove( + struct sway_switch_binding *binding, const char *bindtype, + const char *switchcombo) { + list_t *mode_bindings = config->current_mode->switch_bindings; + for (int i = 0; i < mode_bindings->length; ++i) { + struct sway_switch_binding *config_binding = mode_bindings->items[i]; + if (binding_switch_compare(binding, config_binding)) { + free_switch_binding(config_binding); + free_switch_binding(binding); + list_del(mode_bindings, i); + sway_log(SWAY_DEBUG, "%s - Unbound %s switch", + bindtype, switchcombo); + return cmd_results_new(CMD_SUCCESS, NULL); + } + } + + free_switch_binding(binding); + return cmd_results_new(CMD_FAILURE, "Could not find switch binding `%s`", + switchcombo); +} + +/** + * Insert or update the binding. + * Return the binding which has been replaced or NULL. + */ +static struct sway_binding *binding_upsert(struct sway_binding *binding, + list_t *mode_bindings) { + for (int i = 0; i < mode_bindings->length; ++i) { + struct sway_binding *config_binding = mode_bindings->items[i]; + if (binding_key_compare(binding, config_binding)) { + mode_bindings->items[i] = binding; + return config_binding; + } + } + + list_add(mode_bindings, binding); + return NULL; +} + +static struct cmd_results *binding_add(struct sway_binding *binding, + list_t *mode_bindings, const char *bindtype, + const char *keycombo, bool warn) { + struct sway_binding *config_binding = binding_upsert(binding, mode_bindings); + + if (config_binding) { + sway_log(SWAY_INFO, "Overwriting binding '%s' for device '%s' " + "to `%s` from `%s`", keycombo, binding->input, + binding->command, config_binding->command); + if (warn) { + config_add_swaynag_warning("Overwriting binding" + "'%s' for device '%s' to `%s` from `%s`", + keycombo, binding->input, binding->command, + config_binding->command); + } + free_sway_binding(config_binding); + } else { + sway_log(SWAY_DEBUG, "%s - Bound %s to command `%s` for device '%s'", + bindtype, keycombo, binding->command, binding->input); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} + +static struct cmd_results *binding_remove(struct sway_binding *binding, + list_t *mode_bindings, const char *bindtype, + const char *keycombo) { + for (int i = 0; i < mode_bindings->length; ++i) { + struct sway_binding *config_binding = mode_bindings->items[i]; + if (binding_key_compare(binding, config_binding)) { + sway_log(SWAY_DEBUG, "%s - Unbound `%s` from device '%s'", + bindtype, keycombo, binding->input); + free_sway_binding(config_binding); + free_sway_binding(binding); + list_del(mode_bindings, i); + return cmd_results_new(CMD_SUCCESS, NULL); + } + } + free_sway_binding(binding); + return cmd_results_new(CMD_FAILURE, "Could not find binding `%s` " + "for the given flags", keycombo); +} + +static struct cmd_results *cmd_bindsym_or_bindcode(int argc, char **argv, + bool bindcode, bool unbind) { + const char *bindtype; + int minargs = 2; + if (unbind) { + bindtype = bindcode ? "unbindcode" : "unbindsym"; + minargs--; + } else { + bindtype = bindcode ? "bindcode": "bindsym"; + } + + struct cmd_results *error = NULL; + if ((error = checkarg(argc, bindtype, EXPECTED_AT_LEAST, minargs))) { + return error; + } + + struct sway_binding *binding = calloc(1, sizeof(struct sway_binding)); + if (!binding) { + return cmd_results_new(CMD_FAILURE, "Unable to allocate binding"); + } + binding->input = strdup("*"); + binding->keys = create_list(); + binding->group = XKB_LAYOUT_INVALID; + binding->modifiers = 0; + binding->flags = 0; + binding->type = bindcode ? BINDING_KEYCODE : BINDING_KEYSYM; + + bool exclude_titlebar = false; + bool warn = true; + + while (argc > 0) { + if (strcmp("--release", argv[0]) == 0) { + binding->flags |= BINDING_RELEASE; + } else if (strcmp("--locked", argv[0]) == 0) { + binding->flags |= BINDING_LOCKED; + } else if (strcmp("--inhibited", argv[0]) == 0) { + binding->flags |= BINDING_INHIBITED; + } else if (strcmp("--whole-window", argv[0]) == 0) { + binding->flags |= BINDING_BORDER | BINDING_CONTENTS | BINDING_TITLEBAR; + } else if (strcmp("--border", argv[0]) == 0) { + binding->flags |= BINDING_BORDER; + } else if (strcmp("--to-code", argv[0]) == 0) { + if (!bindcode) { + binding->flags |= BINDING_CODE; + } + } else if (strcmp("--exclude-titlebar", argv[0]) == 0) { + exclude_titlebar = true; + } else if (has_prefix(argv[0], "--input-device=")) { + free(binding->input); + binding->input = strdup(argv[0] + strlen("--input-device=")); + strip_quotes(binding->input); + } else if (strcmp("--no-warn", argv[0]) == 0) { + warn = false; + } else if (strcmp("--no-repeat", argv[0]) == 0) { + binding->flags |= BINDING_NOREPEAT; + } else { + break; + } + argv++; + argc--; + } + if (binding->flags & (BINDING_BORDER | BINDING_CONTENTS | BINDING_TITLEBAR) + || exclude_titlebar) { + binding->type = binding->type == BINDING_KEYCODE ? + BINDING_MOUSECODE : BINDING_MOUSESYM; + } + + if (argc < minargs) { + free_sway_binding(binding); + return cmd_results_new(CMD_FAILURE, + "Invalid %s command " + "(expected at least %d non-option arguments, got %d)", + bindtype, minargs, argc); + } + + list_t *split = split_string(argv[0], "+"); + for (int i = 0; i < split->length; ++i) { + // Check for group + if (has_prefix(split->items[i], "Group")) { + if (binding->group != XKB_LAYOUT_INVALID) { + free_sway_binding(binding); + list_free_items_and_destroy(split); + return cmd_results_new(CMD_FAILURE, + "Only one group can be specified"); + } + char *end; + int group = strtol(split->items[i] + strlen("Group"), &end, 10); + if (group < 1 || group > 4 || end[0] != '\0') { + free_sway_binding(binding); + list_free_items_and_destroy(split); + return cmd_results_new(CMD_FAILURE, "Invalid group"); + } + binding->group = group - 1; + continue; + } else if (strcmp(split->items[i], "Mode_switch") == 0) { + // For full i3 compatibility, Mode_switch is an alias for Group2 + if (binding->group != XKB_LAYOUT_INVALID) { + free_sway_binding(binding); + list_free_items_and_destroy(split); + return cmd_results_new(CMD_FAILURE, + "Only one group can be specified"); + } + binding->group = 1; + } + + // Check for a modifier key + uint32_t mod; + if ((mod = get_modifier_mask_by_name(split->items[i])) > 0) { + binding->modifiers |= mod; + continue; + } + + // Identify the key and possibly change binding->type + uint32_t key_val = 0; + error = identify_key(split->items[i], binding->keys->length == 0, + &key_val, &binding->type); + if (error) { + free_sway_binding(binding); + list_free(split); + return error; + } + + uint32_t *key = calloc(1, sizeof(uint32_t)); + if (!key) { + free_sway_binding(binding); + list_free_items_and_destroy(split); + return cmd_results_new(CMD_FAILURE, + "Unable to allocate binding key"); + } + *key = key_val; + list_add(binding->keys, key); + } + list_free_items_and_destroy(split); + + // refine region of interest for mouse binding once we are certain + // that this is one + if (exclude_titlebar) { + binding->flags &= ~BINDING_TITLEBAR; + } else if (binding->type == BINDING_MOUSECODE + || binding->type == BINDING_MOUSESYM) { + binding->flags |= BINDING_TITLEBAR; + } + + // sort ascending + list_qsort(binding->keys, key_qsort_cmp); + + // translate keysyms into keycodes + if (!translate_binding(binding)) { + sway_log(SWAY_INFO, + "Unable to translate bindsym into bindcode: %s", argv[0]); + } + + list_t *mode_bindings; + if (binding->type == BINDING_KEYCODE) { + mode_bindings = config->current_mode->keycode_bindings; + } else if (binding->type == BINDING_KEYSYM) { + mode_bindings = config->current_mode->keysym_bindings; + } else { + mode_bindings = config->current_mode->mouse_bindings; + } + + if (unbind) { + return binding_remove(binding, mode_bindings, bindtype, argv[0]); + } + + binding->command = join_args(argv + 1, argc - 1); + binding->order = binding_order++; + return binding_add(binding, mode_bindings, bindtype, argv[0], warn); +} + +struct cmd_results *cmd_bind_or_unbind_switch(int argc, char **argv, + bool unbind) { + int minargs = 2; + char *bindtype = "bindswitch"; + if (unbind) { + minargs--; + bindtype = "unbindswitch"; + } + + struct cmd_results *error = NULL; + if ((error = checkarg(argc, bindtype, EXPECTED_AT_LEAST, minargs))) { + return error; + } + struct sway_switch_binding *binding = calloc(1, sizeof(struct sway_switch_binding)); + if (!binding) { + return cmd_results_new(CMD_FAILURE, "Unable to allocate binding"); + } + + bool warn = true; + + // Handle flags + while (argc > 0) { + if (strcmp("--locked", argv[0]) == 0) { + binding->flags |= BINDING_LOCKED; + } else if (strcmp("--no-warn", argv[0]) == 0) { + warn = false; + } else if (strcmp("--reload", argv[0]) == 0) { + binding->flags |= BINDING_RELOAD; + } else { + break; + } + argv++; + argc--; + } + + if (argc < minargs) { + free(binding); + return cmd_results_new(CMD_FAILURE, + "Invalid %s command (expected at least %d " + "non-option arguments, got %d)", bindtype, minargs, argc); + } + + list_t *split = split_string(argv[0], ":"); + if (split->length != 2) { + free_switch_binding(binding); + return cmd_results_new(CMD_FAILURE, + "Invalid %s command (expected binding with the form " + ":)", bindtype); + } + if (strcmp(split->items[0], "tablet") == 0) { + binding->type = WLR_SWITCH_TYPE_TABLET_MODE; + } else if (strcmp(split->items[0], "lid") == 0) { + binding->type = WLR_SWITCH_TYPE_LID; +#if HAVE_LIBINPUT_SWITCH_KEYPAD_SLIDE + } else if (strcmp(split->items[0], "keypad_slide") == 0) { + binding->type = WLR_SWITCH_TYPE_KEYPAD_SLIDE; +#endif + } else { + free_switch_binding(binding); + return cmd_results_new(CMD_FAILURE, + "Invalid %s command (expected switch binding: " + "unknown switch %s)", bindtype, + (const char *)split->items[0]); + } + if (strcmp(split->items[1], "on") == 0) { + binding->trigger = SWAY_SWITCH_TRIGGER_ON; + } else if (strcmp(split->items[1], "off") == 0) { + binding->trigger = SWAY_SWITCH_TRIGGER_OFF; + } else if (strcmp(split->items[1], "toggle") == 0) { + binding->trigger = SWAY_SWITCH_TRIGGER_TOGGLE; + } else { + free_switch_binding(binding); + return cmd_results_new(CMD_FAILURE, + "Invalid %s command " + "(expected switch state: unknown state %s)", + bindtype, (const char *)split->items[1]); + } + list_free_items_and_destroy(split); + + if (unbind) { + return switch_binding_remove(binding, bindtype, argv[0]); + } + binding->command = join_args(argv + 1, argc - 1); + return switch_binding_add(binding, bindtype, argv[0], warn); +} + +struct cmd_results *cmd_bindsym(int argc, char **argv) { + return cmd_bindsym_or_bindcode(argc, argv, false, false); +} + +struct cmd_results *cmd_bindcode(int argc, char **argv) { + return cmd_bindsym_or_bindcode(argc, argv, true, false); +} + +struct cmd_results *cmd_unbindsym(int argc, char **argv) { + return cmd_bindsym_or_bindcode(argc, argv, false, true); +} + +struct cmd_results *cmd_unbindcode(int argc, char **argv) { + return cmd_bindsym_or_bindcode(argc, argv, true, true); +} + +struct cmd_results *cmd_bindswitch(int argc, char **argv) { + return cmd_bind_or_unbind_switch(argc, argv, false); +} + +struct cmd_results *cmd_unbindswitch(int argc, char **argv) { + return cmd_bind_or_unbind_switch(argc, argv, true); +} + +/** + * Execute the command associated to a binding + */ +void seat_execute_command(struct sway_seat *seat, struct sway_binding *binding) { + if (!config->active) { + sway_log(SWAY_DEBUG, "deferring command for binding: %s", + binding->command); + struct sway_binding *deferred = calloc(1, sizeof(struct sway_binding)); + if (!deferred) { + sway_log(SWAY_ERROR, "Failed to allocate deferred binding"); + return; + } + memcpy(deferred, binding, sizeof(struct sway_binding)); + deferred->command = binding->command ? strdup(binding->command) : NULL; + list_add(seat->deferred_bindings, deferred); + return; + } + + sway_log(SWAY_DEBUG, "running command for binding: %s", binding->command); + struct sway_container *con = NULL; + if (binding->type == BINDING_MOUSESYM + || binding->type == BINDING_MOUSECODE) { + struct wlr_surface *surface = NULL; + double sx, sy; + struct sway_node *node = node_at_coords(seat, + seat->cursor->cursor->x, seat->cursor->cursor->y, + &surface, &sx, &sy); + if (node && node->type == N_CONTAINER) { + con = node->sway_container; + } + } + + list_t *res_list = execute_command(binding->command, seat, con); + bool success = true; + for (int i = 0; i < res_list->length; ++i) { + struct cmd_results *results = res_list->items[i]; + if (results->status != CMD_SUCCESS) { + sway_log(SWAY_DEBUG, "could not run command for binding: %s (%s)", + binding->command, results->error); + success = false; + } + free_cmd_results(results); + } + list_free(res_list); + if (success) { + ipc_event_binding(binding); + } + + transaction_commit_dirty(); +} + +/** + * The last found keycode associated with the keysym + * and the total count of matches. + */ +struct keycode_matches { + xkb_keysym_t keysym; + xkb_keycode_t keycode; + int count; +}; + +/** + * Iterate through keycodes in the keymap to find ones matching + * the specified keysym. + */ +static void find_keycode(struct xkb_keymap *keymap, + xkb_keycode_t keycode, void *data) { + xkb_keysym_t keysym = xkb_state_key_get_one_sym( + config->keysym_translation_state, keycode); + + if (keysym == XKB_KEY_NoSymbol) { + return; + } + + struct keycode_matches *matches = data; + if (matches->keysym == keysym) { + matches->keycode = keycode; + matches->count++; + } +} + +/** + * Return the keycode for the specified keysym. + */ +static struct keycode_matches get_keycode_for_keysym(xkb_keysym_t keysym) { + struct keycode_matches matches = { + .keysym = keysym, + .keycode = XKB_KEYCODE_INVALID, + .count = 0, + }; + + xkb_keymap_key_for_each( + xkb_state_get_keymap(config->keysym_translation_state), + find_keycode, &matches); + return matches; +} + +bool translate_binding(struct sway_binding *binding) { + if ((binding->flags & BINDING_CODE) == 0) { + return true; + } + + switch (binding->type) { + // a bindsym to translate + case BINDING_KEYSYM: + binding->syms = binding->keys; + binding->keys = create_list(); + break; + // a bindsym to re-translate + case BINDING_KEYCODE: + list_free_items_and_destroy(binding->keys); + binding->keys = create_list(); + break; + default: + return true; + } + + for (int i = 0; i < binding->syms->length; ++i) { + xkb_keysym_t *keysym = binding->syms->items[i]; + struct keycode_matches matches = get_keycode_for_keysym(*keysym); + + if (matches.count != 1) { + sway_log(SWAY_INFO, "Unable to convert keysym %" PRIu32 " into" + " a single keycode (found %d matches)", + *keysym, matches.count); + goto error; + } + + xkb_keycode_t *keycode = malloc(sizeof(xkb_keycode_t)); + if (!keycode) { + sway_log(SWAY_ERROR, "Unable to allocate memory for a keycode"); + goto error; + } + + *keycode = matches.keycode; + list_add(binding->keys, keycode); + } + + list_qsort(binding->keys, key_qsort_cmp); + binding->type = BINDING_KEYCODE; + return true; + +error: + list_free_items_and_destroy(binding->keys); + binding->type = BINDING_KEYSYM; + binding->keys = binding->syms; + binding->syms = NULL; + return false; +} + +void binding_add_translated(struct sway_binding *binding, + list_t *mode_bindings) { + struct sway_binding *config_binding = + binding_upsert(binding, mode_bindings); + + if (config_binding) { + sway_log(SWAY_INFO, "Overwriting binding for device '%s' " + "to `%s` from `%s`", binding->input, + binding->command, config_binding->command); + free_sway_binding(config_binding); + } +} diff --git a/sway/commands/blur.c b/sway/commands/blur.c new file mode 100644 index 0000000..c125744 --- /dev/null +++ b/sway/commands/blur.c @@ -0,0 +1,39 @@ +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/output.h" +#include "sway/tree/arrange.h" +#include "sway/tree/root.h" +#include "util.h" + +static void arrange_blur_iter(struct sway_container *con, void *data) { + con->blur_enabled = config->blur_enabled; +} + +struct cmd_results *cmd_blur(int argc, char **argv) { + struct cmd_results *error = checkarg(argc, "blur", EXPECTED_AT_LEAST, 1); + + if (error) { + return error; + } + + struct sway_container *con = config->handler_context.container; + + bool result = parse_boolean(argv[0], true); + if (con == NULL) { + config->blur_enabled = result; + + // Config reload: reset all containers to config value + root_for_each_container(arrange_blur_iter, NULL); + arrange_root(); + } else { + con->blur_enabled = result; + arrange_container(con); + } + + struct sway_output *output; + wl_list_for_each(output, &root->all_outputs, link) { + wlr_scene_optimized_blur_mark_dirty(output->layers.blur_layer); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/blur_brightness.c b/sway/commands/blur_brightness.c new file mode 100644 index 0000000..33ded90 --- /dev/null +++ b/sway/commands/blur_brightness.c @@ -0,0 +1,19 @@ +#include "sway/commands.h" +#include "sway/tree/root.h" + +struct cmd_results *cmd_blur_brightness(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "blur_brightness", EXPECTED_EQUAL_TO, 1))) { + return error; + } + + char *inv; + float value = strtof(argv[0], &inv); + if (*inv != '\0' || value < 0 || value > 2) { + return cmd_results_new(CMD_FAILURE, "Invalid brightness specified"); + } + + wlr_scene_set_blur_brightness(root->root_scene, value); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/blur_contrast.c b/sway/commands/blur_contrast.c new file mode 100644 index 0000000..4506afb --- /dev/null +++ b/sway/commands/blur_contrast.c @@ -0,0 +1,19 @@ +#include "sway/commands.h" + +struct cmd_results *cmd_blur_contrast(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "blur_contrast", EXPECTED_EQUAL_TO, 1))) { + return error; + } + + char *inv; + float value = strtof(argv[0], &inv); + if (*inv != '\0' || value < 0 || value > 2) { + return cmd_results_new(CMD_FAILURE, "Invalid contrast specified"); + } + + struct wlr_scene *root_scene = root->root_scene; + wlr_scene_set_blur_contrast(root_scene, value); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/blur_noise.c b/sway/commands/blur_noise.c new file mode 100644 index 0000000..26ddea4 --- /dev/null +++ b/sway/commands/blur_noise.c @@ -0,0 +1,19 @@ +#include "sway/commands.h" + +struct cmd_results *cmd_blur_noise(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "blur_noise", EXPECTED_EQUAL_TO, 1))) { + return error; + } + + char *inv; + float value = strtof(argv[0], &inv); + if (*inv != '\0' || value < 0 || value > 1) { + return cmd_results_new(CMD_FAILURE, "Invalid noise specified"); + } + + struct wlr_scene *root_scene = root->root_scene; + wlr_scene_set_blur_noise(root_scene, value); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/blur_passes.c b/sway/commands/blur_passes.c new file mode 100644 index 0000000..54b5b53 --- /dev/null +++ b/sway/commands/blur_passes.c @@ -0,0 +1,19 @@ +#include "sway/commands.h" + +struct cmd_results *cmd_blur_passes(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "blur_passes", EXPECTED_EQUAL_TO, 1))) { + return error; + } + + char *inv; + int value = strtol(argv[0], &inv, 10); + if (*inv != '\0' || value < 0 || value > 10) { + return cmd_results_new(CMD_FAILURE, "Invalid number of passes specified"); + } + + struct wlr_scene *root_scene = root->root_scene; + wlr_scene_set_blur_num_passes(root_scene, value); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/blur_radius.c b/sway/commands/blur_radius.c new file mode 100644 index 0000000..a2e8110 --- /dev/null +++ b/sway/commands/blur_radius.c @@ -0,0 +1,19 @@ +#include "sway/commands.h" + +struct cmd_results *cmd_blur_radius(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "blur_radius", EXPECTED_AT_LEAST, 1))) { + return error; + } + + char *inv; + int value = strtol(argv[0], &inv, 10); + if (*inv != '\0' || value < 0 || value > 10) { + return cmd_results_new(CMD_FAILURE, "Invalid size specified"); + } + + struct wlr_scene *root_scene = root->root_scene; + wlr_scene_set_blur_radius(root_scene, value); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/blur_saturation.c b/sway/commands/blur_saturation.c new file mode 100644 index 0000000..a378520 --- /dev/null +++ b/sway/commands/blur_saturation.c @@ -0,0 +1,19 @@ +#include "sway/commands.h" + +struct cmd_results *cmd_blur_saturation(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "blur_saturation", EXPECTED_EQUAL_TO, 1))) { + return error; + } + + char *inv; + float value = strtof(argv[0], &inv); + if (*inv != '\0' || value < 0 || value > 2) { + return cmd_results_new(CMD_FAILURE, "Invalid saturation specified"); + } + + struct wlr_scene *root_scene = root->root_scene; + wlr_scene_set_blur_saturation(root_scene, value); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/blur_xray.c b/sway/commands/blur_xray.c new file mode 100644 index 0000000..b5737fd --- /dev/null +++ b/sway/commands/blur_xray.c @@ -0,0 +1,23 @@ +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/output.h" +#include "sway/tree/root.h" +#include "util.h" + +struct cmd_results *cmd_blur_xray(int argc, char **argv) { + struct cmd_results *error = checkarg(argc, "blur_xray", EXPECTED_AT_LEAST, 1); + + if (error) { + return error; + } + + bool result = parse_boolean(argv[0], config->blur_xray); + config->blur_xray = result; + + struct sway_output *output; + wl_list_for_each(output, &root->all_outputs, link) { + wlr_scene_optimized_blur_mark_dirty(output->layers.blur_layer); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/border.c b/sway/commands/border.c new file mode 100644 index 0000000..7818fc9 --- /dev/null +++ b/sway/commands/border.c @@ -0,0 +1,101 @@ +#include "log.h" +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/input/cursor.h" +#include "sway/input/input-manager.h" +#include "sway/tree/arrange.h" +#include "sway/tree/container.h" +#include "sway/tree/view.h" + +// A couple of things here: +// - view->border should never be B_CSD when the view is tiled, even when CSD is +// in use (we set using_csd instead and render a sway border). +// - view->saved_border should be the last applied border when switching to CSD. +// - view->using_csd should always reflect whether CSD is applied or not. +static void set_border(struct sway_container *con, + enum sway_container_border new_border) { + if (con->view) { + if (con->view->using_csd && new_border != B_CSD) { + view_set_csd_from_server(con->view, false); + } else if (!con->view->using_csd && new_border == B_CSD) { + view_set_csd_from_server(con->view, true); + con->saved_border = con->pending.border; + } + } + if (new_border != B_CSD || container_is_floating(con)) { + con->pending.border = new_border; + } + if (con->view) { + con->view->using_csd = new_border == B_CSD; + } +} + +static void border_toggle(struct sway_container *con) { + if (con->view && con->view->using_csd) { + set_border(con, B_NONE); + return; + } + switch (con->pending.border) { + case B_NONE: + set_border(con, B_PIXEL); + break; + case B_PIXEL: + set_border(con, B_NORMAL); + break; + case B_NORMAL: + if (con->view && con->view->xdg_decoration) { + set_border(con, B_CSD); + } else { + set_border(con, B_NONE); + } + break; + case B_CSD: + // view->using_csd should be true so it would have returned above + sway_assert(false, "Unreachable"); + break; + } +} + +struct cmd_results *cmd_border(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "border", EXPECTED_AT_LEAST, 1))) { + return error; + } + + struct sway_container *container = config->handler_context.container; + if (!container || !container->view) { + return cmd_results_new(CMD_INVALID, "Only views can have borders"); + } + struct sway_view *view = container->view; + + if (strcmp(argv[0], "none") == 0) { + set_border(container, B_NONE); + } else if (strcmp(argv[0], "normal") == 0) { + set_border(container, B_NORMAL); + } else if (strcmp(argv[0], "pixel") == 0) { + set_border(container, B_PIXEL); + } else if (strcmp(argv[0], "csd") == 0) { + if (!view->xdg_decoration) { + return cmd_results_new(CMD_INVALID, + "This window doesn't support client side decorations"); + } + set_border(container, B_CSD); + } else if (strcmp(argv[0], "toggle") == 0) { + border_toggle(container); + } else { + return cmd_results_new(CMD_INVALID, + "Expected 'border ' " + "or 'border pixel '"); + } + if (argc == 2) { + container->pending.border_thickness = atoi(argv[1]); + } + + if (container_is_floating(container)) { + container_set_geometry_from_content(container); + } + + arrange_container(container); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/client.c b/sway/commands/client.c new file mode 100644 index 0000000..fd2ac7a --- /dev/null +++ b/sway/commands/client.c @@ -0,0 +1,92 @@ +#include "log.h" +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/output.h" +#include "sway/tree/container.h" +#include "util.h" + +static void container_update_iterator(struct sway_container *con, void *data) { + container_update(con); +} + +static struct cmd_results *handle_command(int argc, char **argv, char *cmd_name, + struct border_colors *class, const char *default_indicator) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, cmd_name, EXPECTED_AT_LEAST, 3)) || + (error = checkarg(argc, cmd_name, EXPECTED_AT_MOST, 5))) { + return error; + } + + if (argc > 3 && strcmp(cmd_name, "client.focused_tab_title") == 0) { + sway_log(SWAY_ERROR, + "Warning: indicator and child_border colors have no effect for %s", + cmd_name); + } + + struct border_colors colors = {0}; + const char *ind_hex = argc > 3 ? argv[3] : default_indicator; + const char *child_hex = argc > 4 ? argv[4] : argv[1]; // def to background + + struct { + const char *name; + const char *hex; + float *rgba[4]; + } properties[] = { + { "border", argv[0], colors.border }, + { "background", argv[1], colors.background }, + { "text", argv[2], colors.text }, + { "indicator", ind_hex, colors.indicator }, + { "child_border", child_hex, colors.child_border } + }; + for (size_t i = 0; i < sizeof(properties) / sizeof(properties[0]); i++) { + uint32_t color; + if (!parse_color(properties[i].hex, &color)) { + return cmd_results_new(CMD_INVALID, "Invalid %s color %s", + properties[i].name, properties[i].hex); + } + color_to_rgba(*properties[i].rgba, color); + } + + memcpy(class, &colors, sizeof(struct border_colors)); + + if (config->active) { + root_for_each_container(container_update_iterator, NULL); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} + +struct cmd_results *cmd_client_focused(int argc, char **argv) { + return handle_command(argc, argv, "client.focused", + &config->border_colors.focused, "#2e9ef4ff"); +} + +struct cmd_results *cmd_client_focused_inactive(int argc, char **argv) { + return handle_command(argc, argv, "client.focused_inactive", + &config->border_colors.focused_inactive, "#484e50ff"); +} + +struct cmd_results *cmd_client_unfocused(int argc, char **argv) { + return handle_command(argc, argv, "client.unfocused", + &config->border_colors.unfocused, "#292d2eff"); +} + +struct cmd_results *cmd_client_urgent(int argc, char **argv) { + return handle_command(argc, argv, "client.urgent", + &config->border_colors.urgent, "#900000ff"); +} + +struct cmd_results *cmd_client_noop(int argc, char **argv) { + sway_log(SWAY_INFO, "Warning: %s is ignored by sway", argv[-1]); + return cmd_results_new(CMD_SUCCESS, NULL); +} + +struct cmd_results *cmd_client_focused_tab_title(int argc, char **argv) { + struct cmd_results *result = handle_command(argc, argv, + "client.focused_tab_title", + &config->border_colors.focused_tab_title, "#2e9ef4ff"); + if (result && result->status == CMD_SUCCESS) { + config->has_focused_tab_title = true; + } + return result; +} diff --git a/sway/commands/corner_radius.c b/sway/commands/corner_radius.c new file mode 100644 index 0000000..0129954 --- /dev/null +++ b/sway/commands/corner_radius.c @@ -0,0 +1,55 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/output.h" +#include "sway/tree/arrange.h" +#include "sway/tree/container.h" +#include "sway/tree/workspace.h" + +bool cmd_corner_radius_parse_value(char *arg, int* result) { + char *inv; + int value = strtol(arg, &inv, 10); + if (*inv != '\0' || value < 0 || value > 99) { + return false; + } + *result = value; + return true; +} + +static void arrange_corner_radius_iter(struct sway_container *con, void *data) { + con->corner_radius = config->corner_radius; +} + +// TODO: handle setting per container +struct cmd_results *cmd_corner_radius(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "corner_radius", EXPECTED_EQUAL_TO, 1))) { + return error; + } + + int value = 0; + if (!cmd_corner_radius_parse_value(argv[0], &value)) { + return cmd_results_new(CMD_FAILURE, "Invalid size specified"); + } + + config->corner_radius = value; + + if (!config->handler_context.container) { + // Config reload: reset all containers to config value + root_for_each_container(arrange_corner_radius_iter, NULL); + arrange_root(); + } + + /* + titlebar padding depends on corner_radius to + ensure that titlebars are rendered nicely + */ + if (value > config->titlebar_h_padding) { + config->titlebar_h_padding = value; + } + if (value > (int)container_titlebar_height()) { + config->titlebar_v_padding = (value - config->font_height) / 2; + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/create_output.c b/sway/commands/create_output.c new file mode 100644 index 0000000..79283fd --- /dev/null +++ b/sway/commands/create_output.c @@ -0,0 +1,49 @@ +#include +#include +#include +#include +#if WLR_HAS_X11_BACKEND +#include +#endif +#include "sway/commands.h" +#include "sway/server.h" +#include "log.h" + +static void create_output(struct wlr_backend *backend, void *data) { + bool *done = data; + if (*done) { + return; + } + + if (wlr_backend_is_wl(backend)) { + wlr_wl_output_create(backend); + *done = true; + } else if (wlr_backend_is_headless(backend)) { + wlr_headless_add_output(backend, 1920, 1080); + *done = true; + } +#if WLR_HAS_X11_BACKEND + else if (wlr_backend_is_x11(backend)) { + wlr_x11_output_create(backend); + *done = true; + } +#endif +} + +/** + * This command is intended for developer use only. + */ +struct cmd_results *cmd_create_output(int argc, char **argv) { + sway_assert(wlr_backend_is_multi(server.backend), + "Expected a multi backend"); + + bool done = false; + wlr_multi_for_each_backend(server.backend, create_output, &done); + + if (!done) { + return cmd_results_new(CMD_INVALID, + "Can only create outputs for Wayland, X11 or headless backends"); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/default_border.c b/sway/commands/default_border.c new file mode 100644 index 0000000..5c0fee3 --- /dev/null +++ b/sway/commands/default_border.c @@ -0,0 +1,27 @@ +#include "log.h" +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/tree/container.h" + +struct cmd_results *cmd_default_border(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "default_border", EXPECTED_AT_LEAST, 1))) { + return error; + } + + if (strcmp(argv[0], "none") == 0) { + config->border = B_NONE; + } else if (strcmp(argv[0], "normal") == 0) { + config->border = B_NORMAL; + } else if (strcmp(argv[0], "pixel") == 0) { + config->border = B_PIXEL; + } else { + return cmd_results_new(CMD_INVALID, + "Expected 'default_border ' or 'default_border '"); + } + if (argc == 2) { + config->border_thickness = atoi(argv[1]); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/default_dim_inactive.c b/sway/commands/default_dim_inactive.c new file mode 100644 index 0000000..b6a681b --- /dev/null +++ b/sway/commands/default_dim_inactive.c @@ -0,0 +1,25 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/tree/arrange.h" + +struct cmd_results *cmd_default_dim_inactive(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "default_dim_inactive", EXPECTED_EQUAL_TO, 1))) { + return error; + } + + char *err; + float val = strtof(argv[0], &err); + if (*err || val < 0.0f || val > 1.0f) { + return cmd_results_new(CMD_INVALID, "default_dim_inactive float invalid"); + } + + config->default_dim_inactive = val; + + if (config->active) { + arrange_root(); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/default_floating_border.c b/sway/commands/default_floating_border.c new file mode 100644 index 0000000..7e4edc3 --- /dev/null +++ b/sway/commands/default_floating_border.c @@ -0,0 +1,29 @@ +#include "log.h" +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/tree/container.h" + +struct cmd_results *cmd_default_floating_border(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "default_floating_border", + EXPECTED_AT_LEAST, 1))) { + return error; + } + + if (strcmp(argv[0], "none") == 0) { + config->floating_border = B_NONE; + } else if (strcmp(argv[0], "normal") == 0) { + config->floating_border = B_NORMAL; + } else if (strcmp(argv[0], "pixel") == 0) { + config->floating_border = B_PIXEL; + } else { + return cmd_results_new(CMD_INVALID, + "Expected 'default_floating_border ' " + "or 'default_floating_border '"); + } + if (argc == 2) { + config->floating_border_thickness = atoi(argv[1]); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/default_orientation.c b/sway/commands/default_orientation.c new file mode 100644 index 0000000..fd42c16 --- /dev/null +++ b/sway/commands/default_orientation.c @@ -0,0 +1,21 @@ +#include +#include +#include "sway/commands.h" + +struct cmd_results *cmd_default_orientation(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "default_orientation", EXPECTED_EQUAL_TO, 1))) { + return error; + } + if (strcasecmp(argv[0], "horizontal") == 0) { + config->default_orientation = L_HORIZ; + } else if (strcasecmp(argv[0], "vertical") == 0) { + config->default_orientation = L_VERT; + } else if (strcasecmp(argv[0], "auto") == 0) { + // Do nothing + } else { + return cmd_results_new(CMD_INVALID, + "Expected 'orientation '"); + } + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/dim_inactive.c b/sway/commands/dim_inactive.c new file mode 100644 index 0000000..fd633d9 --- /dev/null +++ b/sway/commands/dim_inactive.c @@ -0,0 +1,26 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/output.h" +#include "sway/tree/container.h" + +struct cmd_results *cmd_dim_inactive(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "dim_inactive", EXPECTED_EQUAL_TO, 1))) { + return error; + } + + char *err; + float val = strtof(argv[0], &err); + if (*err || val < 0.0f || val > 1.0f) { + return cmd_results_new(CMD_INVALID, "dim_inactive float invalid"); + } + + struct sway_container *container = config->handler_context.container; + if (!container) { + return cmd_results_new(CMD_INVALID, "cmd_dim cannot be used without a for_window rule"); + } + + container->dim = val; + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/dim_inactive_colors.c b/sway/commands/dim_inactive_colors.c new file mode 100644 index 0000000..c5eab89 --- /dev/null +++ b/sway/commands/dim_inactive_colors.c @@ -0,0 +1,35 @@ +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/tree/arrange.h" +#include "util.h" + +static struct cmd_results *handle_command(int argc, char **argv, char *cmd_name, + float config_option[4]) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, cmd_name, EXPECTED_AT_LEAST, 1))) { + return error; + } + + uint32_t color; + if (!parse_color(argv[0], &color)) { + return cmd_results_new(CMD_INVALID, "Invalid %s color %s", + cmd_name, argv[0]); + } + color_to_rgba(config_option, color); + + if (config->active) { + arrange_root(); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} + +struct cmd_results *cmd_dim_inactive_colors_unfocused(int argc, char **argv) { + return handle_command(argc, argv, "dim_inactive_colors.unfocused", + config->dim_inactive_colors.unfocused); +} + +struct cmd_results *cmd_dim_inactive_colors_urgent(int argc, char **argv) { + return handle_command(argc, argv, "dim_inactive_colors.urgent", + config->dim_inactive_colors.urgent); +} diff --git a/sway/commands/exec.c b/sway/commands/exec.c new file mode 100644 index 0000000..2c6f3d2 --- /dev/null +++ b/sway/commands/exec.c @@ -0,0 +1,19 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "log.h" +#include "stringop.h" + +struct cmd_results *cmd_exec(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = cmd_exec_validate(argc, argv))) { + return error; + } + if (config->reloading) { + char *args = join_args(argv, argc); + sway_log(SWAY_DEBUG, "Ignoring 'exec %s' due to reload", args); + free(args); + return cmd_results_new(CMD_SUCCESS, NULL); + } + return cmd_exec_process(argc, argv); +} diff --git a/sway/commands/exec_always.c b/sway/commands/exec_always.c new file mode 100644 index 0000000..e16af09 --- /dev/null +++ b/sway/commands/exec_always.c @@ -0,0 +1,87 @@ +#include +#include +#include +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/server.h" +#include "sway/desktop/launcher.h" +#include "sway/tree/container.h" +#include "sway/tree/root.h" +#include "sway/tree/workspace.h" +#include "log.h" +#include "stringop.h" + +struct cmd_results *cmd_exec_validate(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, argv[-1], EXPECTED_AT_LEAST, 1))) { + return error; + } + if (!config->active || config->validating) { + return cmd_results_new(CMD_DEFER, NULL); + } + return error; +} + +struct cmd_results *cmd_exec_process(int argc, char **argv) { + struct cmd_results *error = NULL; + char *cmd = NULL; + bool no_startup_id = false; + if (strcmp(argv[0], "--no-startup-id") == 0) { + no_startup_id = true; + --argc; ++argv; + if ((error = checkarg(argc, argv[-1], EXPECTED_AT_LEAST, 1))) { + return error; + } + } + + if (argc == 1 && (argv[0][0] == '\'' || argv[0][0] == '"')) { + cmd = strdup(argv[0]); + strip_quotes(cmd); + } else { + cmd = join_args(argv, argc); + } + + sway_log(SWAY_DEBUG, "Executing %s", cmd); + + struct launcher_ctx *ctx = launcher_ctx_create_internal(); + + // Fork process + pid_t child = fork(); + if (child == 0) { + setsid(); + + if (ctx) { + const char *token = launcher_ctx_get_token_name(ctx); + setenv("XDG_ACTIVATION_TOKEN", token, 1); + if (!no_startup_id) { + setenv("DESKTOP_STARTUP_ID", token, 1); + } + } + + execlp("sh", "sh", "-c", cmd, (void*)NULL); + sway_log_errno(SWAY_ERROR, "execve failed"); + _exit(0); // Close child process + } else if (child < 0) { + launcher_ctx_destroy(ctx); + free(cmd); + return cmd_results_new(CMD_FAILURE, "fork() failed"); + } + + sway_log(SWAY_DEBUG, "Child process created with pid %d", child); + if (ctx != NULL) { + sway_log(SWAY_DEBUG, "Recording workspace for process %d", child); + ctx->pid = child; + } + + free(cmd); + return cmd_results_new(CMD_SUCCESS, NULL); +} + +struct cmd_results *cmd_exec_always(int argc, char **argv) { + struct cmd_results *error; + if ((error = cmd_exec_validate(argc, argv))) { + return error; + } + return cmd_exec_process(argc, argv); +} diff --git a/sway/commands/exit.c b/sway/commands/exit.c new file mode 100644 index 0000000..0f326ce --- /dev/null +++ b/sway/commands/exit.c @@ -0,0 +1,13 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/server.h" + +struct cmd_results *cmd_exit(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "exit", EXPECTED_EQUAL_TO, 0))) { + return error; + } + sway_terminate(0); + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/floating.c b/sway/commands/floating.c new file mode 100644 index 0000000..74f6522 --- /dev/null +++ b/sway/commands/floating.c @@ -0,0 +1,59 @@ +#include +#include +#include "sway/commands.h" +#include "sway/input/seat.h" +#include "sway/ipc-server.h" +#include "sway/output.h" +#include "sway/tree/arrange.h" +#include "sway/tree/container.h" +#include "sway/tree/view.h" +#include "sway/tree/workspace.h" +#include "list.h" +#include "util.h" + +struct cmd_results *cmd_floating(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "floating", EXPECTED_EQUAL_TO, 1))) { + return error; + } + if (!root->outputs->length) { + return cmd_results_new(CMD_INVALID, + "Can't run this command while there's no outputs connected."); + } + struct sway_container *container = config->handler_context.container; + struct sway_workspace *workspace = config->handler_context.workspace; + if (!container && workspace->tiling->length == 0) { + return cmd_results_new(CMD_INVALID, "Can't float an empty workspace"); + } + if (!container) { + // Wrap the workspace's children in a container so we can float it + container = workspace_wrap_children(workspace); + workspace->layout = L_HORIZ; + seat_set_focus_container(config->handler_context.seat, container); + } + + if (container_is_scratchpad_hidden(container)) { + return cmd_results_new(CMD_INVALID, + "Can't change floating on hidden scratchpad container"); + } + + // If the container is in a floating split container, + // operate on the split container instead of the child. + if (container_is_floating_or_child(container)) { + while (container->pending.parent) { + container = container->pending.parent; + } + } + + bool wants_floating = + parse_boolean(argv[0], container_is_floating(container)); + + container_set_floating(container, wants_floating); + + // Floating containers in the scratchpad should be ignored + if (container->pending.workspace) { + arrange_workspace(container->pending.workspace); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/floating_minmax_size.c b/sway/commands/floating_minmax_size.c new file mode 100644 index 0000000..e8c24ac --- /dev/null +++ b/sway/commands/floating_minmax_size.c @@ -0,0 +1,52 @@ +#include +#include +#include +#include +#include +#include +#include "sway/commands.h" +#include "log.h" + +static const char min_usage[] = + "Expected 'floating_minimum_size x '"; + +static const char max_usage[] = + "Expected 'floating_maximum_size x '"; + +static struct cmd_results *handle_command(int argc, char **argv, char *cmd_name, + const char *usage, int *config_width, int *config_height) { + struct cmd_results *error; + if ((error = checkarg(argc, cmd_name, EXPECTED_EQUAL_TO, 3))) { + return error; + } + + char *err; + int width = (int)strtol(argv[0], &err, 10); + if (*err) { + return cmd_results_new(CMD_INVALID, "%s", usage); + } + + if (strcmp(argv[1], "x") != 0) { + return cmd_results_new(CMD_INVALID, "%s", usage); + } + + int height = (int)strtol(argv[2], &err, 10); + if (*err) { + return cmd_results_new(CMD_INVALID, "%s", usage); + } + + *config_width = width; + *config_height = height; + + return cmd_results_new(CMD_SUCCESS, NULL); +} + +struct cmd_results *cmd_floating_minimum_size(int argc, char **argv) { + return handle_command(argc, argv, "floating_minimum_size", min_usage, + &config->floating_minimum_width, &config->floating_minimum_height); +} + +struct cmd_results *cmd_floating_maximum_size(int argc, char **argv) { + return handle_command(argc, argv, "floating_maximum_size", max_usage, + &config->floating_maximum_width, &config->floating_maximum_height); +} diff --git a/sway/commands/floating_modifier.c b/sway/commands/floating_modifier.c new file mode 100644 index 0000000..c02bce9 --- /dev/null +++ b/sway/commands/floating_modifier.c @@ -0,0 +1,34 @@ +#include "strings.h" +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/input/keyboard.h" + +struct cmd_results *cmd_floating_modifier(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "floating_modifier", EXPECTED_AT_LEAST, 1))) { + return error; + } + + if (strcasecmp(argv[0], "none") == 0) { + config->floating_mod = 0; + return cmd_results_new(CMD_SUCCESS, NULL); + } + + uint32_t mod = get_modifier_mask_by_name(argv[0]); + if (!mod) { + return cmd_results_new(CMD_INVALID, "Invalid modifier"); + } + + if (argc == 1 || strcasecmp(argv[1], "normal") == 0) { + config->floating_mod_inverse = false; + } else if (strcasecmp(argv[1], "inverse") == 0) { + config->floating_mod_inverse = true; + } else { + return cmd_results_new(CMD_INVALID, + "Usage: floating_modifier [inverse|normal]"); + } + + config->floating_mod = mod; + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/focus.c b/sway/commands/focus.c new file mode 100644 index 0000000..facd82d --- /dev/null +++ b/sway/commands/focus.c @@ -0,0 +1,474 @@ +#include +#include +#include +#include "log.h" +#include "sway/commands.h" +#include "sway/input/input-manager.h" +#include "sway/input/cursor.h" +#include "sway/input/seat.h" +#include "sway/output.h" +#include "sway/tree/arrange.h" +#include "sway/tree/root.h" +#include "sway/tree/view.h" +#include "sway/tree/workspace.h" +#include "stringop.h" +#include "util.h" + +static bool get_direction_from_next_prev(struct sway_container *container, + struct sway_seat *seat, const char *name, enum wlr_direction *out) { + enum sway_container_layout parent_layout = L_NONE; + if (container) { + parent_layout = container_parent_layout(container); + } + + if (strcasecmp(name, "prev") == 0) { + switch (parent_layout) { + case L_HORIZ: + case L_TABBED: + *out = WLR_DIRECTION_LEFT; + break; + case L_VERT: + case L_STACKED: + *out = WLR_DIRECTION_UP; + break; + case L_NONE: + return true; + default: + return false; + } + } else if (strcasecmp(name, "next") == 0) { + switch (parent_layout) { + case L_HORIZ: + case L_TABBED: + *out = WLR_DIRECTION_RIGHT; + break; + case L_VERT: + case L_STACKED: + *out = WLR_DIRECTION_DOWN; + break; + case L_NONE: + return true; + default: + return false; + } + } else { + return false; + } + + return true; +} + +static bool parse_direction(const char *name, + enum wlr_direction *out) { + if (strcasecmp(name, "left") == 0) { + *out = WLR_DIRECTION_LEFT; + } else if (strcasecmp(name, "right") == 0) { + *out = WLR_DIRECTION_RIGHT; + } else if (strcasecmp(name, "up") == 0) { + *out = WLR_DIRECTION_UP; + } else if (strcasecmp(name, "down") == 0) { + *out = WLR_DIRECTION_DOWN; + } else { + return false; + } + + return true; +} + +/** + * Get node in the direction of newly entered output. + */ +static struct sway_node *get_node_in_output_direction( + struct sway_output *output, enum wlr_direction dir) { + struct sway_seat *seat = config->handler_context.seat; + struct sway_workspace *ws = output_get_active_workspace(output); + if (!sway_assert(ws, "Expected output to have a workspace")) { + return NULL; + } + if (ws->fullscreen) { + return seat_get_focus_inactive(seat, &ws->fullscreen->node); + } + struct sway_container *container = NULL; + + if (ws->tiling->length > 0) { + switch (dir) { + case WLR_DIRECTION_LEFT: + if (ws->layout == L_HORIZ || ws->layout == L_TABBED) { + // get most right child of new output + container = ws->tiling->items[ws->tiling->length-1]; + } else { + container = seat_get_focus_inactive_tiling(seat, ws); + } + break; + case WLR_DIRECTION_RIGHT: + if (ws->layout == L_HORIZ || ws->layout == L_TABBED) { + // get most left child of new output + container = ws->tiling->items[0]; + } else { + container = seat_get_focus_inactive_tiling(seat, ws); + } + break; + case WLR_DIRECTION_UP: + if (ws->layout == L_VERT || ws->layout == L_STACKED) { + // get most bottom child of new output + container = ws->tiling->items[ws->tiling->length-1]; + } else { + container = seat_get_focus_inactive_tiling(seat, ws); + } + break; + case WLR_DIRECTION_DOWN: + if (ws->layout == L_VERT || ws->layout == L_STACKED) { + // get most top child of new output + container = ws->tiling->items[0]; + } else { + container = seat_get_focus_inactive_tiling(seat, ws); + } + break; + } + } + + if (container) { + container = seat_get_focus_inactive_view(seat, &container->node); + return &container->node; + } + + return &ws->node; +} + +static struct sway_node *node_get_in_direction_tiling( + struct sway_container *container, struct sway_seat *seat, + enum wlr_direction dir, bool descend) { + struct sway_container *wrap_candidate = NULL; + struct sway_container *current = container; + while (current) { + if (current->pending.fullscreen_mode == FULLSCREEN_WORKSPACE) { + // Fullscreen container with a direction - go straight to outputs + struct sway_output *output = current->pending.workspace->output; + struct sway_output *new_output = + output_get_in_direction(output, dir); + if (!new_output) { + return NULL; + } + return get_node_in_output_direction(new_output, dir); + } + if (current->pending.fullscreen_mode == FULLSCREEN_GLOBAL) { + return NULL; + } + + bool can_move = false; + int desired; + int idx = container_sibling_index(current); + enum sway_container_layout parent_layout = + container_parent_layout(current); + list_t *siblings = container_get_siblings(current); + + if (dir == WLR_DIRECTION_LEFT || dir == WLR_DIRECTION_RIGHT) { + if (parent_layout == L_HORIZ || parent_layout == L_TABBED) { + can_move = true; + desired = idx + (dir == WLR_DIRECTION_LEFT ? -1 : 1); + } + } else { + if (parent_layout == L_VERT || parent_layout == L_STACKED) { + can_move = true; + desired = idx + (dir == WLR_DIRECTION_UP ? -1 : 1); + } + } + + if (can_move) { + if (desired < 0 || desired >= siblings->length) { + int len = siblings->length; + if (config->focus_wrapping != WRAP_NO && !wrap_candidate + && len > 1) { + if (desired < 0) { + wrap_candidate = siblings->items[len-1]; + } else { + wrap_candidate = siblings->items[0]; + } + if (config->focus_wrapping == WRAP_FORCE) { + struct sway_container *c = seat_get_focus_inactive_view( + seat, &wrap_candidate->node); + return &c->node; + } + } + } else { + struct sway_container *desired_con = siblings->items[desired]; + if (!descend) { + return &desired_con->node; + } else { + struct sway_container *c = seat_get_focus_inactive_view( + seat, &desired_con->node); + return &c->node; + } + } + } + + current = current->pending.parent; + } + + // Check a different output + struct sway_output *output = container->pending.workspace->output; + struct sway_output *new_output = output_get_in_direction(output, dir); + if ((config->focus_wrapping != WRAP_WORKSPACE || + container->node.type == N_WORKSPACE) && new_output) { + return get_node_in_output_direction(new_output, dir); + } + + // If there is a wrap candidate, return its focus inactive view + if (wrap_candidate) { + struct sway_container *wrap_inactive = seat_get_focus_inactive_view( + seat, &wrap_candidate->node); + return &wrap_inactive->node; + } + + return NULL; +} + +static struct sway_node *node_get_in_direction_floating( + struct sway_container *con, struct sway_seat *seat, + enum wlr_direction dir) { + double ref_lx = con->pending.x + con->pending.width / 2; + double ref_ly = con->pending.y + con->pending.height / 2; + double closest_distance = DBL_MAX; + struct sway_container *closest_con = NULL; + + if (!con->pending.workspace) { + return NULL; + } + + for (int i = 0; i < con->pending.workspace->floating->length; i++) { + struct sway_container *floater = con->pending.workspace->floating->items[i]; + if (floater == con) { + continue; + } + float distance = dir == WLR_DIRECTION_LEFT || dir == WLR_DIRECTION_RIGHT + ? (floater->pending.x + floater->pending.width / 2) - ref_lx + : (floater->pending.y + floater->pending.height / 2) - ref_ly; + if (dir == WLR_DIRECTION_LEFT || dir == WLR_DIRECTION_UP) { + distance = -distance; + } + if (distance < 0) { + continue; + } + if (distance < closest_distance) { + closest_distance = distance; + closest_con = floater; + } + } + + return closest_con ? &closest_con->node : NULL; +} + +static struct cmd_results *focus_mode(struct sway_workspace *ws, + struct sway_seat *seat, bool floating) { + struct sway_container *new_focus = NULL; + if (floating) { + new_focus = seat_get_focus_inactive_floating(seat, ws); + } else { + new_focus = seat_get_focus_inactive_tiling(seat, ws); + } + if (new_focus) { + struct sway_container *new_focus_view = + seat_get_focus_inactive_view(seat, &new_focus->node); + if (new_focus_view) { + new_focus = new_focus_view; + } + seat_set_focus_container(seat, new_focus); + + // If we're on the floating layer and the floating container area + // overlaps the position on the tiling layer that would be warped to, + // `seat_consider_warp_to_focus` would decide not to warp, but we need + // to anyway. + if (config->mouse_warping == WARP_CONTAINER) { + cursor_warp_to_container(seat->cursor, new_focus, true); + } else { + seat_consider_warp_to_focus(seat); + } + } else { + return cmd_results_new(CMD_FAILURE, + "Failed to find a %s container in workspace.", + floating ? "floating" : "tiling"); + } + return cmd_results_new(CMD_SUCCESS, NULL); +} + +static struct cmd_results *focus_output(struct sway_seat *seat, + int argc, char **argv) { + if (!argc) { + return cmd_results_new(CMD_INVALID, + "Expected 'focus output '."); + } + char *identifier = join_args(argv, argc); + struct sway_output *output = output_by_name_or_id(identifier); + + if (!output) { + enum wlr_direction direction; + if (!parse_direction(identifier, &direction)) { + free(identifier); + return cmd_results_new(CMD_INVALID, + "There is no output with that name."); + } + struct sway_workspace *ws = seat_get_focused_workspace(seat); + if (!ws) { + free(identifier); + return cmd_results_new(CMD_FAILURE, + "No focused workspace to base directions off of."); + } + output = output_get_in_direction(ws->output, direction); + + if (!output) { + int center_lx = ws->output->lx + ws->output->width / 2; + int center_ly = ws->output->ly + ws->output->height / 2; + struct wlr_output *target = wlr_output_layout_farthest_output( + root->output_layout, opposite_direction(direction), + ws->output->wlr_output, center_lx, center_ly); + if (target) { + output = output_from_wlr_output(target); + } + } + } + + free(identifier); + if (output) { + seat_set_focus(seat, seat_get_focus_inactive(seat, &output->node)); + seat_consider_warp_to_focus(seat); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} + +static struct cmd_results *focus_parent(void) { + struct sway_seat *seat = config->handler_context.seat; + struct sway_container *con = config->handler_context.container; + if (!con || con->pending.fullscreen_mode) { + return cmd_results_new(CMD_SUCCESS, NULL); + } + struct sway_node *parent = node_get_parent(&con->node); + if (parent) { + seat_set_focus(seat, parent); + seat_consider_warp_to_focus(seat); + } + return cmd_results_new(CMD_SUCCESS, NULL); +} + +static struct cmd_results *focus_child(void) { + struct sway_seat *seat = config->handler_context.seat; + struct sway_node *node = config->handler_context.node; + struct sway_node *focus = seat_get_active_tiling_child(seat, node); + if (focus) { + seat_set_focus(seat, focus); + seat_consider_warp_to_focus(seat); + } + return cmd_results_new(CMD_SUCCESS, NULL); +} + +struct cmd_results *cmd_focus(int argc, char **argv) { + if (config->reading || !config->active) { + return cmd_results_new(CMD_DEFER, NULL); + } + if (!root->outputs->length) { + return cmd_results_new(CMD_INVALID, + "Can't run this command while there's no outputs connected."); + } + struct sway_node *node = config->handler_context.node; + struct sway_container *container = config->handler_context.container; + struct sway_workspace *workspace = config->handler_context.workspace; + struct sway_seat *seat = config->handler_context.seat; + if (node->type < N_WORKSPACE) { + return cmd_results_new(CMD_FAILURE, + "Command 'focus' cannot be used above the workspace level."); + } + + if (argc == 0) { + if (!container) { + return cmd_results_new(CMD_FAILURE, "No container to focus was specified."); + } + + if (container_is_scratchpad_hidden_or_child(container)) { + root_scratchpad_show(container); + } + // if we are switching to a container under a fullscreen window, we first + // need to exit fullscreen so that the newly focused container becomes visible + struct sway_container *obstructing = container_obstructing_fullscreen_container(container); + if (obstructing) { + container_fullscreen_disable(obstructing); + arrange_root(); + } + seat_set_focus_container(seat, container); + seat_consider_warp_to_focus(seat); + container_raise_floating(container); + return cmd_results_new(CMD_SUCCESS, NULL); + } + + if (strcmp(argv[0], "floating") == 0) { + return focus_mode(workspace, seat, true); + } else if (strcmp(argv[0], "tiling") == 0) { + return focus_mode(workspace, seat, false); + } else if (strcmp(argv[0], "mode_toggle") == 0) { + bool floating = container && container_is_floating_or_child(container); + return focus_mode(workspace, seat, !floating); + } + + if (strcmp(argv[0], "output") == 0) { + argc--; argv++; + return focus_output(seat, argc, argv); + } + + if (strcasecmp(argv[0], "parent") == 0) { + return focus_parent(); + } + if (strcasecmp(argv[0], "child") == 0) { + return focus_child(); + } + + enum wlr_direction direction = 0; + bool descend = true; + if (!parse_direction(argv[0], &direction)) { + if (!get_direction_from_next_prev(container, seat, argv[0], &direction)) { + return cmd_results_new(CMD_INVALID, + "Expected 'focus ' " + "or 'focus output '"); + } else if (argc == 2 && strcasecmp(argv[1], "sibling") == 0) { + descend = false; + } + } + + if (!direction) { + return cmd_results_new(CMD_SUCCESS, NULL); + } + + if (node->type == N_WORKSPACE) { + // Jump to the next output + struct sway_output *new_output = + output_get_in_direction(workspace->output, direction); + if (!new_output) { + return cmd_results_new(CMD_SUCCESS, NULL); + } + + struct sway_node *node = + get_node_in_output_direction(new_output, direction); + seat_set_focus(seat, node); + seat_consider_warp_to_focus(seat); + return cmd_results_new(CMD_SUCCESS, NULL); + } + + if (!sway_assert(container, "Expected container to be non null")) { + return cmd_results_new(CMD_FAILURE, ""); + } + struct sway_node *next_focus = NULL; + if (container_is_floating(container) && + container->pending.fullscreen_mode == FULLSCREEN_NONE) { + next_focus = node_get_in_direction_floating(container, seat, direction); + } else { + next_focus = node_get_in_direction_tiling(container, seat, direction, descend); + } + if (next_focus) { + seat_set_focus(seat, next_focus); + seat_consider_warp_to_focus(seat); + + if (next_focus->type == N_CONTAINER) { + container_raise_floating(next_focus->sway_container); + } + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/focus_follows_mouse.c b/sway/commands/focus_follows_mouse.c new file mode 100644 index 0000000..7afb5f6 --- /dev/null +++ b/sway/commands/focus_follows_mouse.c @@ -0,0 +1,21 @@ +#include +#include +#include "sway/commands.h" +#include "util.h" + +struct cmd_results *cmd_focus_follows_mouse(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "focus_follows_mouse", EXPECTED_EQUAL_TO, 1))) { + return error; + } else if(strcmp(argv[0], "no") == 0) { + config->focus_follows_mouse = FOLLOWS_NO; + } else if(strcmp(argv[0], "yes") == 0) { + config->focus_follows_mouse = FOLLOWS_YES; + } else if(strcmp(argv[0], "always") == 0) { + config->focus_follows_mouse = FOLLOWS_ALWAYS; + } else { + return cmd_results_new(CMD_FAILURE, + "Expected 'focus_follows_mouse no|yes|always'"); + } + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/focus_on_window_activation.c b/sway/commands/focus_on_window_activation.c new file mode 100644 index 0000000..2163fc5 --- /dev/null +++ b/sway/commands/focus_on_window_activation.c @@ -0,0 +1,25 @@ +#include "sway/commands.h" + +struct cmd_results *cmd_focus_on_window_activation(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "focus_on_window_activation", + EXPECTED_EQUAL_TO, 1))) { + return error; + } + + if (strcmp(argv[0], "smart") == 0) { + config->focus_on_window_activation = FOWA_SMART; + } else if (strcmp(argv[0], "urgent") == 0) { + config->focus_on_window_activation = FOWA_URGENT; + } else if (strcmp(argv[0], "focus") == 0) { + config->focus_on_window_activation = FOWA_FOCUS; + } else if (strcmp(argv[0], "none") == 0) { + config->focus_on_window_activation = FOWA_NONE; + } else { + return cmd_results_new(CMD_INVALID, + "Expected " + "'focus_on_window_activation smart|urgent|focus|none'"); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/focus_wrapping.c b/sway/commands/focus_wrapping.c new file mode 100644 index 0000000..d3901cb --- /dev/null +++ b/sway/commands/focus_wrapping.c @@ -0,0 +1,23 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "util.h" + +struct cmd_results *cmd_focus_wrapping(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "focus_wrapping", EXPECTED_EQUAL_TO, 1))) { + return error; + } + + if (strcasecmp(argv[0], "force") == 0) { + config->focus_wrapping = WRAP_FORCE; + } else if (strcasecmp(argv[0], "workspace") == 0) { + config->focus_wrapping = WRAP_WORKSPACE; + } else if (parse_boolean(argv[0], config->focus_wrapping == WRAP_YES)) { + config->focus_wrapping = WRAP_YES; + } else { + config->focus_wrapping = WRAP_NO; + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/font.c b/sway/commands/font.c new file mode 100644 index 0000000..842e8ae --- /dev/null +++ b/sway/commands/font.c @@ -0,0 +1,49 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "log.h" +#include "stringop.h" +#include + +struct cmd_results *cmd_font(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "font", EXPECTED_AT_LEAST, 1))) { + return error; + } + char *font = join_args(argv, argc); + free(config->font); + + if (has_prefix(font, "pango:")) { + config->pango_markup = true; + config->font = strdup(font + strlen("pango:")); + free(font); + } else { + config->pango_markup = false; + config->font = font; + } + + // Parse the font early so we can reject it if it's not valid for pango. + // Also avoids re-parsing each time we render text. + PangoFontDescription *font_description = pango_font_description_from_string(config->font); + + const char *family = pango_font_description_get_family(font_description); + if (family == NULL) { + pango_font_description_free(font_description); + return cmd_results_new(CMD_FAILURE, "Invalid font family."); + } + + const gint size = pango_font_description_get_size(font_description); + if (size == 0) { + pango_font_description_free(font_description); + return cmd_results_new(CMD_FAILURE, "Invalid font size."); + } + + if (config->font_description != NULL) { + pango_font_description_free(config->font_description); + } + + config->font_description = font_description; + config_update_font_height(); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/for_window.c b/sway/commands/for_window.c new file mode 100644 index 0000000..905e677 --- /dev/null +++ b/sway/commands/for_window.c @@ -0,0 +1,37 @@ +#include +#include "sway/commands.h" +#include "sway/criteria.h" +#include "list.h" +#include "log.h" +#include "stringop.h" + +struct cmd_results *cmd_for_window(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "for_window", EXPECTED_AT_LEAST, 2))) { + return error; + } + + char *err_str = NULL; + struct criteria *criteria = criteria_parse(argv[0], &err_str); + if (!criteria) { + error = cmd_results_new(CMD_INVALID, "%s", err_str); + free(err_str); + return error; + } + + criteria->type = CT_COMMAND; + criteria->cmdlist = join_args(argv + 1, argc - 1); + + // Check if it already exists + if (criteria_already_exists(criteria)) { + sway_log(SWAY_DEBUG, "for_window already exists: '%s' -> '%s'", + criteria->raw, criteria->cmdlist); + criteria_destroy(criteria); + return cmd_results_new(CMD_SUCCESS, NULL); + } + + list_add(config->criteria, criteria); + sway_log(SWAY_DEBUG, "for_window: '%s' -> '%s' added", criteria->raw, criteria->cmdlist); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/force_display_urgency_hint.c b/sway/commands/force_display_urgency_hint.c new file mode 100644 index 0000000..3d432cb --- /dev/null +++ b/sway/commands/force_display_urgency_hint.c @@ -0,0 +1,27 @@ +#include "sway/commands.h" +#include "sway/config.h" +#include + +struct cmd_results *cmd_force_display_urgency_hint(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "force_display_urgency_hint", + EXPECTED_AT_LEAST, 1))) { + return error; + } + + errno = 0; + char *end; + int timeout = (int)strtol(argv[0], &end, 10); + if (errno || end == argv[0] || (*end && strcmp(end, "ms") != 0)) { + return cmd_results_new(CMD_INVALID, "timeout integer invalid"); + } + + if (argc > 1 && strcmp(argv[1], "ms") != 0) { + return cmd_results_new(CMD_INVALID, + "Expected 'force_display_urgency_hint [ms]'"); + } + + config->urgent_timeout = timeout > 0 ? timeout : 0; + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/force_focus_wrapping.c b/sway/commands/force_focus_wrapping.c new file mode 100644 index 0000000..fafc1c3 --- /dev/null +++ b/sway/commands/force_focus_wrapping.c @@ -0,0 +1,27 @@ +#include "sway/commands.h" +#include "sway/config.h" +#include "log.h" +#include "util.h" + +struct cmd_results *cmd_force_focus_wrapping(int argc, char **argv) { + sway_log(SWAY_INFO, "Warning: force_focus_wrapping is deprecated. " + "Use focus_wrapping instead."); + if (config->reading) { + config_add_swaynag_warning("force_focus_wrapping is deprecated. " + "Use focus_wrapping instead."); + } + + struct cmd_results *error = + checkarg(argc, "force_focus_wrapping", EXPECTED_EQUAL_TO, 1); + if (error) { + return error; + } + + if (parse_boolean(argv[0], config->focus_wrapping == WRAP_FORCE)) { + config->focus_wrapping = WRAP_FORCE; + } else { + config->focus_wrapping = WRAP_YES; + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/fullscreen.c b/sway/commands/fullscreen.c new file mode 100644 index 0000000..21c1e9a --- /dev/null +++ b/sway/commands/fullscreen.c @@ -0,0 +1,58 @@ +#include +#include "log.h" +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/tree/arrange.h" +#include "sway/tree/container.h" +#include "sway/tree/view.h" +#include "sway/tree/workspace.h" +#include "util.h" + +// fullscreen [enable|disable|toggle] [global] +struct cmd_results *cmd_fullscreen(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "fullscreen", EXPECTED_AT_MOST, 2))) { + return error; + } + if (!root->outputs->length) { + return cmd_results_new(CMD_FAILURE, + "Can't run this command while there's no outputs connected."); + } + struct sway_container *container = config->handler_context.container; + + if (!container) { + // If the focus is not a container, do nothing successfully + return cmd_results_new(CMD_SUCCESS, NULL); + } else if (!container->pending.workspace) { + // If in the scratchpad, operate on the highest container + while (container->pending.parent) { + container = container->pending.parent; + } + } + + bool is_fullscreen = container->pending.fullscreen_mode != FULLSCREEN_NONE; + bool global = false; + bool enable = !is_fullscreen; + + if (argc >= 1) { + if (strcasecmp(argv[0], "global") == 0) { + global = true; + } else { + enable = parse_boolean(argv[0], is_fullscreen); + } + } + + if (argc >= 2) { + global = strcasecmp(argv[1], "global") == 0; + } + + enum sway_fullscreen_mode mode = FULLSCREEN_NONE; + if (enable) { + mode = global ? FULLSCREEN_GLOBAL : FULLSCREEN_WORKSPACE; + } + + container_set_fullscreen(container, mode); + arrange_root(); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/gaps.c b/sway/commands/gaps.c new file mode 100644 index 0000000..7ac6fcf --- /dev/null +++ b/sway/commands/gaps.c @@ -0,0 +1,229 @@ +#include +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/tree/arrange.h" +#include "sway/tree/workspace.h" +#include "log.h" +#include "stringop.h" +#include + +enum gaps_op { + GAPS_OP_SET, + GAPS_OP_ADD, + GAPS_OP_SUBTRACT, + GAPS_OP_TOGGLE +}; + +struct gaps_data { + bool inner; + struct { + bool top; + bool right; + bool bottom; + bool left; + } outer; + enum gaps_op operation; + int amount; +}; + +// Prevent negative outer gaps from moving windows out of the workspace. +static void prevent_invalid_outer_gaps(void) { + if (config->gaps_outer.top < -config->gaps_inner) { + config->gaps_outer.top = -config->gaps_inner; + } + if (config->gaps_outer.right < -config->gaps_inner) { + config->gaps_outer.right = -config->gaps_inner; + } + if (config->gaps_outer.bottom < -config->gaps_inner) { + config->gaps_outer.bottom = -config->gaps_inner; + } + if (config->gaps_outer.left < -config->gaps_inner) { + config->gaps_outer.left = -config->gaps_inner; + } +} + +// gaps inner|outer|horizontal|vertical|top|right|bottom|left +static const char expected_defaults[] = + "'gaps inner|outer|horizontal|vertical|top|right|bottom|left '"; +static struct cmd_results *gaps_set_defaults(int argc, char **argv) { + struct cmd_results *error = checkarg(argc, "gaps", EXPECTED_EQUAL_TO, 2); + if (error) { + return error; + } + + char *end; + int amount = strtol(argv[1], &end, 10); + if (strlen(end) && strcasecmp(end, "px") != 0) { + return cmd_results_new(CMD_INVALID, "Expected %s", expected_defaults); + } + + bool valid = false; + if (!strcasecmp(argv[0], "inner")) { + valid = true; + config->gaps_inner = (amount >= 0) ? amount : 0; + } else { + if (!strcasecmp(argv[0], "outer") || !strcasecmp(argv[0], "vertical") + || !strcasecmp(argv[0], "top")) { + valid = true; + config->gaps_outer.top = amount; + } + if (!strcasecmp(argv[0], "outer") || !strcasecmp(argv[0], "horizontal") + || !strcasecmp(argv[0], "right")) { + valid = true; + config->gaps_outer.right = amount; + } + if (!strcasecmp(argv[0], "outer") || !strcasecmp(argv[0], "vertical") + || !strcasecmp(argv[0], "bottom")) { + valid = true; + config->gaps_outer.bottom = amount; + } + if (!strcasecmp(argv[0], "outer") || !strcasecmp(argv[0], "horizontal") + || !strcasecmp(argv[0], "left")) { + valid = true; + config->gaps_outer.left = amount; + } + } + if (!valid) { + return cmd_results_new(CMD_INVALID, "Expected %s", expected_defaults); + } + + prevent_invalid_outer_gaps(); + return cmd_results_new(CMD_SUCCESS, NULL); +} + +static void apply_gaps_op(int *prop, enum gaps_op op, int amount) { + switch (op) { + case GAPS_OP_SET: + *prop = amount; + break; + case GAPS_OP_ADD: + *prop += amount; + break; + case GAPS_OP_SUBTRACT: + *prop -= amount; + break; + case GAPS_OP_TOGGLE: + *prop = *prop ? 0 : amount; + break; + } +} + +static void configure_gaps(struct sway_workspace *ws, void *_data) { + // Apply operation to gaps + struct gaps_data *data = _data; + if (data->inner) { + apply_gaps_op(&ws->gaps_inner, data->operation, data->amount); + } + if (data->outer.top) { + apply_gaps_op(&(ws->gaps_outer.top), data->operation, data->amount); + } + if (data->outer.right) { + apply_gaps_op(&(ws->gaps_outer.right), data->operation, data->amount); + } + if (data->outer.bottom) { + apply_gaps_op(&(ws->gaps_outer.bottom), data->operation, data->amount); + } + if (data->outer.left) { + apply_gaps_op(&(ws->gaps_outer.left), data->operation, data->amount); + } + + // Prevent invalid gaps configurations. + if (ws->gaps_inner < 0) { + ws->gaps_inner = 0; + } + prevent_invalid_outer_gaps(); + arrange_workspace(ws); +} + +// gaps inner|outer|horizontal|vertical|top|right|bottom|left current|all +// set|plus|minus|toggle +static const char expected_runtime[] = "'gaps inner|outer|horizontal|vertical|" + "top|right|bottom|left current|all set|plus|minus|toggle '"; +static struct cmd_results *gaps_set_runtime(int argc, char **argv) { + struct cmd_results *error = checkarg(argc, "gaps", EXPECTED_EQUAL_TO, 4); + if (error) { + return error; + } + if (!root->outputs->length) { + return cmd_results_new(CMD_INVALID, + "Can't run this command while there's no outputs connected."); + } + + struct gaps_data data = {0}; + + if (strcasecmp(argv[0], "inner") == 0) { + data.inner = true; + } else { + data.outer.top = !strcasecmp(argv[0], "outer") || + !strcasecmp(argv[0], "vertical") || !strcasecmp(argv[0], "top"); + data.outer.right = !strcasecmp(argv[0], "outer") || + !strcasecmp(argv[0], "horizontal") || !strcasecmp(argv[0], "right"); + data.outer.bottom = !strcasecmp(argv[0], "outer") || + !strcasecmp(argv[0], "vertical") || !strcasecmp(argv[0], "bottom"); + data.outer.left = !strcasecmp(argv[0], "outer") || + !strcasecmp(argv[0], "horizontal") || !strcasecmp(argv[0], "left"); + } + if (!data.inner && !data.outer.top && !data.outer.right && + !data.outer.bottom && !data.outer.left) { + return cmd_results_new(CMD_INVALID, "Expected %s", expected_runtime); + } + + bool all; + if (strcasecmp(argv[1], "current") == 0) { + all = false; + } else if (strcasecmp(argv[1], "all") == 0) { + all = true; + } else { + return cmd_results_new(CMD_INVALID, "Expected %s", expected_runtime); + } + + if (strcasecmp(argv[2], "set") == 0) { + data.operation = GAPS_OP_SET; + } else if (strcasecmp(argv[2], "plus") == 0) { + data.operation = GAPS_OP_ADD; + } else if (strcasecmp(argv[2], "minus") == 0) { + data.operation = GAPS_OP_SUBTRACT; + } else if (strcasecmp(argv[2], "toggle") == 0) { + data.operation = GAPS_OP_TOGGLE; + } else { + return cmd_results_new(CMD_INVALID, "Expected %s", expected_runtime); + } + + char *end; + data.amount = strtol(argv[3], &end, 10); + if (strlen(end) && strcasecmp(end, "px") != 0) { + return cmd_results_new(CMD_INVALID, "Expected %s", expected_runtime); + } + + if (all) { + root_for_each_workspace(configure_gaps, &data); + } else { + configure_gaps(config->handler_context.workspace, &data); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} + +// gaps inner|outer|| - sets defaults for workspaces +// gaps inner|outer|| current|all set|plus|minus|toggle - runtime only +// = horizontal|vertical +// = top|right|bottom|left +struct cmd_results *cmd_gaps(int argc, char **argv) { + struct cmd_results *error = checkarg(argc, "gaps", EXPECTED_AT_LEAST, 2); + if (error) { + return error; + } + + if (argc == 2) { + return gaps_set_defaults(argc, argv); + } + if (argc == 4 && !config->reading) { + return gaps_set_runtime(argc, argv); + } + if (config->reading) { + return cmd_results_new(CMD_INVALID, "Expected %s", expected_defaults); + } + return cmd_results_new(CMD_INVALID, "Expected %s or %s", + expected_runtime, expected_defaults); +} diff --git a/sway/commands/gesture.c b/sway/commands/gesture.c new file mode 100644 index 0000000..8dff29a --- /dev/null +++ b/sway/commands/gesture.c @@ -0,0 +1,164 @@ +#include "sway/config.h" + +#include "gesture.h" +#include "log.h" +#include "stringop.h" +#include "sway/commands.h" + +void free_gesture_binding(struct sway_gesture_binding *binding) { + if (!binding) { + return; + } + free(binding->input); + free(binding->command); + free(binding); +} + +/** + * Returns true if the bindings have the same gesture type, direction, etc + */ +static bool binding_gesture_equal(struct sway_gesture_binding *binding_a, + struct sway_gesture_binding *binding_b) { + if (strcmp(binding_a->input, binding_b->input) != 0) { + return false; + } + + if (!gesture_equal(&binding_a->gesture, &binding_b->gesture)) { + return false; + } + + if ((binding_a->flags & BINDING_EXACT) != + (binding_b->flags & BINDING_EXACT)) { + return false; + } + return true; +} + +/** + * Add gesture binding to config + */ +static struct cmd_results *gesture_binding_add( + struct sway_gesture_binding *binding, + const char *gesturecombo, bool warn) { + list_t *mode_bindings = config->current_mode->gesture_bindings; + // overwrite the binding if it already exists + bool overwritten = false; + for (int i = 0; i < mode_bindings->length; ++i) { + struct sway_gesture_binding *config_binding = mode_bindings->items[i]; + if (binding_gesture_equal(binding, config_binding)) { + sway_log(SWAY_INFO, "Overwriting binding '%s' to `%s` from `%s`", + gesturecombo, binding->command, config_binding->command); + if (warn) { + config_add_swaynag_warning("Overwriting binding" + "'%s' to `%s` from `%s`", + gesturecombo, binding->command, + config_binding->command); + } + free_gesture_binding(config_binding); + mode_bindings->items[i] = binding; + overwritten = true; + } + } + + if (!overwritten) { + list_add(mode_bindings, binding); + sway_log(SWAY_DEBUG, "bindgesture - Bound %s to command `%s`", + gesturecombo, binding->command); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} + +/** + * Remove gesture binding from config + */ +static struct cmd_results *gesture_binding_remove( + struct sway_gesture_binding *binding, const char *gesturecombo) { + list_t *mode_bindings = config->current_mode->gesture_bindings; + for (int i = 0; i < mode_bindings->length; ++i) { + struct sway_gesture_binding *config_binding = mode_bindings->items[i]; + if (binding_gesture_equal(binding, config_binding)) { + free_gesture_binding(config_binding); + free_gesture_binding(binding); + list_del(mode_bindings, i); + sway_log(SWAY_DEBUG, "unbindgesture - Unbound %s gesture", + gesturecombo); + return cmd_results_new(CMD_SUCCESS, NULL); + } + } + + free_gesture_binding(binding); + return cmd_results_new(CMD_FAILURE, "Could not find gesture binding `%s`", + gesturecombo); +} + +/** + * Parse and execute bindgesture or unbindgesture command. + */ +static struct cmd_results *cmd_bind_or_unbind_gesture(int argc, char **argv, bool unbind) { + int minargs = 2; + char *bindtype = "bindgesture"; + if (unbind) { + minargs--; + bindtype = "unbindgesture"; + } + + struct cmd_results *error = NULL; + if ((error = checkarg(argc, bindtype, EXPECTED_AT_LEAST, minargs))) { + return error; + } + struct sway_gesture_binding *binding = calloc(1, sizeof(struct sway_gesture_binding)); + if (!binding) { + return cmd_results_new(CMD_FAILURE, "Unable to allocate binding"); + } + binding->input = strdup("*"); + + bool warn = true; + + // Handle flags + while (argc > 0) { + if (strcmp("--exact", argv[0]) == 0) { + binding->flags |= BINDING_EXACT; + } else if (strcmp("--no-warn", argv[0]) == 0) { + warn = false; + } else if (has_prefix(argv[0], "--input-device=")) { + free(binding->input); + binding->input = strdup(argv[0] + strlen("--input-device=")); + } else { + break; + } + argv++; + argc--; + } + + if (argc < minargs) { + free(binding); + return cmd_results_new(CMD_FAILURE, + "Invalid %s command (expected at least %d " + "non-option arguments, got %d)", bindtype, minargs, argc); + } + + char* errmsg = NULL; + if ((errmsg = gesture_parse(argv[0], &binding->gesture))) { + free(binding); + struct cmd_results *final = cmd_results_new(CMD_FAILURE, + "Invalid %s command (%s)", + bindtype, errmsg); + free(errmsg); + return final; + } + + if (unbind) { + return gesture_binding_remove(binding, argv[0]); + } + binding->command = join_args(argv + 1, argc - 1); + return gesture_binding_add(binding, argv[0], warn); +} + +struct cmd_results *cmd_bindgesture(int argc, char **argv) { + return cmd_bind_or_unbind_gesture(argc, argv, false); +} + +struct cmd_results *cmd_unbindgesture(int argc, char **argv) { + return cmd_bind_or_unbind_gesture(argc, argv, true); +} diff --git a/sway/commands/hide_edge_borders.c b/sway/commands/hide_edge_borders.c new file mode 100644 index 0000000..43bd6dc --- /dev/null +++ b/sway/commands/hide_edge_borders.c @@ -0,0 +1,48 @@ +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/tree/arrange.h" +#include "sway/tree/view.h" + +struct cmd_results *cmd_hide_edge_borders(int argc, char **argv) { + const char *expected_syntax = "Expected 'hide_edge_borders [--i3] " + "none|vertical|horizontal|both|smart|smart_no_gaps"; + + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "hide_edge_borders", EXPECTED_AT_LEAST, 1))) { + return error; + } + + bool hide_lone_tab = false; + if (strcmp(*argv, "--i3") == 0) { + hide_lone_tab = true; + ++argv; + --argc; + } + + if (!argc) { + return cmd_results_new(CMD_INVALID, "%s", expected_syntax); + } + + if (strcmp(argv[0], "none") == 0) { + config->hide_edge_borders = E_NONE; + } else if (strcmp(argv[0], "vertical") == 0) { + config->hide_edge_borders = E_VERTICAL; + } else if (strcmp(argv[0], "horizontal") == 0) { + config->hide_edge_borders = E_HORIZONTAL; + } else if (strcmp(argv[0], "both") == 0) { + config->hide_edge_borders = E_BOTH; + } else if (strcmp(argv[0], "smart") == 0) { + config->hide_edge_borders = E_NONE; + config->hide_edge_borders_smart = ESMART_ON; + } else if (strcmp(argv[0], "smart_no_gaps") == 0) { + config->hide_edge_borders = E_NONE; + config->hide_edge_borders_smart = ESMART_NO_GAPS; + } else { + return cmd_results_new(CMD_INVALID, "%s", expected_syntax); + } + config->hide_lone_tab = hide_lone_tab; + + arrange_root(); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/include.c b/sway/commands/include.c new file mode 100644 index 0000000..e0d0c06 --- /dev/null +++ b/sway/commands/include.c @@ -0,0 +1,14 @@ +#include "sway/commands.h" +#include "sway/config.h" + +struct cmd_results *cmd_include(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "include", EXPECTED_AT_LEAST, 1))) { + return error; + } + char *files = join_args(argv, argc); + // We don't care if the included config(s) fails to load. + load_include_configs(files, config, &config->swaynag_config_errors); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/inhibit_idle.c b/sway/commands/inhibit_idle.c new file mode 100644 index 0000000..6125736 --- /dev/null +++ b/sway/commands/inhibit_idle.c @@ -0,0 +1,51 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/desktop/idle_inhibit_v1.h" +#include "sway/tree/container.h" +#include "sway/tree/view.h" + +struct cmd_results *cmd_inhibit_idle(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "inhibit_idle", EXPECTED_EQUAL_TO, 1))) { + return error; + } + + struct sway_container *con = config->handler_context.container; + if (!con || !con->view) { + return cmd_results_new(CMD_INVALID, + "Only views can have idle inhibitors"); + } + + bool clear = false; + enum sway_idle_inhibit_mode mode; + if (strcmp(argv[0], "focus") == 0) { + mode = INHIBIT_IDLE_FOCUS; + } else if (strcmp(argv[0], "fullscreen") == 0) { + mode = INHIBIT_IDLE_FULLSCREEN; + } else if (strcmp(argv[0], "open") == 0) { + mode = INHIBIT_IDLE_OPEN; + } else if (strcmp(argv[0], "none") == 0) { + clear = true; + } else if (strcmp(argv[0], "visible") == 0) { + mode = INHIBIT_IDLE_VISIBLE; + } else { + return cmd_results_new(CMD_INVALID, + "Expected `inhibit_idle focus|fullscreen|open|none|visible`"); + } + + struct sway_idle_inhibitor_v1 *inhibitor = + sway_idle_inhibit_v1_user_inhibitor_for_view(con->view); + if (inhibitor) { + if (clear) { + sway_idle_inhibit_v1_user_inhibitor_destroy(inhibitor); + } else { + inhibitor->mode = mode; + sway_idle_inhibit_v1_check_active(); + } + } else if (!clear) { + sway_idle_inhibit_v1_user_inhibitor_register(con->view, mode); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input.c b/sway/commands/input.c new file mode 100644 index 0000000..310375a --- /dev/null +++ b/sway/commands/input.c @@ -0,0 +1,107 @@ +#include +#include +#include "sway/commands.h" +#include "sway/input/input-manager.h" +#include "sway/input/keyboard.h" +#include "log.h" +#include "stringop.h" + +// must be in order for the bsearch +static const struct cmd_handler input_handlers[] = { + { "accel_profile", input_cmd_accel_profile }, + { "calibration_matrix", input_cmd_calibration_matrix }, + { "click_method", input_cmd_click_method }, + { "clickfinger_button_map", input_cmd_clickfinger_button_map }, + { "drag", input_cmd_drag }, + { "drag_lock", input_cmd_drag_lock }, + { "dwt", input_cmd_dwt }, + { "dwtp", input_cmd_dwtp }, + { "events", input_cmd_events }, + { "left_handed", input_cmd_left_handed }, + { "map_from_region", input_cmd_map_from_region }, + { "map_to_output", input_cmd_map_to_output }, + { "map_to_region", input_cmd_map_to_region }, + { "middle_emulation", input_cmd_middle_emulation }, + { "natural_scroll", input_cmd_natural_scroll }, + { "pointer_accel", input_cmd_pointer_accel }, + { "repeat_delay", input_cmd_repeat_delay }, + { "repeat_rate", input_cmd_repeat_rate }, + { "rotation_angle", input_cmd_rotation_angle }, + { "scroll_button", input_cmd_scroll_button }, + { "scroll_button_lock", input_cmd_scroll_button_lock }, + { "scroll_factor", input_cmd_scroll_factor }, + { "scroll_method", input_cmd_scroll_method }, + { "tap", input_cmd_tap }, + { "tap_button_map", input_cmd_tap_button_map }, + { "tool_mode", input_cmd_tool_mode }, + { "xkb_file", input_cmd_xkb_file }, + { "xkb_layout", input_cmd_xkb_layout }, + { "xkb_model", input_cmd_xkb_model }, + { "xkb_options", input_cmd_xkb_options }, + { "xkb_rules", input_cmd_xkb_rules }, + { "xkb_switch_layout", input_cmd_xkb_switch_layout }, + { "xkb_variant", input_cmd_xkb_variant }, +}; + +// must be in order for the bsearch +static const struct cmd_handler input_config_handlers[] = { + { "xkb_capslock", input_cmd_xkb_capslock }, + { "xkb_numlock", input_cmd_xkb_numlock }, +}; + +struct cmd_results *cmd_input(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "input", EXPECTED_AT_LEAST, 2))) { + return error; + } + + sway_log(SWAY_DEBUG, "entering input block: %s", argv[0]); + + config->handler_context.input_config = new_input_config(argv[0]); + if (!config->handler_context.input_config) { + return cmd_results_new(CMD_FAILURE, "Couldn't allocate config"); + } + + struct cmd_results *res; + + if (find_handler(argv[1], input_config_handlers, + sizeof(input_config_handlers))) { + if (config->reading) { + res = config_subcommand(argv + 1, argc - 1, + input_config_handlers, sizeof(input_config_handlers)); + } else { + res = cmd_results_new(CMD_FAILURE, + "Can only be used in config file."); + } + } else { + res = config_subcommand(argv + 1, argc - 1, + input_handlers, sizeof(input_handlers)); + } + + if ((!res || res->status == CMD_SUCCESS) && + strcmp(argv[1], "xkb_switch_layout") != 0) { + char *error = NULL; + struct input_config *ic = + store_input_config(config->handler_context.input_config, &error); + if (!ic) { + free_input_config(config->handler_context.input_config); + if (res) { + free_cmd_results(res); + } + res = cmd_results_new(CMD_FAILURE, "Failed to compile keymap: %s", + error ? error : "(details unavailable)"); + free(error); + return res; + } + + if (!config->reading) { + input_manager_apply_input_config(ic); + } + } else { + free_input_config(config->handler_context.input_config); + } + + config->handler_context.input_config = NULL; + + return res; +} diff --git a/sway/commands/input/accel_profile.c b/sway/commands/input/accel_profile.c new file mode 100644 index 0000000..08f324c --- /dev/null +++ b/sway/commands/input/accel_profile.c @@ -0,0 +1,27 @@ +#include +#include +#include "sway/config.h" +#include "sway/commands.h" +#include "sway/input/input-manager.h" + +struct cmd_results *input_cmd_accel_profile(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "accel_profile", EXPECTED_AT_LEAST, 1))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + + if (strcasecmp(argv[0], "adaptive") == 0) { + ic->accel_profile = LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE; + } else if (strcasecmp(argv[0], "flat") == 0) { + ic->accel_profile = LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT; + } else { + return cmd_results_new(CMD_INVALID, + "Expected 'accel_profile '"); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input/calibration_matrix.c b/sway/commands/input/calibration_matrix.c new file mode 100644 index 0000000..53fe2c3 --- /dev/null +++ b/sway/commands/input/calibration_matrix.c @@ -0,0 +1,34 @@ +#include +#include +#include "sway/config.h" +#include "sway/commands.h" +#include "sway/input/input-manager.h" +#include "log.h" +#include "stringop.h" +#include "util.h" + +struct cmd_results *input_cmd_calibration_matrix(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "calibration_matrix", EXPECTED_EQUAL_TO, 6))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + + float parsed[6]; + for (int i = 0; i < argc; ++i) { + char *item = argv[i]; + float x = parse_float(item); + if (isnan(x)) { + return cmd_results_new(CMD_FAILURE, "calibration_matrix: unable to parse float: %s", item); + } + parsed[i] = x; + } + + ic->calibration_matrix.configured = true; + memcpy(ic->calibration_matrix.matrix, parsed, sizeof(ic->calibration_matrix.matrix)); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input/click_method.c b/sway/commands/input/click_method.c new file mode 100644 index 0000000..919c7cb --- /dev/null +++ b/sway/commands/input/click_method.c @@ -0,0 +1,30 @@ +#include +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/input/input-manager.h" +#include "log.h" + +struct cmd_results *input_cmd_click_method(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "click_method", EXPECTED_AT_LEAST, 1))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + + if (strcasecmp(argv[0], "none") == 0) { + ic->click_method = LIBINPUT_CONFIG_CLICK_METHOD_NONE; + } else if (strcasecmp(argv[0], "button_areas") == 0) { + ic->click_method = LIBINPUT_CONFIG_CLICK_METHOD_BUTTON_AREAS; + } else if (strcasecmp(argv[0], "clickfinger") == 0) { + ic->click_method = LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER; + } else { + return cmd_results_new(CMD_INVALID, + "Expected 'click_method '"); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input/clickfinger_button_map.c b/sway/commands/input/clickfinger_button_map.c new file mode 100644 index 0000000..57d6e39 --- /dev/null +++ b/sway/commands/input/clickfinger_button_map.c @@ -0,0 +1,27 @@ +#include +#include +#include "sway/config.h" +#include "sway/commands.h" +#include "sway/input/input-manager.h" + +struct cmd_results *input_cmd_clickfinger_button_map(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "clickfinger_button_map", EXPECTED_AT_LEAST, 1))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + + if (strcasecmp(argv[0], "lrm") == 0) { + ic->clickfinger_button_map = LIBINPUT_CONFIG_CLICKFINGER_MAP_LRM; + } else if (strcasecmp(argv[0], "lmr") == 0) { + ic->clickfinger_button_map = LIBINPUT_CONFIG_CLICKFINGER_MAP_LMR; + } else { + return cmd_results_new(CMD_INVALID, + "Expected 'clickfinger_button_map '"); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input/drag.c b/sway/commands/input/drag.c new file mode 100644 index 0000000..062357d --- /dev/null +++ b/sway/commands/input/drag.c @@ -0,0 +1,25 @@ +#include +#include +#include "sway/config.h" +#include "sway/commands.h" +#include "sway/input/input-manager.h" +#include "util.h" + +struct cmd_results *input_cmd_drag(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "drag", EXPECTED_AT_LEAST, 1))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + + if (parse_boolean(argv[0], true)) { + ic->drag = LIBINPUT_CONFIG_DRAG_ENABLED; + } else { + ic->drag = LIBINPUT_CONFIG_DRAG_DISABLED; + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input/drag_lock.c b/sway/commands/input/drag_lock.c new file mode 100644 index 0000000..915a7ad --- /dev/null +++ b/sway/commands/input/drag_lock.c @@ -0,0 +1,30 @@ +#include +#include +#include "sway/config.h" +#include "sway/commands.h" +#include "sway/input/input-manager.h" +#include "util.h" + +struct cmd_results *input_cmd_drag_lock(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "drag_lock", EXPECTED_AT_LEAST, 1))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + +#if HAVE_LIBINPUT_CONFIG_DRAG_LOCK_ENABLED_STICKY + if (strcmp(argv[0], "enabled_sticky") == 0) { + ic->drag_lock = LIBINPUT_CONFIG_DRAG_LOCK_ENABLED_STICKY; + } else +#endif + if (parse_boolean(argv[0], true)) { + ic->drag_lock = LIBINPUT_CONFIG_DRAG_LOCK_ENABLED; + } else { + ic->drag_lock = LIBINPUT_CONFIG_DRAG_LOCK_DISABLED; + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input/dwt.c b/sway/commands/input/dwt.c new file mode 100644 index 0000000..a7a4e49 --- /dev/null +++ b/sway/commands/input/dwt.c @@ -0,0 +1,25 @@ +#include +#include +#include "sway/config.h" +#include "sway/commands.h" +#include "sway/input/input-manager.h" +#include "util.h" + +struct cmd_results *input_cmd_dwt(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "dwt", EXPECTED_AT_LEAST, 1))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + + if (parse_boolean(argv[0], true)) { + ic->dwt = LIBINPUT_CONFIG_DWT_ENABLED; + } else { + ic->dwt = LIBINPUT_CONFIG_DWT_DISABLED; + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input/dwtp.c b/sway/commands/input/dwtp.c new file mode 100644 index 0000000..232e2b2 --- /dev/null +++ b/sway/commands/input/dwtp.c @@ -0,0 +1,25 @@ +#include +#include +#include "sway/config.h" +#include "sway/commands.h" +#include "sway/input/input-manager.h" +#include "util.h" + +struct cmd_results *input_cmd_dwtp(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "dwtp", EXPECTED_AT_LEAST, 1))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + + if (parse_boolean(argv[0], true)) { + ic->dwtp = LIBINPUT_CONFIG_DWTP_ENABLED; + } else { + ic->dwtp = LIBINPUT_CONFIG_DWTP_DISABLED; + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input/events.c b/sway/commands/input/events.c new file mode 100644 index 0000000..4650108 --- /dev/null +++ b/sway/commands/input/events.c @@ -0,0 +1,163 @@ +#include +#include +#include +#include +#include "sway/config.h" +#include "sway/commands.h" +#include "sway/input/input-manager.h" +#include "sway/server.h" +#include "log.h" + +#if WLR_HAS_LIBINPUT_BACKEND +#include +#endif + +static void toggle_supported_send_events_for_device(struct input_config *ic, + struct sway_input_device *input_device) { +#if WLR_HAS_LIBINPUT_BACKEND + struct wlr_input_device *wlr_device = input_device->wlr_device; + if (!wlr_input_device_is_libinput(wlr_device)) { + return; + } + struct libinput_device *libinput_dev = + wlr_libinput_get_device_handle(wlr_device); + + enum libinput_config_send_events_mode mode = + libinput_device_config_send_events_get_mode(libinput_dev); + uint32_t possible = + libinput_device_config_send_events_get_modes(libinput_dev); + + switch (mode) { + case LIBINPUT_CONFIG_SEND_EVENTS_ENABLED: + mode = LIBINPUT_CONFIG_SEND_EVENTS_DISABLED_ON_EXTERNAL_MOUSE; + if (possible & mode) { + break; + } + // fall through + case LIBINPUT_CONFIG_SEND_EVENTS_DISABLED_ON_EXTERNAL_MOUSE: + mode = LIBINPUT_CONFIG_SEND_EVENTS_DISABLED; + if (possible & mode) { + break; + } + // fall through + case LIBINPUT_CONFIG_SEND_EVENTS_DISABLED: + default: + mode = LIBINPUT_CONFIG_SEND_EVENTS_ENABLED; + break; + } + + ic->send_events = mode; +#endif +} + +static int mode_for_name(const char *name) { + if (!strcmp(name, "enabled")) { + return LIBINPUT_CONFIG_SEND_EVENTS_ENABLED; + } else if (!strcmp(name, "disabled_on_external_mouse")) { + return LIBINPUT_CONFIG_SEND_EVENTS_DISABLED_ON_EXTERNAL_MOUSE; + } else if (!strcmp(name, "disabled")) { + return LIBINPUT_CONFIG_SEND_EVENTS_DISABLED; + } + return -1; +} + +static void toggle_select_send_events_for_device(struct input_config *ic, + struct sway_input_device *input_device, int argc, char **argv) { +#if WLR_HAS_LIBINPUT_BACKEND + if (!wlr_input_device_is_libinput(input_device->wlr_device)) { + return; + } + // Get the currently set event mode since ic is a new config that will be + // merged on the existing later. It should be set to INT_MIN before this. + ic->send_events = libinput_device_config_send_events_get_mode( + wlr_libinput_get_device_handle(input_device->wlr_device)); + + int index; + for (index = 0; index < argc; ++index) { + if (mode_for_name(argv[index]) == ic->send_events) { + ++index; + break; + } + } + ic->send_events = mode_for_name(argv[index % argc]); +#endif +} + +static void toggle_send_events(int argc, char **argv) { + struct input_config *ic = config->handler_context.input_config; + bool wildcard = strcmp(ic->identifier, "*") == 0; + const char *type = has_prefix(ic->identifier, "type:") + ? ic->identifier + strlen("type:") : NULL; + struct sway_input_device *device = NULL; + wl_list_for_each(device, &server.input->devices, link) { + if (wildcard || type) { + ic = new_input_config(device->identifier); + if (!ic) { + continue; + } + if (type && strcmp(input_device_get_type(device), type) != 0) { + continue; + } + } else if (strcmp(ic->identifier, device->identifier) != 0) { + continue; + } + + if (argc) { + toggle_select_send_events_for_device(ic, device, argc, argv); + } else { + toggle_supported_send_events_for_device(ic, device); + } + + if (wildcard || type) { + store_input_config(ic, NULL); + } else { + return; + } + } +} + +struct cmd_results *input_cmd_events(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "events", EXPECTED_AT_LEAST, 1))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + + if (strcasecmp(argv[0], "enabled") == 0) { + ic->send_events = LIBINPUT_CONFIG_SEND_EVENTS_ENABLED; + } else if (strcasecmp(argv[0], "disabled") == 0) { + ic->send_events = LIBINPUT_CONFIG_SEND_EVENTS_DISABLED; + } else if (strcasecmp(argv[0], "disabled_on_external_mouse") == 0) { + ic->send_events = + LIBINPUT_CONFIG_SEND_EVENTS_DISABLED_ON_EXTERNAL_MOUSE; + } else if (config->reading) { + return cmd_results_new(CMD_INVALID, + "Expected 'events '"); + } else if (strcasecmp(argv[0], "toggle") == 0) { + for (int i = 1; i < argc; ++i) { + if (mode_for_name(argv[i]) == -1) { + return cmd_results_new(CMD_INVALID, + "Invalid toggle mode %s", argv[i]); + } + } + + toggle_send_events(argc - 1, argv + 1); + + if (strcmp(ic->identifier, "*") == 0 || has_prefix(ic->identifier, "type:")) { + // Update the device input configs and then reset the type/wildcard + // config send events mode so that is does not override the device + // ones. The device ones will be applied when attempting to apply + // the type/wildcard config + ic->send_events = INT_MIN; + } + } else { + return cmd_results_new(CMD_INVALID, + "Expected 'events '"); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input/left_handed.c b/sway/commands/input/left_handed.c new file mode 100644 index 0000000..93d8d56 --- /dev/null +++ b/sway/commands/input/left_handed.c @@ -0,0 +1,21 @@ +#include +#include +#include "sway/config.h" +#include "sway/commands.h" +#include "sway/input/input-manager.h" +#include "util.h" + +struct cmd_results *input_cmd_left_handed(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "left_handed", EXPECTED_AT_LEAST, 1))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + + ic->left_handed = parse_boolean(argv[0], true); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input/map_from_region.c b/sway/commands/input/map_from_region.c new file mode 100644 index 0000000..2f8f753 --- /dev/null +++ b/sway/commands/input/map_from_region.c @@ -0,0 +1,86 @@ +#include +#include +#include +#include "log.h" +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/input/input-manager.h" + +static bool parse_coords(const char *str, double *x, double *y, bool *mm) { + *mm = false; + + char *end; + + // Check for "0x" prefix to avoid strtod treating the string as hex + if (str[0] == '0' && str[1] == 'x') { + if (strlen(str) < 3) { + return false; + } + *x = 0; + end = (char *)str + 2; + } else { + *x = strtod(str, &end); + if (end[0] != 'x') { + return false; + } + ++end; + } + + *y = strtod(end, &end); + if (end[0] == 'm') { + // Expect mm + if (end[1] != 'm') { + return false; + } + *mm = true; + end = &end[2]; + } + if (end[0] != '\0') { + return false; + } + + return true; +} + +struct cmd_results *input_cmd_map_from_region(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "map_from_region", EXPECTED_EQUAL_TO, 2))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined"); + } + + ic->mapped_from_region = + calloc(1, sizeof(struct input_config_mapped_from_region)); + + bool mm1, mm2; + if (!parse_coords(argv[0], &ic->mapped_from_region->x1, + &ic->mapped_from_region->y1, &mm1)) { + free(ic->mapped_from_region); + ic->mapped_from_region = NULL; + return cmd_results_new(CMD_FAILURE, "Invalid top-left coordinates"); + } + if (!parse_coords(argv[1], &ic->mapped_from_region->x2, + &ic->mapped_from_region->y2, &mm2)) { + free(ic->mapped_from_region); + ic->mapped_from_region = NULL; + return cmd_results_new(CMD_FAILURE, "Invalid bottom-right coordinates"); + } + if (ic->mapped_from_region->x1 > ic->mapped_from_region->x2 || + ic->mapped_from_region->y1 > ic->mapped_from_region->y2) { + free(ic->mapped_from_region); + ic->mapped_from_region = NULL; + return cmd_results_new(CMD_FAILURE, "Invalid rectangle"); + } + if (mm1 != mm2) { + free(ic->mapped_from_region); + ic->mapped_from_region = NULL; + return cmd_results_new(CMD_FAILURE, + "Both coordinates must be in the same unit"); + } + ic->mapped_from_region->mm = mm1; + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input/map_to_output.c b/sway/commands/input/map_to_output.c new file mode 100644 index 0000000..a7266ba --- /dev/null +++ b/sway/commands/input/map_to_output.c @@ -0,0 +1,22 @@ +#include +#include +#include "sway/config.h" +#include "sway/commands.h" +#include "sway/input/input-manager.h" +#include "log.h" + +struct cmd_results *input_cmd_map_to_output(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "map_to_output", EXPECTED_EQUAL_TO, 1))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + + ic->mapped_to = MAPPED_TO_OUTPUT; + ic->mapped_to_output = strdup(argv[0]); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input/map_to_region.c b/sway/commands/input/map_to_region.c new file mode 100644 index 0000000..9087c58 --- /dev/null +++ b/sway/commands/input/map_to_region.c @@ -0,0 +1,52 @@ +#include +#include +#include "sway/commands.h" +#include "sway/config.h" + +struct cmd_results *input_cmd_map_to_region(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "map_to_region", EXPECTED_EQUAL_TO, 4))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined"); + } + + ic->mapped_to = MAPPED_TO_REGION; + ic->mapped_to_region = calloc(1, sizeof(struct wlr_box)); + + const char *errstr; + char *end; + + ic->mapped_to_region->x = strtol(argv[0], &end, 10); + if (end[0] != '\0') { + errstr = "Invalid X coordinate"; + goto error; + } + + ic->mapped_to_region->y = strtol(argv[1], &end, 10); + if (end[0] != '\0') { + errstr = "Invalid Y coordinate"; + goto error; + } + + ic->mapped_to_region->width = strtol(argv[2], &end, 10); + if (end[0] != '\0' || ic->mapped_to_region->width < 1) { + errstr = "Invalid width"; + goto error; + } + + ic->mapped_to_region->height = strtol(argv[3], &end, 10); + if (end[0] != '\0' || ic->mapped_to_region->height < 1) { + errstr = "Invalid height"; + goto error; + } + + return cmd_results_new(CMD_SUCCESS, NULL); + +error: + free(ic->mapped_to_region); + ic->mapped_to_region = NULL; + return cmd_results_new(CMD_FAILURE, "%s", errstr); +} diff --git a/sway/commands/input/middle_emulation.c b/sway/commands/input/middle_emulation.c new file mode 100644 index 0000000..9f32692 --- /dev/null +++ b/sway/commands/input/middle_emulation.c @@ -0,0 +1,25 @@ +#include +#include +#include "sway/config.h" +#include "sway/commands.h" +#include "sway/input/input-manager.h" +#include "util.h" + +struct cmd_results *input_cmd_middle_emulation(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "middle_emulation", EXPECTED_AT_LEAST, 1))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + + if (parse_boolean(argv[0], true)) { + ic->middle_emulation = LIBINPUT_CONFIG_MIDDLE_EMULATION_ENABLED; + } else { + ic->middle_emulation = LIBINPUT_CONFIG_MIDDLE_EMULATION_DISABLED; + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input/natural_scroll.c b/sway/commands/input/natural_scroll.c new file mode 100644 index 0000000..dec32a5 --- /dev/null +++ b/sway/commands/input/natural_scroll.c @@ -0,0 +1,21 @@ +#include +#include +#include "sway/config.h" +#include "sway/commands.h" +#include "sway/input/input-manager.h" +#include "util.h" + +struct cmd_results *input_cmd_natural_scroll(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "natural_scroll", EXPECTED_AT_LEAST, 1))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + + ic->natural_scroll = parse_boolean(argv[0], true); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input/pointer_accel.c b/sway/commands/input/pointer_accel.c new file mode 100644 index 0000000..e214b32 --- /dev/null +++ b/sway/commands/input/pointer_accel.c @@ -0,0 +1,29 @@ +#include +#include +#include +#include "sway/config.h" +#include "sway/commands.h" +#include "sway/input/input-manager.h" +#include "util.h" + +struct cmd_results *input_cmd_pointer_accel(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "pointer_accel", EXPECTED_AT_LEAST, 1))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + + float pointer_accel = parse_float(argv[0]); + if (isnan(pointer_accel)) { + return cmd_results_new(CMD_INVALID, + "Invalid pointer accel; expected float."); + } if (pointer_accel < -1 || pointer_accel > 1) { + return cmd_results_new(CMD_INVALID, "Input out of range [-1, 1]"); + } + ic->pointer_accel = pointer_accel; + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input/repeat_delay.c b/sway/commands/input/repeat_delay.c new file mode 100644 index 0000000..5b787fe --- /dev/null +++ b/sway/commands/input/repeat_delay.c @@ -0,0 +1,24 @@ +#include +#include +#include "sway/config.h" +#include "sway/commands.h" +#include "sway/input/input-manager.h" + +struct cmd_results *input_cmd_repeat_delay(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "repeat_delay", EXPECTED_EQUAL_TO, 1))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + + int repeat_delay = atoi(argv[0]); + if (repeat_delay < 0) { + return cmd_results_new(CMD_INVALID, "Repeat delay cannot be negative"); + } + ic->repeat_delay = repeat_delay; + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input/repeat_rate.c b/sway/commands/input/repeat_rate.c new file mode 100644 index 0000000..79c5efe --- /dev/null +++ b/sway/commands/input/repeat_rate.c @@ -0,0 +1,24 @@ +#include +#include +#include "sway/config.h" +#include "sway/commands.h" +#include "sway/input/input-manager.h" + +struct cmd_results *input_cmd_repeat_rate(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "repeat_rate", EXPECTED_EQUAL_TO, 1))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + + int repeat_rate = atoi(argv[0]); + if (repeat_rate < 0) { + return cmd_results_new(CMD_INVALID, "Repeat rate cannot be negative"); + } + ic->repeat_rate = repeat_rate; + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input/rotation_angle.c b/sway/commands/input/rotation_angle.c new file mode 100644 index 0000000..5e278ff --- /dev/null +++ b/sway/commands/input/rotation_angle.c @@ -0,0 +1,29 @@ +#include +#include +#include +#include "sway/config.h" +#include "sway/commands.h" +#include "sway/input/input-manager.h" +#include "util.h" + +struct cmd_results *input_cmd_rotation_angle(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "rotation_angle", EXPECTED_AT_LEAST, 1))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + + float rotation_angle = parse_float(argv[0]); + if (isnan(rotation_angle)) { + return cmd_results_new(CMD_INVALID, + "Invalid rotation_angle; expected float."); + } if (rotation_angle < 0 || rotation_angle > 360) { + return cmd_results_new(CMD_INVALID, "Input out of range [0, 360)"); + } + ic->rotation_angle = rotation_angle; + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input/scroll_button.c b/sway/commands/input/scroll_button.c new file mode 100644 index 0000000..81f69a6 --- /dev/null +++ b/sway/commands/input/scroll_button.c @@ -0,0 +1,37 @@ +#include +#include "sway/config.h" +#include "sway/commands.h" +#include "sway/input/cursor.h" + +struct cmd_results *input_cmd_scroll_button(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "scroll_button", EXPECTED_AT_LEAST, 1))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + + if (strcmp(*argv, "disable") == 0) { + ic->scroll_button = 0; + return cmd_results_new(CMD_SUCCESS, NULL); + } + + char *message = NULL; + uint32_t button = get_mouse_button(*argv, &message); + if (message) { + error = cmd_results_new(CMD_INVALID, "%s", message); + free(message); + return error; + } else if (button == SWAY_SCROLL_UP || button == SWAY_SCROLL_DOWN + || button == SWAY_SCROLL_LEFT || button == SWAY_SCROLL_RIGHT) { + return cmd_results_new(CMD_INVALID, + "X11 axis buttons are not supported for scroll_button"); + } else if (!button) { + return cmd_results_new(CMD_INVALID, "Unknown button %s", *argv); + } + ic->scroll_button = button; + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input/scroll_button_lock.c b/sway/commands/input/scroll_button_lock.c new file mode 100644 index 0000000..f96b651 --- /dev/null +++ b/sway/commands/input/scroll_button_lock.c @@ -0,0 +1,26 @@ +#include +#include +#include +#include "sway/config.h" +#include "sway/commands.h" +#include "sway/input/input-manager.h" +#include "util.h" + +struct cmd_results *input_cmd_scroll_button_lock(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "scroll_button_lock", EXPECTED_AT_LEAST, 1))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + + if (parse_boolean(argv[0], true)) { + ic->scroll_button_lock = LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_ENABLED; + } else { + ic->scroll_button_lock = LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_DISABLED; + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input/scroll_factor.c b/sway/commands/input/scroll_factor.c new file mode 100644 index 0000000..4b72b6f --- /dev/null +++ b/sway/commands/input/scroll_factor.c @@ -0,0 +1,31 @@ +#include +#include +#include +#include +#include "sway/config.h" +#include "sway/commands.h" +#include "sway/input/input-manager.h" +#include "util.h" + +struct cmd_results *input_cmd_scroll_factor(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "scroll_factor", EXPECTED_AT_LEAST, 1))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + + float scroll_factor = parse_float(argv[0]); + if (isnan(scroll_factor)) { + return cmd_results_new(CMD_INVALID, + "Invalid scroll factor; expected float."); + } else if (scroll_factor < 0) { + return cmd_results_new(CMD_INVALID, + "Scroll factor cannot be negative."); + } + ic->scroll_factor = scroll_factor; + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input/scroll_method.c b/sway/commands/input/scroll_method.c new file mode 100644 index 0000000..cd8b5f7 --- /dev/null +++ b/sway/commands/input/scroll_method.c @@ -0,0 +1,31 @@ +#include +#include +#include "sway/config.h" +#include "sway/commands.h" +#include "sway/input/input-manager.h" + +struct cmd_results *input_cmd_scroll_method(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "scroll_method", EXPECTED_AT_LEAST, 1))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + + if (strcasecmp(argv[0], "none") == 0) { + ic->scroll_method = LIBINPUT_CONFIG_SCROLL_NO_SCROLL; + } else if (strcasecmp(argv[0], "two_finger") == 0) { + ic->scroll_method = LIBINPUT_CONFIG_SCROLL_2FG; + } else if (strcasecmp(argv[0], "edge") == 0) { + ic->scroll_method = LIBINPUT_CONFIG_SCROLL_EDGE; + } else if (strcasecmp(argv[0], "on_button_down") == 0) { + ic->scroll_method = LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN; + } else { + return cmd_results_new(CMD_INVALID, + "Expected 'scroll_method '"); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input/tap.c b/sway/commands/input/tap.c new file mode 100644 index 0000000..443fd49 --- /dev/null +++ b/sway/commands/input/tap.c @@ -0,0 +1,26 @@ +#include +#include +#include "sway/config.h" +#include "sway/commands.h" +#include "sway/input/input-manager.h" +#include "log.h" +#include "util.h" + +struct cmd_results *input_cmd_tap(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "tap", EXPECTED_AT_LEAST, 1))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + + if (parse_boolean(argv[0], true)) { + ic->tap = LIBINPUT_CONFIG_TAP_ENABLED; + } else { + ic->tap = LIBINPUT_CONFIG_TAP_DISABLED; + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input/tap_button_map.c b/sway/commands/input/tap_button_map.c new file mode 100644 index 0000000..77ac6de --- /dev/null +++ b/sway/commands/input/tap_button_map.c @@ -0,0 +1,27 @@ +#include +#include +#include "sway/config.h" +#include "sway/commands.h" +#include "sway/input/input-manager.h" + +struct cmd_results *input_cmd_tap_button_map(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "tap_button_map", EXPECTED_AT_LEAST, 1))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + + if (strcasecmp(argv[0], "lrm") == 0) { + ic->tap_button_map = LIBINPUT_CONFIG_TAP_MAP_LRM; + } else if (strcasecmp(argv[0], "lmr") == 0) { + ic->tap_button_map = LIBINPUT_CONFIG_TAP_MAP_LMR; + } else { + return cmd_results_new(CMD_INVALID, + "Expected 'tap_button_map '"); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input/tool_mode.c b/sway/commands/input/tool_mode.c new file mode 100644 index 0000000..0431685 --- /dev/null +++ b/sway/commands/input/tool_mode.c @@ -0,0 +1,73 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" + +static void set_tool_mode(struct input_config *ic, + enum wlr_tablet_tool_type type, enum sway_tablet_tool_mode mode) { + for (int i = 0; i < ic->tools->length; i++) { + struct input_config_tool *tool = ic->tools->items[i]; + if (tool->type == type) { + tool->mode = mode; + return; + } + } + + struct input_config_tool *tool = calloc(1, sizeof(*tool)); + if (tool) { + tool->type = type; + tool->mode = mode; + list_add(ic->tools, tool); + } +} + +struct cmd_results *input_cmd_tool_mode(int argc, char **argv) { + struct cmd_results *error; + if ((error = checkarg(argc, "tool_mode", EXPECTED_AT_LEAST, 2))) { + return error; + } + + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + + enum sway_tablet_tool_mode tool_mode; + if (!strcasecmp(argv[1], "absolute")) { + tool_mode = SWAY_TABLET_TOOL_MODE_ABSOLUTE; + } else if (!strcasecmp(argv[1], "relative")) { + tool_mode = SWAY_TABLET_TOOL_MODE_RELATIVE; + } else { + goto invalid_command; + } + + if (!strcasecmp(argv[0], "*")) { + set_tool_mode(ic, WLR_TABLET_TOOL_TYPE_PEN, tool_mode); + set_tool_mode(ic, WLR_TABLET_TOOL_TYPE_ERASER, tool_mode); + set_tool_mode(ic, WLR_TABLET_TOOL_TYPE_BRUSH, tool_mode); + set_tool_mode(ic, WLR_TABLET_TOOL_TYPE_PENCIL, tool_mode); + set_tool_mode(ic, WLR_TABLET_TOOL_TYPE_AIRBRUSH, tool_mode); + } else { + enum wlr_tablet_tool_type tool_type; + if (!strcasecmp(argv[0], "pen")) { + tool_type = WLR_TABLET_TOOL_TYPE_PEN; + } else if (!strcasecmp(argv[0], "eraser")) { + tool_type = WLR_TABLET_TOOL_TYPE_ERASER; + } else if (!strcasecmp(argv[0], "brush")) { + tool_type = WLR_TABLET_TOOL_TYPE_BRUSH; + } else if (!strcasecmp(argv[0], "pencil")) { + tool_type = WLR_TABLET_TOOL_TYPE_PENCIL; + } else if (!strcasecmp(argv[0], "airbrush")) { + tool_type = WLR_TABLET_TOOL_TYPE_AIRBRUSH; + } else { + goto invalid_command; + } + + set_tool_mode(ic, tool_type, tool_mode); + } + + return cmd_results_new(CMD_SUCCESS, NULL); + +invalid_command: + return cmd_results_new(CMD_INVALID, + "Expected 'tool_mode '"); +} diff --git a/sway/commands/input/xkb_capslock.c b/sway/commands/input/xkb_capslock.c new file mode 100644 index 0000000..7ce98eb --- /dev/null +++ b/sway/commands/input/xkb_capslock.c @@ -0,0 +1,21 @@ +#include +#include +#include "sway/config.h" +#include "sway/commands.h" +#include "sway/input/input-manager.h" +#include "util.h" + +struct cmd_results *input_cmd_xkb_capslock(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "xkb_capslock", EXPECTED_AT_LEAST, 1))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + + ic->xkb_capslock = parse_boolean(argv[0], false); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input/xkb_file.c b/sway/commands/input/xkb_file.c new file mode 100644 index 0000000..056f00e --- /dev/null +++ b/sway/commands/input/xkb_file.c @@ -0,0 +1,48 @@ +#include +#include +#include "sway/config.h" +#include "sway/commands.h" +#include "log.h" +#include "stringop.h" + +struct cmd_results *input_cmd_xkb_file(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "xkb_file", EXPECTED_EQUAL_TO, 1))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + + if (strcmp(argv[0], "-") == 0) { + free(ic->xkb_file); + ic->xkb_file = NULL; + } else { + ic->xkb_file = strdup(argv[0]); + if (!expand_path(&ic->xkb_file)) { + error = cmd_results_new(CMD_INVALID, "Invalid syntax (%s)", + ic->xkb_file); + free(ic->xkb_file); + ic->xkb_file = NULL; + return error; + } + if (!ic->xkb_file) { + sway_log(SWAY_ERROR, "Failed to allocate expanded path"); + return cmd_results_new(CMD_FAILURE, "Unable to allocate resource"); + } + + bool can_access = access(ic->xkb_file, F_OK) != -1; + if (!can_access) { + sway_log_errno(SWAY_ERROR, "Unable to access xkb file '%s'", + ic->xkb_file); + config_add_swaynag_warning("Unable to access xkb file '%s'", + ic->xkb_file); + } + } + ic->xkb_file_is_set = true; + + sway_log(SWAY_DEBUG, "set-xkb_file for config: %s file: %s", + ic->identifier, ic->xkb_file); + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input/xkb_layout.c b/sway/commands/input/xkb_layout.c new file mode 100644 index 0000000..1d01886 --- /dev/null +++ b/sway/commands/input/xkb_layout.c @@ -0,0 +1,20 @@ +#include "sway/config.h" +#include "sway/commands.h" +#include "log.h" + +struct cmd_results *input_cmd_xkb_layout(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "xkb_layout", EXPECTED_EQUAL_TO, 1))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + + ic->xkb_layout = strdup(argv[0]); + + sway_log(SWAY_DEBUG, "set-xkb_layout for config: %s layout: %s", + ic->identifier, ic->xkb_layout); + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input/xkb_model.c b/sway/commands/input/xkb_model.c new file mode 100644 index 0000000..a9144a8 --- /dev/null +++ b/sway/commands/input/xkb_model.c @@ -0,0 +1,20 @@ +#include "sway/config.h" +#include "sway/commands.h" +#include "log.h" + +struct cmd_results *input_cmd_xkb_model(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "xkb_model", EXPECTED_EQUAL_TO, 1))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + + ic->xkb_model = strdup(argv[0]); + + sway_log(SWAY_DEBUG, "set-xkb_model for config: %s model: %s", + ic->identifier, ic->xkb_model); + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input/xkb_numlock.c b/sway/commands/input/xkb_numlock.c new file mode 100644 index 0000000..bbe848f --- /dev/null +++ b/sway/commands/input/xkb_numlock.c @@ -0,0 +1,18 @@ +#include "sway/config.h" +#include "sway/commands.h" +#include "util.h" + +struct cmd_results *input_cmd_xkb_numlock(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "xkb_numlock", EXPECTED_AT_LEAST, 1))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + + ic->xkb_numlock = parse_boolean(argv[0], false); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input/xkb_options.c b/sway/commands/input/xkb_options.c new file mode 100644 index 0000000..7ca2077 --- /dev/null +++ b/sway/commands/input/xkb_options.c @@ -0,0 +1,20 @@ +#include "sway/config.h" +#include "sway/commands.h" +#include "log.h" + +struct cmd_results *input_cmd_xkb_options(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "xkb_options", EXPECTED_EQUAL_TO, 1))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + + ic->xkb_options = strdup(argv[0]); + + sway_log(SWAY_DEBUG, "set-xkb_options for config: %s options: %s", + ic->identifier, ic->xkb_options); + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input/xkb_rules.c b/sway/commands/input/xkb_rules.c new file mode 100644 index 0000000..8fbd26f --- /dev/null +++ b/sway/commands/input/xkb_rules.c @@ -0,0 +1,20 @@ +#include "sway/config.h" +#include "sway/commands.h" +#include "log.h" + +struct cmd_results *input_cmd_xkb_rules(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "xkb_rules", EXPECTED_EQUAL_TO, 1))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + + ic->xkb_rules = strdup(argv[0]); + + sway_log(SWAY_DEBUG, "set-xkb_rules for config: %s rules: %s", + ic->identifier, ic->xkb_rules); + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input/xkb_switch_layout.c b/sway/commands/input/xkb_switch_layout.c new file mode 100644 index 0000000..623dfa1 --- /dev/null +++ b/sway/commands/input/xkb_switch_layout.c @@ -0,0 +1,123 @@ +#include +#include +#include "sway/config.h" +#include "sway/commands.h" +#include "sway/input/input-manager.h" +#include "sway/server.h" +#include "log.h" + +struct xkb_switch_layout_action { + struct wlr_keyboard *keyboard; + xkb_layout_index_t layout; +}; + +static void switch_layout(struct wlr_keyboard *kbd, xkb_layout_index_t idx) { + xkb_layout_index_t num_layouts = xkb_keymap_num_layouts(kbd->keymap); + if (idx >= num_layouts) { + return; + } + wlr_keyboard_notify_modifiers(kbd, kbd->modifiers.depressed, + kbd->modifiers.latched, kbd->modifiers.locked, idx); +} + +static xkb_layout_index_t get_current_layout_index(struct wlr_keyboard *kbd) { + xkb_layout_index_t num_layouts = xkb_keymap_num_layouts(kbd->keymap); + assert(num_layouts > 0); + + xkb_layout_index_t layout_idx; + for (layout_idx = 0; layout_idx < num_layouts; layout_idx++) { + if (xkb_state_layout_index_is_active(kbd->xkb_state, + layout_idx, XKB_STATE_LAYOUT_EFFECTIVE)) { + break; + } + } + return layout_idx; +} + +static xkb_layout_index_t get_layout_relative(struct wlr_keyboard *kbd, int dir) { + xkb_layout_index_t num_layouts = xkb_keymap_num_layouts(kbd->keymap); + xkb_layout_index_t idx = get_current_layout_index(kbd); + return (idx + num_layouts + dir) % num_layouts; +} + +struct cmd_results *input_cmd_xkb_switch_layout(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "xkb_switch_layout", EXPECTED_EQUAL_TO, 1))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + + if (config->reading || !config->active) { + return cmd_results_new(CMD_DEFER, NULL); + } + + const char *layout_str = argv[0]; + int relative, layout; + + if (strcmp(layout_str, "next") == 0) { + relative = 1; + } else if (strcmp(layout_str, "prev") == 0) { + relative = -1; + } else { + char *end; + layout = strtol(layout_str, &end, 10); + if (layout_str[0] == '\0' || end[0] != '\0') { + return cmd_results_new(CMD_FAILURE, "Invalid argument."); + } else if (layout < 0) { + return cmd_results_new(CMD_FAILURE, "Invalid layout index."); + } + relative = 0; + } + + struct xkb_switch_layout_action *actions = calloc( + wl_list_length(&server.input->devices), + sizeof(struct xkb_switch_layout_action)); + size_t actions_len = 0; + + if (!actions) { + return cmd_results_new(CMD_FAILURE, "Unable to allocate actions"); + } + + /* Calculate new indexes first because switching a layout in one + keyboard may result in a change on other keyboards as well because + of keyboard groups. */ + struct sway_input_device *dev; + wl_list_for_each(dev, &server.input->devices, link) { + if (strcmp(ic->identifier, "*") != 0 && + strcmp(ic->identifier, "type:keyboard") != 0 && + strcmp(ic->identifier, dev->identifier) != 0) { + continue; + } + if (dev->wlr_device->type != WLR_INPUT_DEVICE_KEYBOARD) { + continue; + } + + struct wlr_keyboard *keyboard = + wlr_keyboard_from_input_device(dev->wlr_device); + if (keyboard->keymap == NULL && dev->is_virtual) { + // The `sway_keyboard_set_layout` function is by default skipped + // when configuring virtual keyboards. + continue; + } + + struct xkb_switch_layout_action *action = + &actions[actions_len++]; + action->keyboard = keyboard; + + if (relative) { + action->layout = get_layout_relative(action->keyboard, relative); + } else { + action->layout = layout; + } + } + + for (size_t i = 0; i < actions_len; i++) { + switch_layout(actions[i].keyboard, actions[i].layout); + } + free(actions); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/input/xkb_variant.c b/sway/commands/input/xkb_variant.c new file mode 100644 index 0000000..2d14ea9 --- /dev/null +++ b/sway/commands/input/xkb_variant.c @@ -0,0 +1,20 @@ +#include "sway/config.h" +#include "sway/commands.h" +#include "log.h" + +struct cmd_results *input_cmd_xkb_variant(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "xkb_variant", EXPECTED_EQUAL_TO, 1))) { + return error; + } + struct input_config *ic = config->handler_context.input_config; + if (!ic) { + return cmd_results_new(CMD_FAILURE, "No input device defined."); + } + + ic->xkb_variant = strdup(argv[0]); + + sway_log(SWAY_DEBUG, "set-xkb_variant for config: %s variant: %s", + ic->identifier, ic->xkb_variant); + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/kill.c b/sway/commands/kill.c new file mode 100644 index 0000000..f1e95e4 --- /dev/null +++ b/sway/commands/kill.c @@ -0,0 +1,31 @@ +#include "log.h" +#include "sway/input/input-manager.h" +#include "sway/input/seat.h" +#include "sway/tree/container.h" +#include "sway/tree/view.h" +#include "sway/tree/workspace.h" +#include "sway/commands.h" + +static void close_container_iterator(struct sway_container *con, void *data) { + if (con->view) { + view_close(con->view); + } +} + +struct cmd_results *cmd_kill(int argc, char **argv) { + if (!root->outputs->length) { + return cmd_results_new(CMD_INVALID, + "Can't run this command while there's no outputs connected."); + } + struct sway_container *con = config->handler_context.container; + struct sway_workspace *ws = config->handler_context.workspace; + + if (con) { + close_container_iterator(con, NULL); + container_for_each_child(con, close_container_iterator, NULL); + } else { + workspace_for_each_container(ws, close_container_iterator, NULL); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/layer_effects.c b/sway/commands/layer_effects.c new file mode 100644 index 0000000..aea815d --- /dev/null +++ b/sway/commands/layer_effects.c @@ -0,0 +1,50 @@ +#include +#include "log.h" +#include "stringop.h" +#include "sway/commands.h" +#include "sway/layers.h" +#include "sway/output.h" +#include "sway/scene_descriptor.h" +#include "sway/tree/arrange.h" + +struct cmd_results *cmd_layer_effects(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "layer_effects", EXPECTED_AT_LEAST, 2))) { + return error; + } + + struct layer_criteria *criteria = layer_criteria_add(argv[0], join_args(argv + 1, argc - 1)); + if (criteria) { + sway_log(SWAY_DEBUG, "layer_effect: '%s' '%s' added", + criteria->namespace, criteria->cmdlist); + + // Apply the criteria to all applicable layer surfaces + for (int i = 0; i < root->outputs->length; ++i) { + struct sway_output *output = root->outputs->items[i]; + struct wlr_scene_tree *layers[] = { + output->layers.shell_background, + output->layers.shell_bottom, + output->layers.shell_overlay, + output->layers.shell_top, + }; + size_t nlayers = sizeof(layers) / sizeof(layers[0]); + struct wlr_scene_node *node; + for (size_t i = 0; i < nlayers; ++i) { + wl_list_for_each_reverse(node, &layers[i]->children, link) { + struct sway_layer_surface *surface = scene_descriptor_try_get(node, + SWAY_SCENE_DESC_LAYER_SHELL); + if (!surface) { + continue; + } + if (strcmp(surface->layer_surface->namespace, criteria->namespace) == 0) { + layer_apply_criteria(surface, criteria); + } + } + } + + arrange_layers(output); + } + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/layout.c b/sway/commands/layout.c new file mode 100644 index 0000000..22dfdf3 --- /dev/null +++ b/sway/commands/layout.c @@ -0,0 +1,199 @@ +#include +#include +#include +#include "sway/commands.h" +#include "sway/output.h" +#include "sway/tree/arrange.h" +#include "sway/tree/container.h" +#include "sway/tree/workspace.h" +#include "log.h" + +static enum sway_container_layout parse_layout_string(char *s) { + if (strcasecmp(s, "splith") == 0) { + return L_HORIZ; + } else if (strcasecmp(s, "splitv") == 0) { + return L_VERT; + } else if (strcasecmp(s, "tabbed") == 0) { + return L_TABBED; + } else if (strcasecmp(s, "stacking") == 0) { + return L_STACKED; + } + return L_NONE; +} + +static const char expected_syntax[] = + "Expected 'layout default|tabbed|stacking|splitv|splith' or " + "'layout toggle [split|all]' or " + "'layout toggle [split|tabbed|stacking|splitv|splith] [split|tabbed|stacking|splitv|splith]...'"; + +static enum sway_container_layout toggle_split_layout( + enum sway_container_layout layout, + enum sway_container_layout prev_split_layout, + struct sway_output *output) { + if (layout == L_HORIZ) { + return L_VERT; + } else if (layout == L_VERT) { + return L_HORIZ; + } else if (prev_split_layout != L_NONE) { + return prev_split_layout; + } else if (config->default_orientation != L_NONE) { + return config->default_orientation; + } else if (output->height > output->width) { + return L_VERT; + } + return L_HORIZ; +} + +static enum sway_container_layout get_layout_toggle(int argc, char **argv, + enum sway_container_layout layout, + enum sway_container_layout prev_split_layout, + struct sway_output *output) { + // "layout toggle" + if (argc == 1) { + return toggle_split_layout(layout, prev_split_layout, output); + } + + if (argc == 2) { + // "layout toggle split" (same as "layout toggle") + if (strcasecmp(argv[1], "split") == 0) { + return toggle_split_layout(layout, prev_split_layout, output); + } + // "layout toggle all" + if (strcasecmp(argv[1], "all") == 0) { + return layout == L_HORIZ ? L_VERT : + layout == L_VERT ? L_STACKED : + layout == L_STACKED ? L_TABBED : L_HORIZ; + } + return L_NONE; + } + + enum sway_container_layout parsed; + int curr = 1; + for (; curr < argc; curr++) { + parsed = parse_layout_string(argv[curr]); + if (parsed == layout || (strcmp(argv[curr], "split") == 0 && + (layout == L_VERT || layout == L_HORIZ))) { + break; + } + } + for (int i = curr + 1; i != curr; ++i) { + // cycle round to find next valid layout + if (i >= argc) { + i = 1; + } + parsed = parse_layout_string(argv[i]); + if (parsed != L_NONE) { + return parsed; + } + if (strcmp(argv[i], "split") == 0) { + return toggle_split_layout(layout, prev_split_layout, output); + } + // invalid layout strings are silently ignored + } + return L_NONE; +} + +static enum sway_container_layout get_layout(int argc, char **argv, + enum sway_container_layout layout, + enum sway_container_layout prev_split_layout, + struct sway_output *output) { + // Check if assigned directly + enum sway_container_layout parsed = parse_layout_string(argv[0]); + if (parsed != L_NONE) { + return parsed; + } + + if (strcasecmp(argv[0], "default") == 0) { + return prev_split_layout; + } + + if (strcasecmp(argv[0], "toggle") == 0) { + return get_layout_toggle(argc, argv, layout, prev_split_layout, output); + } + + return L_NONE; +} + +struct cmd_results *cmd_layout(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "layout", EXPECTED_AT_LEAST, 1))) { + return error; + } + if (!root->outputs->length) { + return cmd_results_new(CMD_INVALID, + "Can't run this command while there's no outputs connected."); + } + struct sway_container *container = config->handler_context.container; + struct sway_workspace *workspace = config->handler_context.workspace; + + if (container && container_is_floating(container)) { + return cmd_results_new(CMD_FAILURE, + "Unable to change layout of floating windows"); + } + + // Operate on parent container, like i3. + if (container) { + container = container->pending.parent; + // If parent has only a singe child operate on its parent and + // flatten once, like i3 + if (container && container->pending.children->length == 1) { + // Also check grandparent to avoid restricting layouts + struct sway_container *parent = container->pending.parent; + if (parent && parent->pending.children->length == 1) { + struct sway_container *child = container->pending.children->items[0]; + struct sway_container *parent = container->pending.parent; + container_replace(container, child); + container_begin_destroy(container); + container = parent; + } + } + } + + // We could be working with a container OR a workspace. These are different + // structures, so we set up pointers to they layouts so we can refer them in + // an abstract way. + enum sway_container_layout new_layout = L_NONE; + enum sway_container_layout old_layout = L_NONE; + if (container) { + old_layout = container->pending.layout; + new_layout = get_layout(argc, argv, + container->pending.layout, container->prev_split_layout, + container->pending.workspace->output); + } else { + old_layout = workspace->layout; + new_layout = get_layout(argc, argv, + workspace->layout, workspace->prev_split_layout, + workspace->output); + } + if (new_layout == L_NONE) { + return cmd_results_new(CMD_INVALID, "%s", expected_syntax); + } + if (new_layout != old_layout) { + if (container) { + if (old_layout != L_TABBED && old_layout != L_STACKED) { + container->prev_split_layout = old_layout; + } + container->pending.layout = new_layout; + container_update_representation(container); + } else if (config->handler_context.container) { + // i3 avoids changing workspace layouts with a new container + // https://github.com/i3/i3/blob/3cd1c45eba6de073bc4300eebb4e1cc1a0c4479a/src/con.c#L1817 + container = workspace_wrap_children(workspace); + container->pending.layout = new_layout; + container_update_representation(container); + } else { + if (old_layout != L_TABBED && old_layout != L_STACKED) { + workspace->prev_split_layout = old_layout; + } + workspace->layout = new_layout; + workspace_update_representation(workspace); + } + if (root->fullscreen_global) { + arrange_root(); + } else { + arrange_workspace(workspace); + } + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/mark.c b/sway/commands/mark.c new file mode 100644 index 0000000..77c8d23 --- /dev/null +++ b/sway/commands/mark.c @@ -0,0 +1,67 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/tree/view.h" +#include "list.h" +#include "log.h" +#include "stringop.h" + +// mark foo Same as mark --replace foo +// mark --add foo Add this mark to view's list +// mark --replace foo Replace view's marks with this single one +// mark --add --toggle foo Toggle current mark and persist other marks +// mark --replace --toggle foo Toggle current mark and remove other marks + +struct cmd_results *cmd_mark(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "mark", EXPECTED_AT_LEAST, 1))) { + return error; + } + struct sway_container *container = config->handler_context.container; + if (!container) { + return cmd_results_new(CMD_INVALID, "Only containers can have marks"); + } + + bool add = false, toggle = false; + while (argc > 0 && has_prefix(*argv, "--")) { + if (strcmp(*argv, "--add") == 0) { + add = true; + } else if (strcmp(*argv, "--replace") == 0) { + add = false; + } else if (strcmp(*argv, "--toggle") == 0) { + toggle = true; + } else { + return cmd_results_new(CMD_INVALID, + "Unrecognized argument '%s'", *argv); + } + ++argv; + --argc; + } + + if (!argc) { + return cmd_results_new(CMD_INVALID, + "Expected '[--add|--replace] [--toggle] '"); + } + + char *mark = join_args(argv, argc); + bool had_mark = container_has_mark(container, mark); + + if (!add) { + // Replacing + container_clear_marks(container); + } + + container_find_and_unmark(mark); + + if (!toggle || !had_mark) { + container_add_mark(container, mark); + } + + free(mark); + container_update_marks(container); + if (container->view) { + view_execute_criteria(container->view); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/max_render_time.c b/sway/commands/max_render_time.c new file mode 100644 index 0000000..ec3282f --- /dev/null +++ b/sway/commands/max_render_time.c @@ -0,0 +1,32 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/tree/view.h" + +struct cmd_results *cmd_max_render_time(int argc, char **argv) { + if (!argc) { + return cmd_results_new(CMD_INVALID, "Missing max render time argument."); + } + + int max_render_time; + if (!strcmp(*argv, "off")) { + max_render_time = 0; + } else { + char *end; + max_render_time = strtol(*argv, &end, 10); + if (*end || max_render_time <= 0) { + return cmd_results_new(CMD_INVALID, "Invalid max render time."); + } + } + + struct sway_container *container = config->handler_context.container; + if (!container || !container->view) { + return cmd_results_new(CMD_INVALID, + "Only views can have a max_render_time"); + } + + struct sway_view *view = container->view; + view->max_render_time = max_render_time; + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/mode.c b/sway/commands/mode.c new file mode 100644 index 0000000..b321696 --- /dev/null +++ b/sway/commands/mode.c @@ -0,0 +1,89 @@ +#include +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/ipc-server.h" +#include "list.h" +#include "log.h" +#include "stringop.h" + +// Must be in order for the bsearch +static const struct cmd_handler mode_handlers[] = { + { "bindcode", cmd_bindcode }, + { "bindgesture", cmd_bindgesture }, + { "bindswitch", cmd_bindswitch }, + { "bindsym", cmd_bindsym }, + { "set", cmd_set }, + { "unbindcode", cmd_unbindcode }, + { "unbindgesture", cmd_unbindgesture }, + { "unbindswitch", cmd_unbindswitch }, + { "unbindsym", cmd_unbindsym }, +}; + +struct cmd_results *cmd_mode(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "mode", EXPECTED_AT_LEAST, 1))) { + return error; + } + + bool pango = strcmp(*argv, "--pango_markup") == 0; + if (pango) { + argc--; argv++; + if (argc == 0) { + return cmd_results_new(CMD_FAILURE, "Mode name is missing"); + } + } + + if (config->reading && argc == 1) { + return cmd_results_new(CMD_DEFER, NULL); + } + + char *mode_name = *argv; + strip_quotes(mode_name); + struct sway_mode *mode = NULL; + // Find mode + for (int i = 0; i < config->modes->length; ++i) { + struct sway_mode *test = config->modes->items[i]; + if (strcmp(test->name, mode_name) == 0) { + mode = test; + break; + } + } + // Create mode if it doesn't exist + if (!mode && argc > 1) { + mode = calloc(1, sizeof(struct sway_mode)); + if (!mode) { + return cmd_results_new(CMD_FAILURE, "Unable to allocate mode"); + } + mode->name = strdup(mode_name); + mode->keysym_bindings = create_list(); + mode->keycode_bindings = create_list(); + mode->mouse_bindings = create_list(); + mode->switch_bindings = create_list(); + mode->gesture_bindings = create_list(); + mode->pango = pango; + list_add(config->modes, mode); + } + if (!mode) { + error = cmd_results_new(CMD_INVALID, "Unknown mode `%s'", mode_name); + return error; + } + // Set current mode + struct sway_mode *stored_mode = config->current_mode; + config->current_mode = mode; + if (argc == 1) { + // trigger IPC mode event + sway_log(SWAY_DEBUG, "Switching to mode `%s' (pango=%d)", + mode->name, mode->pango); + ipc_event_mode(config->current_mode->name, + config->current_mode->pango); + return cmd_results_new(CMD_SUCCESS, NULL); + } + + // Create binding + struct cmd_results *result = config_subcommand(argv + 1, argc - 1, + mode_handlers, sizeof(mode_handlers)); + config->current_mode = stored_mode; + + return result; +} diff --git a/sway/commands/mouse_warping.c b/sway/commands/mouse_warping.c new file mode 100644 index 0000000..8b643f6 --- /dev/null +++ b/sway/commands/mouse_warping.c @@ -0,0 +1,21 @@ +#include +#include +#include "sway/commands.h" + +struct cmd_results *cmd_mouse_warping(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "mouse_warping", EXPECTED_EQUAL_TO, 1))) { + return error; + } else if (strcasecmp(argv[0], "container") == 0) { + config->mouse_warping = WARP_CONTAINER; + } else if (strcasecmp(argv[0], "output") == 0) { + config->mouse_warping = WARP_OUTPUT; + } else if (strcasecmp(argv[0], "none") == 0) { + config->mouse_warping = WARP_NO; + } else { + return cmd_results_new(CMD_FAILURE, + "Expected 'mouse_warping output|container|none'"); + } + return cmd_results_new(CMD_SUCCESS, NULL); +} + diff --git a/sway/commands/move.c b/sway/commands/move.c new file mode 100644 index 0000000..43fce0d --- /dev/null +++ b/sway/commands/move.c @@ -0,0 +1,1025 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "sway/commands.h" +#include "sway/input/cursor.h" +#include "sway/input/seat.h" +#include "sway/ipc-server.h" +#include "sway/output.h" +#include "sway/server.h" +#include "sway/tree/arrange.h" +#include "sway/tree/container.h" +#include "sway/tree/root.h" +#include "sway/tree/workspace.h" +#include "stringop.h" +#include "list.h" +#include "log.h" +#include "util.h" + +static const char expected_syntax[] = + "Expected 'move <[px] px>' or " + "'move [--no-auto-back-and-forth] [to] workspace ' or " + "'move [to] output ' or " + "'move [to] mark '"; + +static struct sway_output *output_in_direction(const char *direction_string, + struct sway_output *reference, int ref_lx, int ref_ly) { + if (strcasecmp(direction_string, "current") == 0) { + struct sway_workspace *active_ws = + seat_get_focused_workspace(config->handler_context.seat); + if (!active_ws) { + return NULL; + } + return active_ws->output; + } + + struct { + char *name; + enum wlr_direction direction; + } names[] = { + { "up", WLR_DIRECTION_UP }, + { "down", WLR_DIRECTION_DOWN }, + { "left", WLR_DIRECTION_LEFT }, + { "right", WLR_DIRECTION_RIGHT }, + }; + + enum wlr_direction direction = 0; + + for (size_t i = 0; i < sizeof(names) / sizeof(names[0]); ++i) { + if (strcasecmp(names[i].name, direction_string) == 0) { + direction = names[i].direction; + break; + } + } + + if (reference && direction) { + struct wlr_output *target = wlr_output_layout_adjacent_output( + root->output_layout, direction, reference->wlr_output, + ref_lx, ref_ly); + + if (!target) { + target = wlr_output_layout_farthest_output( + root->output_layout, opposite_direction(direction), + reference->wlr_output, ref_lx, ref_ly); + } + + if (target) { + return target->data; + } + } + + return output_by_name_or_id(direction_string); +} + +static bool is_parallel(enum sway_container_layout layout, + enum wlr_direction dir) { + switch (layout) { + case L_TABBED: + case L_HORIZ: + return dir == WLR_DIRECTION_LEFT || dir == WLR_DIRECTION_RIGHT; + case L_STACKED: + case L_VERT: + return dir == WLR_DIRECTION_UP || dir == WLR_DIRECTION_DOWN; + default: + return false; + } +} + +/** + * Ensures all seats focus the fullscreen container if needed. + */ +static void workspace_focus_fullscreen(struct sway_workspace *workspace) { + if (!workspace->fullscreen) { + return; + } + struct sway_seat *seat; + struct sway_workspace *focus_ws; + wl_list_for_each(seat, &server.input->seats, link) { + focus_ws = seat_get_focused_workspace(seat); + if (focus_ws == workspace) { + struct sway_node *new_focus = + seat_get_focus_inactive(seat, &workspace->fullscreen->node); + seat_set_raw_focus(seat, new_focus); + } + } +} + +static void container_move_to_container_from_direction( + struct sway_container *container, struct sway_container *destination, + enum wlr_direction move_dir) { + if (destination->view) { + if (destination->pending.parent == container->pending.parent && + destination->pending.workspace == container->pending.workspace) { + sway_log(SWAY_DEBUG, "Swapping siblings"); + list_t *siblings = container_get_siblings(container); + int container_index = list_find(siblings, container); + int destination_index = list_find(siblings, destination); + list_swap(siblings, container_index, destination_index); + container_update_representation(container); + } else { + sway_log(SWAY_DEBUG, "Promoting to sibling of cousin"); + int offset = + move_dir == WLR_DIRECTION_LEFT || move_dir == WLR_DIRECTION_UP; + int index = container_sibling_index(destination) + offset; + if (destination->pending.parent) { + container_insert_child(destination->pending.parent, container, index); + } else { + workspace_insert_tiling(destination->pending.workspace, + container, index); + } + container->pending.width = container->pending.height = 0; + container->width_fraction = container->height_fraction = 0; + workspace_squash(destination->pending.workspace); + } + return; + } + + if (is_parallel(destination->pending.layout, move_dir)) { + sway_log(SWAY_DEBUG, "Reparenting container (parallel)"); + int index = + move_dir == WLR_DIRECTION_RIGHT || move_dir == WLR_DIRECTION_DOWN ? + 0 : destination->pending.children->length; + container_insert_child(destination, container, index); + container->pending.width = container->pending.height = 0; + container->width_fraction = container->height_fraction = 0; + workspace_squash(destination->pending.workspace); + return; + } + + sway_log(SWAY_DEBUG, "Reparenting container (perpendicular)"); + struct sway_node *focus_inactive = seat_get_active_tiling_child( + config->handler_context.seat, &destination->node); + if (!focus_inactive || focus_inactive == &destination->node) { + // The container has no children + container_add_child(destination, container); + return; + } + + // Try again but with the child + container_move_to_container_from_direction(container, + focus_inactive->sway_container, move_dir); +} + +static void container_move_to_workspace_from_direction( + struct sway_container *container, struct sway_workspace *workspace, + enum wlr_direction move_dir) { + container->pending.width = container->pending.height = 0; + container->width_fraction = container->height_fraction = 0; + + if (is_parallel(workspace->layout, move_dir)) { + sway_log(SWAY_DEBUG, "Reparenting container (parallel)"); + int index = + move_dir == WLR_DIRECTION_RIGHT || move_dir == WLR_DIRECTION_DOWN ? + 0 : workspace->tiling->length; + workspace_insert_tiling(workspace, container, index); + return; + } + + sway_log(SWAY_DEBUG, "Reparenting container (perpendicular)"); + struct sway_container *focus_inactive = seat_get_focus_inactive_tiling( + config->handler_context.seat, workspace); + if (!focus_inactive) { + // The workspace has no tiling children + workspace_add_tiling(workspace, container); + return; + } + while (focus_inactive->pending.parent) { + focus_inactive = focus_inactive->pending.parent; + } + container_move_to_container_from_direction(container, focus_inactive, + move_dir); +} + +static void container_move_to_workspace(struct sway_container *container, + struct sway_workspace *workspace) { + if (container->pending.workspace == workspace) { + return; + } + struct sway_workspace *old_workspace = container->pending.workspace; + if (container_is_floating(container)) { + struct sway_output *old_output = container->pending.workspace->output; + container_detach(container); + workspace_add_floating(workspace, container); + container_handle_fullscreen_reparent(container); + // If changing output, adjust the coordinates of the window. + if (old_output != workspace->output && !container->pending.fullscreen_mode) { + struct wlr_box workspace_box, old_workspace_box; + workspace_get_box(workspace, &workspace_box); + workspace_get_box(old_workspace, &old_workspace_box); + floating_fix_coordinates(container, &old_workspace_box, &workspace_box); + if (container->scratchpad && workspace->output) { + struct wlr_box output_box; + output_get_box(workspace->output, &output_box); + container->transform = workspace_box; + } + } + } else { + container_detach(container); + if (workspace_is_empty(workspace) && container->pending.children) { + workspace_unwrap_children(workspace, container); + container_reap_empty(container); + } else { + container->pending.width = container->pending.height = 0; + container->width_fraction = container->height_fraction = 0; + workspace_add_tiling(workspace, container); + } + container_update_representation(container); + } + if (container->view) { + ipc_event_window(container, "move"); + } + workspace_detect_urgent(old_workspace); + workspace_detect_urgent(workspace); + workspace_focus_fullscreen(workspace); +} + +static void container_move_to_container(struct sway_container *container, + struct sway_container *destination) { + if (container == destination + || container_has_ancestor(destination, container)) { + return; + } + if (container_is_floating(container)) { + container_move_to_workspace(container, destination->pending.workspace); + return; + } + struct sway_workspace *old_workspace = container->pending.workspace; + + container_detach(container); + container->pending.width = container->pending.height = 0; + container->width_fraction = container->height_fraction = 0; + + if (destination->view) { + container_add_sibling(destination, container, 1); + } else { + container_add_child(destination, container); + } + + if (container->view) { + ipc_event_window(container, "move"); + } + + if (destination->pending.workspace) { + workspace_focus_fullscreen(destination->pending.workspace); + workspace_detect_urgent(destination->pending.workspace); + } + + if (old_workspace && old_workspace != destination->pending.workspace) { + workspace_detect_urgent(old_workspace); + } +} + +static bool container_move_to_next_output(struct sway_container *container, + struct sway_output *output, enum wlr_direction move_dir) { + struct sway_output *next_output = + output_get_in_direction(output, move_dir); + if (next_output) { + struct sway_workspace *ws = output_get_active_workspace(next_output); + if (!sway_assert(ws, "Expected output to have a workspace")) { + return false; + } + switch (container->pending.fullscreen_mode) { + case FULLSCREEN_NONE: + container_move_to_workspace_from_direction(container, ws, move_dir); + return true; + case FULLSCREEN_WORKSPACE: + container_move_to_workspace(container, ws); + return true; + case FULLSCREEN_GLOBAL: + return false; + } + } + return false; +} + +// Returns true if moved +static bool container_move_in_direction(struct sway_container *container, + enum wlr_direction move_dir) { + // If moving a fullscreen view, only consider outputs + switch (container->pending.fullscreen_mode) { + case FULLSCREEN_NONE: + break; + case FULLSCREEN_WORKSPACE: + return container_move_to_next_output(container, + container->pending.workspace->output, move_dir); + case FULLSCREEN_GLOBAL: + return false; + } + + int offs = + move_dir == WLR_DIRECTION_LEFT || move_dir == WLR_DIRECTION_UP ? -1 : 1; + int index = -1; + int desired = -1; + list_t *siblings = NULL; + struct sway_container *target = NULL; + + // Look for a suitable ancestor of the container to move within + struct sway_container *ancestor = NULL; + struct sway_container *current = container; + bool wrapped = false; + while (!ancestor) { + // Don't allow containers to move out of their + // fullscreen or floating parent + if (current->pending.fullscreen_mode || container_is_floating(current)) { + return false; + } + + enum sway_container_layout parent_layout = container_parent_layout(current); + if (!is_parallel(parent_layout, move_dir)) { + if (!current->pending.parent) { + // No parallel parent, so we reorient the workspace + current = workspace_wrap_children(current->pending.workspace); + current->pending.workspace->layout = + move_dir == WLR_DIRECTION_LEFT || + move_dir == WLR_DIRECTION_RIGHT ? + L_HORIZ : L_VERT; + container->pending.height = container->pending.width = 0; + container->height_fraction = container->width_fraction = 0; + workspace_update_representation(current->pending.workspace); + wrapped = true; + } else { + // Keep looking for a parallel parent + current = current->pending.parent; + } + continue; + } + + // Only scratchpad hidden containers don't have siblings + // so siblings != NULL here + siblings = container_get_siblings(current); + index = list_find(siblings, current); + desired = index + offs; + target = desired == -1 || desired == siblings->length ? + NULL : siblings->items[desired]; + + // If the move is simple we can complete it here early + if (current == container) { + if (target) { + // Container will swap with or descend into its neighbor + container_move_to_container_from_direction(container, + target, move_dir); + return true; + } else if (!container->pending.parent) { + // Container is at workspace level so we move it to the + // next workspace if possible + return container_move_to_next_output(container, + current->pending.workspace->output, move_dir); + } else { + // Container has escaped its immediate parallel parent + current = current->pending.parent; + continue; + } + } + + // We found a suitable ancestor, the loop will end + ancestor = current; + } + + if (target) { + // Container will move in with its cousin + container_move_to_container_from_direction(container, + target, move_dir); + return true; + } else if (!wrapped && !container->pending.parent->pending.parent && + container->pending.parent->pending.children->length == 1) { + // Treat singleton children as if they are at workspace level like i3 + // https://github.com/i3/i3/blob/1d9160f2d247dbaa83fb62f02fd7041dec767fc2/src/move.c#L367 + return container_move_to_next_output(container, + ancestor->pending.workspace->output, move_dir); + } else { + // Container will be promoted + struct sway_container *old_parent = container->pending.parent; + if (ancestor->pending.parent) { + // Container will move in with its parent + container_insert_child(ancestor->pending.parent, container, + index + (offs < 0 ? 0 : 1)); + } else { + // Container will move to workspace level, + // may be re-split by workspace_layout + workspace_insert_tiling(ancestor->pending.workspace, container, + index + (offs < 0 ? 0 : 1)); + } + ancestor->pending.height = ancestor->pending.width = 0; + ancestor->height_fraction = ancestor->width_fraction = 0; + if (old_parent) { + container_reap_empty(old_parent); + } + workspace_squash(container->pending.workspace); + return true; + } +} + +static struct cmd_results *cmd_move_to_scratchpad(void); + +static struct cmd_results *cmd_move_container(bool no_auto_back_and_forth, + int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "move container/window", + EXPECTED_AT_LEAST, 2))) { + return error; + } + + struct sway_node *node = config->handler_context.node; + struct sway_workspace *workspace = config->handler_context.workspace; + struct sway_container *container = config->handler_context.container; + if (node->type == N_WORKSPACE) { + if (workspace->tiling->length == 0) { + return cmd_results_new(CMD_FAILURE, + "Can't move an empty workspace"); + } + container = workspace_wrap_children(workspace); + } + + if (container->pending.fullscreen_mode == FULLSCREEN_GLOBAL) { + return cmd_results_new(CMD_FAILURE, + "Can't move fullscreen global container"); + } + + struct sway_seat *seat = config->handler_context.seat; + struct sway_container *old_parent = container->pending.parent; + struct sway_workspace *old_ws = container->pending.workspace; + struct sway_output *old_output = old_ws ? old_ws->output : NULL; + struct sway_node *destination = NULL; + + // determine destination + if (strcasecmp(argv[0], "workspace") == 0) { + // move container to workspace x + struct sway_workspace *ws = NULL; + char *ws_name = NULL; + if (strcasecmp(argv[1], "next") == 0 || + strcasecmp(argv[1], "prev") == 0 || + strcasecmp(argv[1], "next_on_output") == 0 || + strcasecmp(argv[1], "prev_on_output") == 0 || + strcasecmp(argv[1], "current") == 0) { + ws = workspace_by_name(argv[1]); + } else if (strcasecmp(argv[1], "back_and_forth") == 0) { + if (!(ws = workspace_by_name(argv[1]))) { + if (seat->prev_workspace_name) { + ws_name = strdup(seat->prev_workspace_name); + } else { + return cmd_results_new(CMD_FAILURE, + "No workspace was previously active."); + } + } + } else { + if (strcasecmp(argv[1], "number") == 0) { + // move [window|container] [to] "workspace number x" + if (argc < 3) { + return cmd_results_new(CMD_INVALID, "%s", expected_syntax); + } + if (!isdigit(argv[2][0])) { + return cmd_results_new(CMD_INVALID, + "Invalid workspace number '%s'", argv[2]); + } + ws_name = join_args(argv + 2, argc - 2); + ws = workspace_by_number(ws_name); + } else { + ws_name = join_args(argv + 1, argc - 1); + ws = workspace_by_name(ws_name); + } + + if (!no_auto_back_and_forth && config->auto_back_and_forth && + seat->prev_workspace_name) { + // auto back and forth move + if (old_ws && old_ws->name && + strcmp(old_ws->name, ws_name) == 0) { + // if target workspace is the current one + free(ws_name); + ws_name = strdup(seat->prev_workspace_name); + ws = workspace_by_name(ws_name); + } + } + } + if (!ws) { + // We have to create the workspace, but if the container is + // sticky and the workspace is going to be created on the same + // output, we'll bail out first. + if (container_is_sticky_or_child(container)) { + struct sway_output *new_output = + workspace_get_initial_output(ws_name); + if (old_output == new_output) { + free(ws_name); + return cmd_results_new(CMD_FAILURE, + "Can't move sticky container to another workspace " + "on the same output"); + } + } + ws = workspace_create(NULL, ws_name); + arrange_workspace(ws); + } + free(ws_name); + struct sway_container *dst = seat_get_focus_inactive_tiling(seat, ws); + destination = dst ? &dst->node : &ws->node; + } else if (strcasecmp(argv[0], "output") == 0) { + struct sway_output *new_output = output_in_direction(argv[1], + old_output, container->pending.x, container->pending.y); + if (!new_output) { + return cmd_results_new(CMD_FAILURE, + "Can't find output with name/direction '%s'", argv[1]); + } + destination = seat_get_focus_inactive(seat, &new_output->node); + } else if (strcasecmp(argv[0], "mark") == 0) { + struct sway_container *dest_con = container_find_mark(argv[1]); + if (dest_con == NULL) { + return cmd_results_new(CMD_FAILURE, + "Mark '%s' not found", argv[1]); + } + destination = &dest_con->node; + } else { + return cmd_results_new(CMD_INVALID, "%s", expected_syntax); + } + + if (destination->type == N_CONTAINER && + container_is_scratchpad_hidden(destination->sway_container)) { + return cmd_move_to_scratchpad(); + } + + if (container_is_sticky_or_child(container) && old_output && + node_has_ancestor(destination, &old_output->node)) { + return cmd_results_new(CMD_FAILURE, "Can't move sticky " + "container to another workspace on the same output"); + } + + struct sway_output *new_output = node_get_output(destination); + struct sway_workspace *new_output_last_ws = NULL; + if (new_output && old_output != new_output) { + new_output_last_ws = output_get_active_workspace(new_output); + } + + // save focus, in case it needs to be restored + struct sway_node *focus = seat_get_focus(seat); + + // move container + if (container_is_scratchpad_hidden_or_child(container)) { + container_detach(container); + root_scratchpad_show(container); + } + switch (destination->type) { + case N_WORKSPACE: + container_move_to_workspace(container, destination->sway_workspace); + break; + case N_OUTPUT: { + struct sway_output *output = destination->sway_output; + struct sway_workspace *ws = output_get_active_workspace(output); + if (!sway_assert(ws, "Expected output to have a workspace")) { + return cmd_results_new(CMD_FAILURE, + "Expected output to have a workspace"); + } + container_move_to_workspace(container, ws); + } + break; + case N_CONTAINER: + container_move_to_container(container, destination->sway_container); + break; + case N_ROOT: + break; + } + + // restore focus on destination output back to its last active workspace + struct sway_workspace *new_workspace = new_output ? + output_get_active_workspace(new_output) : NULL; + if (new_output && + !sway_assert(new_workspace, "Expected output to have a workspace")) { + return cmd_results_new(CMD_FAILURE, + "Expected output to have a workspace"); + } + + if (new_output_last_ws && new_output_last_ws != new_workspace) { + struct sway_node *new_output_last_focus = + seat_get_focus_inactive(seat, &new_output_last_ws->node); + seat_set_raw_focus(seat, new_output_last_focus); + } + + // restore focus + if (focus == &container->node) { + focus = NULL; + if (old_parent) { + focus = seat_get_focus_inactive(seat, &old_parent->node); + } + if (!focus && old_ws) { + focus = seat_get_focus_inactive(seat, &old_ws->node); + } + } + seat_set_focus(seat, focus); + + // clean-up, destroying parents if the container was the last child + if (old_parent) { + container_reap_empty(old_parent); + } else if (old_ws) { + workspace_consider_destroy(old_ws); + } + + // arrange windows + if (root->fullscreen_global) { + arrange_root(); + } else { + if (old_ws && !old_ws->node.destroying) { + arrange_workspace(old_ws); + } + arrange_node(node_get_parent(destination)); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} + +static struct cmd_results *cmd_move_workspace(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "move workspace", EXPECTED_AT_LEAST, 1))) { + return error; + } + + if (strcasecmp(argv[0], "output") == 0) { + --argc; ++argv; + } + + if (!argc) { + return cmd_results_new(CMD_INVALID, + "Expected 'move workspace to [output] '"); + } + + struct sway_workspace *workspace = config->handler_context.workspace; + if (!workspace) { + return cmd_results_new(CMD_FAILURE, "No workspace to move"); + } + + struct sway_output *old_output = workspace->output; + int center_x = workspace->width / 2 + workspace->x, + center_y = workspace->height / 2 + workspace->y; + struct sway_output *new_output = output_in_direction(argv[0], + old_output, center_x, center_y); + if (!new_output) { + return cmd_results_new(CMD_FAILURE, + "Can't find output with name/direction '%s'", argv[0]); + } + workspace_move_to_output(workspace, new_output); + + arrange_output(old_output); + arrange_output(new_output); + + struct sway_seat *seat = config->handler_context.seat; + struct sway_node *focus = seat_get_focus_inactive(seat, &workspace->node); + seat_set_focus(seat, focus); + seat_consider_warp_to_focus(seat); + + return cmd_results_new(CMD_SUCCESS, NULL); +} + +static struct cmd_results *cmd_move_in_direction( + enum wlr_direction direction, int argc, char **argv) { + int move_amt = 10; + if (argc) { + char *inv; + move_amt = (int)strtol(argv[0], &inv, 10); + if (*inv != '\0' && strcasecmp(inv, "px") != 0) { + return cmd_results_new(CMD_FAILURE, "Invalid distance specified"); + } + } + + struct sway_container *container = config->handler_context.container; + if (!container) { + return cmd_results_new(CMD_FAILURE, + "Cannot move workspaces in a direction"); + } + if (container_is_floating(container)) { + if (container->pending.fullscreen_mode) { + return cmd_results_new(CMD_FAILURE, + "Cannot move fullscreen floating container"); + } + double lx = container->pending.x; + double ly = container->pending.y; + switch (direction) { + case WLR_DIRECTION_LEFT: + lx -= move_amt; + break; + case WLR_DIRECTION_RIGHT: + lx += move_amt; + break; + case WLR_DIRECTION_UP: + ly -= move_amt; + break; + case WLR_DIRECTION_DOWN: + ly += move_amt; + break; + } + container_floating_move_to(container, lx, ly); + return cmd_results_new(CMD_SUCCESS, NULL); + } + struct sway_workspace *old_ws = container->pending.workspace; + struct sway_container *old_parent = container->pending.parent; + + if (!container_move_in_direction(container, direction)) { + // Container didn't move + return cmd_results_new(CMD_SUCCESS, NULL); + } + + // clean-up, destroying parents if the container was the last child + if (old_parent) { + container_reap_empty(old_parent); + } else if (old_ws) { + workspace_consider_destroy(old_ws); + } + + struct sway_workspace *new_ws = container->pending.workspace; + + if (root->fullscreen_global) { + arrange_root(); + } else { + arrange_workspace(old_ws); + if (new_ws != old_ws) { + arrange_workspace(new_ws); + } + } + + if (container->view) { + ipc_event_window(container, "move"); + } + + container_end_mouse_operation(container); + + return cmd_results_new(CMD_SUCCESS, NULL); +} + +static struct cmd_results *cmd_move_to_position_pointer( + struct sway_container *container) { + struct sway_seat *seat = config->handler_context.seat; + if (!seat->cursor) { + return cmd_results_new(CMD_FAILURE, "No cursor device"); + } + struct wlr_cursor *cursor = seat->cursor->cursor; + /* Determine where to put the window. */ + double lx = cursor->x - container->pending.width / 2; + double ly = cursor->y - container->pending.height / 2; + + /* Correct target coordinates to be in bounds (on screen). */ + struct wlr_output *output = wlr_output_layout_output_at( + root->output_layout, cursor->x, cursor->y); + if (output) { + struct wlr_box box; + wlr_output_layout_get_box(root->output_layout, output, &box); + lx = fmax(lx, box.x); + ly = fmax(ly, box.y); + if (lx + container->pending.width > box.x + box.width) { + lx = box.x + box.width - container->pending.width; + } + if (ly + container->pending.height > box.y + box.height) { + ly = box.y + box.height - container->pending.height; + } + } + + /* Actually move the container. */ + container_floating_move_to(container, lx, ly); + return cmd_results_new(CMD_SUCCESS, NULL); +} + +static const char expected_position_syntax[] = + "Expected 'move [absolute] position [px] [px]' or " + "'move [absolute] position center' or " + "'move position cursor|mouse|pointer'"; + +static struct cmd_results *cmd_move_to_position(int argc, char **argv) { + struct sway_container *container = config->handler_context.container; + if (!container || !container_is_floating(container)) { + return cmd_results_new(CMD_FAILURE, "Only floating containers " + "can be moved to an absolute position"); + } + + if (!argc) { + return cmd_results_new(CMD_INVALID, "%s", expected_position_syntax); + } + + bool absolute = false; + if (strcmp(argv[0], "absolute") == 0) { + absolute = true; + --argc; + ++argv; + } + if (!argc) { + return cmd_results_new(CMD_INVALID, "%s", expected_position_syntax); + } + if (strcmp(argv[0], "position") == 0) { + --argc; + ++argv; + } + if (!argc) { + return cmd_results_new(CMD_INVALID, "%s", expected_position_syntax); + } + if (strcmp(argv[0], "cursor") == 0 || strcmp(argv[0], "mouse") == 0 || + strcmp(argv[0], "pointer") == 0) { + if (absolute) { + return cmd_results_new(CMD_INVALID, "%s", expected_position_syntax); + } + return cmd_move_to_position_pointer(container); + } else if (strcmp(argv[0], "center") == 0) { + double lx, ly; + if (absolute) { + lx = root->x + (root->width - container->pending.width) / 2; + ly = root->y + (root->height - container->pending.height) / 2; + } else { + struct sway_workspace *ws = container->pending.workspace; + if (!ws) { + struct sway_seat *seat = config->handler_context.seat; + ws = seat_get_focused_workspace(seat); + } + lx = ws->x + (ws->width - container->pending.width) / 2; + ly = ws->y + (ws->height - container->pending.height) / 2; + } + container_floating_move_to(container, lx, ly); + return cmd_results_new(CMD_SUCCESS, NULL); + } + + if (argc < 2) { + return cmd_results_new(CMD_FAILURE, "%s", expected_position_syntax); + } + + struct movement_amount lx = { .amount = 0, .unit = MOVEMENT_UNIT_INVALID }; + // X direction + int num_consumed_args = parse_movement_amount(argc, argv, &lx); + argc -= num_consumed_args; + argv += num_consumed_args; + if (lx.unit == MOVEMENT_UNIT_INVALID) { + return cmd_results_new(CMD_INVALID, "Invalid x position specified"); + } + + if (argc < 1) { + return cmd_results_new(CMD_FAILURE, "%s", expected_position_syntax); + } + + struct movement_amount ly = { .amount = 0, .unit = MOVEMENT_UNIT_INVALID }; + // Y direction + num_consumed_args = parse_movement_amount(argc, argv, &ly); + argc -= num_consumed_args; + argv += num_consumed_args; + if (argc > 0) { + return cmd_results_new(CMD_INVALID, "%s", expected_position_syntax); + } + if (ly.unit == MOVEMENT_UNIT_INVALID) { + return cmd_results_new(CMD_INVALID, "Invalid y position specified"); + } + + struct sway_workspace *ws = container->pending.workspace; + if (!ws) { + struct sway_seat *seat = config->handler_context.seat; + ws = seat_get_focused_workspace(seat); + } + + switch (lx.unit) { + case MOVEMENT_UNIT_PPT: + if (container_is_scratchpad_hidden(container)) { + return cmd_results_new(CMD_FAILURE, + "Cannot move a hidden scratchpad container by ppt"); + } + if (absolute) { + return cmd_results_new(CMD_FAILURE, + "Cannot move to absolute positions by ppt"); + } + // Convert to px + lx.amount = ws->width * lx.amount / 100; + lx.unit = MOVEMENT_UNIT_PX; + // Falls through + case MOVEMENT_UNIT_PX: + case MOVEMENT_UNIT_DEFAULT: + break; + case MOVEMENT_UNIT_INVALID: + sway_assert(false, "invalid x unit"); + break; + } + + switch (ly.unit) { + case MOVEMENT_UNIT_PPT: + if (container_is_scratchpad_hidden(container)) { + return cmd_results_new(CMD_FAILURE, + "Cannot move a hidden scratchpad container by ppt"); + } + if (absolute) { + return cmd_results_new(CMD_FAILURE, + "Cannot move to absolute positions by ppt"); + } + // Convert to px + ly.amount = ws->height * ly.amount / 100; + ly.unit = MOVEMENT_UNIT_PX; + // Falls through + case MOVEMENT_UNIT_PX: + case MOVEMENT_UNIT_DEFAULT: + break; + case MOVEMENT_UNIT_INVALID: + sway_assert(false, "invalid y unit"); + break; + } + if (!absolute) { + lx.amount += ws->x; + ly.amount += ws->y; + } + container_floating_move_to(container, lx.amount, ly.amount); + return cmd_results_new(CMD_SUCCESS, NULL); +} + +static struct cmd_results *cmd_move_to_scratchpad(void) { + struct sway_node *node = config->handler_context.node; + struct sway_container *con = config->handler_context.container; + struct sway_workspace *ws = config->handler_context.workspace; + if (node->type == N_WORKSPACE && ws->tiling->length == 0) { + return cmd_results_new(CMD_INVALID, + "Can't move an empty workspace to the scratchpad"); + } + if (node->type == N_WORKSPACE) { + // Wrap the workspace's children in a container + con = workspace_wrap_children(ws); + ws->layout = L_HORIZ; + } + + // If the container is in a floating split container, + // operate on the split container instead of the child. + if (container_is_floating_or_child(con)) { + while (con->pending.parent) { + con = con->pending.parent; + } + } + + if (!con->scratchpad) { + root_scratchpad_add_container(con, NULL); + } else if (con->pending.workspace) { + root_scratchpad_hide(con); + } + return cmd_results_new(CMD_SUCCESS, NULL); +} + +static const char expected_full_syntax[] = "Expected " + "'move left|right|up|down [ [px]]'" + " or 'move [--no-auto-back-and-forth] [window|container] [to] workspace" + " |next|prev|next_on_output|prev_on_output|current|(number )'" + " or 'move [window|container] [to] output |left|right|up|down'" + " or 'move [window|container] [to] mark '" + " or 'move [window|container] [to] scratchpad'" + " or 'move workspace to [output] |left|right|up|down'" + " or 'move [window|container] [to] [absolute] position [px] [px]'" + " or 'move [window|container] [to] [absolute] position center'" + " or 'move [window|container] [to] position mouse|cursor|pointer'"; + +struct cmd_results *cmd_move(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "move", EXPECTED_AT_LEAST, 1))) { + return error; + } + if (!root->outputs->length) { + return cmd_results_new(CMD_INVALID, + "Can't run this command while there's no outputs connected."); + } + + if (strcasecmp(argv[0], "left") == 0) { + return cmd_move_in_direction(WLR_DIRECTION_LEFT, --argc, ++argv); + } else if (strcasecmp(argv[0], "right") == 0) { + return cmd_move_in_direction(WLR_DIRECTION_RIGHT, --argc, ++argv); + } else if (strcasecmp(argv[0], "up") == 0) { + return cmd_move_in_direction(WLR_DIRECTION_UP, --argc, ++argv); + } else if (strcasecmp(argv[0], "down") == 0) { + return cmd_move_in_direction(WLR_DIRECTION_DOWN, --argc, ++argv); + } else if (strcasecmp(argv[0], "workspace") == 0 && argc >= 2 + && (strcasecmp(argv[1], "to") == 0 || + strcasecmp(argv[1], "output") == 0)) { + argc -= 2; argv += 2; + return cmd_move_workspace(argc, argv); + } + + bool no_auto_back_and_forth = false; + if (strcasecmp(argv[0], "--no-auto-back-and-forth") == 0) { + no_auto_back_and_forth = true; + --argc; ++argv; + } + + if (argc > 0 && (strcasecmp(argv[0], "window") == 0 || + strcasecmp(argv[0], "container") == 0)) { + --argc; ++argv; + } + + if (argc > 0 && strcasecmp(argv[0], "to") == 0) { + --argc; ++argv; + } + + if (!argc) { + return cmd_results_new(CMD_INVALID, "%s", expected_full_syntax); + } + + // Only `move [window|container] [to] workspace` supports + // `--no-auto-back-and-forth` so treat others as invalid syntax + if (no_auto_back_and_forth && strcasecmp(argv[0], "workspace") != 0) { + return cmd_results_new(CMD_INVALID, "%s", expected_full_syntax); + } + + if (strcasecmp(argv[0], "workspace") == 0 || + strcasecmp(argv[0], "output") == 0 || + strcasecmp(argv[0], "mark") == 0) { + return cmd_move_container(no_auto_back_and_forth, argc, argv); + } else if (strcasecmp(argv[0], "scratchpad") == 0) { + return cmd_move_to_scratchpad(); + } else if (strcasecmp(argv[0], "position") == 0 || + (argc > 1 && strcasecmp(argv[0], "absolute") == 0 && + strcasecmp(argv[1], "position") == 0)) { + return cmd_move_to_position(argc, argv); + } + return cmd_results_new(CMD_INVALID, "%s", expected_full_syntax); +} diff --git a/sway/commands/new_float.c b/sway/commands/new_float.c new file mode 100644 index 0000000..4fedb4c --- /dev/null +++ b/sway/commands/new_float.c @@ -0,0 +1,13 @@ +#include "log.h" +#include "sway/commands.h" +#include "sway/config.h" + +struct cmd_results *cmd_new_float(int argc, char **argv) { + sway_log(SWAY_INFO, "Warning: new_float is deprecated. " + "Use default_floating_border instead."); + if (config->reading) { + config_add_swaynag_warning("new_float is deprecated. " + "Use default_floating_border instead."); + } + return cmd_default_floating_border(argc, argv); +} diff --git a/sway/commands/new_window.c b/sway/commands/new_window.c new file mode 100644 index 0000000..e8caa48 --- /dev/null +++ b/sway/commands/new_window.c @@ -0,0 +1,13 @@ +#include "log.h" +#include "sway/commands.h" +#include "sway/config.h" + +struct cmd_results *cmd_new_window(int argc, char **argv) { + sway_log(SWAY_INFO, "Warning: new_window is deprecated. " + "Use default_border instead."); + if (config->reading) { + config_add_swaynag_warning("new_window is deprecated. " + "Use default_border instead."); + } + return cmd_default_border(argc, argv); +} diff --git a/sway/commands/no_focus.c b/sway/commands/no_focus.c new file mode 100644 index 0000000..ccfdec8 --- /dev/null +++ b/sway/commands/no_focus.c @@ -0,0 +1,34 @@ +#include +#include "sway/commands.h" +#include "sway/criteria.h" +#include "list.h" +#include "log.h" + +struct cmd_results *cmd_no_focus(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "no_focus", EXPECTED_AT_LEAST, 1))) { + return error; + } + + char *err_str = NULL; + struct criteria *criteria = criteria_parse(argv[0], &err_str); + if (!criteria) { + error = cmd_results_new(CMD_INVALID, "%s", err_str); + free(err_str); + return error; + } + + + criteria->type = CT_NO_FOCUS; + + // Check if it already exists + if (criteria_already_exists(criteria)) { + sway_log(SWAY_DEBUG, "no_focus already exists: '%s'", criteria->raw); + criteria_destroy(criteria); + return cmd_results_new(CMD_SUCCESS, NULL); + } + + list_add(config->criteria, criteria); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/nop.c b/sway/commands/nop.c new file mode 100644 index 0000000..973ade0 --- /dev/null +++ b/sway/commands/nop.c @@ -0,0 +1,5 @@ +#include "sway/commands.h" + +struct cmd_results *cmd_nop(int argc, char **argv) { + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/opacity.c b/sway/commands/opacity.c new file mode 100644 index 0000000..9acef4c --- /dev/null +++ b/sway/commands/opacity.c @@ -0,0 +1,45 @@ +#include +#include +#include +#include "sway/commands.h" +#include "sway/tree/container.h" +#include "sway/output.h" +#include "log.h" + +struct cmd_results *cmd_opacity(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "opacity", EXPECTED_AT_LEAST, 1))) { + return error; + } + + struct sway_container *con = config->handler_context.container; + + if (con == NULL) { + return cmd_results_new(CMD_FAILURE, "No current container"); + } + + char *err; + float val = strtof(argc == 1 ? argv[0] : argv[1], &err); + if (*err) { + return cmd_results_new(CMD_INVALID, "opacity float invalid"); + } + + if (!strcasecmp(argv[0], "plus")) { + val = con->alpha + val; + } else if (!strcasecmp(argv[0], "minus")) { + val = con->alpha - val; + } else if (argc > 1 && strcasecmp(argv[0], "set")) { + return cmd_results_new(CMD_INVALID, + "Expected: set|plus|minus <0..1>: %s", argv[0]); + } + + if (val < 0 || val > 1) { + return cmd_results_new(CMD_FAILURE, "opacity value out of bounds"); + } + + con->alpha = val; + output_configure_scene(NULL, &con->scene_tree->node, false, NULL); + container_update(con); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/output.c b/sway/commands/output.c new file mode 100644 index 0000000..afff23f --- /dev/null +++ b/sway/commands/output.c @@ -0,0 +1,129 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/output.h" +#include "list.h" +#include "log.h" + +// must be in order for the bsearch +static const struct cmd_handler output_handlers[] = { + { "adaptive_sync", output_cmd_adaptive_sync }, + { "allow_tearing", output_cmd_allow_tearing }, + { "background", output_cmd_background }, + { "bg", output_cmd_background }, + { "color_profile", output_cmd_color_profile }, + { "disable", output_cmd_disable }, + { "dpms", output_cmd_dpms }, + { "enable", output_cmd_enable }, + { "hdr", output_cmd_hdr }, + { "max_render_time", output_cmd_max_render_time }, + { "mode", output_cmd_mode }, + { "modeline", output_cmd_modeline }, + { "pos", output_cmd_position }, + { "position", output_cmd_position }, + { "power", output_cmd_power }, + { "render_bit_depth", output_cmd_render_bit_depth }, + { "res", output_cmd_mode }, + { "resolution", output_cmd_mode }, + { "scale", output_cmd_scale }, + { "scale_filter", output_cmd_scale_filter }, + { "subpixel", output_cmd_subpixel }, + { "toggle", output_cmd_toggle }, + { "transform", output_cmd_transform }, + { "unplug", output_cmd_unplug }, +}; + +struct cmd_results *cmd_output(int argc, char **argv) { + struct cmd_results *error = checkarg(argc, "output", EXPECTED_AT_LEAST, 1); + if (error != NULL) { + return error; + } + + // The HEADLESS-1 output is a dummy output used when there's no outputs + // connected. It should never be configured. + if (strcasecmp(argv[0], root->fallback_output->wlr_output->name) == 0) { + return cmd_results_new(CMD_FAILURE, + "Refusing to configure the no op output"); + } + + struct output_config *output = NULL; + if (strcmp(argv[0], "-") == 0 || strcmp(argv[0], "--") == 0) { + if (config->reading) { + return cmd_results_new(CMD_FAILURE, + "Current output alias (%s) cannot be used in the config", + argv[0]); + } + struct sway_output *sway_output = config->handler_context.node ? + node_get_output(config->handler_context.node) : NULL; + if (!sway_output) { + return cmd_results_new(CMD_FAILURE, "Unknown output"); + } + if (sway_output == root->fallback_output) { + return cmd_results_new(CMD_FAILURE, + "Refusing to configure the no op output"); + } + if (strcmp(argv[0], "-") == 0) { + output = new_output_config(sway_output->wlr_output->name); + } else { + char identifier[128]; + output_get_identifier(identifier, 128, sway_output); + output = new_output_config(identifier); + } + } else { + output = new_output_config(argv[0]); + } + if (!output) { + sway_log(SWAY_ERROR, "Failed to allocate output config"); + return NULL; + } + argc--; argv++; + + config->handler_context.output_config = output; + + while (argc > 0) { + config->handler_context.leftovers.argc = 0; + config->handler_context.leftovers.argv = NULL; + + if (find_handler(*argv, output_handlers, sizeof(output_handlers))) { + error = config_subcommand(argv, argc, output_handlers, + sizeof(output_handlers)); + } else { + error = cmd_results_new(CMD_INVALID, + "Invalid output subcommand: %s.", *argv); + } + + if (error != NULL) { + goto fail; + } + + argc = config->handler_context.leftovers.argc; + argv = config->handler_context.leftovers.argv; + } + + config->handler_context.output_config = NULL; + config->handler_context.leftovers.argc = 0; + config->handler_context.leftovers.argv = NULL; + + bool background = output->background; + + store_output_config(output); + + if (config->reading) { + // When reading the config file, we wait till the end to do a single + // modeset and swaybg spawn. + return cmd_results_new(CMD_SUCCESS, NULL); + } + request_modeset(); + + if (background && !spawn_swaybg()) { + return cmd_results_new(CMD_FAILURE, + "Failed to apply background configuration"); + } + + return cmd_results_new(CMD_SUCCESS, NULL); + +fail: + config->handler_context.output_config = NULL; + free_output_config(output); + return error; +} diff --git a/sway/commands/output/adaptive_sync.c b/sway/commands/output/adaptive_sync.c new file mode 100644 index 0000000..4ce88f8 --- /dev/null +++ b/sway/commands/output/adaptive_sync.c @@ -0,0 +1,38 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/output.h" +#include "util.h" + +struct cmd_results *output_cmd_adaptive_sync(int argc, char **argv) { + if (!config->handler_context.output_config) { + return cmd_results_new(CMD_FAILURE, "Missing output config"); + } + if (argc == 0) { + return cmd_results_new(CMD_INVALID, "Missing adaptive_sync argument"); + } + + bool current_value = true; + if (strcasecmp(argv[0], "toggle") == 0) { + const char *oc_name = config->handler_context.output_config->name; + if (strcmp(oc_name, "*") == 0) { + return cmd_results_new(CMD_INVALID, + "Cannot apply toggle to all outputs"); + } + + struct sway_output *sway_output = all_output_by_name_or_id(oc_name); + if (!sway_output || !sway_output->wlr_output) { + return cmd_results_new(CMD_FAILURE, + "Cannot apply toggle to unknown output %s", oc_name); + } + + current_value = + sway_output->wlr_output->adaptive_sync_status == WLR_OUTPUT_ADAPTIVE_SYNC_ENABLED; + } + + config->handler_context.output_config->adaptive_sync = parse_boolean(argv[0], current_value); + + config->handler_context.leftovers.argc = argc - 1; + config->handler_context.leftovers.argv = argv + 1; + return NULL; +} diff --git a/sway/commands/output/allow_tearing.c b/sway/commands/output/allow_tearing.c new file mode 100644 index 0000000..9a183b9 --- /dev/null +++ b/sway/commands/output/allow_tearing.c @@ -0,0 +1,23 @@ +#include "sway/commands.h" +#include "sway/config.h" +#include "util.h" + +struct cmd_results *output_cmd_allow_tearing(int argc, char **argv) { + if (!config->handler_context.output_config) { + return cmd_results_new(CMD_FAILURE, "Missing output config"); + } + if (argc == 0) { + return cmd_results_new(CMD_INVALID, "Missing allow_tearing argument"); + } + + if (parse_boolean(argv[0], + (config->handler_context.output_config->allow_tearing == 1))) { + config->handler_context.output_config->allow_tearing = 1; + } else { + config->handler_context.output_config->allow_tearing = 0; + } + + config->handler_context.leftovers.argc = argc - 1; + config->handler_context.leftovers.argv = argv + 1; + return NULL; +} diff --git a/sway/commands/output/background.c b/sway/commands/output/background.c new file mode 100644 index 0000000..fe16216 --- /dev/null +++ b/sway/commands/output/background.c @@ -0,0 +1,150 @@ +#include +#include +#include +#include +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "log.h" +#include "stringop.h" + +static const char *bg_options[] = { + "stretch", + "center", + "fill", + "fit", + "tile", +}; + +static bool validate_color(const char *color) { + if (strlen(color) != 7 || color[0] != '#') { + return false; + } + + char *ptr = NULL; + strtol(&color[1], &ptr, 16); + return *ptr == '\0'; +} + +struct cmd_results *output_cmd_background(int argc, char **argv) { + if (!config->handler_context.output_config) { + return cmd_results_new(CMD_FAILURE, "Missing output config"); + } + if (!argc) { + return cmd_results_new(CMD_INVALID, + "Missing background file or color specification."); + } + if (argc < 2) { + return cmd_results_new(CMD_INVALID, + "Missing background scaling mode or `solid_color`."); + } + + struct output_config *output = config->handler_context.output_config; + char *src = NULL; + if (strcasecmp(argv[1], "solid_color") == 0) { + if (!validate_color(argv[0])) { + return cmd_results_new(CMD_INVALID, + "Colors should be of the form #RRGGBB"); + } + if (!(output->background = strdup(argv[0]))) goto cleanup; + if (!(output->background_option = strdup("solid_color"))) goto cleanup; + output->background_fallback = NULL; + argc -= 2; argv += 2; + } else { + bool valid = false; + char *mode; + size_t j; + for (j = 0; j < (size_t)argc; ++j) { + mode = argv[j]; + size_t n = sizeof(bg_options) / sizeof(char *); + for (size_t k = 0; k < n; ++k) { + if (strcasecmp(mode, bg_options[k]) == 0) { + valid = true; + break; + } + } + if (valid) { + break; + } + } + if (!valid) { + return cmd_results_new(CMD_INVALID, + "Missing background scaling mode."); + } + if (j == 0) { + return cmd_results_new(CMD_INVALID, "Missing background file"); + } + + if (!(src = join_args(argv, j))) goto cleanup; + if (!expand_path(&src)) { + struct cmd_results *cmd_res = cmd_results_new(CMD_INVALID, + "Invalid syntax (%s)", src); + free(src); + return cmd_res; + } + + if (config->reading && *src != '/') { + // src file is inside configuration dir + + char *conf = strdup(config->current_config_path); + if (!conf) goto cleanup; + + char *conf_path = dirname(conf); + char *real_src = malloc(strlen(conf_path) + strlen(src) + 2); + if (!real_src) { + free(conf); + goto cleanup; + } + + snprintf(real_src, strlen(conf_path) + strlen(src) + 2, "%s/%s", conf_path, src); + free(src); + free(conf); + src = real_src; + } + + bool can_access = access(src, F_OK) != -1; + argc -= j + 1; argv += j + 1; + free(output->background_option); + free(output->background_fallback); + free(output->background); + output->background = output->background_option = output->background_fallback = NULL; + char *fallback = NULL; + + if (argc && *argv[0] == '#') { + if (validate_color(argv[0])) { + if (!(fallback = strdup(argv[0]))) goto cleanup; + output->background_fallback = fallback; + } else { + sway_log(SWAY_ERROR, "fallback '%s' should be of the form #RRGGBB", argv[0]); + config_add_swaynag_warning("fallback '%s' should be of the form #RRGGBB\n", argv[0]); + } + argc--; argv++; + } + + if (!can_access) { + if (!fallback) { + sway_log(SWAY_ERROR, "Unable to access background file '%s' " + "and no valid fallback provided", src); + struct cmd_results *res = cmd_results_new(CMD_FAILURE, "Unable to access " + "background file '%s' and no valid fallback provided", src); + free(src); + return res; + } + sway_log(SWAY_DEBUG, "Cannot access file '%s', using fallback '%s'", src, fallback); + output->background = fallback; + if (!(output->background_option = strdup("solid_color"))) goto cleanup; + output->background_fallback = NULL; + } else { + output->background = src; + if (!(output->background_option = strdup(mode))) goto cleanup; + } + } + config->handler_context.leftovers.argc = argc; + config->handler_context.leftovers.argv = argv; + return NULL; + +cleanup: + free(src); + sway_log(SWAY_ERROR, "Failed to allocate resources"); + return cmd_results_new(CMD_FAILURE, "Unable to allocate resources"); +} diff --git a/sway/commands/output/color_profile.c b/sway/commands/output/color_profile.c new file mode 100644 index 0000000..0456b19 --- /dev/null +++ b/sway/commands/output/color_profile.c @@ -0,0 +1,134 @@ +#include +#include +#include +#include +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "stringop.h" + +static bool read_file_into_buf(const char *path, void **buf, size_t *size) { + /* Why not use fopen/fread directly? glibc will succesfully open directories, + * not just files, and supports seeking on them. Instead, we directly + * work with file descriptors and use the more consistent open/fstat/read. */ + int fd = open(path, O_RDONLY | O_NOCTTY | O_CLOEXEC); + if (fd == -1) { + return false; + } + char *b = NULL; + struct stat info; + if (fstat(fd, &info) == -1) { + goto fail; + } + // only regular files, to avoid issues with e.g. opening pipes + if (!S_ISREG(info.st_mode)) { + goto fail; + } + off_t s = info.st_size; + if (s <= 0) { + goto fail; + } + b = calloc(1, s); + if (!b) { + goto fail; + } + size_t nread = 0; + while (nread < (size_t)s) { + size_t to_read = (size_t)s - nread; + ssize_t r = read(fd, b + nread, to_read); + if ((r == -1 && errno != EINTR) || r == 0) { + goto fail; + } + nread += (size_t)r; + } + close(fd); + *buf = b; + *size = (size_t)s; + return true; // success +fail: + free(b); + close(fd); + return false; +} + +struct cmd_results *output_cmd_color_profile(int argc, char **argv) { + if (!config->handler_context.output_config) { + return cmd_results_new(CMD_FAILURE, "Missing output config"); + } + + enum color_profile new_mode = COLOR_PROFILE_TRANSFORM; + if (argc >= 2 && strcmp(*argv, "--device-primaries") == 0) { + new_mode = COLOR_PROFILE_TRANSFORM_WITH_DEVICE_PRIMARIES; + argc--; + argv++; + } + + if (!argc) { + return cmd_results_new(CMD_INVALID, "Missing color_profile first argument."); + } + + if (strcmp(*argv, "gamma22") == 0) { + wlr_color_transform_unref(config->handler_context.output_config->color_transform); + config->handler_context.output_config->color_transform = NULL; + config->handler_context.output_config->color_profile = new_mode; + + config->handler_context.leftovers.argc = argc - 1; + config->handler_context.leftovers.argv = argv + 1; + } else if (strcmp(*argv, "srgb") == 0) { + wlr_color_transform_unref(config->handler_context.output_config->color_transform); + config->handler_context.output_config->color_transform = + wlr_color_transform_init_linear_to_inverse_eotf(WLR_COLOR_TRANSFER_FUNCTION_SRGB); + config->handler_context.output_config->color_profile = new_mode; + + config->handler_context.leftovers.argc = argc - 1; + config->handler_context.leftovers.argv = argv + 1; + } else if (strcmp(*argv, "icc") == 0) { + if (argc < 2) { + return cmd_results_new(CMD_INVALID, + "Invalid color profile specification: icc type requires a file"); + } + if (new_mode != COLOR_PROFILE_TRANSFORM) { + return cmd_results_new(CMD_INVALID, + "Invalid color profile specification: --device-primaries cannot be used with icc"); + } + + char *icc_path = strdup(argv[1]); + if (!expand_path(&icc_path)) { + struct cmd_results *cmd_res = cmd_results_new(CMD_INVALID, + "Invalid color profile specification: invalid file path"); + free(icc_path); + return cmd_res; + } + + void *data = NULL; + size_t size = 0; + if (!read_file_into_buf(icc_path, &data, &size)) { + free(icc_path); + return cmd_results_new(CMD_FAILURE, + "Failed to load color profile: could not read ICC file"); + } + free(icc_path); + + struct wlr_color_transform *tmp = + wlr_color_transform_init_linear_to_icc(data, size); + if (!tmp) { + free(data); + return cmd_results_new(CMD_FAILURE, + "Failed to load color profile: failed to initialize transform from ICC"); + } + free(data); + + wlr_color_transform_unref(config->handler_context.output_config->color_transform); + config->handler_context.output_config->color_transform = tmp; + config->handler_context.output_config->color_profile = COLOR_PROFILE_TRANSFORM; + + config->handler_context.leftovers.argc = argc - 2; + config->handler_context.leftovers.argv = argv + 2; + } else { + return cmd_results_new(CMD_INVALID, + "Invalid color profile specification: " + "first argument should be gamma22|icc|srgb"); + } + + return NULL; +} diff --git a/sway/commands/output/disable.c b/sway/commands/output/disable.c new file mode 100644 index 0000000..624f405 --- /dev/null +++ b/sway/commands/output/disable.c @@ -0,0 +1,13 @@ +#include "sway/commands.h" +#include "sway/config.h" + +struct cmd_results *output_cmd_disable(int argc, char **argv) { + if (!config->handler_context.output_config) { + return cmd_results_new(CMD_FAILURE, "Missing output config"); + } + config->handler_context.output_config->enabled = 0; + + config->handler_context.leftovers.argc = argc; + config->handler_context.leftovers.argv = argv; + return NULL; +} diff --git a/sway/commands/output/dpms.c b/sway/commands/output/dpms.c new file mode 100644 index 0000000..c7adbd5 --- /dev/null +++ b/sway/commands/output/dpms.c @@ -0,0 +1,8 @@ +#include "log.h" +#include "sway/commands.h" + +struct cmd_results *output_cmd_dpms(int argc, char **argv) { + sway_log(SWAY_INFO, "The \"output dpms\" command is deprecated, " + "use \"output power\" instead"); + return output_cmd_power(argc, argv); +} diff --git a/sway/commands/output/enable.c b/sway/commands/output/enable.c new file mode 100644 index 0000000..bae3961 --- /dev/null +++ b/sway/commands/output/enable.c @@ -0,0 +1,15 @@ +#include "sway/commands.h" +#include "sway/config.h" + +struct cmd_results *output_cmd_enable(int argc, char **argv) { + if (!config->handler_context.output_config) { + return cmd_results_new(CMD_FAILURE, "Missing output config"); + } + + config->handler_context.output_config->enabled = 1; + + config->handler_context.leftovers.argc = argc; + config->handler_context.leftovers.argv = argv; + return NULL; +} + diff --git a/sway/commands/output/hdr.c b/sway/commands/output/hdr.c new file mode 100644 index 0000000..26d2022 --- /dev/null +++ b/sway/commands/output/hdr.c @@ -0,0 +1,37 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/output.h" +#include "util.h" + +struct cmd_results *output_cmd_hdr(int argc, char **argv) { + if (!config->handler_context.output_config) { + return cmd_results_new(CMD_FAILURE, "Missing output config"); + } + if (argc == 0) { + return cmd_results_new(CMD_INVALID, "Missing hdr argument"); + } + + bool current = false; + if (strcasecmp(argv[0], "toggle") == 0) { + const char *oc_name = config->handler_context.output_config->name; + if (strcmp(oc_name, "*") == 0) { + return cmd_results_new(CMD_INVALID, + "Cannot apply toggle to all outputs"); + } + + struct sway_output *output = all_output_by_name_or_id(oc_name); + if (!output) { + return cmd_results_new(CMD_FAILURE, + "Cannot apply toggle to unknown output %s", oc_name); + } + + current = output->hdr; + } + + config->handler_context.output_config->hdr = parse_boolean(argv[0], current); + + config->handler_context.leftovers.argc = argc - 1; + config->handler_context.leftovers.argv = argv + 1; + return NULL; +} diff --git a/sway/commands/output/max_render_time.c b/sway/commands/output/max_render_time.c new file mode 100644 index 0000000..2d3cebe --- /dev/null +++ b/sway/commands/output/max_render_time.c @@ -0,0 +1,28 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" + +struct cmd_results *output_cmd_max_render_time(int argc, char **argv) { + if (!config->handler_context.output_config) { + return cmd_results_new(CMD_FAILURE, "Missing output config"); + } + if (!argc) { + return cmd_results_new(CMD_INVALID, "Missing max render time argument."); + } + + int max_render_time; + if (!strcmp(*argv, "off")) { + max_render_time = 0; + } else { + char *end; + max_render_time = strtol(*argv, &end, 10); + if (*end || max_render_time <= 0) { + return cmd_results_new(CMD_INVALID, "Invalid max render time."); + } + } + config->handler_context.output_config->max_render_time = max_render_time; + + config->handler_context.leftovers.argc = argc - 1; + config->handler_context.leftovers.argv = argv + 1; + return NULL; +} diff --git a/sway/commands/output/mode.c b/sway/commands/output/mode.c new file mode 100644 index 0000000..019d625 --- /dev/null +++ b/sway/commands/output/mode.c @@ -0,0 +1,118 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" + +struct cmd_results *output_cmd_mode(int argc, char **argv) { + if (!config->handler_context.output_config) { + return cmd_results_new(CMD_FAILURE, "Missing output config"); + } + if (!argc) { + return cmd_results_new(CMD_INVALID, "Missing mode argument."); + } + + struct output_config *output = config->handler_context.output_config; + + if (strcmp(argv[0], "--custom") == 0) { + argv++; + argc--; + output->custom_mode = 1; + } else { + output->custom_mode = 0; + } + + // Reset custom modeline, if any + output->drm_mode.type = 0; + + char *end; + output->width = strtol(*argv, &end, 10); + if (*end) { + // Format is 1234x4321 + if (*end != 'x') { + return cmd_results_new(CMD_INVALID, "Invalid mode width."); + } + ++end; + output->height = strtol(end, &end, 10); + if (*end) { + if (*end != '@') { + return cmd_results_new(CMD_INVALID, "Invalid mode height."); + } + ++end; + output->refresh_rate = strtof(end, &end); + if (strcasecmp("Hz", end) != 0) { + return cmd_results_new(CMD_INVALID, + "Invalid mode refresh rate."); + } + } + } else { + // Format is 1234 4321 + argc--; argv++; + if (!argc) { + return cmd_results_new(CMD_INVALID, + "Missing mode argument (height)."); + } + output->height = strtol(*argv, &end, 10); + if (*end) { + return cmd_results_new(CMD_INVALID, "Invalid mode height."); + } + } + + config->handler_context.leftovers.argc = argc - 1; + config->handler_context.leftovers.argv = argv + 1; + return NULL; +} + +static bool parse_modeline(char **argv, drmModeModeInfo *mode) { + mode->type = DRM_MODE_TYPE_USERDEF; + mode->clock = strtof(argv[0], NULL) * 1000; + mode->hdisplay = strtol(argv[1], NULL, 10); + mode->hsync_start = strtol(argv[2], NULL, 10); + mode->hsync_end = strtol(argv[3], NULL, 10); + mode->htotal = strtol(argv[4], NULL, 10); + mode->vdisplay = strtol(argv[5], NULL, 10); + mode->vsync_start = strtol(argv[6], NULL, 10); + mode->vsync_end = strtol(argv[7], NULL, 10); + mode->vtotal = strtol(argv[8], NULL, 10); + + mode->vrefresh = mode->clock * 1000.0 * 1000.0 + / mode->htotal / mode->vtotal; + if (strcasecmp(argv[9], "+hsync") == 0) { + mode->flags |= DRM_MODE_FLAG_PHSYNC; + } else if (strcasecmp(argv[9], "-hsync") == 0) { + mode->flags |= DRM_MODE_FLAG_NHSYNC; + } else { + return false; + } + + if (strcasecmp(argv[10], "+vsync") == 0) { + mode->flags |= DRM_MODE_FLAG_PVSYNC; + } else if (strcasecmp(argv[10], "-vsync") == 0) { + mode->flags |= DRM_MODE_FLAG_NVSYNC; + } else { + return false; + } + + snprintf(mode->name, sizeof(mode->name), "%dx%d@%d", + mode->hdisplay, mode->vdisplay, mode->vrefresh / 1000); + + return true; +} + +struct cmd_results *output_cmd_modeline(int argc, char **argv) { + if (!config->handler_context.output_config) { + return cmd_results_new(CMD_FAILURE, "Missing output config"); + } + if (!argc) { + return cmd_results_new(CMD_INVALID, "Missing modeline argument."); + } + + struct output_config *output = config->handler_context.output_config; + + if (argc != 11 || !parse_modeline(argv, &output->drm_mode)) { + return cmd_results_new(CMD_INVALID, "Invalid modeline"); + } + + config->handler_context.leftovers.argc = argc - 12; + config->handler_context.leftovers.argv = argv + 12; + return NULL; +} + diff --git a/sway/commands/output/position.c b/sway/commands/output/position.c new file mode 100644 index 0000000..689462c --- /dev/null +++ b/sway/commands/output/position.c @@ -0,0 +1,41 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" + +struct cmd_results *output_cmd_position(int argc, char **argv) { + if (!config->handler_context.output_config) { + return cmd_results_new(CMD_FAILURE, "Missing output config"); + } + if (!argc) { + return cmd_results_new(CMD_INVALID, "Missing position argument."); + } + + char *end; + config->handler_context.output_config->x = strtol(*argv, &end, 10); + if (*end) { + // Format is 1234,4321 + if (*end != ',') { + return cmd_results_new(CMD_INVALID, "Invalid position x."); + } + ++end; + config->handler_context.output_config->y = strtol(end, &end, 10); + if (*end) { + return cmd_results_new(CMD_INVALID, "Invalid position y."); + } + } else { + // Format is 1234 4321 (legacy) + argc--; argv++; + if (!argc) { + return cmd_results_new(CMD_INVALID, "Missing position argument (y)."); + } + config->handler_context.output_config->y = strtol(*argv, &end, 10); + if (*end) { + return cmd_results_new(CMD_INVALID, "Invalid position y."); + } + } + + config->handler_context.leftovers.argc = argc - 1; + config->handler_context.leftovers.argv = argv + 1; + return NULL; +} + diff --git a/sway/commands/output/power.c b/sway/commands/output/power.c new file mode 100644 index 0000000..e6ae285 --- /dev/null +++ b/sway/commands/output/power.c @@ -0,0 +1,43 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/output.h" +#include "util.h" + +struct cmd_results *output_cmd_power(int argc, char **argv) { + if (!config->handler_context.output_config) { + return cmd_results_new(CMD_FAILURE, "Missing output config"); + } + if (argc == 0) { + return cmd_results_new(CMD_INVALID, "Missing power argument"); + } + + bool current = true; + if (strcasecmp(argv[0], "toggle") == 0) { + const char *oc_name = config->handler_context.output_config->name; + if (strcmp(oc_name, "*") == 0) { + return cmd_results_new(CMD_INVALID, + "Cannot apply toggle to all outputs"); + } + + struct sway_output *sway_output = all_output_by_name_or_id(oc_name); + if (!sway_output || !sway_output->wlr_output) { + return cmd_results_new(CMD_FAILURE, + "Cannot apply toggle to unknown output %s", oc_name); + } + + if (sway_output->enabled && !sway_output->wlr_output->enabled) { + current = false; + } + } + + if (parse_boolean(argv[0], current)) { + config->handler_context.output_config->power = 1; + } else { + config->handler_context.output_config->power = 0; + } + + config->handler_context.leftovers.argc = argc - 1; + config->handler_context.leftovers.argv = argv + 1; + return NULL; +} diff --git a/sway/commands/output/render_bit_depth.c b/sway/commands/output/render_bit_depth.c new file mode 100644 index 0000000..3fa30e0 --- /dev/null +++ b/sway/commands/output/render_bit_depth.c @@ -0,0 +1,32 @@ +#include +#include +#include "sway/commands.h" +#include "sway/config.h" + +struct cmd_results *output_cmd_render_bit_depth(int argc, char **argv) { + if (!config->handler_context.output_config) { + return cmd_results_new(CMD_FAILURE, "Missing output config"); + } + if (!argc) { + return cmd_results_new(CMD_INVALID, "Missing bit depth argument."); + } + + if (strcmp(*argv, "6") == 0) { + config->handler_context.output_config->render_bit_depth = + RENDER_BIT_DEPTH_6; + } else if (strcmp(*argv, "8") == 0) { + config->handler_context.output_config->render_bit_depth = + RENDER_BIT_DEPTH_8; + } else if (strcmp(*argv, "10") == 0) { + config->handler_context.output_config->render_bit_depth = + RENDER_BIT_DEPTH_10; + } else { + return cmd_results_new(CMD_INVALID, + "Invalid bit depth. Must be a value in (6|8|10)."); + } + + config->handler_context.leftovers.argc = argc - 1; + config->handler_context.leftovers.argv = argv + 1; + return NULL; +} + diff --git a/sway/commands/output/scale.c b/sway/commands/output/scale.c new file mode 100644 index 0000000..9398e06 --- /dev/null +++ b/sway/commands/output/scale.c @@ -0,0 +1,22 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" + +struct cmd_results *output_cmd_scale(int argc, char **argv) { + if (!config->handler_context.output_config) { + return cmd_results_new(CMD_FAILURE, "Missing output config"); + } + if (!argc) { + return cmd_results_new(CMD_INVALID, "Missing scale argument."); + } + + char *end; + config->handler_context.output_config->scale = strtof(*argv, &end); + if (*end) { + return cmd_results_new(CMD_INVALID, "Invalid scale."); + } + + config->handler_context.leftovers.argc = argc - 1; + config->handler_context.leftovers.argv = argv + 1; + return NULL; +} diff --git a/sway/commands/output/scale_filter.c b/sway/commands/output/scale_filter.c new file mode 100644 index 0000000..fa1e8e0 --- /dev/null +++ b/sway/commands/output/scale_filter.c @@ -0,0 +1,34 @@ +#include +#include "log.h" +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/output.h" + +struct cmd_results *output_cmd_scale_filter(int argc, char **argv) { + if (!config->handler_context.output_config) { + return cmd_results_new(CMD_FAILURE, "Missing output config"); + } + + if (!argc) { + return cmd_results_new(CMD_INVALID, "Missing scale_filter argument."); + } + + + enum scale_filter_mode scale_filter; + if (strcmp(*argv, "linear") == 0) { + scale_filter = SCALE_FILTER_LINEAR; + } else if (strcmp(*argv, "nearest") == 0) { + scale_filter = SCALE_FILTER_NEAREST; + } else if (strcmp(*argv, "smart") == 0) { + scale_filter = SCALE_FILTER_SMART; + } else { + return cmd_results_new(CMD_INVALID, "Invalid output scale_filter."); + } + + struct output_config *oc = config->handler_context.output_config; + config->handler_context.leftovers.argc = argc - 1; + config->handler_context.leftovers.argv = argv + 1; + + oc->scale_filter = scale_filter; + return NULL; +} diff --git a/sway/commands/output/subpixel.c b/sway/commands/output/subpixel.c new file mode 100644 index 0000000..63191ee --- /dev/null +++ b/sway/commands/output/subpixel.c @@ -0,0 +1,36 @@ +#include +#include "log.h" +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/output.h" + +struct cmd_results *output_cmd_subpixel(int argc, char **argv) { + if (!config->handler_context.output_config) { + return cmd_results_new(CMD_FAILURE, "Missing output config"); + } + if (!argc) { + return cmd_results_new(CMD_INVALID, "Missing subpixel argument."); + } + enum wl_output_subpixel subpixel; + + if (strcmp(*argv, "rgb") == 0) { + subpixel = WL_OUTPUT_SUBPIXEL_HORIZONTAL_RGB; + } else if (strcmp(*argv, "bgr") == 0) { + subpixel = WL_OUTPUT_SUBPIXEL_HORIZONTAL_BGR; + } else if (strcmp(*argv, "vrgb") == 0) { + subpixel = WL_OUTPUT_SUBPIXEL_VERTICAL_RGB; + } else if (strcmp(*argv, "vbgr") == 0) { + subpixel = WL_OUTPUT_SUBPIXEL_VERTICAL_BGR; + } else if (strcmp(*argv, "none") == 0) { + subpixel = WL_OUTPUT_SUBPIXEL_NONE; + } else { + return cmd_results_new(CMD_INVALID, "Invalid output subpixel."); + } + + struct output_config *oc = config->handler_context.output_config; + config->handler_context.leftovers.argc = argc - 1; + config->handler_context.leftovers.argv = argv + 1; + + oc->subpixel = subpixel; + return NULL; +} diff --git a/sway/commands/output/toggle.c b/sway/commands/output/toggle.c new file mode 100644 index 0000000..c6b7284 --- /dev/null +++ b/sway/commands/output/toggle.c @@ -0,0 +1,37 @@ +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/output.h" + +struct cmd_results *output_cmd_toggle(int argc, char **argv) { + if (!config->handler_context.output_config) { + return cmd_results_new(CMD_FAILURE, "Missing output config"); + } + + struct output_config *oc = config->handler_context.output_config; + + if (strcmp(oc->name, "*") == 0) { + return cmd_results_new(CMD_INVALID, + "Cannot apply toggle to all outputs."); + } + + struct sway_output *sway_output = all_output_by_name_or_id(oc->name); + + if (sway_output == NULL) { + return cmd_results_new(CMD_FAILURE, + "Cannot apply toggle to unknown output %s", oc->name); + } + + oc = find_output_config(sway_output); + + if (!oc || oc->enabled != 0) { + config->handler_context.output_config->enabled = 0; + } else { + config->handler_context.output_config->enabled = 1; + } + + free_output_config(oc); + config->handler_context.leftovers.argc = argc; + config->handler_context.leftovers.argv = argv; + return NULL; +} + diff --git a/sway/commands/output/transform.c b/sway/commands/output/transform.c new file mode 100644 index 0000000..8db71bb --- /dev/null +++ b/sway/commands/output/transform.c @@ -0,0 +1,90 @@ +#include +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "log.h" +#include "sway/output.h" + +static enum wl_output_transform invert_rotation_direction( + enum wl_output_transform t) { + switch (t) { + case WL_OUTPUT_TRANSFORM_90: + return WL_OUTPUT_TRANSFORM_270; + case WL_OUTPUT_TRANSFORM_270: + return WL_OUTPUT_TRANSFORM_90; + case WL_OUTPUT_TRANSFORM_FLIPPED_90: + return WL_OUTPUT_TRANSFORM_FLIPPED_270; + case WL_OUTPUT_TRANSFORM_FLIPPED_270: + return WL_OUTPUT_TRANSFORM_FLIPPED_90; + default: + return t; + } +} + +struct cmd_results *output_cmd_transform(int argc, char **argv) { + if (!config->handler_context.output_config) { + return cmd_results_new(CMD_FAILURE, "Missing output config"); + } + if (!argc) { + return cmd_results_new(CMD_INVALID, "Missing transform argument."); + } + + enum wl_output_transform transform; + if (strcmp(*argv, "normal") == 0 || + strcmp(*argv, "0") == 0) { + transform = WL_OUTPUT_TRANSFORM_NORMAL; + } else if (strcmp(*argv, "90") == 0) { + transform = WL_OUTPUT_TRANSFORM_90; + } else if (strcmp(*argv, "180") == 0) { + transform = WL_OUTPUT_TRANSFORM_180; + } else if (strcmp(*argv, "270") == 0) { + transform = WL_OUTPUT_TRANSFORM_270; + } else if (strcmp(*argv, "flipped") == 0) { + transform = WL_OUTPUT_TRANSFORM_FLIPPED; + } else if (strcmp(*argv, "flipped-90") == 0) { + transform = WL_OUTPUT_TRANSFORM_FLIPPED_90; + } else if (strcmp(*argv, "flipped-180") == 0) { + transform = WL_OUTPUT_TRANSFORM_FLIPPED_180; + } else if (strcmp(*argv, "flipped-270") == 0) { + transform = WL_OUTPUT_TRANSFORM_FLIPPED_270; + } else { + return cmd_results_new(CMD_INVALID, "Invalid output transform."); + } + + // Sway uses clockwise transforms, while WL_OUTPUT_TRANSFORM_* describe + // anti-clockwise transforms + transform = invert_rotation_direction(transform); + + struct output_config *output = config->handler_context.output_config; + config->handler_context.leftovers.argc = argc - 1; + config->handler_context.leftovers.argv = argv + 1; + if (argc > 1 && + (strcmp(argv[1], "clockwise") == 0 || strcmp(argv[1], "anticlockwise") == 0)) { + if (config->reloading) { + return cmd_results_new(CMD_INVALID, + "Relative transforms cannot be used in the configuration file"); + } + if (!sway_assert(output->name != NULL, "Output config name not set")) { + return NULL; + } + if (strcmp(output->name, "*") == 0) { + return cmd_results_new(CMD_INVALID, + "Cannot apply relative transform to all outputs."); + } + struct sway_output *s_output = output_by_name_or_id(output->name); + if (s_output == NULL) { + return cmd_results_new(CMD_INVALID, + "Cannot apply relative transform to unknown output %s", output->name); + } + if (strcmp(argv[1], "anticlockwise") == 0) { + transform = invert_rotation_direction(transform); + } + struct wlr_output *w_output = s_output->wlr_output; + transform = wlr_output_transform_compose(w_output->transform, transform); + config->handler_context.leftovers.argv += 1; + config->handler_context.leftovers.argc -= 1; + } + + output->transform = transform; + return NULL; +} diff --git a/sway/commands/output/unplug.c b/sway/commands/output/unplug.c new file mode 100644 index 0000000..dfef626 --- /dev/null +++ b/sway/commands/output/unplug.c @@ -0,0 +1,54 @@ +#include +#include +#include +#include +#if WLR_HAS_X11_BACKEND +#include +#endif +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/output.h" + +static bool is_backend_allowed(struct wlr_backend *backend) { + if (wlr_backend_is_headless(backend)) { + return true; + } + if (wlr_backend_is_wl(backend)) { + return true; + } +#if WLR_HAS_X11_BACKEND + if (wlr_backend_is_x11(backend)) { + return true; + } +#endif + return false; +} + +/** + * This command is intended for developer use only. + */ +struct cmd_results *output_cmd_unplug(int argc, char **argv) { + if (!config->handler_context.output_config) { + return cmd_results_new(CMD_FAILURE, "Missing output config"); + } + + const char *oc_name = config->handler_context.output_config->name; + if (strcmp(oc_name, "*") == 0) { + return cmd_results_new(CMD_INVALID, "Won't unplug all outputs"); + } + + struct sway_output *sway_output = all_output_by_name_or_id(oc_name); + if (!sway_output) { + return cmd_results_new(CMD_INVALID, + "Cannot unplug unknown output %s", oc_name); + } + + if (!is_backend_allowed(sway_output->wlr_output->backend)) { + return cmd_results_new(CMD_INVALID, + "Can only unplug outputs with headless, wayland or x11 backend"); + } + + wlr_output_destroy(sway_output->wlr_output); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/popup_during_fullscreen.c b/sway/commands/popup_during_fullscreen.c new file mode 100644 index 0000000..e815626 --- /dev/null +++ b/sway/commands/popup_during_fullscreen.c @@ -0,0 +1,24 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" + +struct cmd_results *cmd_popup_during_fullscreen(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "popup_during_fullscreen", + EXPECTED_EQUAL_TO, 1))) { + return error; + } + + if (strcasecmp(argv[0], "smart") == 0) { + config->popup_during_fullscreen = POPUP_SMART; + } else if (strcasecmp(argv[0], "ignore") == 0) { + config->popup_during_fullscreen = POPUP_IGNORE; + } else if (strcasecmp(argv[0], "leave_fullscreen") == 0) { + config->popup_during_fullscreen = POPUP_LEAVE; + } else { + return cmd_results_new(CMD_INVALID, "Expected " + "'popup_during_fullscreen smart|ignore|leave_fullscreen'"); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/primary_selection.c b/sway/commands/primary_selection.c new file mode 100644 index 0000000..9e2689c --- /dev/null +++ b/sway/commands/primary_selection.c @@ -0,0 +1,25 @@ +#include +#include +#include "sway/config.h" +#include "sway/commands.h" +#include "util.h" + +struct cmd_results *cmd_primary_selection(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "primary_selection", EXPECTED_EQUAL_TO, 1))) { + return error; + } + + bool primary_selection = parse_boolean(argv[0], true); + + // config->primary_selection is reset to the previous value on reload in + // load_main_config() + if (config->reloading && config->primary_selection != primary_selection) { + return cmd_results_new(CMD_FAILURE, + "primary_selection can only be enabled/disabled at launch"); + } + + config->primary_selection = primary_selection; + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/reload.c b/sway/commands/reload.c new file mode 100644 index 0000000..6c0aac2 --- /dev/null +++ b/sway/commands/reload.c @@ -0,0 +1,74 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/ipc-server.h" +#include "sway/server.h" +#include "sway/tree/arrange.h" +#include "sway/tree/view.h" +#include "list.h" +#include "log.h" + +static void title_bar_update_iterator(struct sway_container *con, void *data) { + container_update_title_bar(con); +} + +static void do_reload(void *data) { + // store bar ids to check against new bars for barconfig_update events + list_t *bar_ids = create_list(); + for (int i = 0; i < config->bars->length; ++i) { + struct bar_config *bar = config->bars->items[i]; + list_add(bar_ids, strdup(bar->id)); + } + + const char *path = NULL; + if (config->user_config_path) { + path = config->current_config_path; + } + + if (!load_main_config(path, true, false)) { + sway_log(SWAY_ERROR, "Error(s) reloading config"); + list_free_items_and_destroy(bar_ids); + return; + } + + ipc_event_workspace(NULL, NULL, "reload"); + + load_swaybars(); + + for (int i = 0; i < config->bars->length; ++i) { + struct bar_config *bar = config->bars->items[i]; + for (int j = 0; j < bar_ids->length; ++j) { + if (strcmp(bar->id, bar_ids->items[j]) == 0) { + ipc_event_barconfig_update(bar); + break; + } + } + } + list_free_items_and_destroy(bar_ids); + + root_for_each_container(title_bar_update_iterator, NULL); + + arrange_root(); +} + +struct cmd_results *cmd_reload(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "reload", EXPECTED_EQUAL_TO, 0))) { + return error; + } + + const char *path = NULL; + if (config->user_config_path) { + path = config->current_config_path; + } + + if (!load_main_config(path, true, true)) { + return cmd_results_new(CMD_FAILURE, "Error(s) reloading config."); + } + + // The reload command frees a lot of stuff, so to avoid use-after-frees + // we schedule the reload to happen using an idle event. + wl_event_loop_add_idle(server.wl_event_loop, do_reload, NULL); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/rename.c b/sway/commands/rename.c new file mode 100644 index 0000000..63fac05 --- /dev/null +++ b/sway/commands/rename.c @@ -0,0 +1,105 @@ +#include +#include +#include +#include +#include "log.h" +#include "stringop.h" +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/ipc-server.h" +#include "sway/output.h" +#include "sway/desktop/launcher.h" +#include "sway/tree/container.h" +#include "sway/tree/workspace.h" +#include "sway/tree/root.h" + +static const char expected_syntax[] = + "Expected 'rename workspace to ' or " + "'rename workspace to '"; + +struct cmd_results *cmd_rename(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "rename", EXPECTED_AT_LEAST, 3))) { + return error; + } + if (!root->outputs->length) { + return cmd_results_new(CMD_INVALID, + "Can't run this command while there's no outputs connected."); + } + if (strcasecmp(argv[0], "workspace") != 0) { + return cmd_results_new(CMD_INVALID, "%s", expected_syntax); + } + + int argn = 1; + struct sway_workspace *workspace = NULL; + + if (strcasecmp(argv[1], "to") == 0) { + // 'rename workspace to new_name' + workspace = config->handler_context.workspace; + } else if (strcasecmp(argv[1], "number") == 0) { + // 'rename workspace number x to new_name' + if (!isdigit(argv[2][0])) { + return cmd_results_new(CMD_INVALID, + "Invalid workspace number '%s'", argv[2]); + } + workspace = workspace_by_number(argv[2]); + while (argn < argc && strcasecmp(argv[argn], "to") != 0) { + ++argn; + } + } else { + // 'rename workspace old_name to new_name' + int end = argn; + while (end < argc && strcasecmp(argv[end], "to") != 0) { + ++end; + } + char *old_name = join_args(argv + argn, end - argn); + workspace = workspace_by_name(old_name); + free(old_name); + argn = end; + } + + if (!workspace) { + return cmd_results_new(CMD_INVALID, + "There is no workspace with that name"); + } + + ++argn; // move past "to" + + if (argn >= argc) { + return cmd_results_new(CMD_INVALID, "%s", expected_syntax); + } + + char *new_name = join_args(argv + argn, argc - argn); + if (strcasecmp(new_name, "next") == 0 || + strcasecmp(new_name, "prev") == 0 || + strcasecmp(new_name, "next_on_output") == 0 || + strcasecmp(new_name, "prev_on_output") == 0 || + strcasecmp(new_name, "back_and_forth") == 0 || + strcasecmp(new_name, "current") == 0 || + strcasecmp(new_name, "number") == 0) { + free(new_name); + return cmd_results_new(CMD_INVALID, + "Cannot use special workspace name '%s'", argv[argn]); + } + struct sway_workspace *tmp_workspace = workspace_by_name(new_name); + if (tmp_workspace) { + free(new_name); + if (tmp_workspace == workspace) { + return cmd_results_new(CMD_SUCCESS, NULL); + } else { + return cmd_results_new(CMD_INVALID, "Workspace already exists"); + } + } + + sway_log(SWAY_DEBUG, "renaming workspace '%s' to '%s'", workspace->name, new_name); + + free(workspace->name); + workspace->name = new_name; + + wlr_ext_workspace_handle_v1_set_name(workspace->ext_workspace, workspace->name); + + output_sort_workspaces(workspace->output); + ipc_event_workspace(NULL, workspace, "rename"); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/resize.c b/sway/commands/resize.c new file mode 100644 index 0000000..3530ee5 --- /dev/null +++ b/sway/commands/resize.c @@ -0,0 +1,583 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "sway/commands.h" +#include "sway/tree/arrange.h" +#include "sway/tree/view.h" +#include "sway/tree/workspace.h" +#include "log.h" +#include "util.h" + +#define AXIS_HORIZONTAL (WLR_EDGE_LEFT | WLR_EDGE_RIGHT) +#define AXIS_VERTICAL (WLR_EDGE_TOP | WLR_EDGE_BOTTOM) + +static uint32_t parse_resize_axis(const char *axis) { + if (strcasecmp(axis, "width") == 0 || strcasecmp(axis, "horizontal") == 0) { + return AXIS_HORIZONTAL; + } + if (strcasecmp(axis, "height") == 0 || strcasecmp(axis, "vertical") == 0) { + return AXIS_VERTICAL; + } + if (strcasecmp(axis, "up") == 0) { + return WLR_EDGE_TOP; + } + if (strcasecmp(axis, "down") == 0) { + return WLR_EDGE_BOTTOM; + } + if (strcasecmp(axis, "left") == 0) { + return WLR_EDGE_LEFT; + } + if (strcasecmp(axis, "right") == 0) { + return WLR_EDGE_RIGHT; + } + return WLR_EDGE_NONE; +} + +static bool is_horizontal(uint32_t axis) { + return axis & (WLR_EDGE_LEFT | WLR_EDGE_RIGHT); +} + +struct sway_container *container_find_resize_parent(struct sway_container *con, + uint32_t axis) { + enum sway_container_layout parallel_layout = + is_horizontal(axis) ? L_HORIZ : L_VERT; + bool allow_first = axis != WLR_EDGE_TOP && axis != WLR_EDGE_LEFT; + bool allow_last = axis != WLR_EDGE_RIGHT && axis != WLR_EDGE_BOTTOM; + + while (con) { + list_t *siblings = container_get_siblings(con); + int index = container_sibling_index(con); + if (container_parent_layout(con) == parallel_layout && + siblings->length > 1 && (allow_first || index > 0) && + (allow_last || index < siblings->length - 1)) { + return con; + } + con = con->pending.parent; + } + + return NULL; +} + +void container_resize_tiled(struct sway_container *con, + uint32_t axis, int amount) { + if (!con) { + return; + } + + con = container_find_resize_parent(con, axis); + if (!con) { + // Can't resize in this direction + return; + } + + if (container_is_scratchpad_hidden_or_child(con)) { + return; + } + + // For HORIZONTAL or VERTICAL, we are growing in two directions so select + // all adjacent siblings. For RIGHT or DOWN or LEFT or UP select just the + // previous or next sibling. + list_t *resize = create_list(); + list_t *siblings = container_get_siblings(con); + int index = container_sibling_index(con); + + if (axis == AXIS_HORIZONTAL || axis == AXIS_VERTICAL) { + list_cat(resize, siblings); + } else if (axis == WLR_EDGE_TOP || axis == WLR_EDGE_LEFT) { + if (!sway_assert(index > 0, "Didn't expect first child")) { + goto cleanup; + } + list_add(resize, siblings->items[index - 1]); + list_add(resize, con); + } else { + if (!sway_assert(index < siblings->length - 1, + "Didn't expect last child")) { + goto cleanup; + } + list_add(resize, con); + list_add(resize, siblings->items[index + 1]); + } + + // Apply new dimensions + int sibling_amount = ceil((double)amount / (double)(resize->length - 1)); + + if (is_horizontal(axis)) { + for (int i = 0; i < resize->length; i++) { + struct sway_container *sibling = resize->items[i]; + double change = sibling == con ? amount : -sibling_amount; + if (sibling->pending.width + change < MIN_SANE_W) { + goto cleanup; + } + } + if (con->child_total_width <= 0) { + goto cleanup; + } + + // We're going to resize so snap all the width fractions to full pixels + // to avoid rounding issues + for (int i = 0; i < siblings->length; ++i) { + struct sway_container *con = siblings->items[i]; + con->width_fraction = con->pending.width / con->child_total_width; + } + + double amount_fraction = (double)amount / con->child_total_width; + double sibling_amount_fraction = + amount_fraction / (double)(resize->length - 1); + + for (int i = 0; i < resize->length; i++) { + struct sway_container *sibling = resize->items[i]; + sibling->width_fraction += + sibling == con ? amount_fraction : -sibling_amount_fraction; + } + } else { + for (int i = 0; i < resize->length; i++) { + struct sway_container *sibling = resize->items[i]; + double change = sibling == con ? amount : -sibling_amount; + if (sibling->pending.height + change < MIN_SANE_H) { + goto cleanup; + } + } + if (con->child_total_height <= 0) { + goto cleanup; + } + + // We're going to resize so snap all the height fractions to full pixels + // to avoid rounding issues + for (int i = 0; i < siblings->length; ++i) { + struct sway_container *con = siblings->items[i]; + con->height_fraction = con->pending.height / con->child_total_height; + } + + double amount_fraction = (double)amount / con->child_total_height; + double sibling_amount_fraction = + amount_fraction / (double)(resize->length - 1); + + for (int i = 0; i < resize->length; i++) { + struct sway_container *sibling = resize->items[i]; + sibling->height_fraction += + sibling == con ? amount_fraction : -sibling_amount_fraction; + } + } + + if (con->pending.parent) { + arrange_container(con->pending.parent); + } else { + arrange_workspace(con->pending.workspace); + } + +cleanup: + list_free(resize); +} + +/** + * Implement `resize ` for a floating container. + */ +static struct cmd_results *resize_adjust_floating(uint32_t axis, + struct movement_amount *amount) { + struct sway_container *con = config->handler_context.container; + int grow_width = 0, grow_height = 0; + + if (is_horizontal(axis)) { + grow_width = amount->amount; + } else { + grow_height = amount->amount; + } + + // Make sure we're not adjusting beyond floating min/max size + int min_width, max_width, min_height, max_height; + floating_calculate_constraints(&min_width, &max_width, + &min_height, &max_height); + if (con->pending.width + grow_width < min_width) { + grow_width = min_width - con->pending.width; + } else if (con->pending.width + grow_width > max_width) { + grow_width = max_width - con->pending.width; + } + if (con->pending.height + grow_height < min_height) { + grow_height = min_height - con->pending.height; + } else if (con->pending.height + grow_height > max_height) { + grow_height = max_height - con->pending.height; + } + int grow_x = 0, grow_y = 0; + + if (axis == AXIS_HORIZONTAL) { + grow_x = -grow_width / 2; + } else if (axis == AXIS_VERTICAL) { + grow_y = -grow_height / 2; + } else if (axis == WLR_EDGE_TOP) { + grow_y = -grow_height; + } else if (axis == WLR_EDGE_LEFT) { + grow_x = -grow_width; + } + if (grow_width == 0 && grow_height == 0) { + return cmd_results_new(CMD_INVALID, "Cannot resize any further"); + } + con->pending.x += grow_x; + con->pending.y += grow_y; + con->pending.width += grow_width; + con->pending.height += grow_height; + + con->pending.content_x += grow_x; + con->pending.content_y += grow_y; + con->pending.content_width += grow_width; + con->pending.content_height += grow_height; + + arrange_container(con); + + return cmd_results_new(CMD_SUCCESS, NULL); +} + +/** + * Implement `resize ` for a tiled container. + */ +static struct cmd_results *resize_adjust_tiled(uint32_t axis, + struct movement_amount *amount) { + struct sway_container *current = config->handler_context.container; + + if (container_is_scratchpad_hidden_or_child(current)) { + return cmd_results_new(CMD_FAILURE, "Cannot resize a hidden scratchpad container"); + } + + if (amount->unit == MOVEMENT_UNIT_DEFAULT) { + amount->unit = MOVEMENT_UNIT_PPT; + } + if (amount->unit == MOVEMENT_UNIT_PPT) { + struct sway_container *parent = current->pending.parent; + float pct = amount->amount / 100.0f; + + if (is_horizontal(axis)) { + while (parent && parent->pending.layout != L_HORIZ) { + parent = parent->pending.parent; + } + if (parent) { + amount->amount = (float)parent->pending.width * pct; + } else { + amount->amount = (float)current->pending.workspace->width * pct; + } + } else { + while (parent && parent->pending.layout != L_VERT) { + parent = parent->pending.parent; + } + if (parent) { + amount->amount = (float)parent->pending.height * pct; + } else { + amount->amount = (float)current->pending.workspace->height * pct; + } + } + } + + double old_width = current->width_fraction; + double old_height = current->height_fraction; + container_resize_tiled(current, axis, amount->amount); + if (current->width_fraction == old_width && + current->height_fraction == old_height) { + return cmd_results_new(CMD_INVALID, "Cannot resize any further"); + } + return cmd_results_new(CMD_SUCCESS, NULL); +} + +/** + * Implement `resize set` for a tiled container. + */ +static struct cmd_results *resize_set_tiled(struct sway_container *con, + struct movement_amount *width, struct movement_amount *height) { + + if (container_is_scratchpad_hidden_or_child(con)) { + return cmd_results_new(CMD_FAILURE, "Cannot resize a hidden scratchpad container"); + } + + if (width->amount) { + if (width->unit == MOVEMENT_UNIT_PPT || + width->unit == MOVEMENT_UNIT_DEFAULT) { + // Convert to px + struct sway_container *parent = con->pending.parent; + while (parent && parent->pending.layout != L_HORIZ) { + parent = parent->pending.parent; + } + if (parent) { + width->amount = parent->pending.width * width->amount / 100; + } else { + width->amount = con->pending.workspace->width * width->amount / 100; + } + width->unit = MOVEMENT_UNIT_PX; + } + if (width->unit == MOVEMENT_UNIT_PX) { + container_resize_tiled(con, AXIS_HORIZONTAL, + width->amount - con->pending.width); + } + } + + if (height->amount) { + if (height->unit == MOVEMENT_UNIT_PPT || + height->unit == MOVEMENT_UNIT_DEFAULT) { + // Convert to px + struct sway_container *parent = con->pending.parent; + while (parent && parent->pending.layout != L_VERT) { + parent = parent->pending.parent; + } + if (parent) { + height->amount = parent->pending.height * height->amount / 100; + } else { + height->amount = con->pending.workspace->height * height->amount / 100; + } + height->unit = MOVEMENT_UNIT_PX; + } + if (height->unit == MOVEMENT_UNIT_PX) { + container_resize_tiled(con, AXIS_VERTICAL, + height->amount - con->pending.height); + } + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} + +/** + * Implement `resize set` for a floating container. + */ +static struct cmd_results *resize_set_floating(struct sway_container *con, + struct movement_amount *width, struct movement_amount *height) { + int min_width, max_width, min_height, max_height, grow_width = 0, grow_height = 0; + floating_calculate_constraints(&min_width, &max_width, + &min_height, &max_height); + + if (width->amount) { + switch (width->unit) { + case MOVEMENT_UNIT_PPT: + if (container_is_scratchpad_hidden(con)) { + return cmd_results_new(CMD_FAILURE, + "Cannot resize a hidden scratchpad container by ppt"); + } + // Convert to px + width->amount = con->pending.workspace->width * width->amount / 100; + width->unit = MOVEMENT_UNIT_PX; + // Falls through + case MOVEMENT_UNIT_PX: + case MOVEMENT_UNIT_DEFAULT: + width->amount = fmax(min_width, fmin(width->amount, max_width)); + grow_width = width->amount - con->pending.width; + con->pending.x -= grow_width / 2; + con->pending.width = width->amount; + break; + case MOVEMENT_UNIT_INVALID: + sway_assert(false, "invalid width unit"); + break; + } + } + + if (height->amount) { + switch (height->unit) { + case MOVEMENT_UNIT_PPT: + if (container_is_scratchpad_hidden(con)) { + return cmd_results_new(CMD_FAILURE, + "Cannot resize a hidden scratchpad container by ppt"); + } + // Convert to px + height->amount = con->pending.workspace->height * height->amount / 100; + height->unit = MOVEMENT_UNIT_PX; + // Falls through + case MOVEMENT_UNIT_PX: + case MOVEMENT_UNIT_DEFAULT: + height->amount = fmax(min_height, fmin(height->amount, max_height)); + grow_height = height->amount - con->pending.height; + con->pending.y -= grow_height / 2; + con->pending.height = height->amount; + break; + case MOVEMENT_UNIT_INVALID: + sway_assert(false, "invalid height unit"); + break; + } + } + + con->pending.content_x -= grow_width / 2; + con->pending.content_y -= grow_height / 2; + con->pending.content_width += grow_width; + con->pending.content_height += grow_height; + + arrange_container(con); + + return cmd_results_new(CMD_SUCCESS, NULL); +} + +/** + * resize set + * + * args: [width] [px|ppt] + * : height [px|ppt] + * : [width] [px|ppt] [height] [px|ppt] + */ +static struct cmd_results *cmd_resize_set(int argc, char **argv) { + struct cmd_results *error; + if ((error = checkarg(argc, "resize", EXPECTED_AT_LEAST, 1))) { + return error; + } + const char usage[] = "Expected 'resize set [width] [px|ppt]' or " + "'resize set height [px|ppt]' or " + "'resize set [width] [px|ppt] [height] [px|ppt]'"; + + // Width + struct movement_amount width = {0}; + if (argc >= 2 && !strcmp(argv[0], "width") && strcmp(argv[1], "height")) { + argc--; argv++; + } + if (strcmp(argv[0], "height")) { + int num_consumed_args = parse_movement_amount(argc, argv, &width); + argc -= num_consumed_args; + argv += num_consumed_args; + if (width.unit == MOVEMENT_UNIT_INVALID) { + return cmd_results_new(CMD_INVALID, "%s", usage); + } + } + + // Height + struct movement_amount height = {0}; + if (argc) { + if (argc >= 2 && !strcmp(argv[0], "height")) { + argc--; argv++; + } + int num_consumed_args = parse_movement_amount(argc, argv, &height); + if (argc > num_consumed_args) { + return cmd_results_new(CMD_INVALID, "%s", usage); + } + if (height.unit == MOVEMENT_UNIT_INVALID) { + return cmd_results_new(CMD_INVALID, "%s", usage); + } + } + + // If 0, don't resize that dimension + struct sway_container *con = config->handler_context.container; + if (width.amount <= 0) { + width.amount = con->pending.width; + } + if (height.amount <= 0) { + height.amount = con->pending.height; + } + + if (container_is_floating(con)) { + return resize_set_floating(con, &width, &height); + } + return resize_set_tiled(con, &width, &height); +} + +/** + * resize + * + * args: + * args: + * args: or + */ +static struct cmd_results *cmd_resize_adjust(int argc, char **argv, + int multiplier) { + const char usage[] = "Expected 'resize grow|shrink " + "[ px|ppt [or px|ppt]]'"; + uint32_t axis = parse_resize_axis(*argv); + if (axis == WLR_EDGE_NONE) { + return cmd_results_new(CMD_INVALID, "%s", usage); + } + --argc; ++argv; + + // First amount + struct movement_amount first_amount; + if (argc) { + int num_consumed_args = parse_movement_amount(argc, argv, &first_amount); + argc -= num_consumed_args; + argv += num_consumed_args; + if (first_amount.unit == MOVEMENT_UNIT_INVALID) { + return cmd_results_new(CMD_INVALID, "%s", usage); + } + } else { + first_amount.amount = 10; + first_amount.unit = MOVEMENT_UNIT_DEFAULT; + } + + // "or" + if (argc) { + if (strcmp(*argv, "or") != 0) { + return cmd_results_new(CMD_INVALID, "%s", usage); + } + --argc; ++argv; + } + + // Second amount + struct movement_amount second_amount; + if (argc) { + int num_consumed_args = parse_movement_amount(argc, argv, &second_amount); + if (argc > num_consumed_args) { + return cmd_results_new(CMD_INVALID, "%s", usage); + } + if (second_amount.unit == MOVEMENT_UNIT_INVALID) { + return cmd_results_new(CMD_INVALID, "%s", usage); + } + } else { + second_amount.amount = 0; + second_amount.unit = MOVEMENT_UNIT_INVALID; + } + + first_amount.amount *= multiplier; + second_amount.amount *= multiplier; + + struct sway_container *con = config->handler_context.container; + if (container_is_floating(con)) { + // Floating containers can only resize in px. Choose an amount which + // uses px, with fallback to an amount that specified no unit. + if (first_amount.unit == MOVEMENT_UNIT_PX) { + return resize_adjust_floating(axis, &first_amount); + } else if (second_amount.unit == MOVEMENT_UNIT_PX) { + return resize_adjust_floating(axis, &second_amount); + } else if (first_amount.unit == MOVEMENT_UNIT_DEFAULT) { + return resize_adjust_floating(axis, &first_amount); + } else if (second_amount.unit == MOVEMENT_UNIT_DEFAULT) { + return resize_adjust_floating(axis, &second_amount); + } else { + return cmd_results_new(CMD_INVALID, + "Floating containers cannot use ppt measurements"); + } + } + + // For tiling, prefer ppt -> default -> px + if (first_amount.unit == MOVEMENT_UNIT_PPT) { + return resize_adjust_tiled(axis, &first_amount); + } else if (second_amount.unit == MOVEMENT_UNIT_PPT) { + return resize_adjust_tiled(axis, &second_amount); + } else if (first_amount.unit == MOVEMENT_UNIT_DEFAULT) { + return resize_adjust_tiled(axis, &first_amount); + } else if (second_amount.unit == MOVEMENT_UNIT_DEFAULT) { + return resize_adjust_tiled(axis, &second_amount); + } else { + return resize_adjust_tiled(axis, &first_amount); + } +} + +struct cmd_results *cmd_resize(int argc, char **argv) { + if (!root->outputs->length) { + return cmd_results_new(CMD_INVALID, + "Can't run this command while there's no outputs connected."); + } + struct sway_container *current = config->handler_context.container; + if (!current) { + return cmd_results_new(CMD_INVALID, "Cannot resize nothing"); + } + + struct cmd_results *error; + if ((error = checkarg(argc, "resize", EXPECTED_AT_LEAST, 2))) { + return error; + } + + if (strcasecmp(argv[0], "set") == 0) { + return cmd_resize_set(argc - 1, &argv[1]); + } + if (strcasecmp(argv[0], "grow") == 0) { + return cmd_resize_adjust(argc - 1, &argv[1], 1); + } + if (strcasecmp(argv[0], "shrink") == 0) { + return cmd_resize_adjust(argc - 1, &argv[1], -1); + } + + const char usage[] = "Expected 'resize " + " [] [px|ppt]'"; + + return cmd_results_new(CMD_INVALID, "%s", usage); +} diff --git a/sway/commands/scratchpad.c b/sway/commands/scratchpad.c new file mode 100644 index 0000000..8a63740 --- /dev/null +++ b/sway/commands/scratchpad.c @@ -0,0 +1,132 @@ +#include "log.h" +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/input/input-manager.h" +#include "sway/input/seat.h" +#include "sway/ipc-server.h" +#include "sway/tree/container.h" +#include "sway/tree/root.h" +#include "sway/tree/workspace.h" + +static void scratchpad_toggle_auto(void) { + struct sway_seat *seat = input_manager_current_seat(); + struct sway_container *focus = seat_get_focused_container(seat); + struct sway_workspace *ws = seat_get_focused_workspace(seat); + if (!ws) { + sway_log(SWAY_DEBUG, + "No focused workspace to toggle scratchpad windows on"); + return; + } + + // If the focus is in a floating split container, + // operate on the split container instead of the child. + if (focus && container_is_floating_or_child(focus)) { + while (focus->pending.parent) { + focus = focus->pending.parent; + } + } + + // Check if the currently focused window is a scratchpad window and should + // be hidden again. + if (focus && focus->scratchpad) { + sway_log(SWAY_DEBUG, "Focus is a scratchpad window - hiding %s", + focus->title); + root_scratchpad_hide(focus); + return; + } + + // Check if there is an unfocused scratchpad window on the current workspace + // and focus it. + for (int i = 0; i < ws->floating->length; ++i) { + struct sway_container *floater = ws->floating->items[i]; + if (floater->scratchpad && focus != floater) { + sway_log(SWAY_DEBUG, + "Focusing other scratchpad window (%s) in this workspace", + floater->title); + root_scratchpad_show(floater); + return; + } + } + + // Check if there is a visible scratchpad window on another workspace. + // In this case we move it to the current workspace. + for (int i = 0; i < root->scratchpad->length; ++i) { + struct sway_container *con = root->scratchpad->items[i]; + if (con->pending.parent) { + sway_log(SWAY_DEBUG, + "Moving a visible scratchpad window (%s) to this workspace", + con->title); + root_scratchpad_show(con); + ipc_event_window(con, "move"); + return; + } + } + + // Take the container at the bottom of the scratchpad list + if (!sway_assert(root->scratchpad->length, "Scratchpad is empty")) { + return; + } + struct sway_container *con = root->scratchpad->items[0]; + sway_log(SWAY_DEBUG, "Showing %s from list", con->title); + root_scratchpad_show(con); + ipc_event_window(con, "move"); +} + +static void scratchpad_toggle_container(struct sway_container *con) { + if (!sway_assert(con->scratchpad, "Container isn't in the scratchpad")) { + return; + } + + struct sway_seat *seat = input_manager_current_seat(); + struct sway_workspace *ws = seat_get_focused_workspace(seat); + // Check if it matches a currently visible scratchpad window and hide it. + if (con->pending.workspace && ws == con->pending.workspace) { + root_scratchpad_hide(con); + return; + } + + root_scratchpad_show(con); + ipc_event_window(con, "move"); +} + +struct cmd_results *cmd_scratchpad(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "scratchpad", EXPECTED_EQUAL_TO, 1))) { + return error; + } + if (strcmp(argv[0], "show") != 0) { + return cmd_results_new(CMD_INVALID, "Expected 'scratchpad show'"); + } + if (!root->outputs->length) { + return cmd_results_new(CMD_INVALID, + "Can't run this command while there's no outputs connected."); + } + if (!root->scratchpad->length) { + return cmd_results_new(CMD_INVALID, "Scratchpad is empty"); + } + + if (config->handler_context.node_overridden) { + struct sway_container *con = config->handler_context.container; + + // If the container is in a floating split container, + // operate on the split container instead of the child. + if (con && container_is_floating_or_child(con)) { + while (con->pending.parent) { + con = con->pending.parent; + } + } + + // If using criteria, this command is executed for every container which + // matches the criteria. If this container isn't in the scratchpad, + // we'll return an error. The same is true if the + // overridden node is not a container. + if (!con || !con->scratchpad) { + return cmd_results_new(CMD_INVALID, "Container is not in scratchpad."); + } + scratchpad_toggle_container(con); + } else { + scratchpad_toggle_auto(); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/scratchpad_minimize.c b/sway/commands/scratchpad_minimize.c new file mode 100644 index 0000000..03c35b7 --- /dev/null +++ b/sway/commands/scratchpad_minimize.c @@ -0,0 +1,16 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "util.h" + +struct cmd_results *cmd_scratchpad_minimize(int argc, char **argv) { + struct cmd_results *error = checkarg(argc, "scratchpad_minimize", EXPECTED_AT_LEAST, 1); + + if (error) { + return error; + } + + config->scratchpad_minimize = parse_boolean(argv[0], config->scratchpad_minimize); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/seat.c b/sway/commands/seat.c new file mode 100644 index 0000000..2d197b6 --- /dev/null +++ b/sway/commands/seat.c @@ -0,0 +1,81 @@ +#include +#include +#include "sway/commands.h" +#include "sway/input/input-manager.h" +#include "sway/input/seat.h" +#include "log.h" +#include "stringop.h" + +// must be in order for the bsearch +// these handlers perform actions on the seat +static const struct cmd_handler seat_action_handlers[] = { + { "cursor", seat_cmd_cursor }, +}; + +// must be in order for the bsearch +// these handlers alter the seat config +static const struct cmd_handler seat_handlers[] = { + { "attach", seat_cmd_attach }, + { "fallback", seat_cmd_fallback }, + { "hide_cursor", seat_cmd_hide_cursor }, + { "idle_inhibit", seat_cmd_idle_inhibit }, + { "idle_wake", seat_cmd_idle_wake }, + { "keyboard_grouping", seat_cmd_keyboard_grouping }, + { "pointer_constraint", seat_cmd_pointer_constraint }, + { "shortcuts_inhibitor", seat_cmd_shortcuts_inhibitor }, + { "xcursor_theme", seat_cmd_xcursor_theme }, +}; + +static struct cmd_results *action_handlers(int argc, char **argv) { + struct cmd_results *res = config_subcommand(argv, argc, + seat_action_handlers, sizeof(seat_action_handlers)); + free_seat_config(config->handler_context.seat_config); + config->handler_context.seat_config = NULL; + return res; +} + +static struct cmd_results *config_handlers(int argc, char **argv) { + struct cmd_results *res = config_subcommand(argv, argc, + seat_handlers, sizeof(seat_handlers)); + if (res && res->status != CMD_SUCCESS) { + free_seat_config(config->handler_context.seat_config); + } else { + struct seat_config *sc = + store_seat_config(config->handler_context.seat_config); + if (!config->reading) { + input_manager_apply_seat_config(sc); + } + } + config->handler_context.seat_config = NULL; + return res; +} + +struct cmd_results *cmd_seat(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "seat", EXPECTED_AT_LEAST, 2))) { + return error; + } + + if (!strcmp(argv[0], "-")) { + if (config->reading) { + return cmd_results_new(CMD_FAILURE, + "Current seat alias (-) cannot be used in the config"); + } + config->handler_context.seat_config = + new_seat_config(config->handler_context.seat->wlr_seat->name); + } else { + config->handler_context.seat_config = new_seat_config(argv[0]); + } + if (!config->handler_context.seat_config) { + return cmd_results_new(CMD_FAILURE, "Couldn't allocate config"); + } + + struct cmd_results *res = NULL; + if (find_handler(argv[1], seat_action_handlers, + sizeof(seat_action_handlers))) { + res = action_handlers(argc - 1, argv + 1); + } else { + res = config_handlers(argc - 1, argv + 1); + } + return res ? res : cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/seat/attach.c b/sway/commands/seat/attach.c new file mode 100644 index 0000000..47d1854 --- /dev/null +++ b/sway/commands/seat/attach.c @@ -0,0 +1,27 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "stringop.h" + +struct cmd_results *seat_cmd_attach(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "attach", EXPECTED_AT_LEAST, 1))) { + return error; + } + if (!config->handler_context.seat_config) { + return cmd_results_new(CMD_FAILURE, "No seat defined"); + } + if (!config->active) { + return cmd_results_new(CMD_DEFER, NULL); + } + + struct seat_attachment_config *attachment = seat_attachment_config_new(); + if (!attachment) { + return cmd_results_new(CMD_FAILURE, + "Failed to allocate seat attachment config"); + } + attachment->identifier = strdup(argv[0]); + list_add(config->handler_context.seat_config->attachments, attachment); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/seat/cursor.c b/sway/commands/seat/cursor.c new file mode 100644 index 0000000..434e6bb --- /dev/null +++ b/sway/commands/seat/cursor.c @@ -0,0 +1,131 @@ +#include + +#include +#include +#include +#include "sway/commands.h" +#include "sway/input/cursor.h" +#include "sway/server.h" + +static struct cmd_results *press_or_release(struct sway_cursor *cursor, + char *action, char *button_str); + +static const char expected_syntax[] = "Expected 'cursor ' or " + "'cursor ' or " + "'cursor '"; + +static struct cmd_results *handle_command(struct sway_cursor *cursor, + int argc, char **argv) { + if (strcasecmp(argv[0], "move") == 0) { + if (argc < 3) { + return cmd_results_new(CMD_INVALID, "%s", expected_syntax); + } + int delta_x = strtol(argv[1], NULL, 10); + int delta_y = strtol(argv[2], NULL, 10); + wlr_cursor_move(cursor->cursor, NULL, delta_x, delta_y); + cursor_rebase(cursor); + wlr_seat_pointer_notify_frame(cursor->seat->wlr_seat); + } else if (strcasecmp(argv[0], "set") == 0) { + if (argc < 3) { + return cmd_results_new(CMD_INVALID, "%s", expected_syntax); + } + // map absolute coords (0..1,0..1) to root container coords + float x = strtof(argv[1], NULL) / root->width; + float y = strtof(argv[2], NULL) / root->height; + wlr_cursor_warp_absolute(cursor->cursor, NULL, x, y); + cursor_rebase(cursor); + wlr_seat_pointer_notify_frame(cursor->seat->wlr_seat); + } else { + if (argc < 2) { + return cmd_results_new(CMD_INVALID, "%s", expected_syntax); + } + struct cmd_results *error = NULL; + if ((error = press_or_release(cursor, argv[0], argv[1]))) { + return error; + } + } + + cursor_handle_activity_from_idle_source(cursor, IDLE_SOURCE_POINTER); + return cmd_results_new(CMD_SUCCESS, NULL); +} + +struct cmd_results *seat_cmd_cursor(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "cursor", EXPECTED_AT_LEAST, 2))) { + return error; + } + struct seat_config *sc = config->handler_context.seat_config; + if (!sc) { + return cmd_results_new(CMD_FAILURE, "No seat defined"); + } + + if (config->reading || !config->active) { + return cmd_results_new(CMD_DEFER, NULL); + } + + if (strcmp(sc->name, "*") != 0) { + struct sway_seat *seat = input_manager_get_seat(sc->name, false); + if (!seat) { + return cmd_results_new(CMD_FAILURE, + "Seat %s does not exist", sc->name); + } + error = handle_command(seat->cursor, argc, argv); + } else { + struct sway_seat *seat = NULL; + wl_list_for_each(seat, &server.input->seats, link) { + error = handle_command(seat->cursor, argc, argv); + if (error && error->status != CMD_SUCCESS) { + break; + } + } + } + + return error ? error : cmd_results_new(CMD_SUCCESS, NULL); +} + +static struct cmd_results *press_or_release(struct sway_cursor *cursor, + char *action, char *button_str) { + enum wl_pointer_button_state state; + uint32_t button; + if (strcasecmp(action, "press") == 0) { + state = WL_POINTER_BUTTON_STATE_PRESSED; + } else if (strcasecmp(action, "release") == 0) { + state = WL_POINTER_BUTTON_STATE_RELEASED; + } else { + return cmd_results_new(CMD_INVALID, "%s", expected_syntax); + } + + char *message = NULL; + button = get_mouse_button(button_str, &message); + if (message) { + struct cmd_results *error = + cmd_results_new(CMD_INVALID, "%s", message); + free(message); + return error; + } else if (button == SWAY_SCROLL_UP || button == SWAY_SCROLL_DOWN + || button == SWAY_SCROLL_LEFT || button == SWAY_SCROLL_RIGHT) { + // Dispatch axis event + enum wl_pointer_axis orientation = + (button == SWAY_SCROLL_UP || button == SWAY_SCROLL_DOWN) + ? WL_POINTER_AXIS_VERTICAL_SCROLL + : WL_POINTER_AXIS_HORIZONTAL_SCROLL; + double delta = (button == SWAY_SCROLL_UP || button == SWAY_SCROLL_LEFT) + ? -1 : 1; + struct wlr_pointer_axis_event event = { + .pointer = NULL, + .time_msec = 0, + .source = WL_POINTER_AXIS_SOURCE_WHEEL, + .orientation = orientation, + .delta = delta * 15, + .delta_discrete = delta + }; + dispatch_cursor_axis(cursor, &event); + wlr_seat_pointer_notify_frame(cursor->seat->wlr_seat); + return cmd_results_new(CMD_SUCCESS, NULL); + } else if (!button) { + return cmd_results_new(CMD_INVALID, "Unknown button %s", button_str); + } + dispatch_cursor_button(cursor, NULL, 0, button, state); + wlr_seat_pointer_notify_frame(cursor->seat->wlr_seat); + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/seat/fallback.c b/sway/commands/seat/fallback.c new file mode 100644 index 0000000..0330c35 --- /dev/null +++ b/sway/commands/seat/fallback.c @@ -0,0 +1,18 @@ +#include "sway/config.h" +#include "sway/commands.h" +#include "util.h" + +struct cmd_results *seat_cmd_fallback(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "fallback", EXPECTED_EQUAL_TO, 1))) { + return error; + } + if (!config->handler_context.seat_config) { + return cmd_results_new(CMD_FAILURE, "No seat defined"); + } + + config->handler_context.seat_config->fallback = + parse_boolean(argv[0], false); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/seat/hide_cursor.c b/sway/commands/seat/hide_cursor.c new file mode 100644 index 0000000..f5177a4 --- /dev/null +++ b/sway/commands/seat/hide_cursor.c @@ -0,0 +1,49 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/input/seat.h" +#include "sway/input/cursor.h" +#include "sway/server.h" +#include "stringop.h" +#include "util.h" + +struct cmd_results *seat_cmd_hide_cursor(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "hide_cursor", EXPECTED_AT_LEAST, 1))) { + return error; + } + if ((error = checkarg(argc, "hide_cursor", EXPECTED_AT_MOST, 2))) { + return error; + } + struct seat_config *seat_config = config->handler_context.seat_config; + if (!seat_config) { + return cmd_results_new(CMD_FAILURE, "No seat defined"); + } + + if (argc == 1) { + char *end; + int timeout = strtol(argv[0], &end, 10); + if (*end) { + return cmd_results_new(CMD_INVALID, "Expected an integer timeout"); + } + if (timeout < 100 && timeout != 0) { + timeout = 100; + } + seat_config->hide_cursor_timeout = timeout; + } else { + if (strcmp(argv[0], "when-typing") != 0) { + return cmd_results_new(CMD_INVALID, + "Expected 'hide_cursor |when-typing [enable|disable]'"); + } + seat_config->hide_cursor_when_typing = parse_boolean(argv[1], true) ? + HIDE_WHEN_TYPING_ENABLE : HIDE_WHEN_TYPING_DISABLE; + + // Invalidate all the caches for this config + struct sway_seat *seat = NULL; + wl_list_for_each(seat, &server.input->seats, link) { + seat->cursor->hide_when_typing = HIDE_WHEN_TYPING_DEFAULT; + } + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/seat/idle.c b/sway/commands/seat/idle.c new file mode 100644 index 0000000..2974453 --- /dev/null +++ b/sway/commands/seat/idle.c @@ -0,0 +1,78 @@ +#include +#include +#include +#include +#include "log.h" +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/input/seat.h" + +static const struct { + const char *name; + uint32_t value; +} idle_source_strings[] = { + { "keyboard", IDLE_SOURCE_KEYBOARD }, + { "pointer", IDLE_SOURCE_POINTER }, + { "touch", IDLE_SOURCE_TOUCH }, + { "tablet_pad", IDLE_SOURCE_TABLET_PAD }, + { "tablet_tool", IDLE_SOURCE_TABLET_TOOL }, + { "switch", IDLE_SOURCE_SWITCH }, +}; + +static uint32_t parse_sources(int argc, char **argv) { + uint32_t sources = 0; + for (int i = 0; i < argc; ++i) { + uint32_t value = 0; + for (size_t j = 0; j < sizeof(idle_source_strings) + / sizeof(idle_source_strings[0]); ++j) { + if (strcasecmp(idle_source_strings[j].name, argv[i]) == 0) { + value = idle_source_strings[j].value; + break; + } + } + if (value == 0) { + return UINT32_MAX; + } + sources |= value; + } + return sources; +} + +struct cmd_results *seat_cmd_idle_inhibit(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "idle_inhibit", EXPECTED_AT_LEAST, 1))) { + return error; + } + if (!config->handler_context.seat_config) { + return cmd_results_new(CMD_FAILURE, "No seat defined"); + } + + uint32_t sources = parse_sources(argc, argv); + if (sources == UINT32_MAX) { + return cmd_results_new(CMD_FAILURE, "Invalid idle source"); + } + config->handler_context.seat_config->idle_inhibit_sources = sources; + return cmd_results_new(CMD_SUCCESS, NULL); +} + +struct cmd_results *seat_cmd_idle_wake(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "idle_wake", EXPECTED_AT_LEAST, 1))) { + return error; + } + if (!config->handler_context.seat_config) { + return cmd_results_new(CMD_FAILURE, "No seat defined"); + } + + uint32_t sources = parse_sources(argc, argv); + if (sources == UINT32_MAX) { + return cmd_results_new(CMD_FAILURE, "Invalid idle source"); + } + config->handler_context.seat_config->idle_wake_sources = sources; + sway_log(SWAY_INFO, "Warning: seat idle_wake is deprecated"); + if (config->reading) { + config_add_swaynag_warning("seat idle_wake is deprecated. " + "Only seat idle_inhibit is supported."); + } + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/seat/keyboard_grouping.c b/sway/commands/seat/keyboard_grouping.c new file mode 100644 index 0000000..5ad0f40 --- /dev/null +++ b/sway/commands/seat/keyboard_grouping.c @@ -0,0 +1,26 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "stringop.h" + +struct cmd_results *seat_cmd_keyboard_grouping(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "keyboard_grouping", EXPECTED_EQUAL_TO, 1))) { + return error; + } + if (!config->handler_context.seat_config) { + return cmd_results_new(CMD_INVALID, "No seat defined"); + } + + struct seat_config *seat_config = config->handler_context.seat_config; + if (strcmp(argv[0], "none") == 0) { + seat_config->keyboard_grouping = KEYBOARD_GROUP_NONE; + } else if (strcmp(argv[0], "smart") == 0) { + seat_config->keyboard_grouping = KEYBOARD_GROUP_SMART; + } else { + return cmd_results_new(CMD_INVALID, + "Expected syntax `keyboard_grouping none|smart`"); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/seat/pointer_constraint.c b/sway/commands/seat/pointer_constraint.c new file mode 100644 index 0000000..38f85bc --- /dev/null +++ b/sway/commands/seat/pointer_constraint.c @@ -0,0 +1,59 @@ +#include +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/input/cursor.h" +#include "sway/input/seat.h" +#include "sway/server.h" + +enum operation { + OP_ENABLE, + OP_DISABLE, + OP_ESCAPE, +}; + +// pointer_constraint [enable|disable|escape] +struct cmd_results *seat_cmd_pointer_constraint(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "pointer_constraint", EXPECTED_EQUAL_TO, 1))) { + return error; + } + if (!config->handler_context.seat_config) { + return cmd_results_new(CMD_FAILURE, "No seat defined"); + } + + enum operation op; + if (strcmp(argv[0], "enable") == 0) { + op = OP_ENABLE; + } else if (strcmp(argv[0], "disable") == 0) { + op = OP_DISABLE; + } else if (strcmp(argv[0], "escape") == 0) { + op = OP_ESCAPE; + } else { + return cmd_results_new(CMD_FAILURE, "Expected enable|disable|escape"); + } + + if (op == OP_ESCAPE && config->reading) { + return cmd_results_new(CMD_FAILURE, "Can only escape at runtime."); + } + + struct seat_config *seat_config = config->handler_context.seat_config; + switch (op) { + case OP_ENABLE: + seat_config->allow_constrain = CONSTRAIN_ENABLE; + break; + case OP_DISABLE: + seat_config->allow_constrain = CONSTRAIN_DISABLE; + /* fallthrough */ + case OP_ESCAPE:; + bool wildcard = !strcmp(seat_config->name, "*"); + struct sway_seat *seat = NULL; + wl_list_for_each(seat, &server.input->seats, link) { + if (wildcard || !strcmp(seat->wlr_seat->name, seat_config->name)) { + sway_cursor_constrain(seat->cursor, NULL); + } + } + break; + } + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/seat/shortcuts_inhibitor.c b/sway/commands/seat/shortcuts_inhibitor.c new file mode 100644 index 0000000..df68618 --- /dev/null +++ b/sway/commands/seat/shortcuts_inhibitor.c @@ -0,0 +1,97 @@ +#include "log.h" +#include "sway/commands.h" +#include "sway/input/seat.h" +#include "sway/input/input-manager.h" +#include "sway/server.h" +#include "util.h" + +static struct cmd_results *handle_action(struct seat_config *sc, + struct sway_seat *seat, const char *action) { + struct sway_keyboard_shortcuts_inhibitor *sway_inhibitor = NULL; + if (strcmp(action, "disable") == 0) { + sc->shortcuts_inhibit = SHORTCUTS_INHIBIT_DISABLE; + + wl_list_for_each(sway_inhibitor, + &seat->keyboard_shortcuts_inhibitors, link) { + wlr_keyboard_shortcuts_inhibitor_v1_deactivate( + sway_inhibitor->inhibitor); + } + + sway_log(SWAY_DEBUG, "Deactivated all keyboard shortcuts inhibitors"); + } else { + sway_inhibitor = keyboard_shortcuts_inhibitor_get_for_focused_surface(seat); + if (!sway_inhibitor) { + return cmd_results_new(CMD_FAILURE, + "No inhibitor found for focused surface"); + } + + struct wlr_keyboard_shortcuts_inhibitor_v1 *inhibitor = + sway_inhibitor->inhibitor; + bool inhibit; + if (strcmp(action, "activate") == 0) { + inhibit = true; + } else if (strcmp(action, "deactivate") == 0) { + inhibit = false; + } else if (strcmp(action, "toggle") == 0) { + inhibit = !inhibitor->active; + } else { + return cmd_results_new(CMD_INVALID, "Expected enable|" + "disable|activate|deactivate|toggle"); + } + + if (inhibit) { + wlr_keyboard_shortcuts_inhibitor_v1_activate(inhibitor); + } else { + wlr_keyboard_shortcuts_inhibitor_v1_deactivate(inhibitor); + } + + sway_log(SWAY_DEBUG, "%sctivated keyboard shortcuts inhibitor", + inhibit ? "A" : "Dea"); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} + +// shortcuts_inhibitor [enable|disable|activate|deactivate|toggle] +struct cmd_results *seat_cmd_shortcuts_inhibitor(int argc, char **argv) { + struct cmd_results *error = + checkarg(argc, "shortcuts_inhibitor", EXPECTED_EQUAL_TO, 1); + if (error) { + return error; + } + + struct seat_config *sc = config->handler_context.seat_config; + if (!sc) { + return cmd_results_new(CMD_FAILURE, "No seat defined"); + } + + if (strcmp(argv[0], "enable") == 0) { + sc->shortcuts_inhibit = SHORTCUTS_INHIBIT_ENABLE; + // at runtime disable is an action that also deactivates all active + // inhibitors handled in handle_action() + } else if (strcmp(argv[0], "disable") == 0 && !config->active) { + sc->shortcuts_inhibit = SHORTCUTS_INHIBIT_DISABLE; + } else if (!config->active) { + return cmd_results_new(CMD_INVALID, "only enable and disable " + "can be used in the config"); + } else { + if (strcmp(sc->name, "*") != 0) { + struct sway_seat *seat = input_manager_get_seat(sc->name, false); + if (!seat) { + return cmd_results_new(CMD_FAILURE, + "Seat %s does not exist", sc->name); + } + error = handle_action(sc, seat, argv[0]); + } else { + struct sway_seat *seat = NULL; + wl_list_for_each(seat, &server.input->seats, link) { + error = handle_action(sc, seat, argv[0]); + if (error && error->status != CMD_SUCCESS) { + break; + } + } + } + } + + return error ? error : cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/seat/xcursor_theme.c b/sway/commands/seat/xcursor_theme.c new file mode 100644 index 0000000..61322a5 --- /dev/null +++ b/sway/commands/seat/xcursor_theme.c @@ -0,0 +1,32 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" + +struct cmd_results *seat_cmd_xcursor_theme(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "xcursor_theme", EXPECTED_AT_LEAST, 1)) || + (error = checkarg(argc, "xcursor_theme", EXPECTED_AT_MOST, 2))) { + return error; + } + if (!config->handler_context.seat_config) { + return cmd_results_new(CMD_FAILURE, "No seat defined"); + } + + const char *theme_name = argv[0]; + unsigned size = 24; + + if (argc == 2) { + char *end; + size = strtoul(argv[1], &end, 10); + if (*end) { + return cmd_results_new( + CMD_INVALID, "Expected a positive integer size"); + } + } + + free(config->handler_context.seat_config->xcursor_theme.name); + config->handler_context.seat_config->xcursor_theme.name = strdup(theme_name); + config->handler_context.seat_config->xcursor_theme.size = size; + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/set.c b/sway/commands/set.c new file mode 100644 index 0000000..ba384c7 --- /dev/null +++ b/sway/commands/set.c @@ -0,0 +1,59 @@ +#include +#include +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "list.h" +#include "log.h" +#include "stringop.h" + +// sort in order of longest->shortest +static int compare_set_qsort(const void *_l, const void *_r) { + struct sway_variable const *l = *(void **)_l; + struct sway_variable const *r = *(void **)_r; + return strlen(r->name) - strlen(l->name); +} + +void free_sway_variable(struct sway_variable *var) { + if (!var) { + return; + } + free(var->name); + free(var->value); + free(var); +} + +struct cmd_results *cmd_set(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "set", EXPECTED_AT_LEAST, 2))) { + return error; + } + + if (argv[0][0] != '$') { + return cmd_results_new(CMD_INVALID, "variable '%s' must start with $", argv[0]); + } + + struct sway_variable *var = NULL; + // Find old variable if it exists + int i; + for (i = 0; i < config->symbols->length; ++i) { + var = config->symbols->items[i]; + if (strcmp(var->name, argv[0]) == 0) { + break; + } + var = NULL; + } + if (var) { + free(var->value); + } else { + var = malloc(sizeof(struct sway_variable)); + if (!var) { + return cmd_results_new(CMD_FAILURE, "Unable to allocate variable"); + } + var->name = strdup(argv[0]); + list_add(config->symbols, var); + list_qsort(config->symbols, compare_set_qsort); + } + var->value = join_args(argv + 1, argc - 1); + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/shadow_blur_radius.c b/sway/commands/shadow_blur_radius.c new file mode 100644 index 0000000..036341f --- /dev/null +++ b/sway/commands/shadow_blur_radius.c @@ -0,0 +1,25 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/tree/arrange.h" +#include "sway/tree/container.h" +#include "log.h" + +struct cmd_results *cmd_shadow_blur_radius(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "shadow_blur_radius", EXPECTED_EQUAL_TO, 1))) { + return error; + } + + char *inv; + int value = strtol(argv[0], &inv, 10); + if (*inv != '\0' || value < 0 || value > 99) { + return cmd_results_new(CMD_FAILURE, "Invalid size specified"); + } + + config->shadow_blur_sigma = value; + + arrange_root(); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/shadow_color.c b/sway/commands/shadow_color.c new file mode 100644 index 0000000..d7c8fef --- /dev/null +++ b/sway/commands/shadow_color.c @@ -0,0 +1,28 @@ +#include "log.h" +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/output.h" +#include "sway/tree/arrange.h" +#include "sway/tree/container.h" +#include "util.h" + +struct cmd_results *cmd_shadow_color(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "shadow_color", EXPECTED_AT_LEAST, 1))) { + return error; + } + + uint32_t color; + if (!parse_color(argv[0], &color)) { + return cmd_results_new(CMD_INVALID, "Invalid %s color %s", + "shadow_color", argv[0]); + } + if (memcmp(config->shadow_color, config->shadow_inactive_color, sizeof(config->shadow_color)) == 0) { + color_to_rgba(config->shadow_inactive_color, color); + } + color_to_rgba(config->shadow_color, color); + + arrange_root(); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/shadow_inactive_color.c b/sway/commands/shadow_inactive_color.c new file mode 100644 index 0000000..6ad207e --- /dev/null +++ b/sway/commands/shadow_inactive_color.c @@ -0,0 +1,25 @@ +#include "log.h" +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/output.h" +#include "sway/tree/arrange.h" +#include "sway/tree/container.h" +#include "util.h" + +struct cmd_results *cmd_shadow_inactive_color(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "shadow_inactive_color", EXPECTED_AT_LEAST, 1))) { + return error; + } + + uint32_t color; + if (!parse_color(argv[0], &color)) { + return cmd_results_new(CMD_INVALID, "Invalid %s color %s", + "shadow_inactive_color", argv[0]); + } + color_to_rgba(config->shadow_inactive_color, color); + + arrange_root(); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/shadow_offset.c b/sway/commands/shadow_offset.c new file mode 100644 index 0000000..a73b54c --- /dev/null +++ b/sway/commands/shadow_offset.c @@ -0,0 +1,27 @@ +#include +#include "log.h" +#include "sway/commands.h" +#include "sway/config.h" + +struct cmd_results *cmd_shadow_offset(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "shadow_offset", EXPECTED_AT_LEAST, 2))) { + return error; + } + + char *err; + float offset_x = strtof(argv[0], &err); + float offset_y = strtof(argv[1], &err); + if (*err || offset_x < -99.9f || offset_x > 99.9f) { + return cmd_results_new(CMD_INVALID, "x offset float invalid"); + } + if (*err || offset_y < -99.9f || offset_y > 99.9f) { + return cmd_results_new(CMD_INVALID, "y offset float invalid"); + } + + config->shadow_offset_x = offset_x; + config->shadow_offset_y = offset_y; + + return cmd_results_new(CMD_SUCCESS, NULL); +} + diff --git a/sway/commands/shadows.c b/sway/commands/shadows.c new file mode 100644 index 0000000..a7425b3 --- /dev/null +++ b/sway/commands/shadows.c @@ -0,0 +1,34 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/tree/arrange.h" +#include "sway/tree/view.h" +#include "sway/tree/container.h" +#include "util.h" + +static void arrange_shadow_iter(struct sway_container *con, void *data) { + con->shadow_enabled = config->shadow_enabled; +} + +struct cmd_results *cmd_shadows(int argc, char **argv) { + struct cmd_results *error = checkarg(argc, "shadows", EXPECTED_AT_LEAST, 1); + + if (error) { + return error; + } + + struct sway_container *con = config->handler_context.container; + + bool result = parse_boolean(argv[0], true); + if (con == NULL) { + config->shadow_enabled = result; + // Config reload: reset all containers to config value + root_for_each_container(arrange_shadow_iter, NULL); + } else { + con->shadow_enabled = result; + } + + arrange_root(); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/shadows_on_csd.c b/sway/commands/shadows_on_csd.c new file mode 100644 index 0000000..24f2bea --- /dev/null +++ b/sway/commands/shadows_on_csd.c @@ -0,0 +1,19 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/tree/arrange.h" +#include "util.h" + +struct cmd_results *cmd_shadows_on_csd(int argc, char **argv) { + struct cmd_results *error = checkarg(argc, "shadows_on_csd", EXPECTED_AT_LEAST, 1); + + if (error) { + return error; + } + + config->shadows_on_csd_enabled = parse_boolean(argv[0], config->shadows_on_csd_enabled); + + arrange_root(); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/shortcuts_inhibitor.c b/sway/commands/shortcuts_inhibitor.c new file mode 100644 index 0000000..2dfd1b9 --- /dev/null +++ b/sway/commands/shortcuts_inhibitor.c @@ -0,0 +1,50 @@ +#include +#include "log.h" +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/input/seat.h" +#include "sway/server.h" +#include "sway/tree/container.h" +#include "sway/tree/view.h" + +struct cmd_results *cmd_shortcuts_inhibitor(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "shortcuts_inhibitor", EXPECTED_EQUAL_TO, 1))) { + return error; + } + + struct sway_container *con = config->handler_context.container; + if (!con || !con->view) { + return cmd_results_new(CMD_INVALID, + "Only views can have shortcuts inhibitors"); + } + + struct sway_view *view = con->view; + if (strcmp(argv[0], "enable") == 0) { + view->shortcuts_inhibit = SHORTCUTS_INHIBIT_ENABLE; + } else if (strcmp(argv[0], "disable") == 0) { + view->shortcuts_inhibit = SHORTCUTS_INHIBIT_DISABLE; + + struct sway_seat *seat = NULL; + wl_list_for_each(seat, &server.input->seats, link) { + struct sway_keyboard_shortcuts_inhibitor *sway_inhibitor = + keyboard_shortcuts_inhibitor_get_for_surface( + seat, view->surface); + if (!sway_inhibitor) { + continue; + } + + wlr_keyboard_shortcuts_inhibitor_v1_deactivate( + sway_inhibitor->inhibitor); + sway_log(SWAY_DEBUG, "Deactivated keyboard shortcuts " + "inhibitor for seat %s on view", + seat->wlr_seat->name); + + } + } else { + return cmd_results_new(CMD_INVALID, + "Expected `shortcuts_inhibitor enable|disable`"); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/show_marks.c b/sway/commands/show_marks.c new file mode 100644 index 0000000..60cef9f --- /dev/null +++ b/sway/commands/show_marks.c @@ -0,0 +1,29 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/tree/root.h" +#include "sway/tree/view.h" +#include "sway/output.h" +#include "list.h" +#include "log.h" +#include "stringop.h" +#include "util.h" + +static void title_bar_update_iterator(struct sway_container *con, void *data) { + container_update_marks(con); +} + +struct cmd_results *cmd_show_marks(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "show_marks", EXPECTED_AT_LEAST, 1))) { + return error; + } + + config->show_marks = parse_boolean(argv[0], config->show_marks); + + if (config->show_marks) { + root_for_each_container(title_bar_update_iterator, NULL); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/smart_borders.c b/sway/commands/smart_borders.c new file mode 100644 index 0000000..7387867 --- /dev/null +++ b/sway/commands/smart_borders.c @@ -0,0 +1,23 @@ +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/tree/arrange.h" +#include "sway/tree/view.h" +#include "util.h" + +struct cmd_results *cmd_smart_borders(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "smart_borders", EXPECTED_EQUAL_TO, 1))) { + return error; + } + + if (strcmp(argv[0], "no_gaps") == 0) { + config->hide_edge_borders_smart = ESMART_NO_GAPS; + } else { + config->hide_edge_borders_smart = parse_boolean(argv[0], true) ? + ESMART_ON : ESMART_OFF; + } + + arrange_root(); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/smart_corner_radius.c b/sway/commands/smart_corner_radius.c new file mode 100644 index 0000000..4104cb4 --- /dev/null +++ b/sway/commands/smart_corner_radius.c @@ -0,0 +1,17 @@ +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/tree/arrange.h" +#include "util.h" + +struct cmd_results *cmd_smart_corner_radius(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "smart_corner_radius", EXPECTED_EQUAL_TO, 1))) { + return error; + } + + config->smart_corner_radius = parse_boolean(argv[0], true); + + arrange_root(); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/smart_gaps.c b/sway/commands/smart_gaps.c new file mode 100644 index 0000000..a6d165d --- /dev/null +++ b/sway/commands/smart_gaps.c @@ -0,0 +1,28 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/tree/arrange.h" +#include "sway/tree/view.h" +#include "sway/tree/container.h" +#include "log.h" +#include "stringop.h" +#include "util.h" + +struct cmd_results *cmd_smart_gaps(int argc, char **argv) { + struct cmd_results *error = checkarg(argc, "smart_gaps", EXPECTED_AT_LEAST, 1); + + if (error) { + return error; + } + + if (strcmp(argv[0], "inverse_outer") == 0) { + config->smart_gaps = SMART_GAPS_INVERSE_OUTER; + } else { + config->smart_gaps = parse_boolean(argv[0], config->smart_gaps) + ? SMART_GAPS_ON : SMART_GAPS_OFF; + } + + arrange_root(); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/split.c b/sway/commands/split.c new file mode 100644 index 0000000..500a497 --- /dev/null +++ b/sway/commands/split.c @@ -0,0 +1,115 @@ +#include +#include +#include "sway/commands.h" +#include "sway/tree/arrange.h" +#include "sway/tree/container.h" +#include "sway/tree/view.h" +#include "sway/tree/workspace.h" +#include "sway/input/input-manager.h" +#include "sway/input/seat.h" +#include "log.h" + +static struct cmd_results *do_split(int layout) { + struct sway_container *con = config->handler_context.container; + struct sway_workspace *ws = config->handler_context.workspace; + if (con) { + if (container_is_scratchpad_hidden_or_child(con) && + con->pending.fullscreen_mode != FULLSCREEN_GLOBAL) { + return cmd_results_new(CMD_FAILURE, + "Cannot split a hidden scratchpad container"); + } + container_split(con, layout); + } else { + workspace_split(ws, layout); + } + + if (root->fullscreen_global) { + arrange_root(); + } else { + arrange_workspace(ws); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} + +static struct cmd_results *do_unsplit(void) { + struct sway_container *con = config->handler_context.container; + struct sway_workspace *ws = config->handler_context.workspace; + + if (con && con->pending.parent && con->pending.parent->pending.children->length == 1) { + container_flatten(con->pending.parent); + } else { + return cmd_results_new(CMD_FAILURE, "Can only flatten a child container with no siblings"); + } + + if (root->fullscreen_global) { + arrange_root(); + } else { + arrange_workspace(ws); + } + return cmd_results_new(CMD_SUCCESS, NULL); +} + +struct cmd_results *cmd_split(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "split", EXPECTED_EQUAL_TO, 1))) { + return error; + } + if (!root->outputs->length) { + return cmd_results_new(CMD_INVALID, + "Can't run this command while there's no outputs connected."); + } + if (strcasecmp(argv[0], "v") == 0 || strcasecmp(argv[0], "vertical") == 0) { + return do_split(L_VERT); + } else if (strcasecmp(argv[0], "h") == 0 || + strcasecmp(argv[0], "horizontal") == 0) { + return do_split(L_HORIZ); + } else if (strcasecmp(argv[0], "t") == 0 || + strcasecmp(argv[0], "toggle") == 0) { + struct sway_container *focused = config->handler_context.container; + + if (focused && container_parent_layout(focused) == L_VERT) { + return do_split(L_HORIZ); + } else { + return do_split(L_VERT); + } + } else if (strcasecmp(argv[0], "n") == 0 || + strcasecmp(argv[0], "none") == 0) { + return do_unsplit(); + } else { + return cmd_results_new(CMD_FAILURE, + "Invalid split command (expected either horizontal or vertical)."); + } + return cmd_results_new(CMD_SUCCESS, NULL); +} + +struct cmd_results *cmd_splitv(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "splitv", EXPECTED_EQUAL_TO, 0))) { + return error; + } + return do_split(L_VERT); +} + +struct cmd_results *cmd_splith(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "splith", EXPECTED_EQUAL_TO, 0))) { + return error; + } + return do_split(L_HORIZ); +} + +struct cmd_results *cmd_splitt(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "splitt", EXPECTED_EQUAL_TO, 0))) { + return error; + } + + struct sway_container *con = config->handler_context.container; + + if (con && container_parent_layout(con) == L_VERT) { + return do_split(L_HORIZ); + } else { + return do_split(L_VERT); + } +} diff --git a/sway/commands/sticky.c b/sway/commands/sticky.c new file mode 100644 index 0000000..9b09a0f --- /dev/null +++ b/sway/commands/sticky.c @@ -0,0 +1,49 @@ +#include +#include +#include "sway/commands.h" +#include "sway/input/seat.h" +#include "sway/ipc-server.h" +#include "sway/output.h" +#include "sway/tree/arrange.h" +#include "sway/tree/container.h" +#include "sway/tree/view.h" +#include "sway/tree/workspace.h" +#include "list.h" +#include "log.h" +#include "util.h" + +struct cmd_results *cmd_sticky(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "sticky", EXPECTED_EQUAL_TO, 1))) { + return error; + } + struct sway_container *container = config->handler_context.container; + + if (container == NULL) { + return cmd_results_new(CMD_FAILURE, "No current container"); + }; + + container->is_sticky = parse_boolean(argv[0], container->is_sticky); + + if (container_is_sticky_or_child(container) && + !container_is_scratchpad_hidden(container)) { + // move container to active workspace + struct sway_workspace *active_workspace = + output_get_active_workspace(container->pending.workspace->output); + if (!sway_assert(active_workspace, + "Expected output to have a workspace")) { + return cmd_results_new(CMD_FAILURE, + "Expected output to have a workspace"); + } + if (container->pending.workspace != active_workspace) { + struct sway_workspace *old_workspace = container->pending.workspace; + container_detach(container); + workspace_add_floating(active_workspace, container); + container_handle_fullscreen_reparent(container); + arrange_workspace(active_workspace); + workspace_consider_destroy(old_workspace); + } + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/swap.c b/sway/commands/swap.c new file mode 100644 index 0000000..c0b0d0b --- /dev/null +++ b/sway/commands/swap.c @@ -0,0 +1,107 @@ +#include +#include "config.h" +#include "log.h" +#include "sway/commands.h" +#include "sway/output.h" +#include "sway/tree/arrange.h" +#include "sway/tree/container.h" +#include "sway/tree/root.h" +#include "sway/tree/view.h" +#include "sway/tree/workspace.h" +#include "stringop.h" + +static const char expected_syntax[] = + "Expected 'swap container with id|con_id|mark '"; + +static bool test_con_id(struct sway_container *container, void *data) { + size_t *con_id = data; + return container->node.id == *con_id; +} + +#if WLR_HAS_XWAYLAND +static bool test_id(struct sway_container *container, void *data) { + xcb_window_t *wid = data; + return (container->view && container->view->type == SWAY_VIEW_XWAYLAND + && container->view->wlr_xwayland_surface->window_id == *wid); +} +#endif + +static bool test_mark(struct sway_container *container, void *mark) { + if (container->marks->length) { + return list_seq_find(container->marks, + (int (*)(const void *, const void *))strcmp, mark) != -1; + } + return false; +} + +struct cmd_results *cmd_swap(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "swap", EXPECTED_AT_LEAST, 4))) { + return error; + } + if (!root->outputs->length) { + return cmd_results_new(CMD_INVALID, + "Can't run this command while there's no outputs connected."); + } + + if (strcasecmp(argv[0], "container") || strcasecmp(argv[1], "with")) { + return cmd_results_new(CMD_INVALID, "%s", expected_syntax); + } + + struct sway_container *current = config->handler_context.container; + struct sway_container *other = NULL; + + char *value = join_args(argv + 3, argc - 3); + if (strcasecmp(argv[2], "id") == 0) { +#if WLR_HAS_XWAYLAND + xcb_window_t id = strtol(value, NULL, 0); + other = root_find_container(test_id, &id); +#endif + } else if (strcasecmp(argv[2], "con_id") == 0) { + size_t con_id = atoi(value); + other = root_find_container(test_con_id, &con_id); + } else if (strcasecmp(argv[2], "mark") == 0) { + other = root_find_container(test_mark, value); + } else { + free(value); + return cmd_results_new(CMD_INVALID, "%s", expected_syntax); + } + + if (!other) { + error = cmd_results_new(CMD_FAILURE, + "Failed to find %s '%s'", argv[2], value); + } else if (!current) { + error = cmd_results_new(CMD_FAILURE, + "Can only swap with containers and views"); + } else if (current == other) { + error = cmd_results_new(CMD_FAILURE, + "Cannot swap a container with itself"); + } else if (container_has_ancestor(current, other) + || container_has_ancestor(other, current)) { + error = cmd_results_new(CMD_FAILURE, + "Cannot swap ancestor and descendant"); + } + + free(value); + + if (error) { + return error; + } + + container_swap(current, other); + + if (root->fullscreen_global) { + arrange_root(); + } else { + struct sway_node *current_parent = node_get_parent(¤t->node); + struct sway_node *other_parent = node_get_parent(&other->node); + if (current_parent) { + arrange_node(current_parent); + } + if (other_parent && current_parent != other_parent) { + arrange_node(other_parent); + } + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/swaybg_command.c b/sway/commands/swaybg_command.c new file mode 100644 index 0000000..92e9bdf --- /dev/null +++ b/sway/commands/swaybg_command.c @@ -0,0 +1,25 @@ +#include +#include "sway/commands.h" +#include "log.h" +#include "stringop.h" + +struct cmd_results *cmd_swaybg_command(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "swaybg_command", EXPECTED_AT_LEAST, 1))) { + return error; + } + + free(config->swaybg_command); + config->swaybg_command = NULL; + + char *new_command = join_args(argv, argc); + if (strcmp(new_command, "-") != 0) { + config->swaybg_command = new_command; + sway_log(SWAY_DEBUG, "Using custom swaybg command: %s", + config->swaybg_command); + } else { + free(new_command); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/swaynag_command.c b/sway/commands/swaynag_command.c new file mode 100644 index 0000000..f851d22 --- /dev/null +++ b/sway/commands/swaynag_command.c @@ -0,0 +1,25 @@ +#include +#include "sway/commands.h" +#include "log.h" +#include "stringop.h" + +struct cmd_results *cmd_swaynag_command(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "swaynag_command", EXPECTED_AT_LEAST, 1))) { + return error; + } + + free(config->swaynag_command); + config->swaynag_command = NULL; + + char *new_command = join_args(argv, argc); + if (strcmp(new_command, "-") != 0) { + config->swaynag_command = new_command; + sway_log(SWAY_DEBUG, "Using custom swaynag command: %s", + config->swaynag_command); + } else { + free(new_command); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/tiling_drag.c b/sway/commands/tiling_drag.c new file mode 100644 index 0000000..e95526e --- /dev/null +++ b/sway/commands/tiling_drag.c @@ -0,0 +1,13 @@ +#include "sway/commands.h" +#include "util.h" + +struct cmd_results *cmd_tiling_drag(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "tiling_drag", EXPECTED_EQUAL_TO, 1))) { + return error; + } + + config->tiling_drag = parse_boolean(argv[0], config->tiling_drag); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/tiling_drag_threshold.c b/sway/commands/tiling_drag_threshold.c new file mode 100644 index 0000000..f119111 --- /dev/null +++ b/sway/commands/tiling_drag_threshold.c @@ -0,0 +1,21 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "log.h" + +struct cmd_results *cmd_tiling_drag_threshold(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "tiling_drag_threshold", EXPECTED_EQUAL_TO, 1))) { + return error; + } + + char *inv; + int value = strtol(argv[0], &inv, 10); + if (*inv != '\0' || value < 0) { + return cmd_results_new(CMD_INVALID, "Invalid threshold specified"); + } + + config->tiling_drag_threshold = value; + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/title_align.c b/sway/commands/title_align.c new file mode 100644 index 0000000..be298a2 --- /dev/null +++ b/sway/commands/title_align.c @@ -0,0 +1,31 @@ +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/output.h" +#include "sway/tree/container.h" +#include "sway/tree/root.h" + +static void arrange_title_bar_iterator(struct sway_container *con, void *data) { + container_arrange_title_bar(con); +} + +struct cmd_results *cmd_title_align(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "title_align", EXPECTED_AT_LEAST, 1))) { + return error; + } + + if (strcmp(argv[0], "left") == 0) { + config->title_align = ALIGN_LEFT; + } else if (strcmp(argv[0], "center") == 0) { + config->title_align = ALIGN_CENTER; + } else if (strcmp(argv[0], "right") == 0) { + config->title_align = ALIGN_RIGHT; + } else { + return cmd_results_new(CMD_INVALID, + "Expected 'title_align left|center|right'"); + } + + root_for_each_container(arrange_title_bar_iterator, NULL); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/title_format.c b/sway/commands/title_format.c new file mode 100644 index 0000000..fbade47 --- /dev/null +++ b/sway/commands/title_format.c @@ -0,0 +1,30 @@ +#define _POSIX_C_SOURCE 200809L +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/tree/view.h" +#include "log.h" +#include "stringop.h" + +struct cmd_results *cmd_title_format(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "title_format", EXPECTED_AT_LEAST, 1))) { + return error; + } + struct sway_container *container = config->handler_context.container; + if (!container) { + return cmd_results_new(CMD_INVALID, + "Only valid containers can have a title_format"); + } + char *format = join_args(argv, argc); + if (container->title_format) { + free(container->title_format); + } + container->title_format = format; + if (container->view) { + view_update_title(container->view, true); + } else { + container_update_representation(container); + } + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/titlebar_border_thickness.c b/sway/commands/titlebar_border_thickness.c new file mode 100644 index 0000000..fa3db3c --- /dev/null +++ b/sway/commands/titlebar_border_thickness.c @@ -0,0 +1,33 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/output.h" +#include "sway/tree/arrange.h" +#include "log.h" + +struct cmd_results *cmd_titlebar_border_thickness(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "titlebar_border_thickness", EXPECTED_EQUAL_TO, 1))) { + return error; + } + + char *inv; + int value = strtol(argv[0], &inv, 10); + if (*inv != '\0' || value < 0 || value > config->titlebar_v_padding) { + return cmd_results_new(CMD_FAILURE, "Invalid size specified"); + } + + config->titlebar_border_thickness = value; + + for (int i = 0; i < root->outputs->length; ++i) { + struct sway_output *output = root->outputs->items[i]; + struct sway_workspace *ws = output_get_active_workspace(output); + if (!sway_assert(ws, "Expected output to have a workspace")) { + return cmd_results_new(CMD_FAILURE, + "Expected output to have a workspace"); + } + arrange_workspace(ws); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/titlebar_padding.c b/sway/commands/titlebar_padding.c new file mode 100644 index 0000000..6999f7a --- /dev/null +++ b/sway/commands/titlebar_padding.c @@ -0,0 +1,39 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/output.h" +#include "sway/tree/arrange.h" +#include "log.h" + +struct cmd_results *cmd_titlebar_padding(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "titlebar_padding", EXPECTED_AT_LEAST, 1))) { + return error; + } + + char *inv; + int h_value = strtol(argv[0], &inv, 10); + if (*inv != '\0' || h_value < 0 || h_value < config->titlebar_border_thickness) { + return cmd_results_new(CMD_FAILURE, "Invalid size specified"); + } + + int v_value; + if (argc == 1) { + v_value = h_value; + } else { + v_value = strtol(argv[1], &inv, 10); + if (*inv != '\0' || v_value < 0 || v_value < config->titlebar_border_thickness) { + return cmd_results_new(CMD_FAILURE, "Invalid size specified"); + } + } + + config->titlebar_v_padding = v_value; + config->titlebar_h_padding = h_value; + + for (int i = 0; i < root->outputs->length; ++i) { + struct sway_output *output = root->outputs->items[i]; + arrange_workspace(output_get_active_workspace(output)); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/titlebar_separator.c b/sway/commands/titlebar_separator.c new file mode 100644 index 0000000..05b15b9 --- /dev/null +++ b/sway/commands/titlebar_separator.c @@ -0,0 +1,18 @@ +#include +#include +#include "sway/commands.h" + +struct cmd_results *cmd_titlebar_separator(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "titlebar_separator", EXPECTED_EQUAL_TO, 1))) { + return error; + } else if(strcmp(argv[0], "disable") == 0) { + config->titlebar_separator = false; + } else if(strcmp(argv[0], "enable") == 0) { + config->titlebar_separator = true; + } else { + return cmd_results_new(CMD_FAILURE, + "Expected 'titlebar_separator enable|disable'"); + } + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/unmark.c b/sway/commands/unmark.c new file mode 100644 index 0000000..4aba5ba --- /dev/null +++ b/sway/commands/unmark.c @@ -0,0 +1,55 @@ +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/tree/root.h" +#include "sway/tree/view.h" +#include "list.h" +#include "log.h" +#include "stringop.h" + +static void remove_mark(struct sway_container *con) { + container_clear_marks(con); + container_update_marks(con); +} + +static void remove_all_marks_iterator(struct sway_container *con, void *data) { + remove_mark(con); +} + +// unmark Remove all marks from all views +// unmark foo Remove single mark from whichever view has it +// [criteria] unmark Remove all marks from matched view +// [criteria] unmark foo Remove single mark from matched view + +struct cmd_results *cmd_unmark(int argc, char **argv) { + // Determine the container + struct sway_container *con = NULL; + if (config->handler_context.node_overridden) { + con = config->handler_context.container; + } + + // Determine the mark + char *mark = NULL; + if (argc > 0) { + mark = join_args(argv, argc); + } + + if (con && mark) { + // Remove the mark from the given container + if (container_has_mark(con, mark)) { + container_find_and_unmark(mark); + } + } else if (con && !mark) { + // Clear all marks from the given container + remove_mark(con); + } else if (!con && mark) { + // Remove mark from whichever container has it + container_find_and_unmark(mark); + } else { + // Remove all marks from all containers + root_for_each_container(remove_all_marks_iterator, NULL); + } + free(mark); + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/urgent.c b/sway/commands/urgent.c new file mode 100644 index 0000000..3a2b00e --- /dev/null +++ b/sway/commands/urgent.c @@ -0,0 +1,32 @@ +#include "log.h" +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/tree/arrange.h" +#include "sway/tree/container.h" +#include "sway/tree/view.h" +#include "util.h" + +struct cmd_results *cmd_urgent(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "urgent", EXPECTED_EQUAL_TO, 1))) { + return error; + } + struct sway_container *container = config->handler_context.container; + if (!container) { + return cmd_results_new(CMD_FAILURE, "No current container"); + } + if (!container->view) { + return cmd_results_new(CMD_INVALID, "Only views can be urgent"); + } + struct sway_view *view = container->view; + + if (strcmp(argv[0], "allow") == 0) { + view->allow_request_urgent = true; + } else if (strcmp(argv[0], "deny") == 0) { + view->allow_request_urgent = false; + } else { + view_set_urgent(view, parse_boolean(argv[0], view_is_urgent(view))); + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/workspace.c b/sway/commands/workspace.c new file mode 100644 index 0000000..a14ebb2 --- /dev/null +++ b/sway/commands/workspace.c @@ -0,0 +1,240 @@ +#include +#include +#include +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/input/seat.h" +#include "sway/tree/workspace.h" +#include "list.h" +#include "log.h" +#include "stringop.h" + +static struct workspace_config *workspace_config_find_or_create(char *ws_name) { + struct workspace_config *wsc = workspace_find_config(ws_name); + if (wsc) { + return wsc; + } + wsc = calloc(1, sizeof(struct workspace_config)); + if (!wsc) { + return NULL; + } + wsc->workspace = strdup(ws_name); + wsc->outputs = create_list(); + wsc->gaps_inner = INT_MIN; + wsc->gaps_outer.top = INT_MIN; + wsc->gaps_outer.right = INT_MIN; + wsc->gaps_outer.bottom = INT_MIN; + wsc->gaps_outer.left = INT_MIN; + list_add(config->workspace_configs, wsc); + return wsc; +} + +void free_workspace_config(struct workspace_config *wsc) { + free(wsc->workspace); + list_free_items_and_destroy(wsc->outputs); + free(wsc); +} + +static void prevent_invalid_outer_gaps(struct workspace_config *wsc) { + if (wsc->gaps_outer.top != INT_MIN && + wsc->gaps_outer.top < -wsc->gaps_inner) { + wsc->gaps_outer.top = -wsc->gaps_inner; + } + if (wsc->gaps_outer.right != INT_MIN && + wsc->gaps_outer.right < -wsc->gaps_inner) { + wsc->gaps_outer.right = -wsc->gaps_inner; + } + if (wsc->gaps_outer.bottom != INT_MIN && + wsc->gaps_outer.bottom < -wsc->gaps_inner) { + wsc->gaps_outer.bottom = -wsc->gaps_inner; + } + if (wsc->gaps_outer.left != INT_MIN && + wsc->gaps_outer.left < -wsc->gaps_inner) { + wsc->gaps_outer.left = -wsc->gaps_inner; + } +} + +static struct cmd_results *cmd_workspace_gaps(int argc, char **argv, + int gaps_location) { + const char expected[] = "Expected 'workspace gaps " + "inner|outer|horizontal|vertical|top|right|bottom|left '"; + if (gaps_location == 0) { + return cmd_results_new(CMD_INVALID, "%s", expected); + } + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "workspace", EXPECTED_EQUAL_TO, + gaps_location + 3))) { + return error; + } + char *ws_name = join_args(argv, argc - 3); + struct workspace_config *wsc = workspace_config_find_or_create(ws_name); + free(ws_name); + if (!wsc) { + return cmd_results_new(CMD_FAILURE, + "Unable to allocate workspace output"); + } + + char *end; + int amount = strtol(argv[gaps_location + 2], &end, 10); + if (strlen(end)) { + return cmd_results_new(CMD_FAILURE, "%s", expected); + } + + bool valid = false; + char *type = argv[gaps_location + 1]; + if (!strcasecmp(type, "inner")) { + valid = true; + wsc->gaps_inner = (amount >= 0) ? amount : 0; + } else { + if (!strcasecmp(type, "outer") || !strcasecmp(type, "vertical") + || !strcasecmp(type, "top")) { + valid = true; + wsc->gaps_outer.top = amount; + } + if (!strcasecmp(type, "outer") || !strcasecmp(type, "horizontal") + || !strcasecmp(type, "right")) { + valid = true; + wsc->gaps_outer.right = amount; + } + if (!strcasecmp(type, "outer") || !strcasecmp(type, "vertical") + || !strcasecmp(type, "bottom")) { + valid = true; + wsc->gaps_outer.bottom = amount; + } + if (!strcasecmp(type, "outer") || !strcasecmp(type, "horizontal") + || !strcasecmp(type, "left")) { + valid = true; + wsc->gaps_outer.left = amount; + } + } + if (!valid) { + return cmd_results_new(CMD_INVALID, "%s", expected); + } + + // Prevent invalid gaps configurations. + if (wsc->gaps_inner != INT_MIN && wsc->gaps_inner < 0) { + wsc->gaps_inner = 0; + } + prevent_invalid_outer_gaps(wsc); + + return error; +} + +struct cmd_results *cmd_workspace(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "workspace", EXPECTED_AT_LEAST, 1))) { + return error; + } + + int output_location = -1; + int gaps_location = -1; + + for (int i = 0; i < argc; ++i) { + if (strcasecmp(argv[i], "output") == 0) { + output_location = i; + break; + } + } + for (int i = 0; i < argc; ++i) { + if (strcasecmp(argv[i], "gaps") == 0) { + gaps_location = i; + break; + } + } + if (output_location == 0) { + return cmd_results_new(CMD_INVALID, + "Expected 'workspace output '"); + } else if (output_location > 0) { + if ((error = checkarg(argc, "workspace", EXPECTED_AT_LEAST, + output_location + 2))) { + return error; + } + char *ws_name = join_args(argv, output_location); + struct workspace_config *wsc = workspace_config_find_or_create(ws_name); + free(ws_name); + if (!wsc) { + return cmd_results_new(CMD_FAILURE, + "Unable to allocate workspace output"); + } + for (int i = output_location + 1; i < argc; ++i) { + list_add(wsc->outputs, strdup(argv[i])); + } + } else if (gaps_location >= 0) { + if ((error = cmd_workspace_gaps(argc, argv, gaps_location))) { + return error; + } + } else { + if (config->reading || !config->active) { + return cmd_results_new(CMD_DEFER, NULL); + } else if (!root->outputs->length) { + return cmd_results_new(CMD_INVALID, + "Can't run this command while there's no outputs connected."); + } + + if (root->fullscreen_global) { + return cmd_results_new(CMD_FAILURE, + "Can't switch workspaces while fullscreen global"); + } + + bool auto_back_and_forth = true; + while (strcasecmp(argv[0], "--no-auto-back-and-forth") == 0) { + auto_back_and_forth = false; + if ((error = checkarg(--argc, "workspace", EXPECTED_AT_LEAST, 1))) { + return error; + } + ++argv; + } + + struct sway_seat *seat = config->handler_context.seat; + + struct sway_workspace *ws = NULL; + if (strcasecmp(argv[0], "number") == 0) { + if (argc < 2) { + return cmd_results_new(CMD_INVALID, + "Expected workspace number"); + } + if (!isdigit(argv[1][0])) { + return cmd_results_new(CMD_INVALID, + "Invalid workspace number '%s'", argv[1]); + } + if (!(ws = workspace_by_number(argv[1]))) { + char *name = join_args(argv + 1, argc - 1); + ws = workspace_create(NULL, name); + free(name); + } + if (ws && auto_back_and_forth) { + ws = workspace_auto_back_and_forth(ws); + } + } else if (strcasecmp(argv[0], "next") == 0 || + strcasecmp(argv[0], "prev") == 0 || + strcasecmp(argv[0], "next_on_output") == 0 || + strcasecmp(argv[0], "prev_on_output") == 0 || + strcasecmp(argv[0], "current") == 0) { + ws = workspace_by_name(argv[0]); + } else if (strcasecmp(argv[0], "back_and_forth") == 0) { + if (!seat->prev_workspace_name) { + return cmd_results_new(CMD_INVALID, + "There is no previous workspace"); + } + if (!(ws = workspace_by_name(argv[0]))) { + ws = workspace_create(NULL, seat->prev_workspace_name); + } + } else { + char *name = join_args(argv, argc); + if (!(ws = workspace_by_name(name))) { + ws = workspace_create(NULL, name); + } + free(name); + if (ws && auto_back_and_forth) { + ws = workspace_auto_back_and_forth(ws); + } + } + if (!ws) { + return cmd_results_new(CMD_FAILURE, "No workspace to switch to"); + } + workspace_switch(ws); + seat_consider_warp_to_focus(seat); + } + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/workspace_layout.c b/sway/commands/workspace_layout.c new file mode 100644 index 0000000..4fed05a --- /dev/null +++ b/sway/commands/workspace_layout.c @@ -0,0 +1,21 @@ +#include +#include +#include "sway/commands.h" + +struct cmd_results *cmd_workspace_layout(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "workspace_layout", EXPECTED_EQUAL_TO, 1))) { + return error; + } + if (strcasecmp(argv[0], "default") == 0) { + config->default_layout = L_NONE; + } else if (strcasecmp(argv[0], "stacking") == 0) { + config->default_layout = L_STACKED; + } else if (strcasecmp(argv[0], "tabbed") == 0) { + config->default_layout = L_TABBED; + } else { + return cmd_results_new(CMD_INVALID, + "Expected 'workspace_layout '"); + } + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/ws_auto_back_and_forth.c b/sway/commands/ws_auto_back_and_forth.c new file mode 100644 index 0000000..5827c38 --- /dev/null +++ b/sway/commands/ws_auto_back_and_forth.c @@ -0,0 +1,14 @@ +#include +#include +#include "sway/commands.h" +#include "util.h" + +struct cmd_results *cmd_ws_auto_back_and_forth(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "workspace_auto_back_and_forth", EXPECTED_EQUAL_TO, 1))) { + return error; + } + config->auto_back_and_forth = + parse_boolean(argv[0], config->auto_back_and_forth); + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/commands/xwayland.c b/sway/commands/xwayland.c new file mode 100644 index 0000000..c0b175f --- /dev/null +++ b/sway/commands/xwayland.c @@ -0,0 +1,36 @@ +#include "sway/config.h" +#include "log.h" +#include "sway/commands.h" +#include "sway/server.h" +#include "util.h" + +struct cmd_results *cmd_xwayland(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "xwayland", EXPECTED_EQUAL_TO, 1))) { + return error; + } + +#ifdef WLR_HAS_XWAYLAND + enum xwayland_mode xwayland; + if (strcmp(argv[0], "force") == 0) { + xwayland = XWAYLAND_MODE_IMMEDIATE; + } else if (parse_boolean(argv[0], true)) { + xwayland = XWAYLAND_MODE_LAZY; + } else { + xwayland = XWAYLAND_MODE_DISABLED; + } + + // config->xwayland is reset to the previous value on reload in + // load_main_config() + if (config->reloading && config->xwayland != xwayland) { + return cmd_results_new(CMD_FAILURE, + "xwayland can only be enabled/disabled at launch"); + } + config->xwayland = xwayland; +#else + sway_log(SWAY_INFO, "Ignoring `xwayland` command, " + "sway hasn't been built with Xwayland support"); +#endif + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/config.c b/sway/config.c new file mode 100644 index 0000000..d5ee34c --- /dev/null +++ b/sway/config.c @@ -0,0 +1,1049 @@ +#undef _POSIX_C_SOURCE +#define _XOPEN_SOURCE 700 // for realpath +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "sway/input/input-manager.h" +#include "sway/input/seat.h" +#include "sway/input/switch.h" +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/criteria.h" +#include "sway/layer_criteria.h" +#include "sway/desktop/transaction.h" +#include "sway/server.h" +#include "sway/swaynag.h" +#include "sway/tree/arrange.h" +#include "sway/tree/root.h" +#include "sway/tree/workspace.h" +#include "cairo_util.h" +#include "pango.h" +#include "stringop.h" +#include "list.h" +#include "log.h" +#include "util.h" + +struct sway_config *config = NULL; + +static struct xkb_state *keysym_translation_state_create( + struct xkb_rule_names rules, uint32_t context_flags) { + struct xkb_context *context = xkb_context_new(context_flags | XKB_CONTEXT_NO_SECURE_GETENV); + struct xkb_keymap *xkb_keymap = xkb_keymap_new_from_names( + context, + &rules, + XKB_KEYMAP_COMPILE_NO_FLAGS); + xkb_context_unref(context); + if (xkb_keymap == NULL) { + sway_log(SWAY_ERROR, "Failed to compile keysym translation XKB keymap"); + return NULL; + } + + return xkb_state_new(xkb_keymap); +} + +static void keysym_translation_state_destroy( + struct xkb_state *state) { + if (state == NULL) { + return; + } + xkb_keymap_unref(xkb_state_get_keymap(state)); + xkb_state_unref(state); +} + +static void free_mode(struct sway_mode *mode) { + if (!mode) { + return; + } + free(mode->name); + if (mode->keysym_bindings) { + for (int i = 0; i < mode->keysym_bindings->length; i++) { + free_sway_binding(mode->keysym_bindings->items[i]); + } + list_free(mode->keysym_bindings); + } + if (mode->keycode_bindings) { + for (int i = 0; i < mode->keycode_bindings->length; i++) { + free_sway_binding(mode->keycode_bindings->items[i]); + } + list_free(mode->keycode_bindings); + } + if (mode->mouse_bindings) { + for (int i = 0; i < mode->mouse_bindings->length; i++) { + free_sway_binding(mode->mouse_bindings->items[i]); + } + list_free(mode->mouse_bindings); + } + if (mode->switch_bindings) { + for (int i = 0; i < mode->switch_bindings->length; i++) { + free_switch_binding(mode->switch_bindings->items[i]); + } + list_free(mode->switch_bindings); + } + if (mode->gesture_bindings) { + for (int i = 0; i < mode->gesture_bindings->length; i++) { + free_gesture_binding(mode->gesture_bindings->items[i]); + } + list_free(mode->gesture_bindings); + } + free(mode); +} + +void free_config(struct sway_config *config) { + if (!config) { + return; + } + + memset(&config->handler_context, 0, sizeof(config->handler_context)); + + // TODO: handle all currently unhandled lists as we add implementations + if (config->symbols) { + for (int i = 0; i < config->symbols->length; ++i) { + free_sway_variable(config->symbols->items[i]); + } + list_free(config->symbols); + } + if (config->modes) { + for (int i = 0; i < config->modes->length; ++i) { + free_mode(config->modes->items[i]); + } + list_free(config->modes); + } + if (config->bars) { + for (int i = 0; i < config->bars->length; ++i) { + free_bar_config(config->bars->items[i]); + } + list_free(config->bars); + } + list_free(config->cmd_queue); + if (config->workspace_configs) { + for (int i = 0; i < config->workspace_configs->length; i++) { + free_workspace_config(config->workspace_configs->items[i]); + } + list_free(config->workspace_configs); + } + if (config->output_configs) { + for (int i = 0; i < config->output_configs->length; i++) { + free_output_config(config->output_configs->items[i]); + } + list_free(config->output_configs); + } + if (config->swaybg_client != NULL) { + wl_client_destroy(config->swaybg_client); + } + if (config->input_configs) { + for (int i = 0; i < config->input_configs->length; i++) { + free_input_config(config->input_configs->items[i]); + } + list_free(config->input_configs); + } + if (config->input_type_configs) { + for (int i = 0; i < config->input_type_configs->length; i++) { + free_input_config(config->input_type_configs->items[i]); + } + list_free(config->input_type_configs); + } + if (config->seat_configs) { + for (int i = 0; i < config->seat_configs->length; i++) { + free_seat_config(config->seat_configs->items[i]); + } + list_free(config->seat_configs); + } + if (config->criteria) { + for (int i = 0; i < config->criteria->length; ++i) { + criteria_destroy(config->criteria->items[i]); + } + list_free(config->criteria); + } + if (config->layer_criteria) { + for (int i = 0; i < config->layer_criteria->length; ++i) { + layer_criteria_destroy(config->layer_criteria->items[i]); + } + list_free(config->layer_criteria); + } + list_free(config->no_focus); + list_free(config->active_bar_modifiers); + list_free_items_and_destroy(config->config_chain); + free(config->floating_scroll_up_cmd); + free(config->floating_scroll_down_cmd); + free(config->floating_scroll_left_cmd); + free(config->floating_scroll_right_cmd); + free(config->font); + free(config->swaybg_command); + free(config->swaynag_command); + free((char *)config->current_config_path); + free((char *)config->current_config); + keysym_translation_state_destroy(config->keysym_translation_state); + free(config); +} + +static void destroy_removed_seats(struct sway_config *old_config, + struct sway_config *new_config) { + struct seat_config *seat_config; + struct sway_seat *seat; + int i; + for (i = 0; i < old_config->seat_configs->length; i++) { + seat_config = old_config->seat_configs->items[i]; + // Skip the wildcard seat config, it won't have a matching real seat. + if (strcmp(seat_config->name, "*") == 0) { + continue; + } + + /* Also destroy seats that aren't present in new config */ + if (new_config && list_seq_find(new_config->seat_configs, + seat_name_cmp, seat_config->name) < 0) { + seat = input_manager_get_seat(seat_config->name, false); + if (seat) { + seat_destroy(seat); + } + } + } +} + +static void config_defaults(struct sway_config *config) { + if (!(config->swaynag_command = strdup("swaynag"))) goto cleanup; + config->swaynag_config_errors = (struct swaynag_instance){0}; + config->swaynag_config_errors.args = "--type error " + "--message 'There are errors in your config file' " + "--detailed-message " + "--button-no-terminal 'Exit sway' 'swaymsg exit' " + "--button-no-terminal 'Reload sway' 'swaymsg reload'"; + config->swaynag_config_errors.detailed = true; + + if (!(config->symbols = create_list())) goto cleanup; + if (!(config->modes = create_list())) goto cleanup; + if (!(config->bars = create_list())) goto cleanup; + if (!(config->workspace_configs = create_list())) goto cleanup; + if (!(config->criteria = create_list())) goto cleanup; + if (!(config->no_focus = create_list())) goto cleanup; + if (!(config->seat_configs = create_list())) goto cleanup; + if (!(config->output_configs = create_list())) goto cleanup; + + if (!(config->input_type_configs = create_list())) goto cleanup; + if (!(config->input_configs = create_list())) goto cleanup; + + if (!(config->cmd_queue = create_list())) goto cleanup; + + if (!(config->current_mode = malloc(sizeof(struct sway_mode)))) + goto cleanup; + if (!(config->current_mode->name = malloc(sizeof("default")))) goto cleanup; + strcpy(config->current_mode->name, "default"); + if (!(config->current_mode->keysym_bindings = create_list())) goto cleanup; + if (!(config->current_mode->keycode_bindings = create_list())) goto cleanup; + if (!(config->current_mode->mouse_bindings = create_list())) goto cleanup; + if (!(config->current_mode->switch_bindings = create_list())) goto cleanup; + if (!(config->current_mode->gesture_bindings = create_list())) goto cleanup; + list_add(config->modes, config->current_mode); + + config->floating_mod = 0; + config->floating_mod_inverse = false; + config->dragging_key = BTN_LEFT; + config->resizing_key = BTN_RIGHT; + + if (!(config->floating_scroll_up_cmd = strdup(""))) goto cleanup; + if (!(config->floating_scroll_down_cmd = strdup(""))) goto cleanup; + if (!(config->floating_scroll_left_cmd = strdup(""))) goto cleanup; + if (!(config->floating_scroll_right_cmd = strdup(""))) goto cleanup; + config->default_layout = L_NONE; + config->default_orientation = L_NONE; + if (!(config->font = strdup("monospace 10"))) goto cleanup; + config->font_description = pango_font_description_from_string(config->font); + config->urgent_timeout = 500; + config->focus_on_window_activation = FOWA_URGENT; + config->popup_during_fullscreen = POPUP_SMART; + config->xwayland = XWAYLAND_MODE_LAZY; + + config->titlebar_border_thickness = 1; + config->titlebar_h_padding = 5; + config->titlebar_v_padding = 4; + + // floating view + config->floating_maximum_width = 0; + config->floating_maximum_height = 0; + config->floating_minimum_width = 75; + config->floating_minimum_height = 50; + + // Flags + config->focus_follows_mouse = FOLLOWS_YES; + config->mouse_warping = WARP_OUTPUT; + config->focus_wrapping = WRAP_YES; + config->validating = false; + config->reloading = false; + config->active = false; + config->failed = false; + config->auto_back_and_forth = false; + config->reading = false; + config->show_marks = true; + config->title_align = ALIGN_LEFT; + config->tiling_drag = true; + config->tiling_drag_threshold = 9; + config->primary_selection = true; + + config->smart_gaps = SMART_GAPS_OFF; + config->gaps_inner = 0; + config->gaps_outer.top = 0; + config->gaps_outer.right = 0; + config->gaps_outer.bottom = 0; + config->gaps_outer.left = 0; + + if (!(config->active_bar_modifiers = create_list())) goto cleanup; + + if (!(config->swaybg_command = strdup("swaybg"))) goto cleanup; + + if (!(config->config_chain = create_list())) goto cleanup; + config->current_config_path = NULL; + config->current_config = NULL; + + // borders + config->border = B_NORMAL; + config->floating_border = B_NORMAL; + config->border_thickness = 2; + config->floating_border_thickness = 2; + config->hide_edge_borders = E_NONE; + config->hide_edge_borders_smart = ESMART_OFF; + config->hide_lone_tab = false; + + config->has_focused_tab_title = false; + + // border colors + color_to_rgba(config->border_colors.focused.border, 0x4C7899FF); + color_to_rgba(config->border_colors.focused.background, 0x285577FF); + color_to_rgba(config->border_colors.focused.text, 0xFFFFFFFF); + color_to_rgba(config->border_colors.focused.indicator, 0x2E9EF4FF); + color_to_rgba(config->border_colors.focused.child_border, 0x285577FF); + + color_to_rgba(config->border_colors.focused_inactive.border, 0x333333FF); + color_to_rgba(config->border_colors.focused_inactive.background, 0x5F676AFF); + color_to_rgba(config->border_colors.focused_inactive.text, 0xFFFFFFFF); + color_to_rgba(config->border_colors.focused_inactive.indicator, 0x484E50FF); + color_to_rgba(config->border_colors.focused_inactive.child_border, 0x5F676AFF); + + color_to_rgba(config->border_colors.unfocused.border, 0x333333FF); + color_to_rgba(config->border_colors.unfocused.background, 0x222222FF); + color_to_rgba(config->border_colors.unfocused.text, 0x888888FF); + color_to_rgba(config->border_colors.unfocused.indicator, 0x292D2EFF); + color_to_rgba(config->border_colors.unfocused.child_border, 0x222222FF); + + color_to_rgba(config->border_colors.urgent.border, 0x2F343AFF); + color_to_rgba(config->border_colors.urgent.background, 0x900000FF); + color_to_rgba(config->border_colors.urgent.text, 0xFFFFFFFF); + color_to_rgba(config->border_colors.urgent.indicator, 0x900000FF); + color_to_rgba(config->border_colors.urgent.child_border, 0x900000FF); + + color_to_rgba(config->border_colors.placeholder.border, 0x000000FF); + color_to_rgba(config->border_colors.placeholder.background, 0x0C0C0CFF); + color_to_rgba(config->border_colors.placeholder.text, 0xFFFFFFFF); + color_to_rgba(config->border_colors.placeholder.indicator, 0x000000FF); + color_to_rgba(config->border_colors.placeholder.child_border, 0x0C0C0CFF); + + color_to_rgba(config->border_colors.background, 0xFFFFFFFF); + + // SwayFX defaults + config->animation_duration_ms = 0.0f; + + config->corner_radius = 0; + config->smart_corner_radius = false; + + config->default_dim_inactive = 0.0f; + color_to_rgba(config->dim_inactive_colors.unfocused, 0x000000FF); + color_to_rgba(config->dim_inactive_colors.urgent, 0x900000FF); + + config->blur_enabled = false; + config->blur_xray = false; + config->blur_data = blur_data_get_default(); + + config->shadow_enabled = false; + config->shadows_on_csd_enabled = false; + config->shadow_blur_sigma = 20.0f; + config->shadow_offset_x = 0.0f; + config->shadow_offset_y = 0.0f; + color_to_rgba(config->shadow_color, 0x0000007F); + color_to_rgba(config->shadow_inactive_color, 0x0000007F); + + config->titlebar_separator = true; + config->scratchpad_minimize = false; + + if (!(config->layer_criteria = create_list())) { + goto cleanup; + } + + // The keysym to keycode translation + struct xkb_rule_names rules = {0}; + config->keysym_translation_state = keysym_translation_state_create(rules, 0); + if (config->keysym_translation_state == NULL) { + config->keysym_translation_state = keysym_translation_state_create(rules, + XKB_CONTEXT_NO_ENVIRONMENT_NAMES); + } + if (config->keysym_translation_state == NULL) { + goto cleanup; + } + + return; +cleanup: + sway_abort("Unable to allocate config structures"); +} + +static bool file_exists(const char *path) { + return path && access(path, R_OK) != -1; +} + +static char *config_path(const char *prefix, const char *config_folder) { + if (!prefix || !prefix[0] || !config_folder || !config_folder[0]) { + return NULL; + } + return format_str("%s/%s/config", prefix, config_folder); +} + +static char *get_config_path(void) { + char *path = NULL; + const char *home = getenv("HOME"); + char *config_home_fallback = NULL; + + const char *config_home = getenv("XDG_CONFIG_HOME"); + if ((config_home == NULL || config_home[0] == '\0') && home != NULL) { + config_home_fallback = format_str("%s/.config", home); + config_home = config_home_fallback; + } + + struct config_path { + const char *prefix; + const char *config_folder; + }; + + struct config_path config_paths[] = { + { .prefix = home, .config_folder = ".sway"}, + { .prefix = config_home, .config_folder = "sway"}, + { .prefix = home, .config_folder = ".i3"}, + { .prefix = config_home, .config_folder = "i3"}, + { .prefix = SYSCONFDIR, .config_folder = "sway"}, + { .prefix = SYSCONFDIR, .config_folder = "i3"} + }; + + size_t num_config_paths = sizeof(config_paths)/sizeof(config_paths[0]); + for (size_t i = 0; i < num_config_paths; i++) { + path = config_path(config_paths[i].prefix, config_paths[i].config_folder); + if (!path) { + continue; + } + if (file_exists(path)) { + break; + } + free(path); + path = NULL; + } + + free(config_home_fallback); + return path; +} + +static bool load_config(const char *path, struct sway_config *config, + struct swaynag_instance *swaynag) { + if (path == NULL) { + sway_log(SWAY_ERROR, "Unable to find a config file!"); + return false; + } + + sway_log(SWAY_INFO, "Loading config from %s", path); + + struct stat sb; + if (stat(path, &sb) == 0 && S_ISDIR(sb.st_mode)) { + sway_log(SWAY_ERROR, "%s is a directory not a config file", path); + return false; + } + + FILE *f = fopen(path, "r"); + if (!f) { + sway_log(SWAY_ERROR, "Unable to open %s for reading", path); + return false; + } + + bool config_load_success = read_config(f, config, swaynag); + fclose(f); + + if (!config_load_success) { + sway_log(SWAY_ERROR, "Error(s) loading config!"); + } + + return config->active || !config->validating || config_load_success; +} + +bool load_main_config(const char *file, bool is_active, bool validating) { + char *path; + if (file != NULL) { + path = strdup(file); + } else { + path = get_config_path(); + } + if (path == NULL) { + sway_log(SWAY_ERROR, "Cannot find config file"); + return false; + } + + char *real_path = realpath(path, NULL); + if (real_path == NULL) { + sway_log(SWAY_ERROR, "%s not found", path); + free(path); + return false; + } + + struct sway_config *old_config = config; + config = calloc(1, sizeof(struct sway_config)); + if (!config) { + sway_abort("Unable to allocate config"); + } + + config_defaults(config); + config->validating = validating; + if (is_active) { + sway_log(SWAY_DEBUG, "Performing configuration file %s", + validating ? "validation" : "reload"); + config->reloading = true; + config->active = true; + + // xwayland can only be enabled/disabled at launch + sway_log(SWAY_DEBUG, "xwayland will remain %s", + old_config->xwayland ? "enabled" : "disabled"); + config->xwayland = old_config->xwayland; + + // primary_selection can only be enabled/disabled at launch + sway_log(SWAY_DEBUG, "primary_selection will remain %s", + old_config->primary_selection ? "enabled" : "disabled"); + config->primary_selection = old_config->primary_selection; + + if (!config->validating) { + if (old_config->swaybg_client != NULL) { + wl_client_destroy(old_config->swaybg_client); + } + + if (old_config->swaynag_config_errors.client != NULL) { + wl_client_destroy(old_config->swaynag_config_errors.client); + } + + input_manager_reset_all_inputs(); + } + } + + config->user_config_path = file ? true : false; + config->current_config_path = path; + list_add(config->config_chain, real_path); + + config->reading = true; + + bool success = load_config(path, config, &config->swaynag_config_errors); + + if (validating) { + free_config(config); + config = old_config; + return success; + } + + // Only really necessary if not explicitly `font` is set in the config. + config_update_font_height(); + + if (!validating) { + input_manager_verify_fallback_seat(); + + for (int i = 0; i < config->input_configs->length; i++) { + input_manager_apply_input_config(config->input_configs->items[i]); + } + + for (int i = 0; i < config->input_type_configs->length; i++) { + input_manager_apply_input_config( + config->input_type_configs->items[i]); + } + + for (int i = 0; i < config->seat_configs->length; i++) { + input_manager_apply_seat_config(config->seat_configs->items[i]); + } + sway_switch_retrigger_bindings_for_all(); + + spawn_swaybg(); + + config->reloading = false; + if (is_active) { + request_modeset(); + if (config->swaynag_config_errors.client != NULL) { + swaynag_show(&config->swaynag_config_errors); + } + } + } + + if (old_config) { + destroy_removed_seats(old_config, config); + free_config(old_config); + } + config->reading = false; + return success; +} + +static bool load_include_config(const char *path, struct sway_config *config, + struct swaynag_instance *swaynag) { + // save parent config + const char *parent_config = config->current_config_path; + + char *real_path = realpath(path, NULL); + + if (real_path == NULL) { + sway_log(SWAY_DEBUG, "%s not found.", path); + return false; + } + + // check if config has already been included + int j; + for (j = 0; j < config->config_chain->length; ++j) { + char *old_path = config->config_chain->items[j]; + if (strcmp(real_path, old_path) == 0) { + sway_log(SWAY_DEBUG, + "%s already included once, won't be included again.", + real_path); + free(real_path); + return false; + } + } + + config->current_config_path = real_path; + list_add(config->config_chain, real_path); + int index = config->config_chain->length - 1; + + if (!load_config(real_path, config, swaynag)) { + free(real_path); + config->current_config_path = parent_config; + list_del(config->config_chain, index); + return false; + } + + // restore current_config_path + config->current_config_path = parent_config; + return true; +} + +void load_include_configs(const char *path, struct sway_config *config, + struct swaynag_instance *swaynag) { + char *wd = getcwd(NULL, 0); + char *parent_path = strdup(config->current_config_path); + const char *parent_dir = dirname(parent_path); + + if (chdir(parent_dir) < 0) { + sway_log(SWAY_ERROR, "failed to change working directory"); + goto cleanup; + } + + wordexp_t p; + if (wordexp(path, &p, 0) == 0) { + char **w = p.we_wordv; + size_t i; + for (i = 0; i < p.we_wordc; ++i) { + load_include_config(w[i], config, swaynag); + } + wordfree(&p); + } + + // Attempt to restore working directory before returning. + if (chdir(wd) < 0) { + sway_log(SWAY_ERROR, "failed to change working directory"); + } +cleanup: + free(parent_path); + free(wd); +} + +void run_deferred_commands(void) { + if (!config->cmd_queue->length) { + return; + } + sway_log(SWAY_DEBUG, "Running deferred commands"); + while (config->cmd_queue->length) { + char *line = config->cmd_queue->items[0]; + list_t *res_list = execute_command(line, NULL, NULL); + for (int i = 0; i < res_list->length; ++i) { + struct cmd_results *res = res_list->items[i]; + if (res->status != CMD_SUCCESS) { + sway_log(SWAY_ERROR, "Error on line '%s': %s", + line, res->error); + } + free_cmd_results(res); + } + list_del(config->cmd_queue, 0); + list_free(res_list); + free(line); + } +} + +void run_deferred_bindings(void) { + struct sway_seat *seat; + wl_list_for_each(seat, &(server.input->seats), link) { + if (!seat->deferred_bindings->length) { + continue; + } + sway_log(SWAY_DEBUG, "Running deferred bindings for seat %s", + seat->wlr_seat->name); + while (seat->deferred_bindings->length) { + struct sway_binding *binding = seat->deferred_bindings->items[0]; + seat_execute_command(seat, binding); + list_del(seat->deferred_bindings, 0); + free_sway_binding(binding); + } + } +} + +// get line, with backslash continuation +static ssize_t getline_with_cont(char **lineptr, size_t *line_size, FILE *file, + int *nlines) { + char *next_line = NULL; + size_t next_line_size = 0; + ssize_t nread = getline(lineptr, line_size, file); + *nlines = nread == -1 ? 0 : 1; + while (nread >= 2 && strcmp(&(*lineptr)[nread - 2], "\\\n") == 0 && (*lineptr)[0] != '#') { + ssize_t next_nread = getline(&next_line, &next_line_size, file); + if (next_nread == -1) { + break; + } + (*nlines)++; + + nread += next_nread - 2; + if ((ssize_t) *line_size < nread + 1) { + *line_size = nread + 1; + char *old_ptr = *lineptr; + *lineptr = realloc(*lineptr, *line_size); + if (!*lineptr) { + free(old_ptr); + nread = -1; + break; + } + } + strcpy(&(*lineptr)[nread - next_nread], next_line); + } + free(next_line); + return nread; +} + +static int detect_brace(FILE *file) { + int ret = 0; + int lines = 0; + long pos = ftell(file); + char *line = NULL; + size_t line_size = 0; + while ((getline(&line, &line_size, file)) != -1) { + lines++; + strip_whitespace(line); + if (*line) { + if (strcmp(line, "{") == 0) { + ret = lines; + } + break; + } + } + free(line); + if (ret == 0) { + fseek(file, pos, SEEK_SET); + } + return ret; +} + +static char *expand_line(const char *block, const char *line, bool add_brace) { + int size = (block ? strlen(block) + 1 : 0) + strlen(line) + + (add_brace ? 2 : 0) + 1; + char *expanded = calloc(1, size); + if (!expanded) { + sway_log(SWAY_ERROR, "Cannot allocate expanded line buffer"); + return NULL; + } + snprintf(expanded, size, "%s%s%s%s", block ? block : "", + block ? " " : "", line, add_brace ? " {" : ""); + return expanded; +} + +bool read_config(FILE *file, struct sway_config *config, + struct swaynag_instance *swaynag) { + bool reading_main_config = false; + char *this_config = NULL; + size_t config_size = 0; + if (config->current_config == NULL) { + reading_main_config = true; + + int ret_seek = fseek(file, 0, SEEK_END); + long ret_tell = ftell(file); + if (ret_seek == -1 || ret_tell == -1) { + sway_log(SWAY_ERROR, "Unable to get size of config file"); + return false; + } + config_size = ret_tell; + rewind(file); + + config->current_config = this_config = calloc(1, config_size + 1); + if (this_config == NULL) { + sway_log(SWAY_ERROR, "Unable to allocate buffer for config contents"); + return false; + } + } + + bool success = true; + int line_number = 0; + char *line = NULL; + size_t line_size = 0; + ssize_t nread; + list_t *stack = create_list(); + size_t read = 0; + int nlines = 0; + while ((nread = getline_with_cont(&line, &line_size, file, &nlines)) != -1) { + if (reading_main_config) { + if (read + nread > config_size) { + sway_log(SWAY_ERROR, "Config file changed during reading"); + success = false; + break; + } + + strcpy(&this_config[read], line); + read += nread; + } + + if (line[nread - 1] == '\n') { + line[nread - 1] = '\0'; + } + + line_number += nlines; + sway_log(SWAY_DEBUG, "Read line %d: %s", line_number, line); + + strip_whitespace(line); + if (!*line || line[0] == '#') { + continue; + } + int brace_detected = 0; + if (line[strlen(line) - 1] != '{' && line[strlen(line) - 1] != '}') { + brace_detected = detect_brace(file); + if (brace_detected > 0) { + line_number += brace_detected; + sway_log(SWAY_DEBUG, "Detected open brace on line %d", line_number); + } + } + char *block = stack->length ? stack->items[0] : NULL; + char *expanded = expand_line(block, line, brace_detected > 0); + if (!expanded) { + success = false; + break; + } + config->current_config_line_number = line_number; + config->current_config_line = line; + struct cmd_results *res; + char *new_block = NULL; + if (block && strcmp(block, "") == 0) { + // Special case + res = config_commands_command(expanded); + } else { + res = config_command(expanded, &new_block); + } + switch(res->status) { + case CMD_FAILURE: + case CMD_INVALID: + sway_log(SWAY_ERROR, "Error on line %i '%s': %s (%s)", line_number, + line, res->error, config->current_config_path); + if (!config->validating) { + swaynag_log(config->swaynag_command, swaynag, + "Error on line %i (%s) '%s': %s", line_number, + config->current_config_path, line, res->error); + } + success = false; + break; + + case CMD_DEFER: + sway_log(SWAY_DEBUG, "Deferring command `%s'", line); + list_add(config->cmd_queue, strdup(expanded)); + break; + + case CMD_BLOCK_COMMANDS: + sway_log(SWAY_DEBUG, "Entering commands block"); + list_insert(stack, 0, ""); + break; + + case CMD_BLOCK: + sway_log(SWAY_DEBUG, "Entering block '%s'", new_block); + list_insert(stack, 0, strdup(new_block)); + if (strcmp(new_block, "bar") == 0) { + config->current_bar = NULL; + } + break; + + case CMD_BLOCK_END: + if (!block) { + sway_log(SWAY_DEBUG, "Unmatched '}' on line %i", line_number); + success = false; + break; + } + if (strcmp(block, "bar") == 0) { + config->current_bar = NULL; + } + + sway_log(SWAY_DEBUG, "Exiting block '%s'", block); + list_del(stack, 0); + free(block); + memset(&config->handler_context, 0, + sizeof(config->handler_context)); + default:; + } + free(new_block); + free(expanded); + free_cmd_results(res); + } + free(line); + list_free_items_and_destroy(stack); + config->current_config_line_number = 0; + config->current_config_line = NULL; + + return success; +} + +void config_add_swaynag_warning(char *fmt, ...) { + if (config->reading && !config->validating) { + va_list args; + va_start(args, fmt); + char *str = vformat_str(fmt, args); + va_end(args); + if (str == NULL) { + return; + } + + swaynag_log(config->swaynag_command, &config->swaynag_config_errors, + "Warning on line %i (%s) '%s': %s", + config->current_config_line_number, config->current_config_path, + config->current_config_line, str); + + free(str); + } +} + +char *do_var_replacement(char *str) { + int i; + char *find = str; + while ((find = strchr(find, '$'))) { + // Skip if escaped. + if (find > str && find[-1] == '\\') { + if (find == str + 1 || !(find > str + 1 && find[-2] == '\\')) { + ++find; + continue; + } + } + // Unescape double $ and move on + if (find[1] == '$') { + size_t length = strlen(find + 1); + memmove(find, find + 1, length); + find[length] = '\0'; + ++find; + continue; + } + // Find matching variable + for (i = 0; i < config->symbols->length; ++i) { + struct sway_variable *var = config->symbols->items[i]; + if (has_prefix(find, var->name)) { + int vnlen = strlen(var->name); + int vvlen = strlen(var->value); + char *newstr = malloc(strlen(str) - vnlen + vvlen + 1); + if (!newstr) { + sway_log(SWAY_ERROR, + "Unable to allocate replacement " + "during variable expansion"); + break; + } + char *newptr = newstr; + int offset = find - str; + strncpy(newptr, str, offset); + newptr += offset; + memcpy(newptr, var->value, vvlen); + newptr += vvlen; + strcpy(newptr, find + vnlen); + free(str); + str = newstr; + find = str + offset + vvlen; + break; + } + } + if (i == config->symbols->length) { + ++find; + } + } + return str; +} + +// the naming is intentional (albeit long): a workspace_output_cmp function +// would compare two structs in full, while this method only compares the +// workspace. +int workspace_output_cmp_workspace(const void *a, const void *b) { + const struct workspace_config *wsa = a, *wsb = b; + return lenient_strcmp(wsa->workspace, wsb->workspace); +} + + +void config_update_font_height(void) { + int prev_max_height = config->font_height; + + get_text_metrics(config->font_description, &config->font_height, &config->font_baseline); + + if (config->font_height != prev_max_height) { + arrange_root(); + } +} + +static void translate_binding_list(list_t *bindings, list_t *bindsyms, + list_t *bindcodes) { + for (int i = 0; i < bindings->length; ++i) { + struct sway_binding *binding = bindings->items[i]; + translate_binding(binding); + + switch (binding->type) { + case BINDING_KEYSYM: + binding_add_translated(binding, bindsyms); + break; + case BINDING_KEYCODE: + binding_add_translated(binding, bindcodes); + break; + default: + sway_assert(false, "unexpected translated binding type: %d", + binding->type); + break; + } + + } +} + +void translate_keysyms(struct input_config *input_config) { + keysym_translation_state_destroy(config->keysym_translation_state); + + struct xkb_rule_names rules = {0}; + input_config_fill_rule_names(input_config, &rules); + config->keysym_translation_state = keysym_translation_state_create(rules, 0); + if (config->keysym_translation_state == NULL) { + sway_log(SWAY_ERROR, "Failed to create keysym translation XKB state " + "for device '%s'", input_config->identifier); + return; + } + + for (int i = 0; i < config->modes->length; ++i) { + struct sway_mode *mode = config->modes->items[i]; + + list_t *bindsyms = create_list(); + list_t *bindcodes = create_list(); + + translate_binding_list(mode->keysym_bindings, bindsyms, bindcodes); + translate_binding_list(mode->keycode_bindings, bindsyms, bindcodes); + + list_free(mode->keysym_bindings); + list_free(mode->keycode_bindings); + + mode->keysym_bindings = bindsyms; + mode->keycode_bindings = bindcodes; + } + + sway_log(SWAY_DEBUG, "Translated keysyms using config for device '%s'", + input_config->identifier); +} diff --git a/sway/config/bar.c b/sway/config/bar.c new file mode 100644 index 0000000..f4efb27 --- /dev/null +++ b/sway/config/bar.c @@ -0,0 +1,254 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "sway/config.h" +#include "sway/input/keyboard.h" +#include "sway/output.h" +#include "sway/server.h" +#include "config.h" +#include "list.h" +#include "log.h" +#include "stringop.h" +#include "util.h" + +void free_bar_binding(struct bar_binding *binding) { + if (!binding) { + return; + } + free(binding->command); + free(binding); +} + +void free_bar_config(struct bar_config *bar) { + if (!bar) { + return; + } + free(bar->id); + free(bar->mode); + free(bar->position); + free(bar->hidden_state); + free(bar->status_command); + free(bar->swaybar_command); + free(bar->font); + free(bar->separator_symbol); + if (bar->bindings) { + for (int i = 0; i < bar->bindings->length; i++) { + free_bar_binding(bar->bindings->items[i]); + } + } + list_free(bar->bindings); + list_free_items_and_destroy(bar->outputs); + if (bar->client != NULL) { + wl_client_destroy(bar->client); + } + free(bar->colors.background); + free(bar->colors.statusline); + free(bar->colors.separator); + free(bar->colors.focused_background); + free(bar->colors.focused_statusline); + free(bar->colors.focused_separator); + free(bar->colors.focused_workspace_border); + free(bar->colors.focused_workspace_bg); + free(bar->colors.focused_workspace_text); + free(bar->colors.active_workspace_border); + free(bar->colors.active_workspace_bg); + free(bar->colors.active_workspace_text); + free(bar->colors.inactive_workspace_border); + free(bar->colors.inactive_workspace_bg); + free(bar->colors.inactive_workspace_text); + free(bar->colors.urgent_workspace_border); + free(bar->colors.urgent_workspace_bg); + free(bar->colors.urgent_workspace_text); + free(bar->colors.binding_mode_border); + free(bar->colors.binding_mode_bg); + free(bar->colors.binding_mode_text); +#if HAVE_TRAY + list_free_items_and_destroy(bar->tray_outputs); + free(bar->icon_theme); + + struct tray_binding *tray_bind = NULL, *tmp_tray_bind = NULL; + wl_list_for_each_safe(tray_bind, tmp_tray_bind, &bar->tray_bindings, link) { + wl_list_remove(&tray_bind->link); + free(tray_bind); + } +#endif + free(bar); +} + +struct bar_config *default_bar_config(void) { + struct bar_config *bar = NULL; + bar = calloc(1, sizeof(struct bar_config)); + if (!bar) { + return NULL; + } + bar->outputs = NULL; + bar->position = strdup("bottom"); + bar->pango_markup = PANGO_MARKUP_DEFAULT; + bar->swaybar_command = NULL; + bar->font = NULL; + bar->height = 0; + bar->workspace_buttons = true; + bar->wrap_scroll = false; + bar->separator_symbol = NULL; + bar->strip_workspace_numbers = false; + bar->strip_workspace_name = false; + bar->binding_mode_indicator = true; + bar->verbose = false; + bar->modifier = get_modifier_mask_by_name("Mod4"); + bar->status_padding = 1; + bar->status_edge_padding = 3; + bar->workspace_min_width = 0; + if (!(bar->mode = strdup("dock"))) { + goto cleanup; + } + if (!(bar->hidden_state = strdup("hide"))) { + goto cleanup; + } + if (!(bar->bindings = create_list())) { + goto cleanup; + } + // set default colors + if (!(bar->colors.background = strndup("#000000ff", 9))) { + goto cleanup; + } + if (!(bar->colors.statusline = strndup("#ffffffff", 9))) { + goto cleanup; + } + if (!(bar->colors.separator = strndup("#666666ff", 9))) { + goto cleanup; + } + if (!(bar->colors.focused_workspace_border = strndup("#4c7899ff", 9))) { + goto cleanup; + } + if (!(bar->colors.focused_workspace_bg = strndup("#285577ff", 9))) { + goto cleanup; + } + if (!(bar->colors.focused_workspace_text = strndup("#ffffffff", 9))) { + goto cleanup; + } + if (!(bar->colors.active_workspace_border = strndup("#333333ff", 9))) { + goto cleanup; + } + if (!(bar->colors.active_workspace_bg = strndup("#5f676aff", 9))) { + goto cleanup; + } + if (!(bar->colors.active_workspace_text = strndup("#ffffffff", 9))) { + goto cleanup; + } + if (!(bar->colors.inactive_workspace_border = strndup("#333333ff", 9))) { + goto cleanup; + } + if (!(bar->colors.inactive_workspace_bg = strndup("#222222ff", 9))) { + goto cleanup; + } + if (!(bar->colors.inactive_workspace_text = strndup("#888888ff", 9))) { + goto cleanup; + } + if (!(bar->colors.urgent_workspace_border = strndup("#2f343aff", 9))) { + goto cleanup; + } + if (!(bar->colors.urgent_workspace_bg = strndup("#900000ff", 9))) { + goto cleanup; + } + if (!(bar->colors.urgent_workspace_text = strndup("#ffffffff", 9))) { + goto cleanup; + } + // if the following colors stay undefined, they fall back to background, + // statusline, separator and urgent_workspace_*. + bar->colors.focused_background = NULL; + bar->colors.focused_statusline = NULL; + bar->colors.focused_separator = NULL; + bar->colors.binding_mode_border = NULL; + bar->colors.binding_mode_bg = NULL; + bar->colors.binding_mode_text = NULL; + +#if HAVE_TRAY + bar->tray_padding = 2; + wl_list_init(&bar->tray_bindings); +#endif + + return bar; +cleanup: + free_bar_config(bar); + return NULL; +} + +static void handle_swaybar_client_destroy(struct wl_listener *listener, + void *data) { + struct bar_config *bar = wl_container_of(listener, bar, client_destroy); + wl_list_remove(&bar->client_destroy.link); + wl_list_init(&bar->client_destroy.link); + bar->client = NULL; +} + +static void invoke_swaybar(struct bar_config *bar) { + int sockets[2]; + if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) != 0) { + sway_log_errno(SWAY_ERROR, "socketpair failed"); + return; + } + if (!sway_set_cloexec(sockets[0], true) || !sway_set_cloexec(sockets[1], true)) { + return; + } + + bar->client = wl_client_create(server.wl_display, sockets[0]); + if (bar->client == NULL) { + sway_log_errno(SWAY_ERROR, "wl_client_create failed"); + return; + } + + bar->client_destroy.notify = handle_swaybar_client_destroy; + wl_client_add_destroy_listener(bar->client, &bar->client_destroy); + + pid_t pid = fork(); + if (pid < 0) { + sway_log(SWAY_ERROR, "Failed to create fork for swaybar"); + return; + } else if (pid == 0) { + if (!sway_set_cloexec(sockets[1], false)) { + _exit(EXIT_FAILURE); + } + + char wayland_socket_str[16]; + snprintf(wayland_socket_str, sizeof(wayland_socket_str), + "%d", sockets[1]); + setenv("WAYLAND_SOCKET", wayland_socket_str, true); + + // run custom swaybar + char *const cmd[] = { + bar->swaybar_command ? bar->swaybar_command : "swaybar", + "-b", bar->id, NULL}; + execvp(cmd[0], cmd); + _exit(EXIT_FAILURE); + } + + if (close(sockets[1]) != 0) { + sway_log_errno(SWAY_ERROR, "close failed"); + return; + } + + sway_log(SWAY_DEBUG, "Spawned swaybar %s", bar->id); +} + +void load_swaybar(struct bar_config *bar) { + if (bar->client != NULL) { + wl_client_destroy(bar->client); + } + sway_log(SWAY_DEBUG, "Invoking swaybar for bar id '%s'", bar->id); + invoke_swaybar(bar); +} + +void load_swaybars(void) { + for (int i = 0; i < config->bars->length; ++i) { + struct bar_config *bar = config->bars->items[i]; + load_swaybar(bar); + } +} diff --git a/sway/config/input.c b/sway/config/input.c new file mode 100644 index 0000000..1fb737b --- /dev/null +++ b/sway/config/input.c @@ -0,0 +1,402 @@ +#include +#include +#include +#include "sway/config.h" +#include "sway/input/keyboard.h" +#include "sway/server.h" +#include "log.h" + +struct input_config *new_input_config(const char* identifier) { + struct input_config *input = calloc(1, sizeof(struct input_config)); + if (!input) { + sway_log(SWAY_DEBUG, "Unable to allocate input config"); + return NULL; + } + sway_log(SWAY_DEBUG, "new_input_config(%s)", identifier); + if (!(input->identifier = strdup(identifier))) { + free(input); + sway_log(SWAY_DEBUG, "Unable to allocate input config"); + return NULL; + } + + input->input_type = NULL; + input->tap = INT_MIN; + input->tap_button_map = INT_MIN; + input->drag = INT_MIN; + input->drag_lock = INT_MIN; + input->dwt = INT_MIN; + input->dwtp = INT_MIN; + input->send_events = INT_MIN; + input->click_method = INT_MIN; + input->clickfinger_button_map = INT_MIN; + input->middle_emulation = INT_MIN; + input->natural_scroll = INT_MIN; + input->accel_profile = INT_MIN; + input->rotation_angle = FLT_MIN; + input->pointer_accel = FLT_MIN; + input->scroll_factor = FLT_MIN; + input->scroll_button = INT_MIN; + input->scroll_button_lock = INT_MIN; + input->scroll_method = INT_MIN; + input->left_handed = INT_MIN; + input->repeat_delay = INT_MIN; + input->repeat_rate = INT_MIN; + input->xkb_numlock = INT_MIN; + input->xkb_capslock = INT_MIN; + input->xkb_file_is_set = false; + input->tools = create_list(); + + return input; +} + +void merge_input_config(struct input_config *dst, struct input_config *src) { + if (src->accel_profile != INT_MIN) { + dst->accel_profile = src->accel_profile; + } + if (src->click_method != INT_MIN) { + dst->click_method = src->click_method; + } + if (src->clickfinger_button_map != INT_MIN) { + dst->clickfinger_button_map = src->clickfinger_button_map; + } + if (src->drag != INT_MIN) { + dst->drag = src->drag; + } + if (src->drag_lock != INT_MIN) { + dst->drag_lock = src->drag_lock; + } + if (src->dwt != INT_MIN) { + dst->dwt = src->dwt; + } + if (src->dwtp != INT_MIN) { + dst->dwtp = src->dwtp; + } + if (src->left_handed != INT_MIN) { + dst->left_handed = src->left_handed; + } + if (src->middle_emulation != INT_MIN) { + dst->middle_emulation = src->middle_emulation; + } + if (src->natural_scroll != INT_MIN) { + dst->natural_scroll = src->natural_scroll; + } + if (src->rotation_angle != FLT_MIN) { + dst->rotation_angle = src->rotation_angle; + } + if (src->pointer_accel != FLT_MIN) { + dst->pointer_accel = src->pointer_accel; + } + if (src->scroll_factor != FLT_MIN) { + dst->scroll_factor = src->scroll_factor; + } + if (src->repeat_delay != INT_MIN) { + dst->repeat_delay = src->repeat_delay; + } + if (src->repeat_rate != INT_MIN) { + dst->repeat_rate = src->repeat_rate; + } + if (src->scroll_method != INT_MIN) { + dst->scroll_method = src->scroll_method; + } + if (src->scroll_button != INT_MIN) { + dst->scroll_button = src->scroll_button; + } + if (src->scroll_button_lock != INT_MIN) { + dst->scroll_button_lock = src->scroll_button_lock; + } + if (src->send_events != INT_MIN) { + dst->send_events = src->send_events; + } + if (src->tap != INT_MIN) { + dst->tap = src->tap; + } + if (src->tap_button_map != INT_MIN) { + dst->tap_button_map = src->tap_button_map; + } + if (src->xkb_file_is_set) { + free(dst->xkb_file); + dst->xkb_file = src->xkb_file ? strdup(src->xkb_file) : NULL; + dst->xkb_file_is_set = dst->xkb_file != NULL; + } + if (src->xkb_layout) { + free(dst->xkb_layout); + dst->xkb_layout = strdup(src->xkb_layout); + } + if (src->xkb_model) { + free(dst->xkb_model); + dst->xkb_model = strdup(src->xkb_model); + } + if (src->xkb_options) { + free(dst->xkb_options); + dst->xkb_options = strdup(src->xkb_options); + } + if (src->xkb_rules) { + free(dst->xkb_rules); + dst->xkb_rules = strdup(src->xkb_rules); + } + if (src->xkb_variant) { + free(dst->xkb_variant); + dst->xkb_variant = strdup(src->xkb_variant); + } + if (src->xkb_numlock != INT_MIN) { + dst->xkb_numlock = src->xkb_numlock; + } + if (src->xkb_capslock != INT_MIN) { + dst->xkb_capslock = src->xkb_capslock; + } + if (src->mapped_from_region) { + free(dst->mapped_from_region); + dst->mapped_from_region = + malloc(sizeof(struct input_config_mapped_from_region)); + memcpy(dst->mapped_from_region, src->mapped_from_region, + sizeof(struct input_config_mapped_from_region)); + } + if (src->mapped_to) { + dst->mapped_to = src->mapped_to; + } + if (src->mapped_to_output) { + free(dst->mapped_to_output); + dst->mapped_to_output = strdup(src->mapped_to_output); + } + if (src->mapped_to_region) { + free(dst->mapped_to_region); + dst->mapped_to_region = + malloc(sizeof(struct wlr_box)); + memcpy(dst->mapped_to_region, src->mapped_to_region, + sizeof(struct wlr_box)); + } + if (src->calibration_matrix.configured) { + dst->calibration_matrix.configured = src->calibration_matrix.configured; + memcpy(dst->calibration_matrix.matrix, src->calibration_matrix.matrix, + sizeof(src->calibration_matrix.matrix)); + } + for (int i = 0; i < src->tools->length; i++) { + struct input_config_tool *src_tool = src->tools->items[i]; + for (int j = 0; j < dst->tools->length; j++) { + struct input_config_tool *dst_tool = dst->tools->items[j]; + if (src_tool->type == dst_tool->type) { + dst_tool->mode = src_tool->mode; + goto tool_merge_outer; + } + } + + struct input_config_tool *dst_tool = malloc(sizeof(*dst_tool)); + memcpy(dst_tool, src_tool, sizeof(*dst_tool)); + list_add(dst->tools, dst_tool); + + tool_merge_outer:; + } +} + +static bool validate_xkb_merge(struct input_config *dest, + struct input_config *src, char **xkb_error) { + struct input_config *temp = new_input_config("temp"); + if (dest) { + merge_input_config(temp, dest); + } + merge_input_config(temp, src); + + struct xkb_keymap *keymap = sway_keyboard_compile_keymap(temp, xkb_error); + free_input_config(temp); + if (!keymap) { + return false; + } + + xkb_keymap_unref(keymap); + return true; +} + +static bool validate_wildcard_on_all(struct input_config *wildcard, + char **error) { + for (int i = 0; i < config->input_configs->length; i++) { + struct input_config *ic = config->input_configs->items[i]; + if (strcmp(wildcard->identifier, ic->identifier) != 0) { + sway_log(SWAY_DEBUG, "Validating xkb merge of * on %s", + ic->identifier); + if (!validate_xkb_merge(ic, wildcard, error)) { + return false; + } + } + } + + for (int i = 0; i < config->input_type_configs->length; i++) { + struct input_config *ic = config->input_type_configs->items[i]; + sway_log(SWAY_DEBUG, "Validating xkb merge of * config on %s", + ic->identifier); + if (!validate_xkb_merge(ic, wildcard, error)) { + return false; + } + } + + return true; +} + +static void merge_wildcard_on_all(struct input_config *wildcard) { + for (int i = 0; i < config->input_configs->length; i++) { + struct input_config *ic = config->input_configs->items[i]; + if (strcmp(wildcard->identifier, ic->identifier) != 0) { + sway_log(SWAY_DEBUG, "Merging input * config on %s", ic->identifier); + merge_input_config(ic, wildcard); + } + } + + for (int i = 0; i < config->input_type_configs->length; i++) { + struct input_config *ic = config->input_type_configs->items[i]; + sway_log(SWAY_DEBUG, "Merging input * config on %s", ic->identifier); + merge_input_config(ic, wildcard); + } +} + +static bool validate_type_on_existing(struct input_config *type_wildcard, + char **error) { + for (int i = 0; i < config->input_configs->length; i++) { + struct input_config *ic = config->input_configs->items[i]; + if (ic->input_type == NULL) { + continue; + } + + if (strcmp(ic->input_type, type_wildcard->identifier + 5) == 0) { + sway_log(SWAY_DEBUG, "Validating merge of %s on %s", + type_wildcard->identifier, ic->identifier); + if (!validate_xkb_merge(ic, type_wildcard, error)) { + return false; + } + } + } + return true; +} + +static void merge_type_on_existing(struct input_config *type_wildcard) { + for (int i = 0; i < config->input_configs->length; i++) { + struct input_config *ic = config->input_configs->items[i]; + if (ic->input_type == NULL) { + continue; + } + + if (strcmp(ic->input_type, type_wildcard->identifier + 5) == 0) { + sway_log(SWAY_DEBUG, "Merging %s top of %s", + type_wildcard->identifier, + ic->identifier); + merge_input_config(ic, type_wildcard); + } + } +} + +static const char *set_input_type(struct input_config *ic) { + struct sway_input_device *input_device; + wl_list_for_each(input_device, &server.input->devices, link) { + if (strcmp(input_device->identifier, ic->identifier) == 0) { + ic->input_type = input_device_get_type(input_device); + break; + } + } + return ic->input_type; +} + +struct input_config *store_input_config(struct input_config *ic, + char **error) { + bool wildcard = strcmp(ic->identifier, "*") == 0; + if (wildcard && error && !validate_wildcard_on_all(ic, error)) { + return NULL; + } + + bool type = has_prefix(ic->identifier, "type:"); + if (type && error && !validate_type_on_existing(ic, error)) { + return NULL; + } + + list_t *config_list = type ? config->input_type_configs + : config->input_configs; + + struct input_config *current = NULL; + bool new_current = false; + + int i = list_seq_find(config_list, input_identifier_cmp, ic->identifier); + if (i >= 0) { + current = config_list->items[i]; + } + + if (!current && !wildcard && !type && set_input_type(ic)) { + for (i = 0; i < config->input_type_configs->length; i++) { + struct input_config *tc = config->input_type_configs->items[i]; + if (strcmp(ic->input_type, tc->identifier + 5) == 0) { + current = new_input_config(ic->identifier); + current->input_type = ic->input_type; + merge_input_config(current, tc); + new_current = true; + break; + } + } + } + + i = list_seq_find(config->input_configs, input_identifier_cmp, "*"); + if (!current && i >= 0) { + current = new_input_config(ic->identifier); + merge_input_config(current, config->input_configs->items[i]); + new_current = true; + } + + if (error && !validate_xkb_merge(current, ic, error)) { + if (new_current) { + free_input_config(current); + } + return NULL; + } + + if (wildcard) { + merge_wildcard_on_all(ic); + } + + if (type) { + merge_type_on_existing(ic); + } + + if (current) { + merge_input_config(current, ic); + free_input_config(ic); + ic = current; + } + + ic->xkb_file_is_set = ic->xkb_file != NULL; + + if (!current || new_current) { + list_add(config_list, ic); + } + + sway_log(SWAY_DEBUG, "Config stored for input %s", ic->identifier); + + return ic; +} + +void input_config_fill_rule_names(struct input_config *ic, + struct xkb_rule_names *rules) { + rules->layout = ic->xkb_layout; + rules->model = ic->xkb_model; + rules->options = ic->xkb_options; + rules->rules = ic->xkb_rules; + rules->variant = ic->xkb_variant; +} + +void free_input_config(struct input_config *ic) { + if (!ic) { + return; + } + free(ic->identifier); + free(ic->xkb_file); + free(ic->xkb_layout); + free(ic->xkb_model); + free(ic->xkb_options); + free(ic->xkb_rules); + free(ic->xkb_variant); + free(ic->mapped_from_region); + free(ic->mapped_to_output); + free(ic->mapped_to_region); + list_free_items_and_destroy(ic->tools); + free(ic); +} + +int input_identifier_cmp(const void *item, const void *data) { + const struct input_config *ic = item; + const char *identifier = data; + return strcmp(ic->identifier, identifier); +} diff --git a/sway/config/output.c b/sway/config/output.c new file mode 100644 index 0000000..c915a17 --- /dev/null +++ b/sway/config/output.c @@ -0,0 +1,1300 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "sway/config.h" +#include "sway/desktop/transaction.h" +#include "sway/input/cursor.h" +#include "sway/layers.h" +#include "sway/lock.h" +#include "sway/output.h" +#include "sway/server.h" +#include "sway/tree/arrange.h" +#include "sway/tree/root.h" +#include "log.h" +#include "util.h" + +#if WLR_HAS_DRM_BACKEND +#include +#endif + +void output_get_identifier(char *identifier, size_t len, + struct sway_output *output) { + struct wlr_output *wlr_output = output->wlr_output; + snprintf(identifier, len, "%s %s %s", + wlr_output->make ? wlr_output->make : "Unknown", + wlr_output->model ? wlr_output->model : "Unknown", + wlr_output->serial ? wlr_output->serial : "Unknown"); +} + +const char *sway_output_scale_filter_to_string(enum scale_filter_mode scale_filter) { + switch (scale_filter) { + case SCALE_FILTER_DEFAULT: + return "smart"; + case SCALE_FILTER_LINEAR: + return "linear"; + case SCALE_FILTER_NEAREST: + return "nearest"; + case SCALE_FILTER_SMART: + return "smart"; + } + sway_assert(false, "Unknown value for scale_filter."); + return NULL; +} + +struct output_config *new_output_config(const char *name) { + struct output_config *oc = calloc(1, sizeof(struct output_config)); + if (oc == NULL) { + return NULL; + } + oc->name = strdup(name); + if (oc->name == NULL) { + free(oc); + return NULL; + } + oc->enabled = -1; + oc->width = oc->height = -1; + oc->refresh_rate = -1; + oc->custom_mode = -1; + oc->drm_mode.type = -1; + oc->x = oc->y = INT_MAX; + oc->scale = -1; + oc->scale_filter = SCALE_FILTER_DEFAULT; + oc->transform = -1; + oc->subpixel = WL_OUTPUT_SUBPIXEL_UNKNOWN; + oc->max_render_time = -1; + oc->adaptive_sync = -1; + oc->render_bit_depth = RENDER_BIT_DEPTH_DEFAULT; + oc->color_profile = COLOR_PROFILE_DEFAULT; + oc->color_transform = NULL; + oc->power = -1; + oc->allow_tearing = -1; + oc->hdr = -1; + return oc; +} + +// supersede_output_config clears all fields in dst that were set in src +static void supersede_output_config(struct output_config *dst, struct output_config *src) { + if (src->enabled != -1) { + dst->enabled = -1; + } + if (src->width != -1) { + dst->width = -1; + } + if (src->height != -1) { + dst->height = -1; + } + if (src->x != INT_MAX) { + dst->x = INT_MAX; + } + if (src->y != INT_MAX) { + dst->y = INT_MAX; + } + if (src->scale != -1) { + dst->scale = -1; + } + if (src->scale_filter != SCALE_FILTER_DEFAULT) { + dst->scale_filter = SCALE_FILTER_DEFAULT; + } + if (src->subpixel != WL_OUTPUT_SUBPIXEL_UNKNOWN) { + dst->subpixel = WL_OUTPUT_SUBPIXEL_UNKNOWN; + } + if (src->refresh_rate != -1) { + dst->refresh_rate = -1; + } + if (src->custom_mode != -1) { + dst->custom_mode = -1; + } + if (src->drm_mode.type != (uint32_t) -1) { + dst->drm_mode.type = -1; + } + if (src->transform != -1) { + dst->transform = -1; + } + if (src->max_render_time != -1) { + dst->max_render_time = -1; + } + if (src->adaptive_sync != -1) { + dst->adaptive_sync = -1; + } + if (src->render_bit_depth != RENDER_BIT_DEPTH_DEFAULT) { + dst->render_bit_depth = RENDER_BIT_DEPTH_DEFAULT; + } + if (src->color_profile != COLOR_PROFILE_DEFAULT) { + if (dst->color_transform) { + wlr_color_transform_unref(dst->color_transform); + dst->color_transform = NULL; + } + dst->color_profile = COLOR_PROFILE_DEFAULT; + } + if (src->background) { + free(dst->background); + dst->background = NULL; + } + if (src->background_option) { + free(dst->background_option); + dst->background_option = NULL; + } + if (src->background_fallback) { + free(dst->background_fallback); + dst->background_fallback = NULL; + } + if (src->power != -1) { + dst->power = -1; + } + if (src->allow_tearing != -1) { + dst->allow_tearing = -1; + } + if (src->hdr != -1) { + dst->hdr = -1; + } +} + +// merge_output_config sets all fields in dst that were set in src +static void merge_output_config(struct output_config *dst, struct output_config *src) { + if (src->enabled != -1) { + dst->enabled = src->enabled; + } + if (src->width != -1) { + dst->width = src->width; + } + if (src->height != -1) { + dst->height = src->height; + } + if (src->x != INT_MAX) { + dst->x = src->x; + } + if (src->y != INT_MAX) { + dst->y = src->y; + } + if (src->scale != -1) { + dst->scale = src->scale; + } + if (src->scale_filter != SCALE_FILTER_DEFAULT) { + dst->scale_filter = src->scale_filter; + } + if (src->subpixel != WL_OUTPUT_SUBPIXEL_UNKNOWN) { + dst->subpixel = src->subpixel; + } + if (src->refresh_rate != -1) { + dst->refresh_rate = src->refresh_rate; + } + if (src->custom_mode != -1) { + dst->custom_mode = src->custom_mode; + } + if (src->drm_mode.type != (uint32_t) -1) { + memcpy(&dst->drm_mode, &src->drm_mode, sizeof(src->drm_mode)); + } + if (src->transform != -1) { + dst->transform = src->transform; + } + if (src->max_render_time != -1) { + dst->max_render_time = src->max_render_time; + } + if (src->adaptive_sync != -1) { + dst->adaptive_sync = src->adaptive_sync; + } + if (src->render_bit_depth != RENDER_BIT_DEPTH_DEFAULT) { + dst->render_bit_depth = src->render_bit_depth; + } + if (src->color_profile != COLOR_PROFILE_DEFAULT) { + if (src->color_transform) { + wlr_color_transform_ref(src->color_transform); + } + wlr_color_transform_unref(dst->color_transform); + dst->color_profile = src->color_profile; + dst->color_transform = src->color_transform; + } + if (src->background) { + free(dst->background); + dst->background = strdup(src->background); + } + if (src->background_option) { + free(dst->background_option); + dst->background_option = strdup(src->background_option); + } + if (src->background_fallback) { + free(dst->background_fallback); + dst->background_fallback = strdup(src->background_fallback); + } + if (src->power != -1) { + dst->power = src->power; + } + if (src->allow_tearing != -1) { + dst->allow_tearing = src->allow_tearing; + } + if (src->hdr != -1) { + dst->hdr = src->hdr; + } +} + +void store_output_config(struct output_config *oc) { + bool merged = false; + bool wildcard = strcmp(oc->name, "*") == 0; + struct sway_output *output = wildcard ? NULL : all_output_by_name_or_id(oc->name); + + char id[128]; + if (output) { + output_get_identifier(id, sizeof(id), output); + } + + for (int i = 0; i < config->output_configs->length; i++) { + struct output_config *old = config->output_configs->items[i]; + + // If the old config matches the new config's name, regardless of + // whether it was name or identifier, merge on top of the existing + // config. If the new config is a wildcard, this also merges on top of + // old wildcard configs. + if (strcmp(old->name, oc->name) == 0) { + merge_output_config(old, oc); + merged = true; + continue; + } + + // If the new config is a wildcard config we supersede all non-wildcard + // configs. Old wildcard configs have already been handled above. + if (wildcard) { + supersede_output_config(old, oc); + continue; + } + + // If the new config matches an output's name, and the old config + // matches on that output's identifier, supersede it. + if (output && strcmp(old->name, id) == 0 && + strcmp(oc->name, output->wlr_output->name) == 0) { + supersede_output_config(old, oc); + } + } + + sway_log(SWAY_DEBUG, "Config stored for output %s (enabled: %d) (%dx%d@%fHz " + "position %d,%d scale %f subpixel %s transform %d) (bg %s %s) (power %d) " + "(max render time: %d) (allow tearing: %d) (hdr: %d)", + oc->name, oc->enabled, oc->width, oc->height, oc->refresh_rate, + oc->x, oc->y, oc->scale, sway_wl_output_subpixel_to_string(oc->subpixel), + oc->transform, oc->background, oc->background_option, oc->power, + oc->max_render_time, oc->allow_tearing, oc->hdr); + + // If the configuration was not merged into an existing configuration, add + // it to the list. Otherwise we're done with it and can free it. + if (!merged) { + list_add(config->output_configs, oc); + } else { + free_output_config(oc); + } +} + +static void set_mode(struct wlr_output *output, struct wlr_output_state *pending, + int width, int height, float refresh_rate, bool custom) { + // Not all floating point integers can be represented exactly + // as (int)(1000 * mHz / 1000.f) + // round() the result to avoid any error + int mhz = (int)roundf(refresh_rate * 1000); + // If no target refresh rate is given, match highest available + mhz = mhz <= 0 ? INT_MAX : mhz; + + if (wl_list_empty(&output->modes) || custom) { + wlr_output_state_set_custom_mode(pending, width, height, + refresh_rate > 0 ? mhz : 0); + return; + } + + struct wlr_output_mode *mode, *best = NULL; + int best_diff_mhz = INT_MAX; + wl_list_for_each(mode, &output->modes, link) { + if (mode->width == width && mode->height == height) { + int diff_mhz = abs(mode->refresh - mhz); + if (diff_mhz < best_diff_mhz) { + best_diff_mhz = diff_mhz; + best = mode; + if (best_diff_mhz == 0) { + break; + } + } + } + } + if (!best) { + best = wlr_output_preferred_mode(output); + sway_log(SWAY_INFO, "Configured mode (%dx%d@%.3fHz) not available, " + "applying preferred mode (%dx%d@%.3fHz)", + width, height, refresh_rate, + best->width, best->height, best->refresh / 1000.f); + } + wlr_output_state_set_mode(pending, best); +} + +static void set_modeline(struct wlr_output *output, + struct wlr_output_state *pending, drmModeModeInfo *drm_mode) { +#if WLR_HAS_DRM_BACKEND + if (!wlr_output_is_drm(output)) { + sway_log(SWAY_ERROR, "Modeline can only be set to DRM output"); + return; + } + struct wlr_output_mode *mode = wlr_drm_connector_add_mode(output, drm_mode); + if (mode) { + wlr_output_state_set_mode(pending, mode); + } +#else + sway_log(SWAY_ERROR, "Modeline can only be set to DRM output"); +#endif +} + +bool output_supports_hdr(struct wlr_output *output, const char **unsupported_reason_ptr) { + const char *unsupported_reason = NULL; + if (!(output->supported_primaries & WLR_COLOR_NAMED_PRIMARIES_BT2020)) { + unsupported_reason = "BT2020 primaries not supported by output"; + } else if (!(output->supported_transfer_functions & WLR_COLOR_TRANSFER_FUNCTION_ST2084_PQ)) { + unsupported_reason = "PQ transfer function not supported by output"; + } else if (!server.renderer->features.output_color_transform) { + unsupported_reason = "renderer doesn't support output color transforms"; + } + if (unsupported_reason_ptr != NULL) { + *unsupported_reason_ptr = unsupported_reason; + } + return unsupported_reason == NULL; +} + +static void set_hdr(struct wlr_output *output, struct wlr_output_state *pending, bool enabled) { + const char *unsupported_reason = NULL; + if (enabled && !output_supports_hdr(output, &unsupported_reason)) { + sway_log(SWAY_ERROR, "Cannot enable HDR on output %s: %s", + output->name, unsupported_reason); + enabled = false; + } + + if (!enabled) { + if (output->supported_primaries != 0 || output->supported_transfer_functions != 0) { + sway_log(SWAY_DEBUG, "Disabling HDR on output %s", output->name); + wlr_output_state_set_image_description(pending, NULL); + } + return; + } + + sway_log(SWAY_DEBUG, "Enabling HDR on output %s", output->name); + const struct wlr_output_image_description image_desc = { + .primaries = WLR_COLOR_NAMED_PRIMARIES_BT2020, + .transfer_function = WLR_COLOR_TRANSFER_FUNCTION_ST2084_PQ, + }; + wlr_output_state_set_image_description(pending, &image_desc); +} + +/* Some manufacturers hardcode the aspect-ratio of the output in the physical + * size field. */ +static bool phys_size_is_aspect_ratio(struct wlr_output *output) { + return (output->phys_width == 1600 && output->phys_height == 900) || + (output->phys_width == 1600 && output->phys_height == 1000) || + (output->phys_width == 160 && output->phys_height == 90) || + (output->phys_width == 160 && output->phys_height == 100) || + (output->phys_width == 16 && output->phys_height == 9) || + (output->phys_width == 16 && output->phys_height == 10); +} + +// The minimum DPI at which we turn on a scale of 2 +#define HIDPI_DPI_LIMIT (2 * 96) +// The minimum screen height at which we turn on a scale of 2 +#define HIDPI_MIN_HEIGHT 1200 +// 1 inch = 25.4 mm +#define MM_PER_INCH 25.4 + +static int compute_default_scale(struct wlr_output *output, + struct wlr_output_state *pending) { + struct wlr_box box = { .width = output->width, .height = output->height }; + if (pending->committed & WLR_OUTPUT_STATE_MODE) { + switch (pending->mode_type) { + case WLR_OUTPUT_STATE_MODE_FIXED: + box.width = pending->mode->width; + box.height = pending->mode->height; + break; + case WLR_OUTPUT_STATE_MODE_CUSTOM: + box.width = pending->custom_mode.width; + box.height = pending->custom_mode.height; + break; + } + } + enum wl_output_transform transform = output->transform; + if (pending->committed & WLR_OUTPUT_STATE_TRANSFORM) { + transform = pending->transform; + } + wlr_box_transform(&box, &box, transform, box.width, box.height); + + int width = box.width; + int height = box.height; + + if (height < HIDPI_MIN_HEIGHT) { + return 1; + } + + if (output->phys_width == 0 || output->phys_height == 0) { + return 1; + } + + if (phys_size_is_aspect_ratio(output)) { + return 1; + } + + double dpi_x = (double) width / (output->phys_width / MM_PER_INCH); + double dpi_y = (double) height / (output->phys_height / MM_PER_INCH); + if (dpi_x <= HIDPI_DPI_LIMIT || dpi_y <= HIDPI_DPI_LIMIT) { + return 1; + } + + return 2; +} + +static enum render_bit_depth bit_depth_from_format(uint32_t render_format) { + if (render_format == DRM_FORMAT_XRGB2101010 || render_format == DRM_FORMAT_XBGR2101010) { + return RENDER_BIT_DEPTH_10; + } else if (render_format == DRM_FORMAT_XRGB8888 || render_format == DRM_FORMAT_ARGB8888) { + return RENDER_BIT_DEPTH_8; + } else if (render_format == DRM_FORMAT_RGB565) { + return RENDER_BIT_DEPTH_6; + } + return RENDER_BIT_DEPTH_DEFAULT; +} + +static enum render_bit_depth get_config_render_bit_depth(const struct output_config *oc) { + if (oc && oc->render_bit_depth != RENDER_BIT_DEPTH_DEFAULT) { + return oc->render_bit_depth; + } + if (oc && oc->hdr == 1) { + return RENDER_BIT_DEPTH_10; + } + return RENDER_BIT_DEPTH_8; +} + +static bool render_format_is_bgr(uint32_t fmt) { + return fmt == DRM_FORMAT_XBGR2101010 || fmt == DRM_FORMAT_XBGR8888; +} + +static bool output_config_is_disabling(struct output_config *oc) { + return oc && (!oc->enabled || oc->power == 0); +} + +static void queue_output_config(struct output_config *oc, + struct sway_output *output, struct wlr_output_state *pending) { + if (output == root->fallback_output) { + return; + } + + struct wlr_output *wlr_output = output->wlr_output; + + if (output_config_is_disabling(oc)) { + wlr_output_state_set_enabled(pending, false); + return; + } + wlr_output_state_set_enabled(pending, true); + + if (oc && oc->drm_mode.type != 0 && oc->drm_mode.type != (uint32_t) -1) { + set_modeline(wlr_output, pending, &oc->drm_mode); + } else if (oc && oc->width > 0 && oc->height > 0) { + set_mode(wlr_output, pending, oc->width, oc->height, + oc->refresh_rate, oc->custom_mode == 1); + } else if (!wl_list_empty(&wlr_output->modes)) { + struct wlr_output_mode *preferred_mode = + wlr_output_preferred_mode(wlr_output); + wlr_output_state_set_mode(pending, preferred_mode); + } + + if (oc && oc->subpixel != WL_OUTPUT_SUBPIXEL_UNKNOWN) { + wlr_output_state_set_subpixel(pending, oc->subpixel); + } else { + wlr_output_state_set_subpixel(pending, output->detected_subpixel); + } + + if (oc && oc->transform >= 0) { + wlr_output_state_set_transform(pending, oc->transform); +#if WLR_HAS_DRM_BACKEND + } else if (wlr_output_is_drm(wlr_output)) { + wlr_output_state_set_transform(pending, + wlr_drm_connector_get_panel_orientation(wlr_output)); +#endif + } else { + wlr_output_state_set_transform(pending, WL_OUTPUT_TRANSFORM_NORMAL); + } + + // Apply the scale after sorting out the mode, because the scale + // auto-detection reads the pending output size + if (oc && oc->scale > 0) { + // The factional-scale-v1 protocol uses increments of 120ths to send + // the scale factor to the client. Adjust the scale so that we use the + // same value as the clients'. + wlr_output_state_set_scale(pending, round(oc->scale * 120) / 120); + } else { + wlr_output_state_set_scale(pending, + compute_default_scale(wlr_output, pending)); + } + + if (wlr_output->adaptive_sync_supported) { + if (oc && oc->adaptive_sync != -1) { + wlr_output_state_set_adaptive_sync_enabled(pending, oc->adaptive_sync == 1); + } else { + wlr_output_state_set_adaptive_sync_enabled(pending, false); + } + } + + enum render_bit_depth render_bit_depth = get_config_render_bit_depth(oc); + if (render_bit_depth == RENDER_BIT_DEPTH_10 && + bit_depth_from_format(output->wlr_output->render_format) == render_bit_depth) { + // 10-bit was set successfully before, try to save some tests by reusing the format + wlr_output_state_set_render_format(pending, output->wlr_output->render_format); + } else if (render_bit_depth == RENDER_BIT_DEPTH_10) { + wlr_output_state_set_render_format(pending, DRM_FORMAT_XRGB2101010); + } else if (render_bit_depth == RENDER_BIT_DEPTH_6) { + wlr_output_state_set_render_format(pending, DRM_FORMAT_RGB565); + } else { + wlr_output_state_set_render_format(pending, DRM_FORMAT_XRGB8888); + } + + bool hdr = oc && oc->hdr == 1; + bool color_profile = oc && (oc->color_transform != NULL + || oc->color_profile == COLOR_PROFILE_TRANSFORM_WITH_DEVICE_PRIMARIES); + if (hdr && color_profile) { + sway_log(SWAY_ERROR, "Cannot use HDR on output %s: output has a color profile set", wlr_output->name); + hdr = false; + } + set_hdr(wlr_output, pending, hdr); +} + +struct config_output_state { + struct wlr_color_transform *color_transform; +}; + +static void config_output_state_finish(struct config_output_state *state) { + wlr_color_transform_unref(state->color_transform); +} + +static struct wlr_color_transform *color_profile_from_device(struct wlr_output *wlr_output, + struct wlr_color_transform *transfer_function) { + struct wlr_color_primaries srgb_primaries; + wlr_color_primaries_from_named(&srgb_primaries, WLR_COLOR_NAMED_PRIMARIES_SRGB); + + const struct wlr_color_primaries *primaries = wlr_output->default_primaries; + if (primaries == NULL) { + sway_log(SWAY_INFO, "output has no reported color information"); + if (transfer_function) { + wlr_color_transform_ref(transfer_function); + } + return transfer_function; + } else if (memcmp(primaries, &srgb_primaries, sizeof(*primaries)) == 0) { + sway_log(SWAY_INFO, "output reports sRGB colors, no correction needed"); + if (transfer_function) { + wlr_color_transform_ref(transfer_function); + } + return transfer_function; + } else { + sway_log(SWAY_INFO, "Creating color profile from reported color primaries: " + "R(%f, %f) G(%f, %f) B(%f, %f) W(%f, %f)", + primaries->red.x, primaries->red.y, primaries->green.x, primaries->green.y, + primaries->blue.x, primaries->blue.y, primaries->white.x, primaries->white.y); + float matrix[9]; + wlr_color_primaries_transform_absolute_colorimetric(&srgb_primaries, primaries, matrix); + struct wlr_color_transform *matrix_transform = wlr_color_transform_init_matrix(matrix); + if (matrix_transform == NULL) { + return NULL; + } + struct wlr_color_transform *resolved_tf = transfer_function ? + wlr_color_transform_ref(transfer_function) : + wlr_color_transform_init_linear_to_inverse_eotf(WLR_COLOR_TRANSFER_FUNCTION_GAMMA22); + if (resolved_tf == NULL) { + wlr_color_transform_unref(matrix_transform); + return NULL; + } + struct wlr_color_transform *transforms[] = { matrix_transform, resolved_tf }; + size_t transforms_len = sizeof(transforms) / sizeof(transforms[0]); + struct wlr_color_transform *result = wlr_color_transform_init_pipeline(transforms, transforms_len); + wlr_color_transform_unref(matrix_transform); + wlr_color_transform_unref(resolved_tf); + return result; + } +} + +static struct wlr_color_transform *get_color_profile(struct wlr_output *output, + struct output_config *oc) { + if (oc && oc->color_profile == COLOR_PROFILE_TRANSFORM) { + if (oc->color_transform) { + wlr_color_transform_ref(oc->color_transform); + } + return oc->color_transform; + } else if (oc && oc->color_profile == COLOR_PROFILE_TRANSFORM_WITH_DEVICE_PRIMARIES) { + return color_profile_from_device(output, oc->color_transform); + } else { + return NULL; + } +} + +static bool finalize_output_config(struct output_config *oc, struct sway_output *output, + const struct wlr_output_state *applied, const struct config_output_state *config_applied) { + if (output == root->fallback_output) { + return false; + } + + struct wlr_output *wlr_output = output->wlr_output; + if (oc && !oc->enabled) { + sway_log(SWAY_DEBUG, "Disabling output %s", oc->name); + if (output->enabled) { + output_disable(output); + wlr_output_layout_remove(root->output_layout, wlr_output); + } + return true; + } + + enum scale_filter_mode scale_filter_old = output->scale_filter; + enum scale_filter_mode scale_filter_new = oc ? oc->scale_filter : SCALE_FILTER_DEFAULT; + switch (scale_filter_new) { + case SCALE_FILTER_DEFAULT: + case SCALE_FILTER_SMART: + output->scale_filter = ceilf(wlr_output->scale) == wlr_output->scale ? + SCALE_FILTER_NEAREST : SCALE_FILTER_LINEAR; + break; + case SCALE_FILTER_LINEAR: + case SCALE_FILTER_NEAREST: + output->scale_filter = scale_filter_new; + break; + } + if (scale_filter_old != output->scale_filter) { + sway_log(SWAY_DEBUG, "Set %s scale_filter to %s", oc->name, + sway_output_scale_filter_to_string(output->scale_filter)); + wlr_damage_ring_add_whole(&output->scene_output->damage_ring); + } + + // Find position for it + if (oc && oc->x != INT_MAX && oc->y != INT_MAX) { + sway_log(SWAY_DEBUG, "Set %s position to %d, %d", oc->name, oc->x, oc->y); + wlr_output_layout_add(root->output_layout, wlr_output, oc->x, oc->y); + } else { + wlr_output_layout_add_auto(root->output_layout, wlr_output); + } + + if (!output->enabled) { + output_enable(output); + } + + wlr_color_transform_unref(output->color_transform); + if (config_applied->color_transform != NULL) { + wlr_color_transform_ref(config_applied->color_transform); + } + output->color_transform = config_applied->color_transform; + + output->max_render_time = oc && oc->max_render_time > 0 ? oc->max_render_time : 0; + output->allow_tearing = oc && oc->allow_tearing > 0; + output->hdr = applied->image_description != NULL; + + return true; +} + +static void output_update_position(struct sway_output *output) { + struct wlr_box output_box; + wlr_output_layout_get_box(root->output_layout, output->wlr_output, &output_box); + output->lx = output_box.x; + output->ly = output_box.y; + output->width = output_box.width; + output->height = output_box.height; +} + +// find_output_config_from_list returns a merged output_config containing all +// stored configuration that applies to the specified output. +static struct output_config *find_output_config_from_list( + struct output_config **configs, size_t configs_len, + struct sway_output *sway_output) { + const char *name = sway_output->wlr_output->name; + struct output_config *result = new_output_config(name); + if (result == NULL) { + return NULL; + } + + char id[128]; + output_get_identifier(id, sizeof(id), sway_output); + + // We take a new config and merge on top, in order, the wildcard config, + // output config by name, and output config by identifier to form the final + // config. If there are multiple matches, they are merged in order. + struct output_config *oc = NULL; + const char *names[] = {"*", name, id, NULL}; + for (const char **name = &names[0]; *name; name++) { + for (size_t idx = 0; idx < configs_len; idx++) { + oc = configs[idx]; + if (strcmp(oc->name, *name) == 0) { + merge_output_config(result, oc); + } + } + } + + return result; +} + +struct output_config *find_output_config(struct sway_output *sway_output) { + return find_output_config_from_list( + (struct output_config **)config->output_configs->items, + config->output_configs->length, sway_output); +} + +static bool config_has_manual_mode(struct output_config *oc) { + if (!oc) { + return false; + } + if (oc->drm_mode.type != 0 && oc->drm_mode.type != (uint32_t)-1) { + return true; + } else if (oc->width > 0 && oc->height > 0) { + return true; + } + return false; +} + +/** + * An output config pre-matched to an output + */ +struct matched_output_config { + struct sway_output *output; + struct output_config *config; +}; + +struct search_context { + struct wlr_output_swapchain_manager *swapchain_mgr; + struct wlr_backend_output_state *states; + struct matched_output_config *configs; + size_t configs_len; + bool degrade_to_off; +}; + +static void dump_output_state(struct wlr_output *wlr_output, struct wlr_output_state *state) { + sway_log(SWAY_DEBUG, "Output state for %s", wlr_output->name); + if (state->committed & WLR_OUTPUT_STATE_ENABLED) { + sway_log(SWAY_DEBUG, " enabled: %s", state->enabled ? "yes" : "no"); + } + if (state->committed & WLR_OUTPUT_STATE_RENDER_FORMAT) { + char *format_name = drmGetFormatName(state->render_format); + sway_log(SWAY_DEBUG, " render_format: %s", format_name); + free(format_name); + } + if (state->committed & WLR_OUTPUT_STATE_MODE) { + if (state->mode_type == WLR_OUTPUT_STATE_MODE_CUSTOM) { + sway_log(SWAY_DEBUG, " custom mode: %dx%d@%dmHz", + state->custom_mode.width, state->custom_mode.height, state->custom_mode.refresh); + } else { + sway_log(SWAY_DEBUG, " mode: %dx%d@%dmHz%s", + state->mode->width, state->mode->height, state->mode->refresh, + state->mode->preferred ? " (preferred)" : ""); + } + } + if (state->committed & WLR_OUTPUT_STATE_ADAPTIVE_SYNC_ENABLED) { + sway_log(SWAY_DEBUG, " adaptive_sync: %s", + state->adaptive_sync_enabled ? "enabled": "disabled"); + } + if (state->committed & WLR_OUTPUT_STATE_SCALE) { + sway_log(SWAY_DEBUG, " scale: %f", state->scale); + } + if (state->committed & WLR_OUTPUT_STATE_SUBPIXEL) { + sway_log(SWAY_DEBUG, " subpixel: %s", + sway_wl_output_subpixel_to_string(state->subpixel)); + } +} + +static bool search_valid_config(struct search_context *ctx, size_t output_idx); + +static void reset_output_state(struct wlr_output_state *state) { + wlr_output_state_finish(state); + wlr_output_state_init(state); + state->committed = 0; +} + +static void clear_later_output_states(struct wlr_backend_output_state *states, + size_t configs_len, size_t output_idx) { + + // Clear and disable all output states after this one to avoid conflict + // with previous tests. + for (size_t idx = output_idx+1; idx < configs_len; idx++) { + struct wlr_backend_output_state *backend_state = &states[idx]; + struct wlr_output_state *state = &backend_state->base; + + reset_output_state(state); + wlr_output_state_set_enabled(state, false); + } +} + +static bool search_finish(struct search_context *ctx, size_t output_idx) { + struct wlr_backend_output_state *backend_state = &ctx->states[output_idx]; + struct wlr_output_state *state = &backend_state->base; + struct wlr_output *wlr_output = backend_state->output; + + clear_later_output_states(ctx->states, ctx->configs_len, output_idx); + dump_output_state(wlr_output, state); + return wlr_output_swapchain_manager_prepare(ctx->swapchain_mgr, ctx->states, ctx->configs_len) && + search_valid_config(ctx, output_idx+1); +} + +static bool search_adaptive_sync(struct search_context *ctx, size_t output_idx) { + struct matched_output_config *cfg = &ctx->configs[output_idx]; + struct wlr_backend_output_state *backend_state = &ctx->states[output_idx]; + struct wlr_output_state *state = &backend_state->base; + + if (!backend_state->output->adaptive_sync_supported) { + return search_finish(ctx, output_idx); + } + + if (cfg->config && cfg->config->adaptive_sync == 1) { + wlr_output_state_set_adaptive_sync_enabled(state, true); + if (search_finish(ctx, output_idx)) { + return true; + } + } + + wlr_output_state_set_adaptive_sync_enabled(state, false); + return search_finish(ctx, output_idx); +} + +static bool search_mode(struct search_context *ctx, size_t output_idx) { + struct matched_output_config *cfg = &ctx->configs[output_idx]; + struct wlr_backend_output_state *backend_state = &ctx->states[output_idx]; + struct wlr_output_state *state = &backend_state->base; + struct wlr_output *wlr_output = backend_state->output; + + // We only search for mode if one is not explicitly specified in the config + if (config_has_manual_mode(cfg->config)) { + return search_adaptive_sync(ctx, output_idx); + } + + struct wlr_output_mode *preferred_mode = wlr_output_preferred_mode(wlr_output); + if (preferred_mode) { + wlr_output_state_set_mode(state, preferred_mode); + if (search_adaptive_sync(ctx, output_idx)) { + return true; + } + } + + if (wl_list_empty(&wlr_output->modes)) { + state->committed &= ~WLR_OUTPUT_STATE_MODE; + return search_adaptive_sync(ctx, output_idx); + } + + struct wlr_output_mode *mode; + wl_list_for_each(mode, &backend_state->output->modes, link) { + if (mode == preferred_mode) { + continue; + } + wlr_output_state_set_mode(state, mode); + if (search_adaptive_sync(ctx, output_idx)) { + return true; + } + } + + return false; +} + +static bool search_render_format(struct search_context *ctx, size_t output_idx) { + struct matched_output_config *cfg = &ctx->configs[output_idx]; + struct wlr_backend_output_state *backend_state = &ctx->states[output_idx]; + struct wlr_output_state *state = &backend_state->base; + struct wlr_output *wlr_output = backend_state->output; + + uint32_t fmts[] = { + DRM_FORMAT_XRGB2101010, + DRM_FORMAT_XBGR2101010, + DRM_FORMAT_XRGB8888, + DRM_FORMAT_ARGB8888, + DRM_FORMAT_RGB565, + DRM_FORMAT_INVALID, + }; + if (render_format_is_bgr(wlr_output->render_format)) { + // Start with BGR in the unlikely event that we previously required it. + fmts[0] = DRM_FORMAT_XBGR2101010; + fmts[1] = DRM_FORMAT_XRGB2101010; + } + + const struct wlr_drm_format_set *primary_formats = + wlr_output_get_primary_formats(wlr_output, server.allocator->buffer_caps); + enum render_bit_depth needed_bits = get_config_render_bit_depth(cfg->config); + for (size_t idx = 0; fmts[idx] != DRM_FORMAT_INVALID; idx++) { + enum render_bit_depth format_bits = bit_depth_from_format(fmts[idx]); + if (needed_bits < format_bits) { + continue; + } + // If primary_formats is NULL, all formats are supported + if (primary_formats && !wlr_drm_format_set_get(primary_formats, fmts[idx])) { + // This is not a supported format for this output + continue; + } + wlr_output_state_set_render_format(state, fmts[idx]); + if (search_mode(ctx, output_idx)) { + return true; + } + } + return false; +} + +static bool search_valid_config(struct search_context *ctx, size_t output_idx) { + if (output_idx >= ctx->configs_len) { + // We reached the end of the search, all good! + return true; + } + + struct matched_output_config *cfg = &ctx->configs[output_idx]; + struct wlr_backend_output_state *backend_state = &ctx->states[output_idx]; + struct wlr_output_state *state = &backend_state->base; + struct wlr_output *wlr_output = backend_state->output; + + if (!output_config_is_disabling(cfg->config)) { + // Search through our possible configurations, doing a depth-first + // through render_format, modes, adaptive_sync and the next output's + // config. + queue_output_config(cfg->config, cfg->output, &backend_state->base); + if (search_render_format(ctx, output_idx)) { + return true; + } else if (!ctx->degrade_to_off) { + return false; + } + // We could not get anything to work, try to disable this output to see + // if we can at least make the outputs before us work. + sway_log(SWAY_DEBUG, "Unable to find valid config with output %s, disabling", + wlr_output->name); + reset_output_state(state); + } + + wlr_output_state_set_enabled(state, false); + return search_finish(ctx, output_idx); +} + +static int compare_matched_output_config_priority(const void *a, const void *b) { + + const struct matched_output_config *amc = a; + const struct matched_output_config *bmc = b; + bool a_disabling = output_config_is_disabling(amc->config); + bool b_disabling = output_config_is_disabling(bmc->config); + bool a_enabled = amc->output->enabled; + bool b_enabled = bmc->output->enabled; + + // We want to give priority to existing enabled outputs. To do so, we want + // the configuration order to be: + // 1. Existing, enabled outputs + // 2. Outputs that need to be enabled + // 3. Disabled or disabling outputs + if (a_enabled && !a_disabling) { + return -1; + } else if (b_enabled && !b_disabling) { + return 1; + } else if (b_disabling && !a_disabling) { + return -1; + } else if (a_disabling && !b_disabling) { + return 1; + } + return 0; +} + +static void sort_output_configs_by_priority( + struct matched_output_config *configs, size_t configs_len) { + qsort(configs, configs_len, sizeof(*configs), compare_matched_output_config_priority); +} + +static bool apply_resolved_output_configs(struct matched_output_config *configs, + size_t configs_len, bool test_only, bool degrade_to_off) { + struct wlr_backend_output_state *states = calloc(configs_len, sizeof(*states)); + if (!states) { + return false; + } + struct config_output_state *config_states = calloc(configs_len, sizeof(*config_states)); + if (!config_states) { + free(states); + return false; + } + + sway_log(SWAY_DEBUG, "Committing %zd outputs", configs_len); + for (size_t idx = 0; idx < configs_len; idx++) { + struct matched_output_config *cfg = &configs[idx]; + struct wlr_backend_output_state *backend_state = &states[idx]; + struct config_output_state *config_state = &config_states[idx]; + + backend_state->output = cfg->output->wlr_output; + wlr_output_state_init(&backend_state->base); + + queue_output_config(cfg->config, cfg->output, &backend_state->base); + dump_output_state(cfg->output->wlr_output, &backend_state->base); + + config_state->color_transform = get_color_profile(cfg->output->wlr_output, cfg->config); + } + + struct wlr_output_swapchain_manager swapchain_mgr; + wlr_output_swapchain_manager_init(&swapchain_mgr, server.backend); + + bool ok = wlr_output_swapchain_manager_prepare(&swapchain_mgr, states, configs_len); + if (!ok) { + sway_log(SWAY_ERROR, "Requested backend configuration failed, searching for valid fallbacks"); + struct search_context ctx = { + .swapchain_mgr = &swapchain_mgr, + .states = states, + .configs = configs, + .configs_len = configs_len, + .degrade_to_off = degrade_to_off, + }; + if (!search_valid_config(&ctx, 0)) { + sway_log(SWAY_ERROR, "Search for valid config failed"); + goto out; + } + } + + if (test_only) { + // The swapchain manager already did a test for us + goto out; + } + + for (size_t idx = 0; idx < configs_len; idx++) { + struct matched_output_config *cfg = &configs[idx]; + struct wlr_backend_output_state *backend_state = &states[idx]; + struct config_output_state *config_state = &config_states[idx]; + + struct wlr_scene_output_state_options opts = { + .swapchain = wlr_output_swapchain_manager_get_swapchain( + &swapchain_mgr, backend_state->output), + .color_transform = config_state->color_transform, + }; + struct wlr_scene_output *scene_output = cfg->output->scene_output; + struct wlr_output_state *state = &backend_state->base; + if (!wlr_scene_output_build_state(scene_output, state, &opts)) { + sway_log(SWAY_ERROR, "Building output state for '%s' failed", + backend_state->output->name); + goto out; + } + } + + ok = wlr_backend_commit(server.backend, states, configs_len); + if (!ok) { + sway_log(SWAY_ERROR, "Backend commit failed"); + goto out; + } + + sway_log(SWAY_DEBUG, "Commit of %zd outputs succeeded", configs_len); + + wlr_output_swapchain_manager_apply(&swapchain_mgr); + + for (size_t idx = 0; idx < configs_len; idx++) { + struct matched_output_config *cfg = &configs[idx]; + + // since everything got moved around, mark the optimized blur as dirty + // this is to catch e.g. the output transform since it directly picks things up from the fbo + if (cfg->output->layers.blur_layer) { + cfg->output->layers.blur_layer->dirty = true; + } + + struct wlr_backend_output_state *backend_state = &states[idx]; + struct config_output_state *config_state = &config_states[idx]; + sway_log(SWAY_DEBUG, "Finalizing config for %s", + cfg->output->wlr_output->name); + finalize_output_config(cfg->config, cfg->output, &backend_state->base, config_state); + } + + // Output layout being applied in finalize_output_config can shift outputs + // around, so we do a second pass to update positions and arrange. + for (size_t idx = 0; idx < configs_len; idx++) { + struct matched_output_config *cfg = &configs[idx]; + output_update_position(cfg->output); + arrange_layers(cfg->output); + } + + arrange_root(); + arrange_locks(); + update_output_manager_config(&server); + transaction_commit_dirty(); + +out: + wlr_output_swapchain_manager_finish(&swapchain_mgr); + for (size_t idx = 0; idx < configs_len; idx++) { + struct wlr_backend_output_state *backend_state = &states[idx]; + wlr_output_state_finish(&backend_state->base); + config_output_state_finish(&config_states[idx]); + } + free(states); + free(config_states); + + // Reconfigure all devices, since input config may have been applied before + // this output came online, and some config items (like map_to_output) are + // dependent on an output being present. + input_manager_configure_all_input_mappings(); + // Reconfigure the cursor images, since the scale may have changed. + input_manager_configure_xcursor(); + + struct sway_seat *seat; + wl_list_for_each(seat, &server.input->seats, link) { + wlr_seat_pointer_notify_clear_focus(seat->wlr_seat); + cursor_rebase(seat->cursor); + } + + return ok; +} + +bool apply_output_configs(struct output_config **ocs, size_t ocs_len, + bool test_only, bool degrade_to_off) { + size_t configs_len = wl_list_length(&root->all_outputs); + struct matched_output_config *configs = calloc(configs_len, sizeof(*configs)); + if (!configs) { + return false; + } + + int config_idx = 0; + struct sway_output *sway_output; + wl_list_for_each(sway_output, &root->all_outputs, link) { + if (sway_output == root->fallback_output) { + configs_len--; + continue; + } + + struct matched_output_config *config = &configs[config_idx++]; + config->output = sway_output; + config->config = find_output_config_from_list(ocs, ocs_len, sway_output); + } + + sort_output_configs_by_priority(configs, configs_len); + bool ok = apply_resolved_output_configs(configs, configs_len, test_only, degrade_to_off); + for (size_t idx = 0; idx < configs_len; idx++) { + struct matched_output_config *cfg = &configs[idx]; + free_output_config(cfg->config); + } + free(configs); + return ok; +} + +void apply_stored_output_configs(void) { + apply_output_configs((struct output_config **)config->output_configs->items, + config->output_configs->length, false, true); +} + +void free_output_config(struct output_config *oc) { + if (!oc) { + return; + } + free(oc->name); + free(oc->background); + free(oc->background_option); + free(oc->background_fallback); + wlr_color_transform_unref(oc->color_transform); + free(oc); +} + +static void handle_swaybg_client_destroy(struct wl_listener *listener, + void *data) { + struct sway_config *sway_config = + wl_container_of(listener, sway_config, swaybg_client_destroy); + wl_list_remove(&sway_config->swaybg_client_destroy.link); + wl_list_init(&sway_config->swaybg_client_destroy.link); + sway_config->swaybg_client = NULL; +} + +static bool _spawn_swaybg(char **command) { + if (config->swaybg_client != NULL) { + wl_client_destroy(config->swaybg_client); + } + int sockets[2]; + if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) != 0) { + sway_log_errno(SWAY_ERROR, "socketpair failed"); + return false; + } + if (!sway_set_cloexec(sockets[0], true) || !sway_set_cloexec(sockets[1], true)) { + return false; + } + + config->swaybg_client = wl_client_create(server.wl_display, sockets[0]); + if (config->swaybg_client == NULL) { + sway_log_errno(SWAY_ERROR, "wl_client_create failed"); + return false; + } + + config->swaybg_client_destroy.notify = handle_swaybg_client_destroy; + wl_client_add_destroy_listener(config->swaybg_client, + &config->swaybg_client_destroy); + + pid_t pid = fork(); + if (pid < 0) { + sway_log_errno(SWAY_ERROR, "fork failed"); + return false; + } else if (pid == 0) { + if (!sway_set_cloexec(sockets[1], false)) { + _exit(EXIT_FAILURE); + } + + char wayland_socket_str[16]; + snprintf(wayland_socket_str, sizeof(wayland_socket_str), + "%d", sockets[1]); + setenv("WAYLAND_SOCKET", wayland_socket_str, true); + + execvp(command[0], command); + sway_log_errno(SWAY_ERROR, "failed to execute '%s' " + "(background configuration probably not applied)", + command[0]); + _exit(EXIT_FAILURE); + } + + if (close(sockets[1]) != 0) { + sway_log_errno(SWAY_ERROR, "close failed"); + return false; + } + return true; +} + +bool spawn_swaybg(void) { + if (!config->swaybg_command) { + return true; + } + + size_t length = 2; + for (int i = 0; i < config->output_configs->length; i++) { + struct output_config *oc = config->output_configs->items[i]; + if (!oc->background) { + continue; + } + if (strcmp(oc->background_option, "solid_color") == 0) { + length += 4; + } else if (oc->background_fallback) { + length += 8; + } else { + length += 6; + } + } + + char **cmd = calloc(length, sizeof(char *)); + if (!cmd) { + sway_log(SWAY_ERROR, "Failed to allocate spawn_swaybg command"); + return false; + } + + size_t i = 0; + cmd[i++] = config->swaybg_command; + for (int j = 0; j < config->output_configs->length; j++) { + struct output_config *oc = config->output_configs->items[j]; + if (!oc->background) { + continue; + } + if (strcmp(oc->background_option, "solid_color") == 0) { + cmd[i++] = "-o"; + cmd[i++] = oc->name; + cmd[i++] = "-c"; + cmd[i++] = oc->background; + } else { + cmd[i++] = "-o"; + cmd[i++] = oc->name; + cmd[i++] = "-i"; + cmd[i++] = oc->background; + cmd[i++] = "-m"; + cmd[i++] = oc->background_option; + if (oc->background_fallback) { + cmd[i++] = "-c"; + cmd[i++] = oc->background_fallback; + } + } + assert(i <= length); + } + + for (size_t k = 0; k < i; k++) { + sway_log(SWAY_DEBUG, "spawn_swaybg cmd[%zd] = %s", k, cmd[k]); + } + + bool result = _spawn_swaybg(cmd); + free(cmd); + return result; +} + diff --git a/sway/config/seat.c b/sway/config/seat.c new file mode 100644 index 0000000..f232618 --- /dev/null +++ b/sway/config/seat.c @@ -0,0 +1,226 @@ +#include +#include +#include +#include "sway/config.h" +#include "log.h" + +struct seat_config *new_seat_config(const char* name) { + struct seat_config *seat = calloc(1, sizeof(struct seat_config)); + if (!seat) { + sway_log(SWAY_DEBUG, "Unable to allocate seat config"); + return NULL; + } + + seat->name = strdup(name); + if (!sway_assert(seat->name, "could not allocate name for seat")) { + free(seat); + return NULL; + } + + seat->idle_inhibit_sources = seat->idle_wake_sources = UINT32_MAX; + + seat->fallback = -1; + seat->attachments = create_list(); + if (!sway_assert(seat->attachments, + "could not allocate seat attachments list")) { + free(seat->name); + free(seat); + return NULL; + } + seat->hide_cursor_timeout = -1; + seat->hide_cursor_when_typing = HIDE_WHEN_TYPING_DEFAULT; + seat->allow_constrain = CONSTRAIN_DEFAULT; + seat->shortcuts_inhibit = SHORTCUTS_INHIBIT_DEFAULT; + seat->keyboard_grouping = KEYBOARD_GROUP_DEFAULT; + seat->xcursor_theme.name = NULL; + seat->xcursor_theme.size = 24; + + return seat; +} + +static void merge_wildcard_on_all(struct seat_config *wildcard) { + for (int i = 0; i < config->seat_configs->length; i++) { + struct seat_config *sc = config->seat_configs->items[i]; + if (strcmp(wildcard->name, sc->name) != 0) { + sway_log(SWAY_DEBUG, "Merging seat * config on %s", sc->name); + merge_seat_config(sc, wildcard); + } + } +} + +struct seat_config *store_seat_config(struct seat_config *sc) { + bool wildcard = strcmp(sc->name, "*") == 0; + if (wildcard) { + merge_wildcard_on_all(sc); + } + + int i = list_seq_find(config->seat_configs, seat_name_cmp, sc->name); + if (i >= 0) { + sway_log(SWAY_DEBUG, "Merging on top of existing seat config"); + struct seat_config *current = config->seat_configs->items[i]; + merge_seat_config(current, sc); + free_seat_config(sc); + sc = current; + } else if (!wildcard) { + sway_log(SWAY_DEBUG, "Adding non-wildcard seat config"); + i = list_seq_find(config->seat_configs, seat_name_cmp, "*"); + if (i >= 0) { + sway_log(SWAY_DEBUG, "Merging on top of seat * config"); + struct seat_config *current = new_seat_config(sc->name); + merge_seat_config(current, config->seat_configs->items[i]); + merge_seat_config(current, sc); + free_seat_config(sc); + sc = current; + } + list_add(config->seat_configs, sc); + } else { + // New wildcard config. Just add it + sway_log(SWAY_DEBUG, "Adding seat * config"); + list_add(config->seat_configs, sc); + } + + sway_log(SWAY_DEBUG, "Config stored for seat %s", sc->name); + + return sc; +} + +struct seat_attachment_config *seat_attachment_config_new(void) { + struct seat_attachment_config *attachment = + calloc(1, sizeof(struct seat_attachment_config)); + if (!attachment) { + sway_log(SWAY_DEBUG, "cannot allocate attachment config"); + return NULL; + } + return attachment; +} + +static void seat_attachment_config_free( + struct seat_attachment_config *attachment) { + free(attachment->identifier); + free(attachment); +} + +static struct seat_attachment_config *seat_attachment_config_copy( + struct seat_attachment_config *attachment) { + struct seat_attachment_config *copy = seat_attachment_config_new(); + if (!copy) { + return NULL; + } + + copy->identifier = strdup(attachment->identifier); + + return copy; +} + +static void merge_seat_attachment_config(struct seat_attachment_config *dest, + struct seat_attachment_config *source) { + // nothing to merge yet, but there will be some day +} + +void merge_seat_config(struct seat_config *dest, struct seat_config *source) { + if (source->fallback != -1) { + dest->fallback = source->fallback; + } + + for (int i = 0; i < source->attachments->length; ++i) { + struct seat_attachment_config *source_attachment = + source->attachments->items[i]; + bool found = false; + for (int j = 0; j < dest->attachments->length; ++j) { + struct seat_attachment_config *dest_attachment = + dest->attachments->items[j]; + if (strcmp(source_attachment->identifier, + dest_attachment->identifier) == 0) { + merge_seat_attachment_config(dest_attachment, + source_attachment); + found = true; + } + } + + if (!found) { + struct seat_attachment_config *copy = + seat_attachment_config_copy(source_attachment); + if (copy) { + list_add(dest->attachments, copy); + } + } + } + + if (source->hide_cursor_timeout != -1) { + dest->hide_cursor_timeout = source->hide_cursor_timeout; + } + + if (source->hide_cursor_when_typing != HIDE_WHEN_TYPING_DEFAULT) { + dest->hide_cursor_when_typing = source->hide_cursor_when_typing; + } + + if (source->allow_constrain != CONSTRAIN_DEFAULT) { + dest->allow_constrain = source->allow_constrain; + } + + if (source->shortcuts_inhibit != SHORTCUTS_INHIBIT_DEFAULT) { + dest->shortcuts_inhibit = source->shortcuts_inhibit; + } + + if (source->keyboard_grouping != KEYBOARD_GROUP_DEFAULT) { + dest->keyboard_grouping = source->keyboard_grouping; + } + + if (source->xcursor_theme.name != NULL) { + free(dest->xcursor_theme.name); + dest->xcursor_theme.name = strdup(source->xcursor_theme.name); + dest->xcursor_theme.size = source->xcursor_theme.size; + } + + if (source->idle_inhibit_sources != UINT32_MAX) { + dest->idle_inhibit_sources = source->idle_inhibit_sources; + } + + if (source->idle_wake_sources != UINT32_MAX) { + dest->idle_wake_sources = source->idle_wake_sources; + } +} + +struct seat_config *copy_seat_config(struct seat_config *seat) { + struct seat_config *copy = new_seat_config(seat->name); + if (copy == NULL) { + return NULL; + } + + merge_seat_config(copy, seat); + + return copy; +} + +void free_seat_config(struct seat_config *seat) { + if (!seat) { + return; + } + + free(seat->name); + for (int i = 0; i < seat->attachments->length; ++i) { + seat_attachment_config_free(seat->attachments->items[i]); + } + list_free(seat->attachments); + free(seat->xcursor_theme.name); + free(seat); +} + +int seat_name_cmp(const void *item, const void *data) { + const struct seat_config *sc = item; + const char *name = data; + return strcmp(sc->name, name); +} + +struct seat_attachment_config *seat_config_get_attachment( + struct seat_config *seat_config, char *identifier) { + for (int i = 0; i < seat_config->attachments->length; ++i) { + struct seat_attachment_config *attachment = + seat_config->attachments->items[i]; + if (strcmp(attachment->identifier, identifier) == 0) { + return attachment; + } + } + + return NULL; +} diff --git a/sway/criteria.c b/sway/criteria.c new file mode 100644 index 0000000..6be6e70 --- /dev/null +++ b/sway/criteria.c @@ -0,0 +1,907 @@ +#include +#include +#include +#include +#define PCRE2_CODE_UNIT_WIDTH 8 +#include +#include "sway/criteria.h" +#include "sway/tree/container.h" +#include "sway/config.h" +#include "sway/server.h" +#include "sway/tree/root.h" +#include "sway/tree/view.h" +#include "sway/tree/workspace.h" +#include "stringop.h" +#include "list.h" +#include "log.h" +#include "config.h" + +bool criteria_is_empty(struct criteria *criteria) { + return !criteria->title + && !criteria->shell + && !criteria->all + && !criteria->app_id + && !criteria->con_mark + && !criteria->con_id +#if WLR_HAS_XWAYLAND + && !criteria->class + && !criteria->id + && !criteria->instance + && !criteria->window_role + && criteria->window_type == ATOM_LAST +#endif + && !criteria->floating + && !criteria->tiling + && !criteria->urgent + && !criteria->workspace + && !criteria->pid + && !criteria->sandbox_engine + && !criteria->sandbox_app_id + && !criteria->sandbox_instance_id + && !criteria->tag; +} + +// The error pointer is used for parsing functions, and saves having to pass it +// as an argument in several places. +char *error = NULL; + +// Returns error string on failure or NULL otherwise. +static bool generate_regex(pcre2_code **regex, char *value) { + int errorcode; + PCRE2_SIZE offset; + + *regex = pcre2_compile((PCRE2_SPTR)value, PCRE2_ZERO_TERMINATED, PCRE2_UTF | PCRE2_UCP, &errorcode, &offset, NULL); + if (!*regex) { + PCRE2_UCHAR buffer[256]; + pcre2_get_error_message(errorcode, buffer, sizeof(buffer)); + + const char *fmt = "Regex compilation for '%s' failed: %s"; + int len = strlen(fmt) + strlen(value) + strlen((char*) buffer) - 3; + error = malloc(len); + snprintf(error, len, fmt, value, buffer); + return false; + } + + return true; +} + +static bool pattern_create(struct pattern **pattern, char *value) { + *pattern = calloc(1, sizeof(struct pattern)); + if (!*pattern) { + sway_log(SWAY_ERROR, "Failed to allocate pattern"); + } + + if (strcmp(value, "__focused__") == 0) { + (*pattern)->match_type = PATTERN_FOCUSED; + } else { + (*pattern)->match_type = PATTERN_PCRE2; + if (!generate_regex(&(*pattern)->regex, value)) { + return false; + }; + } + return true; +} + +static void pattern_destroy(struct pattern *pattern) { + if (pattern) { + if (pattern->regex) { + pcre2_code_free(pattern->regex); + } + free(pattern); + } +} + +void criteria_destroy(struct criteria *criteria) { + pattern_destroy(criteria->title); + pattern_destroy(criteria->shell); + pattern_destroy(criteria->app_id); +#if WLR_HAS_XWAYLAND + pattern_destroy(criteria->class); + pattern_destroy(criteria->instance); + pattern_destroy(criteria->window_role); +#endif + pattern_destroy(criteria->con_mark); + pattern_destroy(criteria->workspace); + pattern_destroy(criteria->sandbox_engine); + pattern_destroy(criteria->sandbox_app_id); + pattern_destroy(criteria->sandbox_instance_id); + pattern_destroy(criteria->tag); + free(criteria->target); + free(criteria->cmdlist); + free(criteria->raw); + free(criteria); +} + +static int regex_cmp(const char *item, const pcre2_code *regex) { + pcre2_match_data *match_data = pcre2_match_data_create_from_pattern(regex, NULL); + int result = pcre2_match(regex, (PCRE2_SPTR)item, strlen(item), 0, 0, match_data, NULL); + pcre2_match_data_free(match_data); + return result; +} + +#if WLR_HAS_XWAYLAND +static bool view_has_window_type(struct sway_view *view, enum atom_name name) { + if (view->type != SWAY_VIEW_XWAYLAND) { + return false; + } + struct wlr_xwayland_surface *surface = view->wlr_xwayland_surface; + struct sway_xwayland *xwayland = &server.xwayland; + xcb_atom_t desired_atom = xwayland->atoms[name]; + for (size_t i = 0; i < surface->window_type_len; ++i) { + if (surface->window_type[i] == desired_atom) { + return true; + } + } + return false; +} +#endif + +static int cmp_urgent(const void *_a, const void *_b) { + struct sway_view *a = *(void **)_a; + struct sway_view *b = *(void **)_b; + + if (a->urgent.tv_sec < b->urgent.tv_sec) { + return -1; + } else if (a->urgent.tv_sec > b->urgent.tv_sec) { + return 1; + } + if (a->urgent.tv_nsec < b->urgent.tv_nsec) { + return -1; + } else if (a->urgent.tv_nsec > b->urgent.tv_nsec) { + return 1; + } + return 0; +} + +static void find_urgent_iterator(struct sway_container *con, void *data) { + if (!con->view || !view_is_urgent(con->view)) { + return; + } + list_t *urgent_views = data; + list_add(urgent_views, con->view); +} + +static bool has_container_criteria(struct criteria *criteria) { + return criteria->con_mark || criteria->con_id; +} + +static bool criteria_matches_container(struct criteria *criteria, + struct sway_container *container) { + if (criteria->con_mark) { + bool exists = false; + struct sway_container *con = container; + for (int i = 0; i < con->marks->length; ++i) { + if (regex_cmp(con->marks->items[i], criteria->con_mark->regex) >= 0) { + exists = true; + break; + } + } + if (!exists) { + return false; + } + } + + if (criteria->con_id) { // Internal ID + if (container->node.id != criteria->con_id) { + return false; + } + } + + return true; +} + +static bool criteria_matches_view(struct criteria *criteria, + struct sway_view *view) { + struct sway_seat *seat = input_manager_current_seat(); + struct sway_container *focus = seat_get_focused_container(seat); + struct sway_view *focused = focus ? focus->view : NULL; + + if (!view->container) { + return false; + } + + if (criteria->title) { + const char *title = view_get_title(view); + if (!title) { + title = ""; + } + + switch (criteria->title->match_type) { + case PATTERN_FOCUSED: + if (!focused || lenient_strcmp(title, view_get_title(focused))) { + return false; + } + break; + case PATTERN_PCRE2: + if (regex_cmp(title, criteria->title->regex) < 0) { + return false; + } + break; + } + } + + if (criteria->shell) { + const char *shell = view_get_shell(view); + if (!shell) { + shell = ""; + } + + switch (criteria->shell->match_type) { + case PATTERN_FOCUSED: + if (!focused || strcmp(shell, view_get_shell(focused))) { + return false; + } + break; + case PATTERN_PCRE2: + if (regex_cmp(shell, criteria->shell->regex) < 0) { + return false; + } + break; + } + } + + if (criteria->app_id) { + const char *app_id = view_get_app_id(view); + if (!app_id) { + app_id = ""; + } + + switch (criteria->app_id->match_type) { + case PATTERN_FOCUSED: + if (!focused || lenient_strcmp(app_id, view_get_app_id(focused))) { + return false; + } + break; + case PATTERN_PCRE2: + if (regex_cmp(app_id, criteria->app_id->regex) < 0) { + return false; + } + break; + } + } + + if (criteria->sandbox_engine) { + const char *sandbox_engine = view_get_sandbox_engine(view); + if (!sandbox_engine) { + return false; + } + + switch (criteria->sandbox_engine->match_type) { + case PATTERN_FOCUSED: + if (!focused || lenient_strcmp(sandbox_engine, view_get_sandbox_engine(focused))) { + return false; + } + break; + case PATTERN_PCRE2: + if (regex_cmp(sandbox_engine, criteria->sandbox_engine->regex) < 0) { + return false; + } + break; + } + } + + if (criteria->sandbox_app_id) { + const char *sandbox_app_id = view_get_sandbox_app_id(view); + if (!sandbox_app_id) { + return false; + } + + switch (criteria->sandbox_app_id->match_type) { + case PATTERN_FOCUSED: + if (!focused || lenient_strcmp(sandbox_app_id, view_get_sandbox_app_id(focused))) { + return false; + } + break; + case PATTERN_PCRE2: + if (regex_cmp(sandbox_app_id, criteria->sandbox_app_id->regex) < 0) { + return false; + } + break; + } + } + + if (criteria->sandbox_instance_id) { + const char *sandbox_instance_id = view_get_sandbox_instance_id(view); + if (!sandbox_instance_id) { + return false; + } + + switch (criteria->sandbox_instance_id->match_type) { + case PATTERN_FOCUSED: + if (!focused || lenient_strcmp(sandbox_instance_id, view_get_sandbox_instance_id(focused))) { + return false; + } + break; + case PATTERN_PCRE2: + if (regex_cmp(sandbox_instance_id, criteria->sandbox_instance_id->regex) < 0) { + return false; + } + break; + } + } + + if (criteria->tag) { + const char *tag = view_get_tag(view); + if (!tag) { + return false; + } + + switch (criteria->tag->match_type) { + case PATTERN_FOCUSED: + if (!focused || lenient_strcmp(tag, view_get_tag(focused))) { + return false; + } + break; + case PATTERN_PCRE2: + if (regex_cmp(tag, criteria->tag->regex) < 0) { + return false; + } + break; + } + } + + if (!criteria_matches_container(criteria, view->container)) { + return false; + } + +#if WLR_HAS_XWAYLAND + if (criteria->id) { // X11 window ID + uint32_t x11_window_id = view_get_x11_window_id(view); + if (!x11_window_id || x11_window_id != criteria->id) { + return false; + } + } + + if (criteria->class) { + const char *class = view_get_class(view); + if (!class) { + class = ""; + } + + switch (criteria->class->match_type) { + case PATTERN_FOCUSED: + if (!focused || lenient_strcmp(class, view_get_class(focused))) { + return false; + } + break; + case PATTERN_PCRE2: + if (regex_cmp(class, criteria->class->regex) < 0) { + return false; + } + break; + } + } + + if (criteria->instance) { + const char *instance = view_get_instance(view); + if (!instance) { + instance = ""; + } + + switch (criteria->instance->match_type) { + case PATTERN_FOCUSED: + if (!focused || lenient_strcmp(instance, view_get_instance(focused))) { + return false; + } + break; + case PATTERN_PCRE2: + if (regex_cmp(instance, criteria->instance->regex) < 0) { + return false; + } + break; + } + } + + if (criteria->window_role) { + const char *window_role = view_get_window_role(view); + if (!window_role) { + window_role = ""; + } + + switch (criteria->window_role->match_type) { + case PATTERN_FOCUSED: + if (!focused || lenient_strcmp(window_role, view_get_window_role(focused))) { + return false; + } + break; + case PATTERN_PCRE2: + if (regex_cmp(window_role, criteria->window_role->regex) < 0) { + return false; + } + break; + } + } + + if (criteria->window_type != ATOM_LAST) { + if (!view_has_window_type(view, criteria->window_type)) { + return false; + } + } +#endif + + if (criteria->floating) { + if (!container_is_floating(view->container)) { + return false; + } + } + + if (criteria->tiling) { + if (container_is_floating(view->container)) { + return false; + } + } + + if (criteria->urgent) { + if (!view_is_urgent(view)) { + return false; + } + list_t *urgent_views = create_list(); + root_for_each_container(find_urgent_iterator, urgent_views); + list_stable_sort(urgent_views, cmp_urgent); + struct sway_view *target; + if (criteria->urgent == 'o') { // oldest + target = urgent_views->items[0]; + } else { // latest + target = urgent_views->items[urgent_views->length - 1]; + } + list_free(urgent_views); + if (view != target) { + return false; + } + } + + if (criteria->workspace) { + struct sway_workspace *ws = view->container->pending.workspace; + if (!ws) { + return false; + } + + switch (criteria->workspace->match_type) { + case PATTERN_FOCUSED: + if (!focused || + strcmp(ws->name, focused->container->pending.workspace->name)) { + return false; + } + break; + case PATTERN_PCRE2: + if (regex_cmp(ws->name, criteria->workspace->regex) < 0) { + return false; + } + break; + } + } + + if (criteria->pid) { + if (criteria->pid != view->pid) { + return false; + } + } + + return true; +} + +list_t *criteria_for_view(struct sway_view *view, enum criteria_type types) { + list_t *criterias = config->criteria; + list_t *matches = create_list(); + for (int i = 0; i < criterias->length; ++i) { + struct criteria *criteria = criterias->items[i]; + if ((criteria->type & types) && criteria_matches_view(criteria, view)) { + list_add(matches, criteria); + } + } + return matches; +} + +struct match_data { + struct criteria *criteria; + list_t *matches; +}; + +static void criteria_get_containers_iterator(struct sway_container *container, + void *data) { + struct match_data *match_data = data; + if (container->view) { + if (criteria_matches_view(match_data->criteria, container->view)) { + list_add(match_data->matches, container); + } + } else if (has_container_criteria(match_data->criteria)) { + if (criteria_matches_container(match_data->criteria, container)) { + list_add(match_data->matches, container); + } + } +} + +list_t *criteria_get_containers(struct criteria *criteria) { + list_t *matches = create_list(); + struct match_data data = { + .criteria = criteria, + .matches = matches, + }; + root_for_each_container(criteria_get_containers_iterator, &data); + return matches; +} + +#if WLR_HAS_XWAYLAND +static enum atom_name parse_window_type(const char *type) { + if (strcasecmp(type, "normal") == 0) { + return NET_WM_WINDOW_TYPE_NORMAL; + } else if (strcasecmp(type, "dialog") == 0) { + return NET_WM_WINDOW_TYPE_DIALOG; + } else if (strcasecmp(type, "utility") == 0) { + return NET_WM_WINDOW_TYPE_UTILITY; + } else if (strcasecmp(type, "toolbar") == 0) { + return NET_WM_WINDOW_TYPE_TOOLBAR; + } else if (strcasecmp(type, "splash") == 0) { + return NET_WM_WINDOW_TYPE_SPLASH; + } else if (strcasecmp(type, "menu") == 0) { + return NET_WM_WINDOW_TYPE_MENU; + } else if (strcasecmp(type, "dropdown_menu") == 0) { + return NET_WM_WINDOW_TYPE_DROPDOWN_MENU; + } else if (strcasecmp(type, "popup_menu") == 0) { + return NET_WM_WINDOW_TYPE_POPUP_MENU; + } else if (strcasecmp(type, "tooltip") == 0) { + return NET_WM_WINDOW_TYPE_TOOLTIP; + } else if (strcasecmp(type, "notification") == 0) { + return NET_WM_WINDOW_TYPE_NOTIFICATION; + } + return ATOM_LAST; // ie. invalid +} +#endif + +enum criteria_token { + T_ALL, + T_APP_ID, + T_CON_ID, + T_CON_MARK, + T_FLOATING, +#if WLR_HAS_XWAYLAND + T_CLASS, + T_ID, + T_INSTANCE, + T_WINDOW_ROLE, + T_WINDOW_TYPE, +#endif + T_SHELL, + T_TILING, + T_TITLE, + T_URGENT, + T_WORKSPACE, + T_PID, + T_SANDBOX_ENGINE, + T_SANDBOX_APP_ID, + T_SANDBOX_INSTANCE_ID, + T_TAG, + + T_INVALID, +}; + +static enum criteria_token token_from_name(char *name) { + if (strcmp(name, "all") == 0) { + return T_ALL; + } else if (strcmp(name, "app_id") == 0) { + return T_APP_ID; + } else if (strcmp(name, "con_id") == 0) { + return T_CON_ID; + } else if (strcmp(name, "con_mark") == 0) { + return T_CON_MARK; +#if WLR_HAS_XWAYLAND + } else if (strcmp(name, "class") == 0) { + return T_CLASS; + } else if (strcmp(name, "id") == 0) { + return T_ID; + } else if (strcmp(name, "instance") == 0) { + return T_INSTANCE; + } else if (strcmp(name, "window_role") == 0) { + return T_WINDOW_ROLE; + } else if (strcmp(name, "window_type") == 0) { + return T_WINDOW_TYPE; +#endif + } else if (strcmp(name, "shell") == 0) { + return T_SHELL; + } else if (strcmp(name, "title") == 0) { + return T_TITLE; + } else if (strcmp(name, "urgent") == 0) { + return T_URGENT; + } else if (strcmp(name, "workspace") == 0) { + return T_WORKSPACE; + } else if (strcmp(name, "tiling") == 0) { + return T_TILING; + } else if (strcmp(name, "floating") == 0) { + return T_FLOATING; + } else if (strcmp(name, "pid") == 0) { + return T_PID; + } else if (strcmp(name, "sandbox_engine") == 0) { + return T_SANDBOX_ENGINE; + } else if (strcmp(name, "sandbox_app_id") == 0) { + return T_SANDBOX_APP_ID; + } else if (strcmp(name, "sandbox_instance_id") == 0) { + return T_SANDBOX_INSTANCE_ID; + } else if (strcmp(name, "tag") == 0) { + return T_TAG; + } + return T_INVALID; +} + +static bool parse_token(struct criteria *criteria, char *name, char *value) { + enum criteria_token token = token_from_name(name); + if (token == T_INVALID) { + const char *fmt = "Token '%s' is not recognized"; + int len = strlen(fmt) + strlen(name) - 1; + error = malloc(len); + snprintf(error, len, fmt, name); + return false; + } + + // Require value, unless token is all, floating or tiled + if (!value && token != T_ALL && token != T_FLOATING && token != T_TILING) { + const char *fmt = "Token '%s' requires a value"; + int len = strlen(fmt) + strlen(name) - 1; + error = malloc(len); + snprintf(error, len, fmt, name); + return false; + } + + char *endptr = NULL; + switch (token) { + case T_ALL: + criteria->all = true; + break; + case T_TITLE: + pattern_create(&criteria->title, value); + break; + case T_SHELL: + pattern_create(&criteria->shell, value); + break; + case T_APP_ID: + pattern_create(&criteria->app_id, value); + break; + case T_CON_ID: + if (strcmp(value, "__focused__") == 0) { + struct sway_seat *seat = input_manager_current_seat(); + struct sway_container *focus = seat_get_focused_container(seat); + criteria->con_id = focus ? focus->node.id : 0; + } else { + criteria->con_id = strtoul(value, &endptr, 10); + if (*endptr != 0) { + error = strdup("The value for 'con_id' should be '__focused__' or numeric"); + } + } + break; + case T_CON_MARK: + pattern_create(&criteria->con_mark, value); + break; +#if WLR_HAS_XWAYLAND + case T_CLASS: + pattern_create(&criteria->class, value); + break; + case T_ID: + criteria->id = strtoul(value, &endptr, 10); + if (*endptr != 0) { + error = strdup("The value for 'id' should be numeric"); + } + break; + case T_INSTANCE: + pattern_create(&criteria->instance, value); + break; + case T_WINDOW_ROLE: + pattern_create(&criteria->window_role, value); + break; + case T_WINDOW_TYPE: + criteria->window_type = parse_window_type(value); + break; +#endif + case T_FLOATING: + criteria->floating = true; + break; + case T_TILING: + criteria->tiling = true; + break; + case T_URGENT: + if (strcmp(value, "latest") == 0 || + strcmp(value, "newest") == 0 || + strcmp(value, "last") == 0 || + strcmp(value, "recent") == 0) { + criteria->urgent = 'l'; + } else if (strcmp(value, "oldest") == 0 || + strcmp(value, "first") == 0) { + criteria->urgent = 'o'; + } else { + error = + strdup("The value for 'urgent' must be 'first', 'last', " + "'latest', 'newest', 'oldest' or 'recent'"); + } + break; + case T_WORKSPACE: + pattern_create(&criteria->workspace, value); + break; + case T_PID: + criteria->pid = strtoul(value, &endptr, 10); + if (*endptr != 0) { + error = strdup("The value for 'pid' should be numeric"); + } + break; + case T_SANDBOX_ENGINE: + pattern_create(&criteria->sandbox_engine, value); + break; + case T_SANDBOX_APP_ID: + pattern_create(&criteria->sandbox_app_id, value); + break; + case T_SANDBOX_INSTANCE_ID: + pattern_create(&criteria->sandbox_instance_id, value); + break; + case T_TAG: + pattern_create(&criteria->tag, value); + break; + case T_INVALID: + break; + } + + if (error) { + return false; + } + + return true; +} + +static void skip_spaces(char **head) { + while (**head == ' ') { + ++*head; + } +} + +// Remove escaping slashes from value +static void unescape(char *value) { + if (!strchr(value, '\\')) { + return; + } + char *copy = calloc(strlen(value) + 1, 1); + char *readhead = value; + char *writehead = copy; + while (*readhead) { + if (*readhead == '\\' && *(readhead + 1) == '"') { + // skip the slash + ++readhead; + } + *writehead = *readhead; + ++writehead; + ++readhead; + } + strcpy(value, copy); + free(copy); +} + +/** + * Parse a raw criteria string such as [class="foo" instance="bar"] into a + * criteria struct. + * + * If errors are found, NULL will be returned and the error argument will be + * populated with an error string. It is up to the caller to free the error. + */ +struct criteria *criteria_parse(char *raw, char **error_arg) { + *error_arg = NULL; + error = NULL; + + char *head = raw; + skip_spaces(&head); + if (*head != '[') { + *error_arg = strdup("No criteria"); + return NULL; + } + ++head; + + struct criteria *criteria = calloc(1, sizeof(struct criteria)); +#if WLR_HAS_XWAYLAND + criteria->window_type = ATOM_LAST; // default value +#endif + char *name = NULL, *value = NULL; + bool in_quotes = false; + + while (*head && *head != ']') { + skip_spaces(&head); + // Parse token name + char *namestart = head; + while ((*head >= 'a' && *head <= 'z') || *head == '_') { + ++head; + } + name = calloc(head - namestart + 1, 1); + if (head != namestart) { + memcpy(name, namestart, head - namestart); + } + // Parse token value + skip_spaces(&head); + value = NULL; + if (*head == '=') { + ++head; + skip_spaces(&head); + if (*head == '"') { + in_quotes = true; + ++head; + } + char *valuestart = head; + if (in_quotes) { + while (*head && (*head != '"' || *(head - 1) == '\\')) { + ++head; + } + if (!*head) { + *error_arg = strdup("Quote mismatch in criteria"); + goto cleanup; + } + } else { + while (*head && *head != ' ' && *head != ']') { + ++head; + } + } + value = calloc(head - valuestart + 1, 1); + memcpy(value, valuestart, head - valuestart); + if (in_quotes) { + ++head; + in_quotes = false; + } + unescape(value); + sway_log(SWAY_DEBUG, "Found pair: %s=%s", name, value); + } + if (!parse_token(criteria, name, value)) { + *error_arg = error; + goto cleanup; + } + skip_spaces(&head); + free(name); + free(value); + name = NULL; + value = NULL; + } + if (*head != ']') { + *error_arg = strdup("No closing brace found in criteria"); + goto cleanup; + } + + if (criteria_is_empty(criteria)) { + *error_arg = strdup("Criteria is empty"); + goto cleanup; + } + + ++head; + int len = head - raw; + criteria->raw = calloc(len + 1, 1); + memcpy(criteria->raw, raw, len); + return criteria; + +cleanup: + free(name); + free(value); + criteria_destroy(criteria); + return NULL; +} + +bool criteria_is_equal(struct criteria *left, struct criteria *right) { + if (left->type != right->type) { + return false; + } + // XXX Only implemented for CT_NO_FOCUS for now. + if (left->type == CT_NO_FOCUS) { + return strcmp(left->raw, right->raw) == 0; + } + if (left->type == CT_COMMAND) { + return strcmp(left->raw, right->raw) == 0 + && strcmp(left->cmdlist, right->cmdlist) == 0; + } + return false; +} + +bool criteria_already_exists(struct criteria *criteria) { + // XXX Only implemented for CT_NO_FOCUS and CT_COMMAND for now. + // While criteria_is_equal also obeys this limitation, this is a shortcut + // to avoid processing the list. + if (criteria->type != CT_NO_FOCUS && criteria->type != CT_COMMAND) { + return false; + } + + list_t *criterias = config->criteria; + for (int i = 0; i < criterias->length; ++i) { + struct criteria *existing = criterias->items[i]; + if (criteria_is_equal(criteria, existing)) { + return true; + } + } + return false; +} diff --git a/sway/decoration.c b/sway/decoration.c new file mode 100644 index 0000000..849fa89 --- /dev/null +++ b/sway/decoration.c @@ -0,0 +1,65 @@ +#include +#include "sway/decoration.h" +#include "sway/desktop/transaction.h" +#include "sway/server.h" +#include "sway/tree/arrange.h" +#include "sway/tree/view.h" +#include "log.h" + +static void server_decoration_handle_destroy(struct wl_listener *listener, + void *data) { + struct sway_server_decoration *deco = + wl_container_of(listener, deco, destroy); + wl_list_remove(&deco->destroy.link); + wl_list_remove(&deco->mode.link); + wl_list_remove(&deco->link); + free(deco); +} + +static void server_decoration_handle_mode(struct wl_listener *listener, + void *data) { + struct sway_server_decoration *deco = + wl_container_of(listener, deco, mode); + struct sway_view *view = + view_from_wlr_surface(deco->wlr_server_decoration->surface); + if (view == NULL || view->surface != deco->wlr_server_decoration->surface) { + return; + } + + bool csd = deco->wlr_server_decoration->mode == + WLR_SERVER_DECORATION_MANAGER_MODE_CLIENT; + view_update_csd_from_client(view, csd); + + arrange_container(view->container); + transaction_commit_dirty(); +} + +void handle_server_decoration(struct wl_listener *listener, void *data) { + struct wlr_server_decoration *wlr_deco = data; + + struct sway_server_decoration *deco = calloc(1, sizeof(*deco)); + if (deco == NULL) { + return; + } + + deco->wlr_server_decoration = wlr_deco; + + wl_signal_add(&wlr_deco->events.destroy, &deco->destroy); + deco->destroy.notify = server_decoration_handle_destroy; + + wl_signal_add(&wlr_deco->events.mode, &deco->mode); + deco->mode.notify = server_decoration_handle_mode; + + wl_list_insert(&server.decorations, &deco->link); +} + +struct sway_server_decoration *decoration_from_surface( + struct wlr_surface *surface) { + struct sway_server_decoration *deco; + wl_list_for_each(deco, &server.decorations, link) { + if (deco->wlr_server_decoration->surface == surface) { + return deco; + } + } + return NULL; +} diff --git a/sway/desktop/idle_inhibit_v1.c b/sway/desktop/idle_inhibit_v1.c new file mode 100644 index 0000000..6b2761f --- /dev/null +++ b/sway/desktop/idle_inhibit_v1.c @@ -0,0 +1,194 @@ +#include +#include +#include +#include "log.h" +#include "sway/desktop/idle_inhibit_v1.h" +#include "sway/input/seat.h" +#include "sway/tree/container.h" +#include "sway/tree/view.h" +#include "sway/server.h" + + +static void destroy_inhibitor(struct sway_idle_inhibitor_v1 *inhibitor) { + wl_list_remove(&inhibitor->link); + wl_list_remove(&inhibitor->destroy.link); + sway_idle_inhibit_v1_check_active(); + free(inhibitor); +} + +static void handle_destroy(struct wl_listener *listener, void *data) { + struct sway_idle_inhibitor_v1 *inhibitor = + wl_container_of(listener, inhibitor, destroy); + sway_log(SWAY_DEBUG, "Sway idle inhibitor destroyed"); + destroy_inhibitor(inhibitor); +} + +void handle_idle_inhibitor_v1(struct wl_listener *listener, void *data) { + struct wlr_idle_inhibitor_v1 *wlr_inhibitor = data; + struct sway_idle_inhibit_manager_v1 *manager = + wl_container_of(listener, manager, new_idle_inhibitor_v1); + sway_log(SWAY_DEBUG, "New sway idle inhibitor"); + + struct sway_idle_inhibitor_v1 *inhibitor = + calloc(1, sizeof(struct sway_idle_inhibitor_v1)); + if (!inhibitor) { + return; + } + + inhibitor->mode = INHIBIT_IDLE_APPLICATION; + inhibitor->wlr_inhibitor = wlr_inhibitor; + wl_list_insert(&manager->inhibitors, &inhibitor->link); + + inhibitor->destroy.notify = handle_destroy; + wl_signal_add(&wlr_inhibitor->events.destroy, &inhibitor->destroy); + + sway_idle_inhibit_v1_check_active(); +} + +void handle_manager_destroy(struct wl_listener *listener, void *data) { + struct sway_idle_inhibit_manager_v1 *manager = + wl_container_of(listener, manager, manager_destroy); + + wl_list_remove(&manager->manager_destroy.link); + wl_list_remove(&manager->new_idle_inhibitor_v1.link); +} + +void sway_idle_inhibit_v1_user_inhibitor_register(struct sway_view *view, + enum sway_idle_inhibit_mode mode) { + struct sway_idle_inhibit_manager_v1 *manager = &server.idle_inhibit_manager_v1; + + struct sway_idle_inhibitor_v1 *inhibitor = + calloc(1, sizeof(struct sway_idle_inhibitor_v1)); + if (!inhibitor) { + return; + } + + inhibitor->mode = mode; + inhibitor->view = view; + wl_list_insert(&manager->inhibitors, &inhibitor->link); + + inhibitor->destroy.notify = handle_destroy; + wl_signal_add(&view->events.unmap, &inhibitor->destroy); + + sway_idle_inhibit_v1_check_active(); +} + +struct sway_idle_inhibitor_v1 *sway_idle_inhibit_v1_user_inhibitor_for_view( + struct sway_view *view) { + struct sway_idle_inhibit_manager_v1 *manager = &server.idle_inhibit_manager_v1; + struct sway_idle_inhibitor_v1 *inhibitor; + wl_list_for_each(inhibitor, &manager->inhibitors, link) { + if (inhibitor->mode != INHIBIT_IDLE_APPLICATION && + inhibitor->view == view) { + return inhibitor; + } + } + return NULL; +} + +struct sway_idle_inhibitor_v1 *sway_idle_inhibit_v1_application_inhibitor_for_view( + struct sway_view *view) { + struct sway_idle_inhibit_manager_v1 *manager = &server.idle_inhibit_manager_v1; + struct sway_idle_inhibitor_v1 *inhibitor; + wl_list_for_each(inhibitor, &manager->inhibitors, link) { + if (inhibitor->mode == INHIBIT_IDLE_APPLICATION && + view_from_wlr_surface(inhibitor->wlr_inhibitor->surface) == view) { + return inhibitor; + } + } + return NULL; +} + +void sway_idle_inhibit_v1_user_inhibitor_destroy( + struct sway_idle_inhibitor_v1 *inhibitor) { + if (!inhibitor) { + return; + } + if (!sway_assert(inhibitor->mode != INHIBIT_IDLE_APPLICATION, + "User should not be able to destroy application inhibitor")) { + return; + } + destroy_inhibitor(inhibitor); +} + +bool sway_idle_inhibit_v1_is_active(struct sway_idle_inhibitor_v1 *inhibitor) { + if (server.session_lock.lock) { + // A session lock is active. In this case, only application inhibitors + // on the session lock surface can have any effect. + if (inhibitor->mode != INHIBIT_IDLE_APPLICATION) { + return false; + } + struct wlr_surface *wlr_surface = inhibitor->wlr_inhibitor->surface; + if (!wlr_session_lock_surface_v1_try_from_wlr_surface(wlr_surface)) { + return false; + } + return wlr_surface->mapped; + } + + switch (inhibitor->mode) { + case INHIBIT_IDLE_APPLICATION:; + struct wlr_surface *wlr_surface = inhibitor->wlr_inhibitor->surface; + struct wlr_layer_surface_v1 *layer_surface = + wlr_layer_surface_v1_try_from_wlr_surface(wlr_surface); + if (layer_surface) { + // Layer surfaces can be occluded but are always on screen after + // they have been mapped. + return layer_surface->output && layer_surface->output->enabled && + wlr_surface->mapped; + } + + // If there is no view associated with the inhibitor, assume invisible + struct sway_view *view = view_from_wlr_surface(wlr_surface); + return view && view->container && view_is_visible(view); + case INHIBIT_IDLE_FOCUS:; + struct sway_seat *seat = NULL; + wl_list_for_each(seat, &server.input->seats, link) { + struct sway_container *con = seat_get_focused_container(seat); + if (con && con->view && con->view == inhibitor->view) { + return true; + } + } + return false; + case INHIBIT_IDLE_FULLSCREEN: + return inhibitor->view->container && + container_is_fullscreen_or_child(inhibitor->view->container) && + view_is_visible(inhibitor->view); + case INHIBIT_IDLE_OPEN: + // Inhibitor is destroyed on unmap so it must be open/mapped + return true; + case INHIBIT_IDLE_VISIBLE: + return view_is_visible(inhibitor->view); + } + return false; +} + +void sway_idle_inhibit_v1_check_active(void) { + struct sway_idle_inhibit_manager_v1 *manager = &server.idle_inhibit_manager_v1; + struct sway_idle_inhibitor_v1 *inhibitor; + bool inhibited = false; + wl_list_for_each(inhibitor, &manager->inhibitors, link) { + if ((inhibited = sway_idle_inhibit_v1_is_active(inhibitor))) { + break; + } + } + wlr_idle_notifier_v1_set_inhibited(server.idle_notifier_v1, inhibited); +} + +bool sway_idle_inhibit_manager_v1_init(void) { + struct sway_idle_inhibit_manager_v1 *manager = &server.idle_inhibit_manager_v1; + + manager->wlr_manager = wlr_idle_inhibit_v1_create(server.wl_display); + if (!manager->wlr_manager) { + return false; + } + + wl_signal_add(&manager->wlr_manager->events.new_inhibitor, + &manager->new_idle_inhibitor_v1); + manager->new_idle_inhibitor_v1.notify = handle_idle_inhibitor_v1; + wl_signal_add(&manager->wlr_manager->events.destroy, + &manager->manager_destroy); + manager->manager_destroy.notify = handle_manager_destroy; + wl_list_init(&manager->inhibitors); + + return true; +} diff --git a/sway/desktop/launcher.c b/sway/desktop/launcher.c new file mode 100644 index 0000000..2362e1b --- /dev/null +++ b/sway/desktop/launcher.c @@ -0,0 +1,268 @@ +#include +#include +#include +#include "sway/input/seat.h" +#include "sway/output.h" +#include "sway/desktop/launcher.h" +#include "sway/server.h" +#include "sway/tree/node.h" +#include "sway/tree/container.h" +#include "sway/tree/workspace.h" +#include "sway/tree/root.h" +#include "log.h" + +/** + * Get the pid of a parent process given the pid of a child process. + * + * Returns the parent pid or NULL if the parent pid cannot be determined. + */ +static pid_t get_parent_pid(pid_t child) { + pid_t parent = -1; + char file_name[100]; + char *buffer = NULL; + const char *sep = " "; + FILE *stat = NULL; + size_t buf_size = 0; + + snprintf(file_name, sizeof(file_name), "/proc/%d/stat", child); + + if ((stat = fopen(file_name, "r"))) { + if (getline(&buffer, &buf_size, stat) != -1) { + strtok(buffer, sep); // pid + strtok(NULL, sep); // executable name + strtok(NULL, sep); // state + char *token = strtok(NULL, sep); // parent pid + parent = strtol(token, NULL, 10); + } + free(buffer); + fclose(stat); + } + + if (parent) { + return (parent == child) ? -1 : parent; + } + + return -1; +} + +void launcher_ctx_consume(struct launcher_ctx *ctx) { + // The view is now responsible for destroying this ctx + wl_list_remove(&ctx->token_destroy.link); + wl_list_init(&ctx->token_destroy.link); + + if (!ctx->activated) { + // An unactivated token hasn't been destroyed yet + wlr_xdg_activation_token_v1_destroy(ctx->token); + } + ctx->token = NULL; + + // Prevent additional matches + wl_list_remove(&ctx->link); + wl_list_init(&ctx->link); +} + +void launcher_ctx_destroy(struct launcher_ctx *ctx) { + if (ctx == NULL) { + return; + } + wl_list_remove(&ctx->node_destroy.link); + wl_list_remove(&ctx->token_destroy.link); + if (ctx->seat) { + wl_list_remove(&ctx->seat_destroy.link); + } + wl_list_remove(&ctx->link); + wlr_xdg_activation_token_v1_destroy(ctx->token); + free(ctx->fallback_name); + free(ctx); +} + +struct launcher_ctx *launcher_ctx_find_pid(pid_t pid) { + if (wl_list_empty(&server.pending_launcher_ctxs)) { + return NULL; + } + + struct launcher_ctx *ctx = NULL; + sway_log(SWAY_DEBUG, "Looking up workspace for pid %d", pid); + + do { + struct launcher_ctx *_ctx = NULL; + wl_list_for_each(_ctx, &server.pending_launcher_ctxs, link) { + if (pid == _ctx->pid) { + ctx = _ctx; + sway_log(SWAY_DEBUG, + "found %s match for pid %d: %s", + node_type_to_str(ctx->node->type), pid, node_get_name(ctx->node)); + break; + } + } + pid = get_parent_pid(pid); + } while (pid > 1); + + return ctx; +} + +struct sway_workspace *launcher_ctx_get_workspace( + struct launcher_ctx *ctx) { + struct sway_workspace *ws = NULL; + struct sway_output *output = NULL; + + switch (ctx->node->type) { + case N_CONTAINER: + // Unimplemented + // TODO: add container matching? + ws = ctx->node->sway_container->pending.workspace; + break; + case N_WORKSPACE: + ws = ctx->node->sway_workspace; + break; + case N_OUTPUT: + output = ctx->node->sway_output; + ws = workspace_by_name(ctx->fallback_name); + if (!ws) { + sway_log(SWAY_DEBUG, + "Creating workspace %s for pid %d because it disappeared", + ctx->fallback_name, ctx->pid); + if (!output->enabled) { + sway_log(SWAY_DEBUG, + "Workspace output %s is disabled, trying another one", + output->wlr_output->name); + output = NULL; + } + ws = workspace_create(output, ctx->fallback_name); + } + break; + case N_ROOT: + ws = workspace_create(NULL, ctx->fallback_name); + break; + } + + return ws; +} + +static void ctx_handle_node_destroy(struct wl_listener *listener, void *data) { + struct launcher_ctx *ctx = wl_container_of(listener, ctx, node_destroy); + switch (ctx->node->type) { + case N_CONTAINER: + // Unimplemented + break; + case N_WORKSPACE:; + struct sway_workspace *ws = ctx->node->sway_workspace; + wl_list_remove(&ctx->node_destroy.link); + wl_list_init(&ctx->node_destroy.link); + // We want to save this ws name to recreate later, hopefully on the + // same output + free(ctx->fallback_name); + ctx->fallback_name = strdup(ws->name); + if (!ws->output || ws->output->node.destroying) { + // If the output is being destroyed it would be pointless to track + // If the output is being disabled, we'll find out if it's still + // disabled when we try to match it. + ctx->node = &root->node; + break; + } + ctx->node = &ws->output->node; + wl_signal_add(&ctx->node->events.destroy, &ctx->node_destroy); + break; + case N_OUTPUT: + wl_list_remove(&ctx->node_destroy.link); + wl_list_init(&ctx->node_destroy.link); + // We'll make the ws ctx->name somewhere else + ctx->node = &root->node; + break; + case N_ROOT: + // Unreachable + break; + } +} + +static void token_handle_destroy(struct wl_listener *listener, void *data) { + struct launcher_ctx *ctx = wl_container_of(listener, ctx, token_destroy); + ctx->token = NULL; + launcher_ctx_destroy(ctx); +} + +struct launcher_ctx *launcher_ctx_create(struct wlr_xdg_activation_token_v1 *token, + struct sway_node *node) { + struct launcher_ctx *ctx = calloc(1, sizeof(struct launcher_ctx)); + + const char *fallback_name = NULL; + struct sway_workspace *ws = NULL; + switch (node->type) { + case N_CONTAINER: + // Unimplemented + free(ctx); + return NULL; + case N_WORKSPACE: + ws = node->sway_workspace; + fallback_name = ws->name; + break; + case N_OUTPUT:; + struct sway_output *output = node->sway_output; + ws = output_get_active_workspace(output); + fallback_name = ws ? ws->name : NULL; + break; + case N_ROOT: + // Unimplemented + free(ctx); + return NULL; + } + + if (!fallback_name) { + // TODO: implement a better fallback. + free(ctx); + return NULL; + } + + ctx->fallback_name = strdup(fallback_name); + ctx->token = token; + ctx->node = node; + // Having surface set means that the focus check in wlroots has passed + ctx->had_focused_surface = token->surface != NULL; + + ctx->node_destroy.notify = ctx_handle_node_destroy; + wl_signal_add(&ctx->node->events.destroy, &ctx->node_destroy); + + ctx->token_destroy.notify = token_handle_destroy; + wl_signal_add(&token->events.destroy, &ctx->token_destroy); + + wl_list_init(&ctx->link); + wl_list_insert(&server.pending_launcher_ctxs, &ctx->link); + + token->data = ctx; + return ctx; +} + +static void launch_ctx_handle_seat_destroy(struct wl_listener *listener, void *data) { + struct launcher_ctx *ctx = wl_container_of(listener, ctx, seat_destroy); + ctx->seat = NULL; + wl_list_remove(&ctx->seat_destroy.link); +} + +// Creates a context with a new token for the internal launcher +struct launcher_ctx *launcher_ctx_create_internal(void) { + struct sway_seat *seat = input_manager_current_seat(); + struct sway_workspace *ws = seat_get_focused_workspace(seat); + if (!ws) { + sway_log(SWAY_DEBUG, "Failed to create launch context. No workspace."); + return NULL; + } + + struct wlr_xdg_activation_token_v1 *token = + wlr_xdg_activation_token_v1_create(server.xdg_activation_v1); + + struct launcher_ctx *ctx = launcher_ctx_create(token, &ws->node); + if (!ctx) { + wlr_xdg_activation_token_v1_destroy(token); + return NULL; + } + ctx->seat = seat; + ctx->seat_destroy.notify = launch_ctx_handle_seat_destroy; + wl_signal_add(&seat->wlr_seat->events.destroy, &ctx->seat_destroy); + + return ctx; +} + +const char *launcher_ctx_get_token_name(struct launcher_ctx *ctx) { + const char *token = wlr_xdg_activation_token_v1_get_name(ctx->token); + return token; +} diff --git a/sway/desktop/layer_shell.c b/sway/desktop/layer_shell.c new file mode 100644 index 0000000..3cd6ddd --- /dev/null +++ b/sway/desktop/layer_shell.c @@ -0,0 +1,598 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "log.h" +#include "sway/scene_descriptor.h" +#include "sway/desktop/transaction.h" +#include "sway/input/cursor.h" +#include "sway/input/input-manager.h" +#include "sway/input/seat.h" +#include "sway/layers.h" +#include "sway/output.h" +#include "sway/server.h" +#include "sway/tree/arrange.h" +#include "sway/tree/workspace.h" + +struct wlr_layer_surface_v1 *toplevel_layer_surface_from_surface( + struct wlr_surface *surface) { + struct wlr_layer_surface_v1 *layer; + do { + if (!surface) { + return NULL; + } + // Topmost layer surface + if ((layer = wlr_layer_surface_v1_try_from_wlr_surface(surface))) { + return layer; + } + // Layer subsurface + if (wlr_subsurface_try_from_wlr_surface(surface)) { + surface = wlr_surface_get_root_surface(surface); + continue; + } + + // Layer surface popup + struct wlr_xdg_surface *xdg_surface = NULL; + if ((xdg_surface = wlr_xdg_surface_try_from_wlr_surface(surface)) && + xdg_surface->role == WLR_XDG_SURFACE_ROLE_POPUP && xdg_surface->popup != NULL) { + if (!xdg_surface->popup->parent) { + return NULL; + } + surface = wlr_surface_get_root_surface(xdg_surface->popup->parent); + continue; + } + + // Return early if the surface is not a layer/xdg_popup/sub surface + return NULL; + } while (true); +} + +void layer_apply_criteria(struct sway_layer_surface *surface, struct layer_criteria *criteria) { + if (criteria) { + surface->corner_radius = criteria->corner_radius; + surface->blur_enabled = criteria->blur_enabled; + surface->blur_xray = criteria->blur_xray; + surface->blur_ignore_transparent = criteria->blur_ignore_transparent; + surface->shadow_enabled = criteria->shadow_enabled; + } else { + // Reset + surface->corner_radius = 0; + surface->blur_enabled = false; + surface->blur_xray = false; + surface->blur_ignore_transparent = false; + surface->shadow_enabled = false; + } +} + +static void layer_parse_criteria(struct sway_layer_surface *surface) { + if (!surface || !surface->layer_surface + || surface->layer_surface->current.layer < ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM) { + return; + } + struct layer_criteria *criteria = layer_criteria_for_namespace(surface->layer_surface->namespace); + layer_apply_criteria(surface, criteria); +} + +static void arrange_surface(struct sway_output *output, const struct wlr_box *full_area, + struct wlr_box *usable_area, struct wlr_scene_tree *tree, bool exclusive) { + struct wlr_scene_node *node; + wl_list_for_each(node, &tree->children, link) { + struct sway_layer_surface *surface = scene_descriptor_try_get(node, + SWAY_SCENE_DESC_LAYER_SHELL); + // surface could be null during destruction + if (!surface) { + continue; + } + + if (!surface->scene->layer_surface->initialized) { + continue; + } + + if ((surface->scene->layer_surface->current.exclusive_zone > 0) != exclusive) { + continue; + } + + wlr_scene_node_set_enabled(&surface->blur_node->node, surface->blur_enabled); + wlr_scene_blur_set_size(surface->blur_node, surface->layer_surface->surface->current.width, + surface->layer_surface->surface->current.height); + + wlr_scene_node_set_enabled(&surface->shadow_node->node, surface->shadow_enabled); + + if (surface->shadow_enabled) { + // Adjust the size and position of the shadow node + wlr_scene_shadow_set_size(surface->shadow_node, + surface->layer_surface->surface->current.width + config->shadow_blur_sigma * 2, + surface->layer_surface->surface->current.height + config->shadow_blur_sigma * 2); + int x = config->shadow_offset_x - config->shadow_blur_sigma; + int y = config->shadow_offset_y - config->shadow_blur_sigma; + wlr_scene_node_set_position(&surface->shadow_node->node, x, y); + + wlr_scene_shadow_set_clipped_region(surface->shadow_node, (struct clipped_region) { + .corners = corner_radii_all(surface->corner_radius), + .area = { + .x = -x, + .y = -y, + .width = surface->layer_surface->surface->current.width, + .height = surface->layer_surface->surface->current.height, + }, + }); + } + + wlr_scene_layer_surface_v1_configure(surface->scene, full_area, usable_area); + } +} + +void arrange_layers(struct sway_output *output) { + struct wlr_box usable_area = { 0 }; + wlr_output_effective_resolution(output->wlr_output, + &usable_area.width, &usable_area.height); + const struct wlr_box full_area = usable_area; + + arrange_surface(output, &full_area, &usable_area, output->layers.shell_overlay, true); + arrange_surface(output, &full_area, &usable_area, output->layers.shell_top, true); + arrange_surface(output, &full_area, &usable_area, output->layers.shell_bottom, true); + arrange_surface(output, &full_area, &usable_area, output->layers.shell_background, true); + + arrange_surface(output, &full_area, &usable_area, output->layers.shell_overlay, false); + arrange_surface(output, &full_area, &usable_area, output->layers.shell_top, false); + arrange_surface(output, &full_area, &usable_area, output->layers.shell_bottom, false); + arrange_surface(output, &full_area, &usable_area, output->layers.shell_background, false); + + if (!wlr_box_equal(&usable_area, &output->usable_area)) { + sway_log(SWAY_DEBUG, "Usable area changed, rearranging output"); + output->usable_area = usable_area; + arrange_output(output); + } else { + arrange_popups(root->layers.popup); + } + + // Find topmost keyboard interactive layer, if such a layer exists + struct wlr_scene_tree *layers_above_shell[] = { + output->layers.shell_overlay, + output->layers.shell_top, + }; + size_t nlayers = sizeof(layers_above_shell) / sizeof(layers_above_shell[0]); + struct wlr_scene_node *node; + struct sway_layer_surface *topmost = NULL; + for (size_t i = 0; i < nlayers; ++i) { + wl_list_for_each_reverse(node, + &layers_above_shell[i]->children, link) { + struct sway_layer_surface *surface = scene_descriptor_try_get(node, + SWAY_SCENE_DESC_LAYER_SHELL); + if (surface && surface->layer_surface->current.keyboard_interactive + == ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE && + surface->layer_surface->surface->mapped) { + topmost = surface; + break; + } + } + if (topmost != NULL) { + break; + } + } + + struct sway_seat *seat; + wl_list_for_each(seat, &server.input->seats, link) { + seat->has_exclusive_layer = false; + if (topmost != NULL) { + seat_set_focus_layer(seat, topmost->layer_surface); + } else if (seat->focused_layer && + seat->focused_layer->current.keyboard_interactive + != ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE) { + seat_set_focus_layer(seat, NULL); + } + } +} + +static struct wlr_scene_tree *sway_layer_get_scene(struct sway_output *output, + enum zwlr_layer_shell_v1_layer type) { + switch (type) { + case ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND: + return output->layers.shell_background; + case ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM: + return output->layers.shell_bottom; + case ZWLR_LAYER_SHELL_V1_LAYER_TOP: + return output->layers.shell_top; + case ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY: + return output->layers.shell_overlay; + } + + sway_assert(false, "unreachable"); + return NULL; +} + +static struct sway_layer_surface *sway_layer_surface_create( + struct wlr_scene_layer_surface_v1 *scene) { + struct sway_layer_surface *surface = calloc(1, sizeof(*surface)); + if (!surface) { + sway_log(SWAY_ERROR, "Could not allocate a scene_layer surface"); + return NULL; + } + + struct wlr_scene_tree *popups = wlr_scene_tree_create(root->layers.popup); + if (!popups) { + sway_log(SWAY_ERROR, "Could not allocate a scene_layer popup node"); + free(surface); + return NULL; + } + + surface->desc.relative = &scene->tree->node; + + if (!scene_descriptor_assign(&popups->node, + SWAY_SCENE_DESC_POPUP, &surface->desc)) { + sway_log(SWAY_ERROR, "Failed to allocate a popup scene descriptor"); + wlr_scene_node_destroy(&popups->node); + free(surface); + return NULL; + } + + surface->tree = scene->tree; + surface->scene = scene; + surface->layer_surface = scene->layer_surface; + surface->popups = popups; + surface->layer_surface->data = surface; + + // Effects + surface->corner_radius = 0; + surface->blur_enabled = false; + surface->blur_xray = false; + surface->blur_ignore_transparent = false; + surface->shadow_enabled = false; + + bool failed = false; + surface->blur_node = alloc_scene_blur(surface->tree, 0, 0, &failed); + surface->shadow_node = alloc_scene_shadow(surface->tree, 0, 0, + 0, config->shadow_blur_sigma, config->shadow_color, &failed); + if (failed) { + sway_log(SWAY_ERROR, "Failed to allocate a shadow node"); + wlr_scene_node_destroy(&popups->node); + free(surface); + return NULL; + } + + return surface; +} + +static struct sway_layer_surface *find_mapped_layer_by_client( + struct wl_client *client, struct sway_output *ignore_output) { + for (int i = 0; i < root->outputs->length; ++i) { + struct sway_output *output = root->outputs->items[i]; + if (output == ignore_output) { + continue; + } + // For now we'll only check the overlay layer + struct wlr_scene_node *node; + wl_list_for_each (node, &output->layers.shell_overlay->children, link) { + struct sway_layer_surface *surface = scene_descriptor_try_get(node, + SWAY_SCENE_DESC_LAYER_SHELL); + if (!surface) { + continue; + } + + struct wlr_layer_surface_v1 *layer_surface = surface->layer_surface; + struct wl_resource *resource = layer_surface->resource; + if (wl_resource_get_client(resource) == client + && layer_surface->surface->mapped) { + return surface; + } + } + } + return NULL; +} + +static void handle_node_destroy(struct wl_listener *listener, void *data) { + struct sway_layer_surface *layer = + wl_container_of(listener, layer, node_destroy); + + // destroy the scene descriptor straight away if it exists, otherwise + // we will try to reflow still considering the destroyed node. + scene_descriptor_destroy(&layer->tree->node, SWAY_SCENE_DESC_LAYER_SHELL); + + // Determine if this layer is being used by an exclusive client. If it is, + // try and find another layer owned by this client to pass focus to. + struct sway_seat *seat = input_manager_get_default_seat(); + struct wl_client *client = + wl_resource_get_client(layer->layer_surface->resource); + if (!server.session_lock.lock) { + struct sway_layer_surface *consider_layer = + find_mapped_layer_by_client(client, layer->output); + if (consider_layer) { + seat_set_focus_layer(seat, consider_layer->layer_surface); + } + } + + struct wlr_layer_surface_v1 *wlr_layer_surface = layer->layer_surface; + if (wlr_layer_surface->current.layer == ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND || + wlr_layer_surface->current.layer == ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM) { + if (layer->output) { + wlr_scene_optimized_blur_mark_dirty(layer->output->layers.blur_layer); + } + } + + if (layer->output) { + arrange_layers(layer->output); + transaction_commit_dirty(); + } + + wlr_scene_node_destroy(&layer->popups->node); + + wl_list_remove(&layer->map.link); + wl_list_remove(&layer->unmap.link); + wl_list_remove(&layer->surface_commit.link); + wl_list_remove(&layer->node_destroy.link); + wl_list_remove(&layer->new_popup.link); + + layer->layer_surface->data = NULL; + + wl_list_remove(&layer->link); + free(layer); +} + +static void handle_surface_commit(struct wl_listener *listener, void *data) { + struct sway_layer_surface *surface = + wl_container_of(listener, surface, surface_commit); + + struct wlr_layer_surface_v1 *layer_surface = surface->layer_surface; + + // Rerender the optimized blur on change + if (layer_surface->current.layer == + ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND || + layer_surface->current.layer == ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM) { + if (surface->output) { + wlr_scene_optimized_blur_mark_dirty(surface->output->layers.blur_layer); + } + } + + uint32_t committed = layer_surface->current.committed; + if (layer_surface->initialized && committed & WLR_LAYER_SURFACE_V1_STATE_LAYER) { + enum zwlr_layer_shell_v1_layer layer_type = layer_surface->current.layer; + struct wlr_scene_tree *output_layer = sway_layer_get_scene( + surface->output, layer_type); + wlr_scene_node_reparent(&surface->scene->tree->node, output_layer); + } + + if (layer_surface->initial_commit || committed || layer_surface->surface->mapped != surface->mapped) { + surface->mapped = layer_surface->surface->mapped; + arrange_layers(surface->output); + transaction_commit_dirty(); + } +} + +static void handle_map(struct wl_listener *listener, void *data) { + struct sway_layer_surface *surface = wl_container_of(listener, + surface, map); + + struct wlr_layer_surface_v1 *layer_surface = + surface->scene->layer_surface; + + layer_parse_criteria(surface); + wlr_scene_node_lower_to_bottom(&surface->shadow_node->node); + wlr_scene_node_lower_to_bottom(&surface->blur_node->node); + + // focus on new surface + if (layer_surface->current.keyboard_interactive && + (layer_surface->current.layer == ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY || + layer_surface->current.layer == ZWLR_LAYER_SHELL_V1_LAYER_TOP)) { + struct sway_seat *seat; + wl_list_for_each(seat, &server.input->seats, link) { + // but only if the currently focused layer has a lower precedence + if (!seat->focused_layer || + seat->focused_layer->current.layer >= layer_surface->current.layer) { + seat_set_focus_layer(seat, layer_surface); + } + } + arrange_layers(surface->output); + } + + cursor_rebase_all(); +} + +static void handle_unmap(struct wl_listener *listener, void *data) { + struct sway_layer_surface *surface = wl_container_of( + listener, surface, unmap); + struct sway_seat *seat; + wl_list_for_each(seat, &server.input->seats, link) { + if (seat->focused_layer == surface->layer_surface) { + seat_set_focus_layer(seat, NULL); + } + } + + cursor_rebase_all(); +} + +static void popup_handle_destroy(struct wl_listener *listener, void *data) { + struct sway_layer_popup *popup = + wl_container_of(listener, popup, destroy); + + wl_list_remove(&popup->destroy.link); + wl_list_remove(&popup->new_popup.link); + wl_list_remove(&popup->commit.link); + wl_list_remove(&popup->reposition.link); + free(popup); +} + +static void popup_unconstrain(struct sway_layer_popup *popup) { + struct wlr_xdg_popup *wlr_popup = popup->wlr_popup; + struct sway_output *output = popup->toplevel->output; + + // if a client tries to create a popup while we are in the process of destroying + // its output, don't crash. + if (!output) { + return; + } + + int lx, ly; + wlr_scene_node_coords(&popup->toplevel->scene->tree->node, &lx, &ly); + + // the output box expressed in the coordinate system of the toplevel parent + // of the popup + struct wlr_box output_toplevel_sx_box = { + .x = output->lx - lx, + .y = output->ly - ly, + .width = output->width, + .height = output->height, + }; + + wlr_xdg_popup_unconstrain_from_box(wlr_popup, &output_toplevel_sx_box); +} + +static void popup_handle_commit(struct wl_listener *listener, void *data) { + struct sway_layer_popup *popup = wl_container_of(listener, popup, commit); + if (popup->wlr_popup->base->initial_commit) { + popup_unconstrain(popup); + } +} + +static void popup_handle_reposition(struct wl_listener *listener, void *data) { + struct sway_layer_popup *popup = wl_container_of(listener, popup, reposition); + popup_unconstrain(popup); +} + +static void popup_handle_new_popup(struct wl_listener *listener, void *data); + +static struct sway_layer_popup *create_popup(struct wlr_xdg_popup *wlr_popup, + struct sway_layer_surface *toplevel, struct wlr_scene_tree *parent) { + struct sway_layer_popup *popup = calloc(1, sizeof(*popup)); + if (popup == NULL) { + return NULL; + } + + popup->toplevel = toplevel; + popup->wlr_popup = wlr_popup; + popup->scene = wlr_scene_xdg_surface_create(parent, + wlr_popup->base); + + if (!popup->scene) { + free(popup); + return NULL; + } + + popup->destroy.notify = popup_handle_destroy; + wl_signal_add(&wlr_popup->events.destroy, &popup->destroy); + popup->new_popup.notify = popup_handle_new_popup; + wl_signal_add(&wlr_popup->base->events.new_popup, &popup->new_popup); + popup->commit.notify = popup_handle_commit; + wl_signal_add(&wlr_popup->base->surface->events.commit, &popup->commit); + popup->reposition.notify = popup_handle_reposition; + wl_signal_add(&wlr_popup->events.reposition, &popup->reposition); + + return popup; +} + +static void popup_handle_new_popup(struct wl_listener *listener, void *data) { + struct sway_layer_popup *sway_layer_popup = + wl_container_of(listener, sway_layer_popup, new_popup); + struct wlr_xdg_popup *wlr_popup = data; + create_popup(wlr_popup, sway_layer_popup->toplevel, sway_layer_popup->scene); +} + +static void handle_new_popup(struct wl_listener *listener, void *data) { + struct sway_layer_surface *sway_layer_surface = + wl_container_of(listener, sway_layer_surface, new_popup); + struct wlr_xdg_popup *wlr_popup = data; + create_popup(wlr_popup, sway_layer_surface, sway_layer_surface->popups); +} + +void handle_layer_shell_surface(struct wl_listener *listener, void *data) { + struct wlr_layer_surface_v1 *layer_surface = data; + sway_log(SWAY_DEBUG, "new layer surface: namespace %s layer %d anchor %" PRIu32 + " size %" PRIu32 "x%" PRIu32 " margin %" PRIu32 ",%" PRIu32 ",%" PRIu32 ",%" PRIu32 ",", + layer_surface->namespace, + layer_surface->pending.layer, + layer_surface->pending.anchor, + layer_surface->pending.desired_width, + layer_surface->pending.desired_height, + layer_surface->pending.margin.top, + layer_surface->pending.margin.right, + layer_surface->pending.margin.bottom, + layer_surface->pending.margin.left); + + if (!layer_surface->output) { + // Assign last active output + struct sway_output *output = NULL; + struct sway_seat *seat = input_manager_get_default_seat(); + if (seat) { + struct sway_workspace *ws = seat_get_focused_workspace(seat); + if (ws != NULL) { + output = ws->output; + } + } + if (!output || output == root->fallback_output) { + if (!root->outputs->length) { + sway_log(SWAY_ERROR, + "no output to auto-assign layer surface '%s' to", + layer_surface->namespace); + wlr_layer_surface_v1_destroy(layer_surface); + return; + } + output = root->outputs->items[0]; + } + layer_surface->output = output->wlr_output; + } + + struct sway_output *output = layer_surface->output->data; + + enum zwlr_layer_shell_v1_layer layer_type = layer_surface->pending.layer; + struct wlr_scene_tree *output_layer = sway_layer_get_scene( + output, layer_type); + struct wlr_scene_layer_surface_v1 *scene_surface = + wlr_scene_layer_surface_v1_create(output_layer, layer_surface); + if (!scene_surface) { + sway_log(SWAY_ERROR, "Could not allocate a layer_surface_v1"); + return; + } + + struct sway_layer_surface *surface = + sway_layer_surface_create(scene_surface); + if (!surface) { + wlr_layer_surface_v1_destroy(layer_surface); + + sway_log(SWAY_ERROR, "Could not allocate a sway_layer_surface"); + return; + } + + if (!scene_descriptor_assign(&scene_surface->tree->node, + SWAY_SCENE_DESC_LAYER_SHELL, surface)) { + sway_log(SWAY_ERROR, "Failed to allocate a layer surface descriptor"); + // destroying the layer_surface will also destroy its corresponding + // scene node + wlr_layer_surface_v1_destroy(layer_surface); + return; + } + + surface->output = output; + wl_list_insert(&output->layer_surfaces, &surface->link); + + // now that the surface's output is known, we can advertise its scale + wlr_fractional_scale_v1_notify_scale(surface->layer_surface->surface, + layer_surface->output->scale); + wlr_surface_set_preferred_buffer_scale(surface->layer_surface->surface, + ceil(layer_surface->output->scale)); + + surface->surface_commit.notify = handle_surface_commit; + wl_signal_add(&layer_surface->surface->events.commit, + &surface->surface_commit); + surface->map.notify = handle_map; + wl_signal_add(&layer_surface->surface->events.map, &surface->map); + surface->unmap.notify = handle_unmap; + wl_signal_add(&layer_surface->surface->events.unmap, &surface->unmap); + surface->new_popup.notify = handle_new_popup; + wl_signal_add(&layer_surface->events.new_popup, &surface->new_popup); + + surface->node_destroy.notify = handle_node_destroy; + wl_signal_add(&scene_surface->tree->node.events.destroy, &surface->node_destroy); +} + +void destroy_layers(struct sway_output *output) { + struct sway_layer_surface *layer, *layer_tmp; + wl_list_for_each_safe(layer, layer_tmp, &output->layer_surfaces, link) { + layer->output = NULL; + wlr_layer_surface_v1_destroy(layer->layer_surface); + } +} diff --git a/sway/desktop/output.c b/sway/desktop/output.c new file mode 100644 index 0000000..17964e5 --- /dev/null +++ b/sway/desktop/output.c @@ -0,0 +1,818 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "config.h" +#include "log.h" +#include "sway/config.h" +#include "sway/desktop/transaction.h" +#include "sway/input/input-manager.h" +#include "sway/input/seat.h" +#include "sway/ipc-server.h" +#include "sway/layers.h" +#include "sway/output.h" +#include "sway/scene_descriptor.h" +#include "sway/server.h" +#include "sway/tree/arrange.h" +#include "sway/tree/container.h" +#include "sway/tree/root.h" +#include "sway/tree/view.h" +#include "sway/tree/workspace.h" + +#if WLR_HAS_DRM_BACKEND +#include +#include +#endif + +bool output_match_name_or_id(struct sway_output *output, + const char *name_or_id) { + if (strcmp(name_or_id, "*") == 0) { + return true; + } + + char identifier[128]; + output_get_identifier(identifier, sizeof(identifier), output); + return strcasecmp(identifier, name_or_id) == 0 + || strcasecmp(output->wlr_output->name, name_or_id) == 0; +} + +struct sway_output *output_by_name_or_id(const char *name_or_id) { + for (int i = 0; i < root->outputs->length; ++i) { + struct sway_output *output = root->outputs->items[i]; + if (output_match_name_or_id(output, name_or_id)) { + return output; + } + } + return NULL; +} + +struct sway_output *all_output_by_name_or_id(const char *name_or_id) { + struct sway_output *output; + wl_list_for_each(output, &root->all_outputs, link) { + if (output_match_name_or_id(output, name_or_id)) { + return output; + } + } + return NULL; +} + + +struct sway_workspace *output_get_active_workspace(struct sway_output *output) { + struct sway_seat *seat = input_manager_current_seat(); + struct sway_node *focus = seat_get_active_tiling_child(seat, &output->node); + if (!focus) { + if (!output->workspaces->length) { + return NULL; + } + return output->workspaces->items[0]; + } + return focus->sway_workspace; +} + +struct send_frame_done_data { + struct timespec when; + int msec_until_refresh; + struct sway_output *output; +}; + +struct buffer_timer { + struct wl_listener destroy; + struct wl_event_source *frame_done_timer; +}; + +static int handle_buffer_timer(void *data) { + struct wlr_scene_surface *scene_surface = data; + + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + wlr_scene_surface_send_frame_done(scene_surface, &now); + return 0; +} + +static void handle_buffer_timer_destroy(struct wl_listener *listener, + void *data) { + struct buffer_timer *timer = wl_container_of(listener, timer, destroy); + + wl_list_remove(&timer->destroy.link); + wl_event_source_remove(timer->frame_done_timer); + free(timer); +} + +static struct buffer_timer *buffer_timer_get_or_create(struct wlr_scene_surface *scene_surface) { + struct wlr_scene_buffer *buffer = scene_surface->buffer; + + struct buffer_timer *timer = + scene_descriptor_try_get(&buffer->node, SWAY_SCENE_DESC_BUFFER_TIMER); + if (timer) { + return timer; + } + + timer = calloc(1, sizeof(struct buffer_timer)); + if (!timer) { + return NULL; + } + + timer->frame_done_timer = wl_event_loop_add_timer(server.wl_event_loop, + handle_buffer_timer, scene_surface); + if (!timer->frame_done_timer) { + free(timer); + return NULL; + } + + scene_descriptor_assign(&buffer->node, SWAY_SCENE_DESC_BUFFER_TIMER, timer); + + timer->destroy.notify = handle_buffer_timer_destroy; + wl_signal_add(&buffer->node.events.destroy, &timer->destroy); + + return timer; +} + +static void send_frame_done_iterator(struct wlr_scene_buffer *buffer, + int x, int y, void *user_data) { + struct send_frame_done_data *data = user_data; + struct sway_output *output = data->output; + int view_max_render_time = 0; + + if (buffer->primary_output != data->output->scene_output) { + return; + } + + struct wlr_scene_surface *scene_surface = wlr_scene_surface_try_from_buffer(buffer); + if (scene_surface == NULL) { + return; + } + + struct wlr_scene_node *current = &buffer->node; + while (true) { + struct sway_view *view = scene_descriptor_try_get(current, + SWAY_SCENE_DESC_VIEW); + if (view) { + view_max_render_time = view->max_render_time; + break; + } + + if (!current->parent) { + break; + } + + current = ¤t->parent->node; + } + + int delay = data->msec_until_refresh - output->max_render_time + - view_max_render_time; + + struct buffer_timer *timer = NULL; + + if (output->max_render_time != 0 && view_max_render_time != 0 && delay > 0) { + timer = buffer_timer_get_or_create(scene_surface); + } + + if (timer) { + wl_event_source_timer_update(timer->frame_done_timer, delay); + } else { + wlr_scene_surface_send_frame_done(scene_surface, &data->when); + } +} + +static enum wlr_scale_filter_mode get_scale_filter(struct sway_output *output, + struct wlr_scene_buffer *buffer) { + // if we are scaling down, we should always choose linear + if (buffer->dst_width > 0 && buffer->dst_height > 0 && ( + buffer->dst_width < buffer->WLR_PRIVATE.buffer_width || + buffer->dst_height < buffer->WLR_PRIVATE.buffer_height)) { + return WLR_SCALE_FILTER_BILINEAR; + } + + switch (output->scale_filter) { + case SCALE_FILTER_LINEAR: + return WLR_SCALE_FILTER_BILINEAR; + case SCALE_FILTER_NEAREST: + return WLR_SCALE_FILTER_NEAREST; + default: + abort(); // unreachable + } +} + +static void configure_layer_shell_surface(struct wlr_scene_buffer *buffer, + struct sway_layer_surface *layer_surface) { + int corner_radius = layer_surface->corner_radius; + + wlr_scene_buffer_set_corner_radii(buffer, corner_radii_all(corner_radius)); + wlr_scene_shadow_set_blur_sigma(layer_surface->shadow_node, config->shadow_blur_sigma); + wlr_scene_shadow_set_corner_radius(layer_surface->shadow_node, corner_radius); + + wlr_scene_node_set_enabled(&layer_surface->blur_node->node, layer_surface->blur_enabled); + if (layer_surface->blur_ignore_transparent) { + wlr_scene_blur_set_transparency_mask_source(layer_surface->blur_node, buffer); + } else { + wlr_scene_blur_set_transparency_mask_source(layer_surface->blur_node, NULL); + } + wlr_scene_blur_set_should_only_blur_bottom_layer(layer_surface->blur_node, layer_surface->blur_xray); + wlr_scene_blur_set_corner_radii(layer_surface->blur_node, corner_radii_all(corner_radius)); + wlr_scene_blur_set_size(layer_surface->blur_node, + layer_surface->layer_surface->surface->current.width, + layer_surface->layer_surface->surface->current.height + ); +} + +static bool could_container_overlap(struct sway_container *con) { + if (container_is_floating_or_child(con)) { + return true; + } + + // animations can have tiled containers overlap in flight + return con->animation_state.animation.initialized || + (con->current.workspace && con->current.workspace->animation_state.animation.initialized); +} + +void output_configure_scene(struct sway_output *output, struct wlr_scene_node *node, + bool has_titlebar, struct sway_container *closest_con) { + if (!node->enabled) { + return; + } + + struct sway_container *con = + scene_descriptor_try_get(node, SWAY_SCENE_DESC_CONTAINER); + if (con) { + closest_con = con; + enum sway_container_layout layout = con->current.layout; + has_titlebar |= con->current.border == B_NORMAL || layout == L_STACKED || layout == L_TABBED; + } + + if (node->type == WLR_SCENE_NODE_TREE) { + struct wlr_scene_tree *tree = wlr_scene_tree_from_node(node); + struct wlr_scene_node *child; + wl_list_for_each(child, &tree->children, link) { + output_configure_scene(output, child, has_titlebar, closest_con); + } + return; + } + + float opacity = closest_con ? get_animated_value(closest_con->animation_state.from_alpha, + closest_con->animation_state.to_alpha, &closest_con->animation_state.animation) + * closest_con->alpha : 1.0f; + if (closest_con && closest_con->current.workspace) { + opacity *= get_animated_value( + closest_con->current.workspace->animation_state.from_alpha, + closest_con->current.workspace->animation_state.to_alpha, + &closest_con->current.workspace->animation_state.animation); + } + int corner_radius = closest_con && container_has_corner_radius(closest_con) ? + closest_con->corner_radius : 0; + + if (node->type == WLR_SCENE_NODE_BUFFER) { + struct wlr_scene_buffer *buffer = wlr_scene_buffer_from_node(node); + struct wlr_scene_surface *surface = wlr_scene_surface_try_from_buffer(buffer); + + struct wlr_layer_surface_v1 *wlr_layer_surface = NULL; + if (surface) { + const struct wlr_alpha_modifier_surface_v1_state *alpha_modifier_state = + wlr_alpha_modifier_v1_get_surface_state(surface->surface); + if (alpha_modifier_state != NULL) { + opacity *= (float)alpha_modifier_state->multiplier; + } + + wlr_layer_surface = + wlr_layer_surface_v1_try_from_wlr_surface(surface->surface); + } + + // hack: don't call the scene setter because that will damage all outputs + // We don't want to damage outputs that aren't our current output that + // we're configuring + if (output) { + buffer->filter_mode = get_scale_filter(output, buffer); + } + + wlr_scene_buffer_set_opacity(buffer, opacity); + + if (wlr_layer_surface) { + configure_layer_shell_surface(buffer, wlr_layer_surface->data); + return; + } + + if (!surface && !scene_descriptor_try_get(&buffer->node, SWAY_SCENE_DESC_SAVED_BUFFER)) { + return; + } + + if (!closest_con) { + // no container (e.g. fullscreen view), clear stale corner radii + wlr_scene_buffer_set_corner_radii(buffer, corner_radii_none()); + return; + } + + bool is_subsurface = surface && surface->surface && wlr_subsurface_try_from_wlr_surface(surface->surface); + wlr_scene_buffer_set_corner_radii(buffer, has_titlebar && !is_subsurface ? + corner_radii_bottom(corner_radius) : corner_radii_all(corner_radius)); + + if (is_subsurface) { + return; + } + + int content_width = closest_con->animation_state.current_content_width; + int content_height = closest_con->animation_state.current_content_height; + wlr_scene_buffer_set_dest_size(buffer, MAX(content_width, 0), MAX(content_height, 0)); + } else if (node->type == WLR_SCENE_NODE_BLUR && closest_con) { + struct wlr_scene_blur *blur = wlr_scene_blur_from_node(node); + + bool should_optimize_blur = config->blur_xray || !could_container_overlap(closest_con); + wlr_scene_blur_set_should_only_blur_bottom_layer(blur, should_optimize_blur); + wlr_scene_blur_set_strength(blur, opacity); + wlr_scene_node_set_enabled(node, closest_con->blur_enabled); + wlr_scene_blur_set_corner_radii(blur, has_titlebar ? + corner_radii_bottom(corner_radius) : corner_radii_all(corner_radius)); + } +} + +static bool output_can_tear(struct sway_output *output) { + struct sway_workspace *workspace = output->current.active_workspace; + if (!workspace) { + return false; + } + + struct sway_container *fullscreen_con = root->fullscreen_global; + if (!fullscreen_con) { + fullscreen_con = workspace->current.fullscreen; + } + if (fullscreen_con && fullscreen_con->view) { + return (output->allow_tearing && view_can_tear(fullscreen_con->view)); + } + + return false; +} + +static int output_repaint_timer_handler(void *data) { + struct sway_output *output = data; + + output->wlr_output->frame_pending = false; + if (!output->wlr_output->enabled) { + return 0; + } + + output_configure_scene(output, &root->root_scene->tree.node, false, NULL); + + struct wlr_scene_output_state_options opts = { + .color_transform = output->color_transform, + }; + + struct wlr_scene_output *scene_output = output->scene_output; + if (!wlr_scene_output_needs_frame(scene_output)) { + return 0; + } + + struct wlr_output_state pending; + wlr_output_state_init(&pending); + if (!wlr_scene_output_build_state(output->scene_output, &pending, &opts)) { + wlr_output_state_finish(&pending); + return 0; + } + + if (output_can_tear(output)) { + pending.tearing_page_flip = true; + + if (!wlr_output_test_state(output->wlr_output, &pending)) { + sway_log(SWAY_DEBUG, "Output test failed on '%s', retrying without tearing page-flip", + output->wlr_output->name); + pending.tearing_page_flip = false; + } + } + + if (!wlr_output_commit_state(output->wlr_output, &pending)) { + sway_log(SWAY_ERROR, "Page-flip failed on output %s", output->wlr_output->name); + } + wlr_output_state_finish(&pending); + return 0; +} + +static void handle_frame(struct wl_listener *listener, void *user_data) { + struct sway_output *output = + wl_container_of(listener, output, frame); + if (!output->enabled || !output->wlr_output->enabled) { + return; + } + + // Compute predicted milliseconds until the next refresh. It's used for + // delaying both output rendering and surface frame callbacks. + int msec_until_refresh = 0; + + if (output->max_render_time != 0) { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + + const long NSEC_IN_SECONDS = 1000000000; + struct timespec predicted_refresh = output->last_presentation; + predicted_refresh.tv_nsec += output->refresh_nsec % NSEC_IN_SECONDS; + predicted_refresh.tv_sec += output->refresh_nsec / NSEC_IN_SECONDS; + if (predicted_refresh.tv_nsec >= NSEC_IN_SECONDS) { + predicted_refresh.tv_sec += 1; + predicted_refresh.tv_nsec -= NSEC_IN_SECONDS; + } + + // If the predicted refresh time is before the current time then + // there's no point in delaying. + // + // We only check tv_sec because if the predicted refresh time is less + // than a second before the current time, then msec_until_refresh will + // end up slightly below zero, which will effectively disable the delay + // without potential disastrous negative overflows that could occur if + // tv_sec was not checked. + if (predicted_refresh.tv_sec >= now.tv_sec) { + long nsec_until_refresh + = (predicted_refresh.tv_sec - now.tv_sec) * NSEC_IN_SECONDS + + (predicted_refresh.tv_nsec - now.tv_nsec); + + // We want msec_until_refresh to be conservative, that is, floored. + // If we have 7.9 msec until refresh, we better compute the delay + // as if we had only 7 msec, so that we don't accidentally delay + // more than necessary and miss a frame. + msec_until_refresh = nsec_until_refresh / 1000000; + } + } + + int delay = msec_until_refresh - output->max_render_time; + + // If the delay is less than 1 millisecond (which is the least we can wait) + // then just render right away. + if (delay < 1) { + output_repaint_timer_handler(output); + } else { + output->wlr_output->frame_pending = true; + wl_event_source_timer_update(output->repaint_timer, delay); + } + + // Send frame done to all visible surfaces + struct send_frame_done_data data = {0}; + clock_gettime(CLOCK_MONOTONIC, &data.when); + data.msec_until_refresh = msec_until_refresh; + data.output = output; + wlr_scene_output_for_each_buffer(output->scene_output, send_frame_done_iterator, &data); +} + +void update_output_manager_config(struct sway_server *server) { + struct wlr_output_configuration_v1 *config = + wlr_output_configuration_v1_create(); + + struct sway_output *output; + wl_list_for_each(output, &root->all_outputs, link) { + if (output == root->fallback_output) { + continue; + } + struct wlr_output_configuration_head_v1 *config_head = + wlr_output_configuration_head_v1_create(config, output->wlr_output); + struct wlr_box output_box; + wlr_output_layout_get_box(root->output_layout, + output->wlr_output, &output_box); + // We mark the output enabled when it's switched off but not disabled + config_head->state.enabled = !wlr_box_empty(&output_box); + config_head->state.x = output_box.x; + config_head->state.y = output_box.y; + } + + wlr_output_manager_v1_set_configuration(server->output_manager_v1, config); + + ipc_event_output(); +} + +static int timer_modeset_handle(void *data) { + struct sway_server *server = data; + wl_event_source_remove(server->delayed_modeset); + server->delayed_modeset = NULL; + + apply_stored_output_configs(); + return 0; +} + +void request_modeset(void) { + if (server.delayed_modeset == NULL) { + server.delayed_modeset = wl_event_loop_add_timer(server.wl_event_loop, + timer_modeset_handle, &server); + wl_event_source_timer_update(server.delayed_modeset, 10); + } +} + +bool modeset_is_pending(void) { + return server.delayed_modeset != NULL; +} + +void force_modeset(void) { + if (server.delayed_modeset != NULL) { + wl_event_source_remove(server.delayed_modeset); + server.delayed_modeset = NULL; + } + apply_stored_output_configs(); +} + +static void begin_destroy(struct sway_output *output) { + + wl_list_remove(&output->layout_destroy.link); + wl_list_remove(&output->destroy.link); + wl_list_remove(&output->present.link); + wl_list_remove(&output->frame.link); + wl_list_remove(&output->request_state.link); + + // Remove the scene_output first to ensure that the scene does not emit + // events for this output. + wlr_scene_output_destroy(output->scene_output); + output->scene_output = NULL; + + if (output->enabled) { + output_disable(output); + } + output_begin_destroy(output); + wl_list_remove(&output->link); + + output->wlr_output->data = NULL; + output->wlr_output = NULL; + + wl_event_source_remove(output->repaint_timer); + output->repaint_timer = NULL; + + request_modeset(); +} + +static void handle_destroy(struct wl_listener *listener, void *data) { + struct sway_output *output = wl_container_of(listener, output, destroy); + begin_destroy(output); +} + +static void handle_layout_destroy(struct wl_listener *listener, void *data) { + struct sway_output *output = wl_container_of(listener, output, layout_destroy); + begin_destroy(output); +} + +static void handle_present(struct wl_listener *listener, void *data) { + struct sway_output *output = wl_container_of(listener, output, present); + struct wlr_output_event_present *output_event = data; + + if (!output->enabled || !output_event->presented) { + return; + } + + output->last_presentation = output_event->when; + output->refresh_nsec = output_event->refresh; +} + +static void handle_request_state(struct wl_listener *listener, void *data) { + struct sway_output *output = + wl_container_of(listener, output, request_state); + const struct wlr_output_event_request_state *event = data; + const struct wlr_output_state *state = event->state; + + // Store the requested changes so that the active configuration is + // consistent with the current state, and to avoid duplicate logic to apply + // the changes. + struct output_config *oc = new_output_config(output->wlr_output->name); + if (!oc) { + sway_log(SWAY_ERROR, "Allocation failed"); + return; + } + + int committed = state->committed; + if (committed & WLR_OUTPUT_STATE_MODE) { + if (state->mode != NULL) { + oc->width = state->mode->width; + oc->height = state->mode->height; + oc->refresh_rate = state->mode->refresh / 1000.f; + } else { + oc->width = state->custom_mode.width; + oc->height = state->custom_mode.height; + oc->refresh_rate = state->custom_mode.refresh / 1000.f; + } + committed &= ~WLR_OUTPUT_STATE_MODE; + } + if (committed & WLR_OUTPUT_STATE_SCALE) { + oc->scale = state->scale; + committed &= ~WLR_OUTPUT_STATE_SCALE; + } + if (committed & WLR_OUTPUT_STATE_TRANSFORM) { + oc->transform = state->transform; + committed &= ~WLR_OUTPUT_STATE_TRANSFORM; + } + + // We do not expect or support any other changes here + assert(committed == 0); + store_output_config(oc); + + force_modeset(); +} + +static unsigned int last_headless_num = 0; + +void handle_new_output(struct wl_listener *listener, void *data) { + struct sway_server *server = wl_container_of(listener, server, new_output); + struct wlr_output *wlr_output = data; + + if (wlr_output == root->fallback_output->wlr_output) { + return; + } + + if (wlr_output_is_headless(wlr_output)) { + char name[64]; + snprintf(name, sizeof(name), "HEADLESS-%u", ++last_headless_num); + wlr_output_set_name(wlr_output, name); + } + + sway_log(SWAY_DEBUG, "New output %p: %s (non-desktop: %d)", + wlr_output, wlr_output->name, wlr_output->non_desktop); + + if (wlr_output->non_desktop) { + sway_log(SWAY_DEBUG, "Not configuring non-desktop output"); + struct sway_output_non_desktop *non_desktop = output_non_desktop_create(wlr_output); +#if WLR_HAS_DRM_BACKEND + if (server->drm_lease_manager) { + wlr_drm_lease_v1_manager_offer_output(server->drm_lease_manager, + wlr_output); + } +#endif + list_add(root->non_desktop_outputs, non_desktop); + return; + } + + if (!wlr_output_init_render(wlr_output, server->allocator, + server->renderer)) { + sway_log(SWAY_ERROR, "Failed to init output render"); + return; + } + + // Create the scene output here so we're not accidentally creating one for + // the fallback output + struct wlr_scene_output *scene_output = + wlr_scene_output_create(root->root_scene, wlr_output); + if (!scene_output) { + sway_log(SWAY_ERROR, "Failed to create a scene output"); + return; + } + + struct sway_output *output = output_create(wlr_output); + if (!output) { + sway_log(SWAY_ERROR, "Failed to create a sway output"); + wlr_scene_output_destroy(scene_output); + return; + } + + output->server = server; + output->scene_output = scene_output; + + wl_signal_add(&root->output_layout->events.destroy, &output->layout_destroy); + output->layout_destroy.notify = handle_layout_destroy; + wl_signal_add(&wlr_output->events.destroy, &output->destroy); + output->destroy.notify = handle_destroy; + wl_signal_add(&wlr_output->events.present, &output->present); + output->present.notify = handle_present; + wl_signal_add(&wlr_output->events.frame, &output->frame); + output->frame.notify = handle_frame; + wl_signal_add(&wlr_output->events.request_state, &output->request_state); + output->request_state.notify = handle_request_state; + + output->repaint_timer = wl_event_loop_add_timer(server->wl_event_loop, + output_repaint_timer_handler, output); + + if (server->session_lock.lock) { + sway_session_lock_add_output(server->session_lock.lock, output); + } + + request_modeset(); +} + +static struct output_config *output_config_for_config_head( + struct wlr_output_configuration_head_v1 *config_head) { + struct output_config *oc = new_output_config(config_head->state.output->name); + if (!oc) { + return NULL; + } + + oc->enabled = config_head->state.enabled; + if (!oc->enabled) { + return oc; + } + + if (config_head->state.mode != NULL) { + struct wlr_output_mode *mode = config_head->state.mode; + oc->width = mode->width; + oc->height = mode->height; + oc->refresh_rate = mode->refresh / 1000.f; + } else { + oc->width = config_head->state.custom_mode.width; + oc->height = config_head->state.custom_mode.height; + oc->refresh_rate = + config_head->state.custom_mode.refresh / 1000.f; + } + oc->x = config_head->state.x; + oc->y = config_head->state.y; + oc->transform = config_head->state.transform; + oc->scale = config_head->state.scale; + oc->adaptive_sync = config_head->state.adaptive_sync_enabled; + return oc; +} + +static void output_manager_apply(struct sway_server *server, + struct wlr_output_configuration_v1 *cfg, bool test_only) { + bool ok = false; + size_t configs_len = config->output_configs->length + wl_list_length(&cfg->heads); + struct output_config **configs = calloc(configs_len, sizeof(*configs)); + if (!configs) { + sway_log(SWAY_ERROR, "Allocation failed"); + goto error; + } + size_t start_new_configs = config->output_configs->length; + for (size_t idx = 0; idx < start_new_configs; idx++) { + configs[idx] = config->output_configs->items[idx]; + } + + size_t config_idx = start_new_configs; + struct wlr_output_configuration_head_v1 *config_head; + wl_list_for_each(config_head, &cfg->heads, link) { + // Generate the configuration and store it as a temporary + // config. We keep a record of it so we can remove it later. + struct output_config *oc = output_config_for_config_head(config_head); + if (!oc) { + sway_log(SWAY_ERROR, "Allocation failed"); + goto error_config; + } + configs[config_idx++] = oc; + } + + // Try to commit without degrade to off enabled. Note that this will fail + // if any output configured for enablement fails to be enabled, even if it + // was not part of the config heads we were asked to configure. + ok = apply_output_configs(configs, configs_len, test_only, false); + +error_config: + for (size_t idx = start_new_configs; idx < configs_len; idx++) { + struct output_config *cfg = configs[idx]; + if (!test_only && ok) { + store_output_config(cfg); + } else { + free_output_config(cfg); + } + } + free(configs); + +error: + if (ok) { + wlr_output_configuration_v1_send_succeeded(cfg); + if (server->delayed_modeset != NULL) { + wl_event_source_remove(server->delayed_modeset); + server->delayed_modeset = NULL; + } + } else { + wlr_output_configuration_v1_send_failed(cfg); + } + wlr_output_configuration_v1_destroy(cfg); +} + +void handle_output_manager_apply(struct wl_listener *listener, void *data) { + struct sway_server *server = + wl_container_of(listener, server, output_manager_apply); + struct wlr_output_configuration_v1 *config = data; + + output_manager_apply(server, config, false); +} + +void handle_output_manager_test(struct wl_listener *listener, void *data) { + struct sway_server *server = + wl_container_of(listener, server, output_manager_test); + struct wlr_output_configuration_v1 *config = data; + + output_manager_apply(server, config, true); +} + +void handle_output_power_manager_set_mode(struct wl_listener *listener, + void *data) { + struct wlr_output_power_v1_set_mode_event *event = data; + struct sway_output *output = event->output->data; + + struct output_config *oc = new_output_config(output->wlr_output->name); + if (!oc) { + sway_log(SWAY_ERROR, "Allocation failed"); + return; + } + + switch (event->mode) { + case ZWLR_OUTPUT_POWER_V1_MODE_OFF: + oc->power = 0; + break; + case ZWLR_OUTPUT_POWER_V1_MODE_ON: + oc->power = 1; + break; + } + store_output_config(oc); + request_modeset(); +} diff --git a/sway/desktop/tearing.c b/sway/desktop/tearing.c new file mode 100644 index 0000000..d8d2764 --- /dev/null +++ b/sway/desktop/tearing.c @@ -0,0 +1,64 @@ +#include +#include +#include "sway/server.h" +#include "sway/tree/view.h" +#include "sway/output.h" +#include "log.h" + +struct sway_tearing_controller { + struct wlr_tearing_control_v1 *tearing_control; + struct wl_listener set_hint; + struct wl_listener destroy; + + struct wl_list link; // sway_server::tearing_controllers +}; + +static void handle_tearing_controller_set_hint(struct wl_listener *listener, + void *data) { + struct sway_tearing_controller *controller = + wl_container_of(listener, controller, set_hint); + + struct sway_view *view = view_from_wlr_surface( + controller->tearing_control->surface); + if (view) { + view->tearing_hint = controller->tearing_control->current; + } +} + +static void handle_tearing_controller_destroy(struct wl_listener *listener, + void *data) { + struct sway_tearing_controller *controller = + wl_container_of(listener, controller, destroy); + wl_list_remove(&controller->set_hint.link); + wl_list_remove(&controller->destroy.link); + wl_list_remove(&controller->link); + free(controller); +} + +void handle_new_tearing_hint(struct wl_listener *listener, + void *data) { + struct sway_server *server = + wl_container_of(listener, server, tearing_control_new_object); + struct wlr_tearing_control_v1 *tearing_control = data; + + enum wp_tearing_control_v1_presentation_hint hint = + wlr_tearing_control_manager_v1_surface_hint_from_surface( + server->tearing_control_v1, tearing_control->surface); + sway_log(SWAY_DEBUG, "New presentation hint %d received for surface %p", + hint, tearing_control->surface); + + struct sway_tearing_controller *controller = + calloc(1, sizeof(struct sway_tearing_controller)); + if (!controller) { + return; + } + + controller->tearing_control = tearing_control; + controller->set_hint.notify = handle_tearing_controller_set_hint; + wl_signal_add(&tearing_control->events.set_hint, &controller->set_hint); + controller->destroy.notify = handle_tearing_controller_destroy; + wl_signal_add(&tearing_control->events.destroy, &controller->destroy); + wl_list_init(&controller->link); + + wl_list_insert(&server->tearing_controllers, &controller->link); +} diff --git a/sway/desktop/transaction.c b/sway/desktop/transaction.c new file mode 100644 index 0000000..3a37b40 --- /dev/null +++ b/sway/desktop/transaction.c @@ -0,0 +1,1332 @@ +#include +#include +#include +#include +#include +#include +#include "scenefx/types/fx/clipped_region.h" +#include "sway/animation_manager.h" +#include "sway/config.h" +#include "sway/scene_descriptor.h" +#include "sway/desktop/idle_inhibit_v1.h" +#include "sway/desktop/transaction.h" +#include "sway/input/cursor.h" +#include "sway/input/input-manager.h" +#include "sway/output.h" +#include "sway/server.h" +#include "sway/tree/container.h" +#include "sway/tree/node.h" +#include "sway/tree/view.h" +#include "sway/tree/workspace.h" +#include "list.h" +#include "log.h" + +#define POPIN_FACTOR 0.8f + +struct sway_transaction { + struct wl_event_source *timer; + list_t *instructions; // struct sway_transaction_instruction * + size_t num_waiting; + size_t num_configures; + struct timespec commit_time; +}; + +struct sway_transaction_instruction { + struct sway_transaction *transaction; + struct sway_node *node; + union { + struct sway_output_state output_state; + struct sway_workspace_state workspace_state; + struct sway_container_state container_state; + }; + uint32_t serial; + bool server_request; + bool waiting; +}; + +static struct sway_transaction *transaction_create(void) { + struct sway_transaction *transaction = + calloc(1, sizeof(struct sway_transaction)); + if (!sway_assert(transaction, "Unable to allocate transaction")) { + return NULL; + } + transaction->instructions = create_list(); + return transaction; +} + +static bool con_has_title_bar(struct sway_container *con) { + return !(con->current.parent && + (con->current.parent->current.layout == L_STACKED || + con->current.parent->current.layout == L_TABBED)); +} + +static void _arrange_container(struct sway_container *con, + int width, int height, int x, int y, bool title_bar, int gaps); + +static void anim_update_callback(void *data) { + struct sway_container *con = data; + // if there's a pending transaction there will be a re-arrange anyway + if (server.pending_transaction) { + return; + } + + if (con->current.fullscreen_mode != FULLSCREEN_NONE) { + return; + } + + int width = get_animated_value(con->animation_state.from_width, + con->animation_state.to_width, &con->animation_state.animation); + int height = get_animated_value(con->animation_state.from_height, + con->animation_state.to_height, &con->animation_state.animation); + int x = get_animated_value(con->animation_state.from_x, + con->animation_state.to_x, &con->animation_state.animation); + int y = get_animated_value(con->animation_state.from_y, + con->animation_state.to_y, &con->animation_state.animation); + + bool title_bar = con_has_title_bar(con); + _arrange_container(con, width, height, x, y, title_bar, 0); + // refresh decorations for parent-managed title bars (tabbed/stacked) + if (!title_bar) { + container_update(con); + } +} + +static void close_anim_complete_callback(void *data) { + struct sway_container *con = data; + view_remove_saved_buffer(con->view); + container_destroy(con); +} + +static void _fade_container_update(struct sway_container *con, void *data) { + // skip redundant update if the container is also being animated + if (con->animation_state.animation.initialized) { + return; + } + container_update(con); +} + +static void workspace_fade_update_callback(void *data) { + struct sway_workspace *ws = data; + if (!ws || !ws->output) { + return; + } + // TODO: needed? + output_configure_scene(ws->output, &ws->layers.tiling->node, false, NULL); + workspace_for_each_container(ws, _fade_container_update, NULL); +} + +static void workspace_fade_complete_callback(void *data) { + struct sway_workspace *ws = data; + if (!ws || !ws->output) { + return; + } + struct sway_output *output = ws->output; + if (output->current.active_workspace == ws) { + return; + } + wlr_scene_node_set_enabled(&ws->layers.tiling->node, false); + wlr_scene_node_set_enabled(&ws->layers.fullscreen->node, false); + for (int i = 0; i < ws->current.floating->length; i++) { + struct sway_container *floater = ws->current.floating->items[i]; + wlr_scene_node_set_enabled(&floater->scene_tree->node, false); + } +} + +/* Compensate for scene-graph reparenting by computing the drift between + * the last tracked global coordinates and the actual current global position, + * then applying that delta to the local scene node position +*/ +static void snap_animation_position(struct sway_container *con) { + int lx, ly; + wlr_scene_node_coords(&con->scene_tree->node, &lx, &ly); + int global_delta_x = con->animation_state.current_global_x - lx; + int global_delta_y = con->animation_state.current_global_y - ly; + con->animation_state.from_x = con->scene_tree->node.x + global_delta_x; + con->animation_state.from_y = con->scene_tree->node.y + global_delta_y; +} + +static void transaction_destroy(struct sway_transaction *transaction) { + // Free instructions + for (int i = 0; i < transaction->instructions->length; ++i) { + struct sway_transaction_instruction *instruction = + transaction->instructions->items[i]; + struct sway_node *node = instruction->node; + node->ntxnrefs--; + if (node->instruction == instruction) { + node->instruction = NULL; + } + if (node->destroying && node->ntxnrefs == 0 && !node->dirty) { + switch (node->type) { + case N_ROOT: + sway_assert(false, "Never reached"); + break; + case N_OUTPUT: + output_destroy(node->sway_output); + break; + case N_WORKSPACE: + workspace_destroy(node->sway_workspace); + break; + case N_CONTAINER: + // close animation — pop-out: shrink and fade out centered + if (node->sway_container->view) { + struct sway_container *con = node->sway_container; + snap_animation_position(con); + con->animation_state.from_alpha = get_animated_value(con->animation_state.from_alpha, + con->animation_state.to_alpha, &con->animation_state.animation); + con->animation_state.to_alpha = 0.0f; + con->animation_state.from_width = get_animated_value(con->animation_state.from_width, + con->animation_state.to_width, &con->animation_state.animation); + con->animation_state.from_height = get_animated_value(con->animation_state.from_height, + con->animation_state.to_height, &con->animation_state.animation); + con->animation_state.to_x = con->animation_state.from_x + + (con->animation_state.from_width * (1.0f - POPIN_FACTOR)) / 2.0f; + con->animation_state.to_y = con->animation_state.from_y + + (con->animation_state.from_height * (1.0f - POPIN_FACTOR)) / 2.0f; + con->animation_state.to_width = con->animation_state.from_width * POPIN_FACTOR; + con->animation_state.to_height = con->animation_state.from_height * POPIN_FACTOR; + add_animation(&con->animation_state.animation, anim_update_callback, close_anim_complete_callback); + } else { + container_destroy(node->sway_container); + } + break; + } + } + free(instruction); + } + list_free(transaction->instructions); + + if (transaction->timer) { + wl_event_source_remove(transaction->timer); + } + free(transaction); +} + +static void copy_output_state(struct sway_output *output, + struct sway_transaction_instruction *instruction) { + struct sway_output_state *state = &instruction->output_state; + if (state->workspaces) { + state->workspaces->length = 0; + } else { + state->workspaces = create_list(); + } + list_cat(state->workspaces, output->workspaces); + + state->active_workspace = output_get_active_workspace(output); +} + +static void copy_workspace_state(struct sway_workspace *ws, + struct sway_transaction_instruction *instruction) { + struct sway_workspace_state *state = &instruction->workspace_state; + + state->fullscreen = ws->fullscreen; + state->x = ws->x; + state->y = ws->y; + state->width = ws->width; + state->height = ws->height; + state->layout = ws->layout; + + state->output = ws->output; + if (state->floating) { + state->floating->length = 0; + } else { + state->floating = create_list(); + } + if (state->tiling) { + state->tiling->length = 0; + } else { + state->tiling = create_list(); + } + list_cat(state->floating, ws->floating); + list_cat(state->tiling, ws->tiling); + + struct sway_seat *seat = input_manager_current_seat(); + state->focused = seat_get_focus(seat) == &ws->node; + + // Set focused_inactive_child to the direct tiling child + struct sway_container *focus = seat_get_focus_inactive_tiling(seat, ws); + if (focus) { + while (focus->pending.parent) { + focus = focus->pending.parent; + } + } + state->focused_inactive_child = focus; +} + +static void copy_container_state(struct sway_container *container, + struct sway_transaction_instruction *instruction) { + struct sway_container_state *state = &instruction->container_state; + + if (state->children) { + list_free(state->children); + } + + memcpy(state, &container->pending, sizeof(struct sway_container_state)); + + if (!container->view) { + // We store a copy of the child list to avoid having it mutated after + // we copy the state. + state->children = create_list(); + list_cat(state->children, container->pending.children); + } else { + state->children = NULL; + } + + struct sway_seat *seat = input_manager_current_seat(); + state->focused = seat_get_focus(seat) == &container->node; + + if (!container->view) { + struct sway_node *focus = + seat_get_active_tiling_child(seat, &container->node); + state->focused_inactive_child = focus ? focus->sway_container : NULL; + } +} + +static void transaction_add_node(struct sway_transaction *transaction, + struct sway_node *node, bool server_request) { + struct sway_transaction_instruction *instruction = NULL; + + // Check if we have an instruction for this node already, in which case we + // update that instead of creating a new one. + if (node->ntxnrefs > 0) { + for (int idx = 0; idx < transaction->instructions->length; idx++) { + struct sway_transaction_instruction *other = + transaction->instructions->items[idx]; + if (other->node == node) { + instruction = other; + break; + } + } + } + + if (!instruction) { + instruction = calloc(1, sizeof(struct sway_transaction_instruction)); + if (!sway_assert(instruction, "Unable to allocate instruction")) { + return; + } + instruction->transaction = transaction; + instruction->node = node; + instruction->server_request = server_request; + + list_add(transaction->instructions, instruction); + node->ntxnrefs++; + } else if (server_request) { + instruction->server_request = true; + } + + switch (node->type) { + case N_ROOT: + break; + case N_OUTPUT: + copy_output_state(node->sway_output, instruction); + break; + case N_WORKSPACE: + copy_workspace_state(node->sway_workspace, instruction); + break; + case N_CONTAINER: + copy_container_state(node->sway_container, instruction); + break; + } +} + +static void apply_output_state(struct sway_output *output, + struct sway_output_state *state) { + list_free(output->current.workspaces); + memcpy(&output->current, state, sizeof(struct sway_output_state)); +} + +static void apply_workspace_state(struct sway_workspace *ws, + struct sway_workspace_state *state) { + list_free(ws->current.floating); + list_free(ws->current.tiling); + memcpy(&ws->current, state, sizeof(struct sway_workspace_state)); +} + +static void apply_container_state(struct sway_container *container, + struct sway_container_state *state) { + struct sway_view *view = container->view; + // There are separate children lists for each instruction state, the + // container's current state and the container's pending state + // (ie. con->children). The list itself needs to be freed here. + // Any child containers which are being deleted will be cleaned up in + // transaction_destroy(). + list_free(container->current.children); + + memcpy(&container->current, state, sizeof(struct sway_container_state)); + + if (view) { + if (view->saved_surface_tree) { + if (!container->node.destroying) { + view_remove_saved_buffer(view); + } + } + + // If the view hasn't responded to the configure, center it within + // the container. This is important for fullscreen views which + // refuse to resize to the size of the output. + if (view->surface) { + view_center_and_clip_surface(view); + } + } +} + +static void arrange_title_bar(struct sway_container *con, + int x, int y, int width, int height) { + container_update(con); + + bool has_title_bar = height > 0; + wlr_scene_node_set_enabled(&con->title_bar.tree->node, has_title_bar); + if (!has_title_bar) { + return; + } + + wlr_scene_node_set_position(&con->title_bar.tree->node, x, y); + + con->title_width = width; + container_arrange_title_bar(con); +} + +static void disable_container(struct sway_container *con) { + if (con->view) { + wlr_scene_node_reparent(&con->view->scene_tree->node, con->content_tree); + } else { + for (int i = 0; i < con->current.children->length; i++) { + struct sway_container *child = con->current.children->items[i]; + + wlr_scene_node_reparent(&child->scene_tree->node, con->content_tree); + + disable_container(child); + } + } +} + +static void arrange_container(struct sway_container *con, + int width, int height, int x, int y, bool title_bar, int gaps); + +static void arrange_inactive_child(struct sway_container *child, + int width, int height, int y_pos) { + finish_animation(&child->animation_state.animation); + wlr_scene_node_set_position(&child->scene_tree->node, 0, y_pos); + child->animation_state.current_width = width; + child->animation_state.current_height = height; + if (child->view) { + wlr_scene_node_coords(&child->scene_tree->node, + &child->animation_state.current_global_x, + &child->animation_state.current_global_y); + } + child->animation_state.from_x = 0; + child->animation_state.from_y = y_pos; + disable_container(child); +} + +static void arrange_children(enum sway_container_layout layout, list_t *children, + struct sway_container *active, struct wlr_scene_tree *content, + int width, int height, int gaps) { + int title_bar_height = container_titlebar_height(); + + if (layout == L_TABBED) { + struct sway_container *first = children->length == 1 ? + ((struct sway_container *)children->items[0]) : NULL; + if (config->hide_lone_tab && first && first->view && + first->current.border != B_NORMAL) { + title_bar_height = 0; + } + + double w = (double) width / children->length; + int title_offset = 0; + for (int i = 0; i < children->length; i++) { + struct sway_container *child = children->items[i]; + bool activated = child == active; + int next_title_offset = round(w * i + w); + + arrange_title_bar(child, title_offset, -title_bar_height, + next_title_offset - title_offset, title_bar_height); + wlr_scene_node_set_enabled(&child->border.tree->node, activated); + wlr_scene_node_set_enabled(&child->blur->node, activated); + wlr_scene_node_set_enabled(&child->shadow->node, false); + wlr_scene_node_set_enabled(&child->scene_tree->node, true); + wlr_scene_node_reparent(&child->scene_tree->node, content); + + int net_height = height - title_bar_height; + if (activated && width > 0 && net_height > 0) { + arrange_container(child, width, net_height, 0, title_bar_height, title_bar_height == 0, 0); + } else { + arrange_inactive_child(child, width, net_height, title_bar_height); + } + + title_offset = next_title_offset; + } + } else if (layout == L_STACKED) { + struct sway_container *first = children->length == 1 ? + ((struct sway_container *)children->items[0]) : NULL; + if (config->hide_lone_tab && first && first->view && + first->current.border != B_NORMAL) { + title_bar_height = 0; + } + + int title_height = title_bar_height * children->length; + + int y = 0; + for (int i = 0; i < children->length; i++) { + struct sway_container *child = children->items[i]; + bool activated = child == active; + + arrange_title_bar(child, 0, y - title_height, width, title_bar_height); + wlr_scene_node_set_enabled(&child->border.tree->node, activated); + wlr_scene_node_set_enabled(&child->blur->node, activated); + wlr_scene_node_set_enabled(&child->shadow->node, false); + wlr_scene_node_set_enabled(&child->scene_tree->node, true); + wlr_scene_node_reparent(&child->scene_tree->node, content); + + int net_height = height - title_height; + if (activated && width > 0 && net_height > 0) { + arrange_container(child, width, net_height, 0, title_height, title_bar_height == 0, 0); + } else { + arrange_inactive_child(child, width, net_height, title_height); + } + + y += title_bar_height; + } + } else if (layout == L_VERT) { + int off = 0; + for (int i = 0; i < children->length; i++) { + struct sway_container *child = children->items[i]; + int cheight = child->current.height; + + wlr_scene_node_set_enabled(&child->border.tree->node, true); + wlr_scene_node_set_enabled(&child->blur->node, true); + wlr_scene_node_set_enabled(&child->shadow->node, + container_has_shadow(child) && child->view); + wlr_scene_node_reparent(&child->scene_tree->node, content); + if (width > 0 && cheight > 0) { + arrange_container(child, width, cheight, 0, off, true, gaps); + off += cheight + gaps; + } else { + disable_container(child); + } + } + } else if (layout == L_HORIZ) { + int off = 0; + for (int i = 0; i < children->length; i++) { + struct sway_container *child = children->items[i]; + int cwidth = child->current.width; + + wlr_scene_node_set_enabled(&child->border.tree->node, true); + wlr_scene_node_set_enabled(&child->blur->node, true); + wlr_scene_node_set_enabled(&child->shadow->node, + container_has_shadow(child) && child->view); + wlr_scene_node_reparent(&child->scene_tree->node, content); + if (cwidth > 0 && height > 0) { + arrange_container(child, cwidth, height, off, 0, true, gaps); + off += cwidth + gaps; + } else { + disable_container(child); + } + } + } else { + sway_assert(false, "unreachable"); + } +} + +static void _arrange_container(struct sway_container *con, + int width, int height, int x, int y, bool title_bar, int gaps) { + wlr_scene_node_set_position(&con->scene_tree->node, x, y); + + // this container might have previously been in the scratchpad, + // make sure it's enabled for viewing + wlr_scene_node_set_enabled(&con->scene_tree->node, true); + + bool has_corner_radius = container_has_corner_radius(con); + + if (container_has_shadow(con)) { + int corner_radius = has_corner_radius ? con->corner_radius + con->current.border_thickness : 0; + if (!con->view && title_bar) { + // Stacking/Tabbed containers don't have a border_thickness, so we + // use the config default + corner_radius = con->corner_radius + config->border_thickness; + } + + wlr_scene_shadow_set_size(con->shadow, + width + config->shadow_blur_sigma * 2, + height + config->shadow_blur_sigma * 2); + + int shadow_x = config->shadow_offset_x - config->shadow_blur_sigma; + int shadow_y = config->shadow_offset_y - config->shadow_blur_sigma; + wlr_scene_node_set_position(&con->shadow->node, shadow_x, shadow_y); + + wlr_scene_shadow_set_clipped_region(con->shadow, (struct clipped_region) { + .corners = corner_radii_all(corner_radius), + .area = { + .x = config->shadow_blur_sigma - config->shadow_offset_x, + .y = config->shadow_blur_sigma - config->shadow_offset_y, + .width = width, + .height = height, + } + }); + + wlr_scene_shadow_set_blur_sigma(con->shadow, config->shadow_blur_sigma); + wlr_scene_shadow_set_corner_radius(con->shadow, + !has_corner_radius ? 0 : corner_radius); + } + + if (con->view) { + wlr_scene_node_coords(&con->scene_tree->node, + &con->animation_state.current_global_x, + &con->animation_state.current_global_y); + con->animation_state.current_width = width; + con->animation_state.current_height = height; + + int corner_radius = has_corner_radius ? con->corner_radius : 0; + int vert_border_offset = corner_radius; + int border_top = container_titlebar_height(); + int border_width = con->current.border_thickness; + + if (title_bar && con->current.border != B_NORMAL) { + wlr_scene_node_set_enabled(&con->title_bar.tree->node, false); + wlr_scene_node_set_enabled(&con->border.top->node, true); + } else { + wlr_scene_node_set_enabled(&con->border.top->node, false); + } + + if (con->current.border == B_NORMAL) { + vert_border_offset = 0; + if (title_bar) { + arrange_title_bar(con, 0, 0, width, border_top); + } else { + border_top = 0; + // should be handled by the parent container + } + } else if (con->current.border == B_PIXEL) { + container_update(con); + border_top = title_bar && con->current.border_top ? border_width : 0; + if (!title_bar && !con->current.border_top) { + vert_border_offset = 0; + } + } else if (con->current.border == B_NONE) { + container_update(con); + border_top = 0; + border_width = 0; + } else if (con->current.border == B_CSD) { + border_top = 0; + border_width = 0; + } else { + sway_assert(false, "unreachable"); + } + + int border_bottom = con->current.border_bottom ? border_width : 0; + int border_left = con->current.border_left ? border_width : 0; + int border_right = con->current.border_right ? border_width : 0; + + int vert_border_height = MAX(0, height - border_top - border_bottom - vert_border_offset - corner_radius); + wlr_scene_rect_set_size(con->border.left, border_left, vert_border_height); + wlr_scene_rect_set_size(con->border.right, border_right, vert_border_height); + + if (border_top) { + wlr_scene_rect_set_size(con->border.top, width, border_top + corner_radius); + wlr_scene_rect_set_corner_radii(con->border.top, corner_radii_top(!has_corner_radius ? 0 : + corner_radius + border_width)); + wlr_scene_rect_set_clipped_region(con->border.top, (struct clipped_region) { + .corners = corner_radii_top(corner_radius), + .area = { + .x = border_width, + .y = border_width, + .width = width - 2 * border_width, + .height = border_top + corner_radius + } + }); + } else { + wlr_scene_rect_set_size(con->border.top, 0, 0); + } + + if (border_bottom) { + wlr_scene_rect_set_size(con->border.bottom, width, border_bottom + corner_radius); + wlr_scene_rect_set_corner_radii(con->border.bottom, corner_radii_bottom(!has_corner_radius ? 0 : + corner_radius + border_width)); + + wlr_scene_rect_set_clipped_region(con->border.bottom, (struct clipped_region) { + .corners = corner_radii_bottom(corner_radius), + // shift up one px to fix https://github.com/WillPower3309/swayfx/issues/386 + // TODO: proper fix + .area = { + .x = border_width, + .y = -1, + .width = width - 2 * border_width, + .height = border_bottom - border_width + corner_radius + 1, + } + }); + } else { + wlr_scene_rect_set_size(con->border.bottom, 0, 0); + } + + wlr_scene_node_set_position(&con->border.top->node, 0, 0); + wlr_scene_node_set_position(&con->border.bottom->node, + 0, height - border_bottom - corner_radius); + wlr_scene_node_set_position(&con->border.left->node, + 0, border_top + vert_border_offset); + wlr_scene_node_set_position(&con->border.right->node, + width - border_right, border_top + vert_border_offset); + + int content_width = width - border_left - border_right; + int content_height = height - border_top - border_bottom; + + // only make sure to clip the content if there is content to clip + if (!wl_list_empty(&con->view->content_tree->children)) { + struct wlr_box clip = (struct wlr_box){ + .x = con->view->geometry.x, + .y = con->view->geometry.y, + .width = content_width, + .height = content_height, + }; + wlr_scene_subsurface_tree_set_clip(&con->view->content_tree->node, &clip); + } + con->animation_state.current_content_width = content_width; + con->animation_state.current_content_height = content_height; + if (content_width <= 0 || content_height <= 0) { + return; + } + + // Dim + if (con->dim_rect) { + wlr_scene_node_set_position(&con->dim_rect->node, border_left, border_top); + wlr_scene_rect_set_size(con->dim_rect, content_width, content_height); + bool has_titlebar = !title_bar || con->current.border == B_NORMAL; + wlr_scene_rect_set_corner_radii( + con->dim_rect, + has_titlebar ? corner_radii_bottom(con->corner_radius) : corner_radii_all(con->corner_radius) + ); + } + + // make sure to reparent, it's possible that the client just came out of + // fullscreen mode where the parent of the surface is not the container + wlr_scene_node_reparent(&con->view->scene_tree->node, con->content_tree); + wlr_scene_node_set_position(&con->view->scene_tree->node, + border_left, border_top); + + wlr_scene_node_set_enabled(&con->blur->node, con->blur_enabled); + wlr_scene_node_set_position(&con->blur->node, border_left, border_top); + wlr_scene_blur_set_size(con->blur, content_width, content_height); + + // the output handler for the view wants to detect events for the entire + // container so give it negative coordinates to move it back over the + // decorations + wlr_scene_node_set_position(&con->view->output_handler->node, + -border_left, -border_top); + wlr_scene_buffer_set_dest_size(con->view->output_handler, width, height); + } else { + // make sure to disable the title bar if the parent is not managing it + if (title_bar) { + wlr_scene_node_set_enabled(&con->title_bar.tree->node, false); + } + + wlr_scene_node_set_enabled(&con->shadow->node, + container_has_shadow(con) && + (con->current.layout == L_TABBED || con->current.layout == L_STACKED)); + + arrange_children(con->current.layout, con->current.children, + con->current.focused_inactive_child, con->content_tree, + width, height, gaps); + } +} + +static void arrange_container(struct sway_container *con, + int width, int height, int x, int y, bool title_bar, int gaps) { + if (!config->animation_duration_ms || !con->view + || con->animation_state.seat_is_resizing + || con->animation_state.seat_is_moving_float) { + finish_animation(&con->animation_state.animation); + + _arrange_container(con, width, height, x, y, title_bar, gaps); + return; + } + + if (con->animation_state.to_x == x && + con->animation_state.to_y == y && + con->animation_state.to_width == width && + con->animation_state.to_height == height) { + if (!con->animation_state.animation.initialized || + con->animation_state.animation.progress >= 1.0f) { + _arrange_container(con, width, height, x, y, title_bar, gaps); + } + return; + } + + con->animation_state.to_x = x; + con->animation_state.to_y = y; + con->animation_state.to_width = width; + con->animation_state.to_height = height; + con->animation_state.to_alpha = 1.0f; + + // open animation — pop-in: grow from center while fading in + if (con->animation_state.from_x == -1) { + con->animation_state.from_x = x + width * (1.0f - POPIN_FACTOR) / 2.0f; + con->animation_state.from_y = y + height * (1.0f - POPIN_FACTOR) / 2.0f; + con->animation_state.from_width = width * POPIN_FACTOR; + con->animation_state.from_height = height * POPIN_FACTOR; + con->animation_state.from_alpha = 0.0f; + add_animation(&con->animation_state.animation, anim_update_callback, NULL); + } else { + // move animation + snap_animation_position(con); + con->animation_state.from_width = con->animation_state.current_width; + con->animation_state.from_height = con->animation_state.current_height; + con->animation_state.from_alpha = get_animated_value(con->animation_state.from_alpha, + con->animation_state.to_alpha, &con->animation_state.animation); + add_animation(&con->animation_state.animation, anim_update_callback, NULL); + } + + // arrange at starting state to "win" position race between animation start and the reparent + _arrange_container(con, con->animation_state.from_width, + con->animation_state.from_height, + con->animation_state.from_x, con->animation_state.from_y, + con_has_title_bar(con), 0); +} + +static int container_get_gaps(struct sway_container *con) { + struct sway_workspace *ws = con->current.workspace; + struct sway_container *temp = con; + while (temp) { + enum sway_container_layout layout; + if (temp->current.parent) { + layout = temp->current.parent->current.layout; + } else { + layout = ws->current.layout; + } + if (layout == L_TABBED || layout == L_STACKED) { + return 0; + } + temp = temp->pending.parent; + } + return ws->gaps_inner; +} + +static void arrange_fullscreen(struct wlr_scene_tree *tree, + struct sway_container *fs, struct sway_workspace *ws, + int width, int height) { + struct wlr_scene_node *fs_node; + if (fs->view) { + fs_node = &fs->view->scene_tree->node; + + // if we only care about the view, disable any decorations + wlr_scene_node_set_enabled(&fs->scene_tree->node, false); + + // reconfigure the output handler (for foreign toplevel) to cover the + // view without container decorations + wlr_scene_node_set_position(&fs->view->output_handler->node, 0, 0); + wlr_scene_buffer_set_dest_size(fs->view->output_handler, width, height); + } else { + fs_node = &fs->scene_tree->node; + arrange_container(fs, width, height, 0, 0, true, container_get_gaps(fs)); + } + + wlr_scene_node_reparent(fs_node, tree); + wlr_scene_node_lower_to_bottom(fs_node); + wlr_scene_node_set_position(fs_node, 0, 0); +} + +static void arrange_workspace_floating(struct sway_workspace *ws) { + for (int i = 0; i < ws->current.floating->length; i++) { + struct sway_container *floater = ws->current.floating->items[i]; + struct wlr_scene_tree *layer = root->layers.floating; + + if (floater->current.fullscreen_mode != FULLSCREEN_NONE) { + continue; + } + + if (root->fullscreen_global) { + if (container_is_transient_for(floater, root->fullscreen_global)) { + layer = root->layers.fullscreen_global; + } + } else { + for (int i = 0; i < root->outputs->length; i++) { + struct sway_output *output = root->outputs->items[i]; + struct sway_workspace *active = output->current.active_workspace; + + if (active && active->fullscreen && + container_is_transient_for(floater, active->fullscreen)) { + layer = root->layers.fullscreen; + } + } + } + + wlr_scene_node_reparent(&floater->scene_tree->node, layer); + wlr_scene_node_set_enabled(&floater->scene_tree->node, true); + wlr_scene_node_set_enabled(&floater->shadow->node, container_has_shadow(floater) && floater->view); + wlr_scene_node_set_enabled(&floater->border.tree->node, true); + + arrange_container(floater, floater->current.width, floater->current.height, + floater->current.x, floater->current.y, true, ws->gaps_inner); + } +} + +static void arrange_workspace_tiling(struct sway_workspace *ws, + int width, int height) { + arrange_children(ws->current.layout, ws->current.tiling, + ws->current.focused_inactive_child, ws->layers.tiling, + width, height, ws->gaps_inner); +} + +static void disable_workspace(struct sway_workspace *ws) { + // if any containers were just moved to a disabled workspace it will + // have the parent of the old workspace. Move the workspace so that it won't + // be shown. + for (int i = 0; i < ws->current.tiling->length; i++) { + struct sway_container *child = ws->current.tiling->items[i]; + + wlr_scene_node_reparent(&child->scene_tree->node, ws->layers.tiling); + disable_container(child); + } + + for (int i = 0; i < ws->current.floating->length; i++) { + struct sway_container *floater = ws->current.floating->items[i]; + wlr_scene_node_reparent(&floater->scene_tree->node, root->layers.floating); + disable_container(floater); + wlr_scene_node_set_enabled(&floater->scene_tree->node, false); + } +} + +static void arrange_output(struct sway_output *output, int width, int height) { + struct sway_workspace *new_active = output->current.active_workspace; + struct sway_workspace *old_active = output->prev_active_workspace; + + bool is_ws_switch = old_active && old_active != new_active + && output->wlr_output->enabled && config->animation_duration_ms > 0 && + !(old_active->current.fullscreen || new_active->current.fullscreen); + + if (is_ws_switch) { + new_active->animation_state.from_alpha = 0.0f; + + if (old_active->current.tiling->length == 0 + && old_active->current.floating->length == 0) { + new_active->animation_state.to_alpha = 1.0f; + add_animation(&new_active->animation_state.animation, + workspace_fade_update_callback, NULL); + } else { + new_active->animation_state.to_alpha = 0.0f; + float current_alpha = get_animated_value(old_active->animation_state.from_alpha, + old_active->animation_state.to_alpha, &old_active->animation_state.animation); + old_active->animation_state.from_alpha = current_alpha; + old_active->animation_state.to_alpha = 0.0f; + + finish_animation(&new_active->animation_state.animation); + new_active->animation_state.from_alpha = 0.0f; + new_active->animation_state.to_alpha = 1.0f; + + add_animation(&old_active->animation_state.animation, + workspace_fade_update_callback, workspace_fade_complete_callback); + add_animation(&new_active->animation_state.animation, + workspace_fade_update_callback, workspace_fade_complete_callback); + } + } + + for (int i = 0; i < output->current.workspaces->length; i++) { + struct sway_workspace *child = output->current.workspaces->items[i]; + + bool activated = output->current.active_workspace == child && output->wlr_output->enabled; + bool animating = child->animation_state.animation.initialized; + + wlr_scene_node_reparent(&child->layers.tiling->node, output->layers.tiling); + wlr_scene_node_reparent(&child->layers.fullscreen->node, output->layers.fullscreen); + + for (int i = 0; i < child->current.floating->length; i++) { + struct sway_container *floater = child->current.floating->items[i]; + wlr_scene_node_reparent(&floater->scene_tree->node, root->layers.floating); + wlr_scene_node_set_enabled(&floater->scene_tree->node, activated || animating); + } + + if (activated) { + struct sway_container *fs = child->current.fullscreen; + wlr_scene_node_set_enabled(&child->layers.tiling->node, !fs); + wlr_scene_node_set_enabled(&child->layers.fullscreen->node, fs); + + wlr_scene_node_set_enabled(&output->layers.shell_background->node, !fs); + wlr_scene_node_set_enabled(&output->layers.shell_bottom->node, !fs); + wlr_scene_node_set_enabled(&output->layers.blur_layer->node, !fs); + wlr_scene_node_set_enabled(&output->layers.fullscreen->node, fs); + + if (fs) { + disable_workspace(child); + + wlr_scene_rect_set_size(output->fullscreen_background, width, height); + + arrange_workspace_floating(child); + arrange_fullscreen(child->layers.fullscreen, fs, child, + width, height); + } else { + struct wlr_box *area = &output->usable_area; + struct side_gaps *gaps = &child->current_gaps; + + wlr_scene_node_set_position(&child->layers.tiling->node, + gaps->left + area->x, gaps->top + area->y); + + arrange_workspace_tiling(child, + area->width - gaps->left - gaps->right, + area->height - gaps->top - gaps->bottom); + arrange_workspace_floating(child); + } + } else if (animating && !activated) { + // Workspace fading out - keep visible, alpha handled by render path + struct wlr_box *area = &output->usable_area; + struct side_gaps *gaps = &child->current_gaps; + + wlr_scene_node_set_enabled(&child->layers.tiling->node, true); + wlr_scene_node_set_enabled(&child->layers.fullscreen->node, false); + + wlr_scene_node_set_position(&child->layers.tiling->node, + gaps->left + area->x, gaps->top + area->y); + + arrange_workspace_tiling(child, + area->width - gaps->left - gaps->right, + area->height - gaps->top - gaps->bottom); + } else { + wlr_scene_node_set_enabled(&child->layers.tiling->node, false); + wlr_scene_node_set_enabled(&child->layers.fullscreen->node, false); + + disable_workspace(child); + } + } + + output->prev_active_workspace = new_active; +} + +void arrange_popups(struct wlr_scene_tree *popups) { + struct wlr_scene_node *node; + wl_list_for_each(node, &popups->children, link) { + struct sway_popup_desc *popup = scene_descriptor_try_get(node, + SWAY_SCENE_DESC_POPUP); + + if (popup) { + int lx, ly; + wlr_scene_node_coords(popup->relative, &lx, &ly); + wlr_scene_node_set_position(node, lx, ly); + } + } +} + +static void arrange_root(struct sway_root *root) { + struct sway_container *fs = root->fullscreen_global; + + wlr_scene_node_set_enabled(&root->layers.shell_background->node, !fs); + wlr_scene_node_set_enabled(&root->layers.shell_bottom->node, !fs); + wlr_scene_node_set_enabled(&root->layers.blur_tree->node, !fs); + wlr_scene_node_set_enabled(&root->layers.tiling->node, !fs); + wlr_scene_node_set_enabled(&root->layers.floating->node, !fs); + wlr_scene_node_set_enabled(&root->layers.shell_top->node, !fs); + wlr_scene_node_set_enabled(&root->layers.fullscreen->node, !fs); + + // hide all contents in the scratchpad + for (int i = 0; i < root->scratchpad->length; i++) { + struct sway_container *con = root->scratchpad->items[i]; + + disable_container(con); + wlr_scene_node_set_enabled(&con->scene_tree->node, false); + } + + if (fs) { + for (int i = 0; i < root->outputs->length; i++) { + struct sway_output *output = root->outputs->items[i]; + struct sway_workspace *ws = output->current.active_workspace; + + wlr_scene_output_set_position(output->scene_output, output->lx, output->ly); + + // disable all workspaces to get to a known state + for (int j = 0; j < output->current.workspaces->length; j++) { + struct sway_workspace *workspace = output->current.workspaces->items[j]; + disable_workspace(workspace); + } + + // arrange the active workspace + if (ws) { + arrange_workspace_floating(ws); + } + } + + arrange_fullscreen(root->layers.fullscreen_global, fs, NULL, + root->width, root->height); + } else { + for (int i = 0; i < root->outputs->length; i++) { + struct sway_output *output = root->outputs->items[i]; + + wlr_scene_output_set_position(output->scene_output, output->lx, output->ly); + + wlr_scene_node_reparent(&output->layers.shell_background->node, root->layers.shell_background); + wlr_scene_node_reparent(&output->layers.shell_bottom->node, root->layers.shell_bottom); + wlr_scene_node_reparent(&output->layers.blur_layer->node, root->layers.blur_tree); + wlr_scene_node_reparent(&output->layers.tiling->node, root->layers.tiling); + wlr_scene_node_reparent(&output->layers.shell_top->node, root->layers.shell_top); + wlr_scene_node_reparent(&output->layers.shell_overlay->node, root->layers.shell_overlay); + wlr_scene_node_reparent(&output->layers.fullscreen->node, root->layers.fullscreen); + wlr_scene_node_reparent(&output->layers.session_lock->node, root->layers.session_lock); + + wlr_scene_node_set_position(&output->layers.shell_background->node, output->lx, output->ly); + wlr_scene_node_set_position(&output->layers.shell_bottom->node, output->lx, output->ly); + wlr_scene_node_set_position(&output->layers.blur_layer->node, output->lx, output->ly); + wlr_scene_node_set_position(&output->layers.tiling->node, output->lx, output->ly); + wlr_scene_node_set_position(&output->layers.fullscreen->node, output->lx, output->ly); + wlr_scene_node_set_position(&output->layers.shell_top->node, output->lx, output->ly); + wlr_scene_node_set_position(&output->layers.shell_overlay->node, output->lx, output->ly); + wlr_scene_node_set_position(&output->layers.session_lock->node, output->lx, output->ly); + + arrange_output(output, output->width, output->height); + } + } + + arrange_popups(root->layers.popup); +} + +/** + * Apply a transaction to the "current" state of the tree. + */ +static void transaction_apply(struct sway_transaction *transaction) { + sway_log(SWAY_DEBUG, "Applying transaction %p", transaction); + if (debug.txn_timings) { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + struct timespec *commit = &transaction->commit_time; + float ms = (now.tv_sec - commit->tv_sec) * 1000 + + (now.tv_nsec - commit->tv_nsec) / 1000000.0; + sway_log(SWAY_DEBUG, "Transaction %p: %.1fms waiting " + "(%.1f frames if 60Hz)", transaction, ms, ms / (1000.0f / 60)); + } + + // Apply the instruction state to the node's current state + for (int i = 0; i < transaction->instructions->length; ++i) { + struct sway_transaction_instruction *instruction = + transaction->instructions->items[i]; + struct sway_node *node = instruction->node; + + switch (node->type) { + case N_ROOT: + break; + case N_OUTPUT: + apply_output_state(node->sway_output, &instruction->output_state); + break; + case N_WORKSPACE: + apply_workspace_state(node->sway_workspace, + &instruction->workspace_state); + break; + case N_CONTAINER: + apply_container_state(node->sway_container, + &instruction->container_state); + break; + } + + node->instruction = NULL; + } +} + +static void transaction_commit_pending(void); + +static void transaction_progress(void) { + if (!server.queued_transaction) { + return; + } + if (server.queued_transaction->num_waiting > 0) { + return; + } + transaction_apply(server.queued_transaction); + arrange_root(root); + cursor_rebase_all(); + transaction_destroy(server.queued_transaction); + start_animations(); + server.queued_transaction = NULL; + + if (!server.pending_transaction) { + sway_idle_inhibit_v1_check_active(); + return; + } + + transaction_commit_pending(); +} + +static int handle_timeout(void *data) { + struct sway_transaction *transaction = data; + sway_log(SWAY_DEBUG, "Transaction %p timed out (%zi waiting)", + transaction, transaction->num_waiting); + transaction->num_waiting = 0; + transaction_progress(); + return 0; +} + +static bool should_configure(struct sway_node *node, + struct sway_transaction_instruction *instruction) { + if (!node_is_view(node)) { + return false; + } + if (node->destroying) { + return false; + } + if (!instruction->server_request) { + return false; + } + struct sway_container_state *cstate = &node->sway_container->current; + struct sway_container_state *istate = &instruction->container_state; +#if WLR_HAS_XWAYLAND + // Xwayland views are position-aware and need to be reconfigured + // when their position changes. + if (node->sway_container->view->type == SWAY_VIEW_XWAYLAND) { + // Sway logical coordinates are doubles, but they get truncated to + // integers when sent to Xwayland through `xcb_configure_window`. + // X11 apps will not respond to duplicate configure requests (from their + // truncated point of view) and cause transactions to time out. + if ((int)cstate->content_x != (int)istate->content_x || + (int)cstate->content_y != (int)istate->content_y) { + return true; + } + } +#endif + if (cstate->content_width == istate->content_width && + cstate->content_height == istate->content_height) { + return false; + } + return true; +} + +static void transaction_commit(struct sway_transaction *transaction) { + sway_log(SWAY_DEBUG, "Transaction %p committing with %i instructions", + transaction, transaction->instructions->length); + transaction->num_waiting = 0; + for (int i = 0; i < transaction->instructions->length; ++i) { + struct sway_transaction_instruction *instruction = + transaction->instructions->items[i]; + struct sway_node *node = instruction->node; + bool hidden = node_is_view(node) && !node->destroying && + !view_is_visible(node->sway_container->view); + if (should_configure(node, instruction)) { + instruction->serial = view_configure(node->sway_container->view, + instruction->container_state.content_x, + instruction->container_state.content_y, + instruction->container_state.content_width, + instruction->container_state.content_height); + if (!hidden) { + instruction->waiting = true; + ++transaction->num_waiting; + } + + view_send_frame_done(node->sway_container->view); + } + if (!hidden && node_is_view(node) && + !node->sway_container->view->saved_surface_tree) { + view_save_buffer(node->sway_container->view); + } + node->instruction = instruction; + } + transaction->num_configures = transaction->num_waiting; + if (debug.txn_timings) { + clock_gettime(CLOCK_MONOTONIC, &transaction->commit_time); + } + if (debug.noatomic) { + transaction->num_waiting = 0; + } else if (debug.txn_wait) { + // Force the transaction to time out even if all views are ready. + // We do this by inflating the waiting counter. + transaction->num_waiting += 1000000; + } + + if (transaction->num_waiting) { + // Set up a timer which the views must respond within + transaction->timer = wl_event_loop_add_timer(server.wl_event_loop, + handle_timeout, transaction); + if (transaction->timer) { + wl_event_source_timer_update(transaction->timer, + server.txn_timeout_ms); + } else { + sway_log_errno(SWAY_ERROR, "Unable to create transaction timer " + "(some imperfect frames might be rendered)"); + transaction->num_waiting = 0; + } + } +} + +static void transaction_commit_pending(void) { + if (server.queued_transaction) { + return; + } + struct sway_transaction *transaction = server.pending_transaction; + server.pending_transaction = NULL; + server.queued_transaction = transaction; + transaction_commit(transaction); + transaction_progress(); +} + +static void set_instruction_ready( + struct sway_transaction_instruction *instruction) { + struct sway_transaction *transaction = instruction->transaction; + + if (debug.txn_timings) { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + struct timespec *start = &transaction->commit_time; + float ms = (now.tv_sec - start->tv_sec) * 1000 + + (now.tv_nsec - start->tv_nsec) / 1000000.0; + sway_log(SWAY_DEBUG, "Transaction %p: %zi/%zi ready in %.1fms (%s)", + transaction, + transaction->num_configures - transaction->num_waiting + 1, + transaction->num_configures, ms, + instruction->node->sway_container->title); + } + + // If the transaction has timed out then its num_waiting will be 0 already. + if (instruction->waiting && transaction->num_waiting > 0 && + --transaction->num_waiting == 0) { + sway_log(SWAY_DEBUG, "Transaction %p is ready", transaction); + wl_event_source_timer_update(transaction->timer, 0); + } + + instruction->node->instruction = NULL; + transaction_progress(); +} + +bool transaction_notify_view_ready_by_serial(struct sway_view *view, + uint32_t serial) { + struct sway_transaction_instruction *instruction = + view->container->node.instruction; + if (instruction != NULL && instruction->serial == serial) { + set_instruction_ready(instruction); + return true; + } + return false; +} + +bool transaction_notify_view_ready_by_geometry(struct sway_view *view, + double x, double y, int width, int height) { + struct sway_transaction_instruction *instruction = + view->container->node.instruction; + if (instruction != NULL && + (int)instruction->container_state.content_x == (int)x && + (int)instruction->container_state.content_y == (int)y && + instruction->container_state.content_width == width && + instruction->container_state.content_height == height) { + set_instruction_ready(instruction); + return true; + } + return false; +} + +static void _transaction_commit_dirty(bool server_request) { + if (!server.dirty_nodes->length) { + return; + } + + if (!server.pending_transaction) { + server.pending_transaction = transaction_create(); + if (!server.pending_transaction) { + return; + } + } + + for (int i = 0; i < server.dirty_nodes->length; ++i) { + struct sway_node *node = server.dirty_nodes->items[i]; + transaction_add_node(server.pending_transaction, node, server_request); + node->dirty = false; + } + server.dirty_nodes->length = 0; + + transaction_commit_pending(); +} + +void transaction_commit_dirty(void) { + _transaction_commit_dirty(true); +} + +void transaction_commit_dirty_client(void) { + _transaction_commit_dirty(false); +} diff --git a/sway/desktop/xdg_shell.c b/sway/desktop/xdg_shell.c new file mode 100644 index 0000000..c3b4e4f --- /dev/null +++ b/sway/desktop/xdg_shell.c @@ -0,0 +1,629 @@ +#include +#include +#include +#include +#include +#include +#include +#include "log.h" +#include "sway/decoration.h" +#include "sway/scene_descriptor.h" +#include "sway/desktop/transaction.h" +#include "sway/input/cursor.h" +#include "sway/input/input-manager.h" +#include "sway/input/seat.h" +#include "sway/output.h" +#include "sway/tree/arrange.h" +#include "sway/tree/container.h" +#include "sway/tree/view.h" +#include "sway/tree/workspace.h" +#include "sway/xdg_decoration.h" + +static struct sway_xdg_popup *popup_create( + struct wlr_xdg_popup *wlr_popup, struct sway_view *view, + struct wlr_scene_tree *parent, struct wlr_scene_tree *image_capture_parent); + +static void popup_handle_new_popup(struct wl_listener *listener, void *data) { + struct sway_xdg_popup *popup = + wl_container_of(listener, popup, new_popup); + struct wlr_xdg_popup *wlr_popup = data; + popup_create(wlr_popup, popup->view, popup->xdg_surface_tree, popup->image_capture_tree); +} + +static void popup_handle_destroy(struct wl_listener *listener, void *data) { + struct sway_xdg_popup *popup = wl_container_of(listener, popup, destroy); + + wl_list_remove(&popup->new_popup.link); + wl_list_remove(&popup->destroy.link); + wl_list_remove(&popup->surface_commit.link); + wl_list_remove(&popup->reposition.link); + wlr_scene_node_destroy(&popup->scene_tree->node); + free(popup); +} + +static void popup_unconstrain(struct sway_xdg_popup *popup) { + struct sway_view *view = popup->view; + struct wlr_xdg_popup *wlr_popup = popup->wlr_xdg_popup; + + struct sway_workspace *workspace = view->container->pending.workspace; + if (!workspace) { + // is null if in the scratchpad + return; + } + + struct sway_output *output = workspace->output; + + // the output box expressed in the coordinate system of the toplevel parent + // of the popup + struct wlr_box output_toplevel_sx_box = { + .x = output->lx - view->container->pending.content_x + view->geometry.x, + .y = output->ly - view->container->pending.content_y + view->geometry.y, + .width = output->width, + .height = output->height, + }; + + wlr_xdg_popup_unconstrain_from_box(wlr_popup, &output_toplevel_sx_box); +} + +static void popup_handle_surface_commit(struct wl_listener *listener, void *data) { + struct sway_xdg_popup *popup = wl_container_of(listener, popup, surface_commit); + if (popup->wlr_xdg_popup->base->initial_commit) { + popup_unconstrain(popup); + } +} + +static void popup_handle_reposition(struct wl_listener *listener, void *data) { + struct sway_xdg_popup *popup = wl_container_of(listener, popup, reposition); + popup_unconstrain(popup); +} + +static struct sway_xdg_popup *popup_create(struct wlr_xdg_popup *wlr_popup, + struct sway_view *view, struct wlr_scene_tree *parent, + struct wlr_scene_tree *image_capture_parent) { + struct wlr_xdg_surface *xdg_surface = wlr_popup->base; + + struct sway_xdg_popup *popup = calloc(1, sizeof(struct sway_xdg_popup)); + if (!popup) { + return NULL; + } + + popup->wlr_xdg_popup = wlr_popup; + popup->view = view; + + popup->scene_tree = wlr_scene_tree_create(parent); + if (!popup->scene_tree) { + free(popup); + return NULL; + } + + popup->xdg_surface_tree = wlr_scene_xdg_surface_create( + popup->scene_tree, xdg_surface); + if (!popup->xdg_surface_tree) { + wlr_scene_node_destroy(&popup->scene_tree->node); + free(popup); + return NULL; + } + + popup->desc.relative = &view->content_tree->node; + popup->desc.view = view; + + if (!scene_descriptor_assign(&popup->scene_tree->node, + SWAY_SCENE_DESC_POPUP, &popup->desc)) { + sway_log(SWAY_ERROR, "Failed to allocate a popup scene descriptor"); + wlr_scene_node_destroy(&popup->scene_tree->node); + free(popup); + return NULL; + } + + popup->image_capture_tree = wlr_scene_xdg_surface_create(image_capture_parent, xdg_surface); + if (popup->image_capture_tree == NULL) { + return NULL; + } + + popup->wlr_xdg_popup = xdg_surface->popup; + struct sway_xdg_shell_view *shell_view = + wl_container_of(view, shell_view, view); + xdg_surface->data = shell_view; + + wl_signal_add(&xdg_surface->surface->events.commit, &popup->surface_commit); + popup->surface_commit.notify = popup_handle_surface_commit; + wl_signal_add(&xdg_surface->events.new_popup, &popup->new_popup); + popup->new_popup.notify = popup_handle_new_popup; + wl_signal_add(&wlr_popup->events.reposition, &popup->reposition); + popup->reposition.notify = popup_handle_reposition; + wl_signal_add(&wlr_popup->events.destroy, &popup->destroy); + popup->destroy.notify = popup_handle_destroy; + + return popup; +} + +static struct sway_xdg_shell_view *xdg_shell_view_from_view( + struct sway_view *view) { + if (!sway_assert(view->type == SWAY_VIEW_XDG_SHELL, + "Expected xdg_shell view")) { + return NULL; + } + return (struct sway_xdg_shell_view *)view; +} + +static void get_constraints(struct sway_view *view, double *min_width, + double *max_width, double *min_height, double *max_height) { + struct wlr_xdg_toplevel_state *state = + &view->wlr_xdg_toplevel->current; + *min_width = state->min_width > 0 ? state->min_width : DBL_MIN; + *max_width = state->max_width > 0 ? state->max_width : DBL_MAX; + *min_height = state->min_height > 0 ? state->min_height : DBL_MIN; + *max_height = state->max_height > 0 ? state->max_height : DBL_MAX; +} + +static const char *get_string_prop(struct sway_view *view, + enum sway_view_prop prop) { + struct sway_xdg_shell_view *xdg_shell_view = xdg_shell_view_from_view(view); + if (xdg_shell_view == NULL) { + return NULL; + } + switch (prop) { + case VIEW_PROP_TITLE: + return view->wlr_xdg_toplevel->title; + case VIEW_PROP_APP_ID: + return view->wlr_xdg_toplevel->app_id; + case VIEW_PROP_TAG: + return xdg_shell_view->tag; + default: + return NULL; + } +} + +static uint32_t configure(struct sway_view *view, double lx, double ly, + int width, int height) { + struct sway_xdg_shell_view *xdg_shell_view = + xdg_shell_view_from_view(view); + if (xdg_shell_view == NULL) { + return 0; + } + return wlr_xdg_toplevel_set_size(view->wlr_xdg_toplevel, + width, height); +} + +static void set_activated(struct sway_view *view, bool activated) { + if (xdg_shell_view_from_view(view) == NULL) { + return; + } + wlr_xdg_toplevel_set_activated(view->wlr_xdg_toplevel, activated); +} + +static void set_tiled(struct sway_view *view, bool tiled) { + if (xdg_shell_view_from_view(view) == NULL) { + return; + } + if (wl_resource_get_version(view->wlr_xdg_toplevel->resource) >= + XDG_TOPLEVEL_STATE_TILED_LEFT_SINCE_VERSION) { + enum wlr_edges edges = WLR_EDGE_NONE; + if (tiled) { + edges = WLR_EDGE_LEFT | WLR_EDGE_RIGHT | WLR_EDGE_TOP | + WLR_EDGE_BOTTOM; + } + wlr_xdg_toplevel_set_tiled(view->wlr_xdg_toplevel, edges); + } else { + // The version is too low for the tiled state; configure as maximized instead + // to stop the client from drawing decorations outside of the toplevel geometry. + wlr_xdg_toplevel_set_maximized(view->wlr_xdg_toplevel, tiled); + } +} + +static void set_fullscreen(struct sway_view *view, bool fullscreen) { + if (xdg_shell_view_from_view(view) == NULL) { + return; + } + wlr_xdg_toplevel_set_fullscreen(view->wlr_xdg_toplevel, fullscreen); +} + +static void set_resizing(struct sway_view *view, bool resizing) { + if (xdg_shell_view_from_view(view) == NULL) { + return; + } + wlr_xdg_toplevel_set_resizing(view->wlr_xdg_toplevel, resizing); +} + +static bool wants_floating(struct sway_view *view) { + struct wlr_xdg_toplevel *toplevel = view->wlr_xdg_toplevel; + struct wlr_xdg_toplevel_state *state = &toplevel->current; + return (state->min_width != 0 && state->min_height != 0 + && (state->min_width == state->max_width + || state->min_height == state->max_height)) + || toplevel->parent; +} + +static bool is_transient_for(struct sway_view *child, + struct sway_view *ancestor) { + if (xdg_shell_view_from_view(child) == NULL) { + return false; + } + struct wlr_xdg_toplevel *toplevel = child->wlr_xdg_toplevel; + while (toplevel) { + if (toplevel->parent == ancestor->wlr_xdg_toplevel) { + return true; + } + toplevel = toplevel->parent; + } + return false; +} + +static void _close(struct sway_view *view) { + if (xdg_shell_view_from_view(view) == NULL) { + return; + } + wlr_xdg_toplevel_send_close(view->wlr_xdg_toplevel); +} + +static void close_popups(struct sway_view *view) { + struct wlr_xdg_popup *popup, *tmp; + wl_list_for_each_safe(popup, tmp, &view->wlr_xdg_toplevel->base->popups, link) { + wlr_xdg_popup_destroy(popup); + } +} + +static void destroy(struct sway_view *view) { + struct sway_xdg_shell_view *xdg_shell_view = + xdg_shell_view_from_view(view); + if (xdg_shell_view == NULL) { + return; + } + free(xdg_shell_view->tag); + free(xdg_shell_view); +} + +static const struct sway_view_impl view_impl = { + .get_constraints = get_constraints, + .get_string_prop = get_string_prop, + .configure = configure, + .set_activated = set_activated, + .set_tiled = set_tiled, + .set_fullscreen = set_fullscreen, + .set_resizing = set_resizing, + .wants_floating = wants_floating, + .is_transient_for = is_transient_for, + .close = _close, + .close_popups = close_popups, + .destroy = destroy, +}; + +static void handle_commit(struct wl_listener *listener, void *data) { + struct sway_xdg_shell_view *xdg_shell_view = + wl_container_of(listener, xdg_shell_view, commit); + struct sway_view *view = &xdg_shell_view->view; + struct wlr_xdg_surface *xdg_surface = view->wlr_xdg_toplevel->base; + + if (xdg_surface->initial_commit) { + if (view->xdg_decoration != NULL) { + set_xdg_decoration_mode(view->xdg_decoration); + } + // XXX: https://github.com/swaywm/sway/issues/2176 + wlr_xdg_surface_schedule_configure(xdg_surface); + wlr_xdg_toplevel_set_wm_capabilities(view->wlr_xdg_toplevel, + WLR_XDG_TOPLEVEL_WM_CAPABILITIES_FULLSCREEN); + // TODO: wlr_xdg_toplevel_set_bounds() + return; + } + + if (!xdg_surface->surface->mapped) { + return; + } + + struct wlr_box *new_geo = &xdg_surface->geometry; + bool new_size = new_geo->width != view->geometry.width || + new_geo->height != view->geometry.height || + new_geo->x != view->geometry.x || + new_geo->y != view->geometry.y; + + if (new_size) { + // The client changed its surface size in this commit. For floating + // containers, we resize the container to match. For tiling containers, + // we only recenter the surface. + memcpy(&view->geometry, new_geo, sizeof(struct wlr_box)); + if (container_is_floating(view->container)) { + view_update_size(view); + // Only set the toplevel size the current container actually has a size. + if (view->container->current.width) { + wlr_xdg_toplevel_set_size(view->wlr_xdg_toplevel, view->geometry.width, + view->geometry.height); + } + transaction_commit_dirty_client(); + } + + view_center_and_clip_surface(view); + } + + if (view->container->node.instruction) { + bool successful = transaction_notify_view_ready_by_serial(view, + xdg_surface->current.configure_serial); + + // If we saved the view and this commit isn't what we're looking for + // that means the user will never actually see the buffers submitted to + // us here. Just send frame done events to these surfaces so they can + // commit another time for us. + if (view->saved_surface_tree && !successful) { + view_send_frame_done(view); + } + } +} + +static void handle_set_title(struct wl_listener *listener, void *data) { + struct sway_xdg_shell_view *xdg_shell_view = + wl_container_of(listener, xdg_shell_view, set_title); + struct sway_view *view = &xdg_shell_view->view; + view_update_title(view, false); + view_execute_criteria(view); + transaction_commit_dirty(); +} + +static void handle_set_app_id(struct wl_listener *listener, void *data) { + struct sway_xdg_shell_view *xdg_shell_view = + wl_container_of(listener, xdg_shell_view, set_app_id); + struct sway_view *view = &xdg_shell_view->view; + view_update_app_id(view); + view_execute_criteria(view); + transaction_commit_dirty(); +} + +static void handle_new_popup(struct wl_listener *listener, void *data) { + struct sway_xdg_shell_view *xdg_shell_view = + wl_container_of(listener, xdg_shell_view, new_popup); + struct wlr_xdg_popup *wlr_popup = data; + + struct sway_xdg_popup *popup = popup_create(wlr_popup, + &xdg_shell_view->view, root->layers.popup, xdg_shell_view->image_capture_tree); + if (!popup) { + return; + } + + int lx, ly; + wlr_scene_node_coords(&popup->view->content_tree->node, &lx, &ly); + wlr_scene_node_set_position(&popup->scene_tree->node, lx, ly); +} + +static void handle_request_maximize(struct wl_listener *listener, void *data) { + struct sway_xdg_shell_view *xdg_shell_view = + wl_container_of(listener, xdg_shell_view, request_maximize); + struct wlr_xdg_toplevel *toplevel = xdg_shell_view->view.wlr_xdg_toplevel; + if (!toplevel->base->surface->mapped) { + return; + } + wlr_xdg_surface_schedule_configure(toplevel->base); +} + +static void handle_request_minimize(struct wl_listener *listener, void *data) { + struct sway_xdg_shell_view *xdg_shell_view = + wl_container_of(listener, xdg_shell_view, request_minimize); + struct sway_container *container = xdg_shell_view->view.container; + if (!container->pending.workspace) { + while (container->pending.parent) { + container = container->pending.parent; + } + } + if (!container->scratchpad) { + root_scratchpad_add_container(container, NULL); + } else if (container->pending.workspace) { + root_scratchpad_hide(container); + } + transaction_commit_dirty(); +} + +static void handle_request_fullscreen(struct wl_listener *listener, void *data) { + struct sway_xdg_shell_view *xdg_shell_view = + wl_container_of(listener, xdg_shell_view, request_fullscreen); + struct wlr_xdg_toplevel *toplevel = xdg_shell_view->view.wlr_xdg_toplevel; + struct sway_view *view = &xdg_shell_view->view; + + if (!toplevel->base->surface->mapped) { + return; + } + + struct sway_container *container = view->container; + struct wlr_xdg_toplevel_requested *req = &toplevel->requested; + if (req->fullscreen && req->fullscreen_output && req->fullscreen_output->data) { + struct sway_output *output = req->fullscreen_output->data; + struct sway_workspace *ws = output_get_active_workspace(output); + if (ws && !container_is_scratchpad_hidden(container) && + container->pending.workspace != ws) { + if (container_is_floating(container)) { + workspace_add_floating(ws, container); + } else { + container = workspace_add_tiling(ws, container); + } + } + } + + container_set_fullscreen(container, req->fullscreen); + + arrange_root(); + transaction_commit_dirty(); +} + +static void handle_request_move(struct wl_listener *listener, void *data) { + struct sway_xdg_shell_view *xdg_shell_view = + wl_container_of(listener, xdg_shell_view, request_move); + struct sway_view *view = &xdg_shell_view->view; + if (!container_is_floating(view->container) || + view->container->pending.fullscreen_mode) { + return; + } + struct wlr_xdg_toplevel_move_event *e = data; + struct sway_seat *seat = e->seat->seat->data; + if (e->serial == seat->last_button_serial) { + seatop_begin_move_floating(seat, view->container); + } +} + +static void handle_request_resize(struct wl_listener *listener, void *data) { + struct sway_xdg_shell_view *xdg_shell_view = + wl_container_of(listener, xdg_shell_view, request_resize); + struct sway_view *view = &xdg_shell_view->view; + if (!container_is_floating(view->container)) { + return; + } + struct wlr_xdg_toplevel_resize_event *e = data; + struct sway_seat *seat = e->seat->seat->data; + if (e->serial == seat->last_button_serial) { + seatop_begin_resize_floating(seat, view->container, e->edges); + } +} + +static void handle_unmap(struct wl_listener *listener, void *data) { + struct sway_xdg_shell_view *xdg_shell_view = + wl_container_of(listener, xdg_shell_view, unmap); + struct sway_view *view = &xdg_shell_view->view; + + if (!sway_assert(view->surface, "Cannot unmap unmapped view")) { + return; + } + + view_unmap(view); + + wl_list_remove(&xdg_shell_view->new_popup.link); + wl_list_remove(&xdg_shell_view->request_maximize.link); + if (xdg_shell_view->request_minimize.notify) { + wl_list_remove(&xdg_shell_view->request_minimize.link); + } + wl_list_remove(&xdg_shell_view->request_fullscreen.link); + wl_list_remove(&xdg_shell_view->request_move.link); + wl_list_remove(&xdg_shell_view->request_resize.link); + wl_list_remove(&xdg_shell_view->set_title.link); + wl_list_remove(&xdg_shell_view->set_app_id.link); +} + +static void handle_map(struct wl_listener *listener, void *data) { + struct sway_xdg_shell_view *xdg_shell_view = + wl_container_of(listener, xdg_shell_view, map); + struct sway_view *view = &xdg_shell_view->view; + struct wlr_xdg_toplevel *toplevel = view->wlr_xdg_toplevel; + + view->natural_width = toplevel->base->geometry.width; + view->natural_height = toplevel->base->geometry.height; + + bool csd = false; + + if (view->xdg_decoration) { + enum wlr_xdg_toplevel_decoration_v1_mode mode = + view->xdg_decoration->wlr_xdg_decoration->requested_mode; + csd = mode == WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE; + } else { + struct sway_server_decoration *deco = + decoration_from_surface(toplevel->base->surface); + csd = !deco || deco->wlr_server_decoration->mode == + WLR_SERVER_DECORATION_MANAGER_MODE_CLIENT; + } + + view_map(view, toplevel->base->surface, + toplevel->requested.fullscreen, + toplevel->requested.fullscreen_output, + csd); + + transaction_commit_dirty(); + + xdg_shell_view->new_popup.notify = handle_new_popup; + wl_signal_add(&toplevel->base->events.new_popup, + &xdg_shell_view->new_popup); + + xdg_shell_view->request_maximize.notify = handle_request_maximize; + wl_signal_add(&toplevel->events.request_maximize, + &xdg_shell_view->request_maximize); + + if (config->scratchpad_minimize) { + xdg_shell_view->request_minimize.notify = handle_request_minimize; + wl_signal_add(&toplevel->events.request_minimize, + &xdg_shell_view->request_minimize); + } + + xdg_shell_view->request_fullscreen.notify = handle_request_fullscreen; + wl_signal_add(&toplevel->events.request_fullscreen, + &xdg_shell_view->request_fullscreen); + + xdg_shell_view->request_move.notify = handle_request_move; + wl_signal_add(&toplevel->events.request_move, + &xdg_shell_view->request_move); + + xdg_shell_view->request_resize.notify = handle_request_resize; + wl_signal_add(&toplevel->events.request_resize, + &xdg_shell_view->request_resize); + + xdg_shell_view->set_title.notify = handle_set_title; + wl_signal_add(&toplevel->events.set_title, + &xdg_shell_view->set_title); + + xdg_shell_view->set_app_id.notify = handle_set_app_id; + wl_signal_add(&toplevel->events.set_app_id, + &xdg_shell_view->set_app_id); +} + +static void handle_destroy(struct wl_listener *listener, void *data) { + struct sway_xdg_shell_view *xdg_shell_view = + wl_container_of(listener, xdg_shell_view, destroy); + struct sway_view *view = &xdg_shell_view->view; + if (!sway_assert(view->surface == NULL, "Tried to destroy a mapped view")) { + return; + } + wl_list_remove(&xdg_shell_view->destroy.link); + wl_list_remove(&xdg_shell_view->map.link); + wl_list_remove(&xdg_shell_view->unmap.link); + wl_list_remove(&xdg_shell_view->commit.link); + view->wlr_xdg_toplevel = NULL; + if (view->xdg_decoration) { + view->xdg_decoration->view = NULL; + } + view_begin_destroy(view); +} + +struct sway_view *view_from_wlr_xdg_surface( + struct wlr_xdg_surface *xdg_surface) { + return xdg_surface->data; +} + +void handle_xdg_shell_toplevel(struct wl_listener *listener, void *data) { + struct wlr_xdg_toplevel *xdg_toplevel = data; + + sway_log(SWAY_DEBUG, "New xdg_shell toplevel title='%s' app_id='%s'", + xdg_toplevel->title, xdg_toplevel->app_id); + wlr_xdg_surface_ping(xdg_toplevel->base); + + struct sway_xdg_shell_view *xdg_shell_view = + calloc(1, sizeof(struct sway_xdg_shell_view)); + if (!sway_assert(xdg_shell_view, "Failed to allocate view")) { + return; + } + + if (!view_init(&xdg_shell_view->view, SWAY_VIEW_XDG_SHELL, &view_impl)) { + free(xdg_shell_view); + return; + } + xdg_shell_view->view.wlr_xdg_toplevel = xdg_toplevel; + + xdg_shell_view->map.notify = handle_map; + wl_signal_add(&xdg_toplevel->base->surface->events.map, &xdg_shell_view->map); + + xdg_shell_view->unmap.notify = handle_unmap; + wl_signal_add(&xdg_toplevel->base->surface->events.unmap, &xdg_shell_view->unmap); + + xdg_shell_view->commit.notify = handle_commit; + wl_signal_add(&xdg_toplevel->base->surface->events.commit, + &xdg_shell_view->commit); + + xdg_shell_view->destroy.notify = handle_destroy; + wl_signal_add(&xdg_toplevel->events.destroy, &xdg_shell_view->destroy); + + wlr_scene_xdg_surface_create(xdg_shell_view->view.content_tree, xdg_toplevel->base); + xdg_shell_view->image_capture_tree = + wlr_scene_xdg_surface_create(&xdg_shell_view->view.image_capture_scene->tree, xdg_toplevel->base); + + xdg_toplevel->base->data = xdg_shell_view; +} + +void xdg_toplevel_tag_manager_v1_handle_set_tag(struct wl_listener *listener, void *data) { + const struct wlr_xdg_toplevel_tag_manager_v1_set_tag_event *event = data; + struct sway_view *view = view_from_wlr_xdg_surface(event->toplevel->base); + struct sway_xdg_shell_view *xdg_shell_view = xdg_shell_view_from_view(view); + free(xdg_shell_view->tag); + xdg_shell_view->tag = strdup(event->tag); + view_execute_criteria(view); + transaction_commit_dirty(); +} diff --git a/sway/desktop/xwayland.c b/sway/desktop/xwayland.c new file mode 100644 index 0000000..98062d9 --- /dev/null +++ b/sway/desktop/xwayland.c @@ -0,0 +1,951 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "log.h" +#include "sway/desktop/transaction.h" +#include "sway/input/cursor.h" +#include "sway/input/input-manager.h" +#include "sway/input/seat.h" +#include "sway/output.h" +#include "sway/scene_descriptor.h" +#include "sway/tree/arrange.h" +#include "sway/tree/container.h" +#include "sway/server.h" +#include "sway/tree/view.h" +#include "sway/tree/workspace.h" + +static const char *atom_map[ATOM_LAST] = { + [NET_WM_WINDOW_TYPE_NORMAL] = "_NET_WM_WINDOW_TYPE_NORMAL", + [NET_WM_WINDOW_TYPE_DIALOG] = "_NET_WM_WINDOW_TYPE_DIALOG", + [NET_WM_WINDOW_TYPE_UTILITY] = "_NET_WM_WINDOW_TYPE_UTILITY", + [NET_WM_WINDOW_TYPE_TOOLBAR] = "_NET_WM_WINDOW_TYPE_TOOLBAR", + [NET_WM_WINDOW_TYPE_SPLASH] = "_NET_WM_WINDOW_TYPE_SPLASH", + [NET_WM_WINDOW_TYPE_MENU] = "_NET_WM_WINDOW_TYPE_MENU", + [NET_WM_WINDOW_TYPE_DROPDOWN_MENU] = "_NET_WM_WINDOW_TYPE_DROPDOWN_MENU", + [NET_WM_WINDOW_TYPE_POPUP_MENU] = "_NET_WM_WINDOW_TYPE_POPUP_MENU", + [NET_WM_WINDOW_TYPE_TOOLTIP] = "_NET_WM_WINDOW_TYPE_TOOLTIP", + [NET_WM_WINDOW_TYPE_NOTIFICATION] = "_NET_WM_WINDOW_TYPE_NOTIFICATION", + [NET_WM_STATE_MODAL] = "_NET_WM_STATE_MODAL", +}; + +static void unmanaged_handle_request_configure(struct wl_listener *listener, + void *data) { + struct sway_xwayland_unmanaged *surface = + wl_container_of(listener, surface, request_configure); + struct wlr_xwayland_surface *xsurface = surface->wlr_xwayland_surface; + struct wlr_xwayland_surface_configure_event *ev = data; + wlr_xwayland_surface_configure(xsurface, ev->x, ev->y, + ev->width, ev->height); +} + +static void unmanaged_handle_set_geometry(struct wl_listener *listener, void *data) { + struct sway_xwayland_unmanaged *surface = + wl_container_of(listener, surface, set_geometry); + struct wlr_xwayland_surface *xsurface = surface->wlr_xwayland_surface; + + wlr_scene_node_set_position(&surface->surface_scene->buffer->node, xsurface->x, xsurface->y); +} + +static void unmanaged_handle_map(struct wl_listener *listener, void *data) { + struct sway_xwayland_unmanaged *surface = + wl_container_of(listener, surface, map); + struct wlr_xwayland_surface *xsurface = surface->wlr_xwayland_surface; + + surface->surface_scene = wlr_scene_surface_create(root->layers.unmanaged, + xsurface->surface); + + if (surface->surface_scene) { + scene_descriptor_assign(&surface->surface_scene->buffer->node, + SWAY_SCENE_DESC_XWAYLAND_UNMANAGED, surface); + wlr_scene_node_set_position(&surface->surface_scene->buffer->node, + xsurface->x, xsurface->y); + + wl_signal_add(&xsurface->events.set_geometry, &surface->set_geometry); + surface->set_geometry.notify = unmanaged_handle_set_geometry; + } + + if (wlr_xwayland_surface_override_redirect_wants_focus(xsurface)) { + struct sway_seat *seat = input_manager_current_seat(); + struct wlr_xwayland *xwayland = server.xwayland.wlr_xwayland; + wlr_xwayland_set_seat(xwayland, seat->wlr_seat); + seat_set_focus_surface(seat, xsurface->surface, false); + } +} + +static void unmanaged_handle_unmap(struct wl_listener *listener, void *data) { + struct sway_xwayland_unmanaged *surface = + wl_container_of(listener, surface, unmap); + struct wlr_xwayland_surface *xsurface = surface->wlr_xwayland_surface; + + if (surface->surface_scene) { + wl_list_remove(&surface->set_geometry.link); + + wlr_scene_node_destroy(&surface->surface_scene->buffer->node); + surface->surface_scene = NULL; + } + + struct sway_seat *seat = input_manager_current_seat(); + if (seat->wlr_seat->keyboard_state.focused_surface == xsurface->surface) { + // This simply returns focus to the parent surface if there's one available. + // This seems to handle JetBrains issues. + if (xsurface->parent && xsurface->parent->surface + && wlr_xwayland_surface_override_redirect_wants_focus(xsurface->parent)) { + seat_set_focus_surface(seat, xsurface->parent->surface, false); + return; + } + + // Restore focus + struct sway_node *previous = seat_get_focus_inactive(seat, &root->node); + if (previous) { + // Hack to get seat to re-focus the return value of get_focus + seat_set_focus(seat, NULL); + seat_set_focus(seat, previous); + } + } +} + +static void unmanaged_handle_request_activate(struct wl_listener *listener, void *data) { + struct sway_xwayland_unmanaged *surface = + wl_container_of(listener, surface, request_activate); + struct wlr_xwayland_surface *xsurface = surface->wlr_xwayland_surface; + if (xsurface->surface == NULL || !xsurface->surface->mapped) { + return; + } + struct sway_seat *seat = input_manager_current_seat(); + struct sway_container *focus = seat_get_focused_container(seat); + if (focus && focus->view && focus->view->pid != xsurface->pid) { + return; + } + + seat_set_focus_surface(seat, xsurface->surface, false); +} + +static void unmanaged_handle_associate(struct wl_listener *listener, void *data) { + struct sway_xwayland_unmanaged *surface = + wl_container_of(listener, surface, associate); + struct wlr_xwayland_surface *xsurface = surface->wlr_xwayland_surface; + wl_signal_add(&xsurface->surface->events.map, &surface->map); + surface->map.notify = unmanaged_handle_map; + wl_signal_add(&xsurface->surface->events.unmap, &surface->unmap); + surface->unmap.notify = unmanaged_handle_unmap; +} + +static void unmanaged_handle_dissociate(struct wl_listener *listener, void *data) { + struct sway_xwayland_unmanaged *surface = + wl_container_of(listener, surface, dissociate); + wl_list_remove(&surface->map.link); + wl_list_remove(&surface->unmap.link); +} + +static void unmanaged_handle_destroy(struct wl_listener *listener, void *data) { + struct sway_xwayland_unmanaged *surface = + wl_container_of(listener, surface, destroy); + wl_list_remove(&surface->request_configure.link); + wl_list_remove(&surface->associate.link); + wl_list_remove(&surface->dissociate.link); + wl_list_remove(&surface->destroy.link); + wl_list_remove(&surface->override_redirect.link); + wl_list_remove(&surface->request_activate.link); + free(surface); +} + +static void handle_map(struct wl_listener *listener, void *data); +static void handle_associate(struct wl_listener *listener, void *data); + +struct sway_xwayland_view *create_xwayland_view(struct wlr_xwayland_surface *xsurface); + +static void unmanaged_handle_override_redirect(struct wl_listener *listener, void *data) { + struct sway_xwayland_unmanaged *surface = + wl_container_of(listener, surface, override_redirect); + struct wlr_xwayland_surface *xsurface = surface->wlr_xwayland_surface; + + bool associated = xsurface->surface != NULL; + bool mapped = associated && xsurface->surface->mapped; + if (mapped) { + unmanaged_handle_unmap(&surface->unmap, NULL); + } + if (associated) { + unmanaged_handle_dissociate(&surface->dissociate, NULL); + } + + unmanaged_handle_destroy(&surface->destroy, NULL); + xsurface->data = NULL; + + struct sway_xwayland_view *xwayland_view = create_xwayland_view(xsurface); + if (associated) { + handle_associate(&xwayland_view->associate, NULL); + } + if (mapped) { + handle_map(&xwayland_view->map, xsurface); + } +} + +static struct sway_xwayland_unmanaged *create_unmanaged( + struct wlr_xwayland_surface *xsurface) { + struct sway_xwayland_unmanaged *surface = + calloc(1, sizeof(struct sway_xwayland_unmanaged)); + if (surface == NULL) { + sway_log(SWAY_ERROR, "Allocation failed"); + return NULL; + } + + surface->wlr_xwayland_surface = xsurface; + + wl_signal_add(&xsurface->events.request_configure, + &surface->request_configure); + surface->request_configure.notify = unmanaged_handle_request_configure; + wl_signal_add(&xsurface->events.associate, &surface->associate); + surface->associate.notify = unmanaged_handle_associate; + wl_signal_add(&xsurface->events.dissociate, &surface->dissociate); + surface->dissociate.notify = unmanaged_handle_dissociate; + wl_signal_add(&xsurface->events.destroy, &surface->destroy); + surface->destroy.notify = unmanaged_handle_destroy; + wl_signal_add(&xsurface->events.set_override_redirect, &surface->override_redirect); + surface->override_redirect.notify = unmanaged_handle_override_redirect; + wl_signal_add(&xsurface->events.request_activate, &surface->request_activate); + surface->request_activate.notify = unmanaged_handle_request_activate; + + return surface; +} + +static struct sway_xwayland_view *xwayland_view_from_view( + struct sway_view *view) { + if (!sway_assert(view->type == SWAY_VIEW_XWAYLAND, + "Expected xwayland view")) { + return NULL; + } + return (struct sway_xwayland_view *)view; +} + +static const char *get_string_prop(struct sway_view *view, enum sway_view_prop prop) { + if (xwayland_view_from_view(view) == NULL) { + return NULL; + } + switch (prop) { + case VIEW_PROP_TITLE: + return view->wlr_xwayland_surface->title; + case VIEW_PROP_CLASS: + return view->wlr_xwayland_surface->class; + case VIEW_PROP_INSTANCE: + return view->wlr_xwayland_surface->instance; + case VIEW_PROP_WINDOW_ROLE: + return view->wlr_xwayland_surface->role; + default: + return NULL; + } +} + +static uint32_t get_int_prop(struct sway_view *view, enum sway_view_prop prop) { + if (xwayland_view_from_view(view) == NULL) { + return 0; + } + switch (prop) { + case VIEW_PROP_X11_WINDOW_ID: + return view->wlr_xwayland_surface->window_id; + case VIEW_PROP_X11_PARENT_ID: + if (view->wlr_xwayland_surface->parent) { + return view->wlr_xwayland_surface->parent->window_id; + } + return 0; + case VIEW_PROP_WINDOW_TYPE: + if (view->wlr_xwayland_surface->window_type_len == 0) { + return 0; + } + return view->wlr_xwayland_surface->window_type[0]; + default: + return 0; + } +} + +static uint32_t configure(struct sway_view *view, double lx, double ly, int width, + int height) { + struct sway_xwayland_view *xwayland_view = xwayland_view_from_view(view); + if (xwayland_view == NULL) { + return 0; + } + struct wlr_xwayland_surface *xsurface = view->wlr_xwayland_surface; + + wlr_xwayland_surface_configure(xsurface, lx, ly, width, height); + + // xwayland doesn't give us a serial for the configure + return 0; +} + +static void set_activated(struct sway_view *view, bool activated) { + if (xwayland_view_from_view(view) == NULL) { + return; + } + struct wlr_xwayland_surface *surface = view->wlr_xwayland_surface; + + if (activated && surface->minimized) { + wlr_xwayland_surface_set_minimized(surface, false); + } + + wlr_xwayland_surface_activate(surface, activated); +} + +static void set_tiled(struct sway_view *view, bool tiled) { + if (xwayland_view_from_view(view) == NULL) { + return; + } + struct wlr_xwayland_surface *surface = view->wlr_xwayland_surface; + wlr_xwayland_surface_set_maximized(surface, tiled, tiled); +} + +static void set_fullscreen(struct sway_view *view, bool fullscreen) { + if (xwayland_view_from_view(view) == NULL) { + return; + } + struct wlr_xwayland_surface *surface = view->wlr_xwayland_surface; + wlr_xwayland_surface_set_fullscreen(surface, fullscreen); +} + +static bool wants_floating(struct sway_view *view) { + if (xwayland_view_from_view(view) == NULL) { + return false; + } + struct wlr_xwayland_surface *surface = view->wlr_xwayland_surface; + struct sway_xwayland *xwayland = &server.xwayland; + + if (surface->modal) { + return true; + } + + for (size_t i = 0; i < surface->window_type_len; ++i) { + xcb_atom_t type = surface->window_type[i]; + if (type == xwayland->atoms[NET_WM_WINDOW_TYPE_DIALOG] || + type == xwayland->atoms[NET_WM_WINDOW_TYPE_UTILITY] || + type == xwayland->atoms[NET_WM_WINDOW_TYPE_TOOLBAR] || + type == xwayland->atoms[NET_WM_WINDOW_TYPE_SPLASH]) { + return true; + } + } + + xcb_size_hints_t *size_hints = surface->size_hints; + if (size_hints != NULL && + size_hints->min_width > 0 && size_hints->min_height > 0 && + (size_hints->max_width == size_hints->min_width || + size_hints->max_height == size_hints->min_height)) { + return true; + } + + return false; +} + +static void handle_set_decorations(struct wl_listener *listener, void *data) { + struct sway_xwayland_view *xwayland_view = + wl_container_of(listener, xwayland_view, set_decorations); + struct sway_view *view = &xwayland_view->view; + struct wlr_xwayland_surface *xsurface = view->wlr_xwayland_surface; + + bool csd = xsurface->decorations != WLR_XWAYLAND_SURFACE_DECORATIONS_ALL; + view_update_csd_from_client(view, csd); +} + +static bool is_transient_for(struct sway_view *child, + struct sway_view *ancestor) { + if (xwayland_view_from_view(child) == NULL) { + return false; + } + struct wlr_xwayland_surface *surface = child->wlr_xwayland_surface; + while (surface) { + if (surface->parent == ancestor->wlr_xwayland_surface) { + return true; + } + surface = surface->parent; + } + return false; +} + +static void _close(struct sway_view *view) { + if (xwayland_view_from_view(view) == NULL) { + return; + } + wlr_xwayland_surface_close(view->wlr_xwayland_surface); +} + +static void destroy(struct sway_view *view) { + struct sway_xwayland_view *xwayland_view = xwayland_view_from_view(view); + if (xwayland_view == NULL) { + return; + } + free(xwayland_view); +} + +static void get_constraints(struct sway_view *view, double *min_width, + double *max_width, double *min_height, double *max_height) { + struct wlr_xwayland_surface *surface = view->wlr_xwayland_surface; + xcb_size_hints_t *size_hints = surface->size_hints; + + if (size_hints == NULL) { + *min_width = DBL_MIN; + *max_width = DBL_MAX; + *min_height = DBL_MIN; + *max_height = DBL_MAX; + return; + } + + *min_width = size_hints->min_width > 0 ? size_hints->min_width : DBL_MIN; + *max_width = size_hints->max_width > 0 ? size_hints->max_width : DBL_MAX; + *min_height = size_hints->min_height > 0 ? size_hints->min_height : DBL_MIN; + *max_height = size_hints->max_height > 0 ? size_hints->max_height : DBL_MAX; +} + +static const struct sway_view_impl view_impl = { + .get_constraints = get_constraints, + .get_string_prop = get_string_prop, + .get_int_prop = get_int_prop, + .configure = configure, + .set_activated = set_activated, + .set_tiled = set_tiled, + .set_fullscreen = set_fullscreen, + .wants_floating = wants_floating, + .is_transient_for = is_transient_for, + .close = _close, + .destroy = destroy, +}; + +static void handle_commit(struct wl_listener *listener, void *data) { + struct sway_xwayland_view *xwayland_view = + wl_container_of(listener, xwayland_view, commit); + struct sway_view *view = &xwayland_view->view; + struct wlr_xwayland_surface *xsurface = view->wlr_xwayland_surface; + struct wlr_surface_state *state = &xsurface->surface->current; + + struct wlr_box new_geo = {0}; + new_geo.width = state->width; + new_geo.height = state->height; + + bool new_size = new_geo.width != view->geometry.width || + new_geo.height != view->geometry.height; + + if (new_size) { + // The client changed its surface size in this commit. For floating + // containers, we resize the container to match. For tiling containers, + // we only recenter the surface. + memcpy(&view->geometry, &new_geo, sizeof(struct wlr_box)); + if (container_is_floating(view->container)) { + view_update_size(view); + transaction_commit_dirty_client(); + } + + view_center_and_clip_surface(view); + } + + if (view->container->node.instruction) { + bool successful = transaction_notify_view_ready_by_geometry(view, + xsurface->x, xsurface->y, state->width, state->height); + + // If we saved the view and this commit isn't what we're looking for + // that means the user will never actually see the buffers submitted to + // us here. Just send frame done events to these surfaces so they can + // commit another time for us. + if (view->saved_surface_tree && !successful) { + view_send_frame_done(view); + } + } +} + +static void handle_destroy(struct wl_listener *listener, void *data) { + struct sway_xwayland_view *xwayland_view = + wl_container_of(listener, xwayland_view, destroy); + struct sway_view *view = &xwayland_view->view; + + if (view->surface) { + view_unmap(view); + wl_list_remove(&xwayland_view->commit.link); + } + + xwayland_view->view.wlr_xwayland_surface = NULL; + + wl_list_remove(&xwayland_view->destroy.link); + wl_list_remove(&xwayland_view->request_configure.link); + wl_list_remove(&xwayland_view->request_fullscreen.link); + wl_list_remove(&xwayland_view->request_minimize.link); + wl_list_remove(&xwayland_view->request_move.link); + wl_list_remove(&xwayland_view->request_resize.link); + wl_list_remove(&xwayland_view->request_activate.link); + wl_list_remove(&xwayland_view->set_title.link); + wl_list_remove(&xwayland_view->set_class.link); + wl_list_remove(&xwayland_view->set_role.link); + wl_list_remove(&xwayland_view->set_startup_id.link); + wl_list_remove(&xwayland_view->set_window_type.link); + wl_list_remove(&xwayland_view->set_hints.link); + wl_list_remove(&xwayland_view->set_decorations.link); + wl_list_remove(&xwayland_view->associate.link); + wl_list_remove(&xwayland_view->dissociate.link); + wl_list_remove(&xwayland_view->override_redirect.link); + view_begin_destroy(&xwayland_view->view); +} + +static void handle_unmap(struct wl_listener *listener, void *data) { + struct sway_xwayland_view *xwayland_view = + wl_container_of(listener, xwayland_view, unmap); + struct sway_view *view = &xwayland_view->view; + + if (!sway_assert(view->surface, "Cannot unmap unmapped view")) { + return; + } + + wl_list_remove(&xwayland_view->commit.link); + wl_list_remove(&xwayland_view->surface_tree_destroy.link); + + wlr_scene_node_destroy(&xwayland_view->image_capture_scene_surface->buffer->node); + xwayland_view->image_capture_scene_surface = NULL; + + if (xwayland_view->surface_tree) { + wlr_scene_node_destroy(&xwayland_view->surface_tree->node); + xwayland_view->surface_tree = NULL; + } + + view_unmap(view); +} + +static void handle_surface_tree_destroy(struct wl_listener *listener, void *data) { + struct sway_xwayland_view *xwayland_view = wl_container_of(listener, xwayland_view, + surface_tree_destroy); + xwayland_view->surface_tree = NULL; +} + +static void handle_map(struct wl_listener *listener, void *data) { + struct sway_xwayland_view *xwayland_view = + wl_container_of(listener, xwayland_view, map); + struct sway_view *view = &xwayland_view->view; + struct wlr_xwayland_surface *xsurface = view->wlr_xwayland_surface; + + view->natural_width = xsurface->width; + view->natural_height = xsurface->height; + + // Wire up the commit listener here, because xwayland map/unmap can change + // the underlying wlr_surface + wl_signal_add(&xsurface->surface->events.commit, &xwayland_view->commit); + xwayland_view->commit.notify = handle_commit; + + // Put it back into the tree + view_map(view, xsurface->surface, xsurface->fullscreen, NULL, false); + + xwayland_view->surface_tree = wlr_scene_subsurface_tree_create( + xwayland_view->view.content_tree, xsurface->surface); + + if (xwayland_view->surface_tree) { + xwayland_view->surface_tree_destroy.notify = handle_surface_tree_destroy; + wl_signal_add(&xwayland_view->surface_tree->node.events.destroy, + &xwayland_view->surface_tree_destroy); + } + + xwayland_view->image_capture_scene_surface = + wlr_scene_surface_create(&xwayland_view->view.image_capture_scene->tree, xsurface->surface); + + transaction_commit_dirty(); +} + +static void handle_dissociate(struct wl_listener *listener, void *data); + +static void handle_override_redirect(struct wl_listener *listener, void *data) { + struct sway_xwayland_view *xwayland_view = + wl_container_of(listener, xwayland_view, override_redirect); + struct sway_view *view = &xwayland_view->view; + struct wlr_xwayland_surface *xsurface = view->wlr_xwayland_surface; + + bool associated = xsurface->surface != NULL; + bool mapped = associated && xsurface->surface->mapped; + if (mapped) { + handle_unmap(&xwayland_view->unmap, NULL); + } + if (associated) { + handle_dissociate(&xwayland_view->dissociate, NULL); + } + + handle_destroy(&xwayland_view->destroy, view); + xsurface->data = NULL; + + struct sway_xwayland_unmanaged *unmanaged = create_unmanaged(xsurface); + if (associated) { + unmanaged_handle_associate(&unmanaged->associate, NULL); + } + if (mapped) { + unmanaged_handle_map(&unmanaged->map, xsurface); + } +} + +static void handle_request_configure(struct wl_listener *listener, void *data) { + struct sway_xwayland_view *xwayland_view = + wl_container_of(listener, xwayland_view, request_configure); + struct wlr_xwayland_surface_configure_event *ev = data; + struct sway_view *view = &xwayland_view->view; + struct wlr_xwayland_surface *xsurface = view->wlr_xwayland_surface; + if (xsurface->surface == NULL || !xsurface->surface->mapped) { + wlr_xwayland_surface_configure(xsurface, ev->x, ev->y, + ev->width, ev->height); + return; + } + if (container_is_floating(view->container)) { + // Respect minimum and maximum sizes + view->natural_width = ev->width; + view->natural_height = ev->height; + container_floating_resize_and_center(view->container); + + configure(view, view->container->pending.content_x, + view->container->pending.content_y, + view->container->pending.content_width, + view->container->pending.content_height); + node_set_dirty(&view->container->node); + } else { + configure(view, view->container->current.content_x, + view->container->current.content_y, + view->container->current.content_width, + view->container->current.content_height); + } +} + +static void handle_request_fullscreen(struct wl_listener *listener, void *data) { + struct sway_xwayland_view *xwayland_view = + wl_container_of(listener, xwayland_view, request_fullscreen); + struct sway_view *view = &xwayland_view->view; + struct wlr_xwayland_surface *xsurface = view->wlr_xwayland_surface; + if (xsurface->surface == NULL || !xsurface->surface->mapped) { + return; + } + container_set_fullscreen(view->container, xsurface->fullscreen); + + arrange_root(); + transaction_commit_dirty(); +} + +static void handle_request_minimize(struct wl_listener *listener, void *data) { + struct sway_xwayland_view *xwayland_view = + wl_container_of(listener, xwayland_view, request_minimize); + struct sway_view *view = &xwayland_view->view; + struct wlr_xwayland_surface *xsurface = view->wlr_xwayland_surface; + if (xsurface->surface == NULL || !xsurface->surface->mapped) { + return; + } + + struct wlr_xwayland_minimize_event *e = data; + if (config->scratchpad_minimize) { + struct sway_container *container = view->container; + if (!container->pending.workspace) { + while (container->pending.parent) { + container = container->pending.parent; + } + } + if(e->minimize) { + if (!container->scratchpad) { + root_scratchpad_add_container(container, NULL); + } else if (container->pending.workspace) { + root_scratchpad_hide(container); + } + } else { + if(container->scratchpad) { + root_scratchpad_show(container); + } + } + transaction_commit_dirty(); + return; + } + + struct sway_seat *seat = input_manager_current_seat(); + bool focused = seat_get_focus(seat) == &view->container->node; + wlr_xwayland_surface_set_minimized(xsurface, !focused && e->minimize); +} + +static void handle_request_move(struct wl_listener *listener, void *data) { + struct sway_xwayland_view *xwayland_view = + wl_container_of(listener, xwayland_view, request_move); + struct sway_view *view = &xwayland_view->view; + struct wlr_xwayland_surface *xsurface = view->wlr_xwayland_surface; + if (xsurface->surface == NULL || !xsurface->surface->mapped) { + return; + } + if (!container_is_floating(view->container) || + view->container->pending.fullscreen_mode) { + return; + } + struct sway_seat *seat = input_manager_current_seat(); + seatop_begin_move_floating(seat, view->container); +} + +static void handle_request_resize(struct wl_listener *listener, void *data) { + struct sway_xwayland_view *xwayland_view = + wl_container_of(listener, xwayland_view, request_resize); + struct sway_view *view = &xwayland_view->view; + struct wlr_xwayland_surface *xsurface = view->wlr_xwayland_surface; + if (xsurface->surface == NULL || !xsurface->surface->mapped) { + return; + } + if (!container_is_floating(view->container)) { + return; + } + struct wlr_xwayland_resize_event *e = data; + struct sway_seat *seat = input_manager_current_seat(); + seatop_begin_resize_floating(seat, view->container, e->edges); +} + +static void handle_request_activate(struct wl_listener *listener, void *data) { + struct sway_xwayland_view *xwayland_view = + wl_container_of(listener, xwayland_view, request_activate); + struct sway_view *view = &xwayland_view->view; + struct wlr_xwayland_surface *xsurface = view->wlr_xwayland_surface; + if (xsurface->surface == NULL || !xsurface->surface->mapped) { + return; + } + view_request_activate(view, NULL); + + transaction_commit_dirty(); +} + +static void handle_set_title(struct wl_listener *listener, void *data) { + struct sway_xwayland_view *xwayland_view = + wl_container_of(listener, xwayland_view, set_title); + struct sway_view *view = &xwayland_view->view; + struct wlr_xwayland_surface *xsurface = view->wlr_xwayland_surface; + if (xsurface->surface == NULL || !xsurface->surface->mapped) { + return; + } + view_update_title(view, false); + view_execute_criteria(view); + transaction_commit_dirty(); +} + +static void handle_set_class(struct wl_listener *listener, void *data) { + struct sway_xwayland_view *xwayland_view = + wl_container_of(listener, xwayland_view, set_class); + struct sway_view *view = &xwayland_view->view; + struct wlr_xwayland_surface *xsurface = view->wlr_xwayland_surface; + if (xsurface->surface == NULL || !xsurface->surface->mapped) { + return; + } + view_execute_criteria(view); + transaction_commit_dirty(); +} + +static void handle_set_role(struct wl_listener *listener, void *data) { + struct sway_xwayland_view *xwayland_view = + wl_container_of(listener, xwayland_view, set_role); + struct sway_view *view = &xwayland_view->view; + struct wlr_xwayland_surface *xsurface = view->wlr_xwayland_surface; + if (xsurface->surface == NULL || !xsurface->surface->mapped) { + return; + } + view_execute_criteria(view); + transaction_commit_dirty(); +} + +static void handle_set_startup_id(struct wl_listener *listener, void *data) { + struct sway_xwayland_view *xwayland_view = + wl_container_of(listener, xwayland_view, set_startup_id); + struct sway_view *view = &xwayland_view->view; + struct wlr_xwayland_surface *xsurface = view->wlr_xwayland_surface; + if (xsurface->startup_id == NULL) { + return; + } + + struct wlr_xdg_activation_token_v1 *token = + wlr_xdg_activation_v1_find_token( + server.xdg_activation_v1, xsurface->startup_id); + if (token == NULL) { + // Tried to activate with an unknown or expired token + return; + } + + struct launcher_ctx *ctx = token->data; + if (token->data == NULL) { + // TODO: support external launchers in X + return; + } + view_assign_ctx(view, ctx); +} + +static void handle_set_window_type(struct wl_listener *listener, void *data) { + struct sway_xwayland_view *xwayland_view = + wl_container_of(listener, xwayland_view, set_window_type); + struct sway_view *view = &xwayland_view->view; + struct wlr_xwayland_surface *xsurface = view->wlr_xwayland_surface; + if (xsurface->surface == NULL || !xsurface->surface->mapped) { + return; + } + view_execute_criteria(view); + transaction_commit_dirty(); +} + +static void handle_set_hints(struct wl_listener *listener, void *data) { + struct sway_xwayland_view *xwayland_view = + wl_container_of(listener, xwayland_view, set_hints); + struct sway_view *view = &xwayland_view->view; + struct wlr_xwayland_surface *xsurface = view->wlr_xwayland_surface; + if (xsurface->surface == NULL || !xsurface->surface->mapped) { + return; + } + const bool hints_urgency = xcb_icccm_wm_hints_get_urgency(xsurface->hints); + if (!hints_urgency && view->urgent_timer) { + // The view is in the timeout period. We'll ignore the request to + // unset urgency so that the view remains urgent until the timer clears + // it. + return; + } + if (view->allow_request_urgent) { + view_set_urgent(view, hints_urgency); + } +} + +static void handle_associate(struct wl_listener *listener, void *data) { + struct sway_xwayland_view *xwayland_view = + wl_container_of(listener, xwayland_view, associate); + struct wlr_xwayland_surface *xsurface = + xwayland_view->view.wlr_xwayland_surface; + wl_signal_add(&xsurface->surface->events.unmap, &xwayland_view->unmap); + xwayland_view->unmap.notify = handle_unmap; + wl_signal_add(&xsurface->surface->events.map, &xwayland_view->map); + xwayland_view->map.notify = handle_map; +} + +static void handle_dissociate(struct wl_listener *listener, void *data) { + struct sway_xwayland_view *xwayland_view = + wl_container_of(listener, xwayland_view, dissociate); + wl_list_remove(&xwayland_view->map.link); + wl_list_remove(&xwayland_view->unmap.link); +} + +struct sway_view *view_from_wlr_xwayland_surface( + struct wlr_xwayland_surface *xsurface) { + return xsurface->data; +} + +struct sway_xwayland_view *create_xwayland_view(struct wlr_xwayland_surface *xsurface) { + sway_log(SWAY_DEBUG, "New xwayland surface title='%s' class='%s'", + xsurface->title, xsurface->class); + + struct sway_xwayland_view *xwayland_view = + calloc(1, sizeof(struct sway_xwayland_view)); + if (!sway_assert(xwayland_view, "Failed to allocate view")) { + return NULL; + } + + if (!view_init(&xwayland_view->view, SWAY_VIEW_XWAYLAND, &view_impl)) { + free(xwayland_view); + return NULL; + } + xwayland_view->view.wlr_xwayland_surface = xsurface; + + wl_signal_add(&xsurface->events.destroy, &xwayland_view->destroy); + xwayland_view->destroy.notify = handle_destroy; + + wl_signal_add(&xsurface->events.request_configure, + &xwayland_view->request_configure); + xwayland_view->request_configure.notify = handle_request_configure; + + wl_signal_add(&xsurface->events.request_fullscreen, + &xwayland_view->request_fullscreen); + xwayland_view->request_fullscreen.notify = handle_request_fullscreen; + + wl_signal_add(&xsurface->events.request_minimize, + &xwayland_view->request_minimize); + xwayland_view->request_minimize.notify = handle_request_minimize; + + wl_signal_add(&xsurface->events.request_activate, + &xwayland_view->request_activate); + xwayland_view->request_activate.notify = handle_request_activate; + + wl_signal_add(&xsurface->events.request_move, + &xwayland_view->request_move); + xwayland_view->request_move.notify = handle_request_move; + + wl_signal_add(&xsurface->events.request_resize, + &xwayland_view->request_resize); + xwayland_view->request_resize.notify = handle_request_resize; + + wl_signal_add(&xsurface->events.set_title, &xwayland_view->set_title); + xwayland_view->set_title.notify = handle_set_title; + + wl_signal_add(&xsurface->events.set_class, &xwayland_view->set_class); + xwayland_view->set_class.notify = handle_set_class; + + wl_signal_add(&xsurface->events.set_role, &xwayland_view->set_role); + xwayland_view->set_role.notify = handle_set_role; + + wl_signal_add(&xsurface->events.set_startup_id, + &xwayland_view->set_startup_id); + xwayland_view->set_startup_id.notify = handle_set_startup_id; + + wl_signal_add(&xsurface->events.set_window_type, + &xwayland_view->set_window_type); + xwayland_view->set_window_type.notify = handle_set_window_type; + + wl_signal_add(&xsurface->events.set_hints, &xwayland_view->set_hints); + xwayland_view->set_hints.notify = handle_set_hints; + + wl_signal_add(&xsurface->events.set_decorations, + &xwayland_view->set_decorations); + xwayland_view->set_decorations.notify = handle_set_decorations; + + wl_signal_add(&xsurface->events.associate, &xwayland_view->associate); + xwayland_view->associate.notify = handle_associate; + + wl_signal_add(&xsurface->events.dissociate, &xwayland_view->dissociate); + xwayland_view->dissociate.notify = handle_dissociate; + + wl_signal_add(&xsurface->events.set_override_redirect, + &xwayland_view->override_redirect); + xwayland_view->override_redirect.notify = handle_override_redirect; + + xsurface->data = xwayland_view; + + return xwayland_view; +} + +void handle_xwayland_surface(struct wl_listener *listener, void *data) { + struct wlr_xwayland_surface *xsurface = data; + + if (xsurface->override_redirect) { + sway_log(SWAY_DEBUG, "New xwayland unmanaged surface"); + create_unmanaged(xsurface); + return; + } + + create_xwayland_view(xsurface); +} + +void handle_xwayland_ready(struct wl_listener *listener, void *data) { + struct sway_server *server = + wl_container_of(listener, server, xwayland_ready); + struct sway_xwayland *xwayland = &server->xwayland; + + xcb_connection_t *xcb_conn = xcb_connect(NULL, NULL); + int err = xcb_connection_has_error(xcb_conn); + if (err) { + sway_log(SWAY_ERROR, "XCB connect failed: %d", err); + return; + } + + xcb_intern_atom_cookie_t cookies[ATOM_LAST]; + for (size_t i = 0; i < ATOM_LAST; i++) { + cookies[i] = + xcb_intern_atom(xcb_conn, 0, strlen(atom_map[i]), atom_map[i]); + } + for (size_t i = 0; i < ATOM_LAST; i++) { + xcb_generic_error_t *error = NULL; + xcb_intern_atom_reply_t *reply = + xcb_intern_atom_reply(xcb_conn, cookies[i], &error); + if (reply != NULL && error == NULL) { + xwayland->atoms[i] = reply->atom; + } + free(reply); + + if (error != NULL) { + sway_log(SWAY_ERROR, "could not resolve atom %s, X11 error code %d", + atom_map[i], error->error_code); + free(error); + break; + } + } + + xcb_disconnect(xcb_conn); +} + diff --git a/sway/input/cursor.c b/sway/input/cursor.c new file mode 100644 index 0000000..fc0f11f --- /dev/null +++ b/sway/input/cursor.c @@ -0,0 +1,1417 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "config.h" +#include "log.h" +#include "util.h" +#include "sway/commands.h" +#include "sway/input/cursor.h" +#include "sway/input/keyboard.h" +#include "sway/input/tablet.h" +#include "sway/layers.h" +#include "sway/output.h" +#include "sway/scene_descriptor.h" +#include "sway/server.h" +#include "sway/tree/container.h" +#include "sway/tree/root.h" +#include "sway/tree/view.h" +#include "sway/tree/workspace.h" +#include "wlr-layer-shell-unstable-v1-protocol.h" + +/** + * Returns the node at the cursor's position. If there is a surface at that + * location, it is stored in **surface (it may not be a view). + */ +struct sway_node *node_at_coords( + struct sway_seat *seat, double lx, double ly, + struct wlr_surface **surface, double *sx, double *sy) { + struct wlr_scene_node *scene_node = NULL; + + struct wlr_scene_node *node; + wl_list_for_each_reverse(node, &root->layer_tree->children, link) { + struct wlr_scene_tree *layer = wlr_scene_tree_from_node(node); + + bool non_interactive = scene_descriptor_try_get(&layer->node, + SWAY_SCENE_DESC_NON_INTERACTIVE); + if (non_interactive) { + continue; + } + + scene_node = wlr_scene_node_at(&layer->node, lx, ly, sx, sy); + if (scene_node) { + break; + } + } + + if (scene_node) { + // determine what wlr_surface we clicked on + if (scene_node->type == WLR_SCENE_NODE_BUFFER) { + struct wlr_scene_buffer *scene_buffer = + wlr_scene_buffer_from_node(scene_node); + struct wlr_scene_surface *scene_surface = + wlr_scene_surface_try_from_buffer(scene_buffer); + + if (scene_surface) { + *surface = scene_surface->surface; + } + } + + // determine what container we clicked on + struct wlr_scene_node *current = scene_node; + while (true) { + struct sway_container *con = scene_descriptor_try_get(current, + SWAY_SCENE_DESC_CONTAINER); + + if (!con) { + struct sway_view *view = scene_descriptor_try_get(current, + SWAY_SCENE_DESC_VIEW); + if (view) { + con = view->container; + } + } + + if (!con) { + struct sway_popup_desc *popup = + scene_descriptor_try_get(current, SWAY_SCENE_DESC_POPUP); + if (popup && popup->view) { + con = popup->view->container; + } + } + + if (con && (!con->view || con->view->surface)) { + return &con->node; + } + + if (scene_descriptor_try_get(current, SWAY_SCENE_DESC_LAYER_SHELL)) { + // We don't want to feed through the current workspace on + // layer shells + return NULL; + } + +#if WLR_HAS_XWAYLAND + if (scene_descriptor_try_get(current, SWAY_SCENE_DESC_XWAYLAND_UNMANAGED)) { + return NULL; + } +#endif + + if (!current->parent) { + break; + } + + current = ¤t->parent->node; + } + } + + // if we aren't on a container, determine what workspace we are on + struct wlr_output *wlr_output = wlr_output_layout_output_at( + root->output_layout, lx, ly); + if (wlr_output == NULL) { + return NULL; + } + + struct sway_output *output = wlr_output->data; + if (!output || !output->enabled) { + // output is being destroyed or is being enabled + return NULL; + } + + struct sway_workspace *ws = output_get_active_workspace(output); + if (!ws) { + return NULL; + } + + return &ws->node; +} + +void cursor_rebase(struct sway_cursor *cursor) { + uint32_t time_msec = get_current_time_in_msec(); + seatop_rebase(cursor->seat, time_msec); +} + +void cursor_rebase_all(void) { + if (!root->outputs->length) { + return; + } + + struct sway_seat *seat; + wl_list_for_each(seat, &server.input->seats, link) { + cursor_rebase(seat->cursor); + } +} + +void cursor_update_image(struct sway_cursor *cursor, + struct sway_node *node) { + if (node && node->type == N_CONTAINER) { + // Try a node's resize edge + enum wlr_edges edge = find_resize_edge(node->sway_container, NULL, cursor); + if (edge == WLR_EDGE_NONE) { + cursor_set_image(cursor, "default", NULL); + } else if (container_is_floating(node->sway_container)) { + cursor_set_image(cursor, wlr_xcursor_get_resize_name(edge), NULL); + } else { + if (edge & (WLR_EDGE_LEFT | WLR_EDGE_RIGHT)) { + cursor_set_image(cursor, "col-resize", NULL); + } else { + cursor_set_image(cursor, "row-resize", NULL); + } + } + } else { + cursor_set_image(cursor, "default", NULL); + } +} + +static void cursor_hide(struct sway_cursor *cursor) { + wlr_cursor_unset_image(cursor->cursor); + cursor->hidden = true; + wlr_seat_pointer_notify_clear_focus(cursor->seat->wlr_seat); +} + +static int hide_notify(void *data) { + struct sway_cursor *cursor = data; + cursor_hide(cursor); + return 1; +} + +int cursor_get_timeout(struct sway_cursor *cursor) { + if (cursor->pressed_button_count > 0) { + // Do not hide cursor unless all buttons are released + return 0; + } + + struct seat_config *sc = seat_get_config(cursor->seat); + if (!sc) { + sc = seat_get_config_by_name("*"); + } + int timeout = sc ? sc->hide_cursor_timeout : 0; + if (timeout < 0) { + timeout = 0; + } + return timeout; +} + +void cursor_notify_key_press(struct sway_cursor *cursor) { + if (cursor->hidden) { + return; + } + + if (cursor->hide_when_typing == HIDE_WHEN_TYPING_DEFAULT) { + // No cached value, need to lookup in the seat_config + const struct seat_config *seat_config = seat_get_config(cursor->seat); + if (!seat_config) { + seat_config = seat_get_config_by_name("*"); + if (!seat_config) { + return; + } + } + cursor->hide_when_typing = seat_config->hide_cursor_when_typing; + // The default is currently disabled + if (cursor->hide_when_typing == HIDE_WHEN_TYPING_DEFAULT) { + cursor->hide_when_typing = HIDE_WHEN_TYPING_DISABLE; + } + } + + if (cursor->hide_when_typing == HIDE_WHEN_TYPING_ENABLE) { + cursor_hide(cursor); + } +} + +static enum sway_input_idle_source idle_source_from_device( + struct wlr_input_device *device) { + switch (device->type) { + case WLR_INPUT_DEVICE_KEYBOARD: + return IDLE_SOURCE_KEYBOARD; + case WLR_INPUT_DEVICE_POINTER: + return IDLE_SOURCE_POINTER; + case WLR_INPUT_DEVICE_TOUCH: + return IDLE_SOURCE_TOUCH; + case WLR_INPUT_DEVICE_TABLET: + return IDLE_SOURCE_TABLET_TOOL; + case WLR_INPUT_DEVICE_TABLET_PAD: + return IDLE_SOURCE_TABLET_PAD; + case WLR_INPUT_DEVICE_SWITCH: + return IDLE_SOURCE_SWITCH; + } + + abort(); +} + +void cursor_handle_activity_from_idle_source(struct sway_cursor *cursor, + enum sway_input_idle_source idle_source) { + wl_event_source_timer_update( + cursor->hide_source, cursor_get_timeout(cursor)); + + seat_idle_notify_activity(cursor->seat, idle_source); + if (idle_source != IDLE_SOURCE_TOUCH) { + cursor_unhide(cursor); + } +} + +void cursor_handle_activity_from_device(struct sway_cursor *cursor, + struct wlr_input_device *device) { + enum sway_input_idle_source idle_source = idle_source_from_device(device); + cursor_handle_activity_from_idle_source(cursor, idle_source); +} + +void cursor_unhide(struct sway_cursor *cursor) { + if (!cursor->hidden) { + return; + } + + cursor->hidden = false; + if (cursor->image_surface) { + cursor_set_image_surface(cursor, + cursor->image_surface, + cursor->hotspot_x, + cursor->hotspot_y, + cursor->image_client); + } else { + const char *image = cursor->image; + cursor->image = NULL; + cursor_set_image(cursor, image, cursor->image_client); + } + cursor_rebase(cursor); + wl_event_source_timer_update(cursor->hide_source, cursor_get_timeout(cursor)); +} + +void pointer_motion(struct sway_cursor *cursor, uint32_t time_msec, + struct wlr_input_device *device, double dx, double dy, + double dx_unaccel, double dy_unaccel) { + wlr_relative_pointer_manager_v1_send_relative_motion( + server.relative_pointer_manager, + cursor->seat->wlr_seat, (uint64_t)time_msec * 1000, + dx, dy, dx_unaccel, dy_unaccel); + + // Only apply pointer constraints to real pointer input. + if (cursor->active_constraint && device->type == WLR_INPUT_DEVICE_POINTER) { + struct wlr_surface *surface = NULL; + double sx, sy; + node_at_coords(cursor->seat, + cursor->cursor->x, cursor->cursor->y, &surface, &sx, &sy); + + if (cursor->active_constraint->surface != surface) { + return; + } + + double sx_confined, sy_confined; + if (!wlr_region_confine(&cursor->confine, sx, sy, sx + dx, sy + dy, + &sx_confined, &sy_confined)) { + return; + } + + dx = sx_confined - sx; + dy = sy_confined - sy; + } + + wlr_cursor_move(cursor->cursor, device, dx, dy); + + seatop_pointer_motion(cursor->seat, time_msec); +} + +static void handle_pointer_motion_relative( + struct wl_listener *listener, void *data) { + struct sway_cursor *cursor = wl_container_of(listener, cursor, motion); + struct wlr_pointer_motion_event *e = data; + cursor_handle_activity_from_device(cursor, &e->pointer->base); + + pointer_motion(cursor, e->time_msec, &e->pointer->base, e->delta_x, + e->delta_y, e->unaccel_dx, e->unaccel_dy); +} + +static void handle_pointer_motion_absolute( + struct wl_listener *listener, void *data) { + struct sway_cursor *cursor = + wl_container_of(listener, cursor, motion_absolute); + struct wlr_pointer_motion_absolute_event *event = data; + cursor_handle_activity_from_device(cursor, &event->pointer->base); + + double lx, ly; + wlr_cursor_absolute_to_layout_coords(cursor->cursor, &event->pointer->base, + event->x, event->y, &lx, &ly); + + double dx = lx - cursor->cursor->x; + double dy = ly - cursor->cursor->y; + + pointer_motion(cursor, event->time_msec, &event->pointer->base, dx, dy, + dx, dy); +} + +void dispatch_cursor_button(struct sway_cursor *cursor, + struct wlr_input_device *device, uint32_t time_msec, uint32_t button, + enum wl_pointer_button_state state) { + if (time_msec == 0) { + time_msec = get_current_time_in_msec(); + } + + seatop_button(cursor->seat, time_msec, device, button, state); +} + +static void handle_pointer_button(struct wl_listener *listener, void *data) { + struct sway_cursor *cursor = wl_container_of(listener, cursor, button); + struct wlr_pointer_button_event *event = data; + + if (event->state == WL_POINTER_BUTTON_STATE_PRESSED) { + cursor->pressed_button_count++; + } else { + if (cursor->pressed_button_count > 0) { + cursor->pressed_button_count--; + } else { + sway_log(SWAY_ERROR, "Pressed button count was wrong"); + } + } + + cursor_handle_activity_from_device(cursor, &event->pointer->base); + dispatch_cursor_button(cursor, &event->pointer->base, + event->time_msec, event->button, event->state); +} + +void dispatch_cursor_axis(struct sway_cursor *cursor, + struct wlr_pointer_axis_event *event) { + seatop_pointer_axis(cursor->seat, event); +} + +static void handle_pointer_axis(struct wl_listener *listener, void *data) { + struct sway_cursor *cursor = wl_container_of(listener, cursor, axis); + struct wlr_pointer_axis_event *event = data; + cursor_handle_activity_from_device(cursor, &event->pointer->base); + dispatch_cursor_axis(cursor, event); +} + +static void handle_pointer_frame(struct wl_listener *listener, void *data) { + struct sway_cursor *cursor = wl_container_of(listener, cursor, frame); + wlr_seat_pointer_notify_frame(cursor->seat->wlr_seat); +} + +static void handle_touch_down(struct wl_listener *listener, void *data) { + struct sway_cursor *cursor = wl_container_of(listener, cursor, touch_down); + struct wlr_touch_down_event *event = data; + cursor_handle_activity_from_device(cursor, &event->touch->base); + cursor_hide(cursor); + + struct sway_seat *seat = cursor->seat; + + double lx, ly; + wlr_cursor_absolute_to_layout_coords(cursor->cursor, &event->touch->base, + event->x, event->y, &lx, &ly); + + seat->touch_id = event->touch_id; + seat->touch_x = lx; + seat->touch_y = ly; + + seatop_touch_down(seat, event, lx, ly); +} + +static void handle_touch_up(struct wl_listener *listener, void *data) { + struct sway_cursor *cursor = wl_container_of(listener, cursor, touch_up); + struct wlr_touch_up_event *event = data; + cursor_handle_activity_from_device(cursor, &event->touch->base); + + struct sway_seat *seat = cursor->seat; + + if (cursor->simulating_pointer_from_touch) { + if (cursor->pointer_touch_id == cursor->seat->touch_id) { + cursor->pointer_touch_up = true; + dispatch_cursor_button(cursor, &event->touch->base, + event->time_msec, BTN_LEFT, WL_POINTER_BUTTON_STATE_RELEASED); + } + } else { + seatop_touch_up(seat, event); + } +} + +static void handle_touch_cancel(struct wl_listener *listener, void *data) { + struct sway_cursor *cursor = wl_container_of(listener, cursor, touch_cancel); + struct wlr_touch_cancel_event *event = data; + cursor_handle_activity_from_device(cursor, &event->touch->base); + + struct sway_seat *seat = cursor->seat; + + if (cursor->simulating_pointer_from_touch) { + if (cursor->pointer_touch_id == cursor->seat->touch_id) { + cursor->pointer_touch_up = true; + dispatch_cursor_button(cursor, &event->touch->base, + event->time_msec, BTN_LEFT, WL_POINTER_BUTTON_STATE_RELEASED); + } + } else { + seatop_touch_cancel(seat, event); + } +} + +static void handle_touch_motion(struct wl_listener *listener, void *data) { + struct sway_cursor *cursor = + wl_container_of(listener, cursor, touch_motion); + struct wlr_touch_motion_event *event = data; + cursor_handle_activity_from_device(cursor, &event->touch->base); + + struct sway_seat *seat = cursor->seat; + + double lx, ly; + wlr_cursor_absolute_to_layout_coords(cursor->cursor, &event->touch->base, + event->x, event->y, &lx, &ly); + + if (seat->touch_id == event->touch_id) { + seat->touch_x = lx; + seat->touch_y = ly; + + drag_icons_update_position(seat); + } + + if (cursor->simulating_pointer_from_touch) { + if (seat->touch_id == cursor->pointer_touch_id) { + double dx, dy; + dx = lx - cursor->cursor->x; + dy = ly - cursor->cursor->y; + pointer_motion(cursor, event->time_msec, &event->touch->base, + dx, dy, dx, dy); + } + } else { + seatop_touch_motion(seat, event, lx, ly); + } +} + +static void handle_touch_frame(struct wl_listener *listener, void *data) { + struct sway_cursor *cursor = + wl_container_of(listener, cursor, touch_frame); + + struct wlr_seat *wlr_seat = cursor->seat->wlr_seat; + + if (cursor->simulating_pointer_from_touch) { + wlr_seat_pointer_notify_frame(wlr_seat); + + if (cursor->pointer_touch_up) { + cursor->pointer_touch_up = false; + cursor->simulating_pointer_from_touch = false; + } + } else { + wlr_seat_touch_notify_frame(wlr_seat); + } +} + +static double apply_mapping_from_coord(double low, double high, double value) { + if (isnan(value)) { + return value; + } + + return (value - low) / (high - low); +} + +static void apply_mapping_from_region(struct wlr_input_device *device, + struct input_config_mapped_from_region *region, double *x, double *y) { + double x1 = region->x1, x2 = region->x2; + double y1 = region->y1, y2 = region->y2; + + if (region->mm && device->type == WLR_INPUT_DEVICE_TABLET) { + struct wlr_tablet *tablet = wlr_tablet_from_input_device(device); + if (tablet->width_mm == 0 || tablet->height_mm == 0) { + return; + } + x1 /= tablet->width_mm; + x2 /= tablet->width_mm; + y1 /= tablet->height_mm; + y2 /= tablet->height_mm; + } + + *x = apply_mapping_from_coord(x1, x2, *x); + *y = apply_mapping_from_coord(y1, y2, *y); +} + +static void handle_tablet_tool_position(struct sway_cursor *cursor, + struct sway_tablet_tool *tool, + bool change_x, bool change_y, + double x, double y, double dx, double dy, + int32_t time_msec) { + + if (!change_x && !change_y) { + return; + } + + struct sway_tablet *tablet = tool->tablet; + struct sway_input_device *input_device = tablet->seat_device->input_device; + struct input_config *ic = input_device_get_config(input_device); + if (ic != NULL && ic->mapped_from_region != NULL) { + apply_mapping_from_region(input_device->wlr_device, + ic->mapped_from_region, &x, &y); + } + + switch (tool->mode) { + case SWAY_TABLET_TOOL_MODE_ABSOLUTE: + wlr_cursor_warp_absolute(cursor->cursor, input_device->wlr_device, + change_x ? x : NAN, change_y ? y : NAN); + break; + case SWAY_TABLET_TOOL_MODE_RELATIVE: + wlr_cursor_move(cursor->cursor, input_device->wlr_device, dx, dy); + break; + } + + double sx, sy; + struct wlr_surface *surface = NULL; + struct sway_seat *seat = cursor->seat; + node_at_coords(seat, cursor->cursor->x, cursor->cursor->y, &surface, &sx, &sy); + + // The logic for whether we should send a tablet event or an emulated pointer + // event is tricky. It comes down to: + // * If we began a drag on a non-tablet surface (simulating_pointer_from_tool_tip), + // then we should continue sending emulated pointer events regardless of + // whether the surface currently under us accepts tablet or not. + // * Otherwise, if we are over a surface that accepts tablet, then we should + // send tablet events. + // * If we began a drag over a tablet surface, we should continue sending + // tablet events until the drag is released, even if we are now over a + // non-tablet surface. + if (!cursor->simulating_pointer_from_tool_tip && + ((surface && wlr_surface_accepts_tablet_v2(surface, tablet->tablet_v2)) || + wlr_tablet_tool_v2_has_implicit_grab(tool->tablet_v2_tool))) { + seatop_tablet_tool_motion(seat, tool, time_msec); + } else { + wlr_tablet_v2_tablet_tool_notify_proximity_out(tool->tablet_v2_tool); + pointer_motion(cursor, time_msec, input_device->wlr_device, dx, dy, dx, dy); + wlr_seat_pointer_notify_frame(cursor->seat->wlr_seat); + } +} + +static void handle_tool_axis(struct wl_listener *listener, void *data) { + struct sway_cursor *cursor = wl_container_of(listener, cursor, tool_axis); + struct wlr_tablet_tool_axis_event *event = data; + cursor_handle_activity_from_device(cursor, &event->tablet->base); + + struct sway_tablet_tool *sway_tool = event->tool->data; + if (!sway_tool) { + sway_log(SWAY_DEBUG, "tool axis before proximity"); + return; + } + + handle_tablet_tool_position(cursor, sway_tool, + event->updated_axes & WLR_TABLET_TOOL_AXIS_X, + event->updated_axes & WLR_TABLET_TOOL_AXIS_Y, + event->x, event->y, event->dx, event->dy, event->time_msec); + + if (event->updated_axes & WLR_TABLET_TOOL_AXIS_PRESSURE) { + wlr_tablet_v2_tablet_tool_notify_pressure( + sway_tool->tablet_v2_tool, event->pressure); + } + + if (event->updated_axes & WLR_TABLET_TOOL_AXIS_DISTANCE) { + wlr_tablet_v2_tablet_tool_notify_distance( + sway_tool->tablet_v2_tool, event->distance); + } + + if (event->updated_axes & WLR_TABLET_TOOL_AXIS_TILT_X) { + sway_tool->tilt_x = event->tilt_x; + } + + if (event->updated_axes & WLR_TABLET_TOOL_AXIS_TILT_Y) { + sway_tool->tilt_y = event->tilt_y; + } + + if (event->updated_axes & (WLR_TABLET_TOOL_AXIS_TILT_X | WLR_TABLET_TOOL_AXIS_TILT_Y)) { + wlr_tablet_v2_tablet_tool_notify_tilt( + sway_tool->tablet_v2_tool, + sway_tool->tilt_x, sway_tool->tilt_y); + } + + if (event->updated_axes & WLR_TABLET_TOOL_AXIS_ROTATION) { + wlr_tablet_v2_tablet_tool_notify_rotation( + sway_tool->tablet_v2_tool, event->rotation); + } + + if (event->updated_axes & WLR_TABLET_TOOL_AXIS_SLIDER) { + wlr_tablet_v2_tablet_tool_notify_slider( + sway_tool->tablet_v2_tool, event->slider); + } + + if (event->updated_axes & WLR_TABLET_TOOL_AXIS_WHEEL) { + wlr_tablet_v2_tablet_tool_notify_wheel( + sway_tool->tablet_v2_tool, event->wheel_delta, 0); + } +} + +static void handle_tool_tip(struct wl_listener *listener, void *data) { + struct sway_cursor *cursor = wl_container_of(listener, cursor, tool_tip); + struct wlr_tablet_tool_tip_event *event = data; + cursor_handle_activity_from_device(cursor, &event->tablet->base); + + struct sway_tablet_tool *sway_tool = event->tool->data; + if (!sway_tool) { + sway_log(SWAY_DEBUG, "tool tip before proximity"); + return; + } + + struct wlr_tablet_v2_tablet *tablet_v2 = sway_tool->tablet->tablet_v2; + struct sway_seat *seat = cursor->seat; + + + double sx, sy; + struct wlr_surface *surface = NULL; + node_at_coords(seat, cursor->cursor->x, cursor->cursor->y, + &surface, &sx, &sy); + + if (cursor->simulating_pointer_from_tool_tip && + event->state == WLR_TABLET_TOOL_TIP_UP) { + cursor->simulating_pointer_from_tool_tip = false; + dispatch_cursor_button(cursor, &event->tablet->base, event->time_msec, + BTN_LEFT, WL_POINTER_BUTTON_STATE_RELEASED); + wlr_seat_pointer_notify_frame(cursor->seat->wlr_seat); + } else if (!surface || !wlr_surface_accepts_tablet_v2(surface, tablet_v2)) { + // If we started holding the tool tip down on a surface that accepts + // tablet v2, we should notify that surface if it gets released over a + // surface that doesn't support v2. + if (event->state == WLR_TABLET_TOOL_TIP_UP) { + seatop_tablet_tool_tip(seat, sway_tool, event->time_msec, + WLR_TABLET_TOOL_TIP_UP); + } else { + cursor->simulating_pointer_from_tool_tip = true; + dispatch_cursor_button(cursor, &event->tablet->base, + event->time_msec, BTN_LEFT, WL_POINTER_BUTTON_STATE_PRESSED); + wlr_seat_pointer_notify_frame(cursor->seat->wlr_seat); + } + } else { + seatop_tablet_tool_tip(seat, sway_tool, event->time_msec, event->state); + } +} + +static struct sway_tablet *get_tablet_for_device(struct sway_cursor *cursor, + struct wlr_input_device *device) { + struct sway_tablet *tablet; + wl_list_for_each(tablet, &cursor->tablets, link) { + if (tablet->seat_device->input_device->wlr_device == device) { + return tablet; + } + } + return NULL; +} + +static void handle_tool_proximity(struct wl_listener *listener, void *data) { + struct sway_cursor *cursor = + wl_container_of(listener, cursor, tool_proximity); + struct wlr_tablet_tool_proximity_event *event = data; + cursor_handle_activity_from_device(cursor, &event->tablet->base); + + struct wlr_tablet_tool *tool = event->tool; + if (!tool->data) { + struct sway_tablet *tablet = get_tablet_for_device(cursor, + &event->tablet->base); + if (!tablet) { + sway_log(SWAY_ERROR, "no tablet for tablet tool"); + return; + } + sway_tablet_tool_configure(tablet, tool); + } + + struct sway_tablet_tool *sway_tool = tool->data; + if (!sway_tool) { + sway_log(SWAY_ERROR, "tablet tool not initialized"); + return; + } + + if (event->state == WLR_TABLET_TOOL_PROXIMITY_OUT) { + wlr_tablet_v2_tablet_tool_notify_proximity_out(sway_tool->tablet_v2_tool); + return; + } + + handle_tablet_tool_position(cursor, sway_tool, true, true, event->x, event->y, + 0, 0, event->time_msec); +} + +static void handle_tool_button(struct wl_listener *listener, void *data) { + struct sway_cursor *cursor = wl_container_of(listener, cursor, tool_button); + struct wlr_tablet_tool_button_event *event = data; + cursor_handle_activity_from_device(cursor, &event->tablet->base); + + struct sway_tablet_tool *sway_tool = event->tool->data; + if (!sway_tool) { + sway_log(SWAY_DEBUG, "tool button before proximity"); + return; + } + struct wlr_tablet_v2_tablet *tablet_v2 = sway_tool->tablet->tablet_v2; + + double sx, sy; + struct wlr_surface *surface = NULL; + + node_at_coords(cursor->seat, cursor->cursor->x, cursor->cursor->y, + &surface, &sx, &sy); + + // TODO: floating resize should support graphics tablet events + struct wlr_keyboard *keyboard = wlr_seat_get_keyboard(cursor->seat->wlr_seat); + uint32_t modifiers = keyboard ? wlr_keyboard_get_modifiers(keyboard) : 0; + bool mod_pressed = modifiers & config->floating_mod; + + bool surface_supports_tablet_events = + surface && wlr_surface_accepts_tablet_v2(surface, tablet_v2); + + // Simulate pointer when: + // 1. The modifier key is pressed, OR + // 2. The surface under the cursor does not support tablet events. + bool should_simulate_pointer = mod_pressed || !surface_supports_tablet_events; + + // Similar to tool tip, we need to selectively simulate mouse events, but we + // want to make sure that it is always consistent. Because all tool buttons + // currently map to BTN_RIGHT, we need to keep count of how many tool + // buttons are currently pressed down so we can send consistent events. + // + // The logic follows: + // - If we are already simulating the pointer, we should continue to do so + // until at least no tool button is held down. + // - If we should simulate the pointer and no tool button is currently held + // down, begin simulating the pointer. + // - If neither of the above are true, send the tablet events. + if ((cursor->tool_buttons > 0 && cursor->simulating_pointer_from_tool_button) + || (cursor->tool_buttons == 0 && should_simulate_pointer)) { + cursor->simulating_pointer_from_tool_button = true; + + // TODO: the user may want to configure which tool buttons are mapped to + // which simulated pointer buttons + switch (event->state) { + case WLR_BUTTON_PRESSED: + if (cursor->tool_buttons == 0) { + dispatch_cursor_button(cursor, &event->tablet->base, + event->time_msec, BTN_RIGHT, WL_POINTER_BUTTON_STATE_PRESSED); + } + break; + case WLR_BUTTON_RELEASED: + if (cursor->tool_buttons <= 1) { + dispatch_cursor_button(cursor, &event->tablet->base, + event->time_msec, BTN_RIGHT, WL_POINTER_BUTTON_STATE_RELEASED); + } + break; + } + wlr_seat_pointer_notify_frame(cursor->seat->wlr_seat); + } else { + cursor->simulating_pointer_from_tool_button = false; + + wlr_tablet_v2_tablet_tool_notify_button(sway_tool->tablet_v2_tool, + event->button, (enum zwp_tablet_pad_v2_button_state)event->state); + } + + // Update tool button count. + switch (event->state) { + case WLR_BUTTON_PRESSED: + cursor->tool_buttons++; + break; + case WLR_BUTTON_RELEASED: + if (cursor->tool_buttons == 0) { + sway_log(SWAY_ERROR, "inconsistent tablet tool button events"); + } else { + cursor->tool_buttons--; + } + break; + } +} + +static void check_constraint_region(struct sway_cursor *cursor) { + struct wlr_pointer_constraint_v1 *constraint = cursor->active_constraint; + pixman_region32_t *region = &constraint->region; + struct sway_view *view = view_from_wlr_surface(constraint->surface); + if (cursor->active_confine_requires_warp && view) { + cursor->active_confine_requires_warp = false; + + struct sway_container *con = view->container; + + double sx = cursor->cursor->x - con->pending.content_x + view->geometry.x; + double sy = cursor->cursor->y - con->pending.content_y + view->geometry.y; + + if (!pixman_region32_contains_point(region, + floor(sx), floor(sy), NULL)) { + int nboxes; + pixman_box32_t *boxes = pixman_region32_rectangles(region, &nboxes); + if (nboxes > 0) { + double sx = (boxes[0].x1 + boxes[0].x2) / 2.; + double sy = (boxes[0].y1 + boxes[0].y2) / 2.; + + wlr_cursor_warp_closest(cursor->cursor, NULL, + sx + con->pending.content_x - view->geometry.x, + sy + con->pending.content_y - view->geometry.y); + + cursor_rebase(cursor); + } + } + } + + // A locked pointer will result in an empty region, thus disallowing all movement + if (constraint->type == WLR_POINTER_CONSTRAINT_V1_CONFINED) { + pixman_region32_copy(&cursor->confine, region); + } else { + pixman_region32_clear(&cursor->confine); + } +} + +static void handle_constraint_commit(struct wl_listener *listener, + void *data) { + struct sway_cursor *cursor = + wl_container_of(listener, cursor, constraint_commit); + struct wlr_pointer_constraint_v1 *constraint = cursor->active_constraint; + assert(constraint->surface == data); + + check_constraint_region(cursor); +} + +static void handle_pointer_constraint_set_region(struct wl_listener *listener, + void *data) { + struct sway_pointer_constraint *sway_constraint = + wl_container_of(listener, sway_constraint, set_region); + struct sway_cursor *cursor = sway_constraint->cursor; + + cursor->active_confine_requires_warp = true; +} + +static void handle_request_pointer_set_cursor(struct wl_listener *listener, + void *data) { + struct sway_cursor *cursor = + wl_container_of(listener, cursor, request_set_cursor); + if (!seatop_allows_set_cursor(cursor->seat)) { + return; + } + struct wlr_seat_pointer_request_set_cursor_event *event = data; + + struct wl_client *focused_client = NULL; + struct wlr_surface *focused_surface = + cursor->seat->wlr_seat->pointer_state.focused_surface; + if (focused_surface != NULL) { + focused_client = wl_resource_get_client(focused_surface->resource); + } + + // TODO: check cursor mode + if (focused_client == NULL || + event->seat_client->client != focused_client) { + sway_log(SWAY_DEBUG, "denying request to set cursor from unfocused client"); + return; + } + + cursor_set_image_surface(cursor, event->surface, event->hotspot_x, + event->hotspot_y, focused_client); +} + +static void handle_pointer_hold_begin(struct wl_listener *listener, void *data) { + struct sway_cursor *cursor = wl_container_of( + listener, cursor, hold_begin); + struct wlr_pointer_hold_begin_event *event = data; + cursor_handle_activity_from_device(cursor, &event->pointer->base); + seatop_hold_begin(cursor->seat, event); +} + +static void handle_pointer_hold_end(struct wl_listener *listener, void *data) { + struct sway_cursor *cursor = wl_container_of( + listener, cursor, hold_end); + struct wlr_pointer_hold_end_event *event = data; + cursor_handle_activity_from_device(cursor, &event->pointer->base); + seatop_hold_end(cursor->seat, event); +} + +static void handle_pointer_pinch_begin(struct wl_listener *listener, void *data) { + struct sway_cursor *cursor = wl_container_of( + listener, cursor, pinch_begin); + struct wlr_pointer_pinch_begin_event *event = data; + cursor_handle_activity_from_device(cursor, &event->pointer->base); + seatop_pinch_begin(cursor->seat, event); +} + +static void handle_pointer_pinch_update(struct wl_listener *listener, void *data) { + struct sway_cursor *cursor = wl_container_of( + listener, cursor, pinch_update); + struct wlr_pointer_pinch_update_event *event = data; + cursor_handle_activity_from_device(cursor, &event->pointer->base); + seatop_pinch_update(cursor->seat, event); +} + +static void handle_pointer_pinch_end(struct wl_listener *listener, void *data) { + struct sway_cursor *cursor = wl_container_of( + listener, cursor, pinch_end); + struct wlr_pointer_pinch_end_event *event = data; + cursor_handle_activity_from_device(cursor, &event->pointer->base); + seatop_pinch_end(cursor->seat, event); +} + +static void handle_pointer_swipe_begin(struct wl_listener *listener, void *data) { + struct sway_cursor *cursor = wl_container_of( + listener, cursor, swipe_begin); + struct wlr_pointer_swipe_begin_event *event = data; + cursor_handle_activity_from_device(cursor, &event->pointer->base); + seatop_swipe_begin(cursor->seat, event); +} + +static void handle_pointer_swipe_update(struct wl_listener *listener, void *data) { + struct sway_cursor *cursor = wl_container_of( + listener, cursor, swipe_update); + struct wlr_pointer_swipe_update_event *event = data; + cursor_handle_activity_from_device(cursor, &event->pointer->base); + seatop_swipe_update(cursor->seat, event); +} + +static void handle_pointer_swipe_end(struct wl_listener *listener, void *data) { + struct sway_cursor *cursor = wl_container_of( + listener, cursor, swipe_end); + struct wlr_pointer_swipe_end_event *event = data; + cursor_handle_activity_from_device(cursor, &event->pointer->base); + seatop_swipe_end(cursor->seat, event); +} + +static void handle_image_surface_destroy(struct wl_listener *listener, + void *data) { + struct sway_cursor *cursor = + wl_container_of(listener, cursor, image_surface_destroy); + cursor_set_image(cursor, NULL, cursor->image_client); + cursor_rebase(cursor); +} + +static void set_image_surface(struct sway_cursor *cursor, + struct wlr_surface *surface) { + wl_list_remove(&cursor->image_surface_destroy.link); + cursor->image_surface = surface; + if (surface) { + wl_signal_add(&surface->events.destroy, &cursor->image_surface_destroy); + } else { + wl_list_init(&cursor->image_surface_destroy.link); + } +} + +void cursor_set_image(struct sway_cursor *cursor, const char *image, + struct wl_client *client) { + if (!(cursor->seat->wlr_seat->capabilities & WL_SEAT_CAPABILITY_POINTER)) { + return; + } + + const char *current_image = cursor->image; + set_image_surface(cursor, NULL); + cursor->image = image; + cursor->hotspot_x = cursor->hotspot_y = 0; + cursor->image_client = client; + + if (cursor->hidden) { + return; + } + + if (!image) { + wlr_cursor_unset_image(cursor->cursor); + } else if (!current_image || strcmp(current_image, image) != 0) { + wlr_cursor_set_xcursor(cursor->cursor, cursor->xcursor_manager, image); + } +} + +void cursor_set_image_surface(struct sway_cursor *cursor, + struct wlr_surface *surface, int32_t hotspot_x, int32_t hotspot_y, + struct wl_client *client) { + if (!(cursor->seat->wlr_seat->capabilities & WL_SEAT_CAPABILITY_POINTER)) { + return; + } + + set_image_surface(cursor, surface); + cursor->image = NULL; + cursor->hotspot_x = hotspot_x; + cursor->hotspot_y = hotspot_y; + cursor->image_client = client; + + if (cursor->hidden) { + return; + } + + wlr_cursor_set_surface(cursor->cursor, surface, hotspot_x, hotspot_y); +} + +void sway_cursor_destroy(struct sway_cursor *cursor) { + if (!cursor) { + return; + } + + wl_event_source_remove(cursor->hide_source); + + wl_list_remove(&cursor->image_surface_destroy.link); + wl_list_remove(&cursor->hold_begin.link); + wl_list_remove(&cursor->hold_end.link); + wl_list_remove(&cursor->pinch_begin.link); + wl_list_remove(&cursor->pinch_update.link); + wl_list_remove(&cursor->pinch_end.link); + wl_list_remove(&cursor->swipe_begin.link); + wl_list_remove(&cursor->swipe_update.link); + wl_list_remove(&cursor->swipe_end.link); + wl_list_remove(&cursor->motion.link); + wl_list_remove(&cursor->motion_absolute.link); + wl_list_remove(&cursor->button.link); + wl_list_remove(&cursor->axis.link); + wl_list_remove(&cursor->frame.link); + wl_list_remove(&cursor->touch_down.link); + wl_list_remove(&cursor->touch_up.link); + wl_list_remove(&cursor->touch_cancel.link); + wl_list_remove(&cursor->touch_motion.link); + wl_list_remove(&cursor->touch_frame.link); + wl_list_remove(&cursor->tool_axis.link); + wl_list_remove(&cursor->tool_tip.link); + wl_list_remove(&cursor->tool_proximity.link); + wl_list_remove(&cursor->tool_button.link); + wl_list_remove(&cursor->request_set_cursor.link); + + wlr_xcursor_manager_destroy(cursor->xcursor_manager); + wlr_cursor_destroy(cursor->cursor); + free(cursor); +} + +struct sway_cursor *sway_cursor_create(struct sway_seat *seat) { + struct sway_cursor *cursor = calloc(1, sizeof(struct sway_cursor)); + if (!sway_assert(cursor, "could not allocate sway cursor")) { + return NULL; + } + + struct wlr_cursor *wlr_cursor = wlr_cursor_create(); + if (!sway_assert(wlr_cursor, "could not allocate wlr cursor")) { + free(cursor); + return NULL; + } + + cursor->previous.x = wlr_cursor->x; + cursor->previous.y = wlr_cursor->y; + + cursor->seat = seat; + wlr_cursor_attach_output_layout(wlr_cursor, root->output_layout); + + cursor->hide_source = wl_event_loop_add_timer(server.wl_event_loop, + hide_notify, cursor); + + wl_list_init(&cursor->image_surface_destroy.link); + cursor->image_surface_destroy.notify = handle_image_surface_destroy; + + wl_signal_add(&wlr_cursor->events.hold_begin, &cursor->hold_begin); + cursor->hold_begin.notify = handle_pointer_hold_begin; + wl_signal_add(&wlr_cursor->events.hold_end, &cursor->hold_end); + cursor->hold_end.notify = handle_pointer_hold_end; + + wl_signal_add(&wlr_cursor->events.pinch_begin, &cursor->pinch_begin); + cursor->pinch_begin.notify = handle_pointer_pinch_begin; + wl_signal_add(&wlr_cursor->events.pinch_update, &cursor->pinch_update); + cursor->pinch_update.notify = handle_pointer_pinch_update; + wl_signal_add(&wlr_cursor->events.pinch_end, &cursor->pinch_end); + cursor->pinch_end.notify = handle_pointer_pinch_end; + + wl_signal_add(&wlr_cursor->events.swipe_begin, &cursor->swipe_begin); + cursor->swipe_begin.notify = handle_pointer_swipe_begin; + wl_signal_add(&wlr_cursor->events.swipe_update, &cursor->swipe_update); + cursor->swipe_update.notify = handle_pointer_swipe_update; + wl_signal_add(&wlr_cursor->events.swipe_end, &cursor->swipe_end); + cursor->swipe_end.notify = handle_pointer_swipe_end; + + // input events + wl_signal_add(&wlr_cursor->events.motion, &cursor->motion); + cursor->motion.notify = handle_pointer_motion_relative; + + wl_signal_add(&wlr_cursor->events.motion_absolute, + &cursor->motion_absolute); + cursor->motion_absolute.notify = handle_pointer_motion_absolute; + + wl_signal_add(&wlr_cursor->events.button, &cursor->button); + cursor->button.notify = handle_pointer_button; + + wl_signal_add(&wlr_cursor->events.axis, &cursor->axis); + cursor->axis.notify = handle_pointer_axis; + + wl_signal_add(&wlr_cursor->events.frame, &cursor->frame); + cursor->frame.notify = handle_pointer_frame; + + wl_signal_add(&wlr_cursor->events.touch_down, &cursor->touch_down); + cursor->touch_down.notify = handle_touch_down; + + wl_signal_add(&wlr_cursor->events.touch_up, &cursor->touch_up); + cursor->touch_up.notify = handle_touch_up; + + wl_signal_add(&wlr_cursor->events.touch_cancel, &cursor->touch_cancel); + cursor->touch_cancel.notify = handle_touch_cancel; + + wl_signal_add(&wlr_cursor->events.touch_motion, + &cursor->touch_motion); + cursor->touch_motion.notify = handle_touch_motion; + + wl_signal_add(&wlr_cursor->events.touch_frame, &cursor->touch_frame); + cursor->touch_frame.notify = handle_touch_frame; + + wl_signal_add(&wlr_cursor->events.tablet_tool_axis, + &cursor->tool_axis); + cursor->tool_axis.notify = handle_tool_axis; + + wl_signal_add(&wlr_cursor->events.tablet_tool_tip, &cursor->tool_tip); + cursor->tool_tip.notify = handle_tool_tip; + + wl_signal_add(&wlr_cursor->events.tablet_tool_proximity, &cursor->tool_proximity); + cursor->tool_proximity.notify = handle_tool_proximity; + + wl_signal_add(&wlr_cursor->events.tablet_tool_button, &cursor->tool_button); + cursor->tool_button.notify = handle_tool_button; + + wl_signal_add(&seat->wlr_seat->events.request_set_cursor, + &cursor->request_set_cursor); + cursor->request_set_cursor.notify = handle_request_pointer_set_cursor; + + wl_list_init(&cursor->constraint_commit.link); + wl_list_init(&cursor->tablets); + wl_list_init(&cursor->tablet_pads); + + cursor->cursor = wlr_cursor; + + return cursor; +} + +/** + * Warps the cursor to the middle of the container argument. + * Does nothing if the cursor is already inside the container and `force` is + * false. If container is NULL, returns without doing anything. + */ +void cursor_warp_to_container(struct sway_cursor *cursor, + struct sway_container *container, bool force) { + if (!container) { + return; + } + + struct wlr_box box; + container_get_box(container, &box); + if (!force && wlr_box_contains_point(&box, cursor->cursor->x, + cursor->cursor->y)) { + return; + } + + double x = container->pending.x + container->pending.width / 2.0; + double y = container->pending.y + container->pending.height / 2.0; + + wlr_cursor_warp(cursor->cursor, NULL, x, y); + cursor_unhide(cursor); +} + +/** + * Warps the cursor to the middle of the workspace argument. + * If workspace is NULL, returns without doing anything. + */ +void cursor_warp_to_workspace(struct sway_cursor *cursor, + struct sway_workspace *workspace) { + if (!workspace) { + return; + } + + double x = workspace->x + workspace->width / 2.0; + double y = workspace->y + workspace->height / 2.0; + + wlr_cursor_warp(cursor->cursor, NULL, x, y); + cursor_unhide(cursor); +} + +uint32_t get_mouse_bindsym(const char *name, char **error) { + if (strncasecmp(name, "button", strlen("button")) == 0) { + // Map to x11 mouse buttons + int number = name[strlen("button")] - '0'; + if (number < 1 || number > 9 || strlen(name) > strlen("button0")) { + *error = strdup("Only buttons 1-9 are supported. For other mouse " + "buttons, use the name of the event code."); + return 0; + } + static const uint32_t buttons[] = {BTN_LEFT, BTN_MIDDLE, BTN_RIGHT, + SWAY_SCROLL_UP, SWAY_SCROLL_DOWN, SWAY_SCROLL_LEFT, + SWAY_SCROLL_RIGHT, BTN_SIDE, BTN_EXTRA}; + return buttons[number - 1]; + } else if (has_prefix(name, "BTN_")) { + // Get event code from name + int code = libevdev_event_code_from_name(EV_KEY, name); + if (code == -1) { + *error = format_str("Unknown event %s", name); + return 0; + } + return code; + } + return 0; +} + +uint32_t get_mouse_bindcode(const char *name, char **error) { + // Validate event code + errno = 0; + char *endptr; + int code = strtol(name, &endptr, 10); + if (endptr == name && code <= 0) { + *error = strdup("Button event code must be a positive integer."); + return 0; + } else if (errno == ERANGE) { + *error = strdup("Button event code out of range."); + return 0; + } + const char *event = libevdev_event_code_get_name(EV_KEY, code); + if (!event || !has_prefix(event, "BTN_")) { + *error = format_str("Event code %d (%s) is not a button", + code, event ? event : "(null)"); + return 0; + } + return code; +} + +uint32_t get_mouse_button(const char *name, char **error) { + uint32_t button = get_mouse_bindsym(name, error); + if (!button && !*error) { + button = get_mouse_bindcode(name, error); + } + return button; +} + +const char *get_mouse_button_name(uint32_t button) { + const char *name = libevdev_event_code_get_name(EV_KEY, button); + if (!name) { + if (button == SWAY_SCROLL_UP) { + name = "SWAY_SCROLL_UP"; + } else if (button == SWAY_SCROLL_DOWN) { + name = "SWAY_SCROLL_DOWN"; + } else if (button == SWAY_SCROLL_LEFT) { + name = "SWAY_SCROLL_LEFT"; + } else if (button == SWAY_SCROLL_RIGHT) { + name = "SWAY_SCROLL_RIGHT"; + } + } + return name; +} + +static void warp_to_constraint_cursor_hint(struct sway_cursor *cursor) { + struct wlr_pointer_constraint_v1 *constraint = cursor->active_constraint; + + if (constraint->current.cursor_hint.enabled) { + double sx = constraint->current.cursor_hint.x; + double sy = constraint->current.cursor_hint.y; + + struct sway_view *view = view_from_wlr_surface(constraint->surface); + if (!view) { + return; + } + + struct sway_container *con = view->container; + + double lx = sx + con->pending.content_x - view->geometry.x; + double ly = sy + con->pending.content_y - view->geometry.y; + + wlr_cursor_warp(cursor->cursor, NULL, lx, ly); + + // Warp the pointer as well, so that on the next pointer rebase we don't + // send an unexpected synthetic motion event to clients. + wlr_seat_pointer_warp(constraint->seat, sx, sy); + } +} + +void handle_constraint_destroy(struct wl_listener *listener, void *data) { + struct sway_pointer_constraint *sway_constraint = + wl_container_of(listener, sway_constraint, destroy); + struct wlr_pointer_constraint_v1 *constraint = data; + struct sway_cursor *cursor = sway_constraint->cursor; + + wl_list_remove(&sway_constraint->set_region.link); + wl_list_remove(&sway_constraint->destroy.link); + + if (cursor->active_constraint == constraint) { + warp_to_constraint_cursor_hint(cursor); + + if (cursor->constraint_commit.link.next != NULL) { + wl_list_remove(&cursor->constraint_commit.link); + } + wl_list_init(&cursor->constraint_commit.link); + cursor->active_constraint = NULL; + } + + free(sway_constraint); +} + +void handle_pointer_constraint(struct wl_listener *listener, void *data) { + struct wlr_pointer_constraint_v1 *constraint = data; + struct sway_seat *seat = constraint->seat->data; + + struct sway_pointer_constraint *sway_constraint = + calloc(1, sizeof(struct sway_pointer_constraint)); + sway_constraint->cursor = seat->cursor; + sway_constraint->constraint = constraint; + + sway_constraint->set_region.notify = handle_pointer_constraint_set_region; + wl_signal_add(&constraint->events.set_region, &sway_constraint->set_region); + + sway_constraint->destroy.notify = handle_constraint_destroy; + wl_signal_add(&constraint->events.destroy, &sway_constraint->destroy); + + struct wlr_surface *surface = seat->wlr_seat->keyboard_state.focused_surface; + if (surface && surface == constraint->surface) { + sway_cursor_constrain(seat->cursor, constraint); + } +} + +void sway_cursor_constrain(struct sway_cursor *cursor, + struct wlr_pointer_constraint_v1 *constraint) { + struct seat_config *config = seat_get_config(cursor->seat); + if (!config) { + config = seat_get_config_by_name("*"); + } + + if (!config || config->allow_constrain == CONSTRAIN_DISABLE) { + return; + } + + if (cursor->active_constraint == constraint) { + return; + } + + wl_list_remove(&cursor->constraint_commit.link); + if (cursor->active_constraint) { + if (constraint == NULL) { + warp_to_constraint_cursor_hint(cursor); + } + wlr_pointer_constraint_v1_send_deactivated( + cursor->active_constraint); + } + + cursor->active_constraint = constraint; + + if (constraint == NULL) { + wl_list_init(&cursor->constraint_commit.link); + return; + } + + cursor->active_confine_requires_warp = true; + + // FIXME: Big hack, stolen from wlr_pointer_constraints_v1.c:121. + // This is necessary because the focus may be set before the surface + // has finished committing, which means that warping won't work properly, + // since this code will be run *after* the focus has been set. + // That is why we duplicate the code here. + if (pixman_region32_not_empty(&constraint->current.region)) { + pixman_region32_intersect(&constraint->region, + &constraint->surface->input_region, &constraint->current.region); + } else { + pixman_region32_copy(&constraint->region, + &constraint->surface->input_region); + } + + check_constraint_region(cursor); + + wlr_pointer_constraint_v1_send_activated(constraint); + + cursor->constraint_commit.notify = handle_constraint_commit; + wl_signal_add(&constraint->surface->events.commit, + &cursor->constraint_commit); +} + +void handle_request_set_cursor_shape(struct wl_listener *listener, void *data) { + const struct wlr_cursor_shape_manager_v1_request_set_shape_event *event = data; + struct sway_seat *seat = event->seat_client->seat->data; + + if (!seatop_allows_set_cursor(seat)) { + return; + } + + struct wl_client *focused_client = NULL; + struct wlr_surface *focused_surface = seat->wlr_seat->pointer_state.focused_surface; + if (focused_surface != NULL) { + focused_client = wl_resource_get_client(focused_surface->resource); + } + + // TODO: check cursor mode + if (focused_client == NULL || event->seat_client->client != focused_client) { + sway_log(SWAY_DEBUG, "denying request to set cursor from unfocused client"); + return; + } + + cursor_set_image(seat->cursor, wlr_cursor_shape_v1_name(event->shape), focused_client); +} diff --git a/sway/input/input-manager.c b/sway/input/input-manager.c new file mode 100644 index 0000000..ffcf8fc --- /dev/null +++ b/sway/input/input-manager.c @@ -0,0 +1,733 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "sway/config.h" +#include "sway/input/cursor.h" +#include "sway/input/input-manager.h" +#include "sway/input/keyboard.h" +#include "sway/input/libinput.h" +#include "sway/input/seat.h" +#include "sway/ipc-server.h" +#include "sway/server.h" +#include "sway/tree/view.h" +#include "stringop.h" +#include "list.h" +#include "log.h" + +#if WLR_HAS_LIBINPUT_BACKEND +#include +#endif + +#define DEFAULT_SEAT "seat0" + +struct input_config *current_input_config = NULL; +struct seat_config *current_seat_config = NULL; + +struct sway_seat *input_manager_current_seat(void) { + struct sway_seat *seat = config->handler_context.seat; + if (!seat) { + seat = input_manager_get_default_seat(); + } + return seat; +} + +struct sway_seat *input_manager_get_default_seat(void) { + return input_manager_get_seat(DEFAULT_SEAT, true); +} + +struct sway_seat *input_manager_get_seat(const char *seat_name, bool create) { + struct sway_seat *seat = NULL; + wl_list_for_each(seat, &server.input->seats, link) { + if (strcmp(seat->wlr_seat->name, seat_name) == 0) { + return seat; + } + } + + return create ? seat_create(seat_name) : NULL; +} + +struct sway_seat *input_manager_sway_seat_from_wlr_seat(struct wlr_seat *wlr_seat) { + struct sway_seat *seat = NULL; + + wl_list_for_each(seat, &server.input->seats, link) { + if (seat->wlr_seat == wlr_seat) { + return seat; + } + } + + return NULL; +} + +char *input_device_get_identifier(struct wlr_input_device *device) { + int vendor = 0, product = 0; +#if WLR_HAS_LIBINPUT_BACKEND + if (wlr_input_device_is_libinput(device)) { + struct libinput_device *libinput_dev = wlr_libinput_get_device_handle(device); + vendor = libinput_device_get_id_vendor(libinput_dev); + product = libinput_device_get_id_product(libinput_dev); + } +#endif + + char *name = strdup(device->name ? device->name : ""); + strip_whitespace(name); + + char *p = name; + for (; *p; ++p) { + // There are in fact input devices with unprintable characters in its name + if (*p == ' ' || !isprint(*p)) { + *p = '_'; + } + } + + char *identifier = format_str("%d:%d:%s", vendor, product, name); + free(name); + return identifier; +} + +static bool device_is_touchpad(struct sway_input_device *device) { +#if WLR_HAS_LIBINPUT_BACKEND + if (device->wlr_device->type != WLR_INPUT_DEVICE_POINTER || + !wlr_input_device_is_libinput(device->wlr_device)) { + return false; + } + + struct libinput_device *libinput_device = + wlr_libinput_get_device_handle(device->wlr_device); + + return libinput_device_config_tap_get_finger_count(libinput_device) > 0; +#else + return false; +#endif +} + +const char *input_device_get_type(struct sway_input_device *device) { + switch (device->wlr_device->type) { + case WLR_INPUT_DEVICE_POINTER: + if (device_is_touchpad(device)) { + return "touchpad"; + } else { + return "pointer"; + } + case WLR_INPUT_DEVICE_KEYBOARD: + return "keyboard"; + case WLR_INPUT_DEVICE_TOUCH: + return "touch"; + case WLR_INPUT_DEVICE_TABLET: + return "tablet_tool"; + case WLR_INPUT_DEVICE_TABLET_PAD: + return "tablet_pad"; + case WLR_INPUT_DEVICE_SWITCH: + return "switch"; + } + return "unknown"; +} + +static void apply_input_type_config(struct sway_input_device *input_device) { + const char *device_type = input_device_get_type(input_device); + struct input_config *type_config = NULL; + for (int i = 0; i < config->input_type_configs->length; i++) { + struct input_config *ic = config->input_type_configs->items[i]; + if (strcmp(ic->identifier + 5, device_type) == 0) { + type_config = ic; + break; + } + } + if (type_config == NULL) { + return; + } + + for (int i = 0; i < config->input_configs->length; i++) { + struct input_config *ic = config->input_configs->items[i]; + if (strcmp(input_device->identifier, ic->identifier) == 0) { + struct input_config *current = new_input_config(ic->identifier); + merge_input_config(current, type_config); + merge_input_config(current, ic); + + current->input_type = device_type; + config->input_configs->items[i] = current; + free_input_config(ic); + ic = NULL; + + break; + } + } +} + +static struct sway_input_device *input_sway_device_from_wlr( + struct wlr_input_device *device) { + struct sway_input_device *input_device = NULL; + wl_list_for_each(input_device, &server.input->devices, link) { + if (input_device->wlr_device == device) { + return input_device; + } + } + return NULL; +} + +static bool input_has_seat_fallback_configuration(void) { + struct sway_seat *seat = NULL; + wl_list_for_each(seat, &server.input->seats, link) { + struct seat_config *seat_config = seat_get_config(seat); + if (seat_config && strcmp(seat_config->name, "*") != 0 + && seat_config->fallback != -1) { + return true; + } + } + + return false; +} + +void input_manager_verify_fallback_seat(void) { + struct sway_seat *seat = NULL; + if (!input_has_seat_fallback_configuration()) { + sway_log(SWAY_DEBUG, "no fallback seat config - creating default"); + seat = input_manager_get_default_seat(); + struct seat_config *sc = new_seat_config(seat->wlr_seat->name); + sc->fallback = true; + sc = store_seat_config(sc); + input_manager_apply_seat_config(sc); + } +} + +static void handle_device_destroy(struct wl_listener *listener, void *data) { + struct wlr_input_device *device = data; + + struct sway_input_device *input_device = input_sway_device_from_wlr(device); + + if (!sway_assert(input_device, "could not find sway device")) { + return; + } + + sway_log(SWAY_DEBUG, "removing device: '%s'", + input_device->identifier); + + struct sway_seat *seat = NULL; + wl_list_for_each(seat, &server.input->seats, link) { + seat_remove_device(seat, input_device); + } + + ipc_event_input("removed", input_device); + + wl_list_remove(&input_device->link); + wl_list_remove(&input_device->device_destroy.link); + free(input_device->identifier); + free(input_device); +} + +static void handle_new_input(struct wl_listener *listener, void *data) { + struct sway_input_manager *input = + wl_container_of(listener, input, new_input); + struct wlr_input_device *device = data; + + struct sway_input_device *input_device = + calloc(1, sizeof(struct sway_input_device)); + if (!sway_assert(input_device, "could not allocate input device")) { + return; + } + device->data = input_device; + + input_device->wlr_device = device; + input_device->identifier = input_device_get_identifier(device); + wl_list_insert(&input->devices, &input_device->link); + + sway_log(SWAY_DEBUG, "adding device: '%s'", + input_device->identifier); + + apply_input_type_config(input_device); + +#if WLR_HAS_LIBINPUT_BACKEND + bool config_changed = sway_input_configure_libinput_device(input_device); +#else + bool config_changed = false; +#endif + + wl_signal_add(&device->events.destroy, &input_device->device_destroy); + input_device->device_destroy.notify = handle_device_destroy; + + input_manager_verify_fallback_seat(); + + bool added = false; + struct sway_seat *seat = NULL; + wl_list_for_each(seat, &input->seats, link) { + struct seat_config *seat_config = seat_get_config(seat); + bool has_attachment = seat_config && + (seat_config_get_attachment(seat_config, input_device->identifier) || + seat_config_get_attachment(seat_config, "*")); + + if (has_attachment) { + seat_add_device(seat, input_device); + added = true; + } + } + + if (!added) { + wl_list_for_each(seat, &input->seats, link) { + struct seat_config *seat_config = seat_get_config(seat); + if (seat_config && seat_config->fallback == 1) { + seat_add_device(seat, input_device); + added = true; + } + } + } + + if (!added) { + sway_log(SWAY_DEBUG, + "device '%s' is not configured on any seats", + input_device->identifier); + } + + ipc_event_input("added", input_device); + + if (config_changed) { + ipc_event_input("libinput_config", input_device); + } +} + +static void handle_keyboard_shortcuts_inhibitor_destroy( + struct wl_listener *listener, void *data) { + struct sway_keyboard_shortcuts_inhibitor *sway_inhibitor = + wl_container_of(listener, sway_inhibitor, destroy); + + sway_log(SWAY_DEBUG, "Removing keyboard shortcuts inhibitor"); + + // sway_seat::keyboard_shortcuts_inhibitors + wl_list_remove(&sway_inhibitor->link); + wl_list_remove(&sway_inhibitor->destroy.link); + free(sway_inhibitor); +} + +static void handle_keyboard_shortcuts_inhibit_new_inhibitor( + struct wl_listener *listener, void *data) { + struct sway_input_manager *input_manager = + wl_container_of(listener, input_manager, + keyboard_shortcuts_inhibit_new_inhibitor); + struct wlr_keyboard_shortcuts_inhibitor_v1 *inhibitor = data; + + sway_log(SWAY_DEBUG, "Adding keyboard shortcuts inhibitor"); + + struct sway_keyboard_shortcuts_inhibitor *sway_inhibitor = + calloc(1, sizeof(struct sway_keyboard_shortcuts_inhibitor)); + if (!sway_assert(sway_inhibitor, "could not allocate keyboard " + "shortcuts inhibitor")) { + return; + } + sway_inhibitor->inhibitor = inhibitor; + + sway_inhibitor->destroy.notify = handle_keyboard_shortcuts_inhibitor_destroy; + wl_signal_add(&inhibitor->events.destroy, &sway_inhibitor->destroy); + + // attach inhibitor to the seat it applies to + struct sway_seat *seat = inhibitor->seat->data; + wl_list_insert(&seat->keyboard_shortcuts_inhibitors, &sway_inhibitor->link); + + // per-view, seat-agnostic config via criteria + struct sway_view *view = view_from_wlr_surface(inhibitor->surface); + enum seat_config_shortcuts_inhibit inhibit = SHORTCUTS_INHIBIT_DEFAULT; + if (view) { + inhibit = view->shortcuts_inhibit; + } + + if (inhibit == SHORTCUTS_INHIBIT_DEFAULT) { + struct seat_config *config = seat_get_config(seat); + if (!config) { + config = seat_get_config_by_name("*"); + } + + if (config) { + inhibit = config->shortcuts_inhibit; + } + } + + if (inhibit == SHORTCUTS_INHIBIT_DISABLE) { + /** + * Here we deny to honour the inhibitor by never sending the + * activate signal. We can not, however, destroy the inhibitor + * because the protocol doesn't allow for it. So it will linger + * until the client removes it im- or explicitly. But at least + * it can only be one inhibitor per surface and seat at a time. + * + * We also want to allow the user to activate the inhibitor + * manually later which is why we do this check here where the + * inhibitor is already attached to its seat and ready for use. + */ + return; + } + + wlr_keyboard_shortcuts_inhibitor_v1_activate(inhibitor); +} + +void handle_virtual_keyboard(struct wl_listener *listener, void *data) { + struct sway_input_manager *input_manager = + wl_container_of(listener, input_manager, virtual_keyboard_new); + struct wlr_virtual_keyboard_v1 *keyboard = data; + struct wlr_input_device *device = &keyboard->keyboard.base; + + // TODO: Amend protocol to allow NULL seat + struct sway_seat *seat = keyboard->seat ? + input_manager_sway_seat_from_wlr_seat(keyboard->seat) : + input_manager_get_default_seat(); + + struct sway_input_device *input_device = + calloc(1, sizeof(struct sway_input_device)); + if (!sway_assert(input_device, "could not allocate input device")) { + return; + } + device->data = input_device; + + input_device->is_virtual = true; + input_device->wlr_device = device; + input_device->identifier = input_device_get_identifier(device); + wl_list_insert(&input_manager->devices, &input_device->link); + + sway_log(SWAY_DEBUG, "adding virtual keyboard: '%s'", + input_device->identifier); + + wl_signal_add(&device->events.destroy, &input_device->device_destroy); + input_device->device_destroy.notify = handle_device_destroy; + + seat_add_device(seat, input_device); +} + +void handle_virtual_pointer(struct wl_listener *listener, void *data) { + struct sway_input_manager *input_manager = + wl_container_of(listener, input_manager, virtual_pointer_new); + struct wlr_virtual_pointer_v1_new_pointer_event *event = data; + struct wlr_virtual_pointer_v1 *pointer = event->new_pointer; + struct wlr_input_device *device = &pointer->pointer.base; + + struct sway_seat *seat = event->suggested_seat ? + input_manager_sway_seat_from_wlr_seat(event->suggested_seat) : + input_manager_get_default_seat(); + + struct sway_input_device *input_device = + calloc(1, sizeof(struct sway_input_device)); + if (!sway_assert(input_device, "could not allocate input device")) { + return; + } + device->data = input_device; + + input_device->is_virtual = true; + input_device->wlr_device = device; + input_device->identifier = input_device_get_identifier(device); + wl_list_insert(&input_manager->devices, &input_device->link); + + sway_log(SWAY_DEBUG, "adding virtual pointer: '%s'", + input_device->identifier); + + wl_signal_add(&device->events.destroy, &input_device->device_destroy); + input_device->device_destroy.notify = handle_device_destroy; + + seat_add_device(seat, input_device); + + if (event->suggested_output) { + wlr_cursor_map_input_to_output(seat->cursor->cursor, device, + event->suggested_output); + } +} + +static void handle_transient_seat_manager_create_seat( + struct wl_listener *listener, void *data) { + struct wlr_transient_seat_v1 *transient_seat = data; + static uint64_t i; + char name[256]; + snprintf(name, sizeof(name), "transient-%"PRIx64, i++); + struct sway_seat *seat = seat_create(name); + if (seat && seat->wlr_seat) { + wlr_transient_seat_v1_ready(transient_seat, seat->wlr_seat); + } else { + wlr_transient_seat_v1_deny(transient_seat); + } +} + +struct sway_input_manager *input_manager_create(struct sway_server *server) { + struct sway_input_manager *input = + calloc(1, sizeof(struct sway_input_manager)); + if (!input) { + return NULL; + } + + wl_list_init(&input->devices); + wl_list_init(&input->seats); + + input->new_input.notify = handle_new_input; + wl_signal_add(&server->backend->events.new_input, &input->new_input); + + input->virtual_keyboard = wlr_virtual_keyboard_manager_v1_create( + server->wl_display); + wl_signal_add(&input->virtual_keyboard->events.new_virtual_keyboard, + &input->virtual_keyboard_new); + input->virtual_keyboard_new.notify = handle_virtual_keyboard; + + input->virtual_pointer = wlr_virtual_pointer_manager_v1_create( + server->wl_display + ); + wl_signal_add(&input->virtual_pointer->events.new_virtual_pointer, + &input->virtual_pointer_new); + input->virtual_pointer_new.notify = handle_virtual_pointer; + + input->keyboard_shortcuts_inhibit = + wlr_keyboard_shortcuts_inhibit_v1_create(server->wl_display); + input->keyboard_shortcuts_inhibit_new_inhibitor.notify = + handle_keyboard_shortcuts_inhibit_new_inhibitor; + wl_signal_add(&input->keyboard_shortcuts_inhibit->events.new_inhibitor, + &input->keyboard_shortcuts_inhibit_new_inhibitor); + + input->pointer_gestures = wlr_pointer_gestures_v1_create(server->wl_display); + + input->transient_seat_manager = + wlr_transient_seat_manager_v1_create(server->wl_display); + assert(input->transient_seat_manager); + + input->transient_seat_create.notify = + handle_transient_seat_manager_create_seat; + wl_signal_add(&input->transient_seat_manager->events.create_seat, + &input->transient_seat_create); + + return input; +} + +void input_manager_finish(struct sway_input_manager *input) { + wl_list_remove(&input->new_input.link); + wl_list_remove(&input->virtual_keyboard_new.link); + wl_list_remove(&input->virtual_pointer_new.link); + wl_list_remove(&input->keyboard_shortcuts_inhibit_new_inhibitor.link); + wl_list_remove(&input->transient_seat_create.link); +} + +bool input_manager_has_focus(struct sway_node *node) { + struct sway_seat *seat = NULL; + wl_list_for_each(seat, &server.input->seats, link) { + if (seat_get_focus(seat) == node) { + return true; + } + } + + return false; +} + +void input_manager_set_focus(struct sway_node *node) { + struct sway_seat *seat; + wl_list_for_each(seat, &server.input->seats, link) { + seat_set_focus(seat, node); + seat_consider_warp_to_focus(seat); + } +} + +/** + * Re-translate keysyms if a change in the input config could affect them. + */ +static void retranslate_keysyms(struct input_config *input_config) { + for (int i = 0; i < config->input_configs->length; ++i) { + struct input_config *ic = config->input_configs->items[i]; + if (ic->xkb_layout || ic->xkb_file) { + // this is the first config with xkb_layout or xkb_file + if (ic->identifier == input_config->identifier) { + translate_keysyms(ic); + } + + return; + } + } + + for (int i = 0; i < config->input_type_configs->length; ++i) { + struct input_config *ic = config->input_type_configs->items[i]; + if (ic->xkb_layout || ic->xkb_file) { + // this is the first config with xkb_layout or xkb_file + if (ic->identifier == input_config->identifier) { + translate_keysyms(ic); + } + + return; + } + } +} + +static void input_manager_configure_input( + struct sway_input_device *input_device) { +#if WLR_HAS_LIBINPUT_BACKEND + bool config_changed = sway_input_configure_libinput_device(input_device); +#else + bool config_changed = false; +#endif + struct sway_seat *seat = NULL; + wl_list_for_each(seat, &server.input->seats, link) { + seat_configure_device(seat, input_device); + } + if (config_changed) { + ipc_event_input("libinput_config", input_device); + } +} + +void input_manager_configure_all_input_mappings(void) { + struct sway_input_device *input_device; + wl_list_for_each(input_device, &server.input->devices, link) { + struct sway_seat *seat; + wl_list_for_each(seat, &server.input->seats, link) { + seat_configure_device_mapping(seat, input_device); + } + +#if WLR_HAS_LIBINPUT_BACKEND + // Input devices mapped to unavailable outputs get their libinput + // send_events setting switched to false. We need to re-enable this + // when the output appears. + sway_input_configure_libinput_device_send_events(input_device); +#endif + } +} + +void input_manager_apply_input_config(struct input_config *input_config) { + struct sway_input_device *input_device = NULL; + bool wildcard = strcmp(input_config->identifier, "*") == 0; + bool type_wildcard = has_prefix(input_config->identifier, "type:"); + wl_list_for_each(input_device, &server.input->devices, link) { + bool type_matches = type_wildcard && + strcmp(input_device_get_type(input_device), input_config->identifier + 5) == 0; + if (strcmp(input_device->identifier, input_config->identifier) == 0 + || wildcard + || type_matches) { + input_manager_configure_input(input_device); + } + } + + retranslate_keysyms(input_config); +} + +void input_manager_reset_input(struct sway_input_device *input_device) { +#if WLR_HAS_LIBINPUT_BACKEND + sway_input_reset_libinput_device(input_device); +#endif + struct sway_seat *seat = NULL; + wl_list_for_each(seat, &server.input->seats, link) { + seat_reset_device(seat, input_device); + } +} + +void input_manager_reset_all_inputs(void) { + // Set the active keyboard to NULL to avoid spamming configuration updates + // for all keyboard devices. + struct sway_seat *seat; + wl_list_for_each(seat, &server.input->seats, link) { + wlr_seat_set_keyboard(seat->wlr_seat, NULL); + } + + struct sway_input_device *input_device = NULL; + wl_list_for_each(input_device, &server.input->devices, link) { + input_manager_reset_input(input_device); + } + + // If there is at least one keyboard using the default keymap, repeat delay, + // and repeat rate, then it is possible that there is a keyboard group that + // need their keyboard disarmed. + wl_list_for_each(seat, &server.input->seats, link) { + struct sway_keyboard_group *group; + wl_list_for_each(group, &seat->keyboard_groups, link) { + sway_keyboard_disarm_key_repeat(group->seat_device->keyboard); + } + } +} + +void input_manager_apply_seat_config(struct seat_config *seat_config) { + sway_log(SWAY_DEBUG, "applying seat config for seat %s", seat_config->name); + if (strcmp(seat_config->name, "*") == 0) { + struct sway_seat *seat = NULL; + wl_list_for_each(seat, &server.input->seats, link) { + // Only apply the wildcard config directly if there is no seat + // specific config + struct seat_config *sc = seat_get_config(seat); + if (!sc) { + sc = seat_config; + } + seat_apply_config(seat, sc); + } + } else { + struct sway_seat *seat = + input_manager_get_seat(seat_config->name, true); + if (!seat) { + return; + } + seat_apply_config(seat, seat_config); + } + + // for every device, try to add it to a seat and if no seat has it + // attached, add it to the fallback seats. + struct sway_input_device *input_device = NULL; + wl_list_for_each(input_device, &server.input->devices, link) { + list_t *seat_list = create_list(); + struct sway_seat *seat = NULL; + wl_list_for_each(seat, &server.input->seats, link) { + struct seat_config *seat_config = seat_get_config(seat); + if (!seat_config) { + continue; + } + if (seat_config_get_attachment(seat_config, "*") || + seat_config_get_attachment(seat_config, + input_device->identifier)) { + list_add(seat_list, seat); + } + } + + if (seat_list->length) { + wl_list_for_each(seat, &server.input->seats, link) { + bool attached = false; + for (int i = 0; i < seat_list->length; ++i) { + if (seat == seat_list->items[i]) { + attached = true; + break; + } + } + if (attached) { + seat_add_device(seat, input_device); + } else { + seat_remove_device(seat, input_device); + } + } + } else { + wl_list_for_each(seat, &server.input->seats, link) { + struct seat_config *seat_config = seat_get_config(seat); + if (seat_config && seat_config->fallback == 1) { + seat_add_device(seat, input_device); + } else { + seat_remove_device(seat, input_device); + } + } + } + list_free(seat_list); + } +} + +void input_manager_configure_xcursor(void) { + struct sway_seat *seat = NULL; + wl_list_for_each(seat, &server.input->seats, link) { + seat_configure_xcursor(seat); + } +} + +struct input_config *input_device_get_config(struct sway_input_device *device) { + struct input_config *wildcard_config = NULL; + struct input_config *input_config = NULL; + for (int i = 0; i < config->input_configs->length; ++i) { + input_config = config->input_configs->items[i]; + if (strcmp(input_config->identifier, device->identifier) == 0) { + return input_config; + } else if (strcmp(input_config->identifier, "*") == 0) { + wildcard_config = input_config; + } + } + + const char *device_type = input_device_get_type(device); + for (int i = 0; i < config->input_type_configs->length; ++i) { + input_config = config->input_type_configs->items[i]; + if (strcmp(input_config->identifier + 5, device_type) == 0) { + return input_config; + } + } + + return wildcard_config; +} diff --git a/sway/input/keyboard.c b/sway/input/keyboard.c new file mode 100644 index 0000000..d182373 --- /dev/null +++ b/sway/input/keyboard.c @@ -0,0 +1,1148 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "sway/commands.h" +#include "sway/input/input-manager.h" +#include "sway/input/keyboard.h" +#include "sway/input/seat.h" +#include "sway/input/cursor.h" +#include "sway/ipc-server.h" +#include "sway/server.h" +#include "log.h" + +#if WLR_HAS_SESSION +#include +#endif + +static struct modifier_key { + char *name; + uint32_t mod; +} modifiers[] = { + { XKB_MOD_NAME_SHIFT, WLR_MODIFIER_SHIFT }, + { XKB_MOD_NAME_CAPS, WLR_MODIFIER_CAPS }, + { XKB_MOD_NAME_CTRL, WLR_MODIFIER_CTRL }, + { "Ctrl", WLR_MODIFIER_CTRL }, + { XKB_MOD_NAME_ALT, WLR_MODIFIER_ALT }, + { "Alt", WLR_MODIFIER_ALT }, + { XKB_MOD_NAME_NUM, WLR_MODIFIER_MOD2 }, + { "Mod3", WLR_MODIFIER_MOD3 }, + { XKB_MOD_NAME_LOGO, WLR_MODIFIER_LOGO }, + { "Super", WLR_MODIFIER_LOGO }, + { "Mod5", WLR_MODIFIER_MOD5 }, +}; + +uint32_t get_modifier_mask_by_name(const char *name) { + int i; + for (i = 0; i < (int)(sizeof(modifiers) / sizeof(struct modifier_key)); ++i) { + if (strcasecmp(modifiers[i].name, name) == 0) { + return modifiers[i].mod; + } + } + + return 0; +} + +const char *get_modifier_name_by_mask(uint32_t modifier) { + int i; + for (i = 0; i < (int)(sizeof(modifiers) / sizeof(struct modifier_key)); ++i) { + if (modifiers[i].mod == modifier) { + return modifiers[i].name; + } + } + + return NULL; +} + +int get_modifier_names(const char **names, uint32_t modifier_masks) { + int length = 0; + int i; + for (i = 0; i < (int)(sizeof(modifiers) / sizeof(struct modifier_key)); ++i) { + if ((modifier_masks & modifiers[i].mod) != 0) { + names[length] = modifiers[i].name; + ++length; + modifier_masks ^= modifiers[i].mod; + } + } + + return length; +} + +/** + * Remove all key ids associated to a keycode from the list of pressed keys + */ +static bool state_erase_key(struct sway_shortcut_state *state, + uint32_t keycode) { + bool found = false; + size_t j = 0; + for (size_t i = 0; i < state->npressed; ++i) { + if (i > j) { + state->pressed_keys[j] = state->pressed_keys[i]; + state->pressed_keycodes[j] = state->pressed_keycodes[i]; + } + if (state->pressed_keycodes[i] != keycode) { + ++j; + } else { + found = true; + } + } + while(state->npressed > j) { + --state->npressed; + state->pressed_keys[state->npressed] = 0; + state->pressed_keycodes[state->npressed] = 0; + } + state->current_key = 0; + return found; +} + +/** + * Add a key id (with associated keycode) to the list of pressed keys, + * if the list is not full. + */ +static void state_add_key(struct sway_shortcut_state *state, + uint32_t keycode, uint32_t key_id) { + if (state->npressed >= SWAY_KEYBOARD_PRESSED_KEYS_CAP) { + return; + } + size_t i = 0; + while (i < state->npressed && state->pressed_keys[i] < key_id) { + ++i; + } + size_t j = state->npressed; + while (j > i) { + state->pressed_keys[j] = state->pressed_keys[j - 1]; + state->pressed_keycodes[j] = state->pressed_keycodes[j - 1]; + --j; + } + state->pressed_keys[i] = key_id; + state->pressed_keycodes[i] = keycode; + state->npressed++; + state->current_key = key_id; +} + +/** + * Update the shortcut model state in response to new input + */ +static bool update_shortcut_state(struct sway_shortcut_state *state, + uint32_t keycode, enum wl_keyboard_key_state keystate, uint32_t new_key, + uint32_t raw_modifiers) { + bool last_key_was_a_modifier = raw_modifiers != state->last_raw_modifiers; + state->last_raw_modifiers = raw_modifiers; + + if (last_key_was_a_modifier && state->last_keycode) { + // Last pressed key before this one was a modifier + state_erase_key(state, state->last_keycode); + } + + if (keystate == WL_KEYBOARD_KEY_STATE_PRESSED) { + // Add current key to set; there may be duplicates + state_add_key(state, keycode, new_key); + state->last_keycode = keycode; + } else { + return state_erase_key(state, keycode); + } + + return false; +} + +/** + * If one exists, finds a binding which matches the shortcut model state, + * current modifiers, release state, and locked state. + */ +static void get_active_binding(const struct sway_shortcut_state *state, + list_t *bindings, struct sway_binding **current_binding, + uint32_t modifiers, bool release, bool locked, bool inhibited, + const char *input, bool exact_input, xkb_layout_index_t group) { + for (int i = 0; i < bindings->length; ++i) { + struct sway_binding *binding = bindings->items[i]; + bool binding_locked = (binding->flags & BINDING_LOCKED) != 0; + bool binding_inhibited = (binding->flags & BINDING_INHIBITED) != 0; + bool binding_release = binding->flags & BINDING_RELEASE; + + if (modifiers ^ binding->modifiers || + release != binding_release || + locked > binding_locked || + inhibited > binding_inhibited || + (binding->group != XKB_LAYOUT_INVALID && + binding->group != group) || + (strcmp(binding->input, input) != 0 && + (strcmp(binding->input, "*") != 0 || exact_input))) { + continue; + } + + bool match = false; + if (state->npressed == (size_t)binding->keys->length) { + match = true; + for (size_t j = 0; j < state->npressed; j++) { + uint32_t key = *(uint32_t *)binding->keys->items[j]; + if (key != state->pressed_keys[j]) { + match = false; + break; + } + } + } else if (binding->keys->length == 1) { + /* + * If no multiple-key binding has matched, try looking for + * single-key bindings that match the newly-pressed key. + */ + match = state->current_key == *(uint32_t *)binding->keys->items[0]; + } + if (!match) { + continue; + } + + if (*current_binding) { + if (*current_binding == binding) { + continue; + } + + bool current_locked = + ((*current_binding)->flags & BINDING_LOCKED) != 0; + bool current_inhibited = + ((*current_binding)->flags & BINDING_INHIBITED) != 0; + bool current_input = strcmp((*current_binding)->input, input) == 0; + bool current_group_set = + (*current_binding)->group != XKB_LAYOUT_INVALID; + bool binding_input = strcmp(binding->input, input) == 0; + bool binding_group_set = binding->group != XKB_LAYOUT_INVALID; + + if (current_input == binding_input + && current_locked == binding_locked + && current_inhibited == binding_inhibited + && current_group_set == binding_group_set) { + sway_log(SWAY_DEBUG, + "Encountered conflicting bindings %d and %d", + (*current_binding)->order, binding->order); + continue; + } + + if (current_input && !binding_input) { + continue; // Prefer the correct input + } + + if (current_input == binding_input && + (*current_binding)->group == group) { + continue; // Prefer correct group for matching inputs + } + + if (current_input == binding_input && + current_group_set == binding_group_set && + current_locked == locked) { + continue; // Prefer correct lock state for matching input+group + } + + if (current_input == binding_input && + current_group_set == binding_group_set && + current_locked == binding_locked && + current_inhibited == inhibited) { + // Prefer correct inhibition state for matching + // input+group+locked + continue; + } + } + + *current_binding = binding; + if (strcmp((*current_binding)->input, input) == 0 && + (((*current_binding)->flags & BINDING_LOCKED) == locked) && + (((*current_binding)->flags & BINDING_INHIBITED) == inhibited) && + (*current_binding)->group == group) { + return; // If a perfect match is found, quit searching + } + } +} + +/** + * Execute a built-in, hardcoded compositor binding. These are triggered from a + * single keysym. + * + * Returns true if the keysym was handled by a binding and false if the event + * should be propagated to clients. + */ +static bool keyboard_execute_compositor_binding(struct sway_keyboard *keyboard, + const xkb_keysym_t *pressed_keysyms, uint32_t modifiers, size_t keysyms_len) { + for (size_t i = 0; i < keysyms_len; ++i) { + xkb_keysym_t keysym = pressed_keysyms[i]; + + if (keysym >= XKB_KEY_XF86Switch_VT_1 && + keysym <= XKB_KEY_XF86Switch_VT_12) { +#if WLR_HAS_SESSION + if (server.session) { + unsigned vt = keysym - XKB_KEY_XF86Switch_VT_1 + 1; + wlr_session_change_vt(server.session, vt); + } +#endif + return true; + } + } + + return false; +} + +static bool keyboard_execute_pointer_keysyms(struct sway_keyboard *keyboard, + uint32_t time, const xkb_keysym_t *pressed_keysyms, size_t keysyms_len, + enum wl_keyboard_key_state state) { + struct sway_cursor *cursor = keyboard->seat_device->sway_seat->cursor; + + for (size_t i = 0; i < keysyms_len; ++i) { + xkb_keysym_t keysym = pressed_keysyms[i]; + + uint32_t button = wlr_keyboard_keysym_to_pointer_button(keysym); + if (button != 0) { + dispatch_cursor_button(cursor, &keyboard->wlr->base, time, button, + (enum wl_pointer_button_state)state); + wlr_seat_pointer_notify_frame(cursor->seat->wlr_seat); + return true; + } + + int dx, dy; + wlr_keyboard_keysym_to_pointer_motion(keysym, &dx, &dy); + if (state == WL_KEYBOARD_KEY_STATE_PRESSED && (dx != 0 || dy != 0)) { + dx *= 10; + dy *= 10; + pointer_motion(cursor, time, &keyboard->wlr->base, dx, dy, dx, dy); + wlr_seat_pointer_notify_frame(cursor->seat->wlr_seat); + return true; + } + } + + return false; +} + +/** + * Get keysyms and modifiers from the keyboard as xkb sees them. + * + * This uses the xkb keysyms translation based on pressed modifiers and clears + * the consumed modifiers from the list of modifiers passed to keybind + * detection. + * + * On US layout, pressing Alt+Shift+2 will trigger Alt+@. + */ +static size_t keyboard_keysyms_translated(struct sway_keyboard *keyboard, + xkb_keycode_t keycode, const xkb_keysym_t **keysyms, + uint32_t *modifiers) { + *modifiers = wlr_keyboard_get_modifiers(keyboard->wlr); + xkb_mod_mask_t consumed = xkb_state_key_get_consumed_mods2( + keyboard->wlr->xkb_state, keycode, XKB_CONSUMED_MODE_XKB); + *modifiers = *modifiers & ~consumed; + + return xkb_state_key_get_syms(keyboard->wlr->xkb_state, + keycode, keysyms); +} + +/** + * Get keysyms and modifiers from the keyboard as if modifiers didn't change + * keysyms. + * + * This avoids the xkb keysym translation based on modifiers considered pressed + * in the state. + * + * This will trigger keybinds such as Alt+Shift+2. + */ +static size_t keyboard_keysyms_raw(struct sway_keyboard *keyboard, + xkb_keycode_t keycode, const xkb_keysym_t **keysyms, + uint32_t *modifiers) { + *modifiers = wlr_keyboard_get_modifiers(keyboard->wlr); + + xkb_layout_index_t layout_index = xkb_state_key_get_layout( + keyboard->wlr->xkb_state, keycode); + return xkb_keymap_key_get_syms_by_level(keyboard->wlr->keymap, + keycode, layout_index, 0, keysyms); +} + +void sway_keyboard_disarm_key_repeat(struct sway_keyboard *keyboard) { + if (!keyboard) { + return; + } + keyboard->repeat_binding = NULL; + if (wl_event_source_timer_update(keyboard->key_repeat_source, 0) < 0) { + sway_log(SWAY_DEBUG, "failed to disarm key repeat timer"); + } +} + +struct key_info { + uint32_t keycode; + uint32_t code_modifiers; + + const xkb_keysym_t *raw_keysyms; + uint32_t raw_modifiers; + size_t raw_keysyms_len; + + const xkb_keysym_t *translated_keysyms; + uint32_t translated_modifiers; + size_t translated_keysyms_len; +}; + +static void update_keyboard_state(struct sway_keyboard *keyboard, + uint32_t raw_keycode, enum wl_keyboard_key_state keystate, + struct key_info *keyinfo) { + // Identify new keycode, raw keysym(s), and translated keysym(s) + keyinfo->keycode = raw_keycode + 8; + + keyinfo->raw_keysyms_len = keyboard_keysyms_raw(keyboard, keyinfo->keycode, + &keyinfo->raw_keysyms, &keyinfo->raw_modifiers); + + keyinfo->translated_keysyms_len = keyboard_keysyms_translated(keyboard, + keyinfo->keycode, &keyinfo->translated_keysyms, + &keyinfo->translated_modifiers); + + keyinfo->code_modifiers = wlr_keyboard_get_modifiers(keyboard->wlr); + + // Update shortcut model keyinfo + update_shortcut_state(&keyboard->state_keycodes, raw_keycode, keystate, + keyinfo->keycode, keyinfo->code_modifiers); + for (size_t i = 0; i < keyinfo->raw_keysyms_len; ++i) { + update_shortcut_state(&keyboard->state_keysyms_raw, + raw_keycode, keystate, keyinfo->raw_keysyms[i], + keyinfo->code_modifiers); + } + for (size_t i = 0; i < keyinfo->translated_keysyms_len; ++i) { + update_shortcut_state(&keyboard->state_keysyms_translated, + raw_keycode, keystate, keyinfo->translated_keysyms[i], + keyinfo->code_modifiers); + } +} + +/** + * Get keyboard grab of the seat from sway_keyboard if we should forward events + * to it. + * + * Returns NULL if the keyboard is not grabbed by an input method, + * or if event is from virtual keyboard of the same client as grab. + * TODO: see swaywm/wlroots#2322 + */ +static struct wlr_input_method_keyboard_grab_v2 *keyboard_get_im_grab( + struct sway_keyboard *keyboard) { + struct wlr_input_method_v2 *input_method = keyboard->seat_device-> + sway_seat->im_relay.input_method; + struct wlr_virtual_keyboard_v1 *virtual_keyboard = + wlr_input_device_get_virtual_keyboard(keyboard->seat_device->input_device->wlr_device); + if (!input_method || !input_method->keyboard_grab || (virtual_keyboard && + wl_resource_get_client(virtual_keyboard->resource) == + wl_resource_get_client(input_method->keyboard_grab->resource))) { + return NULL; + } + return input_method->keyboard_grab; +} + +static void handle_key_event(struct sway_keyboard *keyboard, + struct wlr_keyboard_key_event *event) { + struct sway_seat *seat = keyboard->seat_device->sway_seat; + struct wlr_seat *wlr_seat = seat->wlr_seat; + struct wlr_input_device *wlr_device = + keyboard->seat_device->input_device->wlr_device; + char *device_identifier = input_device_get_identifier(wlr_device); + bool exact_identifier = keyboard->wlr->group != NULL; + seat_idle_notify_activity(seat, IDLE_SOURCE_KEYBOARD); + bool locked = server.session_lock.lock; + struct sway_keyboard_shortcuts_inhibitor *sway_inhibitor = + keyboard_shortcuts_inhibitor_get_for_focused_surface(seat); + bool shortcuts_inhibited = sway_inhibitor && sway_inhibitor->inhibitor->active; + + if (event->state == WL_KEYBOARD_KEY_STATE_PRESSED) { + cursor_notify_key_press(seat->cursor); + } + + // Identify new keycode, raw keysym(s), and translated keysym(s) + struct key_info keyinfo; + update_keyboard_state(keyboard, event->keycode, event->state, &keyinfo); + + bool handled = false; + // Identify active release binding + struct sway_binding *binding_released = NULL; + get_active_binding(&keyboard->state_keycodes, + config->current_mode->keycode_bindings, &binding_released, + keyinfo.code_modifiers, true, locked, + shortcuts_inhibited, device_identifier, + exact_identifier, keyboard->effective_layout); + get_active_binding(&keyboard->state_keysyms_raw, + config->current_mode->keysym_bindings, &binding_released, + keyinfo.raw_modifiers, true, locked, + shortcuts_inhibited, device_identifier, + exact_identifier, keyboard->effective_layout); + get_active_binding(&keyboard->state_keysyms_translated, + config->current_mode->keysym_bindings, &binding_released, + keyinfo.translated_modifiers, true, locked, + shortcuts_inhibited, device_identifier, + exact_identifier, keyboard->effective_layout); + + // Execute stored release binding once no longer active + if (keyboard->held_binding && binding_released != keyboard->held_binding && + event->state == WL_KEYBOARD_KEY_STATE_RELEASED) { + seat_execute_command(seat, keyboard->held_binding); + handled = true; + } + if (binding_released != keyboard->held_binding) { + keyboard->held_binding = NULL; + } + if (binding_released && event->state == WL_KEYBOARD_KEY_STATE_PRESSED) { + keyboard->held_binding = binding_released; + } + + // Identify and execute active pressed binding + struct sway_binding *binding = NULL; + if (event->state == WL_KEYBOARD_KEY_STATE_PRESSED) { + get_active_binding(&keyboard->state_keycodes, + config->current_mode->keycode_bindings, &binding, + keyinfo.code_modifiers, false, locked, + shortcuts_inhibited, device_identifier, + exact_identifier, keyboard->effective_layout); + get_active_binding(&keyboard->state_keysyms_raw, + config->current_mode->keysym_bindings, &binding, + keyinfo.raw_modifiers, false, locked, + shortcuts_inhibited, device_identifier, + exact_identifier, keyboard->effective_layout); + get_active_binding(&keyboard->state_keysyms_translated, + config->current_mode->keysym_bindings, &binding, + keyinfo.translated_modifiers, false, locked, + shortcuts_inhibited, device_identifier, + exact_identifier, keyboard->effective_layout); + } + + // Set up (or clear) keyboard repeat for a pressed binding. Since the + // binding may remove the keyboard, the timer needs to be updated first + if (binding && !(binding->flags & BINDING_NOREPEAT) && + keyboard->wlr->repeat_info.delay > 0) { + keyboard->repeat_binding = binding; + if (wl_event_source_timer_update(keyboard->key_repeat_source, + keyboard->wlr->repeat_info.delay) < 0) { + sway_log(SWAY_DEBUG, "failed to set key repeat timer"); + } + } else if (keyboard->repeat_binding) { + sway_keyboard_disarm_key_repeat(keyboard); + } + + if (binding) { + seat_execute_command(seat, binding); + handled = true; + } + + if (!handled && keyboard->wlr->group) { + // Only handle device specific bindings for keyboards in a group + free(device_identifier); + return; + } + + // Compositor bindings + if (!handled && event->state == WL_KEYBOARD_KEY_STATE_PRESSED) { + handled = keyboard_execute_compositor_binding( + keyboard, keyinfo.translated_keysyms, + keyinfo.translated_modifiers, keyinfo.translated_keysyms_len); + } + if (!handled && event->state == WL_KEYBOARD_KEY_STATE_PRESSED) { + handled = keyboard_execute_compositor_binding( + keyboard, keyinfo.raw_keysyms, keyinfo.raw_modifiers, + keyinfo.raw_keysyms_len); + } + if (!handled) { + handled = keyboard_execute_pointer_keysyms(keyboard, event->time_msec, + keyinfo.translated_keysyms, keyinfo.translated_keysyms_len, + event->state); + } + + if (event->state == WL_KEYBOARD_KEY_STATE_RELEASED) { + // If the pressed event was sent to a client and we have a focused + // surface immediately before this event, also send the released + // event. In particular, don't send the released event to the IM grab. + bool pressed_sent = update_shortcut_state( + &keyboard->state_pressed_sent, event->keycode, + event->state, keyinfo.keycode, 0); + if (pressed_sent && seat->wlr_seat->keyboard_state.focused_surface) { + wlr_seat_set_keyboard(wlr_seat, keyboard->wlr); + wlr_seat_keyboard_notify_key(wlr_seat, event->time_msec, + event->keycode, event->state); + handled = true; + } + } + + if (!handled) { + struct wlr_input_method_keyboard_grab_v2 *kb_grab = keyboard_get_im_grab(keyboard); + + if (kb_grab) { + wlr_input_method_keyboard_grab_v2_set_keyboard(kb_grab, keyboard->wlr); + wlr_input_method_keyboard_grab_v2_send_key(kb_grab, + event->time_msec, event->keycode, event->state); + handled = true; + } + } + + if (!handled && event->state != WL_KEYBOARD_KEY_STATE_RELEASED) { + // If a released event failed pressed sent test, and not in sent to + // keyboard grab, it is still not handled. Don't handle released here. + update_shortcut_state( + &keyboard->state_pressed_sent, event->keycode, event->state, + keyinfo.keycode, 0); + wlr_seat_set_keyboard(wlr_seat, keyboard->wlr); + wlr_seat_keyboard_notify_key(wlr_seat, event->time_msec, + event->keycode, event->state); + } + + free(device_identifier); +} + +static void handle_keyboard_key(struct wl_listener *listener, void *data) { + struct sway_keyboard *keyboard = + wl_container_of(listener, keyboard, keyboard_key); + handle_key_event(keyboard, data); +} + +static void handle_keyboard_group_key(struct wl_listener *listener, + void *data) { + struct sway_keyboard_group *sway_group = + wl_container_of(listener, sway_group, keyboard_key); + handle_key_event(sway_group->seat_device->keyboard, data); +} + +static void handle_keyboard_group_enter(struct wl_listener *listener, + void *data) { + struct sway_keyboard_group *sway_group = + wl_container_of(listener, sway_group, enter); + struct sway_keyboard *keyboard = sway_group->seat_device->keyboard; + struct wl_array *keycodes = data; + + uint32_t *keycode; + wl_array_for_each(keycode, keycodes) { + struct key_info keyinfo; + update_keyboard_state(keyboard, *keycode, WL_KEYBOARD_KEY_STATE_PRESSED, &keyinfo); + } +} + +static void handle_keyboard_group_leave(struct wl_listener *listener, + void *data) { + struct sway_keyboard_group *sway_group = + wl_container_of(listener, sway_group, leave); + struct sway_keyboard *keyboard = sway_group->seat_device->keyboard; + struct wl_array *keycodes = data; + + bool pressed_sent = false; + + uint32_t *keycode; + wl_array_for_each(keycode, keycodes) { + struct key_info keyinfo; + update_keyboard_state(keyboard, *keycode, WL_KEYBOARD_KEY_STATE_RELEASED, &keyinfo); + + pressed_sent |= update_shortcut_state(&keyboard->state_pressed_sent, + *keycode, WL_KEYBOARD_KEY_STATE_RELEASED, keyinfo.keycode, 0); + } + + if (!pressed_sent) { + return; + } + + // Refocus the focused node, layer surface, or unmanaged surface so that + // it picks up the current keyboard state. + struct sway_seat *seat = keyboard->seat_device->sway_seat; + struct sway_node *focus = seat_get_focus(seat); + if (focus) { + seat_set_focus(seat, NULL); + seat_set_focus(seat, focus); + } else if (seat->focused_layer) { + struct wlr_layer_surface_v1 *layer = seat->focused_layer; + seat_set_focus_layer(seat, NULL); + seat_set_focus_layer(seat, layer); + } else { + struct wlr_surface *unmanaged = seat->wlr_seat->keyboard_state.focused_surface; + seat_set_focus_surface(seat, NULL, false); + seat_set_focus_surface(seat, unmanaged, false); + } +} + +static int handle_keyboard_repeat(void *data) { + struct sway_keyboard *keyboard = data; + if (keyboard->repeat_binding) { + if (keyboard->wlr->repeat_info.rate > 0) { + // We queue the next event first, as the command might cancel it + if (wl_event_source_timer_update(keyboard->key_repeat_source, + 1000 / keyboard->wlr->repeat_info.rate) < 0) { + sway_log(SWAY_DEBUG, "failed to update key repeat timer"); + } + } + + seat_execute_command(keyboard->seat_device->sway_seat, + keyboard->repeat_binding); + } + return 0; +} + +static void determine_bar_visibility(uint32_t modifiers) { + for (int i = 0; i < config->bars->length; ++i) { + struct bar_config *bar = config->bars->items[i]; + if (bar->modifier == 0) { + continue; + } + + bool vis_by_mod = (~modifiers & bar->modifier) == 0; + if (bar->visible_by_modifier != vis_by_mod) { + // If visible by modifier is set, send that it is no longer visible + // by modifier (regardless of bar mode and state). Otherwise, only + // send the visible by modifier status if mode and state are hide + if (bar->visible_by_modifier + || strcmp(bar->mode, bar->hidden_state) == 0) { + bar->visible_by_modifier = vis_by_mod; + ipc_event_bar_state_update(bar); + } + } + } +} + +static void handle_modifier_event(struct sway_keyboard *keyboard) { + if (!keyboard->wlr->group) { + struct wlr_input_method_keyboard_grab_v2 *kb_grab = keyboard_get_im_grab(keyboard); + + if (kb_grab) { + wlr_input_method_keyboard_grab_v2_set_keyboard(kb_grab, keyboard->wlr); + wlr_input_method_keyboard_grab_v2_send_modifiers(kb_grab, + &keyboard->wlr->modifiers); + } else { + struct wlr_seat *wlr_seat = keyboard->seat_device->sway_seat->wlr_seat; + wlr_seat_set_keyboard(wlr_seat, keyboard->wlr); + wlr_seat_keyboard_notify_modifiers(wlr_seat, + &keyboard->wlr->modifiers); + } + + uint32_t modifiers = wlr_keyboard_get_modifiers(keyboard->wlr); + determine_bar_visibility(modifiers); + } + + if (keyboard->wlr->modifiers.group != keyboard->effective_layout) { + keyboard->effective_layout = keyboard->wlr->modifiers.group; + + if (!wlr_keyboard_group_from_wlr_keyboard(keyboard->wlr)) { + ipc_event_input("xkb_layout", keyboard->seat_device->input_device); + } + } +} + +static void handle_keyboard_modifiers(struct wl_listener *listener, + void *data) { + struct sway_keyboard *keyboard = + wl_container_of(listener, keyboard, keyboard_modifiers); + handle_modifier_event(keyboard); +} + +static void handle_keyboard_group_modifiers(struct wl_listener *listener, + void *data) { + struct sway_keyboard_group *group = + wl_container_of(listener, group, keyboard_modifiers); + handle_modifier_event(group->seat_device->keyboard); +} + +struct sway_keyboard *sway_keyboard_create(struct sway_seat *seat, + struct sway_seat_device *device) { + struct sway_keyboard *keyboard = + calloc(1, sizeof(struct sway_keyboard)); + if (!sway_assert(keyboard, "could not allocate sway keyboard")) { + return NULL; + } + + keyboard->seat_device = device; + keyboard->wlr = wlr_keyboard_from_input_device(device->input_device->wlr_device); + device->keyboard = keyboard; + + wl_list_init(&keyboard->keyboard_key.link); + wl_list_init(&keyboard->keyboard_modifiers.link); + + keyboard->key_repeat_source = wl_event_loop_add_timer(server.wl_event_loop, + handle_keyboard_repeat, keyboard); + + return keyboard; +} + +static void handle_xkb_context_log(struct xkb_context *context, + enum xkb_log_level level, const char *format, va_list args) { + char *error = vformat_str(format, args); + + size_t len = strlen(error); + if (error[len - 1] == '\n') { + error[len - 1] = '\0'; + } + + sway_log_importance_t importance = SWAY_DEBUG; + if (level <= XKB_LOG_LEVEL_ERROR) { // Critical and Error + importance = SWAY_ERROR; + } else if (level <= XKB_LOG_LEVEL_INFO) { // Warning and Info + importance = SWAY_INFO; + } + sway_log(importance, "[xkbcommon] %s", error); + + char **data = xkb_context_get_user_data(context); + if (importance == SWAY_ERROR && data && !*data) { + *data = error; + } else { + free(error); + } +} + +struct xkb_keymap *sway_keyboard_compile_keymap(struct input_config *ic, + char **error) { + struct xkb_context *context = xkb_context_new(XKB_CONTEXT_NO_SECURE_GETENV); + if (!sway_assert(context, "cannot create XKB context")) { + return NULL; + } + xkb_context_set_user_data(context, error); + xkb_context_set_log_fn(context, handle_xkb_context_log); + + struct xkb_keymap *keymap = NULL; + + if (ic && ic->xkb_file) { + FILE *keymap_file = fopen(ic->xkb_file, "r"); + if (!keymap_file) { + sway_log_errno(SWAY_ERROR, "cannot read xkb file %s", ic->xkb_file); + if (error) { + *error = format_str("cannot read xkb file %s: %s", + ic->xkb_file, strerror(errno)); + } + goto cleanup; + } + + keymap = xkb_keymap_new_from_file(context, keymap_file, + XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS); + + if (fclose(keymap_file) != 0) { + sway_log_errno(SWAY_ERROR, "Failed to close xkb file %s", + ic->xkb_file); + } + } else { + struct xkb_rule_names rules = {0}; + if (ic) { + input_config_fill_rule_names(ic, &rules); + } + keymap = xkb_keymap_new_from_names(context, &rules, + XKB_KEYMAP_COMPILE_NO_FLAGS); + } + +cleanup: + xkb_context_set_user_data(context, NULL); + xkb_context_unref(context); + return keymap; +} + +static bool repeat_info_match(struct sway_keyboard *a, struct wlr_keyboard *b) { + return a->repeat_rate == b->repeat_info.rate && + a->repeat_delay == b->repeat_info.delay; +} + +static void destroy_empty_wlr_keyboard_group(void *data) { + wlr_keyboard_group_destroy(data); +} + +static void sway_keyboard_group_remove(struct sway_keyboard *keyboard) { + struct sway_input_device *device = keyboard->seat_device->input_device; + struct wlr_keyboard_group *wlr_group = keyboard->wlr->group; + + sway_log(SWAY_DEBUG, "Removing keyboard %s from group %p", + device->identifier, wlr_group); + + wlr_keyboard_group_remove_keyboard(keyboard->wlr->group, keyboard->wlr); + + if (wl_list_empty(&wlr_group->devices)) { + sway_log(SWAY_DEBUG, "Destroying empty keyboard group %p", + wlr_group); + struct sway_keyboard_group *sway_group = wlr_group->data; + wlr_group->data = NULL; + wl_list_remove(&sway_group->link); + wl_list_remove(&sway_group->keyboard_key.link); + wl_list_remove(&sway_group->keyboard_modifiers.link); + wl_list_remove(&sway_group->enter.link); + wl_list_remove(&sway_group->leave.link); + sway_keyboard_destroy(sway_group->seat_device->keyboard); + free(sway_group->seat_device->input_device); + free(sway_group->seat_device); + free(sway_group); + + // To prevent use-after-free conditions when handling key events, defer + // freeing the wlr_keyboard_group until idle + wl_event_loop_add_idle(server.wl_event_loop, + destroy_empty_wlr_keyboard_group, wlr_group); + } +} + +static void sway_keyboard_group_remove_invalid(struct sway_keyboard *keyboard) { + if (!keyboard->wlr->group) { + return; + } + + struct sway_seat *seat = keyboard->seat_device->sway_seat; + struct seat_config *sc = seat_get_config(seat); + if (!sc) { + sc = seat_get_config_by_name("*"); + } + + switch (sc ? sc->keyboard_grouping : KEYBOARD_GROUP_DEFAULT) { + case KEYBOARD_GROUP_NONE: + sway_keyboard_group_remove(keyboard); + break; + case KEYBOARD_GROUP_DEFAULT: /* fallthrough */ + case KEYBOARD_GROUP_SMART:; + struct wlr_keyboard_group *group = keyboard->wlr->group; + if (!wlr_keyboard_keymaps_match(keyboard->keymap, group->keyboard.keymap) || + !repeat_info_match(keyboard, &group->keyboard)) { + sway_keyboard_group_remove(keyboard); + } + break; + } +} + +static void sway_keyboard_group_add(struct sway_keyboard *keyboard) { + struct sway_input_device *device = keyboard->seat_device->input_device; + struct sway_seat *seat = keyboard->seat_device->sway_seat; + struct seat_config *sc = seat_get_config(seat); + + if (device->is_virtual) { + // Virtual devices should not be grouped + return; + } + + if (!sc) { + sc = seat_get_config_by_name("*"); + } + + if (sc && sc->keyboard_grouping == KEYBOARD_GROUP_NONE) { + // Keyboard grouping is disabled for the seat + return; + } + + struct sway_keyboard_group *group; + wl_list_for_each(group, &seat->keyboard_groups, link) { + switch (sc ? sc->keyboard_grouping : KEYBOARD_GROUP_DEFAULT) { + case KEYBOARD_GROUP_NONE: + // Nothing to do. This shouldn't even be reached + return; + case KEYBOARD_GROUP_DEFAULT: /* fallthrough */ + case KEYBOARD_GROUP_SMART:; + struct wlr_keyboard_group *wlr_group = group->wlr_group; + if (wlr_keyboard_keymaps_match(keyboard->keymap, + wlr_group->keyboard.keymap) && + repeat_info_match(keyboard, &wlr_group->keyboard)) { + sway_log(SWAY_DEBUG, "Adding keyboard %s to group %p", + device->identifier, wlr_group); + wlr_keyboard_group_add_keyboard(wlr_group, keyboard->wlr); + return; + } + break; + } + } + + struct sway_keyboard_group *sway_group = + calloc(1, sizeof(struct sway_keyboard_group)); + if (!sway_group) { + sway_log(SWAY_ERROR, "Failed to allocate sway_keyboard_group"); + return; + } + + sway_group->wlr_group = wlr_keyboard_group_create(); + if (!sway_group->wlr_group) { + sway_log(SWAY_ERROR, "Failed to create keyboard group"); + goto cleanup; + } + sway_group->wlr_group->data = sway_group; + wlr_keyboard_set_keymap(&sway_group->wlr_group->keyboard, keyboard->keymap); + wlr_keyboard_set_repeat_info(&sway_group->wlr_group->keyboard, + keyboard->repeat_rate, keyboard->repeat_delay); + sway_log(SWAY_DEBUG, "Created keyboard group %p", sway_group->wlr_group); + + sway_group->seat_device = calloc(1, sizeof(struct sway_seat_device)); + if (!sway_group->seat_device) { + sway_log(SWAY_ERROR, "Failed to allocate sway_seat_device for group"); + goto cleanup; + } + sway_group->seat_device->sway_seat = seat; + + sway_group->seat_device->input_device = + calloc(1, sizeof(struct sway_input_device)); + if (!sway_group->seat_device->input_device) { + sway_log(SWAY_ERROR, "Failed to allocate sway_input_device for group"); + goto cleanup; + } + sway_group->seat_device->input_device->wlr_device = + &sway_group->wlr_group->keyboard.base; + + if (!sway_keyboard_create(seat, sway_group->seat_device)) { + sway_log(SWAY_ERROR, "Failed to allocate sway_keyboard for group"); + goto cleanup; + } + + sway_log(SWAY_DEBUG, "Adding keyboard %s to group %p", + device->identifier, sway_group->wlr_group); + wlr_keyboard_group_add_keyboard(sway_group->wlr_group, keyboard->wlr); + + wl_list_insert(&seat->keyboard_groups, &sway_group->link); + + wl_signal_add(&sway_group->wlr_group->keyboard.events.key, + &sway_group->keyboard_key); + sway_group->keyboard_key.notify = handle_keyboard_group_key; + + wl_signal_add(&sway_group->wlr_group->keyboard.events.modifiers, + &sway_group->keyboard_modifiers); + sway_group->keyboard_modifiers.notify = handle_keyboard_group_modifiers; + + wl_signal_add(&sway_group->wlr_group->events.enter, &sway_group->enter); + sway_group->enter.notify = handle_keyboard_group_enter; + + wl_signal_add(&sway_group->wlr_group->events.leave, &sway_group->leave); + sway_group->leave.notify = handle_keyboard_group_leave; + return; + +cleanup: + if (sway_group && sway_group->wlr_group) { + wlr_keyboard_group_destroy(sway_group->wlr_group); + } + free(sway_group->seat_device->keyboard); + free(sway_group->seat_device->input_device); + free(sway_group->seat_device); + free(sway_group); +} + +static void sway_keyboard_set_layout(struct sway_keyboard *keyboard, + struct input_config *input_config) { + struct xkb_keymap *keymap = sway_keyboard_compile_keymap(input_config, NULL); + if (!keymap) { + sway_log(SWAY_ERROR, "Failed to compile keymap. Attempting defaults"); + keymap = sway_keyboard_compile_keymap(NULL, NULL); + if (!keymap) { + sway_log(SWAY_ERROR, + "Failed to compile default keymap. Aborting configure"); + return; + } + } + + bool keymap_changed = keyboard->keymap ? + !wlr_keyboard_keymaps_match(keyboard->keymap, keymap) : true; + bool effective_layout_changed = keyboard->effective_layout != 0; + + if (keymap_changed || config->reloading) { + xkb_keymap_unref(keyboard->keymap); + keyboard->keymap = keymap; + keyboard->effective_layout = 0; + + sway_keyboard_group_remove_invalid(keyboard); + wlr_keyboard_set_keymap(keyboard->wlr, keyboard->keymap); + if (!keyboard->wlr->group) { + sway_keyboard_group_add(keyboard); + } + + xkb_mod_mask_t locked_mods = 0; + if (input_config && input_config->xkb_numlock > 0) { + xkb_mod_index_t mod_index = xkb_map_mod_get_index(keymap, + XKB_MOD_NAME_NUM); + if (mod_index != XKB_MOD_INVALID) { + locked_mods |= (uint32_t)1 << mod_index; + } + } + if (input_config && input_config->xkb_capslock > 0) { + xkb_mod_index_t mod_index = xkb_map_mod_get_index(keymap, + XKB_MOD_NAME_CAPS); + if (mod_index != XKB_MOD_INVALID) { + locked_mods |= (uint32_t)1 << mod_index; + } + } + if (locked_mods) { + wlr_keyboard_notify_modifiers(keyboard->wlr, 0, 0, + locked_mods, 0); + uint32_t leds = 0; + for (uint32_t i = 0; i < WLR_LED_COUNT; ++i) { + if (xkb_state_led_index_is_active(keyboard->wlr->xkb_state, + keyboard->wlr->led_indexes[i])) { + leds |= (1 << i); + } + } + if (keyboard->wlr->group) { + wlr_keyboard_led_update(&keyboard->wlr->group->keyboard, leds); + } else { + wlr_keyboard_led_update(keyboard->wlr, leds); + } + } + } else { + xkb_keymap_unref(keymap); + sway_keyboard_group_remove_invalid(keyboard); + if (!keyboard->wlr->group) { + sway_keyboard_group_add(keyboard); + } + } + + if (keymap_changed) { + ipc_event_input("xkb_keymap", + keyboard->seat_device->input_device); + } else if (effective_layout_changed) { + ipc_event_input("xkb_layout", + keyboard->seat_device->input_device); + } +} + +void sway_keyboard_configure(struct sway_keyboard *keyboard) { + struct input_config *input_config = + input_device_get_config(keyboard->seat_device->input_device); + + if (!sway_assert(!wlr_keyboard_group_from_wlr_keyboard(keyboard->wlr), + "sway_keyboard_configure should not be called with a " + "keyboard group's keyboard")) { + return; + } + + int repeat_rate = 25; + if (input_config && input_config->repeat_rate != INT_MIN) { + repeat_rate = input_config->repeat_rate; + } + int repeat_delay = 600; + if (input_config && input_config->repeat_delay != INT_MIN) { + repeat_delay = input_config->repeat_delay; + } + + bool repeat_info_changed = keyboard->repeat_rate != repeat_rate || + keyboard->repeat_delay != repeat_delay; + + if (repeat_info_changed || config->reloading) { + keyboard->repeat_rate = repeat_rate; + keyboard->repeat_delay = repeat_delay; + + wlr_keyboard_set_repeat_info(keyboard->wlr, + keyboard->repeat_rate, keyboard->repeat_delay); + } + + if (!keyboard->seat_device->input_device->is_virtual) { + sway_keyboard_set_layout(keyboard, input_config); + } + + // If the seat has no active keyboard, set this one + struct wlr_seat *seat = keyboard->seat_device->sway_seat->wlr_seat; + struct wlr_keyboard *current_keyboard = seat->keyboard_state.keyboard; + if (current_keyboard == NULL) { + wlr_seat_set_keyboard(seat, keyboard->wlr); + } + + wl_list_remove(&keyboard->keyboard_key.link); + wl_signal_add(&keyboard->wlr->events.key, &keyboard->keyboard_key); + keyboard->keyboard_key.notify = handle_keyboard_key; + + wl_list_remove(&keyboard->keyboard_modifiers.link); + wl_signal_add(&keyboard->wlr->events.modifiers, + &keyboard->keyboard_modifiers); + keyboard->keyboard_modifiers.notify = handle_keyboard_modifiers; + +} + +void sway_keyboard_destroy(struct sway_keyboard *keyboard) { + if (!keyboard) { + return; + } + if (keyboard->wlr->group) { + sway_keyboard_group_remove(keyboard); + } + struct wlr_seat *wlr_seat = keyboard->seat_device->sway_seat->wlr_seat; + if (wlr_seat_get_keyboard(wlr_seat) == keyboard->wlr) { + wlr_seat_set_keyboard(wlr_seat, NULL); + } + if (keyboard->keymap) { + xkb_keymap_unref(keyboard->keymap); + } + wl_list_remove(&keyboard->keyboard_key.link); + wl_list_remove(&keyboard->keyboard_modifiers.link); + sway_keyboard_disarm_key_repeat(keyboard); + wl_event_source_remove(keyboard->key_repeat_source); + free(keyboard); +} diff --git a/sway/input/libinput.c b/sway/input/libinput.c new file mode 100644 index 0000000..7a5fded --- /dev/null +++ b/sway/input/libinput.c @@ -0,0 +1,427 @@ +#include +#include +#include +#include +#include +#include "log.h" +#include "sway/config.h" +#include "sway/output.h" +#include "sway/input/input-manager.h" +#include "sway/ipc-server.h" + +static void log_status(enum libinput_config_status status) { + if (status != LIBINPUT_CONFIG_STATUS_SUCCESS) { + sway_log(SWAY_ERROR, "Failed to apply libinput config: %s", + libinput_config_status_to_str(status)); + } +} + +static bool set_send_events(struct libinput_device *device, uint32_t mode) { + if (libinput_device_config_send_events_get_mode(device) == mode) { + return false; + } + sway_log(SWAY_DEBUG, "send_events_set_mode(%" PRIu32 ")", mode); + log_status(libinput_device_config_send_events_set_mode(device, mode)); + return true; +} + +static bool set_tap(struct libinput_device *device, + enum libinput_config_tap_state tap) { + if (libinput_device_config_tap_get_finger_count(device) <= 0 || + libinput_device_config_tap_get_enabled(device) == tap) { + return false; + } + sway_log(SWAY_DEBUG, "tap_set_enabled(%d)", tap); + log_status(libinput_device_config_tap_set_enabled(device, tap)); + return true; +} + +static bool set_tap_button_map(struct libinput_device *device, + enum libinput_config_tap_button_map map) { + if (libinput_device_config_tap_get_finger_count(device) <= 0 || + libinput_device_config_tap_get_button_map(device) == map) { + return false; + } + sway_log(SWAY_DEBUG, "tap_set_button_map(%d)", map); + log_status(libinput_device_config_tap_set_button_map(device, map)); + return true; +} + +static bool set_tap_drag(struct libinput_device *device, + enum libinput_config_drag_state drag) { + if (libinput_device_config_tap_get_finger_count(device) <= 0 || + libinput_device_config_tap_get_drag_enabled(device) == drag) { + return false; + } + sway_log(SWAY_DEBUG, "tap_set_drag_enabled(%d)", drag); + log_status(libinput_device_config_tap_set_drag_enabled(device, drag)); + return true; +} + +static bool set_tap_drag_lock(struct libinput_device *device, + enum libinput_config_drag_lock_state lock) { + if (libinput_device_config_tap_get_finger_count(device) <= 0 || + libinput_device_config_tap_get_drag_lock_enabled(device) == lock) { + return false; + } + sway_log(SWAY_DEBUG, "tap_set_drag_lock_enabled(%d)", lock); + log_status(libinput_device_config_tap_set_drag_lock_enabled(device, lock)); + return true; +} + +static bool set_accel_speed(struct libinput_device *device, double speed) { + if (!libinput_device_config_accel_is_available(device) || + libinput_device_config_accel_get_speed(device) == speed) { + return false; + } + sway_log(SWAY_DEBUG, "accel_set_speed(%f)", speed); + log_status(libinput_device_config_accel_set_speed(device, speed)); + return true; +} + +static bool set_rotation_angle(struct libinput_device *device, double angle) { + if (!libinput_device_config_rotation_is_available(device) || + libinput_device_config_rotation_get_angle(device) == angle) { + return false; + } + sway_log(SWAY_DEBUG, "rotation_set_angle(%f)", angle); + log_status(libinput_device_config_rotation_set_angle(device, angle)); + return true; +} + +static bool set_accel_profile(struct libinput_device *device, + enum libinput_config_accel_profile profile) { + if (!libinput_device_config_accel_is_available(device) || + libinput_device_config_accel_get_profile(device) == profile) { + return false; + } + sway_log(SWAY_DEBUG, "accel_set_profile(%d)", profile); + log_status(libinput_device_config_accel_set_profile(device, profile)); + return true; +} + +static bool set_natural_scroll(struct libinput_device *d, bool n) { + if (!libinput_device_config_scroll_has_natural_scroll(d) || + libinput_device_config_scroll_get_natural_scroll_enabled(d) == n) { + return false; + } + sway_log(SWAY_DEBUG, "scroll_set_natural_scroll(%d)", n); + log_status(libinput_device_config_scroll_set_natural_scroll_enabled(d, n)); + return true; +} + +static bool set_left_handed(struct libinput_device *device, bool left) { + if (!libinput_device_config_left_handed_is_available(device) || + libinput_device_config_left_handed_get(device) == left) { + return false; + } + sway_log(SWAY_DEBUG, "left_handed_set(%d)", left); + log_status(libinput_device_config_left_handed_set(device, left)); + return true; +} + +static bool set_click_method(struct libinput_device *device, + enum libinput_config_click_method method) { + uint32_t click = libinput_device_config_click_get_methods(device); + if ((click & ~LIBINPUT_CONFIG_CLICK_METHOD_NONE) == 0 || + libinput_device_config_click_get_method(device) == method) { + return false; + } + sway_log(SWAY_DEBUG, "click_set_method(%d)", method); + log_status(libinput_device_config_click_set_method(device, method)); + return true; +} + +static bool set_clickfinger_button_map(struct libinput_device *device, + enum libinput_config_clickfinger_button_map map) { + if (libinput_device_config_click_get_clickfinger_button_map(device) == map) { + return false; + } + sway_log(SWAY_DEBUG, "clickfinger_set_button_map(%d)", map); + log_status(libinput_device_config_click_set_clickfinger_button_map(device, map)); + return true; +} + +static bool set_middle_emulation(struct libinput_device *dev, + enum libinput_config_middle_emulation_state mid) { + if (!libinput_device_config_middle_emulation_is_available(dev) || + libinput_device_config_middle_emulation_get_enabled(dev) == mid) { + return false; + } + sway_log(SWAY_DEBUG, "middle_emulation_set_enabled(%d)", mid); + log_status(libinput_device_config_middle_emulation_set_enabled(dev, mid)); + return true; +} + +static bool set_scroll_method(struct libinput_device *device, + enum libinput_config_scroll_method method) { + uint32_t scroll = libinput_device_config_scroll_get_methods(device); + if ((scroll & ~LIBINPUT_CONFIG_SCROLL_NO_SCROLL) == 0 || + libinput_device_config_scroll_get_method(device) == method) { + return false; + } + sway_log(SWAY_DEBUG, "scroll_set_method(%d)", method); + log_status(libinput_device_config_scroll_set_method(device, method)); + return true; +} + +static bool set_scroll_button(struct libinput_device *dev, uint32_t button) { + uint32_t scroll = libinput_device_config_scroll_get_methods(dev); + if ((scroll & ~LIBINPUT_CONFIG_SCROLL_NO_SCROLL) == 0 || + libinput_device_config_scroll_get_button(dev) == button) { + return false; + } + sway_log(SWAY_DEBUG, "scroll_set_button(%" PRIu32 ")", button); + log_status(libinput_device_config_scroll_set_button(dev, button)); + return true; +} + +static bool set_scroll_button_lock(struct libinput_device *dev, + enum libinput_config_scroll_button_lock_state lock) { + uint32_t scroll = libinput_device_config_scroll_get_methods(dev); + if ((scroll & ~LIBINPUT_CONFIG_SCROLL_NO_SCROLL) == 0 || + libinput_device_config_scroll_get_button_lock(dev) == lock) { + return false; + } + sway_log(SWAY_DEBUG, "scroll_set_button_lock(%" PRIu32 ")", lock); + log_status(libinput_device_config_scroll_set_button_lock(dev, lock)); + return true; +} + +static bool set_dwt(struct libinput_device *device, bool dwt) { + if (!libinput_device_config_dwt_is_available(device) || + libinput_device_config_dwt_get_enabled(device) == dwt) { + return false; + } + sway_log(SWAY_DEBUG, "dwt_set_enabled(%d)", dwt); + log_status(libinput_device_config_dwt_set_enabled(device, dwt)); + return true; +} + +static bool set_dwtp(struct libinput_device *device, bool dwtp) { + if (!libinput_device_config_dwtp_is_available(device) || + libinput_device_config_dwtp_get_enabled(device) == dwtp) { + return false; + } + sway_log(SWAY_DEBUG, "dwtp_set_enabled(%d)", dwtp); + log_status(libinput_device_config_dwtp_set_enabled(device, dwtp)); + return true; +} + +static bool set_calibration_matrix(struct libinput_device *dev, float mat[6]) { + if (!libinput_device_config_calibration_has_matrix(dev)) { + return false; + } + bool changed = false; + float current[6]; + libinput_device_config_calibration_get_matrix(dev, current); + for (int i = 0; i < 6; i++) { + if (current[i] != mat[i]) { + changed = true; + break; + } + } + if (changed) { + sway_log(SWAY_DEBUG, "calibration_set_matrix(%f, %f, %f, %f, %f, %f)", + mat[0], mat[1], mat[2], mat[3], mat[4], mat[5]); + log_status(libinput_device_config_calibration_set_matrix(dev, mat)); + } + return changed; +} + +static bool configure_send_events(struct libinput_device *device, + struct input_config *ic) { + if (ic->mapped_to_output && + strcmp("*", ic->mapped_to_output) != 0 && + !output_by_name_or_id(ic->mapped_to_output)) { + sway_log(SWAY_DEBUG, + "%s '%s' is mapped to offline output '%s'; disabling input", + ic->input_type, ic->identifier, ic->mapped_to_output); + return set_send_events(device, LIBINPUT_CONFIG_SEND_EVENTS_DISABLED); + } else if (ic->send_events != INT_MIN) { + return set_send_events(device, ic->send_events); + } else { + // Have to reset to the default mode here, otherwise if ic->send_events + // is unset and a mapped output just came online after being disabled, + // we'd remain stuck sending no events. + return set_send_events(device, + libinput_device_config_send_events_get_default_mode(device)); + } +} + +bool sway_input_configure_libinput_device(struct sway_input_device *input_device) { + struct input_config *ic = input_device_get_config(input_device); + if (!ic || !wlr_input_device_is_libinput(input_device->wlr_device)) { + return false; + } + + struct libinput_device *device = + wlr_libinput_get_device_handle(input_device->wlr_device); + sway_log(SWAY_DEBUG, "sway_input_configure_libinput_device('%s' on '%s')", + ic->identifier, input_device->identifier); + + bool changed = configure_send_events(device, ic); + if (ic->tap != INT_MIN) { + changed |= set_tap(device, ic->tap); + } + if (ic->tap_button_map != INT_MIN) { + changed |= set_tap_button_map(device, ic->tap_button_map); + } + if (ic->drag != INT_MIN) { + changed |= set_tap_drag(device, ic->drag); + } + if (ic->drag_lock != INT_MIN) { + changed |= set_tap_drag_lock(device, ic->drag_lock); + } + if (ic->pointer_accel != FLT_MIN) { + changed |= set_accel_speed(device, ic->pointer_accel); + } + if (ic->rotation_angle != FLT_MIN) { + changed |= set_rotation_angle(device, ic->rotation_angle); + } + if (ic->accel_profile != INT_MIN) { + changed |= set_accel_profile(device, ic->accel_profile); + } + if (ic->natural_scroll != INT_MIN) { + changed |= set_natural_scroll(device, ic->natural_scroll); + } + if (ic->left_handed != INT_MIN) { + changed |= set_left_handed(device, ic->left_handed); + } + if (ic->click_method != INT_MIN) { + changed |= set_click_method(device, ic->click_method); + } + if (ic->clickfinger_button_map != INT_MIN) { + changed |= set_clickfinger_button_map(device, ic->clickfinger_button_map); + } + if (ic->middle_emulation != INT_MIN) { + changed |= set_middle_emulation(device, ic->middle_emulation); + } + if (ic->scroll_method != INT_MIN) { + changed |= set_scroll_method(device, ic->scroll_method); + } + if (ic->scroll_button != INT_MIN) { + changed |= set_scroll_button(device, ic->scroll_button); + } + if (ic->scroll_button_lock != INT_MIN) { + changed |= set_scroll_button_lock(device, ic->scroll_button_lock); + } + if (ic->dwt != INT_MIN) { + changed |= set_dwt(device, ic->dwt); + } + if (ic->dwtp != INT_MIN) { + changed |= set_dwtp(device, ic->dwtp); + } + if (ic->calibration_matrix.configured) { + changed |= set_calibration_matrix(device, ic->calibration_matrix.matrix); + } + + return changed; +} + +void sway_input_configure_libinput_device_send_events( + struct sway_input_device *input_device) { + struct input_config *ic = input_device_get_config(input_device); + if (!ic || !wlr_input_device_is_libinput(input_device->wlr_device)) { + return; + } + + struct libinput_device *device = + wlr_libinput_get_device_handle(input_device->wlr_device); + bool changed = configure_send_events(device, ic); + + if (changed) { + ipc_event_input("libinput_config", input_device); + } +} + +void sway_input_reset_libinput_device(struct sway_input_device *input_device) { + if (!wlr_input_device_is_libinput(input_device->wlr_device)) { + return; + } + + struct libinput_device *device = + wlr_libinput_get_device_handle(input_device->wlr_device); + sway_log(SWAY_DEBUG, "sway_input_reset_libinput_device(%s)", + input_device->identifier); + bool changed = false; + + changed |= set_send_events(device, + libinput_device_config_send_events_get_default_mode(device)); + changed |= set_tap(device, + libinput_device_config_tap_get_default_enabled(device)); + changed |= set_tap_button_map(device, + libinput_device_config_tap_get_default_button_map(device)); + changed |= set_tap_drag(device, + libinput_device_config_tap_get_default_drag_enabled(device)); +#if HAVE_LIBINPUT_CONFIG_DRAG_LOCK_ENABLED_STICKY + changed |= set_tap_drag_lock(device, LIBINPUT_CONFIG_DRAG_LOCK_ENABLED_STICKY); +#else + changed |= set_tap_drag_lock(device, + libinput_device_config_tap_get_default_drag_lock_enabled(device)); +#endif + changed |= set_accel_speed(device, + libinput_device_config_accel_get_default_speed(device)); + changed |= set_rotation_angle(device, + libinput_device_config_rotation_get_default_angle(device)); + changed |= set_accel_profile(device, + libinput_device_config_accel_get_default_profile(device)); + changed |= set_natural_scroll(device, + libinput_device_config_scroll_get_default_natural_scroll_enabled( + device)); + changed |= set_left_handed(device, + libinput_device_config_left_handed_get_default(device)); + changed |= set_click_method(device, + libinput_device_config_click_get_default_method(device)); + changed |= set_clickfinger_button_map(device, + libinput_device_config_click_get_default_clickfinger_button_map(device)); + changed |= set_middle_emulation(device, + libinput_device_config_middle_emulation_get_default_enabled(device)); + changed |= set_scroll_method(device, + libinput_device_config_scroll_get_default_method(device)); + changed |= set_scroll_button(device, + libinput_device_config_scroll_get_default_button(device)); + changed |= set_dwt(device, + libinput_device_config_dwt_get_default_enabled(device)); + changed |= set_dwtp(device, + libinput_device_config_dwtp_get_default_enabled(device)); + + float matrix[6]; + libinput_device_config_calibration_get_default_matrix(device, matrix); + changed |= set_calibration_matrix(device, matrix); + + if (changed) { + ipc_event_input("libinput_config", input_device); + } +} + +static bool sway_udev_device_is_builtin(struct udev_device *udev_device) { + const char *id_path = udev_device_get_property_value(udev_device, "ID_PATH"); + if (!id_path) { + return false; + } + + if (has_prefix(id_path, "platform-")) { + return true; + } + + return has_prefix(id_path, "pci-") && strstr(id_path, "-platform-"); +} + +bool sway_libinput_device_is_builtin(struct sway_input_device *sway_device) { + if (!wlr_input_device_is_libinput(sway_device->wlr_device)) { + return false; + } + + struct libinput_device *device = + wlr_libinput_get_device_handle(sway_device->wlr_device); + struct udev_device *udev_device = + libinput_device_get_udev_device(device); + if (!udev_device) { + return false; + } + + bool is_builtin = sway_udev_device_is_builtin(udev_device); + udev_device_unref(udev_device); + return is_builtin; +} diff --git a/sway/input/seat.c b/sway/input/seat.c new file mode 100644 index 0000000..0434d63 --- /dev/null +++ b/sway/input/seat.c @@ -0,0 +1,1714 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "list.h" +#include "log.h" +#include "sway/config.h" +#include "sway/scene_descriptor.h" +#include "sway/input/cursor.h" +#include "sway/input/input-manager.h" +#include "sway/input/keyboard.h" +#include "sway/input/libinput.h" +#include "sway/input/seat.h" +#include "sway/input/switch.h" +#include "sway/input/tablet.h" +#include "sway/ipc-server.h" +#include "sway/layers.h" +#include "sway/output.h" +#include "sway/server.h" +#include "sway/tree/arrange.h" +#include "sway/tree/container.h" +#include "sway/tree/root.h" +#include "sway/tree/view.h" +#include "sway/tree/workspace.h" + +static void seat_device_destroy(struct sway_seat_device *seat_device) { + if (!seat_device) { + return; + } + + sway_keyboard_destroy(seat_device->keyboard); + sway_tablet_destroy(seat_device->tablet); + sway_tablet_pad_destroy(seat_device->tablet_pad); + sway_switch_destroy(seat_device->switch_device); + wlr_cursor_detach_input_device(seat_device->sway_seat->cursor->cursor, + seat_device->input_device->wlr_device); + wl_list_remove(&seat_device->link); + free(seat_device); +} + +static void seat_node_destroy(struct sway_seat_node *seat_node) { + wl_list_remove(&seat_node->destroy.link); + wl_list_remove(&seat_node->link); + + /* + * This is the only time we remove items from the focus stack without + * immediately re-adding them. If we just removed the last thing, + * mark that nothing has focus anymore. + */ + if (wl_list_empty(&seat_node->seat->focus_stack)) { + seat_node->seat->has_focus = false; + } + + free(seat_node); +} + +void seat_destroy(struct sway_seat *seat) { + wlr_seat_destroy(seat->wlr_seat); +} + +static void handle_seat_destroy(struct wl_listener *listener, void *data) { + struct sway_seat *seat = wl_container_of(listener, seat, destroy); + + if (seat == config->handler_context.seat) { + config->handler_context.seat = input_manager_get_default_seat(); + } + struct sway_seat_device *seat_device, *next; + wl_list_for_each_safe(seat_device, next, &seat->devices, link) { + seat_device_destroy(seat_device); + } + struct sway_seat_node *seat_node, *next_seat_node; + wl_list_for_each_safe(seat_node, next_seat_node, &seat->focus_stack, + link) { + seat_node_destroy(seat_node); + } + sway_input_method_relay_finish(&seat->im_relay); + sway_cursor_destroy(seat->cursor); + wl_list_remove(&seat->new_node.link); + wl_list_remove(&seat->request_start_drag.link); + wl_list_remove(&seat->start_drag.link); + wl_list_remove(&seat->request_set_selection.link); + wl_list_remove(&seat->request_set_primary_selection.link); + wl_list_remove(&seat->link); + wl_list_remove(&seat->destroy.link); + for (int i = 0; i < seat->deferred_bindings->length; i++) { + free_sway_binding(seat->deferred_bindings->items[i]); + } + wlr_scene_node_destroy(&seat->scene_tree->node); + list_free(seat->deferred_bindings); + free(seat->prev_workspace_name); + free(seat); +} + +void seat_idle_notify_activity(struct sway_seat *seat, + enum sway_input_idle_source source) { + if ((source & seat->idle_inhibit_sources) == 0) { + return; + } + wlr_idle_notifier_v1_notify_activity(server.idle_notifier_v1, seat->wlr_seat); +} + +/** + * Activate all views within this container recursively. + */ +static void seat_send_activate(struct sway_node *node, struct sway_seat *seat) { + if (node_is_view(node)) { + if (!seat_is_input_allowed(seat, node->sway_container->view->surface)) { + sway_log(SWAY_DEBUG, "Refusing to set focus, input is inhibited"); + return; + } + view_set_activated(node->sway_container->view, true); + } else { + list_t *children = node_get_children(node); + for (int i = 0; i < children->length; ++i) { + struct sway_container *child = children->items[i]; + seat_send_activate(&child->node, seat); + } + } +} + +static struct sway_keyboard *sway_keyboard_for_wlr_keyboard( + struct sway_seat *seat, struct wlr_keyboard *wlr_keyboard) { + struct sway_seat_device *seat_device; + wl_list_for_each(seat_device, &seat->devices, link) { + struct sway_input_device *input_device = seat_device->input_device; + if (input_device->wlr_device->type != WLR_INPUT_DEVICE_KEYBOARD) { + continue; + } + if (input_device->wlr_device == &wlr_keyboard->base) { + return seat_device->keyboard; + } + } + struct sway_keyboard_group *group; + wl_list_for_each(group, &seat->keyboard_groups, link) { + struct sway_input_device *input_device = + group->seat_device->input_device; + if (input_device->wlr_device == &wlr_keyboard->base) { + return group->seat_device->keyboard; + } + } + return NULL; +} + +static void seat_keyboard_notify_enter(struct sway_seat *seat, + struct wlr_surface *surface) { + struct wlr_keyboard *keyboard = wlr_seat_get_keyboard(seat->wlr_seat); + if (!keyboard) { + wlr_seat_keyboard_notify_enter(seat->wlr_seat, surface, NULL, 0, NULL); + return; + } + + struct sway_keyboard *sway_keyboard = + sway_keyboard_for_wlr_keyboard(seat, keyboard); + assert(sway_keyboard && "Cannot find sway_keyboard for seat keyboard"); + + struct sway_shortcut_state *state = &sway_keyboard->state_pressed_sent; + wlr_seat_keyboard_notify_enter(seat->wlr_seat, surface, + state->pressed_keycodes, state->npressed, &keyboard->modifiers); +} + +static void seat_tablet_pads_set_focus(struct sway_seat *seat, + struct wlr_surface *surface) { + struct sway_seat_device *seat_device; + wl_list_for_each(seat_device, &seat->devices, link) { + sway_tablet_pad_set_focus(seat_device->tablet_pad, surface); + } +} + +/** + * If con is a view, set it as active and enable keyboard input. + * If con is a container, set all child views as active and don't enable + * keyboard input on any. + */ +static void seat_send_focus(struct sway_node *node, struct sway_seat *seat) { + seat_send_activate(node, seat); + + struct sway_view *view = node->type == N_CONTAINER ? + node->sway_container->view : NULL; + + if (view && seat_is_input_allowed(seat, view->surface)) { +#if WLR_HAS_XWAYLAND + if (view->type == SWAY_VIEW_XWAYLAND) { + struct wlr_xwayland *xwayland = server.xwayland.wlr_xwayland; + wlr_xwayland_set_seat(xwayland, seat->wlr_seat); + } +#endif + + seat_keyboard_notify_enter(seat, view->surface); + seat_tablet_pads_set_focus(seat, view->surface); + sway_input_method_relay_set_focus(&seat->im_relay, view->surface); + + struct wlr_pointer_constraint_v1 *constraint = + wlr_pointer_constraints_v1_constraint_for_surface( + server.pointer_constraints, view->surface, seat->wlr_seat); + sway_cursor_constrain(seat->cursor, constraint); + } +} + +void seat_for_each_node(struct sway_seat *seat, + void (*f)(struct sway_node *node, void *data), void *data) { + struct sway_seat_node *current = NULL; + wl_list_for_each(current, &seat->focus_stack, link) { + f(current->node, data); + } +} + +struct sway_container *seat_get_focus_inactive_view(struct sway_seat *seat, + struct sway_node *ancestor) { + if (node_is_view(ancestor)) { + return ancestor->sway_container; + } + struct sway_seat_node *current; + wl_list_for_each(current, &seat->focus_stack, link) { + struct sway_node *node = current->node; + if (node_is_view(node) && node_has_ancestor(node, ancestor)) { + return node->sway_container; + } + } + return NULL; +} + +static void handle_seat_node_destroy(struct wl_listener *listener, void *data) { + struct sway_seat_node *seat_node = + wl_container_of(listener, seat_node, destroy); + struct sway_seat *seat = seat_node->seat; + struct sway_node *node = seat_node->node; + struct sway_node *parent = node_get_parent(node); + struct sway_node *focus = seat_get_focus(seat); + + if (node->type == N_WORKSPACE) { + seat_node_destroy(seat_node); + // If an unmanaged or layer surface is focused when an output gets + // disabled and an empty workspace on the output was focused by the + // seat, the seat needs to refocus its focus inactive to update the + // value of seat->workspace. + if (seat->workspace == node->sway_workspace) { + struct sway_node *node = seat_get_focus_inactive(seat, &root->node); + seat_set_focus(seat, NULL); + if (node) { + seat_set_focus(seat, node); + } else { + seat->workspace = NULL; + } + } + return; + } + + // Even though the container being destroyed might be nowhere near the + // focused container, we still need to set focus_inactive on a sibling of + // the container being destroyed. + bool needs_new_focus = focus && + (focus == node || node_has_ancestor(focus, node)); + + seat_node_destroy(seat_node); + + if (!parent && !needs_new_focus) { + // Destroying a container that is no longer in the tree + return; + } + + // Find new focus_inactive (ie. sibling, or workspace if no siblings left) + struct sway_node *next_focus = NULL; + while (next_focus == NULL && parent != NULL) { + struct sway_container *con = + seat_get_focus_inactive_view(seat, parent); + next_focus = con ? &con->node : NULL; + + if (next_focus == NULL && parent->type == N_WORKSPACE) { + next_focus = parent; + break; + } + + parent = node_get_parent(parent); + } + + if (!next_focus) { + struct sway_workspace *ws = seat_get_last_known_workspace(seat); + if (!ws) { + return; + } + struct sway_container *con = + seat_get_focus_inactive_view(seat, &ws->node); + next_focus = con ? &(con->node) : &(ws->node); + } + + if (next_focus->type == N_WORKSPACE && + !workspace_is_visible(next_focus->sway_workspace)) { + // Do not change focus to a non-visible workspace + return; + } + + if (needs_new_focus) { + // Make sure the workspace IPC event gets sent + if (node->type == N_CONTAINER && node->sway_container->scratchpad) { + seat_set_focus(seat, NULL); + } + // The structure change might have caused it to move up to the top of + // the focus stack without sending focus notifications to the view + if (seat_get_focus(seat) == next_focus) { + seat_send_focus(next_focus, seat); + } else { + seat_set_focus(seat, next_focus); + } + } else { + // Setting focus_inactive + focus = seat_get_focus_inactive(seat, &root->node); + seat_set_raw_focus(seat, next_focus); + if (focus->type == N_CONTAINER && focus->sway_container->pending.workspace) { + seat_set_raw_focus(seat, &focus->sway_container->pending.workspace->node); + } + seat_set_raw_focus(seat, focus); + } +} + +static struct sway_seat_node *seat_node_from_node( + struct sway_seat *seat, struct sway_node *node) { + if (node->type == N_ROOT || node->type == N_OUTPUT) { + // these don't get seat nodes ever + return NULL; + } + + struct sway_seat_node *seat_node = NULL; + wl_list_for_each(seat_node, &seat->focus_stack, link) { + if (seat_node->node == node) { + return seat_node; + } + } + + seat_node = calloc(1, sizeof(struct sway_seat_node)); + if (seat_node == NULL) { + sway_log(SWAY_ERROR, "could not allocate seat node"); + return NULL; + } + + seat_node->node = node; + seat_node->seat = seat; + wl_list_insert(seat->focus_stack.prev, &seat_node->link); + wl_signal_add(&node->events.destroy, &seat_node->destroy); + seat_node->destroy.notify = handle_seat_node_destroy; + + return seat_node; +} + +static void handle_new_node(struct wl_listener *listener, void *data) { + struct sway_seat *seat = wl_container_of(listener, seat, new_node); + struct sway_node *node = data; + seat_node_from_node(seat, node); +} + +static void drag_icon_update_position(struct sway_seat *seat, struct wlr_scene_node *node) { + struct wlr_drag_icon *wlr_icon = scene_descriptor_try_get(node, SWAY_SCENE_DESC_DRAG_ICON); + struct wlr_cursor *cursor = seat->cursor->cursor; + + switch (wlr_icon->drag->grab_type) { + case WLR_DRAG_GRAB_KEYBOARD: + return; + case WLR_DRAG_GRAB_KEYBOARD_POINTER: + wlr_scene_node_set_position(node, cursor->x, cursor->y); + break; + case WLR_DRAG_GRAB_KEYBOARD_TOUCH:; + struct wlr_touch_point *point = + wlr_seat_touch_get_point(seat->wlr_seat, wlr_icon->drag->touch_id); + if (point == NULL) { + return; + } + wlr_scene_node_set_position(node, seat->touch_x, seat->touch_y); + } +} + +void drag_icons_update_position(struct sway_seat *seat) { + struct wlr_scene_node *node; + wl_list_for_each(node, &seat->drag_icons->children, link) { + drag_icon_update_position(seat, node); + } +} + +static void drag_handle_destroy(struct wl_listener *listener, void *data) { + struct sway_drag *drag = wl_container_of(listener, drag, destroy); + + // Focus enter isn't sent during drag, so refocus the focused node, layer + // surface or unmanaged surface. + struct sway_seat *seat = drag->seat; + struct sway_node *focus = seat_get_focus(seat); + if (focus) { + seat_set_focus(seat, NULL); + seat_set_focus(seat, focus); + } else if (seat->focused_layer) { + struct wlr_layer_surface_v1 *layer = seat->focused_layer; + seat_set_focus_layer(seat, NULL); + seat_set_focus_layer(seat, layer); + } else { + struct wlr_surface *unmanaged = seat->wlr_seat->keyboard_state.focused_surface; + seat_set_focus_surface(seat, NULL, false); + seat_set_focus_surface(seat, unmanaged, false); + } + + drag->wlr_drag->data = NULL; + wl_list_remove(&drag->destroy.link); + free(drag); +} + +static void handle_request_start_drag(struct wl_listener *listener, + void *data) { + struct sway_seat *seat = wl_container_of(listener, seat, request_start_drag); + struct wlr_seat_request_start_drag_event *event = data; + + if (wlr_seat_validate_pointer_grab_serial(seat->wlr_seat, + event->origin, event->serial)) { + wlr_seat_start_pointer_drag(seat->wlr_seat, event->drag, event->serial); + return; + } + + struct wlr_touch_point *point; + if (wlr_seat_validate_touch_grab_serial(seat->wlr_seat, + event->origin, event->serial, &point)) { + wlr_seat_start_touch_drag(seat->wlr_seat, + event->drag, event->serial, point); + return; + } + + // TODO: tablet grabs + + sway_log(SWAY_DEBUG, "Ignoring start_drag request: " + "could not validate pointer or touch serial %" PRIu32, event->serial); + wlr_data_source_destroy(event->drag->source); +} + +static void handle_start_drag(struct wl_listener *listener, void *data) { + struct sway_seat *seat = wl_container_of(listener, seat, start_drag); + struct wlr_drag *wlr_drag = data; + + struct sway_drag *drag = calloc(1, sizeof(struct sway_drag)); + if (drag == NULL) { + sway_log(SWAY_ERROR, "Allocation failed"); + return; + } + drag->seat = seat; + drag->wlr_drag = wlr_drag; + wlr_drag->data = drag; + + drag->destroy.notify = drag_handle_destroy; + wl_signal_add(&wlr_drag->events.destroy, &drag->destroy); + + struct wlr_drag_icon *wlr_drag_icon = wlr_drag->icon; + if (wlr_drag_icon != NULL) { + struct wlr_scene_tree *tree = wlr_scene_drag_icon_create(seat->drag_icons, wlr_drag_icon); + if (!tree) { + sway_log(SWAY_ERROR, "Failed to allocate a drag icon scene tree"); + return; + } + + if (!scene_descriptor_assign(&tree->node, SWAY_SCENE_DESC_DRAG_ICON, + wlr_drag_icon)) { + sway_log(SWAY_ERROR, "Failed to allocate a drag icon scene descriptor"); + wlr_scene_node_destroy(&tree->node); + return; + } + + drag_icon_update_position(seat, &tree->node); + } + seatop_begin_default(seat); +} + +static void handle_request_set_selection(struct wl_listener *listener, + void *data) { + struct sway_seat *seat = + wl_container_of(listener, seat, request_set_selection); + struct wlr_seat_request_set_selection_event *event = data; + wlr_seat_set_selection(seat->wlr_seat, event->source, event->serial); +} + +static void handle_request_set_primary_selection(struct wl_listener *listener, + void *data) { + struct sway_seat *seat = + wl_container_of(listener, seat, request_set_primary_selection); + struct wlr_seat_request_set_primary_selection_event *event = data; + wlr_seat_set_primary_selection(seat->wlr_seat, event->source, event->serial); +} + +static void collect_focus_iter(struct sway_node *node, void *data) { + struct sway_seat *seat = data; + struct sway_seat_node *seat_node = seat_node_from_node(seat, node); + if (!seat_node) { + return; + } + wl_list_remove(&seat_node->link); + wl_list_insert(&seat->focus_stack, &seat_node->link); +} + +static void collect_focus_workspace_iter(struct sway_workspace *workspace, + void *data) { + collect_focus_iter(&workspace->node, data); +} + +static void collect_focus_container_iter(struct sway_container *container, + void *data) { + collect_focus_iter(&container->node, data); +} + +struct sway_seat *seat_create(const char *seat_name) { + struct sway_seat *seat = calloc(1, sizeof(struct sway_seat)); + if (!seat) { + return NULL; + } + + bool failed = false; + seat->scene_tree = alloc_scene_tree(root->layers.seat, &failed); + seat->drag_icons = alloc_scene_tree(seat->scene_tree, &failed); + if (failed) { + wlr_scene_node_destroy(&seat->scene_tree->node); + free(seat); + return NULL; + } + + seat->wlr_seat = wlr_seat_create(server.wl_display, seat_name); + if (!sway_assert(seat->wlr_seat, "could not allocate seat")) { + wlr_scene_node_destroy(&seat->scene_tree->node); + free(seat); + return NULL; + } + seat->wlr_seat->data = seat; + + seat->cursor = sway_cursor_create(seat); + if (!seat->cursor) { + wlr_scene_node_destroy(&seat->scene_tree->node); + wlr_seat_destroy(seat->wlr_seat); + free(seat); + return NULL; + } + + seat->destroy.notify = handle_seat_destroy; + wl_signal_add(&seat->wlr_seat->events.destroy, &seat->destroy); + + seat->idle_inhibit_sources = seat->idle_wake_sources = + IDLE_SOURCE_KEYBOARD | + IDLE_SOURCE_POINTER | + IDLE_SOURCE_TOUCH | + IDLE_SOURCE_TABLET_PAD | + IDLE_SOURCE_TABLET_TOOL | + IDLE_SOURCE_SWITCH; + + // init the focus stack + wl_list_init(&seat->focus_stack); + + wl_list_init(&seat->devices); + + root_for_each_workspace(collect_focus_workspace_iter, seat); + root_for_each_container(collect_focus_container_iter, seat); + + seat->deferred_bindings = create_list(); + + wl_signal_add(&root->events.new_node, &seat->new_node); + seat->new_node.notify = handle_new_node; + + wl_signal_add(&seat->wlr_seat->events.request_start_drag, + &seat->request_start_drag); + seat->request_start_drag.notify = handle_request_start_drag; + + wl_signal_add(&seat->wlr_seat->events.start_drag, &seat->start_drag); + seat->start_drag.notify = handle_start_drag; + + wl_signal_add(&seat->wlr_seat->events.request_set_selection, + &seat->request_set_selection); + seat->request_set_selection.notify = handle_request_set_selection; + + wl_signal_add(&seat->wlr_seat->events.request_set_primary_selection, + &seat->request_set_primary_selection); + seat->request_set_primary_selection.notify = + handle_request_set_primary_selection; + + wl_list_init(&seat->keyboard_groups); + wl_list_init(&seat->keyboard_shortcuts_inhibitors); + + sway_input_method_relay_init(seat, &seat->im_relay); + + bool first = wl_list_empty(&server.input->seats); + wl_list_insert(&server.input->seats, &seat->link); + + if (!first) { + // Since this is not the first seat, attempt to set initial focus + struct sway_seat *current_seat = input_manager_current_seat(); + struct sway_node *current_focus = + seat_get_focus_inactive(current_seat, &root->node); + seat_set_focus(seat, current_focus); + } + + seatop_begin_default(seat); + + return seat; +} + +static void seat_update_capabilities(struct sway_seat *seat) { + uint32_t caps = 0; + uint32_t previous_caps = seat->wlr_seat->capabilities; + struct sway_seat_device *seat_device; + wl_list_for_each(seat_device, &seat->devices, link) { + switch (seat_device->input_device->wlr_device->type) { + case WLR_INPUT_DEVICE_KEYBOARD: + caps |= WL_SEAT_CAPABILITY_KEYBOARD; + break; + case WLR_INPUT_DEVICE_POINTER: + caps |= WL_SEAT_CAPABILITY_POINTER; + break; + case WLR_INPUT_DEVICE_TOUCH: + caps |= WL_SEAT_CAPABILITY_TOUCH; + break; + case WLR_INPUT_DEVICE_TABLET: + caps |= WL_SEAT_CAPABILITY_POINTER; + break; + case WLR_INPUT_DEVICE_SWITCH: + case WLR_INPUT_DEVICE_TABLET_PAD: + break; + } + } + + // Hide cursor if seat doesn't have pointer capability. + // We must call cursor_set_image while the wlr_seat has the capabilities + // otherwise it's a no op. + if ((caps & WL_SEAT_CAPABILITY_POINTER) == 0) { + cursor_set_image(seat->cursor, NULL, NULL); + wlr_seat_set_capabilities(seat->wlr_seat, caps); + } else { + wlr_seat_set_capabilities(seat->wlr_seat, caps); + if ((previous_caps & WL_SEAT_CAPABILITY_POINTER) == 0) { + cursor_set_image(seat->cursor, "default", NULL); + } + } +} + +static void seat_reset_input_config(struct sway_seat *seat, + struct sway_seat_device *sway_device) { + sway_log(SWAY_DEBUG, "Resetting output mapping for input device %s", + sway_device->input_device->identifier); + wlr_cursor_map_input_to_output(seat->cursor->cursor, + sway_device->input_device->wlr_device, NULL); +} + +/** + * Get the name of the built-in output, if any. Returns NULL if there isn't + * exactly one built-in output. + */ +static const char *get_builtin_output_name(void) { + const char *match = NULL; + for (int i = 0; i < root->outputs->length; ++i) { + struct sway_output *output = root->outputs->items[i]; + const char *name = output->wlr_output->name; + if (has_prefix(name, "eDP-") || has_prefix(name, "LVDS-") || + has_prefix(name, "DSI-")) { + if (match != NULL) { + return NULL; + } + match = name; + } + } + return match; +} + +static bool is_touch_or_tablet_tool(struct sway_seat_device *seat_device) { + switch (seat_device->input_device->wlr_device->type) { + case WLR_INPUT_DEVICE_TOUCH: + case WLR_INPUT_DEVICE_TABLET: + return true; + default: + return false; + } +} + +static void seat_apply_input_mapping(struct sway_seat *seat, + struct sway_seat_device *sway_device) { + struct input_config *ic = + input_device_get_config(sway_device->input_device); + + switch (sway_device->input_device->wlr_device->type) { + case WLR_INPUT_DEVICE_POINTER: + case WLR_INPUT_DEVICE_TOUCH: + case WLR_INPUT_DEVICE_TABLET: + break; + default: + return; // these devices don't support mappings + } + + sway_log(SWAY_DEBUG, "Applying input mapping to %s", + sway_device->input_device->identifier); + + const char *mapped_to_output = ic == NULL ? NULL : ic->mapped_to_output; + struct wlr_box *mapped_to_region = ic == NULL ? NULL : ic->mapped_to_region; + enum input_config_mapped_to mapped_to = + ic == NULL ? MAPPED_TO_DEFAULT : ic->mapped_to; + + switch (mapped_to) { + case MAPPED_TO_DEFAULT:; + /* + * If the wlroots backend provides an output name, use that. + * + * Otherwise, try to map built-in touch and pointer devices to the + * built-in output. + */ + struct wlr_input_device *dev = sway_device->input_device->wlr_device; + switch (dev->type) { + case WLR_INPUT_DEVICE_POINTER: + mapped_to_output = wlr_pointer_from_input_device(dev)->output_name; + break; + case WLR_INPUT_DEVICE_TOUCH: + mapped_to_output = wlr_touch_from_input_device(dev)->output_name; + break; + default: + mapped_to_output = NULL; + break; + } +#if WLR_HAS_LIBINPUT_BACKEND + if (mapped_to_output == NULL && is_touch_or_tablet_tool(sway_device) && + sway_libinput_device_is_builtin(sway_device->input_device)) { + mapped_to_output = get_builtin_output_name(); + if (mapped_to_output) { + sway_log(SWAY_DEBUG, "Auto-detected output '%s' for device '%s'", + mapped_to_output, sway_device->input_device->identifier); + } + } +#else + (void)is_touch_or_tablet_tool; + (void)get_builtin_output_name; +#endif + if (mapped_to_output == NULL) { + return; + } + /* fallthrough */ + case MAPPED_TO_OUTPUT: + sway_log(SWAY_DEBUG, "Mapping input device %s to output %s", + sway_device->input_device->identifier, mapped_to_output); + if (strcmp("*", mapped_to_output) == 0) { + wlr_cursor_map_input_to_output(seat->cursor->cursor, + sway_device->input_device->wlr_device, NULL); + wlr_cursor_map_input_to_region(seat->cursor->cursor, + sway_device->input_device->wlr_device, NULL); + sway_log(SWAY_DEBUG, "Reset output mapping"); + return; + } + struct sway_output *output = output_by_name_or_id(mapped_to_output); + if (!output) { + sway_log(SWAY_DEBUG, "Requested output %s for device %s isn't present", + mapped_to_output, sway_device->input_device->identifier); + return; + } + wlr_cursor_map_input_to_output(seat->cursor->cursor, + sway_device->input_device->wlr_device, output->wlr_output); + wlr_cursor_map_input_to_region(seat->cursor->cursor, + sway_device->input_device->wlr_device, NULL); + sway_log(SWAY_DEBUG, + "Mapped to output %s", output->wlr_output->name); + return; + case MAPPED_TO_REGION: + sway_log(SWAY_DEBUG, "Mapping input device %s to %d,%d %dx%d", + sway_device->input_device->identifier, + mapped_to_region->x, mapped_to_region->y, + mapped_to_region->width, mapped_to_region->height); + wlr_cursor_map_input_to_output(seat->cursor->cursor, + sway_device->input_device->wlr_device, NULL); + wlr_cursor_map_input_to_region(seat->cursor->cursor, + sway_device->input_device->wlr_device, mapped_to_region); + return; + } +} + +static void seat_configure_pointer(struct sway_seat *seat, + struct sway_seat_device *sway_device) { + seat_configure_xcursor(seat); + wlr_cursor_attach_input_device(seat->cursor->cursor, + sway_device->input_device->wlr_device); + wl_event_source_timer_update( + seat->cursor->hide_source, cursor_get_timeout(seat->cursor)); +} + +static void seat_configure_keyboard(struct sway_seat *seat, + struct sway_seat_device *seat_device) { + if (!seat_device->keyboard) { + sway_keyboard_create(seat, seat_device); + } + sway_keyboard_configure(seat_device->keyboard); + + // We only need to update the current keyboard, as the rest will be updated + // as they are activated. + struct wlr_keyboard *wlr_keyboard = + wlr_keyboard_from_input_device(seat_device->input_device->wlr_device); + struct wlr_keyboard *current_keyboard = seat->wlr_seat->keyboard_state.keyboard; + if (wlr_keyboard != current_keyboard) { + return; + } + + // Notify reenter to pick up the new configuration. This reuses + // the current focused surface to avoid breaking input grabs. + struct wlr_surface *surface = seat->wlr_seat->keyboard_state.focused_surface; + if (surface) { + seat_keyboard_notify_enter(seat, surface); + } +} + +static void seat_configure_switch(struct sway_seat *seat, + struct sway_seat_device *seat_device) { + if (!seat_device->switch_device) { + sway_switch_create(seat, seat_device); + } + sway_switch_configure(seat_device->switch_device); +} + +static void seat_configure_touch(struct sway_seat *seat, + struct sway_seat_device *sway_device) { + wlr_cursor_attach_input_device(seat->cursor->cursor, + sway_device->input_device->wlr_device); +} + +static void seat_configure_tablet_tool(struct sway_seat *seat, + struct sway_seat_device *sway_device) { + if (!sway_device->tablet) { + sway_device->tablet = sway_tablet_create(seat, sway_device); + } + sway_configure_tablet(sway_device->tablet); + wlr_cursor_attach_input_device(seat->cursor->cursor, + sway_device->input_device->wlr_device); +} + +static void seat_configure_tablet_pad(struct sway_seat *seat, + struct sway_seat_device *sway_device) { + if (!sway_device->tablet_pad) { + sway_device->tablet_pad = sway_tablet_pad_create(seat, sway_device); + } + sway_configure_tablet_pad(sway_device->tablet_pad); +} + +static struct sway_seat_device *seat_get_device(struct sway_seat *seat, + struct sway_input_device *input_device) { + struct sway_seat_device *seat_device = NULL; + wl_list_for_each(seat_device, &seat->devices, link) { + if (seat_device->input_device == input_device) { + return seat_device; + } + } + + struct sway_keyboard_group *group = NULL; + wl_list_for_each(group, &seat->keyboard_groups, link) { + if (group->seat_device->input_device == input_device) { + return group->seat_device; + } + } + + return NULL; +} + +void seat_configure_device(struct sway_seat *seat, + struct sway_input_device *input_device) { + struct sway_seat_device *seat_device = seat_get_device(seat, input_device); + if (!seat_device) { + return; + } + + switch (input_device->wlr_device->type) { + case WLR_INPUT_DEVICE_POINTER: + seat_configure_pointer(seat, seat_device); + break; + case WLR_INPUT_DEVICE_KEYBOARD: + seat_configure_keyboard(seat, seat_device); + break; + case WLR_INPUT_DEVICE_SWITCH: + seat_configure_switch(seat, seat_device); + break; + case WLR_INPUT_DEVICE_TOUCH: + seat_configure_touch(seat, seat_device); + break; + case WLR_INPUT_DEVICE_TABLET: + seat_configure_tablet_tool(seat, seat_device); + break; + case WLR_INPUT_DEVICE_TABLET_PAD: + seat_configure_tablet_pad(seat, seat_device); + break; + } + + seat_apply_input_mapping(seat, seat_device); +} + +void seat_configure_device_mapping(struct sway_seat *seat, + struct sway_input_device *input_device) { + struct sway_seat_device *seat_device = seat_get_device(seat, input_device); + if (!seat_device) { + return; + } + + seat_apply_input_mapping(seat, seat_device); +} + +void seat_reset_device(struct sway_seat *seat, + struct sway_input_device *input_device) { + struct sway_seat_device *seat_device = seat_get_device(seat, input_device); + if (!seat_device) { + return; + } + + switch (input_device->wlr_device->type) { + case WLR_INPUT_DEVICE_POINTER: + seat_reset_input_config(seat, seat_device); + break; + case WLR_INPUT_DEVICE_KEYBOARD: + sway_keyboard_disarm_key_repeat(seat_device->keyboard); + sway_keyboard_configure(seat_device->keyboard); + break; + case WLR_INPUT_DEVICE_TOUCH: + seat_reset_input_config(seat, seat_device); + break; + case WLR_INPUT_DEVICE_TABLET: + seat_reset_input_config(seat, seat_device); + break; + case WLR_INPUT_DEVICE_TABLET_PAD: + sway_log(SWAY_DEBUG, "TODO: reset tablet pad"); + break; + case WLR_INPUT_DEVICE_SWITCH: + sway_log(SWAY_DEBUG, "TODO: reset switch device"); + break; + } +} + +void seat_add_device(struct sway_seat *seat, + struct sway_input_device *input_device) { + if (seat_get_device(seat, input_device)) { + seat_configure_device(seat, input_device); + return; + } + + struct sway_seat_device *seat_device = + calloc(1, sizeof(struct sway_seat_device)); + if (!seat_device) { + sway_log(SWAY_DEBUG, "could not allocate seat device"); + return; + } + + sway_log(SWAY_DEBUG, "adding device %s to seat %s", + input_device->identifier, seat->wlr_seat->name); + + seat_device->sway_seat = seat; + seat_device->input_device = input_device; + wl_list_insert(&seat->devices, &seat_device->link); + + seat_configure_device(seat, input_device); + + seat_update_capabilities(seat); +} + +void seat_remove_device(struct sway_seat *seat, + struct sway_input_device *input_device) { + struct sway_seat_device *seat_device = seat_get_device(seat, input_device); + + if (!seat_device) { + return; + } + + sway_log(SWAY_DEBUG, "removing device %s from seat %s", + input_device->identifier, seat->wlr_seat->name); + + seat_device_destroy(seat_device); + + seat_update_capabilities(seat); +} + +static bool xcursor_manager_is_named(const struct wlr_xcursor_manager *manager, + const char *name) { + return (!manager->name && !name) || + (name && manager->name && strcmp(name, manager->name) == 0); +} + +void seat_configure_xcursor(struct sway_seat *seat) { + unsigned cursor_size = 24; + const char *cursor_theme = NULL; + + const struct seat_config *seat_config = seat_get_config(seat); + if (!seat_config) { + seat_config = seat_get_config_by_name("*"); + } + if (seat_config) { + cursor_size = seat_config->xcursor_theme.size; + cursor_theme = seat_config->xcursor_theme.name; + } + + if (seat == input_manager_get_default_seat()) { + char cursor_size_fmt[16]; + snprintf(cursor_size_fmt, sizeof(cursor_size_fmt), "%u", cursor_size); + setenv("XCURSOR_SIZE", cursor_size_fmt, 1); + if (cursor_theme != NULL) { + setenv("XCURSOR_THEME", cursor_theme, 1); + } + +#if WLR_HAS_XWAYLAND + if (server.xwayland.wlr_xwayland && (!server.xwayland.xcursor_manager || + !xcursor_manager_is_named(server.xwayland.xcursor_manager, + cursor_theme) || + server.xwayland.xcursor_manager->size != cursor_size)) { + + wlr_xcursor_manager_destroy(server.xwayland.xcursor_manager); + + server.xwayland.xcursor_manager = + wlr_xcursor_manager_create(cursor_theme, cursor_size); + sway_assert(server.xwayland.xcursor_manager, + "Cannot create XCursor manager for theme"); + + wlr_xcursor_manager_load(server.xwayland.xcursor_manager, 1); + struct wlr_xcursor *xcursor = wlr_xcursor_manager_get_xcursor( + server.xwayland.xcursor_manager, "default", 1); + if (xcursor != NULL) { + struct wlr_xcursor_image *image = xcursor->images[0]; + struct wlr_buffer *buffer = wlr_xcursor_image_get_buffer(image); + wlr_xwayland_set_cursor( + server.xwayland.wlr_xwayland, buffer, + image->hotspot_x, image->hotspot_y); + } + } +#endif + } + + /* Create xcursor manager if we don't have one already, or if the + * theme has changed */ + if (!seat->cursor->xcursor_manager || + !xcursor_manager_is_named( + seat->cursor->xcursor_manager, cursor_theme) || + seat->cursor->xcursor_manager->size != cursor_size) { + + wlr_xcursor_manager_destroy(seat->cursor->xcursor_manager); + seat->cursor->xcursor_manager = + wlr_xcursor_manager_create(cursor_theme, cursor_size); + if (!seat->cursor->xcursor_manager) { + sway_log(SWAY_ERROR, + "Cannot create XCursor manager for theme '%s'", cursor_theme); + } + + + for (int i = 0; i < root->outputs->length; ++i) { + struct sway_output *sway_output = root->outputs->items[i]; + struct wlr_output *output = sway_output->wlr_output; + bool result = + wlr_xcursor_manager_load(seat->cursor->xcursor_manager, + output->scale); + if (!result) { + sway_log(SWAY_ERROR, + "Cannot load xcursor theme for output '%s' with scale %f", + output->name, output->scale); + } + } + + // Reset the cursor so that we apply it to outputs that just appeared + cursor_set_image(seat->cursor, NULL, NULL); + cursor_set_image(seat->cursor, "default", NULL); + wlr_cursor_warp(seat->cursor->cursor, NULL, seat->cursor->cursor->x, + seat->cursor->cursor->y); + } +} + +bool seat_is_input_allowed(struct sway_seat *seat, + struct wlr_surface *surface) { + if (server.session_lock.lock) { + return sway_session_lock_has_surface(server.session_lock.lock, surface); + } + return true; +} + +static void send_unfocus(struct sway_container *con, void *data) { + if (con->view) { + view_close_popups(con->view); + view_set_activated(con->view, false); + } +} + +// Unfocus the container and any children (eg. when leaving `focus parent`) +static void seat_send_unfocus(struct sway_node *node, struct sway_seat *seat) { + sway_cursor_constrain(seat->cursor, NULL); + wlr_seat_keyboard_notify_clear_focus(seat->wlr_seat); + if (node->type == N_WORKSPACE) { + workspace_for_each_container(node->sway_workspace, send_unfocus, seat); + } else { + send_unfocus(node->sway_container, seat); + container_for_each_child(node->sway_container, send_unfocus, seat); + } +} + +static int handle_urgent_timeout(void *data) { + struct sway_view *view = data; + view_set_urgent(view, false); + container_update_itself_and_parents(view->container); + return 0; +} + +static void set_workspace(struct sway_seat *seat, + struct sway_workspace *new_ws) { + if (seat->workspace == new_ws) { + return; + } + + if (seat->workspace) { + free(seat->prev_workspace_name); + seat->prev_workspace_name = strdup(seat->workspace->name); + if (!seat->prev_workspace_name) { + sway_log(SWAY_ERROR, "Unable to allocate previous workspace name"); + } + } + + ipc_event_workspace(seat->workspace, new_ws, "focus"); + seat->workspace = new_ws; +} + +void seat_set_raw_focus(struct sway_seat *seat, struct sway_node *node) { + struct sway_seat_node *seat_node = seat_node_from_node(seat, node); + wl_list_remove(&seat_node->link); + wl_list_insert(&seat->focus_stack, &seat_node->link); + node_set_dirty(node); + + // If focusing a scratchpad container that is fullscreen global, parent + // will be NULL + struct sway_node *parent = node_get_parent(node); + if (parent) { + node_set_dirty(parent); + } +} + +static void seat_set_workspace_focus(struct sway_seat *seat, struct sway_node *node) { + struct sway_node *last_focus = seat_get_focus(seat); + if (last_focus == node) { + return; + } + + if (node == NULL) { + seat_send_unfocus(last_focus, seat); + sway_input_method_relay_set_focus(&seat->im_relay, NULL); + seat->has_focus = false; + return; + } + + struct sway_workspace *new_workspace = node->type == N_WORKSPACE ? + node->sway_workspace : node->sway_container->pending.workspace; + struct sway_container *container = node->type == N_CONTAINER ? + node->sway_container : NULL; + + // Deny setting focus to a view which is hidden by a fullscreen container or global + if (container && container_obstructing_fullscreen_container(container)) { + return; + } + + // Deny setting focus to a workspace node when using fullscreen global + if (root->fullscreen_global && !container && new_workspace) { + return; + } + + // Find the output's last workspace, which might have to be removed if empty + struct sway_output *new_output = + new_workspace ? new_workspace->output : NULL; + struct sway_workspace *last_workspace = + new_output ? output_get_active_workspace(new_output) : NULL; + if (last_workspace != new_workspace && new_output) { + node_set_dirty(&new_output->node); + } + + // Unfocus the previous focus + if (last_focus) { + seat_send_unfocus(last_focus, seat); + node_set_dirty(last_focus); + struct sway_node *parent = node_get_parent(last_focus); + if (parent) { + node_set_dirty(parent); + } + } + + // Put the container parents on the focus stack, then the workspace, then + // the focused container. + if (container) { + struct sway_container *parent = container->pending.parent; + while (parent) { + seat_set_raw_focus(seat, &parent->node); + parent = parent->pending.parent; + } + } + if (new_workspace) { + seat_set_raw_focus(seat, &new_workspace->node); + } + if (container) { + seat_set_raw_focus(seat, &container->node); + seat_send_focus(&container->node, seat); + } + + // emit ipc events + set_workspace(seat, new_workspace); + if (container && container->view) { + ipc_event_window(container, "focus"); + } + + if (last_workspace && last_workspace != new_workspace) { + wlr_ext_workspace_handle_v1_set_active(last_workspace->ext_workspace, + workspace_is_visible(last_workspace)); + } + if (new_workspace) { + wlr_ext_workspace_handle_v1_set_active(new_workspace->ext_workspace, + workspace_is_visible(new_workspace)); + } + + // Move sticky containers to new workspace + if (new_workspace && last_workspace + && new_workspace != last_workspace) { + for (int i = 0; i < last_workspace->floating->length; ++i) { + struct sway_container *floater = + last_workspace->floating->items[i]; + if (container_is_sticky(floater)) { + container_detach(floater); + workspace_add_floating(new_workspace, floater); + --i; + } + } + } + + // If urgent, either unset the urgency or start a timer to unset it + if (container && container->view && view_is_urgent(container->view) && + !container->view->urgent_timer) { + struct sway_view *view = container->view; + if (last_workspace && last_workspace != new_workspace && + config->urgent_timeout > 0) { + view->urgent_timer = wl_event_loop_add_timer(server.wl_event_loop, + handle_urgent_timeout, view); + if (view->urgent_timer) { + wl_event_source_timer_update(view->urgent_timer, + config->urgent_timeout); + } else { + sway_log_errno(SWAY_ERROR, "Unable to create urgency timer"); + handle_urgent_timeout(view); + } + } else { + view_set_urgent(view, false); + } + } + + if (last_workspace) { + workspace_consider_destroy(last_workspace); + } + seat->has_focus = true; + + if (config->smart_gaps && new_workspace) { + // When smart gaps is on, gaps may change when the focus changes so + // the workspace needs to be arranged + arrange_workspace(new_workspace); + } +} + +void seat_set_focus(struct sway_seat *seat, struct sway_node *node) { + // Prevents the layer from losing focus if it has keyboard exclusivity + if (seat->has_exclusive_layer) { + struct wlr_layer_surface_v1 *layer = seat->focused_layer; + seat_set_focus_layer(seat, NULL); + seat_set_workspace_focus(seat, node); + seat_set_focus_layer(seat, layer); + } else if (seat->focused_layer) { + seat_set_focus_layer(seat, NULL); + seat_set_workspace_focus(seat, node); + } else { + seat_set_workspace_focus(seat, node); + } + if (server.session_lock.lock) { + seat_set_focus_surface(seat, server.session_lock.lock->focused, false); + } +} + +void seat_set_focus_container(struct sway_seat *seat, + struct sway_container *con) { + seat_set_focus(seat, con ? &con->node : NULL); +} + +void seat_set_focus_workspace(struct sway_seat *seat, + struct sway_workspace *ws) { + seat_set_focus(seat, ws ? &ws->node : NULL); +} + +void seat_set_focus_surface(struct sway_seat *seat, + struct wlr_surface *surface, bool unfocus) { + if (seat->has_focus && unfocus) { + struct sway_node *focus = seat_get_focus(seat); + seat_send_unfocus(focus, seat); + seat->has_focus = false; + } + + if (surface) { + seat_keyboard_notify_enter(seat, surface); + } else { + wlr_seat_keyboard_notify_clear_focus(seat->wlr_seat); + } + + sway_input_method_relay_set_focus(&seat->im_relay, surface); + seat_tablet_pads_set_focus(seat, surface); +} + +void seat_set_focus_layer(struct sway_seat *seat, + struct wlr_layer_surface_v1 *layer) { + if (!layer && seat->focused_layer) { + seat->focused_layer = NULL; + struct sway_node *previous = seat_get_focus_inactive(seat, &root->node); + if (previous) { + // Hack to get seat to re-focus the return value of get_focus + seat_set_focus(seat, NULL); + seat_set_focus(seat, previous); + } + return; + } else if (!layer) { + return; + } + assert(layer->surface->mapped); + if (layer->current.layer >= ZWLR_LAYER_SHELL_V1_LAYER_TOP && + layer->current.keyboard_interactive + == ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE) { + seat->has_exclusive_layer = true; + } + if (seat->focused_layer == layer) { + return; + } + seat_set_focus_surface(seat, layer->surface, true); + seat->focused_layer = layer; +} + +void seat_unfocus_unless_client(struct sway_seat *seat, struct wl_client *client) { + if (seat->focused_layer) { + if (wl_resource_get_client(seat->focused_layer->resource) != client) { + seat_set_focus_layer(seat, NULL); + } + } + if (seat->has_focus) { + struct sway_node *focus = seat_get_focus(seat); + if (node_is_view(focus) && wl_resource_get_client( + focus->sway_container->view->surface->resource) != client) { + seat_set_focus(seat, NULL); + } + } + if (seat->wlr_seat->pointer_state.focused_client) { + if (seat->wlr_seat->pointer_state.focused_client->client != client) { + wlr_seat_pointer_notify_clear_focus(seat->wlr_seat); + } + } + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + struct wlr_touch_point *point; + wl_list_for_each(point, &seat->wlr_seat->touch_state.touch_points, link) { + if (point->client->client != client) { + wlr_seat_touch_point_clear_focus(seat->wlr_seat, + now.tv_nsec / 1000, point->touch_id); + } + } +} + +struct sway_node *seat_get_focus_inactive(struct sway_seat *seat, + struct sway_node *node) { + if (node_is_view(node)) { + return node; + } + struct sway_seat_node *current; + wl_list_for_each(current, &seat->focus_stack, link) { + if (node_has_ancestor(current->node, node)) { + return current->node; + } + } + if (node->type == N_WORKSPACE) { + return node; + } + return NULL; +} + +struct sway_container *seat_get_focus_inactive_tiling(struct sway_seat *seat, + struct sway_workspace *workspace) { + if (!workspace->tiling->length) { + return NULL; + } + struct sway_seat_node *current; + wl_list_for_each(current, &seat->focus_stack, link) { + struct sway_node *node = current->node; + if (node->type == N_CONTAINER && + !container_is_floating_or_child(node->sway_container) && + node->sway_container->pending.workspace == workspace) { + return node->sway_container; + } + } + return NULL; +} + +struct sway_container *seat_get_focus_inactive_floating(struct sway_seat *seat, + struct sway_workspace *workspace) { + if (!workspace->floating->length) { + return NULL; + } + struct sway_seat_node *current; + wl_list_for_each(current, &seat->focus_stack, link) { + struct sway_node *node = current->node; + if (node->type == N_CONTAINER && + container_is_floating_or_child(node->sway_container) && + node->sway_container->pending.workspace == workspace) { + return node->sway_container; + } + } + return NULL; +} + +struct sway_node *seat_get_active_tiling_child(struct sway_seat *seat, + struct sway_node *parent) { + if (node_is_view(parent)) { + return parent; + } + struct sway_seat_node *current; + wl_list_for_each(current, &seat->focus_stack, link) { + struct sway_node *node = current->node; + if (node_get_parent(node) != parent) { + continue; + } + if (parent->type == N_WORKSPACE) { + // Only consider tiling children + struct sway_workspace *ws = parent->sway_workspace; + if (list_find(ws->tiling, node->sway_container) == -1) { + continue; + } + } + return node; + } + return NULL; +} + +struct sway_node *seat_get_focus(struct sway_seat *seat) { + if (!seat->has_focus) { + return NULL; + } + sway_assert(!wl_list_empty(&seat->focus_stack), + "focus_stack is empty, but has_focus is true"); + struct sway_seat_node *current = + wl_container_of(seat->focus_stack.next, current, link); + return current->node; +} + +struct sway_workspace *seat_get_focused_workspace(struct sway_seat *seat) { + struct sway_node *focus = seat_get_focus_inactive(seat, &root->node); + if (!focus) { + return NULL; + } + if (focus->type == N_CONTAINER) { + return focus->sway_container->pending.workspace; + } + if (focus->type == N_WORKSPACE) { + return focus->sway_workspace; + } + return NULL; // output doesn't have a workspace yet +} + +struct sway_workspace *seat_get_last_known_workspace(struct sway_seat *seat) { + struct sway_seat_node *current; + wl_list_for_each(current, &seat->focus_stack, link) { + struct sway_node *node = current->node; + if (node->type == N_CONTAINER && + node->sway_container->pending.workspace) { + return node->sway_container->pending.workspace; + } else if (node->type == N_WORKSPACE) { + return node->sway_workspace; + } + } + return NULL; +} + +struct sway_container *seat_get_focused_container(struct sway_seat *seat) { + struct sway_node *focus = seat_get_focus(seat); + if (focus && focus->type == N_CONTAINER) { + return focus->sway_container; + } + return NULL; +} + +void seat_apply_config(struct sway_seat *seat, + struct seat_config *seat_config) { + struct sway_seat_device *seat_device = NULL; + + if (!seat_config) { + return; + } + + seat->idle_inhibit_sources = seat_config->idle_inhibit_sources; + seat->idle_wake_sources = seat_config->idle_wake_sources; + + wl_list_for_each(seat_device, &seat->devices, link) { + seat_configure_device(seat, seat_device->input_device); + cursor_handle_activity_from_device(seat->cursor, + seat_device->input_device->wlr_device); + } +} + +struct seat_config *seat_get_config(struct sway_seat *seat) { + struct seat_config *seat_config = NULL; + for (int i = 0; i < config->seat_configs->length; ++i ) { + seat_config = config->seat_configs->items[i]; + if (strcmp(seat->wlr_seat->name, seat_config->name) == 0) { + return seat_config; + } + } + + return NULL; +} + +struct seat_config *seat_get_config_by_name(const char *name) { + struct seat_config *seat_config = NULL; + for (int i = 0; i < config->seat_configs->length; ++i ) { + seat_config = config->seat_configs->items[i]; + if (strcmp(name, seat_config->name) == 0) { + return seat_config; + } + } + + return NULL; +} + +void seat_pointer_notify_button(struct sway_seat *seat, uint32_t time_msec, + uint32_t button, enum wl_pointer_button_state state) { + seat->last_button_serial = wlr_seat_pointer_notify_button(seat->wlr_seat, + time_msec, button, state); +} + +void seat_consider_warp_to_focus(struct sway_seat *seat) { + struct sway_node *focus = seat_get_focus(seat); + if (config->mouse_warping == WARP_NO || !focus) { + return; + } + if (config->mouse_warping == WARP_OUTPUT) { + struct sway_output *output = node_get_output(focus); + if (output) { + struct wlr_box box; + output_get_box(output, &box); + if (wlr_box_contains_point(&box, + seat->cursor->cursor->x, seat->cursor->cursor->y)) { + return; + } + } + } + + if (focus->type == N_CONTAINER) { + cursor_warp_to_container(seat->cursor, focus->sway_container, false); + } else { + cursor_warp_to_workspace(seat->cursor, focus->sway_workspace); + } +} + +void seatop_unref(struct sway_seat *seat, struct sway_container *con) { + if (seat->seatop_impl->unref) { + seat->seatop_impl->unref(seat, con); + } +} + +void seatop_button(struct sway_seat *seat, uint32_t time_msec, + struct wlr_input_device *device, uint32_t button, + enum wl_pointer_button_state state) { + if (seat->seatop_impl->button) { + seat->seatop_impl->button(seat, time_msec, device, button, state); + } +} + +void seatop_pointer_motion(struct sway_seat *seat, uint32_t time_msec) { + if (seat->seatop_impl->pointer_motion) { + seat->seatop_impl->pointer_motion(seat, time_msec); + } +} + +void seatop_pointer_axis(struct sway_seat *seat, + struct wlr_pointer_axis_event *event) { + if (seat->seatop_impl->pointer_axis) { + seat->seatop_impl->pointer_axis(seat, event); + } +} + +void seatop_touch_motion(struct sway_seat *seat, struct wlr_touch_motion_event *event, + double lx, double ly) { + if (seat->seatop_impl->touch_motion) { + seat->seatop_impl->touch_motion(seat, event, lx, ly); + } +} + +void seatop_touch_up(struct sway_seat *seat, struct wlr_touch_up_event *event) { + if (seat->seatop_impl->touch_up) { + seat->seatop_impl->touch_up(seat, event); + } +} + +void seatop_touch_down(struct sway_seat *seat, struct wlr_touch_down_event *event, + double lx, double ly) { + if (seat->seatop_impl->touch_down) { + seat->seatop_impl->touch_down(seat, event, lx, ly); + } +} + +void seatop_touch_cancel(struct sway_seat *seat, struct wlr_touch_cancel_event *event) { + if (seat->seatop_impl->touch_cancel) { + seat->seatop_impl->touch_cancel(seat, event); + } +} + +void seatop_tablet_tool_tip(struct sway_seat *seat, + struct sway_tablet_tool *tool, uint32_t time_msec, + enum wlr_tablet_tool_tip_state state) { + if (seat->seatop_impl->tablet_tool_tip) { + seat->seatop_impl->tablet_tool_tip(seat, tool, time_msec, state); + } +} + +void seatop_tablet_tool_motion(struct sway_seat *seat, + struct sway_tablet_tool *tool, uint32_t time_msec) { + if (seat->seatop_impl->tablet_tool_motion) { + seat->seatop_impl->tablet_tool_motion(seat, tool, time_msec); + } else { + seatop_pointer_motion(seat, time_msec); + } +} + +void seatop_hold_begin(struct sway_seat *seat, + struct wlr_pointer_hold_begin_event *event) { + if (seat->seatop_impl->hold_begin) { + seat->seatop_impl->hold_begin(seat, event); + } +} + +void seatop_hold_end(struct sway_seat *seat, + struct wlr_pointer_hold_end_event *event) { + if (seat->seatop_impl->hold_end) { + seat->seatop_impl->hold_end(seat, event); + } +} + +void seatop_pinch_begin(struct sway_seat *seat, + struct wlr_pointer_pinch_begin_event *event) { + if (seat->seatop_impl->pinch_begin) { + seat->seatop_impl->pinch_begin(seat, event); + } +} + +void seatop_pinch_update(struct sway_seat *seat, + struct wlr_pointer_pinch_update_event *event) { + if (seat->seatop_impl->pinch_update) { + seat->seatop_impl->pinch_update(seat, event); + } +} + +void seatop_pinch_end(struct sway_seat *seat, + struct wlr_pointer_pinch_end_event *event) { + if (seat->seatop_impl->pinch_end) { + seat->seatop_impl->pinch_end(seat, event); + } +} + +void seatop_swipe_begin(struct sway_seat *seat, + struct wlr_pointer_swipe_begin_event *event) { + if (seat->seatop_impl->swipe_begin) { + seat->seatop_impl->swipe_begin(seat, event); + } +} + +void seatop_swipe_update(struct sway_seat *seat, + struct wlr_pointer_swipe_update_event *event) { + if (seat->seatop_impl->swipe_update) { + seat->seatop_impl->swipe_update(seat, event); + } +} + +void seatop_swipe_end(struct sway_seat *seat, + struct wlr_pointer_swipe_end_event *event) { + if (seat->seatop_impl->swipe_end) { + seat->seatop_impl->swipe_end(seat, event); + } +} + +void seatop_rebase(struct sway_seat *seat, uint32_t time_msec) { + if (seat->seatop_impl->rebase) { + seat->seatop_impl->rebase(seat, time_msec); + } +} + +void seatop_end(struct sway_seat *seat) { + if (seat->seatop_impl && seat->seatop_impl->end) { + seat->seatop_impl->end(seat); + } + free(seat->seatop_data); + seat->seatop_data = NULL; + seat->seatop_impl = NULL; +} + +bool seatop_allows_set_cursor(struct sway_seat *seat) { + return seat->seatop_impl->allow_set_cursor; +} + +struct sway_keyboard_shortcuts_inhibitor * +keyboard_shortcuts_inhibitor_get_for_surface( + const struct sway_seat *seat, + const struct wlr_surface *surface) { + struct sway_keyboard_shortcuts_inhibitor *sway_inhibitor = NULL; + wl_list_for_each(sway_inhibitor, &seat->keyboard_shortcuts_inhibitors, link) { + if (sway_inhibitor->inhibitor->surface == surface) { + return sway_inhibitor; + } + } + + return NULL; +} + +struct sway_keyboard_shortcuts_inhibitor * +keyboard_shortcuts_inhibitor_get_for_focused_surface( + const struct sway_seat *seat) { + return keyboard_shortcuts_inhibitor_get_for_surface(seat, + seat->wlr_seat->keyboard_state.focused_surface); +} diff --git a/sway/input/seatop_default.c b/sway/input/seatop_default.c new file mode 100644 index 0000000..df8232a --- /dev/null +++ b/sway/input/seatop_default.c @@ -0,0 +1,1155 @@ +#include +#include +#include +#include +#include +#include +#include "gesture.h" +#include "sway/desktop/transaction.h" +#include "sway/input/cursor.h" +#include "sway/input/seat.h" +#include "sway/input/tablet.h" +#include "sway/layers.h" +#include "sway/output.h" +#include "sway/server.h" +#include "sway/scene_descriptor.h" +#include "sway/tree/view.h" +#include "sway/tree/workspace.h" +#include "log.h" +#include "util.h" +#if WLR_HAS_XWAYLAND +#include "sway/xwayland.h" +#endif + +struct seatop_default_event { + struct sway_node *previous_node; + uint32_t pressed_buttons[SWAY_CURSOR_PRESSED_BUTTONS_CAP]; + size_t pressed_button_count; + struct gesture_tracker gestures; +}; + +/*-----------------------------------------\ + * Functions shared by multiple callbacks / + *---------------------------------------*/ + +/** + * Determine if the edge of the given container is on the edge of the + * workspace/output. + */ +static bool edge_is_external(struct sway_container *cont, enum wlr_edges edge) { + enum sway_container_layout layout = L_NONE; + switch (edge) { + case WLR_EDGE_TOP: + case WLR_EDGE_BOTTOM: + layout = L_VERT; + break; + case WLR_EDGE_LEFT: + case WLR_EDGE_RIGHT: + layout = L_HORIZ; + break; + case WLR_EDGE_NONE: + sway_assert(false, "Never reached"); + return false; + } + + // Iterate the parents until we find one with the layout we want, + // then check if the child has siblings between it and the edge. + while (cont) { + if (container_parent_layout(cont) == layout) { + list_t *siblings = container_get_siblings(cont); + if (!siblings) { + return false; + } + int index = list_find(siblings, cont); + if (index > 0 && (edge == WLR_EDGE_LEFT || edge == WLR_EDGE_TOP)) { + return false; + } + if (index < siblings->length - 1 && + (edge == WLR_EDGE_RIGHT || edge == WLR_EDGE_BOTTOM)) { + return false; + } + } + cont = cont->pending.parent; + } + return true; +} + +static enum wlr_edges find_edge(struct sway_container *cont, + struct wlr_surface *surface, struct sway_cursor *cursor) { + if (!cont->view || (surface && cont->view->surface != surface)) { + return WLR_EDGE_NONE; + } + if (cont->pending.border == B_NONE || !cont->pending.border_thickness || + cont->pending.border == B_CSD) { + return WLR_EDGE_NONE; + } + if (cont->pending.fullscreen_mode) { + return WLR_EDGE_NONE; + } + + enum wlr_edges edge = 0; + if (cursor->cursor->x < cont->pending.x + cont->pending.border_thickness) { + edge |= WLR_EDGE_LEFT; + } + if (cursor->cursor->y < cont->pending.y + cont->pending.border_thickness) { + edge |= WLR_EDGE_TOP; + } + if (cursor->cursor->x >= cont->pending.x + cont->pending.width - cont->pending.border_thickness) { + edge |= WLR_EDGE_RIGHT; + } + if (cursor->cursor->y >= cont->pending.y + cont->pending.height - cont->pending.border_thickness) { + edge |= WLR_EDGE_BOTTOM; + } + + return edge; +} + +/** + * If the cursor is over a _resizable_ edge, return the edge. + * Edges that can't be resized are edges of the workspace. + */ +enum wlr_edges find_resize_edge(struct sway_container *cont, + struct wlr_surface *surface, struct sway_cursor *cursor) { + enum wlr_edges edge = find_edge(cont, surface, cursor); + if (edge && !container_is_floating(cont) && edge_is_external(cont, edge)) { + return WLR_EDGE_NONE; + } + return edge; +} + +/** + * Return the mouse binding which matches modifier, click location, release, + * and pressed button state, otherwise return null. + */ +static struct sway_binding* get_active_mouse_binding( + struct seatop_default_event *e, list_t *bindings, uint32_t modifiers, + bool release, bool on_titlebar, bool on_border, bool on_content, + bool on_workspace, const char *identifier) { + uint32_t click_region = + ((on_titlebar || on_workspace) ? BINDING_TITLEBAR : 0) | + ((on_border || on_workspace) ? BINDING_BORDER : 0) | + ((on_content || on_workspace) ? BINDING_CONTENTS : 0); + + struct sway_binding *current = NULL; + for (int i = 0; i < bindings->length; ++i) { + struct sway_binding *binding = bindings->items[i]; + if (modifiers ^ binding->modifiers || + e->pressed_button_count != (size_t)binding->keys->length || + release != (binding->flags & BINDING_RELEASE) || + !(click_region & binding->flags) || + (on_workspace && + (click_region & binding->flags) != click_region) || + (strcmp(binding->input, identifier) != 0 && + strcmp(binding->input, "*") != 0)) { + continue; + } + + bool match = true; + for (size_t j = 0; j < e->pressed_button_count; j++) { + uint32_t key = *(uint32_t *)binding->keys->items[j]; + if (key != e->pressed_buttons[j]) { + match = false; + break; + } + } + if (!match) { + continue; + } + + if (!current || strcmp(current->input, "*") == 0) { + current = binding; + if (strcmp(current->input, identifier) == 0) { + // If a binding is found for the exact input, quit searching + break; + } + } + } + return current; +} + +/** + * Remove a button (and duplicates) from the sorted list of currently pressed + * buttons. + */ +static void state_erase_button(struct seatop_default_event *e, + uint32_t button) { + size_t j = 0; + for (size_t i = 0; i < e->pressed_button_count; ++i) { + if (i > j) { + e->pressed_buttons[j] = e->pressed_buttons[i]; + } + if (e->pressed_buttons[i] != button) { + ++j; + } + } + while (e->pressed_button_count > j) { + --e->pressed_button_count; + e->pressed_buttons[e->pressed_button_count] = 0; + } +} + +/** + * Add a button to the sorted list of currently pressed buttons, if there + * is space. + */ +static void state_add_button(struct seatop_default_event *e, uint32_t button) { + if (e->pressed_button_count >= SWAY_CURSOR_PRESSED_BUTTONS_CAP) { + return; + } + size_t i = 0; + while (i < e->pressed_button_count && e->pressed_buttons[i] < button) { + ++i; + } + size_t j = e->pressed_button_count; + while (j > i) { + e->pressed_buttons[j] = e->pressed_buttons[j - 1]; + --j; + } + e->pressed_buttons[i] = button; + e->pressed_button_count++; +} + +/*-------------------------------------------\ + * Functions used by handle_tablet_tool_tip / + *-----------------------------------------*/ + +static void handle_tablet_tool_tip(struct sway_seat *seat, + struct sway_tablet_tool *tool, uint32_t time_msec, + enum wlr_tablet_tool_tip_state state) { + if (state == WLR_TABLET_TOOL_TIP_UP) { + wlr_tablet_v2_tablet_tool_notify_up(tool->tablet_v2_tool); + return; + } + + struct sway_cursor *cursor = seat->cursor; + struct wlr_surface *surface = NULL; + double sx, sy; + struct sway_node *node = node_at_coords(seat, + cursor->cursor->x, cursor->cursor->y, &surface, &sx, &sy); + + if (!sway_assert(surface, + "Expected null-surface tablet input to route through pointer emulation")) { + return; + } + + struct sway_container *cont = node && node->type == N_CONTAINER ? + node->sway_container : NULL; + + struct wlr_layer_surface_v1 *layer; +#if WLR_HAS_XWAYLAND + struct wlr_xwayland_surface *xsurface; +#endif + if ((layer = wlr_layer_surface_v1_try_from_wlr_surface(surface)) && + layer->current.keyboard_interactive) { + // Handle tapping a layer surface + seat_set_focus_layer(seat, layer); + transaction_commit_dirty(); + } else if (cont) { + bool is_floating_or_child = container_is_floating_or_child(cont); + bool is_fullscreen_or_child = container_is_fullscreen_or_child(cont); + struct wlr_keyboard *keyboard = wlr_seat_get_keyboard(seat->wlr_seat); + bool mod_pressed = keyboard && + (wlr_keyboard_get_modifiers(keyboard) & config->floating_mod); + + // Handle beginning floating move + if (is_floating_or_child && !is_fullscreen_or_child && mod_pressed) { + seat_set_focus_container(seat, + seat_get_focus_inactive_view(seat, &cont->node)); + seatop_begin_move_floating(seat, container_toplevel_ancestor(cont)); + return; + } + + // Handle moving a tiling container + if (config->tiling_drag && mod_pressed && !is_floating_or_child && + cont->pending.fullscreen_mode == FULLSCREEN_NONE) { + seatop_begin_move_tiling(seat, cont); + return; + } + + // Handle tapping on a container surface + seat_set_focus_container(seat, cont); + seatop_begin_down(seat, node->sway_container, sx, sy); + } +#if WLR_HAS_XWAYLAND + // Handle tapping on an xwayland unmanaged view + else if ((xsurface = wlr_xwayland_surface_try_from_wlr_surface(surface)) && + xsurface->override_redirect && + wlr_xwayland_surface_override_redirect_wants_focus(xsurface)) { + struct wlr_xwayland *xwayland = server.xwayland.wlr_xwayland; + wlr_xwayland_set_seat(xwayland, seat->wlr_seat); + seat_set_focus_surface(seat, xsurface->surface, false); + transaction_commit_dirty(); + } +#endif + + wlr_tablet_v2_tablet_tool_notify_down(tool->tablet_v2_tool); + wlr_tablet_tool_v2_start_implicit_grab(tool->tablet_v2_tool); +} + +/*----------------------------------\ + * Functions used by handle_button / + *--------------------------------*/ + +static bool trigger_pointer_button_binding(struct sway_seat *seat, + struct wlr_input_device *device, uint32_t button, + enum wl_pointer_button_state state, uint32_t modifiers, + bool on_titlebar, bool on_border, bool on_contents, bool on_workspace) { + // We can reach this for non-pointer devices if we're currently emulating + // pointer input for one. Emulated input should not trigger bindings. The + // device can be NULL if this is synthetic (e.g. swaymsg-generated) input. + if (device && device->type != WLR_INPUT_DEVICE_POINTER) { + return false; + } + + struct seatop_default_event *e = seat->seatop_data; + + char *device_identifier = device ? input_device_get_identifier(device) + : strdup("*"); + struct sway_binding *binding = NULL; + if (state == WL_POINTER_BUTTON_STATE_PRESSED) { + state_add_button(e, button); + binding = get_active_mouse_binding(e, + config->current_mode->mouse_bindings, modifiers, false, + on_titlebar, on_border, on_contents, on_workspace, + device_identifier); + } else { + binding = get_active_mouse_binding(e, + config->current_mode->mouse_bindings, modifiers, true, + on_titlebar, on_border, on_contents, on_workspace, + device_identifier); + state_erase_button(e, button); + } + + free(device_identifier); + if (binding) { + seat_execute_command(seat, binding); + return true; + } + + return false; +} + +static void handle_button(struct sway_seat *seat, uint32_t time_msec, + struct wlr_input_device *device, uint32_t button, + enum wl_pointer_button_state state) { + struct sway_cursor *cursor = seat->cursor; + + // Determine what's under the cursor + struct wlr_surface *surface = NULL; + double sx, sy; + struct sway_node *node = node_at_coords(seat, + cursor->cursor->x, cursor->cursor->y, &surface, &sx, &sy); + + struct sway_container *cont = node && node->type == N_CONTAINER ? + node->sway_container : NULL; + bool is_floating = cont && container_is_floating(cont); + bool is_floating_or_child = cont && container_is_floating_or_child(cont); + bool is_fullscreen_or_child = cont && container_is_fullscreen_or_child(cont); + enum wlr_edges edge = cont ? find_edge(cont, surface, cursor) : WLR_EDGE_NONE; + enum wlr_edges resize_edge = cont && edge ? + find_resize_edge(cont, surface, cursor) : WLR_EDGE_NONE; + bool on_border = edge != WLR_EDGE_NONE; + bool on_contents = cont && !on_border && surface; + bool on_workspace = node && node->type == N_WORKSPACE; + bool on_titlebar = cont && !on_border && !surface; + + struct wlr_keyboard *keyboard = wlr_seat_get_keyboard(seat->wlr_seat); + uint32_t modifiers = keyboard ? wlr_keyboard_get_modifiers(keyboard) : 0; + + bool mod_pressed = modifiers & config->floating_mod; + uint32_t mod_move_btn = config->floating_mod_inverse ? BTN_RIGHT : BTN_LEFT; + uint32_t mod_resize_btn = config->floating_mod_inverse ? BTN_LEFT : BTN_RIGHT; + bool mod_move_btn_pressed = mod_pressed && button == mod_move_btn; + bool mod_resize_btn_pressed = mod_pressed && button == mod_resize_btn; + bool titlebar_left_btn_pressed = on_titlebar && button == BTN_LEFT; + + // Handle mouse bindings + if (trigger_pointer_button_binding(seat, device, button, state, modifiers, + on_titlebar, on_border, on_contents, on_workspace)) { + return; + } + + // Handle clicking an empty workspace + if (node && node->type == N_WORKSPACE) { + if (state == WL_POINTER_BUTTON_STATE_PRESSED) { + seat_set_focus(seat, node); + transaction_commit_dirty(); + } + seat_pointer_notify_button(seat, time_msec, button, state); + return; + } + + // Handle clicking a layer surface and its popups/subsurfaces + struct wlr_layer_surface_v1 *layer = NULL; + if ((layer = toplevel_layer_surface_from_surface(surface))) { + if (layer->current.keyboard_interactive) { + seat_set_focus_layer(seat, layer); + transaction_commit_dirty(); + } + if (state == WL_POINTER_BUTTON_STATE_PRESSED) { + seatop_begin_down_on_surface(seat, surface, sx, sy); + } + seat_pointer_notify_button(seat, time_msec, button, state); + return; + } + + // Handle tiling resize via border + if (cont && resize_edge && button == BTN_LEFT && + state == WL_POINTER_BUTTON_STATE_PRESSED && !is_floating) { + // If a resize is triggered on a tabbed or stacked container, change + // focus to the tab which already had inactive focus -- otherwise, we'd + // change the active tab when the user probably just wanted to resize. + struct sway_container *cont_to_focus = cont; + enum sway_container_layout layout = container_parent_layout(cont); + if (layout == L_TABBED || layout == L_STACKED) { + cont_to_focus = seat_get_focus_inactive_view(seat, &cont->pending.parent->node); + } + + seat_set_focus_container(seat, cont_to_focus); + seatop_begin_resize_tiling(seat, cont, edge); + return; + } + + // Handle tiling resize via mod + if (cont && !is_floating_or_child && mod_pressed && mod_resize_btn_pressed && + state == WL_POINTER_BUTTON_STATE_PRESSED) { + edge = 0; + edge |= cursor->cursor->x > cont->pending.x + cont->pending.width / 2 ? + WLR_EDGE_RIGHT : WLR_EDGE_LEFT; + edge |= cursor->cursor->y > cont->pending.y + cont->pending.height / 2 ? + WLR_EDGE_BOTTOM : WLR_EDGE_TOP; + + const char *image = NULL; + if (edge == (WLR_EDGE_LEFT | WLR_EDGE_TOP)) { + image = "nw-resize"; + } else if (edge == (WLR_EDGE_TOP | WLR_EDGE_RIGHT)) { + image = "ne-resize"; + } else if (edge == (WLR_EDGE_RIGHT | WLR_EDGE_BOTTOM)) { + image = "se-resize"; + } else if (edge == (WLR_EDGE_BOTTOM | WLR_EDGE_LEFT)) { + image = "sw-resize"; + } + cursor_set_image(seat->cursor, image, NULL); + seat_set_focus_container(seat, cont); + seatop_begin_resize_tiling(seat, cont, edge); + return; + } + + // Handle changing focus when clicking on a container + if (cont && state == WL_POINTER_BUTTON_STATE_PRESSED) { + // Default case: focus the container that was just clicked. + node = &cont->node; + + // If the container is a tab/stacked container and the click happened + // on a tab, switch to the tab. If the tab contents were already + // focused, focus the tab container itself. If the tab container was + // already focused, cycle back to focusing the tab contents. + if (on_titlebar) { + struct sway_container *focus = seat_get_focused_container(seat); + if (focus == cont || !container_has_ancestor(focus, cont)) { + node = seat_get_focus_inactive(seat, &cont->node); + } + } + + seat_set_focus(seat, node); + transaction_commit_dirty(); + } + + // Handle beginning floating move + if (cont && is_floating_or_child && !is_fullscreen_or_child && + state == WL_POINTER_BUTTON_STATE_PRESSED && + (mod_move_btn_pressed || titlebar_left_btn_pressed)) { + seatop_begin_move_floating(seat, container_toplevel_ancestor(cont)); + return; + } + + // Handle beginning floating resize + if (cont && is_floating_or_child && !is_fullscreen_or_child && + state == WL_POINTER_BUTTON_STATE_PRESSED) { + // Via border + if (button == BTN_LEFT && resize_edge != WLR_EDGE_NONE) { + seat_set_focus_container(seat, cont); + seatop_begin_resize_floating(seat, cont, resize_edge); + return; + } + + // Via mod+click + if (mod_resize_btn_pressed) { + struct sway_container *floater = container_toplevel_ancestor(cont); + edge = 0; + edge |= cursor->cursor->x > floater->pending.x + floater->pending.width / 2 ? + WLR_EDGE_RIGHT : WLR_EDGE_LEFT; + edge |= cursor->cursor->y > floater->pending.y + floater->pending.height / 2 ? + WLR_EDGE_BOTTOM : WLR_EDGE_TOP; + seat_set_focus_container(seat, floater); + seatop_begin_resize_floating(seat, floater, edge); + return; + } + } + + // Handle moving a tiling container + if (config->tiling_drag && (mod_move_btn_pressed || titlebar_left_btn_pressed) && + state == WL_POINTER_BUTTON_STATE_PRESSED && !is_floating_or_child && + cont && cont->pending.fullscreen_mode == FULLSCREEN_NONE) { + // If moving a container by its title bar, use a threshold for the drag + if (!mod_pressed && config->tiling_drag_threshold > 0) { + seatop_begin_move_tiling_threshold(seat, cont); + } else { + seatop_begin_move_tiling(seat, cont); + } + return; + } + + // Handle mousedown on a container surface + if (surface && cont && state == WL_POINTER_BUTTON_STATE_PRESSED) { + seatop_begin_down(seat, cont, sx, sy); + seat_pointer_notify_button(seat, time_msec, button, WL_POINTER_BUTTON_STATE_PRESSED); + return; + } + + // Handle clicking a container surface or decorations + if (cont && state == WL_POINTER_BUTTON_STATE_PRESSED) { + seat_pointer_notify_button(seat, time_msec, button, state); + return; + } + +#if WLR_HAS_XWAYLAND + // Handle clicking on xwayland unmanaged view + struct wlr_xwayland_surface *xsurface; + if (surface && + (xsurface = wlr_xwayland_surface_try_from_wlr_surface(surface)) && + xsurface->override_redirect && + wlr_xwayland_surface_override_redirect_wants_focus(xsurface)) { + struct wlr_xwayland *xwayland = server.xwayland.wlr_xwayland; + wlr_xwayland_set_seat(xwayland, seat->wlr_seat); + seat_set_focus_surface(seat, xsurface->surface, false); + transaction_commit_dirty(); + seat_pointer_notify_button(seat, time_msec, button, state); + } +#endif + + seat_pointer_notify_button(seat, time_msec, button, state); +} + +/*------------------------------------------\ + * Functions used by handle_pointer_motion / + *----------------------------------------*/ + +static void check_focus_follows_mouse(struct sway_seat *seat, + struct seatop_default_event *e, struct sway_node *hovered_node) { + struct sway_node *focus = seat_get_focus(seat); + + // This is the case if a layer-shell surface is hovered. + // If it's on another output, focus the active workspace there. + if (!hovered_node) { + struct wlr_output *wlr_output = wlr_output_layout_output_at( + root->output_layout, seat->cursor->cursor->x, seat->cursor->cursor->y); + if (wlr_output == NULL) { + return; + } + + struct wlr_surface *surface = NULL; + double sx, sy; + node_at_coords(seat, seat->cursor->cursor->x, seat->cursor->cursor->y, + &surface, &sx, &sy); + + // Focus topmost layer surface + struct wlr_layer_surface_v1 *layer = NULL; + if ((layer = toplevel_layer_surface_from_surface(surface)) && + layer->current.keyboard_interactive) { + seat_set_focus_layer(seat, layer); + transaction_commit_dirty(); + return; + } + + struct sway_output *hovered_output = wlr_output->data; + if (focus && hovered_output != node_get_output(focus)) { + struct sway_workspace *ws = output_get_active_workspace(hovered_output); + seat_set_focus(seat, &ws->node); + transaction_commit_dirty(); + } + return; + } + + // If a workspace node is hovered (eg. in the gap area), only set focus if + // the workspace is on a different output to the previous focus. + if (focus && hovered_node->type == N_WORKSPACE) { + struct sway_output *focused_output = node_get_output(focus); + struct sway_output *hovered_output = node_get_output(hovered_node); + if (hovered_output != focused_output) { + seat_set_focus(seat, seat_get_focus_inactive(seat, hovered_node)); + transaction_commit_dirty(); + } + return; + } + + // This is where we handle the common case. We don't want to focus inactive + // tabs, hence the view_is_visible check. + if (node_is_view(hovered_node) && + view_is_visible(hovered_node->sway_container->view)) { + // e->previous_node is the node which the cursor was over previously. + // If focus_follows_mouse is yes and the cursor got over the view due + // to, say, a workspace switch, we don't want to set the focus. + // But if focus_follows_mouse is "always", we do. + if (hovered_node != e->previous_node || + config->focus_follows_mouse == FOLLOWS_ALWAYS) { + seat_set_focus(seat, hovered_node); + transaction_commit_dirty(); + } + } +} + +static void handle_pointer_motion(struct sway_seat *seat, uint32_t time_msec) { + struct seatop_default_event *e = seat->seatop_data; + struct sway_cursor *cursor = seat->cursor; + + struct wlr_surface *surface = NULL; + double sx, sy; + struct sway_node *node = node_at_coords(seat, + cursor->cursor->x, cursor->cursor->y, &surface, &sx, &sy); + + if (config->focus_follows_mouse != FOLLOWS_NO) { + check_focus_follows_mouse(seat, e, node); + } + + if (surface) { + if (seat_is_input_allowed(seat, surface)) { + wlr_seat_pointer_notify_enter(seat->wlr_seat, surface, sx, sy); + wlr_seat_pointer_notify_motion(seat->wlr_seat, time_msec, sx, sy); + } + } else { + cursor_update_image(cursor, node); + wlr_seat_pointer_notify_clear_focus(seat->wlr_seat); + } + + drag_icons_update_position(seat); + + e->previous_node = node; +} + +static void handle_tablet_tool_motion(struct sway_seat *seat, + struct sway_tablet_tool *tool, uint32_t time_msec) { + struct seatop_default_event *e = seat->seatop_data; + struct sway_cursor *cursor = seat->cursor; + + struct wlr_surface *surface = NULL; + double sx, sy; + struct sway_node *node = node_at_coords(seat, + cursor->cursor->x, cursor->cursor->y, &surface, &sx, &sy); + + if (config->focus_follows_mouse != FOLLOWS_NO) { + check_focus_follows_mouse(seat, e, node); + } + + if (surface) { + if (seat_is_input_allowed(seat, surface)) { + wlr_tablet_v2_tablet_tool_notify_proximity_in(tool->tablet_v2_tool, + tool->tablet->tablet_v2, surface); + wlr_tablet_v2_tablet_tool_notify_motion(tool->tablet_v2_tool, sx, sy); + } + } else { + cursor_update_image(cursor, node); + wlr_tablet_v2_tablet_tool_notify_proximity_out(tool->tablet_v2_tool); + } + + drag_icons_update_position(seat); + + e->previous_node = node; +} + +static void handle_touch_down(struct sway_seat *seat, + struct wlr_touch_down_event *event, double lx, double ly) { + struct wlr_surface *surface = NULL; + struct wlr_seat *wlr_seat = seat->wlr_seat; + struct sway_cursor *cursor = seat->cursor; + double sx, sy; + node_at_coords(seat, seat->touch_x, seat->touch_y, &surface, &sx, &sy); + + if (surface && wlr_surface_accepts_touch(surface, wlr_seat)) { + if (seat_is_input_allowed(seat, surface)) { + cursor->simulating_pointer_from_touch = false; + seatop_begin_touch_down(seat, surface, event, sx, sy, lx, ly); + } + } else if (!cursor->simulating_pointer_from_touch && + (!surface || seat_is_input_allowed(seat, surface))) { + // Fallback to cursor simulation. + // The pointer_touch_id state is needed, so drags are not aborted when over + // a surface supporting touch and multi touch events don't interfere. + cursor->simulating_pointer_from_touch = true; + cursor->pointer_touch_id = seat->touch_id; + double dx, dy; + dx = seat->touch_x - cursor->cursor->x; + dy = seat->touch_y - cursor->cursor->y; + pointer_motion(cursor, event->time_msec, &event->touch->base, dx, dy, + dx, dy); + dispatch_cursor_button(cursor, &event->touch->base, event->time_msec, + BTN_LEFT, WL_POINTER_BUTTON_STATE_PRESSED); + } +} + +/*----------------------------------------\ + * Functions used by handle_pointer_axis / + *--------------------------------------*/ + +static uint32_t wl_axis_to_button(struct wlr_pointer_axis_event *event) { + switch (event->orientation) { + case WL_POINTER_AXIS_VERTICAL_SCROLL: + return event->delta < 0 ? SWAY_SCROLL_UP : SWAY_SCROLL_DOWN; + case WL_POINTER_AXIS_HORIZONTAL_SCROLL: + return event->delta < 0 ? SWAY_SCROLL_LEFT : SWAY_SCROLL_RIGHT; + default: + sway_log(SWAY_DEBUG, "Unknown axis orientation"); + return 0; + } +} + +static void handle_pointer_axis(struct sway_seat *seat, + struct wlr_pointer_axis_event *event) { + struct sway_input_device *input_device = + event->pointer ? event->pointer->base.data : NULL; + struct input_config *ic = + input_device ? input_device_get_config(input_device) : NULL; + struct sway_cursor *cursor = seat->cursor; + struct seatop_default_event *e = seat->seatop_data; + + // Determine what's under the cursor + struct wlr_surface *surface = NULL; + double sx, sy; + struct sway_node *node = node_at_coords(seat, + cursor->cursor->x, cursor->cursor->y, &surface, &sx, &sy); + struct sway_container *cont = node && node->type == N_CONTAINER ? + node->sway_container : NULL; + enum wlr_edges edge = cont ? find_edge(cont, surface, cursor) : WLR_EDGE_NONE; + bool on_border = edge != WLR_EDGE_NONE; + bool on_titlebar = cont && !on_border && !surface; + bool on_titlebar_border = cont && on_border && + cursor->cursor->y < cont->pending.content_y; + bool on_contents = cont && !on_border && surface; + bool on_workspace = node && node->type == N_WORKSPACE; + float scroll_factor = + (ic == NULL || ic->scroll_factor == FLT_MIN) ? 1.0f : ic->scroll_factor; + + bool handled = false; + + // Gather information needed for mouse bindings + struct wlr_keyboard *keyboard = wlr_seat_get_keyboard(seat->wlr_seat); + uint32_t modifiers = keyboard ? wlr_keyboard_get_modifiers(keyboard) : 0; + struct wlr_input_device *device = + input_device ? input_device->wlr_device : NULL; + char *dev_id = device ? input_device_get_identifier(device) : strdup("*"); + uint32_t button = wl_axis_to_button(event); + + // Handle mouse bindings - x11 mouse buttons 4-7 - press event + struct sway_binding *binding = NULL; + state_add_button(e, button); + binding = get_active_mouse_binding(e, config->current_mode->mouse_bindings, + modifiers, false, on_titlebar, on_border, on_contents, on_workspace, + dev_id); + if (binding) { + seat_execute_command(seat, binding); + handled = true; + } + + // Scrolling on a tabbed or stacked title bar (handled as press event) + if (!handled && (on_titlebar || on_titlebar_border)) { + struct sway_node *new_focus; + enum sway_container_layout layout = container_parent_layout(cont); + if (layout == L_TABBED || layout == L_STACKED) { + struct sway_node *tabcontainer = node_get_parent(node); + struct sway_node *active = + seat_get_active_tiling_child(seat, tabcontainer); + list_t *siblings = container_get_siblings(cont); + int desired = list_find(siblings, active->sway_container) + + roundf(scroll_factor * event->delta_discrete / WLR_POINTER_AXIS_DISCRETE_STEP); + if (desired < 0) { + desired = 0; + } else if (desired >= siblings->length) { + desired = siblings->length - 1; + } + + struct sway_container *new_sibling_con = siblings->items[desired]; + struct sway_node *new_sibling = &new_sibling_con->node; + // Use the focused child of the tabbed/stacked container, not the + // container the user scrolled on. + new_focus = seat_get_focus_inactive(seat, new_sibling); + } else { + new_focus = seat_get_focus_inactive(seat, &cont->node); + } + + seat_set_focus(seat, new_focus); + transaction_commit_dirty(); + handled = true; + } + + // Handle mouse bindings - x11 mouse buttons 4-7 - release event + binding = get_active_mouse_binding(e, config->current_mode->mouse_bindings, + modifiers, true, on_titlebar, on_border, on_contents, on_workspace, + dev_id); + state_erase_button(e, button); + if (binding) { + seat_execute_command(seat, binding); + handled = true; + } + free(dev_id); + + if (!handled) { + wlr_seat_pointer_notify_axis(cursor->seat->wlr_seat, event->time_msec, + event->orientation, scroll_factor * event->delta, + roundf(scroll_factor * event->delta_discrete), event->source, + event->relative_direction); + } +} + +/*------------------------------------\ + * Functions used by gesture support / + *----------------------------------*/ + +/** + * Check gesture binding for a specific gesture type and finger count. + * Returns true if binding is present, false otherwise + */ +static bool gesture_binding_check(list_t *bindings, enum gesture_type type, + uint8_t fingers, struct sway_input_device *device) { + char *input = + device ? input_device_get_identifier(device->wlr_device) : strdup("*"); + + for (int i = 0; i < bindings->length; ++i) { + struct sway_gesture_binding *binding = bindings->items[i]; + + // Check type and finger count + if (!gesture_check(&binding->gesture, type, fingers)) { + continue; + } + + // Check that input matches + if (strcmp(binding->input, "*") != 0 && + strcmp(binding->input, input) != 0) { + continue; + } + + free(input); + + return true; + } + + free(input); + + return false; +} + +/** + * Return the gesture binding which matches gesture type, finger count + * and direction, otherwise return null. + */ +static struct sway_gesture_binding* gesture_binding_match( + list_t *bindings, struct gesture *gesture, const char *input) { + struct sway_gesture_binding *current = NULL; + + // Find best matching binding + for (int i = 0; i < bindings->length; ++i) { + struct sway_gesture_binding *binding = bindings->items[i]; + bool exact = binding->flags & BINDING_EXACT; + + // Check gesture matching + if (!gesture_match(&binding->gesture, gesture, exact)) { + continue; + } + + // Check input matching + if (strcmp(binding->input, "*") != 0 && + strcmp(binding->input, input) != 0) { + continue; + } + + // If we already have a match ... + if (current) { + // ... check if input matching is equivalent + if (strcmp(current->input, binding->input) == 0) { + + // ... - do not override an exact binding + if (!exact && current->flags & BINDING_EXACT) { + continue; + } + + // ... - and ensure direction matching is better or equal + if (gesture_compare(¤t->gesture, &binding->gesture) > 0) { + continue; + } + } else if (strcmp(binding->input, "*") == 0) { + // ... do not accept worse input match + continue; + } + } + + // Accept newer or better match + current = binding; + + // If exact binding and input is found, quit search + if (strcmp(current->input, input) == 0 && + gesture_compare(¤t->gesture, gesture) == 0) { + break; + } + } // for all gesture bindings + + return current; +} + +// Wrapper around gesture_tracker_end to use tracker with sway bindings +static struct sway_gesture_binding* gesture_tracker_end_and_match( + struct gesture_tracker *tracker, struct sway_input_device* device) { + // Determine name of input that received gesture + char *input = device + ? input_device_get_identifier(device->wlr_device) + : strdup("*"); + + // Match tracking result to binding + struct gesture *gesture = gesture_tracker_end(tracker); + struct sway_gesture_binding *binding = gesture_binding_match( + config->current_mode->gesture_bindings, gesture, input); + free(gesture); + free(input); + + return binding; +} + +// Small wrapper around seat_execute_command to work on gesture bindings +static void gesture_binding_execute(struct sway_seat *seat, + struct sway_gesture_binding *binding) { + struct sway_binding *dummy_binding = + calloc(1, sizeof(struct sway_binding)); + dummy_binding->type = BINDING_GESTURE; + dummy_binding->command = binding->command; + + char *description = gesture_to_string(&binding->gesture); + sway_log(SWAY_DEBUG, "executing gesture binding: %s", description); + free(description); + + seat_execute_command(seat, dummy_binding); + + free(dummy_binding); +} + +static void handle_hold_begin(struct sway_seat *seat, + struct wlr_pointer_hold_begin_event *event) { + // Start tracking gesture if there is a matching binding ... + struct sway_input_device *device = + event->pointer ? event->pointer->base.data : NULL; + list_t *bindings = config->current_mode->gesture_bindings; + if (gesture_binding_check(bindings, GESTURE_TYPE_HOLD, event->fingers, device)) { + struct seatop_default_event *seatop = seat->seatop_data; + gesture_tracker_begin(&seatop->gestures, GESTURE_TYPE_HOLD, event->fingers); + } else { + // ... otherwise forward to client + struct sway_cursor *cursor = seat->cursor; + wlr_pointer_gestures_v1_send_hold_begin( + server.input->pointer_gestures, cursor->seat->wlr_seat, + event->time_msec, event->fingers); + } +} + +static void handle_hold_end(struct sway_seat *seat, + struct wlr_pointer_hold_end_event *event) { + // Ensure that gesture is being tracked and was not cancelled + struct seatop_default_event *seatop = seat->seatop_data; + if (!gesture_tracker_check(&seatop->gestures, GESTURE_TYPE_HOLD)) { + struct sway_cursor *cursor = seat->cursor; + wlr_pointer_gestures_v1_send_hold_end( + server.input->pointer_gestures, cursor->seat->wlr_seat, + event->time_msec, event->cancelled); + return; + } + if (event->cancelled) { + gesture_tracker_cancel(&seatop->gestures); + return; + } + + // End gesture tracking and execute matched binding + struct sway_input_device *device = + event->pointer ? event->pointer->base.data : NULL; + struct sway_gesture_binding *binding = gesture_tracker_end_and_match( + &seatop->gestures, device); + + if (binding) { + gesture_binding_execute(seat, binding); + } +} + +static void handle_pinch_begin(struct sway_seat *seat, + struct wlr_pointer_pinch_begin_event *event) { + // Start tracking gesture if there is a matching binding ... + struct sway_input_device *device = + event->pointer ? event->pointer->base.data : NULL; + list_t *bindings = config->current_mode->gesture_bindings; + if (gesture_binding_check(bindings, GESTURE_TYPE_PINCH, event->fingers, device)) { + struct seatop_default_event *seatop = seat->seatop_data; + gesture_tracker_begin(&seatop->gestures, GESTURE_TYPE_PINCH, event->fingers); + } else { + // ... otherwise forward to client + struct sway_cursor *cursor = seat->cursor; + wlr_pointer_gestures_v1_send_pinch_begin( + server.input->pointer_gestures, cursor->seat->wlr_seat, + event->time_msec, event->fingers); + } +} + +static void handle_pinch_update(struct sway_seat *seat, + struct wlr_pointer_pinch_update_event *event) { + // Update any ongoing tracking ... + struct seatop_default_event *seatop = seat->seatop_data; + if (gesture_tracker_check(&seatop->gestures, GESTURE_TYPE_PINCH)) { + gesture_tracker_update(&seatop->gestures, event->dx, event->dy, + event->scale, event->rotation); + } else { + // ... otherwise forward to client + struct sway_cursor *cursor = seat->cursor; + wlr_pointer_gestures_v1_send_pinch_update( + server.input->pointer_gestures, + cursor->seat->wlr_seat, + event->time_msec, event->dx, event->dy, + event->scale, event->rotation); + } +} + +static void handle_pinch_end(struct sway_seat *seat, + struct wlr_pointer_pinch_end_event *event) { + // Ensure that gesture is being tracked and was not cancelled + struct seatop_default_event *seatop = seat->seatop_data; + if (!gesture_tracker_check(&seatop->gestures, GESTURE_TYPE_PINCH)) { + struct sway_cursor *cursor = seat->cursor; + wlr_pointer_gestures_v1_send_pinch_end( + server.input->pointer_gestures, cursor->seat->wlr_seat, + event->time_msec, event->cancelled); + return; + } + if (event->cancelled) { + gesture_tracker_cancel(&seatop->gestures); + return; + } + + // End gesture tracking and execute matched binding + struct sway_input_device *device = + event->pointer ? event->pointer->base.data : NULL; + struct sway_gesture_binding *binding = gesture_tracker_end_and_match( + &seatop->gestures, device); + + if (binding) { + gesture_binding_execute(seat, binding); + } +} + +static void handle_swipe_begin(struct sway_seat *seat, + struct wlr_pointer_swipe_begin_event *event) { + // Start tracking gesture if there is a matching binding ... + struct sway_input_device *device = + event->pointer ? event->pointer->base.data : NULL; + list_t *bindings = config->current_mode->gesture_bindings; + if (gesture_binding_check(bindings, GESTURE_TYPE_SWIPE, event->fingers, device)) { + struct seatop_default_event *seatop = seat->seatop_data; + gesture_tracker_begin(&seatop->gestures, GESTURE_TYPE_SWIPE, event->fingers); + } else { + // ... otherwise forward to client + struct sway_cursor *cursor = seat->cursor; + wlr_pointer_gestures_v1_send_swipe_begin( + server.input->pointer_gestures, cursor->seat->wlr_seat, + event->time_msec, event->fingers); + } +} + +static void handle_swipe_update(struct sway_seat *seat, + struct wlr_pointer_swipe_update_event *event) { + + // Update any ongoing tracking ... + struct seatop_default_event *seatop = seat->seatop_data; + if (gesture_tracker_check(&seatop->gestures, GESTURE_TYPE_SWIPE)) { + gesture_tracker_update(&seatop->gestures, + event->dx, event->dy, NAN, NAN); + } else { + // ... otherwise forward to client + struct sway_cursor *cursor = seat->cursor; + wlr_pointer_gestures_v1_send_swipe_update( + server.input->pointer_gestures, cursor->seat->wlr_seat, + event->time_msec, event->dx, event->dy); + } +} + +static void handle_swipe_end(struct sway_seat *seat, + struct wlr_pointer_swipe_end_event *event) { + // Ensure gesture is being tracked and was not cancelled + struct seatop_default_event *seatop = seat->seatop_data; + if (!gesture_tracker_check(&seatop->gestures, GESTURE_TYPE_SWIPE)) { + struct sway_cursor *cursor = seat->cursor; + wlr_pointer_gestures_v1_send_swipe_end(server.input->pointer_gestures, + cursor->seat->wlr_seat, event->time_msec, event->cancelled); + return; + } + if (event->cancelled) { + gesture_tracker_cancel(&seatop->gestures); + return; + } + + // End gesture tracking and execute matched binding + struct sway_input_device *device = + event->pointer ? event->pointer->base.data : NULL; + struct sway_gesture_binding *binding = gesture_tracker_end_and_match( + &seatop->gestures, device); + + if (binding) { + gesture_binding_execute(seat, binding); + } +} + +/*----------------------------------\ + * Functions used by handle_rebase / + *--------------------------------*/ + +static void handle_rebase(struct sway_seat *seat, uint32_t time_msec) { + struct seatop_default_event *e = seat->seatop_data; + struct sway_cursor *cursor = seat->cursor; + struct wlr_surface *surface = NULL; + double sx = 0.0, sy = 0.0; + e->previous_node = node_at_coords(seat, + cursor->cursor->x, cursor->cursor->y, &surface, &sx, &sy); + + if (surface) { + if (seat_is_input_allowed(seat, surface) && !cursor->hidden) { + wlr_seat_pointer_notify_enter(seat->wlr_seat, surface, sx, sy); + wlr_seat_pointer_notify_motion(seat->wlr_seat, time_msec, sx, sy); + } + } else { + cursor_update_image(cursor, e->previous_node); + wlr_seat_pointer_notify_clear_focus(seat->wlr_seat); + } +} + +static const struct sway_seatop_impl seatop_impl = { + .button = handle_button, + .pointer_motion = handle_pointer_motion, + .pointer_axis = handle_pointer_axis, + .tablet_tool_tip = handle_tablet_tool_tip, + .tablet_tool_motion = handle_tablet_tool_motion, + .hold_begin = handle_hold_begin, + .hold_end = handle_hold_end, + .pinch_begin = handle_pinch_begin, + .pinch_update = handle_pinch_update, + .pinch_end = handle_pinch_end, + .swipe_begin = handle_swipe_begin, + .swipe_update = handle_swipe_update, + .swipe_end = handle_swipe_end, + .touch_down = handle_touch_down, + .rebase = handle_rebase, + .allow_set_cursor = true, +}; + +void seatop_begin_default(struct sway_seat *seat) { + seatop_end(seat); + + struct seatop_default_event *e = + calloc(1, sizeof(struct seatop_default_event)); + sway_assert(e, "Unable to allocate seatop_default_event"); + + seat->seatop_impl = &seatop_impl; + seat->seatop_data = e; + + uint32_t time_msec = get_current_time_in_msec(); + seatop_rebase(seat, time_msec); +} diff --git a/sway/input/seatop_down.c b/sway/input/seatop_down.c new file mode 100644 index 0000000..f64e5a4 --- /dev/null +++ b/sway/input/seatop_down.c @@ -0,0 +1,264 @@ +#include +#include +#include +#include +#include "sway/input/cursor.h" +#include "sway/input/seat.h" +#include "sway/tree/view.h" +#include "sway/desktop/transaction.h" +#include "log.h" + +struct seatop_touch_point_event { + double ref_lx, ref_ly; // touch's x/y at start of op + double ref_con_lx, ref_con_ly; // container's x/y at start of op + int32_t touch_id; + struct wl_list link; +}; + +struct seatop_down_event { + struct sway_container *con; + struct sway_seat *seat; + struct wl_listener surface_destroy; + struct wlr_surface *surface; + double ref_lx, ref_ly; // cursor's x/y at start of op + double ref_con_lx, ref_con_ly; // container's x/y at start of op + struct wl_list point_events; // seatop_touch_point_event::link +}; + +static void handle_touch_motion(struct sway_seat *seat, + struct wlr_touch_motion_event *event, double lx, double ly) { + struct seatop_down_event *e = seat->seatop_data; + + struct seatop_touch_point_event *point_event; + bool found = false; + wl_list_for_each(point_event, &e->point_events, link) { + if (point_event->touch_id == event->touch_id) { + found = true; + break; + } + } + if (!found) { + return; // Probably not a point_event from this seatop_down + } + + double moved_x = lx - point_event->ref_lx; + double moved_y = ly - point_event->ref_ly; + double sx = point_event->ref_con_lx + moved_x; + double sy = point_event->ref_con_ly + moved_y; + + wlr_seat_touch_notify_motion(seat->wlr_seat, event->time_msec, + event->touch_id, sx, sy); +} + +static void handle_touch_up(struct sway_seat *seat, + struct wlr_touch_up_event *event) { + struct seatop_down_event *e = seat->seatop_data; + struct seatop_touch_point_event *point_event, *tmp; + + wl_list_for_each_safe(point_event, tmp, &e->point_events, link) { + if (point_event->touch_id == event->touch_id) { + wl_list_remove(&point_event->link); + free(point_event); + break; + } + } + + wlr_seat_touch_notify_up(seat->wlr_seat, event->time_msec, event->touch_id); + + if (wl_list_empty(&e->point_events)) { + seatop_begin_default(seat); + } +} + +static void handle_touch_down(struct sway_seat *seat, + struct wlr_touch_down_event *event, double lx, double ly) { + struct seatop_down_event *e = seat->seatop_data; + double sx, sy; + struct wlr_surface *surface = NULL; + struct sway_node *focused_node = node_at_coords(seat, seat->touch_x, + seat->touch_y, &surface, &sx, &sy); + + if (!surface || surface != e->surface) { // Must start from the initial surface + return; + } + + struct seatop_touch_point_event *point_event = + calloc(1, sizeof(struct seatop_touch_point_event)); + if (!sway_assert(point_event, "Unable to allocate point_event")) { + return; + } + point_event->touch_id = event->touch_id; + point_event->ref_lx = lx; + point_event->ref_ly = ly; + point_event->ref_con_lx = sx; + point_event->ref_con_ly = sy; + + wl_list_insert(&e->point_events, &point_event->link); + + wlr_seat_touch_notify_down(seat->wlr_seat, surface, event->time_msec, + event->touch_id, sx, sy); + + if (focused_node) { + seat_set_focus(seat, focused_node); + transaction_commit_dirty(); + } +} + +static void handle_touch_cancel(struct sway_seat *seat, + struct wlr_touch_cancel_event *event) { + struct seatop_down_event *e = seat->seatop_data; + struct seatop_touch_point_event *point_event, *tmp; + + wl_list_for_each_safe(point_event, tmp, &e->point_events, link) { + if (point_event->touch_id == event->touch_id) { + wl_list_remove(&point_event->link); + free(point_event); + break; + } + } + + if (e->surface) { + struct wl_client *client = wl_resource_get_client(e->surface->resource); + struct wlr_seat_client *seat_client = wlr_seat_client_for_wl_client(seat->wlr_seat, client); + if (seat_client != NULL) { + wlr_seat_touch_notify_cancel(seat->wlr_seat, seat_client); + } + } + + if (wl_list_empty(&e->point_events)) { + seatop_begin_default(seat); + } +} + +static void handle_pointer_axis(struct sway_seat *seat, + struct wlr_pointer_axis_event *event) { + struct sway_input_device *input_device = + event->pointer ? event->pointer->base.data : NULL; + struct input_config *ic = + input_device ? input_device_get_config(input_device) : NULL; + float scroll_factor = + (ic == NULL || ic->scroll_factor == FLT_MIN) ? 1.0f : ic->scroll_factor; + + wlr_seat_pointer_notify_axis(seat->wlr_seat, event->time_msec, + event->orientation, scroll_factor * event->delta, + roundf(scroll_factor * event->delta_discrete), event->source, + event->relative_direction); +} + +static void handle_button(struct sway_seat *seat, uint32_t time_msec, + struct wlr_input_device *device, uint32_t button, + enum wl_pointer_button_state state) { + seat_pointer_notify_button(seat, time_msec, button, state); + + if (seat->cursor->pressed_button_count == 0) { + seatop_begin_default(seat); + } +} + +static void handle_pointer_motion(struct sway_seat *seat, uint32_t time_msec) { + struct seatop_down_event *e = seat->seatop_data; + if (seat_is_input_allowed(seat, e->surface)) { + double moved_x = seat->cursor->cursor->x - e->ref_lx; + double moved_y = seat->cursor->cursor->y - e->ref_ly; + double sx = e->ref_con_lx + moved_x; + double sy = e->ref_con_ly + moved_y; + wlr_seat_pointer_notify_motion(seat->wlr_seat, time_msec, sx, sy); + } +} + +static void handle_tablet_tool_tip(struct sway_seat *seat, + struct sway_tablet_tool *tool, uint32_t time_msec, + enum wlr_tablet_tool_tip_state state) { + if (state == WLR_TABLET_TOOL_TIP_UP) { + wlr_tablet_v2_tablet_tool_notify_up(tool->tablet_v2_tool); + seatop_begin_default(seat); + } +} + +static void handle_tablet_tool_motion(struct sway_seat *seat, + struct sway_tablet_tool *tool, uint32_t time_msec) { + struct seatop_down_event *e = seat->seatop_data; + if (seat_is_input_allowed(seat, e->surface)) { + double moved_x = seat->cursor->cursor->x - e->ref_lx; + double moved_y = seat->cursor->cursor->y - e->ref_ly; + double sx = e->ref_con_lx + moved_x; + double sy = e->ref_con_ly + moved_y; + wlr_tablet_v2_tablet_tool_notify_motion(tool->tablet_v2_tool, sx, sy); + } +} + +static void handle_destroy(struct wl_listener *listener, void *data) { + struct seatop_down_event *e = + wl_container_of(listener, e, surface_destroy); + if (e) { + seatop_begin_default(e->seat); + } +} + +static void handle_unref(struct sway_seat *seat, struct sway_container *con) { + struct seatop_down_event *e = seat->seatop_data; + if (e->con == con) { + seatop_begin_default(seat); + } +} + +static void handle_end(struct sway_seat *seat) { + struct seatop_down_event *e = seat->seatop_data; + wl_list_remove(&e->surface_destroy.link); +} + +static const struct sway_seatop_impl seatop_impl = { + .button = handle_button, + .pointer_motion = handle_pointer_motion, + .pointer_axis = handle_pointer_axis, + .tablet_tool_tip = handle_tablet_tool_tip, + .tablet_tool_motion = handle_tablet_tool_motion, + .touch_motion = handle_touch_motion, + .touch_up = handle_touch_up, + .touch_down = handle_touch_down, + .touch_cancel = handle_touch_cancel, + .unref = handle_unref, + .end = handle_end, + .allow_set_cursor = true, +}; + +void seatop_begin_down(struct sway_seat *seat, struct sway_container *con, + double sx, double sy) { + seatop_begin_down_on_surface(seat, con->view->surface, sx, sy); + struct seatop_down_event *e = seat->seatop_data; + e->con = con; + + container_raise_floating(con); + transaction_commit_dirty(); +} + +void seatop_begin_touch_down(struct sway_seat *seat, + struct wlr_surface *surface, struct wlr_touch_down_event *event, + double sx, double sy, double lx, double ly) { + seatop_begin_down_on_surface(seat, surface, sx, sy); + handle_touch_down(seat, event, lx, ly); +} + +void seatop_begin_down_on_surface(struct sway_seat *seat, + struct wlr_surface *surface, double sx, double sy) { + seatop_end(seat); + + struct seatop_down_event *e = + calloc(1, sizeof(struct seatop_down_event)); + if (!sway_assert(e, "Unable to allocate e")) { + return; + } + e->con = NULL; + e->seat = seat; + e->surface = surface; + wl_signal_add(&e->surface->events.destroy, &e->surface_destroy); + e->surface_destroy.notify = handle_destroy; + e->ref_lx = seat->cursor->cursor->x; + e->ref_ly = seat->cursor->cursor->y; + e->ref_con_lx = sx; + e->ref_con_ly = sy; + wl_list_init(&e->point_events); + + seat->seatop_impl = &seatop_impl; + seat->seatop_data = e; +} diff --git a/sway/input/seatop_move_floating.c b/sway/input/seatop_move_floating.c new file mode 100644 index 0000000..37b7aad --- /dev/null +++ b/sway/input/seatop_move_floating.c @@ -0,0 +1,87 @@ +#include +#include "sway/desktop/transaction.h" +#include "sway/input/cursor.h" +#include "sway/input/seat.h" + +struct seatop_move_floating_event { + struct sway_container *con; + double dx, dy; // cursor offset in container +}; + +static void finalize_move(struct sway_seat *seat) { + struct seatop_move_floating_event *e = seat->seatop_data; + + // We "move" the container to its own location + // so it discovers its output again. + container_floating_move_to(e->con, e->con->pending.x, e->con->pending.y); + transaction_commit_dirty(); + + e->con->animation_state.seat_is_moving_float = false; + + seatop_begin_default(seat); +} + +static void handle_button(struct sway_seat *seat, uint32_t time_msec, + struct wlr_input_device *device, uint32_t button, + enum wl_pointer_button_state state) { + if (seat->cursor->pressed_button_count == 0) { + finalize_move(seat); + } +} + +static void handle_tablet_tool_tip(struct sway_seat *seat, + struct sway_tablet_tool *tool, uint32_t time_msec, + enum wlr_tablet_tool_tip_state state) { + if (state == WLR_TABLET_TOOL_TIP_UP) { + finalize_move(seat); + } +} +static void handle_pointer_motion(struct sway_seat *seat, uint32_t time_msec) { + struct seatop_move_floating_event *e = seat->seatop_data; + struct wlr_cursor *cursor = seat->cursor->cursor; + + e->con->animation_state.seat_is_moving_float = true; + + container_floating_move_to(e->con, cursor->x - e->dx, cursor->y - e->dy); + transaction_commit_dirty(); +} + +static void handle_unref(struct sway_seat *seat, struct sway_container *con) { + struct seatop_move_floating_event *e = seat->seatop_data; + if (e->con == con) { + seatop_begin_default(seat); + } +} + +static const struct sway_seatop_impl seatop_impl = { + .button = handle_button, + .pointer_motion = handle_pointer_motion, + .tablet_tool_tip = handle_tablet_tool_tip, + .unref = handle_unref, +}; + +void seatop_begin_move_floating(struct sway_seat *seat, + struct sway_container *con) { + seatop_end(seat); + + struct sway_cursor *cursor = seat->cursor; + struct seatop_move_floating_event *e = + calloc(1, sizeof(struct seatop_move_floating_event)); + if (!e) { + return; + } + e->con = con; + e->dx = cursor->cursor->x - con->pending.x; + e->dy = cursor->cursor->y - con->pending.y; + + con->animation_state.seat_is_moving_float = true; + + seat->seatop_impl = &seatop_impl; + seat->seatop_data = e; + + container_raise_floating(con); + transaction_commit_dirty(); + + cursor_set_image(cursor, "grab", NULL); + wlr_seat_pointer_notify_clear_focus(seat->wlr_seat); +} diff --git a/sway/input/seatop_move_tiling.c b/sway/input/seatop_move_tiling.c new file mode 100644 index 0000000..69fbe6b --- /dev/null +++ b/sway/input/seatop_move_tiling.c @@ -0,0 +1,516 @@ +#include +#include +#include +#include "sway/desktop/transaction.h" +#include "sway/input/cursor.h" +#include "sway/input/seat.h" +#include "sway/ipc-server.h" +#include "sway/output.h" +#include "sway/tree/arrange.h" +#include "sway/tree/node.h" +#include "sway/tree/view.h" +#include "sway/tree/workspace.h" + +// Thickness of the dropzone when dragging to the edge of a layout container +#define DROP_LAYOUT_BORDER 30 + +// Thickness of indicator when dropping onto a titlebar. This should be a +// multiple of 2. +#define DROP_SPLIT_INDICATOR 10 + +struct seatop_move_tiling_event { + struct sway_container *con; + struct sway_node *target_node; + enum wlr_edges target_edge; + double ref_lx, ref_ly; // cursor's x/y at start of op + bool threshold_reached; + bool split_target; + bool insert_after_target; + struct wlr_scene_rect *indicator_rect; + struct wlr_scene_blur *indicator_blur; +}; + +static void handle_end(struct sway_seat *seat) { + struct seatop_move_tiling_event *e = seat->seatop_data; + wlr_scene_node_destroy(&e->indicator_rect->node); + wlr_scene_node_destroy(&e->indicator_blur->node); + e->indicator_rect = NULL; +} + +static void handle_motion_prethreshold(struct sway_seat *seat) { + struct seatop_move_tiling_event *e = seat->seatop_data; + double cx = seat->cursor->cursor->x; + double cy = seat->cursor->cursor->y; + double sx = e->ref_lx; + double sy = e->ref_ly; + + // Get the scaled threshold for the output. Even if the operation goes + // across multiple outputs of varying scales, just use the scale for the + // output that the cursor is currently on for simplicity. + struct wlr_output *wlr_output = wlr_output_layout_output_at( + root->output_layout, cx, cy); + double output_scale = wlr_output ? wlr_output->scale : 1; + double threshold = config->tiling_drag_threshold * output_scale; + threshold *= threshold; + + // If the threshold has been exceeded, start the actual drag + if ((cx - sx) * (cx - sx) + (cy - sy) * (cy - sy) > threshold) { + wlr_scene_node_set_enabled(&e->indicator_rect->node, true); + wlr_scene_node_set_enabled(&e->indicator_blur->node, true); + e->threshold_reached = true; + cursor_set_image(seat->cursor, "grab", NULL); + } +} + +static void resize_box(struct wlr_box *box, enum wlr_edges edge, + int thickness) { + switch (edge) { + case WLR_EDGE_TOP: + box->height = thickness; + break; + case WLR_EDGE_LEFT: + box->width = thickness; + break; + case WLR_EDGE_RIGHT: + box->x = box->x + box->width - thickness; + box->width = thickness; + break; + case WLR_EDGE_BOTTOM: + box->y = box->y + box->height - thickness; + box->height = thickness; + break; + case WLR_EDGE_NONE: + box->x += thickness; + box->y += thickness; + box->width -= thickness * 2; + box->height -= thickness * 2; + break; + } +} + +static void split_border(double pos, int offset, int len, int n_children, + int avoid, int *out_pos, bool *out_after) { + int region = 2 * n_children * (pos - offset) / len; + // If the cursor is over the right side of a left-adjacent titlebar, or the + // left side of a right-adjacent titlebar, it's position when dropped will + // be the same. To avoid this, shift the region for adjacent containers. + if (avoid >= 0) { + if (region == 2 * avoid - 1 || region == 2 * avoid) { + region--; + } else if (region == 2 * avoid + 1 || region == 2 * avoid + 2) { + region++; + } + } + + int child_index = (region + 1) / 2; + *out_after = region % 2; + // When dropping at the beginning or end of a container, show the drop + // region within the container boundary, otherwise show it on top of the + // border between two titlebars. + if (child_index == 0) { + *out_pos = offset; + } else if (child_index == n_children) { + *out_pos = offset + len - DROP_SPLIT_INDICATOR; + } else { + *out_pos = offset + child_index * len / n_children - + DROP_SPLIT_INDICATOR / 2; + } +} + +static bool split_titlebar(struct sway_node *node, struct sway_container *avoid, + struct wlr_cursor *cursor, struct wlr_box *title_box, bool *after) { + struct sway_container *con = node->sway_container; + struct sway_node *parent = &con->pending.parent->node; + int title_height = container_titlebar_height(); + struct wlr_box box; + int n_children, avoid_index; + enum sway_container_layout layout = + parent ? node_get_layout(parent) : L_NONE; + if (layout == L_TABBED || layout == L_STACKED) { + node_get_box(parent, &box); + n_children = node_get_children(parent)->length; + avoid_index = list_find(node_get_children(parent), avoid); + } else { + node_get_box(node, &box); + n_children = 1; + avoid_index = -1; + } + if (layout == L_STACKED && cursor->y < box.y + title_height * n_children) { + // Drop into stacked titlebars. + title_box->width = box.width; + title_box->height = DROP_SPLIT_INDICATOR; + title_box->x = box.x; + split_border(cursor->y, box.y, title_height * n_children, + n_children, avoid_index, &title_box->y, after); + return true; + } else if (layout != L_STACKED && cursor->y < box.y + title_height) { + // Drop into side-by-side titlebars. + title_box->width = DROP_SPLIT_INDICATOR; + title_box->height = title_height; + title_box->y = box.y; + split_border(cursor->x, box.x, box.width, n_children, + avoid_index, &title_box->x, after); + return true; + } + return false; +} + +static void update_indicator(struct seatop_move_tiling_event *e, struct wlr_box *box) { + wlr_scene_node_set_position(&e->indicator_rect->node, box->x, box->y); + wlr_scene_rect_set_size(e->indicator_rect, box->width, box->height); + + wlr_scene_node_set_position(&e->indicator_blur->node, box->x, box->y); + wlr_scene_blur_set_size(e->indicator_blur, box->width, box->height); + + int corner_radius = config->corner_radius; + if (e->con) { + corner_radius = e->con->corner_radius; + // The indicator will be shown above a view if the type is of a container, + // otherwise it'll be shown above a container border/title bar + if (e->target_node && e->target_node->type != N_CONTAINER) { + corner_radius += e->con->current.border_thickness; + } + } + + int corner_radius_real = MIN(corner_radius, MIN(box->width / 2, box->height / 2)); + wlr_scene_rect_set_corner_radii(e->indicator_rect, corner_radii_all(corner_radius_real)); + wlr_scene_blur_set_corner_radii(e->indicator_blur, corner_radii_all(corner_radius_real)); +} + +static void handle_motion_postthreshold(struct sway_seat *seat) { + struct seatop_move_tiling_event *e = seat->seatop_data; + e->split_target = false; + struct wlr_surface *surface = NULL; + double sx, sy; + struct sway_cursor *cursor = seat->cursor; + struct sway_node *node = node_at_coords(seat, + cursor->cursor->x, cursor->cursor->y, &surface, &sx, &sy); + + if (!node) { + // Eg. hovered over a layer surface such as swaybar + e->target_node = NULL; + e->target_edge = WLR_EDGE_NONE; + return; + } + + if (node->type == N_WORKSPACE) { + // Empty workspace + e->target_node = node; + e->target_edge = WLR_EDGE_NONE; + + struct wlr_box drop_box; + workspace_get_box(node->sway_workspace, &drop_box); + update_indicator(e, &drop_box); + return; + } + + // Deny moving within own workspace if this is the only child + struct sway_container *con = node->sway_container; + if (workspace_num_tiling_views(e->con->pending.workspace) == 1 && + con->pending.workspace == e->con->pending.workspace) { + e->target_node = NULL; + e->target_edge = WLR_EDGE_NONE; + return; + } + + struct wlr_box drop_box = { + .x = con->pending.content_x, + .y = con->pending.content_y, + .width = con->pending.content_width, + .height = con->pending.content_height, + }; + + // Check if the cursor is over a tilebar only if the destination + // container is not a descendant of the source container. + if (!surface && !container_has_ancestor(con, e->con) && + split_titlebar(node, e->con, cursor->cursor, + &drop_box, &e->insert_after_target)) { + // Don't allow dropping over the source container's titlebar + // to give users a chance to cancel a drag operation. + if (con == e->con) { + e->target_node = NULL; + } else { + e->target_node = node; + e->split_target = true; + } + e->target_edge = WLR_EDGE_NONE; + update_indicator(e, &drop_box); + return; + } + + // Traverse the ancestors, trying to find a layout container perpendicular + // to the edge. Eg. close to the top or bottom of a horiz layout. + int thresh_top = con->pending.content_y + DROP_LAYOUT_BORDER; + int thresh_bottom = con->pending.content_y + + con->pending.content_height - DROP_LAYOUT_BORDER; + int thresh_left = con->pending.content_x + DROP_LAYOUT_BORDER; + int thresh_right = con->pending.content_x + + con->pending.content_width - DROP_LAYOUT_BORDER; + while (con) { + enum wlr_edges edge = WLR_EDGE_NONE; + enum sway_container_layout layout = container_parent_layout(con); + struct wlr_box box; + node_get_box(node_get_parent(&con->node), &box); + if (layout == L_HORIZ || layout == L_TABBED) { + if (cursor->cursor->y < thresh_top) { + edge = WLR_EDGE_TOP; + if (thresh_top < box.y) thresh_top = box.y; + box.height = thresh_top - box.y; + } else if (cursor->cursor->y > thresh_bottom) { + edge = WLR_EDGE_BOTTOM; + if (thresh_bottom > box.y + box.height) thresh_bottom = box.y + box.height; + box.height = box.y + box.height - thresh_bottom; + box.y = thresh_bottom; + } + } else if (layout == L_VERT || layout == L_STACKED) { + if (cursor->cursor->x < thresh_left) { + edge = WLR_EDGE_LEFT; + if (thresh_left < box.x) thresh_left = box.x; + box.width = thresh_left - box.x; + } else if (cursor->cursor->x > thresh_right) { + edge = WLR_EDGE_RIGHT; + if (thresh_right > box.x + box.width) thresh_right = box.x + box.width; + box.width = box.x + box.width - thresh_right; + box.x = thresh_right; + } + } + if (edge) { + e->target_node = node_get_parent(&con->node); + if (e->target_node && (e->target_node == &e->con->node || + node_has_ancestor(e->target_node, &e->con->node))) { + e->target_node = node_get_parent(&e->con->node); + } + e->target_edge = edge; + update_indicator(e, &box); + return; + } + con = con->pending.parent; + } + + // Use the hovered view - but we must be over the actual surface + con = node->sway_container; + if (!con->view || !con->view->surface || node == &e->con->node + || node_has_ancestor(node, &e->con->node)) { + e->target_node = NULL; + e->target_edge = WLR_EDGE_NONE; + return; + } + + // Find the closest edge + size_t thickness = fmin(con->pending.content_width, con->pending.content_height) * 0.3; + size_t closest_dist = INT_MAX; + size_t dist; + e->target_edge = WLR_EDGE_NONE; + if ((dist = cursor->cursor->y - con->pending.y) < closest_dist) { + closest_dist = dist; + e->target_edge = WLR_EDGE_TOP; + } + if ((dist = cursor->cursor->x - con->pending.x) < closest_dist) { + closest_dist = dist; + e->target_edge = WLR_EDGE_LEFT; + } + if ((dist = con->pending.x + con->pending.width - cursor->cursor->x) < closest_dist) { + closest_dist = dist; + e->target_edge = WLR_EDGE_RIGHT; + } + if ((dist = con->pending.y + con->pending.height - cursor->cursor->y) < closest_dist) { + closest_dist = dist; + e->target_edge = WLR_EDGE_BOTTOM; + } + + if (closest_dist > thickness) { + e->target_edge = WLR_EDGE_NONE; + } + + e->target_node = node; + resize_box(&drop_box, e->target_edge, thickness); + update_indicator(e, &drop_box); +} + +static void handle_pointer_motion(struct sway_seat *seat, uint32_t time_msec) { + struct seatop_move_tiling_event *e = seat->seatop_data; + if (e->threshold_reached) { + handle_motion_postthreshold(seat); + } else { + handle_motion_prethreshold(seat); + } + transaction_commit_dirty(); +} + +static bool is_parallel(enum sway_container_layout layout, + enum wlr_edges edge) { + bool layout_is_horiz = layout == L_HORIZ || layout == L_TABBED; + bool edge_is_horiz = edge == WLR_EDGE_LEFT || edge == WLR_EDGE_RIGHT; + return layout_is_horiz == edge_is_horiz; +} + +static void finalize_move(struct sway_seat *seat) { + struct seatop_move_tiling_event *e = seat->seatop_data; + + if (!e->target_node) { + seatop_begin_default(seat); + return; + } + + struct sway_container *con = e->con; + struct sway_container *old_parent = con->pending.parent; + struct sway_workspace *old_ws = con->pending.workspace; + struct sway_node *target_node = e->target_node; + struct sway_workspace *new_ws = target_node->type == N_WORKSPACE ? + target_node->sway_workspace : target_node->sway_container->pending.workspace; + enum wlr_edges edge = e->target_edge; + int after = edge != WLR_EDGE_TOP && edge != WLR_EDGE_LEFT; + bool swap = edge == WLR_EDGE_NONE && target_node->type == N_CONTAINER && + !e->split_target; + + if (!swap) { + container_detach(con); + } + + // Moving container into empty workspace + if (target_node->type == N_WORKSPACE && edge == WLR_EDGE_NONE) { + con = workspace_add_tiling(new_ws, con); + } else if (e->split_target) { + struct sway_container *target = target_node->sway_container; + enum sway_container_layout layout = container_parent_layout(target); + if (layout != L_TABBED && layout != L_STACKED) { + container_split(target, L_TABBED); + } + container_add_sibling(target, con, e->insert_after_target); + ipc_event_window(con, "move"); + } else if (target_node->type == N_CONTAINER) { + // Moving container before/after another + struct sway_container *target = target_node->sway_container; + if (swap) { + container_swap(target_node->sway_container, con); + } else { + enum sway_container_layout layout = container_parent_layout(target); + if (edge && !is_parallel(layout, edge)) { + enum sway_container_layout new_layout = edge == WLR_EDGE_TOP || + edge == WLR_EDGE_BOTTOM ? L_VERT : L_HORIZ; + container_split(target, new_layout); + } + container_add_sibling(target, con, after); + ipc_event_window(con, "move"); + } + } else { + // Target is a workspace which requires splitting + enum sway_container_layout new_layout = edge == WLR_EDGE_TOP || + edge == WLR_EDGE_BOTTOM ? L_VERT : L_HORIZ; + workspace_split(new_ws, new_layout); + workspace_insert_tiling(new_ws, con, after ? new_ws->tiling->length : 0); + } + + if (old_parent) { + container_reap_empty(old_parent); + } + + // This is a bit dirty, but we'll set the dimensions to that of a sibling. + // I don't think there's any other way to make it consistent without + // changing how we auto-size containers. + list_t *siblings = container_get_siblings(con); + if (siblings->length > 1) { + int index = list_find(siblings, con); + struct sway_container *sibling = index == 0 ? + siblings->items[1] : siblings->items[index - 1]; + con->pending.width = sibling->pending.width; + con->pending.height = sibling->pending.height; + con->width_fraction = sibling->width_fraction; + con->height_fraction = sibling->height_fraction; + } + + arrange_workspace(old_ws); + if (new_ws != old_ws) { + arrange_workspace(new_ws); + } + + transaction_commit_dirty(); + seatop_begin_default(seat); +} + +static void handle_button(struct sway_seat *seat, uint32_t time_msec, + struct wlr_input_device *device, uint32_t button, + enum wl_pointer_button_state state) { + if (seat->cursor->pressed_button_count == 0) { + finalize_move(seat); + } +} + +static void handle_tablet_tool_tip(struct sway_seat *seat, + struct sway_tablet_tool *tool, uint32_t time_msec, + enum wlr_tablet_tool_tip_state state) { + if (state == WLR_TABLET_TOOL_TIP_UP) { + finalize_move(seat); + } +} + +static void handle_unref(struct sway_seat *seat, struct sway_container *con) { + struct seatop_move_tiling_event *e = seat->seatop_data; + if (e->target_node == &con->node) { // Drop target + e->target_node = NULL; + } + if (e->con == con) { // The container being moved + seatop_begin_default(seat); + } +} + +static const struct sway_seatop_impl seatop_impl = { + .button = handle_button, + .pointer_motion = handle_pointer_motion, + .tablet_tool_tip = handle_tablet_tool_tip, + .unref = handle_unref, + .end = handle_end, +}; + +void seatop_begin_move_tiling_threshold(struct sway_seat *seat, + struct sway_container *con) { + seatop_end(seat); + + struct seatop_move_tiling_event *e = + calloc(1, sizeof(struct seatop_move_tiling_event)); + if (!e) { + return; + } + + const float *indicator = config->border_colors.focused.indicator; + float color[4] = { + indicator[0] * .5, + indicator[1] * .5, + indicator[2] * .5, + indicator[3] * .5, + }; + + e->indicator_blur = wlr_scene_blur_create(seat->scene_tree, 0, 0); + if (!e->indicator_blur) { + free(e); + return; + } + + e->indicator_rect = wlr_scene_rect_create(seat->scene_tree, 0, 0, color); + if (!e->indicator_rect) { + wlr_scene_node_destroy(&e->indicator_blur->node); + free(e); + return; + } + + e->con = con; + e->ref_lx = seat->cursor->cursor->x; + e->ref_ly = seat->cursor->cursor->y; + + seat->seatop_impl = &seatop_impl; + seat->seatop_data = e; + + container_raise_floating(con); + transaction_commit_dirty(); + wlr_seat_pointer_notify_clear_focus(seat->wlr_seat); +} + +void seatop_begin_move_tiling(struct sway_seat *seat, + struct sway_container *con) { + seatop_begin_move_tiling_threshold(seat, con); + struct seatop_move_tiling_event *e = seat->seatop_data; + if (e) { + e->threshold_reached = true; + cursor_set_image(seat->cursor, "grab", NULL); + } +} diff --git a/sway/input/seatop_resize_floating.c b/sway/input/seatop_resize_floating.c new file mode 100644 index 0000000..bec86e3 --- /dev/null +++ b/sway/input/seatop_resize_floating.c @@ -0,0 +1,195 @@ +#include +#include +#include +#include "sway/desktop/transaction.h" +#include "sway/input/cursor.h" +#include "sway/input/seat.h" +#include "sway/tree/arrange.h" +#include "sway/tree/view.h" +#include "sway/tree/workspace.h" +#include "sway/tree/container.h" + +struct seatop_resize_floating_event { + struct sway_container *con; + enum wlr_edges edge; + bool preserve_ratio; + double ref_lx, ref_ly; // cursor's x/y at start of op + double ref_width, ref_height; // container's size at start of op + double ref_con_lx, ref_con_ly; // container's x/y at start of op +}; + +static void handle_button(struct sway_seat *seat, uint32_t time_msec, + struct wlr_input_device *device, uint32_t button, + enum wl_pointer_button_state state) { + struct seatop_resize_floating_event *e = seat->seatop_data; + struct sway_container *con = e->con; + + if (seat->cursor->pressed_button_count == 0) { + container_set_resizing(con, false); + arrange_container(con); // Send configure w/o resizing hint + transaction_commit_dirty(); + seatop_begin_default(seat); + } +} + +static void handle_pointer_motion(struct sway_seat *seat, uint32_t time_msec) { + struct seatop_resize_floating_event *e = seat->seatop_data; + struct sway_container *con = e->con; + enum wlr_edges edge = e->edge; + struct sway_cursor *cursor = seat->cursor; + + // The amount the mouse has moved since the start of the resize operation + // Positive is down/right + double mouse_move_x = cursor->cursor->x - e->ref_lx; + double mouse_move_y = cursor->cursor->y - e->ref_ly; + + if (edge == WLR_EDGE_TOP || edge == WLR_EDGE_BOTTOM) { + mouse_move_x = 0; + } + if (edge == WLR_EDGE_LEFT || edge == WLR_EDGE_RIGHT) { + mouse_move_y = 0; + } + + double grow_width = edge & WLR_EDGE_LEFT ? -mouse_move_x : mouse_move_x; + double grow_height = edge & WLR_EDGE_TOP ? -mouse_move_y : mouse_move_y; + + if (e->preserve_ratio) { + double x_multiplier = grow_width / e->ref_width; + double y_multiplier = grow_height / e->ref_height; + double max_multiplier = fmax(x_multiplier, y_multiplier); + grow_width = e->ref_width * max_multiplier; + grow_height = e->ref_height * max_multiplier; + } + + struct sway_container_state *state = &con->current; + double border_width = 0.0; + if (con->current.border == B_NORMAL || con->current.border == B_PIXEL) { + border_width = state->border_thickness * 2; + } + double border_height = 0.0; + if (con->current.border == B_NORMAL) { + border_height += container_titlebar_height(); + border_height += state->border_thickness; + } else if (con->current.border == B_PIXEL) { + border_height += state->border_thickness * 2; + } + + // Determine new width/height, and accommodate for floating min/max values + double width = e->ref_width + grow_width; + double height = e->ref_height + grow_height; + int min_width, max_width, min_height, max_height; + floating_calculate_constraints(&min_width, &max_width, + &min_height, &max_height); + width = fmin(width, max_width - border_width); + width = fmax(width, min_width + border_width); + width = fmax(width, 1); + height = fmin(height, max_height - border_height); + height = fmax(height, min_height + border_height); + height = fmax(height, 1); + + // Apply the view's min/max size + if (con->view) { + double view_min_width, view_max_width, view_min_height, view_max_height; + view_get_constraints(con->view, &view_min_width, &view_max_width, + &view_min_height, &view_max_height); + width = fmin(width, view_max_width - border_width); + width = fmax(width, view_min_width + border_width); + width = fmax(width, 1); + height = fmin(height, view_max_height - border_height); + height = fmax(height, view_min_height + border_height); + height = fmax(height, 1); + + } + + // Recalculate these, in case we hit a min/max limit + grow_width = width - e->ref_width; + grow_height = height - e->ref_height; + + // Determine grow x/y values - these are relative to the container's x/y at + // the start of the resize operation. + double grow_x = 0, grow_y = 0; + if (edge & WLR_EDGE_LEFT) { + grow_x = -grow_width; + } else if (edge & WLR_EDGE_RIGHT) { + grow_x = 0; + } else { + grow_x = -grow_width / 2; + } + if (edge & WLR_EDGE_TOP) { + grow_y = -grow_height; + } else if (edge & WLR_EDGE_BOTTOM) { + grow_y = 0; + } else { + grow_y = -grow_height / 2; + } + + // Determine the amounts we need to bump everything relative to the current + // size. + int relative_grow_width = width - con->pending.width; + int relative_grow_height = height - con->pending.height; + int relative_grow_x = (e->ref_con_lx + grow_x) - con->pending.x; + int relative_grow_y = (e->ref_con_ly + grow_y) - con->pending.y; + + // Actually resize stuff + con->pending.x += relative_grow_x; + con->pending.y += relative_grow_y; + con->pending.width += relative_grow_width; + con->pending.height += relative_grow_height; + + con->pending.content_x += relative_grow_x; + con->pending.content_y += relative_grow_y; + con->pending.content_width += relative_grow_width; + con->pending.content_height += relative_grow_height; + + arrange_container(con); + transaction_commit_dirty(); +} + +static void handle_unref(struct sway_seat *seat, struct sway_container *con) { + struct seatop_resize_floating_event *e = seat->seatop_data; + if (e->con == con) { + seatop_begin_default(seat); + } +} + +static const struct sway_seatop_impl seatop_impl = { + .button = handle_button, + .pointer_motion = handle_pointer_motion, + .unref = handle_unref, +}; + +void seatop_begin_resize_floating(struct sway_seat *seat, + struct sway_container *con, enum wlr_edges edge) { + seatop_end(seat); + + struct seatop_resize_floating_event *e = + calloc(1, sizeof(struct seatop_resize_floating_event)); + if (!e) { + return; + } + e->con = con; + + struct wlr_keyboard *keyboard = wlr_seat_get_keyboard(seat->wlr_seat); + e->preserve_ratio = keyboard && + (wlr_keyboard_get_modifiers(keyboard) & WLR_MODIFIER_SHIFT); + + e->edge = edge == WLR_EDGE_NONE ? WLR_EDGE_BOTTOM | WLR_EDGE_RIGHT : edge; + e->ref_lx = seat->cursor->cursor->x; + e->ref_ly = seat->cursor->cursor->y; + e->ref_con_lx = con->pending.x; + e->ref_con_ly = con->pending.y; + e->ref_width = con->pending.width; + e->ref_height = con->pending.height; + + seat->seatop_impl = &seatop_impl; + seat->seatop_data = e; + + container_set_resizing(con, true); + container_raise_floating(con); + transaction_commit_dirty(); + + const char *image = edge == WLR_EDGE_NONE ? + "se-resize" : wlr_xcursor_get_resize_name(edge); + cursor_set_image(seat->cursor, image, NULL); + wlr_seat_pointer_notify_clear_focus(seat->wlr_seat); +} diff --git a/sway/input/seatop_resize_tiling.c b/sway/input/seatop_resize_tiling.c new file mode 100644 index 0000000..be7b3c1 --- /dev/null +++ b/sway/input/seatop_resize_tiling.c @@ -0,0 +1,162 @@ +#include +#include +#include "sway/commands.h" +#include "sway/desktop/transaction.h" +#include "sway/input/cursor.h" +#include "sway/input/seat.h" +#include "sway/tree/arrange.h" +#include "sway/tree/container.h" +#include "sway/tree/view.h" + +struct seatop_resize_tiling_event { + struct sway_container *con; // leaf container + + // con, or ancestor of con which will be resized horizontally/vertically + struct sway_container *h_con; + struct sway_container *v_con; + + // sibling con(s) that will be resized to accommodate + struct sway_container *h_sib; + struct sway_container *v_sib; + + enum wlr_edges edge; + enum wlr_edges edge_x, edge_y; + double ref_lx, ref_ly; // cursor's x/y at start of op + double h_con_orig_width; // width of the horizontal ancestor at start + double v_con_orig_height; // height of the vertical ancestor at start +}; + +static struct sway_container *container_get_resize_sibling( + struct sway_container *con, uint32_t edge) { + if (!con) { + return NULL; + } + + list_t *siblings = container_get_siblings(con); + int index = container_sibling_index(con); + int offset = edge & (WLR_EDGE_TOP | WLR_EDGE_LEFT) ? -1 : 1; + + if (siblings->length == 1) { + return NULL; + } else { + return siblings->items[index + offset]; + } +} + +static void handle_button(struct sway_seat *seat, uint32_t time_msec, + struct wlr_input_device *device, uint32_t button, + enum wl_pointer_button_state state) { + struct seatop_resize_tiling_event *e = seat->seatop_data; + + if (seat->cursor->pressed_button_count == 0) { + if (e->h_con) { + container_set_resizing(e->h_con, false); + container_set_resizing(e->h_sib, false); + if (e->h_con->pending.parent) { + arrange_container(e->h_con->pending.parent); + } else { + arrange_workspace(e->h_con->pending.workspace); + } + } + if (e->v_con) { + container_set_resizing(e->v_con, false); + container_set_resizing(e->v_sib, false); + if (e->v_con->pending.parent) { + arrange_container(e->v_con->pending.parent); + } else { + arrange_workspace(e->v_con->pending.workspace); + } + } + transaction_commit_dirty(); + seatop_begin_default(seat); + } +} + +static void handle_pointer_motion(struct sway_seat *seat, uint32_t time_msec) { + struct seatop_resize_tiling_event *e = seat->seatop_data; + int amount_x = 0; + int amount_y = 0; + int moved_x = seat->cursor->cursor->x - e->ref_lx; + int moved_y = seat->cursor->cursor->y - e->ref_ly; + + if (e->h_con) { + if (e->edge & WLR_EDGE_LEFT) { + amount_x = (e->h_con_orig_width - moved_x) - e->h_con->pending.width; + } else if (e->edge & WLR_EDGE_RIGHT) { + amount_x = (e->h_con_orig_width + moved_x) - e->h_con->pending.width; + } + } + if (e->v_con) { + if (e->edge & WLR_EDGE_TOP) { + amount_y = (e->v_con_orig_height - moved_y) - e->v_con->pending.height; + } else if (e->edge & WLR_EDGE_BOTTOM) { + amount_y = (e->v_con_orig_height + moved_y) - e->v_con->pending.height; + } + } + + if (amount_x != 0) { + container_resize_tiled(e->h_con, e->edge_x, amount_x); + } + if (amount_y != 0) { + container_resize_tiled(e->v_con, e->edge_y, amount_y); + } + transaction_commit_dirty(); +} + +static void handle_unref(struct sway_seat *seat, struct sway_container *con) { + struct seatop_resize_tiling_event *e = seat->seatop_data; + if (e->con == con || e->h_sib == con || e->v_sib == con) { + seatop_begin_default(seat); + } +} + +static const struct sway_seatop_impl seatop_impl = { + .button = handle_button, + .pointer_motion = handle_pointer_motion, + .unref = handle_unref, +}; + +void seatop_begin_resize_tiling(struct sway_seat *seat, + struct sway_container *con, enum wlr_edges edge) { + seatop_end(seat); + + struct seatop_resize_tiling_event *e = + calloc(1, sizeof(struct seatop_resize_tiling_event)); + if (!e) { + return; + } + e->con = con; + e->edge = edge; + + e->ref_lx = seat->cursor->cursor->x; + e->ref_ly = seat->cursor->cursor->y; + + if (edge & (WLR_EDGE_LEFT | WLR_EDGE_RIGHT)) { + e->edge_x = edge & (WLR_EDGE_LEFT | WLR_EDGE_RIGHT); + e->h_con = container_find_resize_parent(e->con, e->edge_x); + e->h_sib = container_get_resize_sibling(e->h_con, e->edge_x); + + if (e->h_con) { + container_set_resizing(e->h_con, true); + container_set_resizing(e->h_sib, true); + e->h_con_orig_width = e->h_con->pending.width; + } + } + if (edge & (WLR_EDGE_TOP | WLR_EDGE_BOTTOM)) { + e->edge_y = edge & (WLR_EDGE_TOP | WLR_EDGE_BOTTOM); + e->v_con = container_find_resize_parent(e->con, e->edge_y); + e->v_sib = container_get_resize_sibling(e->v_con, e->edge_y); + + if (e->v_con) { + container_set_resizing(e->v_con, true); + container_set_resizing(e->v_sib, true); + e->v_con_orig_height = e->v_con->pending.height; + } + } + + seat->seatop_impl = &seatop_impl; + seat->seatop_data = e; + + transaction_commit_dirty(); + wlr_seat_pointer_notify_clear_focus(seat->wlr_seat); +} diff --git a/sway/input/switch.c b/sway/input/switch.c new file mode 100644 index 0000000..6aab4ad --- /dev/null +++ b/sway/input/switch.c @@ -0,0 +1,125 @@ +#include "sway/config.h" +#include "sway/input/switch.h" +#include "sway/server.h" +#include "log.h" + +struct sway_switch *sway_switch_create(struct sway_seat *seat, + struct sway_seat_device *device) { + struct sway_switch *switch_device = + calloc(1, sizeof(struct sway_switch)); + if (!sway_assert(switch_device, "could not allocate switch")) { + return NULL; + } + device->switch_device = switch_device; + switch_device->wlr = wlr_switch_from_input_device(device->input_device->wlr_device); + switch_device->seat_device = device; + switch_device->state = WLR_SWITCH_STATE_OFF; + wl_list_init(&switch_device->switch_toggle.link); + sway_log(SWAY_DEBUG, "Allocated switch for device"); + + return switch_device; +} + +static bool sway_switch_trigger_test(enum sway_switch_trigger trigger, + enum wlr_switch_state state) { + switch (trigger) { + case SWAY_SWITCH_TRIGGER_ON: + return state == WLR_SWITCH_STATE_ON; + case SWAY_SWITCH_TRIGGER_OFF: + return state == WLR_SWITCH_STATE_OFF; + case SWAY_SWITCH_TRIGGER_TOGGLE: + return true; + } + abort(); // unreachable +} + +static void execute_binding(struct sway_switch *sway_switch) { + struct sway_seat *seat = sway_switch->seat_device->sway_seat; + bool locked = server.session_lock.lock; + + list_t *bindings = config->current_mode->switch_bindings; + struct sway_switch_binding *matched_binding = NULL; + for (int i = 0; i < bindings->length; ++i) { + struct sway_switch_binding *binding = bindings->items[i]; + if (binding->type != sway_switch->type) { + continue; + } + if (!sway_switch_trigger_test(binding->trigger, sway_switch->state)) { + continue; + } + if (config->reloading && (binding->trigger == SWAY_SWITCH_TRIGGER_TOGGLE + || (binding->flags & BINDING_RELOAD) == 0)) { + continue; + } + bool binding_locked = binding->flags & BINDING_LOCKED; + if (!binding_locked && locked) { + continue; + } + + matched_binding = binding; + + if (binding_locked == locked) { + break; + } + } + + if (matched_binding) { + struct sway_binding *dummy_binding = + calloc(1, sizeof(struct sway_binding)); + dummy_binding->type = BINDING_SWITCH; + dummy_binding->flags = matched_binding->flags; + dummy_binding->command = matched_binding->command; + + seat_execute_command(seat, dummy_binding); + free(dummy_binding); + } +} + +static void handle_switch_toggle(struct wl_listener *listener, void *data) { + struct sway_switch *sway_switch = + wl_container_of(listener, sway_switch, switch_toggle); + struct wlr_switch_toggle_event *event = data; + struct sway_seat *seat = sway_switch->seat_device->sway_seat; + seat_idle_notify_activity(seat, IDLE_SOURCE_SWITCH); + + struct wlr_input_device *wlr_device = + sway_switch->seat_device->input_device->wlr_device; + char *device_identifier = input_device_get_identifier(wlr_device); + sway_log(SWAY_DEBUG, "%s: type %d state %d", device_identifier, + event->switch_type, event->switch_state); + free(device_identifier); + + sway_switch->type = event->switch_type; + sway_switch->state = event->switch_state; + execute_binding(sway_switch); +} + +void sway_switch_configure(struct sway_switch *sway_switch) { + wl_list_remove(&sway_switch->switch_toggle.link); + wl_signal_add(&sway_switch->wlr->events.toggle, + &sway_switch->switch_toggle); + sway_switch->switch_toggle.notify = handle_switch_toggle; + sway_log(SWAY_DEBUG, "Configured switch for device"); +} + +void sway_switch_destroy(struct sway_switch *sway_switch) { + if (!sway_switch) { + return; + } + wl_list_remove(&sway_switch->switch_toggle.link); + free(sway_switch); +} + +void sway_switch_retrigger_bindings_for_all(void) { + struct sway_seat *seat; + wl_list_for_each(seat, &server.input->seats, link) { + struct sway_seat_device *seat_device; + wl_list_for_each(seat_device, &seat->devices, link) { + struct sway_input_device *input_device = seat_device->input_device; + if (input_device->wlr_device->type != WLR_INPUT_DEVICE_SWITCH) { + continue; + } + execute_binding(seat_device->switch_device); + }; + } +} diff --git a/sway/input/tablet.c b/sway/input/tablet.c new file mode 100644 index 0000000..19d5deb --- /dev/null +++ b/sway/input/tablet.c @@ -0,0 +1,376 @@ +#include +#include +#include +#include +#include +#include "log.h" +#include "sway/input/cursor.h" +#include "sway/input/seat.h" +#include "sway/input/tablet.h" +#include "sway/server.h" + +#if WLR_HAS_LIBINPUT_BACKEND +#include +#endif + +static void handle_pad_tablet_destroy(struct wl_listener *listener, void *data) { + struct sway_tablet_pad *pad = + wl_container_of(listener, pad, tablet_destroy); + + pad->tablet = NULL; + + wl_list_remove(&pad->tablet_destroy.link); + wl_list_init(&pad->tablet_destroy.link); +} + +static void attach_tablet_pad(struct sway_tablet_pad *tablet_pad, + struct sway_tablet *tablet) { + sway_log(SWAY_DEBUG, "Attaching tablet pad \"%s\" to tablet tool \"%s\"", + tablet_pad->seat_device->input_device->wlr_device->name, + tablet->seat_device->input_device->wlr_device->name); + + tablet_pad->tablet = tablet; + + wl_list_remove(&tablet_pad->tablet_destroy.link); + tablet_pad->tablet_destroy.notify = handle_pad_tablet_destroy; + wl_signal_add(&tablet->seat_device->input_device->wlr_device->events.destroy, + &tablet_pad->tablet_destroy); +} + +struct sway_tablet *sway_tablet_create(struct sway_seat *seat, + struct sway_seat_device *device) { + struct sway_tablet *tablet = + calloc(1, sizeof(struct sway_tablet)); + if (!sway_assert(tablet, "could not allocate sway tablet for seat")) { + return NULL; + } + + wl_list_insert(&seat->cursor->tablets, &tablet->link); + + device->tablet = tablet; + tablet->seat_device = device; + + return tablet; +} + +void sway_configure_tablet(struct sway_tablet *tablet) { + struct wlr_input_device *device = + tablet->seat_device->input_device->wlr_device; + struct sway_seat *seat = tablet->seat_device->sway_seat; + + seat_configure_xcursor(seat); + + if (!tablet->tablet_v2) { + tablet->tablet_v2 = + wlr_tablet_create(server.tablet_v2, seat->wlr_seat, device); + } + +#if WLR_HAS_LIBINPUT_BACKEND + /* Search for a sibling tablet pad */ + if (!wlr_input_device_is_libinput(device)) { + /* We can only do this on libinput devices */ + return; + } + + struct libinput_device_group *group = + libinput_device_get_device_group(wlr_libinput_get_device_handle(device)); + struct sway_tablet_pad *tablet_pad; + wl_list_for_each(tablet_pad, &seat->cursor->tablet_pads, link) { + struct wlr_input_device *pad_device = + tablet_pad->seat_device->input_device->wlr_device; + if (!wlr_input_device_is_libinput(pad_device)) { + continue; + } + + struct libinput_device_group *pad_group = + libinput_device_get_device_group(wlr_libinput_get_device_handle(pad_device)); + + if (pad_group == group) { + attach_tablet_pad(tablet_pad, tablet); + break; + } + } +#endif +} + +void sway_tablet_destroy(struct sway_tablet *tablet) { + if (!tablet) { + return; + } + wl_list_remove(&tablet->link); + free(tablet); +} + +static void handle_tablet_tool_set_cursor(struct wl_listener *listener, void *data) { + struct sway_tablet_tool *tool = + wl_container_of(listener, tool, set_cursor); + struct wlr_tablet_v2_event_cursor *event = data; + + struct sway_cursor *cursor = tool->seat->cursor; + if (!seatop_allows_set_cursor(cursor->seat)) { + return; + } + + struct wl_client *focused_client = NULL; + struct wlr_surface *focused_surface = tool->tablet_v2_tool->focused_surface; + if (focused_surface != NULL) { + focused_client = wl_resource_get_client(focused_surface->resource); + } + + // TODO: check cursor mode + if (focused_client == NULL || + event->seat_client->client != focused_client) { + sway_log(SWAY_DEBUG, "denying request to set cursor from unfocused client"); + return; + } + + cursor_set_image_surface(cursor, event->surface, event->hotspot_x, + event->hotspot_y, focused_client); +} + +static void handle_tablet_tool_destroy(struct wl_listener *listener, void *data) { + struct sway_tablet_tool *tool = + wl_container_of(listener, tool, tool_destroy); + + wl_list_remove(&tool->tool_destroy.link); + wl_list_remove(&tool->set_cursor.link); + + free(tool); +} + +void sway_tablet_tool_configure(struct sway_tablet *tablet, + struct wlr_tablet_tool *wlr_tool) { + struct sway_tablet_tool *tool = + calloc(1, sizeof(struct sway_tablet_tool)); + if (!sway_assert(tool, "could not allocate sway tablet tool for tablet")) { + return; + } + + switch (wlr_tool->type) { + case WLR_TABLET_TOOL_TYPE_LENS: + case WLR_TABLET_TOOL_TYPE_MOUSE: + tool->mode = SWAY_TABLET_TOOL_MODE_RELATIVE; + break; + default: + tool->mode = SWAY_TABLET_TOOL_MODE_ABSOLUTE; + + struct input_config *ic = input_device_get_config( + tablet->seat_device->input_device); + if (!ic) { + break; + } + + for (int i = 0; i < ic->tools->length; i++) { + struct input_config_tool *tool_config = ic->tools->items[i]; + if (tool_config->type == wlr_tool->type) { + tool->mode = tool_config->mode; + break; + } + } + } + + tool->seat = tablet->seat_device->sway_seat; + tool->tablet = tablet; + tool->tablet_v2_tool = + wlr_tablet_tool_create(server.tablet_v2, + tablet->seat_device->sway_seat->wlr_seat, wlr_tool); + + tool->tool_destroy.notify = handle_tablet_tool_destroy; + wl_signal_add(&wlr_tool->events.destroy, &tool->tool_destroy); + + tool->set_cursor.notify = handle_tablet_tool_set_cursor; + wl_signal_add(&tool->tablet_v2_tool->events.set_cursor, + &tool->set_cursor); + + wlr_tool->data = tool; +} + +static void handle_tablet_pad_attach(struct wl_listener *listener, + void *data) { + struct sway_tablet_pad *pad = wl_container_of(listener, pad, attach); + struct wlr_tablet_tool *wlr_tool = data; + struct sway_tablet_tool *tool = wlr_tool->data; + + if (!tool) { + return; + } + + attach_tablet_pad(pad, tool->tablet); +} + +static void handle_tablet_pad_ring(struct wl_listener *listener, void *data) { + struct sway_tablet_pad *pad = wl_container_of(listener, pad, ring); + struct wlr_tablet_pad_ring_event *event = data; + + if (!pad->current_surface) { + return; + } + + wlr_tablet_v2_tablet_pad_notify_ring(pad->tablet_v2_pad, + event->ring, event->position, + event->source == WLR_TABLET_PAD_RING_SOURCE_FINGER, + event->time_msec); +} + +static void handle_tablet_pad_strip(struct wl_listener *listener, void *data) { + struct sway_tablet_pad *pad = wl_container_of(listener, pad, strip); + struct wlr_tablet_pad_strip_event *event = data; + + if (!pad->current_surface) { + return; + } + + wlr_tablet_v2_tablet_pad_notify_strip(pad->tablet_v2_pad, + event->strip, event->position, + event->source == WLR_TABLET_PAD_STRIP_SOURCE_FINGER, + event->time_msec); +} + +static void handle_tablet_pad_button(struct wl_listener *listener, void *data) { + struct sway_tablet_pad *pad = wl_container_of(listener, pad, button); + struct wlr_tablet_pad_button_event *event = data; + + if (!pad->current_surface) { + return; + } + + wlr_tablet_v2_tablet_pad_notify_mode(pad->tablet_v2_pad, + event->group, event->mode, event->time_msec); + + wlr_tablet_v2_tablet_pad_notify_button(pad->tablet_v2_pad, + event->button, event->time_msec, + (enum zwp_tablet_pad_v2_button_state)event->state); +} + +struct sway_tablet_pad *sway_tablet_pad_create(struct sway_seat *seat, + struct sway_seat_device *device) { + struct sway_tablet_pad *tablet_pad = + calloc(1, sizeof(struct sway_tablet_pad)); + if (!sway_assert(tablet_pad, "could not allocate sway tablet")) { + return NULL; + } + + tablet_pad->wlr = wlr_tablet_pad_from_input_device(device->input_device->wlr_device); + tablet_pad->seat_device = device; + wl_list_init(&tablet_pad->attach.link); + wl_list_init(&tablet_pad->button.link); + wl_list_init(&tablet_pad->strip.link); + wl_list_init(&tablet_pad->ring.link); + wl_list_init(&tablet_pad->surface_destroy.link); + wl_list_init(&tablet_pad->tablet_destroy.link); + + wl_list_insert(&seat->cursor->tablet_pads, &tablet_pad->link); + + return tablet_pad; +} + +void sway_configure_tablet_pad(struct sway_tablet_pad *tablet_pad) { + struct wlr_input_device *wlr_device = + tablet_pad->seat_device->input_device->wlr_device; + struct sway_seat *seat = tablet_pad->seat_device->sway_seat; + + if (!tablet_pad->tablet_v2_pad) { + tablet_pad->tablet_v2_pad = + wlr_tablet_pad_create(server.tablet_v2, seat->wlr_seat, wlr_device); + } + + wl_list_remove(&tablet_pad->attach.link); + tablet_pad->attach.notify = handle_tablet_pad_attach; + wl_signal_add(&tablet_pad->wlr->events.attach_tablet, + &tablet_pad->attach); + + wl_list_remove(&tablet_pad->button.link); + tablet_pad->button.notify = handle_tablet_pad_button; + wl_signal_add(&tablet_pad->wlr->events.button, &tablet_pad->button); + + wl_list_remove(&tablet_pad->strip.link); + tablet_pad->strip.notify = handle_tablet_pad_strip; + wl_signal_add(&tablet_pad->wlr->events.strip, &tablet_pad->strip); + + wl_list_remove(&tablet_pad->ring.link); + tablet_pad->ring.notify = handle_tablet_pad_ring; + wl_signal_add(&tablet_pad->wlr->events.ring, &tablet_pad->ring); + +#if WLR_HAS_LIBINPUT_BACKEND + /* Search for a sibling tablet */ + if (!wlr_input_device_is_libinput(wlr_device)) { + /* We can only do this on libinput devices */ + return; + } + + struct libinput_device_group *group = + libinput_device_get_device_group(wlr_libinput_get_device_handle(wlr_device)); + struct sway_tablet *tool; + wl_list_for_each(tool, &seat->cursor->tablets, link) { + struct wlr_input_device *tablet = + tool->seat_device->input_device->wlr_device; + if (!wlr_input_device_is_libinput(tablet)) { + continue; + } + + struct libinput_device_group *tablet_group = + libinput_device_get_device_group(wlr_libinput_get_device_handle(tablet)); + + if (tablet_group == group) { + attach_tablet_pad(tablet_pad, tool); + break; + } + } +#endif +} + +void sway_tablet_pad_destroy(struct sway_tablet_pad *tablet_pad) { + if (!tablet_pad) { + return; + } + + wl_list_remove(&tablet_pad->link); + wl_list_remove(&tablet_pad->attach.link); + wl_list_remove(&tablet_pad->button.link); + wl_list_remove(&tablet_pad->strip.link); + wl_list_remove(&tablet_pad->ring.link); + wl_list_remove(&tablet_pad->surface_destroy.link); + wl_list_remove(&tablet_pad->tablet_destroy.link); + + free(tablet_pad); +} + +static void handle_pad_tablet_surface_destroy(struct wl_listener *listener, + void *data) { + struct sway_tablet_pad *tablet_pad = + wl_container_of(listener, tablet_pad, surface_destroy); + + sway_tablet_pad_set_focus(tablet_pad, NULL); +} + +void sway_tablet_pad_set_focus(struct sway_tablet_pad *tablet_pad, + struct wlr_surface *surface) { + if (!tablet_pad || !tablet_pad->tablet) { + return; + } + + if (surface == tablet_pad->current_surface) { + return; + } + + /* Leave current surface */ + if (tablet_pad->current_surface) { + wlr_tablet_v2_tablet_pad_notify_leave(tablet_pad->tablet_v2_pad, + tablet_pad->current_surface); + wl_list_remove(&tablet_pad->surface_destroy.link); + wl_list_init(&tablet_pad->surface_destroy.link); + tablet_pad->current_surface = NULL; + } + + if (surface == NULL || + !wlr_surface_accepts_tablet_v2(surface, tablet_pad->tablet->tablet_v2)) { + return; + } + + wlr_tablet_v2_tablet_pad_notify_enter(tablet_pad->tablet_v2_pad, + tablet_pad->tablet->tablet_v2, surface); + + tablet_pad->current_surface = surface; + tablet_pad->surface_destroy.notify = handle_pad_tablet_surface_destroy; + wl_signal_add(&surface->events.destroy, &tablet_pad->surface_destroy); +} diff --git a/sway/input/text_input.c b/sway/input/text_input.c new file mode 100644 index 0000000..e496bff --- /dev/null +++ b/sway/input/text_input.c @@ -0,0 +1,687 @@ +#include +#include +#include "log.h" +#include "sway/input/seat.h" +#include "sway/scene_descriptor.h" +#include "sway/tree/root.h" +#include "sway/tree/view.h" +#include "sway/output.h" +#include "sway/input/text_input.h" +#include "sway/input/text_input_popup.h" +#include "sway/layers.h" +#include "sway/server.h" +#include + +static struct sway_text_input *relay_get_focusable_text_input( + struct sway_input_method_relay *relay) { + struct sway_text_input *text_input = NULL; + wl_list_for_each(text_input, &relay->text_inputs, link) { + if (text_input->pending_focused_surface) { + return text_input; + } + } + return NULL; +} + +static struct sway_text_input *relay_get_focused_text_input( + struct sway_input_method_relay *relay) { + struct sway_text_input *text_input = NULL; + wl_list_for_each(text_input, &relay->text_inputs, link) { + if (text_input->input->focused_surface) { + return text_input; + } + } + return NULL; +} + +static void handle_im_commit(struct wl_listener *listener, void *data) { + struct sway_input_method_relay *relay = wl_container_of(listener, relay, + input_method_commit); + + struct sway_text_input *text_input = relay_get_focused_text_input(relay); + if (!text_input) { + return; + } + if (relay->input_method->current.preedit.text) { + wlr_text_input_v3_send_preedit_string(text_input->input, + relay->input_method->current.preedit.text, + relay->input_method->current.preedit.cursor_begin, + relay->input_method->current.preedit.cursor_end); + } + if (relay->input_method->current.commit_text) { + wlr_text_input_v3_send_commit_string(text_input->input, + relay->input_method->current.commit_text); + } + if (relay->input_method->current.delete.before_length + || relay->input_method->current.delete.after_length) { + wlr_text_input_v3_send_delete_surrounding_text(text_input->input, + relay->input_method->current.delete.before_length, + relay->input_method->current.delete.after_length); + } + wlr_text_input_v3_send_done(text_input->input); +} + +static void handle_im_keyboard_grab_destroy(struct wl_listener *listener, void *data) { + struct sway_input_method_relay *relay = wl_container_of(listener, relay, + input_method_keyboard_grab_destroy); + struct wlr_input_method_keyboard_grab_v2 *keyboard_grab = relay->input_method->keyboard_grab; + struct wlr_seat *wlr_seat = keyboard_grab->input_method->seat; + wl_list_remove(&relay->input_method_keyboard_grab_destroy.link); + + if (keyboard_grab->keyboard) { + // send modifier state to original client + wlr_seat_set_keyboard(wlr_seat, keyboard_grab->keyboard); + wlr_seat_keyboard_notify_modifiers(wlr_seat, + &keyboard_grab->keyboard->modifiers); + } +} + +static void handle_im_grab_keyboard(struct wl_listener *listener, void *data) { + struct sway_input_method_relay *relay = wl_container_of(listener, relay, + input_method_grab_keyboard); + struct wlr_input_method_keyboard_grab_v2 *keyboard_grab = data; + + // send modifier state to grab + struct wlr_keyboard *active_keyboard = wlr_seat_get_keyboard(relay->seat->wlr_seat); + wlr_input_method_keyboard_grab_v2_set_keyboard(keyboard_grab, + active_keyboard); + + wl_signal_add(&keyboard_grab->events.destroy, + &relay->input_method_keyboard_grab_destroy); + relay->input_method_keyboard_grab_destroy.notify = + handle_im_keyboard_grab_destroy; +} + +static void text_input_set_pending_focused_surface( + struct sway_text_input *text_input, struct wlr_surface *surface) { + wl_list_remove(&text_input->pending_focused_surface_destroy.link); + text_input->pending_focused_surface = surface; + + if (surface) { + wl_signal_add(&surface->events.destroy, + &text_input->pending_focused_surface_destroy); + } else { + wl_list_init(&text_input->pending_focused_surface_destroy.link); + } +} + +static void handle_im_destroy(struct wl_listener *listener, void *data) { + struct sway_input_method_relay *relay = wl_container_of(listener, relay, + input_method_destroy); + wl_list_remove(&relay->input_method_commit.link); + wl_list_remove(&relay->input_method_grab_keyboard.link); + wl_list_remove(&relay->input_method_destroy.link); + wl_list_remove(&relay->input_method_new_popup_surface.link); + relay->input_method = NULL; + struct sway_text_input *text_input = relay_get_focused_text_input(relay); + if (text_input) { + // keyboard focus is still there, so keep the surface at hand in case + // the input method returns + text_input_set_pending_focused_surface(text_input, + text_input->input->focused_surface); + wlr_text_input_v3_send_leave(text_input->input); + } +} + +static void constrain_popup(struct sway_input_popup *popup) { + struct sway_text_input *text_input = + relay_get_focused_text_input(popup->relay); + + if (!popup->desc.relative) { + return; + } + + struct wlr_box parent = {0}; + wlr_scene_node_coords(&popup->desc.relative->parent->node, &parent.x, &parent.y); + + struct wlr_box geo = {0}; + struct wlr_output *output; + + if (popup->desc.view) { + struct sway_view *view = popup->desc.view; + output = wlr_output_layout_output_at(root->output_layout, + view->container->pending.content_x + view->geometry.x, + view->container->pending.content_y + view->geometry.y); + + parent.width = view->geometry.width; + parent.height = view->geometry.height; + geo = view->geometry; + } else { + output = popup->fixed_output; + } + + struct wlr_box output_box; + wlr_output_layout_get_box(root->output_layout, output, &output_box); + + bool cursor_rect = text_input->input->current.features & + WLR_TEXT_INPUT_V3_FEATURE_CURSOR_RECTANGLE; + struct wlr_box cursor_area; + if (cursor_rect) { + cursor_area = text_input->input->current.cursor_rectangle; + } else { + cursor_area = (struct wlr_box) { + .width = parent.width, + .height = parent.height, + }; + } + + int popup_width = popup->popup_surface->surface->current.width; + int popup_height = popup->popup_surface->surface->current.height; + int x1 = parent.x + cursor_area.x; + int x2 = parent.x + cursor_area.x + cursor_area.width; + int y1 = parent.y + cursor_area.y; + int y2 = parent.y + cursor_area.y + cursor_area.height; + int x = x1; + int y = y2; + + int available_right = output_box.x + output_box.width - x1; + int available_left = x2 - output_box.x; + if (available_right < popup_width && available_left > available_right) { + x = x2 - popup_width; + } + + int available_down = output_box.y + output_box.height - y2; + int available_up = y1 - output_box.y; + if (available_down < popup_height && available_up > available_down) { + y = y1 - popup_height; + } + + wlr_scene_node_set_position(popup->desc.relative, x - parent.x - geo.x, y - parent.y - geo.y); + if (cursor_rect) { + struct wlr_box box = { + .x = x1 - x, + .y = y1 - y, + .width = cursor_area.width, + .height = cursor_area.height, + }; + wlr_input_popup_surface_v2_send_text_input_rectangle( + popup->popup_surface, &box); + } + + if (popup->scene_tree) { + wlr_scene_node_set_position(&popup->scene_tree->node, x - geo.x, y - geo.y); + } +} + +static void input_popup_set_focus(struct sway_input_popup *popup, + struct wlr_surface *surface); + +static void relay_send_im_state(struct sway_input_method_relay *relay, + struct wlr_text_input_v3 *input) { + struct wlr_input_method_v2 *input_method = relay->input_method; + if (!input_method) { + sway_log(SWAY_INFO, "Sending IM_DONE but im is gone"); + return; + } + // TODO: only send each of those if they were modified + if (input->active_features & WLR_TEXT_INPUT_V3_FEATURE_SURROUNDING_TEXT) { + wlr_input_method_v2_send_surrounding_text(input_method, + input->current.surrounding.text, input->current.surrounding.cursor, + input->current.surrounding.anchor); + } + wlr_input_method_v2_send_text_change_cause(input_method, + input->current.text_change_cause); + if (input->active_features & WLR_TEXT_INPUT_V3_FEATURE_CONTENT_TYPE) { + wlr_input_method_v2_send_content_type(input_method, + input->current.content_type.hint, + input->current.content_type.purpose); + } + + struct sway_text_input *text_input = relay_get_focused_text_input(relay); + + struct sway_input_popup *popup; + wl_list_for_each(popup, &relay->input_popups, link) { + if (text_input != NULL) { + input_popup_set_focus(popup, text_input->input->focused_surface); + } else { + input_popup_set_focus(popup, NULL); + } + } + wlr_input_method_v2_send_done(input_method); + // TODO: pass intent, display popup size +} + +static void handle_text_input_enable(struct wl_listener *listener, void *data) { + struct sway_text_input *text_input = wl_container_of(listener, text_input, + text_input_enable); + if (text_input->input->focused_surface == NULL) { + sway_log(SWAY_DEBUG, "Enabling text input, but no longer focused"); + return; + } + if (text_input->relay->input_method == NULL) { + sway_log(SWAY_INFO, "Enabling text input when input method is gone"); + return; + } + wlr_input_method_v2_send_activate(text_input->relay->input_method); + relay_send_im_state(text_input->relay, text_input->input); +} + +static void handle_text_input_commit(struct wl_listener *listener, + void *data) { + struct sway_text_input *text_input = wl_container_of(listener, text_input, + text_input_commit); + if (text_input->input->focused_surface == NULL) { + sway_log(SWAY_DEBUG, "Unfocused text input tried to commit an update"); + return; + } + if (!text_input->input->current_enabled) { + sway_log(SWAY_INFO, "Inactive text input tried to commit an update"); + return; + } + sway_log(SWAY_DEBUG, "Text input committed update"); + if (text_input->relay->input_method == NULL) { + sway_log(SWAY_INFO, "Text input committed, but input method is gone"); + return; + } + relay_send_im_state(text_input->relay, text_input->input); +} + +static void relay_disable_text_input(struct sway_input_method_relay *relay, + struct sway_text_input *text_input) { + if (relay->input_method == NULL) { + sway_log(SWAY_DEBUG, "Disabling text input, but input method is gone"); + return; + } + wlr_input_method_v2_send_deactivate(relay->input_method); + relay_send_im_state(relay, text_input->input); +} + +static void handle_text_input_disable(struct wl_listener *listener, + void *data) { + struct sway_text_input *text_input = wl_container_of(listener, text_input, + text_input_disable); + if (text_input->input->focused_surface == NULL) { + sway_log(SWAY_DEBUG, "Disabling text input, but no longer focused"); + return; + } + relay_disable_text_input(text_input->relay, text_input); +} + +static void handle_text_input_destroy(struct wl_listener *listener, + void *data) { + struct sway_text_input *text_input = wl_container_of(listener, text_input, + text_input_destroy); + + if (text_input->input->current_enabled) { + relay_disable_text_input(text_input->relay, text_input); + } + text_input_set_pending_focused_surface(text_input, NULL); + wl_list_remove(&text_input->text_input_commit.link); + wl_list_remove(&text_input->text_input_destroy.link); + wl_list_remove(&text_input->text_input_disable.link); + wl_list_remove(&text_input->text_input_enable.link); + wl_list_remove(&text_input->link); + free(text_input); +} + +static void handle_pending_focused_surface_destroy(struct wl_listener *listener, + void *data) { + struct sway_text_input *text_input = wl_container_of(listener, text_input, + pending_focused_surface_destroy); + text_input->pending_focused_surface = NULL; + wl_list_remove(&text_input->pending_focused_surface_destroy.link); + wl_list_init(&text_input->pending_focused_surface_destroy.link); +} + +struct sway_text_input *sway_text_input_create( + struct sway_input_method_relay *relay, + struct wlr_text_input_v3 *text_input) { + struct sway_text_input *input = calloc(1, sizeof(struct sway_text_input)); + if (!input) { + return NULL; + } + input->input = text_input; + input->relay = relay; + + wl_list_insert(&relay->text_inputs, &input->link); + + input->text_input_enable.notify = handle_text_input_enable; + wl_signal_add(&text_input->events.enable, &input->text_input_enable); + + input->text_input_commit.notify = handle_text_input_commit; + wl_signal_add(&text_input->events.commit, &input->text_input_commit); + + input->text_input_disable.notify = handle_text_input_disable; + wl_signal_add(&text_input->events.disable, &input->text_input_disable); + + input->text_input_destroy.notify = handle_text_input_destroy; + wl_signal_add(&text_input->events.destroy, &input->text_input_destroy); + + input->pending_focused_surface_destroy.notify = + handle_pending_focused_surface_destroy; + wl_list_init(&input->pending_focused_surface_destroy.link); + return input; +} + +static void relay_handle_text_input(struct wl_listener *listener, + void *data) { + struct sway_input_method_relay *relay = wl_container_of(listener, relay, + text_input_new); + struct wlr_text_input_v3 *wlr_text_input = data; + if (relay->seat->wlr_seat != wlr_text_input->seat) { + return; + } + + sway_text_input_create(relay, wlr_text_input); +} + +static void input_popup_set_focus(struct sway_input_popup *popup, + struct wlr_surface *surface) { + wl_list_remove(&popup->focused_surface_unmap.link); + + if (!popup->scene_tree) { + wl_list_init(&popup->focused_surface_unmap.link); + return; + } + + if (popup->desc.relative) { + scene_descriptor_destroy(&popup->scene_tree->node, SWAY_SCENE_DESC_POPUP); + wlr_scene_node_destroy(popup->desc.relative); + popup->desc.relative = NULL; + } + + if (surface == NULL) { + wl_list_init(&popup->focused_surface_unmap.link); + wlr_scene_node_set_enabled(&popup->scene_tree->node, false); + return; + } + + struct wlr_layer_surface_v1 *layer_surface = + wlr_layer_surface_v1_try_from_wlr_surface(surface); + struct wlr_session_lock_surface_v1 *lock_surface = + wlr_session_lock_surface_v1_try_from_wlr_surface(surface); + + struct wlr_scene_tree *relative_parent; + if (layer_surface) { + wl_signal_add(&layer_surface->surface->events.unmap, + &popup->focused_surface_unmap); + + struct sway_layer_surface *layer = layer_surface->data; + if (layer == NULL) { + return; + } + + relative_parent = layer->scene->tree; + popup->desc.view = NULL; + + // we don't need to add an event here to NULL out this field because + // this field will only be initialized if the popup is part of a layer + // surface. Layer surfaces get destroyed as part of the output being + // destroyed, thus also trickling down to popups. + popup->fixed_output = layer->layer_surface->output; + } else if (lock_surface) { + wl_signal_add(&lock_surface->surface->events.unmap, + &popup->focused_surface_unmap); + + struct sway_layer_surface *lock = lock_surface->data; + if (lock == NULL) { + return; + } + + relative_parent = lock->scene->tree; + popup->desc.view = NULL; + + // we don't need to add an event here to NULL out this field because + // this field will only be initialized if the popup is part of a layer + // surface. Layer surfaces get destroyed as part of the output being + // destroyed, thus also trickling down to popups. + popup->fixed_output = lock->layer_surface->output; + } else { + struct sway_view *view = view_from_wlr_surface(surface); + // In the future there may be other shells been added, so we also need to check here. + if (view == NULL) { + sway_log(SWAY_DEBUG, "Unsupported IME focus surface"); + return; + } + wl_signal_add(&view->events.unmap, &popup->focused_surface_unmap); + relative_parent = view->scene_tree; + popup->desc.view = view; + } + + struct wlr_scene_tree *relative = wlr_scene_tree_create(relative_parent); + + popup->desc.relative = &relative->node; + if (!scene_descriptor_assign(&popup->scene_tree->node, + SWAY_SCENE_DESC_POPUP, &popup->desc)) { + wlr_scene_node_destroy(&popup->scene_tree->node); + popup->scene_tree = NULL; + return; + } + + constrain_popup(popup); + wlr_scene_node_set_enabled(&popup->scene_tree->node, true); +} + +static void handle_im_popup_destroy(struct wl_listener *listener, void *data) { + struct sway_input_popup *popup = + wl_container_of(listener, popup, popup_destroy); + wlr_scene_node_destroy(&popup->scene_tree->node); + wl_list_remove(&popup->focused_surface_unmap.link); + wl_list_remove(&popup->popup_surface_commit.link); + wl_list_remove(&popup->popup_surface_map.link); + wl_list_remove(&popup->popup_surface_unmap.link); + wl_list_remove(&popup->popup_destroy.link); + wl_list_remove(&popup->link); + + free(popup); +} + +static void handle_im_popup_surface_map(struct wl_listener *listener, void *data) { + struct sway_input_popup *popup = + wl_container_of(listener, popup, popup_surface_map); + struct sway_text_input *text_input = relay_get_focused_text_input(popup->relay); + if (text_input != NULL) { + input_popup_set_focus(popup, text_input->input->focused_surface); + } else { + input_popup_set_focus(popup, NULL); + } +} + +static void handle_im_popup_surface_unmap(struct wl_listener *listener, void *data) { + struct sway_input_popup *popup = + wl_container_of(listener, popup, popup_surface_unmap); + + scene_descriptor_destroy(&popup->scene_tree->node, SWAY_SCENE_DESC_POPUP); + // relative should already be freed as it should be a child of the just unmapped scene + popup->desc.relative = NULL; + + input_popup_set_focus(popup, NULL); +} + +static void handle_im_popup_surface_commit(struct wl_listener *listener, void *data) { + struct sway_input_popup *popup = + wl_container_of(listener, popup, popup_surface_commit); + + constrain_popup(popup); +} + +static void handle_im_focused_surface_unmap( + struct wl_listener *listener, void *data) { + struct sway_input_popup *popup = + wl_container_of(listener, popup, focused_surface_unmap); + + input_popup_set_focus(popup, NULL); +} + +static void handle_im_new_popup_surface(struct wl_listener *listener, + void *data) { + struct sway_input_method_relay *relay = wl_container_of(listener, relay, + input_method_new_popup_surface); + struct sway_input_popup *popup = calloc(1, sizeof(*popup)); + if (!popup) { + sway_log(SWAY_ERROR, "Failed to allocate an input method popup"); + return; + } + + popup->relay = relay; + popup->popup_surface = data; + popup->popup_surface->data = popup; + + popup->scene_tree = wlr_scene_tree_create(root->layers.popup); + if (!popup->scene_tree) { + sway_log(SWAY_ERROR, "Failed to allocate scene tree"); + free(popup); + return; + } + + if (!wlr_scene_subsurface_tree_create(popup->scene_tree, + popup->popup_surface->surface)) { + sway_log(SWAY_ERROR, "Failed to allocate subsurface tree"); + wlr_scene_node_destroy(&popup->scene_tree->node); + free(popup); + return; + } + + wl_signal_add(&popup->popup_surface->events.destroy, &popup->popup_destroy); + popup->popup_destroy.notify = handle_im_popup_destroy; + wl_signal_add(&popup->popup_surface->surface->events.commit, &popup->popup_surface_commit); + popup->popup_surface_commit.notify = handle_im_popup_surface_commit; + wl_signal_add(&popup->popup_surface->surface->events.map, &popup->popup_surface_map); + popup->popup_surface_map.notify = handle_im_popup_surface_map; + wl_signal_add(&popup->popup_surface->surface->events.unmap, &popup->popup_surface_unmap); + popup->popup_surface_unmap.notify = handle_im_popup_surface_unmap; + wl_list_init(&popup->focused_surface_unmap.link); + popup->focused_surface_unmap.notify = handle_im_focused_surface_unmap; + + struct sway_text_input *text_input = relay_get_focused_text_input(relay); + if (text_input != NULL) { + input_popup_set_focus(popup, text_input->input->focused_surface); + } else { + input_popup_set_focus(popup, NULL); + } + + wl_list_insert(&relay->input_popups, &popup->link); +} + +static void text_input_send_enter(struct sway_text_input *text_input, + struct wlr_surface *surface) { + wlr_text_input_v3_send_enter(text_input->input, surface); + struct sway_input_popup *popup; + wl_list_for_each(popup, &text_input->relay->input_popups, link) { + input_popup_set_focus(popup, surface); + } +} + +static void relay_handle_input_method(struct wl_listener *listener, + void *data) { + struct sway_input_method_relay *relay = wl_container_of(listener, relay, + input_method_new); + struct wlr_input_method_v2 *input_method = data; + if (relay->seat->wlr_seat != input_method->seat) { + return; + } + + if (relay->input_method != NULL) { + sway_log(SWAY_INFO, "Attempted to connect second input method to a seat"); + wlr_input_method_v2_send_unavailable(input_method); + return; + } + + relay->input_method = input_method; + wl_signal_add(&relay->input_method->events.commit, + &relay->input_method_commit); + relay->input_method_commit.notify = handle_im_commit; + wl_signal_add(&relay->input_method->events.grab_keyboard, + &relay->input_method_grab_keyboard); + relay->input_method_grab_keyboard.notify = handle_im_grab_keyboard; + wl_signal_add(&relay->input_method->events.destroy, + &relay->input_method_destroy); + relay->input_method_destroy.notify = handle_im_destroy; + wl_signal_add(&relay->input_method->events.new_popup_surface, + &relay->input_method_new_popup_surface); + relay->input_method_new_popup_surface.notify = handle_im_new_popup_surface; + + struct sway_text_input *text_input = relay_get_focusable_text_input(relay); + if (text_input) { + text_input_send_enter(text_input, + text_input->pending_focused_surface); + text_input_set_pending_focused_surface(text_input, NULL); + } +} + +static void sway_input_method_relay_finish_text_input(struct sway_input_method_relay *relay) { + wl_list_remove(&relay->text_input_new.link); + wl_list_remove(&relay->text_input_manager_destroy.link); + wl_list_init(&relay->text_input_new.link); + wl_list_init(&relay->text_input_manager_destroy.link); +} + +static void relay_handle_text_input_manager_destroy(struct wl_listener *listener, void *data) { + struct sway_input_method_relay *relay = wl_container_of(listener, relay, + text_input_manager_destroy); + + sway_input_method_relay_finish_text_input(relay); +} + +static void sway_input_method_relay_finish_input_method(struct sway_input_method_relay *relay) { + wl_list_remove(&relay->input_method_new.link); + wl_list_remove(&relay->input_method_manager_destroy.link); + wl_list_init(&relay->input_method_new.link); + wl_list_init(&relay->input_method_manager_destroy.link); +} + +static void relay_handle_input_method_manager_destroy(struct wl_listener *listener, void *data) { + struct sway_input_method_relay *relay = wl_container_of(listener, relay, + input_method_manager_destroy); + + sway_input_method_relay_finish_input_method(relay); +} + +void sway_input_method_relay_init(struct sway_seat *seat, + struct sway_input_method_relay *relay) { + relay->seat = seat; + wl_list_init(&relay->text_inputs); + wl_list_init(&relay->input_popups); + + relay->text_input_new.notify = relay_handle_text_input; + wl_signal_add(&server.text_input->events.new_text_input, + &relay->text_input_new); + relay->text_input_manager_destroy.notify = relay_handle_text_input_manager_destroy; + wl_signal_add(&server.text_input->events.destroy, + &relay->text_input_manager_destroy); + + relay->input_method_new.notify = relay_handle_input_method; + wl_signal_add( + &server.input_method->events.new_input_method, + &relay->input_method_new); + relay->input_method_manager_destroy.notify = relay_handle_input_method_manager_destroy; + wl_signal_add(&server.input_method->events.destroy, + &relay->input_method_manager_destroy); +} + +void sway_input_method_relay_finish(struct sway_input_method_relay *relay) { + sway_input_method_relay_finish_text_input(relay); + sway_input_method_relay_finish_input_method(relay); +} + +void sway_input_method_relay_set_focus(struct sway_input_method_relay *relay, + struct wlr_surface *surface) { + struct sway_text_input *text_input; + wl_list_for_each(text_input, &relay->text_inputs, link) { + if (text_input->pending_focused_surface) { + assert(text_input->input->focused_surface == NULL); + if (surface != text_input->pending_focused_surface) { + text_input_set_pending_focused_surface(text_input, NULL); + } + } else if (text_input->input->focused_surface) { + assert(text_input->pending_focused_surface == NULL); + if (surface != text_input->input->focused_surface) { + relay_disable_text_input(relay, text_input); + wlr_text_input_v3_send_leave(text_input->input); + } else { + sway_log(SWAY_DEBUG, "IM relay set_focus already focused"); + continue; + } + } + + if (surface + && wl_resource_get_client(text_input->input->resource) + == wl_resource_get_client(surface->resource)) { + if (relay->input_method) { + wlr_text_input_v3_send_enter(text_input->input, surface); + } else { + text_input_set_pending_focused_surface(text_input, surface); + } + } + } +} diff --git a/sway/ipc-json.c b/sway/ipc-json.c new file mode 100644 index 0000000..bb0566a --- /dev/null +++ b/sway/ipc-json.c @@ -0,0 +1,1563 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "config.h" +#include "json_object.h" +#include "log.h" +#include "sway/config.h" +#include "sway/ipc-json.h" +#include "sway/scene_descriptor.h" +#include "sway/server.h" +#include "sway/tree/container.h" +#include "sway/tree/root.h" +#include "sway/tree/view.h" +#include "sway/tree/workspace.h" +#include "sway/output.h" +#include "sway/input/input-manager.h" +#include "sway/input/cursor.h" +#include "sway/input/seat.h" +#include "sway/layers.h" +#include "wlr-layer-shell-unstable-v1-protocol.h" +#include "sway/desktop/idle_inhibit_v1.h" + +#if WLR_HAS_LIBINPUT_BACKEND +#include +#endif + +static const int i3_output_id = INT32_MAX; +static const int i3_scratch_id = INT32_MAX - 1; + +static const char *ipc_json_node_type_description(enum sway_node_type node_type) { + switch (node_type) { + case N_ROOT: + return "root"; + case N_OUTPUT: + return "output"; + case N_WORKSPACE: + return "workspace"; + case N_CONTAINER: + return "con"; + } + return "none"; +} + +static const char *ipc_json_layout_description(enum sway_container_layout l) { + switch (l) { + case L_VERT: + return "splitv"; + case L_HORIZ: + return "splith"; + case L_TABBED: + return "tabbed"; + case L_STACKED: + return "stacked"; + case L_NONE: + break; + } + return "none"; +} + +static const char *ipc_json_orientation_description(enum sway_container_layout l) { + switch (l) { + case L_VERT: + return "vertical"; + case L_HORIZ: + return "horizontal"; + default: + return "none"; + } +} + +static const char *ipc_json_border_description(enum sway_container_border border) { + switch (border) { + case B_NONE: + return "none"; + case B_PIXEL: + return "pixel"; + case B_NORMAL: + return "normal"; + case B_CSD: + return "csd"; + } + return "unknown"; +} + +static const char *ipc_json_output_transform_description(enum wl_output_transform transform) { + switch (transform) { + case WL_OUTPUT_TRANSFORM_NORMAL: + return "normal"; + case WL_OUTPUT_TRANSFORM_90: + // Sway uses clockwise transforms, while WL_OUTPUT_TRANSFORM_* describes + // anti-clockwise transforms. + return "270"; + case WL_OUTPUT_TRANSFORM_180: + return "180"; + case WL_OUTPUT_TRANSFORM_270: + // Transform also inverted here. + return "90"; + case WL_OUTPUT_TRANSFORM_FLIPPED: + return "flipped"; + case WL_OUTPUT_TRANSFORM_FLIPPED_90: + // Inverted. + return "flipped-270"; + case WL_OUTPUT_TRANSFORM_FLIPPED_180: + return "flipped-180"; + case WL_OUTPUT_TRANSFORM_FLIPPED_270: + // Inverted. + return "flipped-90"; + } + return NULL; +} + +static const char *ipc_json_output_adaptive_sync_status_description( + enum wlr_output_adaptive_sync_status status) { + switch (status) { + case WLR_OUTPUT_ADAPTIVE_SYNC_DISABLED: + return "disabled"; + case WLR_OUTPUT_ADAPTIVE_SYNC_ENABLED: + return "enabled"; + } + return NULL; +} + +static const char *ipc_json_output_mode_aspect_ratio_description( + enum wlr_output_mode_aspect_ratio aspect_ratio) { + switch (aspect_ratio) { + case WLR_OUTPUT_MODE_ASPECT_RATIO_NONE: + return "none"; + case WLR_OUTPUT_MODE_ASPECT_RATIO_4_3: + return "4:3"; + case WLR_OUTPUT_MODE_ASPECT_RATIO_16_9: + return "16:9"; + case WLR_OUTPUT_MODE_ASPECT_RATIO_64_27: + return "64:27"; + case WLR_OUTPUT_MODE_ASPECT_RATIO_256_135: + return "256:135"; + } + return NULL; +} + +static json_object *ipc_json_output_mode_description( + const struct wlr_output_mode *mode) { + const char *pic_ar = + ipc_json_output_mode_aspect_ratio_description(mode->picture_aspect_ratio); + json_object *mode_object = json_object_new_object(); + json_object_object_add(mode_object, "width", + json_object_new_int(mode->width)); + json_object_object_add(mode_object, "height", + json_object_new_int(mode->height)); + json_object_object_add(mode_object, "refresh", + json_object_new_int(mode->refresh)); + json_object_object_add(mode_object, "picture_aspect_ratio", + json_object_new_string(pic_ar)); + return mode_object; +} + +#if WLR_HAS_XWAYLAND +static const char *ipc_json_xwindow_type_description(struct sway_view *view) { + struct wlr_xwayland_surface *surface = view->wlr_xwayland_surface; + struct sway_xwayland *xwayland = &server.xwayland; + + for (size_t i = 0; i < surface->window_type_len; ++i) { + xcb_atom_t type = surface->window_type[i]; + if (type == xwayland->atoms[NET_WM_WINDOW_TYPE_NORMAL]) { + return "normal"; + } else if (type == xwayland->atoms[NET_WM_WINDOW_TYPE_DIALOG]) { + return "dialog"; + } else if (type == xwayland->atoms[NET_WM_WINDOW_TYPE_UTILITY]) { + return "utility"; + } else if (type == xwayland->atoms[NET_WM_WINDOW_TYPE_TOOLBAR]) { + return "toolbar"; + } else if (type == xwayland->atoms[NET_WM_WINDOW_TYPE_SPLASH]) { + return "splash"; + } else if (type == xwayland->atoms[NET_WM_WINDOW_TYPE_MENU]) { + return "menu"; + } else if (type == xwayland->atoms[NET_WM_WINDOW_TYPE_DROPDOWN_MENU]) { + return "dropdown_menu"; + } else if (type == xwayland->atoms[NET_WM_WINDOW_TYPE_POPUP_MENU]) { + return "popup_menu"; + } else if (type == xwayland->atoms[NET_WM_WINDOW_TYPE_TOOLTIP]) { + return "tooltip"; + } else if (type == xwayland->atoms[NET_WM_WINDOW_TYPE_NOTIFICATION]) { + return "notification"; + } else { + return "unknown"; + } + } + + return "unknown"; +} +#endif + +static const char *ipc_json_user_idle_inhibitor_description(enum sway_idle_inhibit_mode mode) { + switch (mode) { + case INHIBIT_IDLE_FOCUS: + return "focus"; + case INHIBIT_IDLE_FULLSCREEN: + return "fullscreen"; + case INHIBIT_IDLE_OPEN: + return "open"; + case INHIBIT_IDLE_VISIBLE: + return "visible"; + case INHIBIT_IDLE_APPLICATION: + return NULL; + } + return NULL; +} + +static const char *ipc_json_content_type_description(enum wp_content_type_v1_type type) { + switch (type) { + case WP_CONTENT_TYPE_V1_TYPE_NONE: + return "none"; + case WP_CONTENT_TYPE_V1_TYPE_PHOTO: + return "photo"; + case WP_CONTENT_TYPE_V1_TYPE_VIDEO: + return "video"; + case WP_CONTENT_TYPE_V1_TYPE_GAME: + return "game"; + } + return NULL; +} + +json_object *ipc_json_get_version(void) { + int major = 0, minor = 0, patch = 0; + json_object *version = json_object_new_object(); + + sscanf(SWAY_VERSION, "%d.%d.%d", &major, &minor, &patch); + + json_object_object_add(version, "human_readable", json_object_new_string(SWAY_VERSION)); + json_object_object_add(version, "sway_original_version", json_object_new_string(SWAY_ORIGINAL_VERSION)); + json_object_object_add(version, "variant", json_object_new_string("sway")); + json_object_object_add(version, "major", json_object_new_int(major)); + json_object_object_add(version, "minor", json_object_new_int(minor)); + json_object_object_add(version, "patch", json_object_new_int(patch)); + json_object_object_add(version, "loaded_config_file_name", json_object_new_string(config->current_config_path)); + + return version; +} + +static json_object *ipc_json_create_rect(struct wlr_box *box) { + json_object *rect = json_object_new_object(); + + json_object_object_add(rect, "x", json_object_new_int(box->x)); + json_object_object_add(rect, "y", json_object_new_int(box->y)); + json_object_object_add(rect, "width", json_object_new_int(box->width)); + json_object_object_add(rect, "height", json_object_new_int(box->height)); + + return rect; +} + +static json_object *ipc_json_create_empty_rect(void) { + struct wlr_box empty = {0, 0, 0, 0}; + + return ipc_json_create_rect(&empty); +} + +static json_object *ipc_json_create_node(int id, const char* type, char *name, + bool focused, json_object *focus, struct wlr_box *box) { + json_object *object = json_object_new_object(); + + json_object_object_add(object, "id", json_object_new_int(id)); + json_object_object_add(object, "type", json_object_new_string(type)); + json_object_object_add(object, "orientation", + json_object_new_string( + ipc_json_orientation_description(L_HORIZ))); + json_object_object_add(object, "percent", NULL); + json_object_object_add(object, "urgent", json_object_new_boolean(false)); + json_object_object_add(object, "marks", json_object_new_array()); + json_object_object_add(object, "focused", json_object_new_boolean(focused)); + json_object_object_add(object, "layout", + json_object_new_string( + ipc_json_layout_description(L_HORIZ))); + + // set default values to be compatible with i3 + json_object_object_add(object, "border", + json_object_new_string( + ipc_json_border_description(B_NONE))); + json_object_object_add(object, "current_border_width", + json_object_new_int(0)); + json_object_object_add(object, "rect", ipc_json_create_rect(box)); + json_object_object_add(object, "deco_rect", ipc_json_create_empty_rect()); + json_object_object_add(object, "window_rect", ipc_json_create_empty_rect()); + json_object_object_add(object, "geometry", ipc_json_create_empty_rect()); + json_object_object_add(object, "name", + name ? json_object_new_string(name) : NULL); + json_object_object_add(object, "window", NULL); + json_object_object_add(object, "nodes", json_object_new_array()); + json_object_object_add(object, "floating_nodes", json_object_new_array()); + json_object_object_add(object, "focus", focus); + json_object_object_add(object, "fullscreen_mode", json_object_new_int(0)); + json_object_object_add(object, "sticky", json_object_new_boolean(false)); + json_object_object_add(object, "floating", NULL); + json_object_object_add(object, "scratchpad_state", NULL); + + return object; +} + +static void ipc_json_describe_wlr_output(struct wlr_output *wlr_output, json_object *object) { + json_object_object_add(object, "primary", json_object_new_boolean(false)); + json_object_object_add(object, "make", + json_object_new_string(wlr_output->make ? wlr_output->make : "Unknown")); + json_object_object_add(object, "model", + json_object_new_string(wlr_output->model ? wlr_output->model : "Unknown")); + json_object_object_add(object, "serial", + json_object_new_string(wlr_output->serial ? wlr_output->serial : "Unknown")); + + json_object *modes_array = json_object_new_array(); + struct wlr_output_mode *mode; + wl_list_for_each(mode, &wlr_output->modes, link) { + json_object *mode_object = json_object_new_object(); + json_object_object_add(mode_object, "width", + json_object_new_int(mode->width)); + json_object_object_add(mode_object, "height", + json_object_new_int(mode->height)); + json_object_object_add(mode_object, "refresh", + json_object_new_int(mode->refresh)); + json_object_array_add(modes_array, mode_object); + } + json_object_object_add(object, "modes", modes_array); + + json_object *features_object = json_object_new_object(); + json_object_object_add(features_object, "adaptive_sync", + json_object_new_boolean(wlr_output->adaptive_sync_supported || + wlr_output->adaptive_sync_status == WLR_OUTPUT_ADAPTIVE_SYNC_ENABLED)); + json_object_object_add(features_object, "hdr", + json_object_new_boolean(output_supports_hdr(wlr_output, NULL))); + json_object_object_add(object, "features", features_object); +} + +static void ipc_json_describe_output(struct sway_output *output, + json_object *object) { + ipc_json_describe_wlr_output(output->wlr_output, object); +} + +static void ipc_json_describe_enabled_output(struct sway_output *output, + json_object *object) { + ipc_json_describe_output(output, object); + + struct wlr_output *wlr_output = output->wlr_output; + json_object_object_add(object, "non_desktop", json_object_new_boolean(false)); + json_object_object_add(object, "active", json_object_new_boolean(true)); + json_object_object_add(object, "dpms", + json_object_new_boolean(wlr_output->enabled)); + json_object_object_add(object, "power", + json_object_new_boolean(wlr_output->enabled)); + json_object_object_add(object, "layout", json_object_new_string("output")); + json_object_object_add(object, "orientation", + json_object_new_string( + ipc_json_orientation_description(L_NONE))); + json_object_object_add(object, "scale", + json_object_new_double(wlr_output->scale)); + json_object_object_add(object, "scale_filter", + json_object_new_string( + sway_output_scale_filter_to_string(output->scale_filter))); + json_object_object_add(object, "transform", + json_object_new_string( + ipc_json_output_transform_description(wlr_output->transform))); + const char *adaptive_sync_status = + ipc_json_output_adaptive_sync_status_description( + wlr_output->adaptive_sync_status); + json_object_object_add(object, "adaptive_sync_status", + json_object_new_string(adaptive_sync_status)); + + // Layer effects + struct json_object *layers = json_object_new_array(); + struct wlr_scene_tree *scene_layers[] = { + output->layers.shell_background, + output->layers.shell_bottom, + output->layers.shell_overlay, + output->layers.shell_top, + }; + size_t nlayers = sizeof(scene_layers) / sizeof(scene_layers[0]); + struct wlr_scene_node *node; + for (size_t i = 0; i < nlayers; ++i) { + wl_list_for_each_reverse(node, &scene_layers[i]->children, link) { + struct sway_layer_surface *surface = scene_descriptor_try_get(node, + SWAY_SCENE_DESC_LAYER_SHELL); + if (!surface || !surface->layer_surface) { + continue; + } + struct wlr_layer_surface_v1 *lsurface = surface->layer_surface; + + // Get the node coordinates + int layer_x, layer_y; + if (wlr_scene_node_coords(&surface->tree->node, &layer_x, &layer_y)) { + // Transform the root-local coordinates to output-local + double lx = layer_x, ly = layer_y; + wlr_output_layout_output_coords(root->output_layout, + output->wlr_output, &lx, &ly); + layer_x = lx; + layer_y = ly; + } else { + // The surface is not visible + continue; + } + + json_object *layer = json_object_new_object(); + json_object_object_add(layer, "namespace", + json_object_new_string(surface->layer_surface->namespace)); + + struct json_object *layer_name = NULL; + switch (lsurface->current.layer) { + case ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND: + layer_name = json_object_new_string("background"); + break; + case ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM: + layer_name = json_object_new_string("bottom"); + break; + case ZWLR_LAYER_SHELL_V1_LAYER_TOP: + layer_name = json_object_new_string("top"); + break; + case ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY: + layer_name = json_object_new_string("overlay"); + break; + } + json_object_object_add(layer, "layer", layer_name); + + json_object *extent = json_object_new_object(); + json_object_object_add(extent, "width", + json_object_new_int(lsurface->current.actual_width)); + json_object_object_add(extent, "height", + json_object_new_int(lsurface->current.actual_height)); + json_object_object_add(extent, "x", json_object_new_int(layer_x)); + json_object_object_add(extent, "y", json_object_new_int(layer_y)); + json_object_object_add(layer, "extent", extent); + + json_object *effects = json_object_new_object(); + json_object_object_add(effects, "blur", json_object_new_boolean(surface->blur_enabled)); + json_object_object_add(effects, "blur_xray", json_object_new_boolean(surface->blur_xray)); + json_object_object_add(effects, "blur_ignore_transparent", + json_object_new_boolean(surface->blur_ignore_transparent)); + json_object_object_add(effects, "shadows", json_object_new_boolean(surface->shadow_enabled)); + json_object_object_add(effects, "corner_radius", json_object_new_int(surface->corner_radius)); + json_object_object_add(layer, "effects", effects); + + json_object_array_add(layers, layer); + } + } + json_object_object_add(object, "layer_shell_surfaces", layers); + + struct sway_workspace *ws = output_get_active_workspace(output); + if (!sway_assert(ws, "Expected output to have a workspace")) { + return; + } + json_object_object_add(object, "current_workspace", + json_object_new_string(ws->name)); + + json_object *modes_array = json_object_new_array(); + struct wlr_output_mode *mode; + wl_list_for_each(mode, &wlr_output->modes, link) { + json_object *mode_object = + ipc_json_output_mode_description(mode); + json_object_array_add(modes_array, mode_object); + } + + json_object_object_add(object, "modes", modes_array); + + json_object *current_mode_object; + if (wlr_output->current_mode != NULL) { + current_mode_object = + ipc_json_output_mode_description(wlr_output->current_mode); + } else { + current_mode_object = json_object_new_object(); + json_object_object_add(current_mode_object, "width", + json_object_new_int(wlr_output->width)); + json_object_object_add(current_mode_object, "height", + json_object_new_int(wlr_output->height)); + json_object_object_add(current_mode_object, "refresh", + json_object_new_int(wlr_output->refresh)); + } + json_object_object_add(object, "current_mode", current_mode_object); + + struct sway_node *parent = node_get_parent(&output->node); + struct wlr_box parent_box = {0, 0, 0, 0}; + + if (parent != NULL) { + node_get_box(parent, &parent_box); + } + + if (parent_box.width != 0 && parent_box.height != 0) { + double percent = ((double)output->width / parent_box.width) + * ((double)output->height / parent_box.height); + json_object_object_add(object, "percent", json_object_new_double(percent)); + } + + json_object_object_add(object, "max_render_time", json_object_new_int(output->max_render_time)); + json_object_object_add(object, "allow_tearing", json_object_new_boolean(output->allow_tearing)); + json_object_object_add(object, "hdr", json_object_new_boolean(output->hdr)); +} + +json_object *ipc_json_describe_disabled_output(struct sway_output *output) { + struct wlr_output *wlr_output = output->wlr_output; + + json_object *object = json_object_new_object(); + + ipc_json_describe_output(output, object); + + json_object_object_add(object, "non_desktop", json_object_new_boolean(false)); + json_object_object_add(object, "type", json_object_new_string("output")); + json_object_object_add(object, "name", + json_object_new_string(wlr_output->name)); + json_object_object_add(object, "active", json_object_new_boolean(false)); + json_object_object_add(object, "dpms", json_object_new_boolean(false)); + json_object_object_add(object, "power", json_object_new_boolean(false)); + + json_object_object_add(object, "current_workspace", NULL); + + json_object *rect_object = json_object_new_object(); + json_object_object_add(rect_object, "x", json_object_new_int(0)); + json_object_object_add(rect_object, "y", json_object_new_int(0)); + json_object_object_add(rect_object, "width", json_object_new_int(0)); + json_object_object_add(rect_object, "height", json_object_new_int(0)); + json_object_object_add(object, "rect", rect_object); + + json_object_object_add(object, "percent", NULL); + + return object; +} + +json_object *ipc_json_describe_non_desktop_output(struct sway_output_non_desktop *output) { + struct wlr_output *wlr_output = output->wlr_output; + + json_object *object = json_object_new_object(); + + ipc_json_describe_wlr_output(wlr_output, object); + + json_object_object_add(object, "non_desktop", json_object_new_boolean(true)); + json_object_object_add(object, "type", json_object_new_string("output")); + json_object_object_add(object, "name", + json_object_new_string(wlr_output->name)); + + return object; +} + +static json_object *ipc_json_describe_scratchpad_output(void) { + struct wlr_box box; + root_get_box(root, &box); + + // Create focus stack for __i3_scratch workspace + json_object *workspace_focus = json_object_new_array(); + for (int i = root->scratchpad->length - 1; i >= 0; --i) { + struct sway_container *container = root->scratchpad->items[i]; + json_object_array_add(workspace_focus, + json_object_new_int(container->node.id)); + } + + json_object *workspace = ipc_json_create_node(i3_scratch_id, "workspace", + "__i3_scratch", false, workspace_focus, &box); + json_object_object_add(workspace, "fullscreen_mode", json_object_new_int(1)); + + // List all hidden scratchpad containers as floating nodes + json_object *floating_array = json_object_new_array(); + for (int i = 0; i < root->scratchpad->length; ++i) { + struct sway_container *container = root->scratchpad->items[i]; + if (container_is_scratchpad_hidden(container)) { + json_object_array_add(floating_array, + ipc_json_describe_node_recursive(&container->node)); + } + } + json_object_object_add(workspace, "floating_nodes", floating_array); + + // Create focus stack for __i3 output + json_object *output_focus = json_object_new_array(); + json_object_array_add(output_focus, json_object_new_int(i3_scratch_id)); + + json_object *output = ipc_json_create_node(i3_output_id, "output", + "__i3", false, output_focus, &box); + json_object_object_add(output, "layout", + json_object_new_string("output")); + + json_object *nodes = json_object_new_array(); + json_object_array_add(nodes, workspace); + json_object_object_add(output, "nodes", nodes); + + return output; +} + +static void ipc_json_describe_workspace(struct sway_workspace *workspace, + json_object *object) { + int num; + if (isdigit(workspace->name[0])) { + errno = 0; + char *endptr = NULL; + long long parsed_num = strtoll(workspace->name, &endptr, 10); + if (errno != 0 || parsed_num > INT32_MAX || parsed_num < 0 || endptr == workspace->name) { + num = -1; + } else { + num = (int) parsed_num; + } + } else { + num = -1; + } + json_object_object_add(object, "num", json_object_new_int(num)); + json_object_object_add(object, "fullscreen_mode", json_object_new_int(1)); + json_object_object_add(object, "output", workspace->output ? + json_object_new_string(workspace->output->wlr_output->name) : NULL); + json_object_object_add(object, "urgent", + json_object_new_boolean(workspace->urgent)); + json_object_object_add(object, "representation", workspace->representation ? + json_object_new_string(workspace->representation) : NULL); + + json_object_object_add(object, "layout", + json_object_new_string( + ipc_json_layout_description(workspace->layout))); + json_object_object_add(object, "orientation", + json_object_new_string( + ipc_json_orientation_description(workspace->layout))); + + // Floating + json_object *floating_array = json_object_new_array(); + for (int i = 0; i < workspace->floating->length; ++i) { + struct sway_container *floater = workspace->floating->items[i]; + json_object_array_add(floating_array, + ipc_json_describe_node_recursive(&floater->node)); + } + json_object_object_add(object, "floating_nodes", floating_array); +} + +static void get_deco_rect(struct sway_container *c, struct wlr_box *deco_rect) { + enum sway_container_layout parent_layout = container_parent_layout(c); + list_t *siblings = container_get_siblings(c); + bool tab_or_stack = (parent_layout == L_TABBED || parent_layout == L_STACKED) + && ((siblings && siblings->length > 1) || !config->hide_lone_tab); + if (((!tab_or_stack || container_is_floating(c)) && + c->current.border != B_NORMAL) || + c->pending.fullscreen_mode != FULLSCREEN_NONE || + c->pending.workspace == NULL) { + deco_rect->x = deco_rect->y = deco_rect->width = deco_rect->height = 0; + return; + } + + if (c->pending.parent) { + deco_rect->x = c->pending.x - c->pending.parent->pending.x; + deco_rect->y = c->pending.y - c->pending.parent->pending.y; + } else { + deco_rect->x = c->pending.x - c->pending.workspace->x; + deco_rect->y = c->pending.y - c->pending.workspace->y; + } + deco_rect->width = c->pending.width; + deco_rect->height = container_titlebar_height(); + + if (!container_is_floating(c)) { + if (parent_layout == L_TABBED) { + deco_rect->width = c->pending.parent + ? c->pending.parent->pending.width / c->pending.parent->pending.children->length + : c->pending.workspace->width / c->pending.workspace->tiling->length; + deco_rect->x += deco_rect->width * container_sibling_index(c); + } else if (parent_layout == L_STACKED) { + if (!c->view) { + size_t siblings = container_get_siblings(c)->length; + deco_rect->y -= deco_rect->height * siblings; + } + deco_rect->y += deco_rect->height * container_sibling_index(c); + } + } +} + +static void ipc_json_describe_view(struct sway_container *c, json_object *object) { + json_object_object_add(object, "pid", json_object_new_int(c->view->pid)); + + const char *app_id = view_get_app_id(c->view); + json_object_object_add(object, "app_id", + app_id ? json_object_new_string(app_id) : NULL); + + json_object_object_add(object, "foreign_toplevel_identifier", + c->view->ext_foreign_toplevel ? + json_object_new_string(c->view->ext_foreign_toplevel->identifier) : NULL); + + bool visible = view_is_visible(c->view); + json_object_object_add(object, "visible", json_object_new_boolean(visible)); + + bool has_titlebar = c->title_bar.tree->node.enabled; + struct wlr_box window_box = { + c->pending.content_x - c->pending.x, + has_titlebar ? 0 : c->pending.content_y - c->pending.y, + c->pending.content_width, + c->pending.content_height + }; + + json_object_object_add(object, "window_rect", ipc_json_create_rect(&window_box)); + + struct wlr_box geometry = {0, 0, c->view->natural_width, c->view->natural_height}; + json_object_object_add(object, "geometry", ipc_json_create_rect(&geometry)); + + json_object_object_add(object, "max_render_time", json_object_new_int(c->view->max_render_time)); + + json_object_object_add(object, "allow_tearing", json_object_new_boolean(view_can_tear(c->view))); + + json_object_object_add(object, "shell", json_object_new_string(view_get_shell(c->view))); + + json_object_object_add(object, "inhibit_idle", + json_object_new_boolean(view_inhibit_idle(c->view))); + + const char *sandbox_engine = view_get_sandbox_engine(c->view); + json_object_object_add(object, "sandbox_engine", + sandbox_engine ? json_object_new_string(sandbox_engine) : NULL); + + const char *sandbox_app_id = view_get_sandbox_app_id(c->view); + json_object_object_add(object, "sandbox_app_id", + sandbox_app_id ? json_object_new_string(sandbox_app_id) : NULL); + + const char *sandbox_instance_id = view_get_sandbox_instance_id(c->view); + json_object_object_add(object, "sandbox_instance_id", + sandbox_instance_id ? json_object_new_string(sandbox_instance_id) : NULL); + + const char *tag = view_get_tag(c->view); + json_object_object_add(object, "tag", tag ? json_object_new_string(tag) : NULL); + + json_object *idle_inhibitors = json_object_new_object(); + + struct sway_idle_inhibitor_v1 *user_inhibitor = + sway_idle_inhibit_v1_user_inhibitor_for_view(c->view); + + if (user_inhibitor) { + json_object_object_add(idle_inhibitors, "user", + json_object_new_string( + ipc_json_user_idle_inhibitor_description(user_inhibitor->mode))); + } else { + json_object_object_add(idle_inhibitors, "user", + json_object_new_string("none")); + } + + struct sway_idle_inhibitor_v1 *application_inhibitor = + sway_idle_inhibit_v1_application_inhibitor_for_view(c->view); + + if (application_inhibitor) { + json_object_object_add(idle_inhibitors, "application", + json_object_new_string("enabled")); + } else { + json_object_object_add(idle_inhibitors, "application", + json_object_new_string("none")); + } + + json_object_object_add(object, "idle_inhibitors", idle_inhibitors); + + enum wp_content_type_v1_type content_type = WP_CONTENT_TYPE_V1_TYPE_NONE; + if (c->view->surface != NULL) { + content_type = wlr_surface_get_content_type_v1(server.content_type_manager_v1, + c->view->surface); + } + if (content_type != WP_CONTENT_TYPE_V1_TYPE_NONE) { + json_object_object_add(object, "content_type", + json_object_new_string(ipc_json_content_type_description(content_type))); + } + +#if WLR_HAS_XWAYLAND + if (c->view->type == SWAY_VIEW_XWAYLAND) { + json_object_object_add(object, "window", + json_object_new_int(view_get_x11_window_id(c->view))); + + json_object *window_props = json_object_new_object(); + + const char *class = view_get_class(c->view); + if (class) { + json_object_object_add(window_props, "class", json_object_new_string(class)); + } + const char *instance = view_get_instance(c->view); + if (instance) { + json_object_object_add(window_props, "instance", json_object_new_string(instance)); + } + if (c->title) { + json_object_object_add(window_props, "title", json_object_new_string(c->title)); + } + + // the transient_for key is always present in i3's output + uint32_t parent_id = view_get_x11_parent_id(c->view); + json_object_object_add(window_props, "transient_for", + parent_id ? json_object_new_int(parent_id) : NULL); + + const char *role = view_get_window_role(c->view); + if (role) { + json_object_object_add(window_props, "window_role", json_object_new_string(role)); + } + + uint32_t window_type = view_get_window_type(c->view); + if (window_type) { + json_object_object_add(window_props, "window_type", + json_object_new_string( + ipc_json_xwindow_type_description(c->view))); + } + + json_object_object_add(object, "window_properties", window_props); + } +#endif +} + +static void ipc_json_describe_container(struct sway_container *c, json_object *object) { + json_object_object_add(object, "name", + c->title ? json_object_new_string(c->title) : NULL); + bool floating = container_is_floating(c); + if (floating) { + json_object_object_add(object, "type", + json_object_new_string("floating_con")); + } + + json_object_object_add(object, "layout", + json_object_new_string( + ipc_json_layout_description(c->pending.layout))); + + json_object_object_add(object, "orientation", + json_object_new_string( + ipc_json_orientation_description(c->pending.layout))); + + bool urgent = c->view ? + view_is_urgent(c->view) : container_has_urgent_child(c); + json_object_object_add(object, "urgent", json_object_new_boolean(urgent)); + json_object_object_add(object, "sticky", json_object_new_boolean(c->is_sticky)); + + // sway doesn't track the floating reason, so we can't use "auto_on" or "user_off" + json_object_object_add(object, "floating", + json_object_new_string(floating ? "user_on" : "auto_off")); + + json_object_object_add(object, "fullscreen_mode", + json_object_new_int(c->pending.fullscreen_mode)); + + // sway doesn't track if window was resized in scratchpad, so we can't use "changed" + json_object_object_add(object, "scratchpad_state", + json_object_new_string(!c->scratchpad ? "none" : "fresh")); + + struct sway_node *parent = node_get_parent(&c->node); + struct wlr_box parent_box = {0, 0, 0, 0}; + + if (parent != NULL) { + node_get_box(parent, &parent_box); + } + + if (parent_box.width != 0 && parent_box.height != 0) { + double percent = ((double)c->pending.width / parent_box.width) + * ((double)c->pending.height / parent_box.height); + json_object_object_add(object, "percent", json_object_new_double(percent)); + } + + json_object_object_add(object, "border", + json_object_new_string( + ipc_json_border_description(c->current.border))); + json_object_object_add(object, "current_border_width", + json_object_new_int(c->current.border_thickness)); + json_object_object_add(object, "floating_nodes", json_object_new_array()); + + struct wlr_box deco_box = {0, 0, 0, 0}; + get_deco_rect(c, &deco_box); + json_object_object_add(object, "deco_rect", ipc_json_create_rect(&deco_box)); + + json_object *marks = json_object_new_array(); + list_t *con_marks = c->marks; + for (int i = 0; i < con_marks->length; ++i) { + json_object_array_add(marks, json_object_new_string(con_marks->items[i])); + } + + json_object_object_add(object, "marks", marks); + + if (c->view) { + ipc_json_describe_view(c, object); + } +} + +struct focus_inactive_data { + struct sway_node *node; + json_object *object; +}; + +static void focus_inactive_children_iterator(struct sway_node *node, + void *_data) { + struct focus_inactive_data *data = _data; + json_object *focus = data->object; + if (data->node == &root->node) { + struct sway_output *output = node_get_output(node); + if (output == NULL) { + return; + } + size_t id = output->node.id; + int len = json_object_array_length(focus); + for (int i = 0; i < len; ++i) { + if ((size_t) json_object_get_int(json_object_array_get_idx(focus, i)) == id) { + return; + } + } + node = &output->node; + } else if (node_get_parent(node) != data->node) { + return; + } + json_object_array_add(focus, json_object_new_int(node->id)); +} + +json_object *ipc_json_describe_node(struct sway_node *node) { + struct sway_seat *seat = input_manager_get_default_seat(); + bool focused = seat_get_focus(seat) == node; + char *name = node_get_name(node); + + struct wlr_box box; + node_get_box(node, &box); + if (node->type == N_CONTAINER) { + struct wlr_box deco_rect = {0, 0, 0, 0}; + get_deco_rect(node->sway_container, &deco_rect); + size_t count = 1; + if (container_parent_layout(node->sway_container) == L_STACKED) { + count = container_get_siblings(node->sway_container)->length; + } + box.y += deco_rect.height * count; + box.height -= deco_rect.height * count; + } + + json_object *focus = json_object_new_array(); + struct focus_inactive_data data = { + .node = node, + .object = focus, + }; + seat_for_each_node(seat, focus_inactive_children_iterator, &data); + + json_object *object = ipc_json_create_node((int)node->id, + ipc_json_node_type_description(node->type), name, focused, focus, &box); + + switch (node->type) { + case N_ROOT: + break; + case N_OUTPUT: + ipc_json_describe_enabled_output(node->sway_output, object); + break; + case N_CONTAINER: + ipc_json_describe_container(node->sway_container, object); + break; + case N_WORKSPACE: + ipc_json_describe_workspace(node->sway_workspace, object); + break; + } + + return object; +} + +json_object *ipc_json_describe_node_recursive(struct sway_node *node) { + json_object *object = ipc_json_describe_node(node); + int i; + + json_object *children = json_object_new_array(); + switch (node->type) { + case N_ROOT: + json_object_array_add(children, + ipc_json_describe_scratchpad_output()); + for (i = 0; i < root->outputs->length; ++i) { + struct sway_output *output = root->outputs->items[i]; + json_object_array_add(children, + ipc_json_describe_node_recursive(&output->node)); + } + break; + case N_OUTPUT: + for (i = 0; i < node->sway_output->workspaces->length; ++i) { + struct sway_workspace *ws = node->sway_output->workspaces->items[i]; + json_object_array_add(children, + ipc_json_describe_node_recursive(&ws->node)); + } + break; + case N_WORKSPACE: + for (i = 0; i < node->sway_workspace->tiling->length; ++i) { + struct sway_container *con = node->sway_workspace->tiling->items[i]; + json_object_array_add(children, + ipc_json_describe_node_recursive(&con->node)); + } + break; + case N_CONTAINER: + if (node->sway_container->pending.children) { + for (i = 0; i < node->sway_container->pending.children->length; ++i) { + struct sway_container *child = + node->sway_container->pending.children->items[i]; + json_object_array_add(children, + ipc_json_describe_node_recursive(&child->node)); + } + } + break; + } + json_object_object_add(object, "nodes", children); + + return object; +} + +#if WLR_HAS_LIBINPUT_BACKEND +static json_object *describe_libinput_device(struct libinput_device *device) { + json_object *object = json_object_new_object(); + + const char *events = "unknown"; + switch (libinput_device_config_send_events_get_mode(device)) { + case LIBINPUT_CONFIG_SEND_EVENTS_ENABLED: + events = "enabled"; + break; + case LIBINPUT_CONFIG_SEND_EVENTS_DISABLED_ON_EXTERNAL_MOUSE: + events = "disabled_on_external_mouse"; + break; + case LIBINPUT_CONFIG_SEND_EVENTS_DISABLED: + events = "disabled"; + break; + } + json_object_object_add(object, "send_events", + json_object_new_string(events)); + + if (libinput_device_config_tap_get_finger_count(device) > 0) { + const char *tap = "unknown"; + switch (libinput_device_config_tap_get_enabled(device)) { + case LIBINPUT_CONFIG_TAP_ENABLED: + tap = "enabled"; + break; + case LIBINPUT_CONFIG_TAP_DISABLED: + tap = "disabled"; + break; + } + json_object_object_add(object, "tap", json_object_new_string(tap)); + + const char *button_map = "unknown"; + switch (libinput_device_config_tap_get_button_map(device)) { + case LIBINPUT_CONFIG_TAP_MAP_LRM: + button_map = "lrm"; + break; + case LIBINPUT_CONFIG_TAP_MAP_LMR: + button_map = "lmr"; + break; + } + json_object_object_add(object, "tap_button_map", + json_object_new_string(button_map)); + + const char* drag = "unknown"; + switch (libinput_device_config_tap_get_drag_enabled(device)) { + case LIBINPUT_CONFIG_DRAG_ENABLED: + drag = "enabled"; + break; + case LIBINPUT_CONFIG_DRAG_DISABLED: + drag = "disabled"; + break; + } + json_object_object_add(object, "tap_drag", + json_object_new_string(drag)); + + const char *drag_lock = "unknown"; + switch (libinput_device_config_tap_get_drag_lock_enabled(device)) { + case LIBINPUT_CONFIG_DRAG_LOCK_ENABLED: + drag_lock = "enabled"; + break; + case LIBINPUT_CONFIG_DRAG_LOCK_DISABLED: + drag_lock = "disabled"; + break; +#if HAVE_LIBINPUT_CONFIG_DRAG_LOCK_ENABLED_STICKY + case LIBINPUT_CONFIG_DRAG_LOCK_ENABLED_STICKY: + drag_lock = "enabled_sticky"; + break; +#endif + } + json_object_object_add(object, "tap_drag_lock", + json_object_new_string(drag_lock)); + } + + if (libinput_device_config_accel_is_available(device)) { + double accel = libinput_device_config_accel_get_speed(device); + json_object_object_add(object, "accel_speed", + json_object_new_double(accel)); + + const char *accel_profile = "unknown"; + switch (libinput_device_config_accel_get_profile(device)) { + case LIBINPUT_CONFIG_ACCEL_PROFILE_NONE: + accel_profile = "none"; + break; + case LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT: + accel_profile = "flat"; + break; + case LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE: + accel_profile = "adaptive"; + break; +#if HAVE_LIBINPUT_CONFIG_ACCEL_PROFILE_CUSTOM + case LIBINPUT_CONFIG_ACCEL_PROFILE_CUSTOM: + accel_profile = "custom"; + break; +#endif + } + json_object_object_add(object, "accel_profile", + json_object_new_string(accel_profile)); + } + + if (libinput_device_config_scroll_has_natural_scroll(device)) { + const char *natural_scroll = "disabled"; + if (libinput_device_config_scroll_get_natural_scroll_enabled(device)) { + natural_scroll = "enabled"; + } + json_object_object_add(object, "natural_scroll", + json_object_new_string(natural_scroll)); + } + + if (libinput_device_config_left_handed_is_available(device)) { + const char *left_handed = "disabled"; + if (libinput_device_config_left_handed_get(device) != 0) { + left_handed = "enabled"; + } + json_object_object_add(object, "left_handed", + json_object_new_string(left_handed)); + } + + uint32_t click_methods = libinput_device_config_click_get_methods(device); + if ((click_methods & ~LIBINPUT_CONFIG_CLICK_METHOD_NONE) != 0) { + const char *click_method = "unknown"; + switch (libinput_device_config_click_get_method(device)) { + case LIBINPUT_CONFIG_CLICK_METHOD_NONE: + click_method = "none"; + break; + case LIBINPUT_CONFIG_CLICK_METHOD_BUTTON_AREAS: + click_method = "button_areas"; + break; + case LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER: + click_method = "clickfinger"; + break; + } + json_object_object_add(object, "click_method", + json_object_new_string(click_method)); + + const char *button_map = "unknown"; + switch (libinput_device_config_click_get_clickfinger_button_map(device)) { + case LIBINPUT_CONFIG_CLICKFINGER_MAP_LRM: + button_map = "lrm"; + break; + case LIBINPUT_CONFIG_CLICKFINGER_MAP_LMR: + button_map = "lmr"; + break; + } + json_object_object_add(object, "clickfinger_button_map", + json_object_new_string(button_map)); + } + + if (libinput_device_config_middle_emulation_is_available(device)) { + const char *middle_emulation = "unknown"; + switch (libinput_device_config_middle_emulation_get_enabled(device)) { + case LIBINPUT_CONFIG_MIDDLE_EMULATION_ENABLED: + middle_emulation = "enabled"; + break; + case LIBINPUT_CONFIG_MIDDLE_EMULATION_DISABLED: + middle_emulation = "disabled"; + break; + } + json_object_object_add(object, "middle_emulation", + json_object_new_string(middle_emulation)); + } + + uint32_t scroll_methods = libinput_device_config_scroll_get_methods(device); + if ((scroll_methods & ~LIBINPUT_CONFIG_SCROLL_NO_SCROLL) != 0) { + const char *scroll_method = "unknown"; + switch (libinput_device_config_scroll_get_method(device)) { + case LIBINPUT_CONFIG_SCROLL_NO_SCROLL: + scroll_method = "none"; + break; + case LIBINPUT_CONFIG_SCROLL_2FG: + scroll_method = "two_finger"; + break; + case LIBINPUT_CONFIG_SCROLL_EDGE: + scroll_method = "edge"; + break; + case LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN: + scroll_method = "on_button_down"; + break; + } + json_object_object_add(object, "scroll_method", + json_object_new_string(scroll_method)); + + if ((scroll_methods & LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN) != 0) { + uint32_t button = libinput_device_config_scroll_get_button(device); + json_object_object_add(object, "scroll_button", + json_object_new_int(button)); + const char *lock = "unknown"; + switch (libinput_device_config_scroll_get_button_lock(device)) { + case LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_ENABLED: + lock = "enabled"; + break; + case LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_DISABLED: + lock = "disabled"; + break; + } + json_object_object_add(object, "scroll_button_lock", + json_object_new_string(lock)); + } + } + + if (libinput_device_config_dwt_is_available(device)) { + const char *dwt = "unknown"; + switch (libinput_device_config_dwt_get_enabled(device)) { + case LIBINPUT_CONFIG_DWT_ENABLED: + dwt = "enabled"; + break; + case LIBINPUT_CONFIG_DWT_DISABLED: + dwt = "disabled"; + break; + } + json_object_object_add(object, "dwt", json_object_new_string(dwt)); + } + + if (libinput_device_config_dwtp_is_available(device)) { + const char *dwtp = "unknown"; + switch (libinput_device_config_dwtp_get_enabled(device)) { + case LIBINPUT_CONFIG_DWTP_ENABLED: + dwtp = "enabled"; + break; + case LIBINPUT_CONFIG_DWTP_DISABLED: + dwtp = "disabled"; + break; + } + json_object_object_add(object, "dwtp", json_object_new_string(dwtp)); + } + + if (libinput_device_config_calibration_has_matrix(device)) { + float matrix[6]; + libinput_device_config_calibration_get_matrix(device, matrix); + struct json_object* array = json_object_new_array(); + struct json_object* x; + for (int i = 0; i < 6; i++) { + x = json_object_new_double(matrix[i]); + json_object_array_add(array, x); + } + json_object_object_add(object, "calibration_matrix", array); + } + + return object; +} +#endif + +json_object *ipc_json_describe_input(struct sway_input_device *device) { + if (!(sway_assert(device, "Device must not be null"))) { + return NULL; + } + + json_object *object = json_object_new_object(); + + json_object_object_add(object, "identifier", + json_object_new_string(device->identifier)); + json_object_object_add(object, "name", + json_object_new_string(device->wlr_device->name)); + json_object_object_add(object, "type", + json_object_new_string( + input_device_get_type(device))); + + if (device->wlr_device->type == WLR_INPUT_DEVICE_KEYBOARD) { + struct wlr_keyboard *keyboard = + wlr_keyboard_from_input_device(device->wlr_device); + struct xkb_keymap *keymap = keyboard->keymap; + struct xkb_state *state = keyboard->xkb_state; + + json_object_object_add(object, "repeat_delay", + json_object_new_int(keyboard->repeat_info.delay)); + json_object_object_add(object, "repeat_rate", + json_object_new_int(keyboard->repeat_info.rate)); + + json_object *layouts_arr = json_object_new_array(); + json_object_object_add(object, "xkb_layout_names", layouts_arr); + + xkb_layout_index_t num_layouts = + keymap ? xkb_keymap_num_layouts(keymap) : 0; + // Virtual keyboards might have null keymap + xkb_layout_index_t layout_idx; + for (layout_idx = 0; layout_idx < num_layouts; layout_idx++) { + const char *layout = xkb_keymap_layout_get_name(keymap, layout_idx); + json_object_array_add(layouts_arr, + layout ? json_object_new_string(layout) : NULL); + + bool is_active = xkb_state_layout_index_is_active(state, + layout_idx, XKB_STATE_LAYOUT_EFFECTIVE); + if (is_active) { + json_object_object_add(object, "xkb_active_layout_index", + json_object_new_int(layout_idx)); + json_object_object_add(object, "xkb_active_layout_name", + layout ? json_object_new_string(layout) : NULL); + } + } + } + + if (device->wlr_device->type == WLR_INPUT_DEVICE_POINTER) { + struct input_config *ic = input_device_get_config(device); + float scroll_factor = 1.0f; + if (ic != NULL && !isnan(ic->scroll_factor) && + ic->scroll_factor != FLT_MIN) { + scroll_factor = ic->scroll_factor; + } + json_object_object_add(object, "scroll_factor", + json_object_new_double(scroll_factor)); + } + +#if WLR_HAS_LIBINPUT_BACKEND + if (wlr_input_device_is_libinput(device->wlr_device)) { + struct libinput_device *libinput_dev; + libinput_dev = wlr_libinput_get_device_handle(device->wlr_device); + json_object_object_add(object, "libinput", + describe_libinput_device(libinput_dev)); + json_object_object_add(object, "vendor", + json_object_new_int(libinput_device_get_id_vendor(libinput_dev))); + json_object_object_add(object, "product", + json_object_new_int(libinput_device_get_id_product(libinput_dev))); + } +#endif + + return object; +} + +json_object *ipc_json_describe_seat(struct sway_seat *seat) { + if (!(sway_assert(seat, "Seat must not be null"))) { + return NULL; + } + + json_object *object = json_object_new_object(); + struct sway_node *focus = seat_get_focus(seat); + + json_object_object_add(object, "name", + json_object_new_string(seat->wlr_seat->name)); + json_object_object_add(object, "capabilities", + json_object_new_int(seat->wlr_seat->capabilities)); + json_object_object_add(object, "focus", + json_object_new_int(focus ? focus->id : 0)); + + json_object *devices = json_object_new_array(); + struct sway_seat_device *device = NULL; + wl_list_for_each(device, &seat->devices, link) { + json_object_array_add(devices, ipc_json_describe_input(device->input_device)); + } + json_object_object_add(object, "devices", devices); + + return object; +} + +static uint32_t event_to_x11_button(uint32_t event) { + switch (event) { + case BTN_LEFT: + return 1; + case BTN_MIDDLE: + return 2; + case BTN_RIGHT: + return 3; + case SWAY_SCROLL_UP: + return 4; + case SWAY_SCROLL_DOWN: + return 5; + case SWAY_SCROLL_LEFT: + return 6; + case SWAY_SCROLL_RIGHT: + return 7; + case BTN_SIDE: + return 8; + case BTN_EXTRA: + return 9; + default: + return 0; + } +} + +json_object *ipc_json_describe_bar_config(struct bar_config *bar) { + if (!sway_assert(bar, "Bar must not be NULL")) { + return NULL; + } + + json_object *json = json_object_new_object(); + json_object_object_add(json, "id", json_object_new_string(bar->id)); + json_object_object_add(json, "mode", json_object_new_string(bar->mode)); + json_object_object_add(json, "hidden_state", + json_object_new_string(bar->hidden_state)); + json_object_object_add(json, "position", + json_object_new_string(bar->position)); + json_object_object_add(json, "status_command", bar->status_command ? + json_object_new_string(bar->status_command) : NULL); + json_object_object_add(json, "font", + json_object_new_string((bar->font) ? bar->font : config->font)); + + json_object *gaps = json_object_new_object(); + json_object_object_add(gaps, "top", + json_object_new_int(bar->gaps.top)); + json_object_object_add(gaps, "right", + json_object_new_int(bar->gaps.right)); + json_object_object_add(gaps, "bottom", + json_object_new_int(bar->gaps.bottom)); + json_object_object_add(gaps, "left", + json_object_new_int(bar->gaps.left)); + json_object_object_add(json, "gaps", gaps); + + if (bar->separator_symbol) { + json_object_object_add(json, "separator_symbol", + json_object_new_string(bar->separator_symbol)); + } + json_object_object_add(json, "bar_height", + json_object_new_int(bar->height)); + json_object_object_add(json, "status_padding", + json_object_new_int(bar->status_padding)); + json_object_object_add(json, "status_edge_padding", + json_object_new_int(bar->status_edge_padding)); + json_object_object_add(json, "wrap_scroll", + json_object_new_boolean(bar->wrap_scroll)); + json_object_object_add(json, "workspace_buttons", + json_object_new_boolean(bar->workspace_buttons)); + json_object_object_add(json, "strip_workspace_numbers", + json_object_new_boolean(bar->strip_workspace_numbers)); + json_object_object_add(json, "strip_workspace_name", + json_object_new_boolean(bar->strip_workspace_name)); + json_object_object_add(json, "workspace_min_width", + json_object_new_int(bar->workspace_min_width)); + json_object_object_add(json, "binding_mode_indicator", + json_object_new_boolean(bar->binding_mode_indicator)); + json_object_object_add(json, "verbose", + json_object_new_boolean(bar->verbose)); + json_object_object_add(json, "pango_markup", + json_object_new_boolean(bar->pango_markup == PANGO_MARKUP_DEFAULT + ? config->pango_markup + : bar->pango_markup)); + + json_object *colors = json_object_new_object(); + json_object_object_add(colors, "background", + json_object_new_string(bar->colors.background)); + json_object_object_add(colors, "statusline", + json_object_new_string(bar->colors.statusline)); + json_object_object_add(colors, "separator", + json_object_new_string(bar->colors.separator)); + + if (bar->colors.focused_background) { + json_object_object_add(colors, "focused_background", + json_object_new_string(bar->colors.focused_background)); + } else { + json_object_object_add(colors, "focused_background", + json_object_new_string(bar->colors.background)); + } + + if (bar->colors.focused_statusline) { + json_object_object_add(colors, "focused_statusline", + json_object_new_string(bar->colors.focused_statusline)); + } else { + json_object_object_add(colors, "focused_statusline", + json_object_new_string(bar->colors.statusline)); + } + + if (bar->colors.focused_separator) { + json_object_object_add(colors, "focused_separator", + json_object_new_string(bar->colors.focused_separator)); + } else { + json_object_object_add(colors, "focused_separator", + json_object_new_string(bar->colors.separator)); + } + + json_object_object_add(colors, "focused_workspace_border", + json_object_new_string(bar->colors.focused_workspace_border)); + json_object_object_add(colors, "focused_workspace_bg", + json_object_new_string(bar->colors.focused_workspace_bg)); + json_object_object_add(colors, "focused_workspace_text", + json_object_new_string(bar->colors.focused_workspace_text)); + + json_object_object_add(colors, "inactive_workspace_border", + json_object_new_string(bar->colors.inactive_workspace_border)); + json_object_object_add(colors, "inactive_workspace_bg", + json_object_new_string(bar->colors.inactive_workspace_bg)); + json_object_object_add(colors, "inactive_workspace_text", + json_object_new_string(bar->colors.inactive_workspace_text)); + + json_object_object_add(colors, "active_workspace_border", + json_object_new_string(bar->colors.active_workspace_border)); + json_object_object_add(colors, "active_workspace_bg", + json_object_new_string(bar->colors.active_workspace_bg)); + json_object_object_add(colors, "active_workspace_text", + json_object_new_string(bar->colors.active_workspace_text)); + + json_object_object_add(colors, "urgent_workspace_border", + json_object_new_string(bar->colors.urgent_workspace_border)); + json_object_object_add(colors, "urgent_workspace_bg", + json_object_new_string(bar->colors.urgent_workspace_bg)); + json_object_object_add(colors, "urgent_workspace_text", + json_object_new_string(bar->colors.urgent_workspace_text)); + + if (bar->colors.binding_mode_border) { + json_object_object_add(colors, "binding_mode_border", + json_object_new_string(bar->colors.binding_mode_border)); + } else { + json_object_object_add(colors, "binding_mode_border", + json_object_new_string(bar->colors.urgent_workspace_border)); + } + + if (bar->colors.binding_mode_bg) { + json_object_object_add(colors, "binding_mode_bg", + json_object_new_string(bar->colors.binding_mode_bg)); + } else { + json_object_object_add(colors, "binding_mode_bg", + json_object_new_string(bar->colors.urgent_workspace_bg)); + } + + if (bar->colors.binding_mode_text) { + json_object_object_add(colors, "binding_mode_text", + json_object_new_string(bar->colors.binding_mode_text)); + } else { + json_object_object_add(colors, "binding_mode_text", + json_object_new_string(bar->colors.urgent_workspace_text)); + } + + json_object_object_add(json, "colors", colors); + + if (bar->bindings->length > 0) { + json_object *bindings = json_object_new_array(); + for (int i = 0; i < bar->bindings->length; ++i) { + struct bar_binding *binding = bar->bindings->items[i]; + json_object *bind = json_object_new_object(); + json_object_object_add(bind, "input_code", + json_object_new_int(event_to_x11_button(binding->button))); + json_object_object_add(bind, "event_code", + json_object_new_int(binding->button)); + json_object_object_add(bind, "command", + json_object_new_string(binding->command)); + json_object_object_add(bind, "release", + json_object_new_boolean(binding->release)); + json_object_array_add(bindings, bind); + } + json_object_object_add(json, "bindings", bindings); + } + + // Add outputs if defined + if (bar->outputs && bar->outputs->length > 0) { + json_object *outputs = json_object_new_array(); + for (int i = 0; i < bar->outputs->length; ++i) { + const char *name = bar->outputs->items[i]; + json_object_array_add(outputs, json_object_new_string(name)); + } + json_object_object_add(json, "outputs", outputs); + } +#if HAVE_TRAY + // Add tray outputs if defined + if (bar->tray_outputs && bar->tray_outputs->length > 0) { + json_object *tray_outputs = json_object_new_array(); + for (int i = 0; i < bar->tray_outputs->length; ++i) { + const char *name = bar->tray_outputs->items[i]; + json_object_array_add(tray_outputs, json_object_new_string(name)); + } + json_object_object_add(json, "tray_outputs", tray_outputs); + } + + json_object *tray_bindings = json_object_new_array(); + struct tray_binding *tray_bind = NULL; + wl_list_for_each(tray_bind, &bar->tray_bindings, link) { + json_object *bind = json_object_new_object(); + json_object_object_add(bind, "input_code", + json_object_new_int(event_to_x11_button(tray_bind->button))); + json_object_object_add(bind, "event_code", + json_object_new_int(tray_bind->button)); + json_object_object_add(bind, "command", + json_object_new_string(tray_bind->command)); + json_object_array_add(tray_bindings, bind); + } + if (json_object_array_length(tray_bindings) > 0) { + json_object_object_add(json, "tray_bindings", tray_bindings); + } else { + json_object_put(tray_bindings); + } + + if (bar->icon_theme) { + json_object_object_add(json, "icon_theme", + json_object_new_string(bar->icon_theme)); + } + + json_object_object_add(json, "tray_padding", + json_object_new_int(bar->tray_padding)); +#endif + return json; +} + +json_object *ipc_json_get_binding_mode(void) { + json_object *current_mode = json_object_new_object(); + json_object_object_add(current_mode, "name", + json_object_new_string(config->current_mode->name)); + return current_mode; +} diff --git a/sway/ipc-server.c b/sway/ipc-server.c new file mode 100644 index 0000000..b934bb5 --- /dev/null +++ b/sway/ipc-server.c @@ -0,0 +1,977 @@ +// See https://i3wm.org/docs/ipc.html for protocol information +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/desktop/transaction.h" +#include "sway/ipc-json.h" +#include "sway/ipc-server.h" +#include "sway/output.h" +#include "sway/server.h" +#include "sway/input/input-manager.h" +#include "sway/input/keyboard.h" +#include "sway/input/seat.h" +#include "sway/tree/root.h" +#include "sway/tree/view.h" +#include "sway/tree/workspace.h" +#include "list.h" +#include "log.h" +#include "util.h" + +static int ipc_socket = -1; +static struct wl_event_source *ipc_event_source = NULL; +static struct sockaddr_un *ipc_sockaddr = NULL; +static list_t *ipc_client_list = NULL; +static struct wl_listener ipc_display_destroy; + +static const char ipc_magic[] = {'i', '3', '-', 'i', 'p', 'c'}; + +#define IPC_HEADER_SIZE (sizeof(ipc_magic) + 8) + +struct ipc_client { + struct wl_event_source *event_source; + struct wl_event_source *writable_event_source; + struct sway_server *server; + int fd; + enum ipc_command_type subscribed_events; + size_t write_buffer_len; + size_t write_buffer_size; + char *write_buffer; + // The following are for storing data between event_loop calls + uint32_t pending_length; + enum ipc_command_type pending_type; +}; + +int ipc_handle_connection(int fd, uint32_t mask, void *data); +int ipc_client_handle_readable(int client_fd, uint32_t mask, void *data); +int ipc_client_handle_writable(int client_fd, uint32_t mask, void *data); +void ipc_client_disconnect(struct ipc_client *client); +void ipc_client_handle_command(struct ipc_client *client, uint32_t payload_length, + enum ipc_command_type payload_type); +bool ipc_send_reply(struct ipc_client *client, enum ipc_command_type payload_type, + const char *payload, uint32_t payload_length); + +static void handle_display_destroy(struct wl_listener *listener, void *data) { + if (ipc_event_source) { + wl_event_source_remove(ipc_event_source); + } + close(ipc_socket); + unlink(ipc_sockaddr->sun_path); + + while (ipc_client_list->length) { + ipc_client_disconnect(ipc_client_list->items[ipc_client_list->length-1]); + } + list_free(ipc_client_list); + + free(ipc_sockaddr); + + wl_list_remove(&ipc_display_destroy.link); +} + +void ipc_init(struct sway_server *server) { + ipc_socket = socket(AF_UNIX, SOCK_STREAM, 0); + if (ipc_socket == -1) { + sway_abort("Unable to create IPC socket"); + } + if (fcntl(ipc_socket, F_SETFD, FD_CLOEXEC) == -1) { + sway_abort("Unable to set CLOEXEC on IPC socket"); + } + if (fcntl(ipc_socket, F_SETFL, O_NONBLOCK) == -1) { + sway_abort("Unable to set NONBLOCK on IPC socket"); + } + + ipc_sockaddr = ipc_user_sockaddr(); + + // We want to use socket name set by user, not existing socket from another sway instance. + if (getenv("SWAYSOCK") != NULL && access(getenv("SWAYSOCK"), F_OK) == -1) { + strncpy(ipc_sockaddr->sun_path, getenv("SWAYSOCK"), sizeof(ipc_sockaddr->sun_path) - 1); + ipc_sockaddr->sun_path[sizeof(ipc_sockaddr->sun_path) - 1] = 0; + } + + unlink(ipc_sockaddr->sun_path); + if (bind(ipc_socket, (struct sockaddr *)ipc_sockaddr, sizeof(*ipc_sockaddr)) == -1) { + sway_abort("Unable to bind IPC socket"); + } + + if (listen(ipc_socket, 3) == -1) { + sway_abort("Unable to listen on IPC socket"); + } + + // Set i3 IPC socket path so that i3-msg works out of the box + setenv("I3SOCK", ipc_sockaddr->sun_path, 1); + setenv("SWAYSOCK", ipc_sockaddr->sun_path, 1); + + ipc_client_list = create_list(); + + ipc_display_destroy.notify = handle_display_destroy; + wl_display_add_destroy_listener(server->wl_display, &ipc_display_destroy); + + ipc_event_source = wl_event_loop_add_fd(server->wl_event_loop, ipc_socket, + WL_EVENT_READABLE, ipc_handle_connection, server); +} + +struct sockaddr_un *ipc_user_sockaddr(void) { + struct sockaddr_un *ipc_sockaddr = malloc(sizeof(struct sockaddr_un)); + if (ipc_sockaddr == NULL) { + sway_abort("Can't allocate ipc_sockaddr"); + } + + ipc_sockaddr->sun_family = AF_UNIX; + int path_size = sizeof(ipc_sockaddr->sun_path); + + // Env var typically set by logind, e.g. "/run/user/" + const char *dir = getenv("XDG_RUNTIME_DIR"); + if (!dir) { + dir = "/tmp"; + } + if (path_size <= snprintf(ipc_sockaddr->sun_path, path_size, + "%s/sway-ipc.%u.%i.sock", dir, getuid(), getpid())) { + sway_abort("Socket path won't fit into ipc_sockaddr->sun_path"); + } + + return ipc_sockaddr; +} + +int ipc_handle_connection(int fd, uint32_t mask, void *data) { + (void) fd; + struct sway_server *server = data; + assert(mask == WL_EVENT_READABLE); + + int client_fd = accept(ipc_socket, NULL, NULL); + if (client_fd == -1) { + sway_log_errno(SWAY_ERROR, "Unable to accept IPC client connection"); + return 0; + } + + int flags; + if ((flags = fcntl(client_fd, F_GETFD)) == -1 + || fcntl(client_fd, F_SETFD, flags|FD_CLOEXEC) == -1) { + sway_log_errno(SWAY_ERROR, "Unable to set CLOEXEC on IPC client socket"); + close(client_fd); + return 0; + } + if ((flags = fcntl(client_fd, F_GETFL)) == -1 + || fcntl(client_fd, F_SETFL, flags|O_NONBLOCK) == -1) { + sway_log_errno(SWAY_ERROR, "Unable to set NONBLOCK on IPC client socket"); + close(client_fd); + return 0; + } + + struct ipc_client *client = malloc(sizeof(struct ipc_client)); + if (!client) { + sway_log(SWAY_ERROR, "Unable to allocate ipc client"); + close(client_fd); + return 0; + } + client->server = server; + client->pending_length = 0; + client->fd = client_fd; + client->subscribed_events = 0; + client->event_source = wl_event_loop_add_fd(server->wl_event_loop, + client_fd, WL_EVENT_READABLE, ipc_client_handle_readable, client); + client->writable_event_source = NULL; + + client->write_buffer_size = 128; + client->write_buffer_len = 0; + client->write_buffer = malloc(client->write_buffer_size); + if (!client->write_buffer) { + sway_log(SWAY_ERROR, "Unable to allocate ipc client write buffer"); + close(client_fd); + return 0; + } + + sway_log(SWAY_DEBUG, "New client: fd %d", client_fd); + list_add(ipc_client_list, client); + return 0; +} + +int ipc_client_handle_readable(int client_fd, uint32_t mask, void *data) { + struct ipc_client *client = data; + + if (mask & WL_EVENT_ERROR) { + sway_log(SWAY_ERROR, "IPC Client socket error, removing client"); + ipc_client_disconnect(client); + return 0; + } + + if (mask & WL_EVENT_HANGUP) { + ipc_client_disconnect(client); + return 0; + } + + int read_available; + if (ioctl(client_fd, FIONREAD, &read_available) == -1) { + sway_log_errno(SWAY_INFO, "Unable to read IPC socket buffer size"); + ipc_client_disconnect(client); + return 0; + } + + // Wait for the rest of the command payload in case the header has already been read + if (client->pending_length > 0) { + if ((uint32_t)read_available >= client->pending_length) { + // Reset pending values. + uint32_t pending_length = client->pending_length; + enum ipc_command_type pending_type = client->pending_type; + client->pending_length = 0; + ipc_client_handle_command(client, pending_length, pending_type); + } + return 0; + } + + if (read_available < (int) IPC_HEADER_SIZE) { + return 0; + } + + uint8_t buf[IPC_HEADER_SIZE]; + // Should be fully available, because read_available >= IPC_HEADER_SIZE + ssize_t received = recv(client_fd, buf, IPC_HEADER_SIZE, 0); + if (received == -1) { + sway_log_errno(SWAY_INFO, "Unable to receive header from IPC client"); + ipc_client_disconnect(client); + return 0; + } + + if (memcmp(buf, ipc_magic, sizeof(ipc_magic)) != 0) { + sway_log(SWAY_DEBUG, "IPC header check failed"); + ipc_client_disconnect(client); + return 0; + } + + memcpy(&client->pending_length, buf + sizeof(ipc_magic), sizeof(uint32_t)); + memcpy(&client->pending_type, buf + sizeof(ipc_magic) + sizeof(uint32_t), sizeof(uint32_t)); + + if (read_available - received >= (long)client->pending_length) { + // Reset pending values. + uint32_t pending_length = client->pending_length; + enum ipc_command_type pending_type = client->pending_type; + client->pending_length = 0; + ipc_client_handle_command(client, pending_length, pending_type); + } + + return 0; +} + +static bool ipc_has_event_listeners(enum ipc_command_type event) { + for (int i = 0; i < ipc_client_list->length; i++) { + struct ipc_client *client = ipc_client_list->items[i]; + if ((client->subscribed_events & event_mask(event)) != 0) { + return true; + } + } + return false; +} + +static void ipc_send_event(const char *json_string, enum ipc_command_type event) { + struct ipc_client *client; + for (int i = 0; i < ipc_client_list->length; i++) { + client = ipc_client_list->items[i]; + if ((client->subscribed_events & event_mask(event)) == 0) { + continue; + } + if (!ipc_send_reply(client, event, json_string, + (uint32_t)strlen(json_string))) { + sway_log_errno(SWAY_INFO, "Unable to send reply to IPC client"); + /* ipc_send_reply destroys client on error, which also + * removes it from the list, so we need to process + * current index again */ + i--; + } + } +} + +void ipc_event_workspace(struct sway_workspace *old, + struct sway_workspace *new, const char *change) { + if (!ipc_has_event_listeners(IPC_EVENT_WORKSPACE)) { + return; + } + sway_log(SWAY_DEBUG, "Sending workspace::%s event", change); + json_object *obj = json_object_new_object(); + json_object_object_add(obj, "change", json_object_new_string(change)); + if (old) { + json_object_object_add(obj, "old", + ipc_json_describe_node_recursive(&old->node)); + } else { + json_object_object_add(obj, "old", NULL); + } + + if (new) { + json_object_object_add(obj, "current", + ipc_json_describe_node_recursive(&new->node)); + } else { + json_object_object_add(obj, "current", NULL); + } + + const char *json_string = json_object_to_json_string(obj); + ipc_send_event(json_string, IPC_EVENT_WORKSPACE); + json_object_put(obj); +} + +void ipc_event_window(struct sway_container *window, const char *change) { + if (!ipc_has_event_listeners(IPC_EVENT_WINDOW)) { + return; + } + sway_log(SWAY_DEBUG, "Sending window::%s event", change); + json_object *obj = json_object_new_object(); + json_object_object_add(obj, "change", json_object_new_string(change)); + json_object_object_add(obj, "container", + ipc_json_describe_node_recursive(&window->node)); + + const char *json_string = json_object_to_json_string(obj); + ipc_send_event(json_string, IPC_EVENT_WINDOW); + json_object_put(obj); +} + +void ipc_event_barconfig_update(struct bar_config *bar) { + if (!ipc_has_event_listeners(IPC_EVENT_BARCONFIG_UPDATE)) { + return; + } + sway_log(SWAY_DEBUG, "Sending barconfig_update event"); + json_object *json = ipc_json_describe_bar_config(bar); + + const char *json_string = json_object_to_json_string(json); + ipc_send_event(json_string, IPC_EVENT_BARCONFIG_UPDATE); + json_object_put(json); +} + +void ipc_event_bar_state_update(struct bar_config *bar) { + if (!ipc_has_event_listeners(IPC_EVENT_BAR_STATE_UPDATE)) { + return; + } + sway_log(SWAY_DEBUG, "Sending bar_state_update event"); + + json_object *json = json_object_new_object(); + json_object_object_add(json, "id", json_object_new_string(bar->id)); + json_object_object_add(json, "visible_by_modifier", + json_object_new_boolean(bar->visible_by_modifier)); + + const char *json_string = json_object_to_json_string(json); + ipc_send_event(json_string, IPC_EVENT_BAR_STATE_UPDATE); + json_object_put(json); +} + +void ipc_event_mode(const char *mode, bool pango) { + if (!ipc_has_event_listeners(IPC_EVENT_MODE)) { + return; + } + sway_log(SWAY_DEBUG, "Sending mode::%s event", mode); + json_object *obj = json_object_new_object(); + json_object_object_add(obj, "change", json_object_new_string(mode)); + json_object_object_add(obj, "pango_markup", + json_object_new_boolean(pango)); + + const char *json_string = json_object_to_json_string(obj); + ipc_send_event(json_string, IPC_EVENT_MODE); + json_object_put(obj); +} + +void ipc_event_shutdown(const char *reason) { + if (!ipc_has_event_listeners(IPC_EVENT_SHUTDOWN)) { + return; + } + sway_log(SWAY_DEBUG, "Sending shutdown::%s event", reason); + + json_object *json = json_object_new_object(); + json_object_object_add(json, "change", json_object_new_string(reason)); + + const char *json_string = json_object_to_json_string(json); + ipc_send_event(json_string, IPC_EVENT_SHUTDOWN); + json_object_put(json); +} + +void ipc_event_binding(struct sway_binding *binding) { + if (!ipc_has_event_listeners(IPC_EVENT_BINDING)) { + return; + } + sway_log(SWAY_DEBUG, "Sending binding event"); + + json_object *json_binding = json_object_new_object(); + json_object_object_add(json_binding, "command", json_object_new_string(binding->command)); + + const char *names[10]; + int len = get_modifier_names(names, binding->modifiers); + json_object *modifiers = json_object_new_array(); + for (int i = 0; i < len; ++i) { + json_object_array_add(modifiers, json_object_new_string(names[i])); + } + json_object_object_add(json_binding, "event_state_mask", modifiers); + + json_object *input_codes = json_object_new_array(); + int input_code = 0; + json_object *symbols = json_object_new_array(); + json_object *symbol = NULL; + + switch (binding->type) { + case BINDING_KEYCODE:; // bindcode: populate input_codes + uint32_t keycode; + for (int i = 0; i < binding->keys->length; ++i) { + keycode = *(uint32_t *)binding->keys->items[i]; + json_object_array_add(input_codes, json_object_new_int(keycode)); + if (i == 0) { + input_code = keycode; + } + } + break; + + case BINDING_KEYSYM: + case BINDING_MOUSESYM: + case BINDING_MOUSECODE:; // bindsym/mouse: populate symbols + uint32_t keysym; + char buffer[64]; + for (int i = 0; i < binding->keys->length; ++i) { + keysym = *(uint32_t *)binding->keys->items[i]; + if (keysym >= BTN_LEFT && keysym <= BTN_LEFT + 8) { + snprintf(buffer, 64, "button%u", keysym - BTN_LEFT + 1); + } else if (xkb_keysym_get_name(keysym, buffer, 64) < 0) { + continue; + } + + json_object *str = json_object_new_string(buffer); + if (i == 0) { + // str is owned by both symbol and symbols. Make sure + // to bump the ref count. + json_object_array_add(symbols, json_object_get(str)); + symbol = str; + } else { + json_object_array_add(symbols, str); + } + } + break; + + default: + sway_log(SWAY_DEBUG, "Unsupported ipc binding event"); + json_object_put(input_codes); + json_object_put(symbols); + json_object_put(json_binding); + return; // do not send any event + } + + json_object_object_add(json_binding, "input_codes", input_codes); + json_object_object_add(json_binding, "input_code", json_object_new_int(input_code)); + json_object_object_add(json_binding, "symbols", symbols); + json_object_object_add(json_binding, "symbol", symbol); + + bool mouse = binding->type == BINDING_MOUSECODE || + binding->type == BINDING_MOUSESYM; + json_object_object_add(json_binding, "input_type", mouse + ? json_object_new_string("mouse") + : json_object_new_string("keyboard")); + + json_object *json = json_object_new_object(); + json_object_object_add(json, "change", json_object_new_string("run")); + json_object_object_add(json, "binding", json_binding); + const char *json_string = json_object_to_json_string(json); + ipc_send_event(json_string, IPC_EVENT_BINDING); + json_object_put(json); +} + +static void ipc_event_tick(const char *payload) { + if (!ipc_has_event_listeners(IPC_EVENT_TICK)) { + return; + } + sway_log(SWAY_DEBUG, "Sending tick event"); + + json_object *json = json_object_new_object(); + json_object_object_add(json, "first", json_object_new_boolean(false)); + json_object_object_add(json, "payload", json_object_new_string(payload)); + + const char *json_string = json_object_to_json_string(json); + ipc_send_event(json_string, IPC_EVENT_TICK); + json_object_put(json); +} + +void ipc_event_input(const char *change, struct sway_input_device *device) { + if (!ipc_has_event_listeners(IPC_EVENT_INPUT)) { + return; + } + sway_log(SWAY_DEBUG, "Sending input event"); + + json_object *json = json_object_new_object(); + json_object_object_add(json, "change", json_object_new_string(change)); + json_object_object_add(json, "input", ipc_json_describe_input(device)); + + const char *json_string = json_object_to_json_string(json); + ipc_send_event(json_string, IPC_EVENT_INPUT); + json_object_put(json); +} + +void ipc_event_output(void) { + if (!ipc_has_event_listeners(IPC_EVENT_OUTPUT)) { + return; + } + sway_log(SWAY_DEBUG, "Sending output event"); + + json_object *json = json_object_new_object(); + json_object_object_add(json, "change", json_object_new_string("unspecified")); + + const char *json_string = json_object_to_json_string(json); + ipc_send_event(json_string, IPC_EVENT_OUTPUT); + json_object_put(json); +} + +int ipc_client_handle_writable(int client_fd, uint32_t mask, void *data) { + struct ipc_client *client = data; + + if (mask & WL_EVENT_ERROR) { + sway_log(SWAY_ERROR, "IPC Client socket error, removing client"); + ipc_client_disconnect(client); + return 0; + } + + if (mask & WL_EVENT_HANGUP) { + ipc_client_disconnect(client); + return 0; + } + + if (client->write_buffer_len <= 0) { + return 0; + } + + ssize_t written = write(client->fd, client->write_buffer, client->write_buffer_len); + + if (written == -1 && errno == EAGAIN) { + return 0; + } else if (written == -1) { + sway_log_errno(SWAY_INFO, "Unable to send data from queue to IPC client"); + ipc_client_disconnect(client); + return 0; + } + + memmove(client->write_buffer, client->write_buffer + written, client->write_buffer_len - written); + client->write_buffer_len -= written; + + if (client->write_buffer_len == 0 && client->writable_event_source) { + wl_event_source_remove(client->writable_event_source); + client->writable_event_source = NULL; + } + + return 0; +} + +void ipc_client_disconnect(struct ipc_client *client) { + if (!sway_assert(client != NULL, "client != NULL")) { + return; + } + + shutdown(client->fd, SHUT_RDWR); + + sway_log(SWAY_INFO, "IPC Client %d disconnected", client->fd); + wl_event_source_remove(client->event_source); + if (client->writable_event_source) { + wl_event_source_remove(client->writable_event_source); + } + int i = 0; + while (i < ipc_client_list->length && ipc_client_list->items[i] != client) { + i++; + } + list_del(ipc_client_list, i); + free(client->write_buffer); + close(client->fd); + free(client); +} + +static void ipc_get_workspaces_callback(struct sway_workspace *workspace, + void *data) { + json_object *workspace_json = ipc_json_describe_node(&workspace->node); + // override the default focused indicator because + // it's set differently for the get_workspaces reply + struct sway_seat *seat = input_manager_get_default_seat(); + struct sway_workspace *focused_ws = seat_get_focused_workspace(seat); + bool focused = workspace == focused_ws; + json_object_object_del(workspace_json, "focused"); + json_object_object_add(workspace_json, "focused", + json_object_new_boolean(focused)); + json_object_array_add((json_object *)data, workspace_json); + + focused_ws = output_get_active_workspace(workspace->output); + bool visible = workspace == focused_ws; + json_object_object_add(workspace_json, "visible", + json_object_new_boolean(visible)); +} + +static void ipc_get_marks_callback(struct sway_container *con, void *data) { + json_object *marks = (json_object *)data; + for (int i = 0; i < con->marks->length; ++i) { + char *mark = (char *)con->marks->items[i]; + json_object_array_add(marks, json_object_new_string(mark)); + } +} + +void ipc_client_handle_command(struct ipc_client *client, uint32_t payload_length, + enum ipc_command_type payload_type) { + if (!sway_assert(client != NULL, "client != NULL")) { + return; + } + + char *buf = malloc(payload_length + 1); + if (!buf) { + sway_log_errno(SWAY_INFO, "Unable to allocate IPC payload"); + ipc_client_disconnect(client); + return; + } + if (payload_length > 0) { + // Payload should be fully available + ssize_t received = recv(client->fd, buf, payload_length, 0); + if (received == -1) + { + sway_log_errno(SWAY_INFO, "Unable to receive payload from IPC client"); + ipc_client_disconnect(client); + free(buf); + return; + } + } + buf[payload_length] = '\0'; + + switch (payload_type) { + case IPC_COMMAND: + { + char *line = strtok(buf, "\n"); + while (line) { + size_t line_length = strlen(line); + if (line + line_length >= buf + payload_length) { + break; + } + line[line_length] = ';'; + line = strtok(NULL, "\n"); + } + + list_t *res_list = execute_command(buf, NULL, NULL); + if (modeset_is_pending()) { + // IPC expects commands to have taken immediate effect, so we need + // to force a modeset after output commands. We do a single modeset + // here to avoid modesetting for every output command in sequence. + force_modeset(); + } + transaction_commit_dirty(); + char *json = cmd_results_to_json(res_list); + int length = strlen(json); + ipc_send_reply(client, payload_type, json, (uint32_t)length); + free(json); + while (res_list->length) { + struct cmd_results *results = res_list->items[0]; + free_cmd_results(results); + list_del(res_list, 0); + } + list_free(res_list); + goto exit_cleanup; + } + + case IPC_SEND_TICK: + { + ipc_event_tick(buf); + ipc_send_reply(client, payload_type, "{\"success\": true}", 17); + goto exit_cleanup; + } + + case IPC_GET_OUTPUTS: + { + json_object *outputs = json_object_new_array(); + for (int i = 0; i < root->outputs->length; ++i) { + struct sway_output *output = root->outputs->items[i]; + json_object *output_json = ipc_json_describe_node(&output->node); + + // override the default focused indicator because it's set + // differently for the get_outputs reply + struct sway_seat *seat = input_manager_get_default_seat(); + struct sway_workspace *focused_ws = + seat_get_focused_workspace(seat); + bool focused = focused_ws && output == focused_ws->output; + json_object_object_del(output_json, "focused"); + json_object_object_add(output_json, "focused", + json_object_new_boolean(focused)); + + const char *subpixel = sway_wl_output_subpixel_to_string(output->wlr_output->subpixel); + json_object_object_add(output_json, "subpixel_hinting", json_object_new_string(subpixel)); + json_object_array_add(outputs, output_json); + } + struct sway_output *output; + wl_list_for_each(output, &root->all_outputs, link) { + if (!output->enabled && output != root->fallback_output) { + json_object_array_add(outputs, + ipc_json_describe_disabled_output(output)); + } + } + + for (int i = 0; i < root->non_desktop_outputs->length; i++) { + struct sway_output_non_desktop *non_desktop_output = root->non_desktop_outputs->items[i]; + json_object_array_add(outputs, ipc_json_describe_non_desktop_output(non_desktop_output)); + } + + const char *json_string = json_object_to_json_string(outputs); + ipc_send_reply(client, payload_type, json_string, + (uint32_t)strlen(json_string)); + json_object_put(outputs); // free + goto exit_cleanup; + } + + case IPC_GET_WORKSPACES: + { + json_object *workspaces = json_object_new_array(); + root_for_each_workspace(ipc_get_workspaces_callback, workspaces); + const char *json_string = json_object_to_json_string(workspaces); + ipc_send_reply(client, payload_type, json_string, + (uint32_t)strlen(json_string)); + json_object_put(workspaces); // free + goto exit_cleanup; + } + + case IPC_SUBSCRIBE: + { + // TODO: Check if they're permitted to use these events + struct json_object *request = json_tokener_parse(buf); + if (request == NULL || !json_object_is_type(request, json_type_array)) { + const char msg[] = "{\"success\": false}"; + ipc_send_reply(client, payload_type, msg, strlen(msg)); + sway_log(SWAY_INFO, "Failed to parse subscribe request"); + goto exit_cleanup; + } + + bool is_tick = false; + // parse requested event types + for (size_t i = 0; i < json_object_array_length(request); i++) { + const char *event_type = json_object_get_string(json_object_array_get_idx(request, i)); + if (strcmp(event_type, "workspace") == 0) { + client->subscribed_events |= event_mask(IPC_EVENT_WORKSPACE); + } else if (strcmp(event_type, "output") == 0) { + client->subscribed_events |= event_mask(IPC_EVENT_OUTPUT); + } else if (strcmp(event_type, "barconfig_update") == 0) { + client->subscribed_events |= event_mask(IPC_EVENT_BARCONFIG_UPDATE); + } else if (strcmp(event_type, "bar_state_update") == 0) { + client->subscribed_events |= event_mask(IPC_EVENT_BAR_STATE_UPDATE); + } else if (strcmp(event_type, "mode") == 0) { + client->subscribed_events |= event_mask(IPC_EVENT_MODE); + } else if (strcmp(event_type, "shutdown") == 0) { + client->subscribed_events |= event_mask(IPC_EVENT_SHUTDOWN); + } else if (strcmp(event_type, "window") == 0) { + client->subscribed_events |= event_mask(IPC_EVENT_WINDOW); + } else if (strcmp(event_type, "binding") == 0) { + client->subscribed_events |= event_mask(IPC_EVENT_BINDING); + } else if (strcmp(event_type, "tick") == 0) { + client->subscribed_events |= event_mask(IPC_EVENT_TICK); + is_tick = true; + } else if (strcmp(event_type, "input") == 0) { + client->subscribed_events |= event_mask(IPC_EVENT_INPUT); + } else { + const char msg[] = "{\"success\": false}"; + ipc_send_reply(client, payload_type, msg, strlen(msg)); + json_object_put(request); + sway_log(SWAY_INFO, "Unsupported event type in subscribe request"); + goto exit_cleanup; + } + } + + json_object_put(request); + const char msg[] = "{\"success\": true}"; + ipc_send_reply(client, payload_type, msg, strlen(msg)); + if (is_tick) { + const char tickmsg[] = "{\"first\": true, \"payload\": \"\"}"; + ipc_send_reply(client, IPC_EVENT_TICK, tickmsg, + strlen(tickmsg)); + } + goto exit_cleanup; + } + + case IPC_GET_INPUTS: + { + json_object *inputs = json_object_new_array(); + struct sway_input_device *device = NULL; + wl_list_for_each(device, &server.input->devices, link) { + json_object_array_add(inputs, ipc_json_describe_input(device)); + } + const char *json_string = json_object_to_json_string(inputs); + ipc_send_reply(client, payload_type, json_string, + (uint32_t)strlen(json_string)); + json_object_put(inputs); // free + goto exit_cleanup; + } + + case IPC_GET_SEATS: + { + json_object *seats = json_object_new_array(); + struct sway_seat *seat = NULL; + wl_list_for_each(seat, &server.input->seats, link) { + json_object_array_add(seats, ipc_json_describe_seat(seat)); + } + const char *json_string = json_object_to_json_string(seats); + ipc_send_reply(client, payload_type, json_string, + (uint32_t)strlen(json_string)); + json_object_put(seats); // free + goto exit_cleanup; + } + + case IPC_GET_TREE: + { + json_object *tree = ipc_json_describe_node_recursive(&root->node); + const char *json_string = json_object_to_json_string(tree); + ipc_send_reply(client, payload_type, json_string, + (uint32_t)strlen(json_string)); + json_object_put(tree); + goto exit_cleanup; + } + + case IPC_GET_MARKS: + { + json_object *marks = json_object_new_array(); + root_for_each_container(ipc_get_marks_callback, marks); + const char *json_string = json_object_to_json_string(marks); + ipc_send_reply(client, payload_type, json_string, + (uint32_t)strlen(json_string)); + json_object_put(marks); + goto exit_cleanup; + } + + case IPC_GET_VERSION: + { + json_object *version = ipc_json_get_version(); + const char *json_string = json_object_to_json_string(version); + ipc_send_reply(client, payload_type, json_string, + (uint32_t)strlen(json_string)); + json_object_put(version); // free + goto exit_cleanup; + } + + case IPC_GET_BAR_CONFIG: + { + if (!buf[0]) { + // Send list of configured bar IDs + json_object *bars = json_object_new_array(); + for (int i = 0; i < config->bars->length; ++i) { + struct bar_config *bar = config->bars->items[i]; + json_object_array_add(bars, json_object_new_string(bar->id)); + } + const char *json_string = json_object_to_json_string(bars); + ipc_send_reply(client, payload_type, json_string, + (uint32_t)strlen(json_string)); + json_object_put(bars); // free + } else { + // Send particular bar's details + struct bar_config *bar = NULL; + for (int i = 0; i < config->bars->length; ++i) { + bar = config->bars->items[i]; + if (strcmp(buf, bar->id) == 0) { + break; + } + bar = NULL; + } + if (!bar) { + const char *error = "{ \"success\": false, \"error\": \"No bar with that ID\" }"; + ipc_send_reply(client, payload_type, error, + (uint32_t)strlen(error)); + goto exit_cleanup; + } + json_object *json = ipc_json_describe_bar_config(bar); + const char *json_string = json_object_to_json_string(json); + ipc_send_reply(client, payload_type, json_string, + (uint32_t)strlen(json_string)); + json_object_put(json); // free + } + goto exit_cleanup; + } + + case IPC_GET_BINDING_MODES: + { + json_object *modes = json_object_new_array(); + for (int i = 0; i < config->modes->length; i++) { + struct sway_mode *mode = config->modes->items[i]; + json_object_array_add(modes, json_object_new_string(mode->name)); + } + const char *json_string = json_object_to_json_string(modes); + ipc_send_reply(client, payload_type, json_string, + (uint32_t)strlen(json_string)); + json_object_put(modes); // free + goto exit_cleanup; + } + + case IPC_GET_BINDING_STATE: + { + json_object *current_mode = ipc_json_get_binding_mode(); + const char *json_string = json_object_to_json_string(current_mode); + ipc_send_reply(client, payload_type, json_string, + (uint32_t)strlen(json_string)); + json_object_put(current_mode); // free + goto exit_cleanup; + } + + case IPC_GET_CONFIG: + { + json_object *json = json_object_new_object(); + json_object_object_add(json, "config", json_object_new_string(config->current_config)); + const char *json_string = json_object_to_json_string(json); + ipc_send_reply(client, payload_type, json_string, + (uint32_t)strlen(json_string)); + json_object_put(json); // free + goto exit_cleanup; + } + + case IPC_SYNC: + { + // It was decided sway will not support this, just return success:false + const char msg[] = "{\"success\": false}"; + ipc_send_reply(client, payload_type, msg, strlen(msg)); + goto exit_cleanup; + } + + default: + sway_log(SWAY_INFO, "Unknown IPC command type %x", payload_type); + goto exit_cleanup; + } + +exit_cleanup: + free(buf); +} + +bool ipc_send_reply(struct ipc_client *client, enum ipc_command_type payload_type, + const char *payload, uint32_t payload_length) { + assert(payload); + + char data[IPC_HEADER_SIZE]; + + memcpy(data, ipc_magic, sizeof(ipc_magic)); + memcpy(data + sizeof(ipc_magic), &payload_length, sizeof(payload_length)); + memcpy(data + sizeof(ipc_magic) + sizeof(payload_length), &payload_type, sizeof(payload_type)); + + while (client->write_buffer_len + IPC_HEADER_SIZE + payload_length >= + client->write_buffer_size) { + client->write_buffer_size *= 2; + } + + if (client->write_buffer_size > 4e6) { // 4 MB + sway_log(SWAY_ERROR, "Client write buffer too big (%zu), disconnecting client", + client->write_buffer_size); + ipc_client_disconnect(client); + return false; + } + + char *new_buffer = realloc(client->write_buffer, client->write_buffer_size); + if (!new_buffer) { + sway_log(SWAY_ERROR, "Unable to reallocate ipc client write buffer"); + ipc_client_disconnect(client); + return false; + } + client->write_buffer = new_buffer; + + memcpy(client->write_buffer + client->write_buffer_len, data, IPC_HEADER_SIZE); + client->write_buffer_len += IPC_HEADER_SIZE; + memcpy(client->write_buffer + client->write_buffer_len, payload, payload_length); + client->write_buffer_len += payload_length; + + if (!client->writable_event_source) { + client->writable_event_source = wl_event_loop_add_fd( + server.wl_event_loop, client->fd, WL_EVENT_WRITABLE, + ipc_client_handle_writable, client); + } + + return true; +} diff --git a/sway/layer_criteria.c b/sway/layer_criteria.c new file mode 100644 index 0000000..5652169 --- /dev/null +++ b/sway/layer_criteria.c @@ -0,0 +1,155 @@ +#include +#include +#include "list.h" +#include "log.h" +#include "stringop.h" +#include "sway/commands.h" +#include "sway/layer_criteria.h" +#include "util.h" + +static void init_criteria_effects(struct layer_criteria *criteria) { + criteria->corner_radius = 0; + criteria->blur_enabled = false; + criteria->blur_xray = false; + criteria->blur_ignore_transparent = false; + criteria->shadow_enabled = false; +} + +static void copy_criteria_effects(struct layer_criteria *dst, struct layer_criteria *src) { + dst->corner_radius = src->corner_radius; + dst->blur_enabled = src->blur_enabled; + dst->blur_xray = src->blur_xray; + dst->blur_ignore_transparent = src->blur_ignore_transparent; + dst->shadow_enabled = src->shadow_enabled; +} + +static bool layer_criteria_find(char *namespace, + struct layer_criteria **criteria, int *index) { + *index = -1; + *criteria = NULL; + + list_t *criterias = config->layer_criteria; + for (int i = 0; i < criterias->length; ++i) { + struct layer_criteria *existing = criterias->items[i]; + if (strcmp(namespace, existing->namespace) == 0) { + *index = i; + *criteria = existing; + return true; + } + } + return false; +} + +static bool layer_criteria_parse(struct layer_criteria *criteria) { + char matched_delim = ';'; + char *head = malloc(strlen(criteria->cmdlist) + 1); + strcpy(head, criteria->cmdlist); + do { + // Trim leading whitespaces + for (; isspace(*head); ++head) {} + // Split command list + char *cmd = argsep(&head, ";,", &matched_delim); + for (; isspace(*cmd); ++cmd) {} + + if (strcmp(cmd, "") == 0) { + sway_log(SWAY_INFO, "Ignoring empty layer effect."); + continue; + } + sway_log(SWAY_INFO, "Handling layer effect '%s'", cmd); + + int argc; + char **argv = split_args(cmd, &argc); + // Strip all quotes from each token + for (int i = 1; i < argc; ++i) { + if (*argv[i] == '\"' || *argv[i] == '\'') { + strip_quotes(argv[i]); + } + } + if (strcmp(argv[0], "reset") == 0) { + // Reset all args + init_criteria_effects(criteria); + return true; + } else if (strcmp(argv[0], "blur") == 0) { + criteria->blur_enabled = parse_boolean(argv[1], true); + continue; + } if (strcmp(argv[0], "blur_xray") == 0) { + criteria->blur_xray = parse_boolean(argv[1], true); + continue; + } if (strcmp(argv[0], "blur_ignore_transparent") == 0) { + criteria->blur_ignore_transparent = parse_boolean(argv[1], true); + continue; + } else if (strcmp(argv[0], "shadows") == 0) { + criteria->shadow_enabled = parse_boolean(argv[1], true); + continue; + } else if (strcmp(argv[0], "corner_radius") == 0) { + int value; + if (cmd_corner_radius_parse_value(argv[1], &value)) { + criteria->corner_radius = value; + continue; + } + sway_log(SWAY_ERROR, + "Invalid layer_effects corner_radius size! Got \"%s\"", + argv[1]); + return false; + } else { + sway_log(SWAY_ERROR, "Invalid layer_effects effect! Got \"%s\"", cmd); + return false; + } + } while(head); + + return true; +} + +struct layer_criteria *layer_criteria_add(char *namespace, char *cmdlist) { + struct layer_criteria *criteria = calloc(1, sizeof(*criteria)); + init_criteria_effects(criteria); + + // Check if the rule already exists + int existing_index; + struct layer_criteria *existing = NULL; + if (layer_criteria_find(namespace, &existing, &existing_index)) { + sway_log(SWAY_DEBUG, "layer_effect '%s' already exists: '%s'. Replacing with: '%s'", + namespace, existing->cmdlist, cmdlist); + // Clone the effects into the new criteria + copy_criteria_effects(criteria, existing); + layer_criteria_destroy(existing); + list_del(config->layer_criteria, existing_index); + } + + // Create a new criteria + if (!criteria) { + sway_log(SWAY_ERROR, "Could not allocate new layer_criteria"); + return NULL; + } + criteria->namespace = malloc(strlen(namespace) + 1); + strcpy(criteria->namespace, namespace); + criteria->cmdlist = cmdlist; + + // Parsing + if (!layer_criteria_parse(criteria)) { + // Failed to parse + layer_criteria_destroy(criteria); + return NULL; + } + + list_add(config->layer_criteria, criteria); + + return criteria; +} + +struct layer_criteria *layer_criteria_for_namespace(char *namespace) { + list_t *criterias = config->layer_criteria; + for (int i = criterias->length - 1; i >= 0; i--) { + struct layer_criteria *criteria = criterias->items[i]; + if (strcmp(criteria->namespace, namespace) == 0) { + return criteria; + } + } + return NULL; +} + +void layer_criteria_destroy(struct layer_criteria *criteria) { + free(criteria->namespace); + free(criteria->cmdlist); + free(criteria); +} diff --git a/sway/lock.c b/sway/lock.c new file mode 100644 index 0000000..e70b257 --- /dev/null +++ b/sway/lock.c @@ -0,0 +1,361 @@ +#include +#include +#include +#include "log.h" +#include "sway/input/cursor.h" +#include "sway/input/keyboard.h" +#include "sway/input/seat.h" +#include "sway/layers.h" +#include "sway/output.h" +#include "sway/server.h" +#include "sway/lock.h" + +struct sway_session_lock_output { + struct wlr_scene_tree *tree; + struct wlr_scene_rect *background; + struct sway_session_lock *lock; + + struct sway_output *output; + + struct wl_list link; // sway_session_lock::outputs + + struct wl_listener destroy; + + struct wlr_session_lock_surface_v1 *surface; + + // invalid if surface is NULL + struct wl_listener surface_destroy; + struct wl_listener surface_map; +}; + +static void focus_surface(struct sway_session_lock *lock, + struct wlr_surface *focused) { + lock->focused = focused; + + struct sway_seat *seat; + wl_list_for_each(seat, &server.input->seats, link) { + seat_set_focus_surface(seat, focused, false); + } +} + +static void refocus_output(struct sway_session_lock_output *output) { + // Move the seat focus to another surface if one is available + if (output->lock->focused == output->surface->surface) { + struct wlr_surface *next_focus = NULL; + + struct sway_session_lock_output *candidate; + wl_list_for_each(candidate, &output->lock->outputs, link) { + if (candidate == output || !candidate->surface) { + continue; + } + + if (candidate->surface->surface->mapped) { + next_focus = candidate->surface->surface; + break; + } + } + + focus_surface(output->lock, next_focus); + } +} + +static void handle_surface_map(struct wl_listener *listener, void *data) { + struct sway_session_lock_output *surf = wl_container_of(listener, surf, surface_map); + if (surf->lock->focused == NULL) { + focus_surface(surf->lock, surf->surface->surface); + } + cursor_rebase_all(); +} + +static void handle_surface_destroy(struct wl_listener *listener, void *data) { + struct sway_session_lock_output *output = + wl_container_of(listener, output, surface_destroy); + refocus_output(output); + + sway_assert(output->surface, "Trying to destroy a surface that the lock doesn't think exists"); + output->surface = NULL; + wl_list_remove(&output->surface_destroy.link); + wl_list_remove(&output->surface_map.link); +} + +static void lock_output_reconfigure(struct sway_session_lock_output *output) { + int width = output->output->width; + int height = output->output->height; + + wlr_scene_rect_set_size(output->background, width, height); + + if (output->surface) { + wlr_session_lock_surface_v1_configure(output->surface, width, height); + } +} + +void arrange_locks(void) { + if (server.session_lock.lock == NULL) { + return; + } + + struct sway_session_lock_output *lock_output; + wl_list_for_each(lock_output, &server.session_lock.lock->outputs, link) { + lock_output_reconfigure(lock_output); + } +} + +static void handle_new_surface(struct wl_listener *listener, void *data) { + struct sway_session_lock *lock = wl_container_of(listener, lock, new_surface); + struct wlr_session_lock_surface_v1 *lock_surface = data; + struct sway_output *output = lock_surface->output->data; + + sway_log(SWAY_DEBUG, "new lock layer surface"); + + struct sway_session_lock_output *current_lock_output, *lock_output = NULL; + wl_list_for_each(current_lock_output, &lock->outputs, link) { + if (current_lock_output->output == output) { + lock_output = current_lock_output; + break; + } + } + sway_assert(lock_output, "Couldn't find output to lock"); + sway_assert(!lock_output->surface, "Tried to reassign a surface to an existing output"); + + lock_output->surface = lock_surface; + + wlr_scene_subsurface_tree_create(lock_output->tree, lock_surface->surface); + + lock_output->surface_destroy.notify = handle_surface_destroy; + wl_signal_add(&lock_surface->events.destroy, &lock_output->surface_destroy); + lock_output->surface_map.notify = handle_surface_map; + wl_signal_add(&lock_surface->surface->events.map, &lock_output->surface_map); + + lock_output_reconfigure(lock_output); +} + +static void sway_session_lock_output_destroy(struct sway_session_lock_output *output) { + if (output->surface) { + refocus_output(output); + wl_list_remove(&output->surface_destroy.link); + wl_list_remove(&output->surface_map.link); + } + + wl_list_remove(&output->destroy.link); + wl_list_remove(&output->link); + + free(output); +} + +static void lock_node_handle_destroy(struct wl_listener *listener, void *data) { + struct sway_session_lock_output *output = + wl_container_of(listener, output, destroy); + sway_session_lock_output_destroy(output); +} + +static struct sway_session_lock_output *session_lock_output_create( + struct sway_session_lock *lock, struct sway_output *output) { + struct sway_session_lock_output *lock_output = calloc(1, sizeof(*lock_output)); + if (!lock_output) { + sway_log(SWAY_ERROR, "failed to allocate a session lock output"); + return NULL; + } + + struct wlr_scene_tree *tree = wlr_scene_tree_create(output->layers.session_lock); + if (!tree) { + sway_log(SWAY_ERROR, "failed to allocate a session lock output scene tree"); + free(lock_output); + return NULL; + } + + struct wlr_scene_rect *background = wlr_scene_rect_create(tree, 0, 0, (float[4]){ + lock->abandoned ? 1.f : 0.f, + 0.f, + 0.f, + 1.f, + }); + if (!background) { + sway_log(SWAY_ERROR, "failed to allocate a session lock output scene background"); + wlr_scene_node_destroy(&tree->node); + free(lock_output); + return NULL; + } + + lock_output->output = output; + lock_output->tree = tree; + lock_output->background = background; + lock_output->lock = lock; + + lock_output->destroy.notify = lock_node_handle_destroy; + wl_signal_add(&tree->node.events.destroy, &lock_output->destroy); + + lock_output_reconfigure(lock_output); + + wl_list_insert(&lock->outputs, &lock_output->link); + + return lock_output; +} + +static void sway_session_lock_destroy(struct sway_session_lock* lock) { + struct sway_session_lock_output *lock_output, *tmp_lock_output; + wl_list_for_each_safe(lock_output, tmp_lock_output, &lock->outputs, link) { + // destroying the node will also destroy the whole lock output + wlr_scene_node_destroy(&lock_output->tree->node); + } + + if (server.session_lock.lock == lock) { + server.session_lock.lock = NULL; + } + + if (!lock->abandoned) { + wl_list_remove(&lock->destroy.link); + wl_list_remove(&lock->unlock.link); + wl_list_remove(&lock->new_surface.link); + } + + free(lock); +} + +static void handle_unlock(struct wl_listener *listener, void *data) { + struct sway_session_lock *lock = wl_container_of(listener, lock, unlock); + sway_log(SWAY_DEBUG, "session unlocked"); + + sway_session_lock_destroy(lock); + + struct sway_seat *seat; + wl_list_for_each(seat, &server.input->seats, link) { + // copied from seat_set_focus_layer -- deduplicate? + struct sway_node *previous = seat_get_focus_inactive(seat, &root->node); + if (previous) { + // Hack to get seat to re-focus the return value of get_focus + seat_set_focus(seat, NULL); + seat_set_focus(seat, previous); + } + } + + // Triggers a refocus of the topmost surface layer if necessary + // TODO: Make layer surface focus per-output based on cursor position + for (int i = 0; i < root->outputs->length; ++i) { + struct sway_output *output = root->outputs->items[i]; + arrange_layers(output); + } + + // Views are now visible, so check if we need to activate inhibition again. + sway_idle_inhibit_v1_check_active(); +} + +static void handle_abandon(struct wl_listener *listener, void *data) { + struct sway_session_lock *lock = wl_container_of(listener, lock, destroy); + sway_log(SWAY_INFO, "session lock abandoned"); + + struct sway_session_lock_output *lock_output; + wl_list_for_each(lock_output, &lock->outputs, link) { + wlr_scene_rect_set_color(lock_output->background, + (float[4]){ 1.f, 0.f, 0.f, 1.f }); + } + + lock->abandoned = true; + wl_list_remove(&lock->destroy.link); + wl_list_remove(&lock->unlock.link); + wl_list_remove(&lock->new_surface.link); +} + +static void handle_session_lock(struct wl_listener *listener, void *data) { + struct wlr_session_lock_v1 *lock = data; + struct wl_client *client = wl_resource_get_client(lock->resource); + + if (server.session_lock.lock) { + if (server.session_lock.lock->abandoned) { + sway_log(SWAY_INFO, "Replacing abandoned lock"); + sway_session_lock_destroy(server.session_lock.lock); + } else { + sway_log(SWAY_ERROR, "Cannot lock an already locked session"); + wlr_session_lock_v1_destroy(lock); + return; + } + } + + struct sway_session_lock *sway_lock = calloc(1, sizeof(*sway_lock)); + if (!sway_lock) { + sway_log(SWAY_ERROR, "failed to allocate a session lock object"); + wlr_session_lock_v1_destroy(lock); + return; + } + + wl_list_init(&sway_lock->outputs); + + sway_log(SWAY_DEBUG, "session locked"); + + struct sway_seat *seat; + wl_list_for_each(seat, &server.input->seats, link) { + seat_unfocus_unless_client(seat, client); + } + + struct sway_output *output; + wl_list_for_each(output, &root->all_outputs, link) { + sway_session_lock_add_output(sway_lock, output); + } + + sway_lock->new_surface.notify = handle_new_surface; + wl_signal_add(&lock->events.new_surface, &sway_lock->new_surface); + sway_lock->unlock.notify = handle_unlock; + wl_signal_add(&lock->events.unlock, &sway_lock->unlock); + sway_lock->destroy.notify = handle_abandon; + wl_signal_add(&lock->events.destroy, &sway_lock->destroy); + + wlr_session_lock_v1_send_locked(lock); + server.session_lock.lock = sway_lock; + + // The lock screen covers everything, so check if any active inhibition got + // deactivated due to lost visibility. + sway_idle_inhibit_v1_check_active(); +} + +static void handle_session_lock_destroy(struct wl_listener *listener, void *data) { + // if the server shuts down while a lock is active, destroy the lock + if (server.session_lock.lock) { + sway_session_lock_destroy(server.session_lock.lock); + } + + wl_list_remove(&server.session_lock.new_lock.link); + wl_list_remove(&server.session_lock.manager_destroy.link); + + server.session_lock.manager = NULL; +} + +void sway_session_lock_add_output(struct sway_session_lock *lock, + struct sway_output *output) { + struct sway_session_lock_output *lock_output = + session_lock_output_create(lock, output); + + // if we run out of memory while trying to lock the screen, the best we + // can do is kill the sway process. Security conscious users will have + // the sway session fall back to a login shell. + if (!lock_output) { + sway_log(SWAY_ERROR, "aborting: failed to allocate a lock output"); + abort(); + } +} + +bool sway_session_lock_has_surface(struct sway_session_lock *lock, + struct wlr_surface *surface) { + struct sway_session_lock_output *lock_output; + wl_list_for_each(lock_output, &lock->outputs, link) { + if (lock_output->surface && lock_output->surface->surface == surface) { + return true; + } + } + + return false; +} + +bool sway_session_lock_init(void) { + server.session_lock.manager = wlr_session_lock_manager_v1_create(server.wl_display); + if (!server.session_lock.manager) { + sway_log(SWAY_ERROR, "Failed to create session lock manager"); + return false; + } + + server.session_lock.new_lock.notify = handle_session_lock; + server.session_lock.manager_destroy.notify = handle_session_lock_destroy; + wl_signal_add(&server.session_lock.manager->events.new_lock, + &server.session_lock.new_lock); + wl_signal_add(&server.session_lock.manager->events.destroy, + &server.session_lock.manager_destroy); + return true; +} diff --git a/sway/main.c b/sway/main.c new file mode 100644 index 0000000..ab1d05e --- /dev/null +++ b/sway/main.c @@ -0,0 +1,417 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "sway/commands.h" +#include "sway/config.h" +#include "sway/server.h" +#include "sway/swaynag.h" +#include "sway/desktop/transaction.h" +#include "sway/tree/root.h" +#include "sway/ipc-server.h" +#include "ipc-client.h" +#include "log.h" +#include "stringop.h" +#include "util.h" + +static bool terminate_request = false; +static int exit_value = 0; +static struct rlimit original_nofile_rlimit = {0}; +struct sway_server server = {0}; +struct sway_debug debug = {0}; + +void sway_terminate(int exit_code) { + if (!server.wl_display) { + // Running as IPC client + exit(exit_code); + } else { + // Running as server + terminate_request = true; + exit_value = exit_code; + ipc_event_shutdown("exit"); + wl_display_terminate(server.wl_display); + } +} + +void run_as_ipc_client(char *command, char *socket_path) { + int socketfd = ipc_open_socket(socket_path); + uint32_t len = strlen(command); + char *resp = ipc_single_command(socketfd, IPC_COMMAND, command, &len); + printf("%s\n", resp); + free(resp); + close(socketfd); +} + +static void log_env(void) { + const char *log_vars[] = { + "LD_LIBRARY_PATH", + "LD_PRELOAD", + "PATH", + "SWAYSOCK", + }; + for (size_t i = 0; i < sizeof(log_vars) / sizeof(char *); ++i) { + char *value = getenv(log_vars[i]); + sway_log(SWAY_INFO, "%s=%s", log_vars[i], value != NULL ? value : ""); + } +} + +static void log_file(FILE *f) { + char *line = NULL; + size_t line_size = 0; + ssize_t nread; + while ((nread = getline(&line, &line_size, f)) != -1) { + if (line[nread - 1] == '\n') { + line[nread - 1] = '\0'; + } + sway_log(SWAY_INFO, "%s", line); + } + free(line); +} + +static void log_distro(void) { + const char *paths[] = { + "/etc/lsb-release", + "/etc/os-release", + "/etc/debian_version", + "/etc/redhat-release", + "/etc/gentoo-release", + }; + for (size_t i = 0; i < sizeof(paths) / sizeof(char *); ++i) { + FILE *f = fopen(paths[i], "r"); + if (f) { + sway_log(SWAY_INFO, "Contents of %s:", paths[i]); + log_file(f); + fclose(f); + } + } +} + +static void log_kernel(void) { + FILE *f = popen("uname -a", "r"); + if (!f) { + sway_log(SWAY_INFO, "Unable to determine kernel version"); + return; + } + log_file(f); + pclose(f); +} + +static void restore_nofile_limit(void) { + if (original_nofile_rlimit.rlim_cur == 0) { + return; + } + if (setrlimit(RLIMIT_NOFILE, &original_nofile_rlimit) != 0) { + sway_log_errno(SWAY_ERROR, "Failed to restore max open files limit: " + "setrlimit(NOFILE) failed"); + } +} + +static void increase_nofile_limit(void) { + if (getrlimit(RLIMIT_NOFILE, &original_nofile_rlimit) != 0) { + sway_log_errno(SWAY_ERROR, "Failed to bump max open files limit: " + "getrlimit(NOFILE) failed"); + return; + } + + struct rlimit new_rlimit = original_nofile_rlimit; + new_rlimit.rlim_cur = new_rlimit.rlim_max; + if (setrlimit(RLIMIT_NOFILE, &new_rlimit) != 0) { + sway_log_errno(SWAY_ERROR, "Failed to bump max open files limit: " + "setrlimit(NOFILE) failed"); + sway_log(SWAY_INFO, "Running with %d max open files", + (int)original_nofile_rlimit.rlim_cur); + return; + } + + pthread_atfork(NULL, NULL, restore_nofile_limit); +} + +static int term_signal(int signal, void *data) { + sway_terminate(EXIT_SUCCESS); + return 0; +} + +static void restore_signals(void) { + sigset_t set; + sigemptyset(&set); + sigprocmask(SIG_SETMASK, &set, NULL); + + struct sigaction sa_dfl = { .sa_handler = SIG_DFL }; + sigaction(SIGCHLD, &sa_dfl, NULL); + sigaction(SIGPIPE, &sa_dfl, NULL); +} + +static void init_signals(void) { + wl_event_loop_add_signal(server.wl_event_loop, SIGTERM, term_signal, NULL); + wl_event_loop_add_signal(server.wl_event_loop, SIGINT, term_signal, NULL); + + struct sigaction sa_ign = { .sa_handler = SIG_IGN }; + // avoid need to reap children + sigaction(SIGCHLD, &sa_ign, NULL); + // prevent ipc write errors from crashing sway + sigaction(SIGPIPE, &sa_ign, NULL); + + pthread_atfork(NULL, NULL, restore_signals); +} + +void enable_debug_flag(const char *flag) { + if (strcmp(flag, "noatomic") == 0) { + debug.noatomic = true; + } else if (strcmp(flag, "txn-wait") == 0) { + debug.txn_wait = true; + } else if (strcmp(flag, "txn-timings") == 0) { + debug.txn_timings = true; + } else if (has_prefix(flag, "txn-timeout=")) { + server.txn_timeout_ms = atoi(&flag[strlen("txn-timeout=")]); + } else { + sway_log(SWAY_ERROR, "Unknown debug flag: %s", flag); + } +} + +static sway_log_importance_t convert_wlr_log_importance( + enum wlr_log_importance importance) { + switch (importance) { + case WLR_ERROR: + return SWAY_ERROR; + case WLR_INFO: + return SWAY_INFO; + default: + return SWAY_DEBUG; + } +} + +static void handle_wlr_log(enum wlr_log_importance importance, + const char *fmt, va_list args) { + static char sway_fmt[1024]; + snprintf(sway_fmt, sizeof(sway_fmt), "[wlr] %s", fmt); + _sway_vlog(convert_wlr_log_importance(importance), sway_fmt, args); +} + +static const struct option long_options[] = { + {"help", no_argument, NULL, 'h'}, + {"config", required_argument, NULL, 'c'}, + {"validate", no_argument, NULL, 'C'}, + {"debug", no_argument, NULL, 'd'}, + {"version", no_argument, NULL, 'v'}, + {"verbose", no_argument, NULL, 'V'}, + {"get-socketpath", no_argument, NULL, 'p'}, + {"unsupported-gpu", no_argument, NULL, 'u'}, + {0, 0, 0, 0} +}; + +static const char usage[] = + "Usage: sway [options] [command]\n" + "\n" + " -h, --help Show help message and quit.\n" + " -c, --config Specify a config file.\n" + " -C, --validate Check the validity of the config file, then exit.\n" + " -d, --debug Enables full logging, including debug information.\n" + " -v, --version Show the version number and quit.\n" + " -V, --verbose Enables more verbose logging.\n" + " --get-socketpath Gets the IPC socket path and prints it, then exits.\n" + "\n"; + +int main(int argc, char **argv) { + bool verbose = false, debug = false, validate = false, allow_unsupported_gpu = false; + + char *config_path = NULL; + + int c; + while (1) { + int option_index = 0; + c = getopt_long(argc, argv, "hCdD:vVc:", long_options, &option_index); + if (c == -1) { + break; + } + switch (c) { + case 'h': // help + printf("%s", usage); + exit(EXIT_SUCCESS); + break; + case 'c': // config + free(config_path); + config_path = strdup(optarg); + break; + case 'C': // validate + validate = true; + break; + case 'd': // debug + debug = true; + break; + case 'D': // extended debug options + enable_debug_flag(optarg); + break; + case 'u': + allow_unsupported_gpu = true; + break; + case 'v': // version + printf("swayfx version " SWAY_VERSION " (based on sway " SWAY_ORIGINAL_VERSION ")\n"); + exit(EXIT_SUCCESS); + break; + case 'V': // verbose + verbose = true; + break; + case 'p': // --get-socketpath + if (getenv("SWAYSOCK")) { + printf("%s\n", getenv("SWAYSOCK")); + exit(EXIT_SUCCESS); + } else { + fprintf(stderr, "sway socket not detected.\n"); + exit(EXIT_FAILURE); + } + break; + default: + fprintf(stderr, "%s", usage); + exit(EXIT_FAILURE); + } + } + + // Since wayland requires XDG_RUNTIME_DIR to be set, abort with just the + // clear error message (when not running as an IPC client). + if (!getenv("XDG_RUNTIME_DIR") && optind == argc) { + fprintf(stderr, + "XDG_RUNTIME_DIR is not set in the environment. Aborting.\n"); + exit(EXIT_FAILURE); + } + + char *unsupported_gpu_env = getenv("SWAY_UNSUPPORTED_GPU"); + // we let the flag override the environment variable + if (!allow_unsupported_gpu && unsupported_gpu_env) { + allow_unsupported_gpu = parse_boolean(unsupported_gpu_env, false); + } + + // As the 'callback' function for wlr_log is equivalent to that for + // sway, we do not need to override it. + if (debug) { + sway_log_init(SWAY_DEBUG, sway_terminate); + wlr_log_init(WLR_DEBUG, handle_wlr_log); + } else if (verbose) { + sway_log_init(SWAY_INFO, sway_terminate); + wlr_log_init(WLR_INFO, handle_wlr_log); + } else { + sway_log_init(SWAY_ERROR, sway_terminate); + wlr_log_init(WLR_ERROR, handle_wlr_log); + } + + sway_log(SWAY_INFO, "swayfx version " SWAY_VERSION " (based on sway version " SWAY_ORIGINAL_VERSION ")"); + sway_log(SWAY_INFO, "wlroots version " WLR_VERSION_STR); + sway_log(SWAY_INFO, "scenefx version " SCENEFX_VERSION); + log_kernel(); + log_distro(); + log_env(); + + if (optind < argc) { // Behave as IPC client + if (optind != 1) { + sway_log(SWAY_ERROR, + "Detected both options and positional arguments. If you " + "are trying to use the IPC client, options are not " + "supported. Otherwise, check the provided arguments for " + "issues. See `man 1 sway` or `sway -h` for usage. If you " + "are trying to generate a debug log, use " + "`sway -d 2>sway.log`."); + exit(EXIT_FAILURE); + } + char *socket_path = getenv("SWAYSOCK"); + if (!socket_path) { + sway_log(SWAY_ERROR, "Unable to retrieve socket path"); + exit(EXIT_FAILURE); + } + char *command = join_args(argv + optind, argc - optind); + run_as_ipc_client(command, socket_path); + free(command); + return 0; + } + + increase_nofile_limit(); + + sway_log(SWAY_INFO, "Starting swayfx version " SWAY_VERSION + " (based on sway version " SWAY_ORIGINAL_VERSION); + + if (!server_init(&server)) { + return 1; + } + + init_signals(); + + if (server.linux_dmabuf_v1) { + wlr_scene_set_linux_dmabuf_v1(root->root_scene, server.linux_dmabuf_v1); + } + + if (validate) { + bool valid = load_main_config(config_path, false, true); + free(config_path); + return valid ? 0 : 1; + } + + ipc_init(&server); + + setenv("WAYLAND_DISPLAY", server.socket, true); + if (!load_main_config(config_path, false, false)) { + sway_terminate(EXIT_FAILURE); + goto shutdown; + } + + set_rr_scheduling(); + + if (!server_start(&server)) { + sway_terminate(EXIT_FAILURE); + goto shutdown; + } + + config->active = true; + force_modeset(); + load_swaybars(); + run_deferred_commands(); + run_deferred_bindings(); + transaction_commit_dirty(); + + if (config->swaynag_config_errors.client != NULL) { + swaynag_show(&config->swaynag_config_errors); + } + + struct swaynag_instance nag_gpu = (struct swaynag_instance){ + .args = "--type error " + "--message 'Proprietary GPU drivers are not supported by sway. Do not report issues.' " + "--detailed-message", + .detailed = true, + }; + + if (unsupported_gpu_detected && !allow_unsupported_gpu) { + swaynag_log(config->swaynag_command, &nag_gpu, + "To remove this message, launch sway with --unsupported-gpu " + "or set the environment variable SWAY_UNSUPPORTED_GPU=true."); + swaynag_show(&nag_gpu); + } + + server_run(&server); + +shutdown: + sway_log(SWAY_INFO, "Shutting down sway"); + + server_fini(&server); + root_destroy(root); + root = NULL; + + free(config_path); + free_config(config); + + if (nag_gpu.client != NULL) { + wl_client_destroy(nag_gpu.client); + } + + pango_cairo_font_map_set_default(NULL); + + return exit_value; +} diff --git a/sway/meson.build b/sway/meson.build new file mode 100644 index 0000000..ecd2865 --- /dev/null +++ b/sway/meson.build @@ -0,0 +1,281 @@ +sway_sources = files( + 'animation_manager.c', + 'commands.c', + 'config.c', + 'criteria.c', + 'decoration.c', + 'ipc-json.c', + 'ipc-server.c', + 'layer_criteria.c', + 'lock.c', + 'main.c', + 'realtime.c', + 'scene_descriptor.c', + 'server.c', + 'sway_text_node.c', + 'swaynag.c', + 'xdg_activation_v1.c', + 'xdg_decoration.c', + + 'desktop/idle_inhibit_v1.c', + 'desktop/layer_shell.c', + 'desktop/output.c', + 'desktop/tearing.c', + 'desktop/transaction.c', + 'desktop/xdg_shell.c', + 'desktop/launcher.c', + + 'input/input-manager.c', + 'input/cursor.c', + 'input/keyboard.c', + 'input/seat.c', + 'input/seatop_default.c', + 'input/seatop_down.c', + 'input/seatop_move_floating.c', + 'input/seatop_move_tiling.c', + 'input/seatop_resize_floating.c', + 'input/seatop_resize_tiling.c', + 'input/switch.c', + 'input/tablet.c', + 'input/text_input.c', + + 'config/bar.c', + 'config/output.c', + 'config/seat.c', + 'config/input.c', + + 'commands/animation_duration_ms.c', + 'commands/allow_tearing.c', + 'commands/assign.c', + 'commands/bar.c', + 'commands/bind.c', + 'commands/blur.c', + 'commands/blur_brightness.c', + 'commands/blur_contrast.c', + 'commands/blur_passes.c', + 'commands/blur_radius.c', + 'commands/blur_xray.c', + 'commands/blur_noise.c', + 'commands/blur_saturation.c', + 'commands/border.c', + 'commands/client.c', + 'commands/corner_radius.c', + 'commands/create_output.c', + 'commands/default_border.c', + 'commands/default_dim_inactive.c', + 'commands/default_floating_border.c', + 'commands/default_orientation.c', + 'commands/dim_inactive.c', + 'commands/dim_inactive_colors.c', + 'commands/exit.c', + 'commands/exec.c', + 'commands/exec_always.c', + 'commands/floating.c', + 'commands/floating_minmax_size.c', + 'commands/floating_modifier.c', + 'commands/focus.c', + 'commands/focus_follows_mouse.c', + 'commands/focus_on_window_activation.c', + 'commands/focus_wrapping.c', + 'commands/font.c', + 'commands/for_window.c', + 'commands/force_display_urgency_hint.c', + 'commands/force_focus_wrapping.c', + 'commands/fullscreen.c', + 'commands/gaps.c', + 'commands/gesture.c', + 'commands/hide_edge_borders.c', + 'commands/inhibit_idle.c', + 'commands/kill.c', + 'commands/mark.c', + 'commands/max_render_time.c', + 'commands/opacity.c', + 'commands/include.c', + 'commands/input.c', + 'commands/layout.c', + 'commands/layer_effects.c', + 'commands/mode.c', + 'commands/mouse_warping.c', + 'commands/move.c', + 'commands/new_float.c', + 'commands/new_window.c', + 'commands/no_focus.c', + 'commands/nop.c', + 'commands/output.c', + 'commands/popup_during_fullscreen.c', + 'commands/primary_selection.c', + 'commands/reload.c', + 'commands/rename.c', + 'commands/resize.c', + 'commands/scratchpad.c', + 'commands/scratchpad_minimize.c', + 'commands/seat.c', + 'commands/seat/attach.c', + 'commands/seat/cursor.c', + 'commands/seat/fallback.c', + 'commands/seat/hide_cursor.c', + 'commands/seat/idle.c', + 'commands/seat/keyboard_grouping.c', + 'commands/seat/pointer_constraint.c', + 'commands/seat/shortcuts_inhibitor.c', + 'commands/seat/xcursor_theme.c', + 'commands/set.c', + 'commands/shadow_blur_radius.c', + 'commands/shadow_color.c', + 'commands/shadow_inactive_color.c', + 'commands/shadow_offset.c', + 'commands/shadows.c', + 'commands/shadows_on_csd.c', + 'commands/show_marks.c', + 'commands/shortcuts_inhibitor.c', + 'commands/smart_borders.c', + 'commands/smart_corner_radius.c', + 'commands/smart_gaps.c', + 'commands/split.c', + 'commands/sticky.c', + 'commands/swaybg_command.c', + 'commands/swaynag_command.c', + 'commands/swap.c', + 'commands/tiling_drag.c', + 'commands/tiling_drag_threshold.c', + 'commands/title_align.c', + 'commands/title_format.c', + 'commands/titlebar_border_thickness.c', + 'commands/titlebar_padding.c', + 'commands/titlebar_separator.c', + 'commands/unmark.c', + 'commands/urgent.c', + 'commands/workspace.c', + 'commands/workspace_layout.c', + 'commands/ws_auto_back_and_forth.c', + 'commands/xwayland.c', + + 'commands/bar/bind.c', + 'commands/bar/binding_mode_indicator.c', + 'commands/bar/colors.c', + 'commands/bar/font.c', + 'commands/bar/gaps.c', + 'commands/bar/height.c', + 'commands/bar/hidden_state.c', + 'commands/bar/icon_theme.c', + 'commands/bar/id.c', + 'commands/bar/mode.c', + 'commands/bar/modifier.c', + 'commands/bar/output.c', + 'commands/bar/pango_markup.c', + 'commands/bar/position.c', + 'commands/bar/separator_symbol.c', + 'commands/bar/status_command.c', + 'commands/bar/status_edge_padding.c', + 'commands/bar/status_padding.c', + 'commands/bar/strip_workspace_numbers.c', + 'commands/bar/strip_workspace_name.c', + 'commands/bar/swaybar_command.c', + 'commands/bar/tray_bind.c', + 'commands/bar/tray_output.c', + 'commands/bar/tray_padding.c', + 'commands/bar/workspace_buttons.c', + 'commands/bar/workspace_min_width.c', + 'commands/bar/wrap_scroll.c', + + 'commands/input/accel_profile.c', + 'commands/input/calibration_matrix.c', + 'commands/input/click_method.c', + 'commands/input/clickfinger_button_map.c', + 'commands/input/drag.c', + 'commands/input/drag_lock.c', + 'commands/input/dwt.c', + 'commands/input/dwtp.c', + 'commands/input/events.c', + 'commands/input/left_handed.c', + 'commands/input/map_from_region.c', + 'commands/input/map_to_output.c', + 'commands/input/map_to_region.c', + 'commands/input/middle_emulation.c', + 'commands/input/natural_scroll.c', + 'commands/input/pointer_accel.c', + 'commands/input/rotation_angle.c', + 'commands/input/repeat_delay.c', + 'commands/input/repeat_rate.c', + 'commands/input/scroll_button.c', + 'commands/input/scroll_button_lock.c', + 'commands/input/scroll_factor.c', + 'commands/input/scroll_method.c', + 'commands/input/tap.c', + 'commands/input/tap_button_map.c', + 'commands/input/tool_mode.c', + 'commands/input/xkb_capslock.c', + 'commands/input/xkb_file.c', + 'commands/input/xkb_layout.c', + 'commands/input/xkb_model.c', + 'commands/input/xkb_numlock.c', + 'commands/input/xkb_options.c', + 'commands/input/xkb_rules.c', + 'commands/input/xkb_switch_layout.c', + 'commands/input/xkb_variant.c', + + 'commands/output/adaptive_sync.c', + 'commands/output/allow_tearing.c', + 'commands/output/background.c', + 'commands/output/disable.c', + 'commands/output/dpms.c', + 'commands/output/enable.c', + 'commands/output/hdr.c', + 'commands/output/max_render_time.c', + 'commands/output/mode.c', + 'commands/output/position.c', + 'commands/output/power.c', + 'commands/output/render_bit_depth.c', + 'commands/output/scale.c', + 'commands/output/scale_filter.c', + 'commands/output/subpixel.c', + 'commands/output/toggle.c', + 'commands/output/transform.c', + 'commands/output/unplug.c', + 'commands/output/color_profile.c', + + 'tree/arrange.c', + 'tree/container.c', + 'tree/node.c', + 'tree/root.c', + 'tree/view.c', + 'tree/workspace.c', + 'tree/output.c', +) + +sway_deps = [ + cairo, + drm, + jsonc, + libevdev, + libinput, + libudev, + math, + pango, + pcre2, + pixman, + scenefx, + threads, + wayland_server, + wlroots, + xkbcommon, + xcb, + xcb_icccm, +] + +if wlroots_features['xwayland'] + sway_sources += 'desktop/xwayland.c' +endif + +if wlroots_features['libinput_backend'] + sway_sources += 'input/libinput.c' +endif + +executable( + 'sway', + sway_sources + wl_protos_src, + include_directories: [sway_inc], + dependencies: sway_deps, + link_with: [lib_sway_common], + install: true +) diff --git a/sway/realtime.c b/sway/realtime.c new file mode 100644 index 0000000..11154af --- /dev/null +++ b/sway/realtime.c @@ -0,0 +1,40 @@ +#include +#include +#include +#include +#include "sway/server.h" +#include "log.h" + +static void child_fork_callback(void) { + struct sched_param param; + + param.sched_priority = 0; + + int ret = pthread_setschedparam(pthread_self(), SCHED_OTHER, ¶m); + if (ret != 0) { + sway_log(SWAY_ERROR, "Failed to reset scheduler policy on fork"); + } +} + +void set_rr_scheduling(void) { + int prio = sched_get_priority_min(SCHED_RR); + int old_policy; + int ret; + struct sched_param param; + + ret = pthread_getschedparam(pthread_self(), &old_policy, ¶m); + if (ret != 0) { + sway_log(SWAY_DEBUG, "Failed to get old scheduling priority"); + return; + } + + param.sched_priority = prio; + + ret = pthread_setschedparam(pthread_self(), SCHED_RR, ¶m); + if (ret != 0) { + sway_log(SWAY_INFO, "Failed to set scheduling priority to %d", prio); + return; + } + + pthread_atfork(NULL, NULL, child_fork_callback); +} diff --git a/sway/scene_descriptor.c b/sway/scene_descriptor.c new file mode 100644 index 0000000..92bdda0 --- /dev/null +++ b/sway/scene_descriptor.c @@ -0,0 +1,69 @@ +#include +#include +#include "log.h" +#include "sway/scene_descriptor.h" + +struct scene_descriptor { + void *data; + struct wlr_addon addon; +}; + +static const struct wlr_addon_interface addon_interface; + +static struct scene_descriptor *scene_node_get_descriptor( + struct wlr_scene_node *node, enum sway_scene_descriptor_type type) { + struct wlr_addon *addon = wlr_addon_find(&node->addons, (void *)type, &addon_interface); + if (!addon) { + return NULL; + } + + struct scene_descriptor *desc = wl_container_of(addon, desc, addon); + return desc; +} + +static void descriptor_destroy(struct scene_descriptor *desc) { + wlr_addon_finish(&desc->addon); + free(desc); +} + +void *scene_descriptor_try_get(struct wlr_scene_node *node, + enum sway_scene_descriptor_type type) { + struct scene_descriptor *desc = scene_node_get_descriptor(node, type); + if (!desc) { + return NULL; + } + + return desc->data; +} + +void scene_descriptor_destroy(struct wlr_scene_node *node, + enum sway_scene_descriptor_type type) { + struct scene_descriptor *desc = scene_node_get_descriptor(node, type); + if (!desc) { + return; + } + descriptor_destroy(desc); +} + +static void addon_handle_destroy(struct wlr_addon *addon) { + struct scene_descriptor *desc = wl_container_of(addon, desc, addon); + descriptor_destroy(desc); +} + +static const struct wlr_addon_interface addon_interface = { + .name = "sway_scene_descriptor", + .destroy = addon_handle_destroy, +}; + +bool scene_descriptor_assign(struct wlr_scene_node *node, + enum sway_scene_descriptor_type type, void *data) { + struct scene_descriptor *desc = calloc(1, sizeof(*desc)); + if (!desc) { + sway_log(SWAY_ERROR, "Could not allocate a scene descriptor"); + return false; + } + + wlr_addon_init(&desc->addon, &node->addons, (void *)type, &addon_interface); + desc->data = data; + return true; +} diff --git a/sway/server.c b/sway/server.c new file mode 100644 index 0000000..8526b82 --- /dev/null +++ b/sway/server.c @@ -0,0 +1,806 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "config.h" +#include "list.h" +#include "log.h" +#include "sway/animation_manager.h" +#include "sway/config.h" +#include "sway/desktop/idle_inhibit_v1.h" +#include "sway/input/input-manager.h" +#include "sway/output.h" +#include "sway/server.h" +#include "sway/input/cursor.h" +#include "sway/tree/root.h" +#include "sway/tree/workspace.h" + +#if WLR_HAS_XWAYLAND +#include +#include "sway/xwayland.h" +#endif + +#if WLR_HAS_DRM_BACKEND +#include +#endif + +#define SWAY_XDG_SHELL_VERSION 5 +#define SWAY_LAYER_SHELL_VERSION 5 +#define SWAY_FOREIGN_TOPLEVEL_LIST_VERSION 1 +#define SWAY_PRESENTATION_VERSION 2 + +bool unsupported_gpu_detected = false; + +#if WLR_HAS_DRM_BACKEND +static void handle_drm_lease_request(struct wl_listener *listener, void *data) { + /* We only offer non-desktop outputs, but in the future we might want to do + * more logic here. */ + + struct wlr_drm_lease_request_v1 *req = data; + struct wlr_drm_lease_v1 *lease = wlr_drm_lease_request_v1_grant(req); + if (!lease) { + sway_log(SWAY_ERROR, "Failed to grant lease request"); + wlr_drm_lease_request_v1_reject(req); + } +} +#endif + +static bool is_privileged(const struct wl_global *global) { +#if WLR_HAS_DRM_BACKEND + if (server.drm_lease_manager != NULL) { + struct wlr_drm_lease_device_v1 *drm_lease_dev; + wl_list_for_each(drm_lease_dev, &server.drm_lease_manager->devices, link) { + if (drm_lease_dev->global == global) { + return true; + } + } + } +#endif + + return + global == server.output_manager_v1->global || + global == server.output_power_manager_v1->global || + global == server.input_method->global || + global == server.foreign_toplevel_list->global || + global == server.foreign_toplevel_manager->global || + global == server.wlr_data_control_manager_v1->global || + global == server.ext_data_control_manager_v1->global || + global == server.screencopy_manager_v1->global || + global == server.ext_image_copy_capture_manager_v1->global || + global == server.export_dmabuf_manager_v1->global || + global == server.security_context_manager_v1->global || + global == server.gamma_control_manager_v1->global || + global == server.layer_shell->global || + global == server.session_lock.manager->global || + global == server.input->keyboard_shortcuts_inhibit->global || + global == server.input->virtual_keyboard->global || + global == server.input->virtual_pointer->global || + global == server.input->transient_seat_manager->global || + global == server.xdg_output_manager_v1->global || + global == server.workspace_manager_v1->global; +} + +static bool filter_global(const struct wl_client *client, + const struct wl_global *global, void *data) { +#if WLR_HAS_XWAYLAND + struct wlr_xwayland *xwayland = server.xwayland.wlr_xwayland; + if (xwayland && global == xwayland->shell_v1->global) { + return xwayland->server != NULL && client == xwayland->server->client; + } +#endif + + // Restrict usage of privileged protocols to unsandboxed clients + // TODO: add a way for users to configure an allow-list + const struct wlr_security_context_v1_state *security_context = + wlr_security_context_manager_v1_lookup_client( + server.security_context_manager_v1, (struct wl_client *)client); + if (is_privileged(global)) { + return security_context == NULL; + } + + return true; +} + +static void detect_proprietary(struct wlr_backend *backend, void *data) { + int drm_fd = wlr_backend_get_drm_fd(backend); + if (drm_fd < 0) { + return; + } + + drmVersion *version = drmGetVersion(drm_fd); + if (version == NULL) { + sway_log(SWAY_ERROR, "drmGetVersion() failed"); + return; + } + + if (strcmp(version->name, "nvidia-drm") == 0) { + unsupported_gpu_detected = true; + sway_log(SWAY_ERROR, "!!! Proprietary Nvidia drivers are in use !!!"); + } + + if (strcmp(version->name, "evdi") == 0) { + unsupported_gpu_detected = true; + sway_log(SWAY_ERROR, "!!! Proprietary DisplayLink drivers are in use !!!"); + } + + drmFreeVersion(version); +} + +static void do_renderer_recreate(void *data) { + struct sway_server *server = data; + server->recreating_renderer = NULL; + + sway_log(SWAY_INFO, "Re-creating renderer after GPU reset"); + struct wlr_renderer *renderer = fx_renderer_create(server->backend); + if (renderer == NULL) { + sway_log(SWAY_ERROR, "Unable to create renderer"); + return; + } + + struct wlr_allocator *allocator = + wlr_allocator_autocreate(server->backend, renderer); + if (allocator == NULL) { + sway_log(SWAY_ERROR, "Unable to create allocator"); + wlr_renderer_destroy(renderer); + return; + } + + struct wlr_renderer *old_renderer = server->renderer; + struct wlr_allocator *old_allocator = server->allocator; + server->renderer = renderer; + server->allocator = allocator; + + wl_list_remove(&server->renderer_lost.link); + wl_signal_add(&server->renderer->events.lost, &server->renderer_lost); + + wlr_compositor_set_renderer(server->compositor, renderer); + + struct sway_output *output; + wl_list_for_each(output, &root->all_outputs, link) { + wlr_output_init_render(output->wlr_output, + server->allocator, server->renderer); + } + + wlr_allocator_destroy(old_allocator); + wlr_renderer_destroy(old_renderer); +} + +static void handle_renderer_lost(struct wl_listener *listener, void *data) { + struct sway_server *server = wl_container_of(listener, server, renderer_lost); + + if (server->recreating_renderer != NULL) { + sway_log(SWAY_DEBUG, "Re-creation of renderer already scheduled"); + return; + } + + sway_log(SWAY_INFO, "Scheduling re-creation of renderer after GPU reset"); + server->recreating_renderer = wl_event_loop_add_idle(server->wl_event_loop, do_renderer_recreate, server); +} + +static void handle_new_foreign_toplevel_capture_request(struct wl_listener *listener, void *data) { + struct wlr_ext_foreign_toplevel_image_capture_source_manager_v1_request *request = data; + struct sway_view *view = request->toplevel_handle->data; + + if (view->image_capture_source == NULL) { + view->image_capture_source = wlr_ext_image_capture_source_v1_create_with_scene_node( + &view->image_capture_scene->tree.node, server.wl_event_loop, server.allocator, server.renderer); + if (view->image_capture_source == NULL) { + return; + } + } + + wlr_ext_foreign_toplevel_image_capture_source_manager_v1_request_accept(request, view->image_capture_source); +} + +bool server_init(struct sway_server *server) { + sway_log(SWAY_DEBUG, "Initializing Wayland server"); + server->wl_display = wl_display_create(); + if (!server->wl_display) { + sway_log(SWAY_ERROR, "Failed to create wl_display"); + return false; + } + server->wl_event_loop = wl_display_get_event_loop(server->wl_display); + + wl_display_set_global_filter(server->wl_display, filter_global, NULL); + wl_display_set_default_max_buffer_size(server->wl_display, 1024 * 1024); + + if (!wlr_fixes_create(server->wl_display, 1)) { + sway_log(SWAY_ERROR, "Failed to create wp_fixes global"); + return false; + } + root = root_create(server->wl_display); + if (!root) { + sway_log(SWAY_ERROR, "Failed to create root"); + wl_display_destroy(server->wl_display); + return false; + } + + server->backend = wlr_backend_autocreate(server->wl_event_loop, &server->session); + if (!server->backend) { + sway_log(SWAY_ERROR, "Unable to create backend"); + return false; + } + + wlr_multi_for_each_backend(server->backend, detect_proprietary, NULL); + + server->renderer = fx_renderer_create(server->backend); + if (!server->renderer) { + sway_log(SWAY_ERROR, "Failed to create renderer"); + return false; + } + + server->renderer_lost.notify = handle_renderer_lost; + wl_signal_add(&server->renderer->events.lost, &server->renderer_lost); + + wlr_renderer_init_wl_shm(server->renderer, server->wl_display); + + if (wlr_renderer_get_drm_fd(server->renderer) >= 0 && + wlr_renderer_get_texture_formats(server->renderer, WLR_BUFFER_CAP_DMABUF) != NULL) { + server->linux_dmabuf_v1 = wlr_linux_dmabuf_v1_create_with_renderer( + server->wl_display, 5, server->renderer); + if (!server->linux_dmabuf_v1) { + sway_log(SWAY_ERROR, "Failed to create linux-dmabuf v1"); + return false; + } + } + if (wlr_renderer_get_drm_fd(server->renderer) >= 0 && + server->renderer->features.timeline && + server->backend->features.timeline) { + wlr_linux_drm_syncobj_manager_v1_create(server->wl_display, 1, + wlr_renderer_get_drm_fd(server->renderer)); + } + + server->allocator = wlr_allocator_autocreate(server->backend, + server->renderer); + if (!server->allocator) { + sway_log(SWAY_ERROR, "Failed to create allocator"); + return false; + } + + server->compositor = wlr_compositor_create(server->wl_display, 6, + server->renderer); + if (!server->compositor) { + sway_log(SWAY_ERROR, "Failed to create compositor"); + return false; + } + + if (!wlr_subcompositor_create(server->wl_display)) { + sway_log(SWAY_ERROR, "Failed to create subcompositor"); + return false; + } + + server->data_device_manager = + wlr_data_device_manager_create(server->wl_display); + if (!server->data_device_manager) { + sway_log(SWAY_ERROR, "Failed to create data device manager"); + return false; + } + + server->gamma_control_manager_v1 = + wlr_gamma_control_manager_v1_create(server->wl_display); + if (!server->gamma_control_manager_v1) { + sway_log(SWAY_ERROR, "Failed to create gamma control manager"); + return false; + } + wlr_scene_set_gamma_control_manager_v1(root->root_scene, + server->gamma_control_manager_v1); + + server->new_output.notify = handle_new_output; + wl_signal_add(&server->backend->events.new_output, &server->new_output); + + server->xdg_output_manager_v1 = + wlr_xdg_output_manager_v1_create(server->wl_display, root->output_layout); + if (!server->xdg_output_manager_v1) { + sway_log(SWAY_ERROR, "Failed to create XDG output manager"); + return false; + } + + server->idle_notifier_v1 = wlr_idle_notifier_v1_create(server->wl_display); + if (!server->idle_notifier_v1) { + sway_log(SWAY_ERROR, "Failed to create idle notifier"); + return false; + } + if (!sway_idle_inhibit_manager_v1_init()) { + sway_log(SWAY_ERROR, "Failed to init idle inhibit manager"); + return false; + } + + server->layer_shell = wlr_layer_shell_v1_create(server->wl_display, + SWAY_LAYER_SHELL_VERSION); + if (!server->layer_shell) { + sway_log(SWAY_ERROR, "Failed to create layer shell"); + return false; + } + wl_signal_add(&server->layer_shell->events.new_surface, + &server->layer_shell_surface); + server->layer_shell_surface.notify = handle_layer_shell_surface; + + server->xdg_shell = wlr_xdg_shell_create(server->wl_display, + SWAY_XDG_SHELL_VERSION); + if (!server->xdg_shell) { + sway_log(SWAY_ERROR, "Failed to create XDG shell"); + return false; + } + wl_signal_add(&server->xdg_shell->events.new_toplevel, + &server->xdg_shell_toplevel); + server->xdg_shell_toplevel.notify = handle_xdg_shell_toplevel; + + server->tablet_v2 = wlr_tablet_v2_create(server->wl_display); + if (!server->tablet_v2) { + sway_log(SWAY_ERROR, "Failed to create tablet manager"); + return false; + } + + server->server_decoration_manager = + wlr_server_decoration_manager_create(server->wl_display); + if (!server->server_decoration_manager) { + sway_log(SWAY_ERROR, "Failed to create server decoration manager"); + return false; + } + wlr_server_decoration_manager_set_default_mode( + server->server_decoration_manager, + WLR_SERVER_DECORATION_MANAGER_MODE_SERVER); + wl_signal_add(&server->server_decoration_manager->events.new_decoration, + &server->server_decoration); + server->server_decoration.notify = handle_server_decoration; + wl_list_init(&server->decorations); + + server->xdg_decoration_manager = + wlr_xdg_decoration_manager_v1_create(server->wl_display); + if (!server->xdg_decoration_manager) { + sway_log(SWAY_ERROR, "Failed to create XDG decoration manager"); + return false; + } + wl_signal_add( + &server->xdg_decoration_manager->events.new_toplevel_decoration, + &server->xdg_decoration); + server->xdg_decoration.notify = handle_xdg_decoration; + wl_list_init(&server->xdg_decorations); + + server->relative_pointer_manager = + wlr_relative_pointer_manager_v1_create(server->wl_display); + if (!server->relative_pointer_manager) { + sway_log(SWAY_ERROR, "Failed to create relative pointer manager"); + return false; + } + + server->pointer_constraints = + wlr_pointer_constraints_v1_create(server->wl_display); + if (!server->pointer_constraints) { + sway_log(SWAY_ERROR, "Failed to create pointer constraints"); + return false; + } + server->pointer_constraint.notify = handle_pointer_constraint; + wl_signal_add(&server->pointer_constraints->events.new_constraint, + &server->pointer_constraint); + + if (!wlr_presentation_create(server->wl_display, server->backend, SWAY_PRESENTATION_VERSION)) { + sway_log(SWAY_ERROR, "Failed to create presentation"); + return false; + } + if (!wlr_alpha_modifier_v1_create(server->wl_display)) { + sway_log(SWAY_ERROR, "Failed to create alpha modifier"); + return false; + } + + server->output_manager_v1 = + wlr_output_manager_v1_create(server->wl_display); + if (!server->output_manager_v1) { + sway_log(SWAY_ERROR, "Failed to create output manager"); + return false; + } + server->output_manager_apply.notify = handle_output_manager_apply; + wl_signal_add(&server->output_manager_v1->events.apply, + &server->output_manager_apply); + server->output_manager_test.notify = handle_output_manager_test; + wl_signal_add(&server->output_manager_v1->events.test, + &server->output_manager_test); + + server->output_power_manager_v1 = + wlr_output_power_manager_v1_create(server->wl_display); + if (!server->output_power_manager_v1) { + sway_log(SWAY_ERROR, "Failed to create output power manager"); + return false; + } + server->output_power_manager_set_mode.notify = + handle_output_power_manager_set_mode; + wl_signal_add(&server->output_power_manager_v1->events.set_mode, + &server->output_power_manager_set_mode); + server->input_method = wlr_input_method_manager_v2_create(server->wl_display); + if (!server->input_method) { + sway_log(SWAY_ERROR, "Failed to create input method manager"); + return false; + } + server->text_input = wlr_text_input_manager_v3_create(server->wl_display); + if (!server->text_input) { + sway_log(SWAY_ERROR, "Failed to create text input manager"); + return false; + } + server->foreign_toplevel_list = + wlr_ext_foreign_toplevel_list_v1_create(server->wl_display, SWAY_FOREIGN_TOPLEVEL_LIST_VERSION); + if (!server->foreign_toplevel_list) { + sway_log(SWAY_ERROR, "Failed to create foreign toplevel list"); + return false; + } + server->foreign_toplevel_manager = + wlr_foreign_toplevel_manager_v1_create(server->wl_display); + if (!server->foreign_toplevel_manager) { + sway_log(SWAY_ERROR, "Failed to create foreign toplevel manager"); + return false; + } + + if (!sway_session_lock_init()) { + return false; + } + if (!sway_ext_workspace_init()) { + return false; + } + +#if WLR_HAS_DRM_BACKEND + server->drm_lease_manager= + wlr_drm_lease_v1_manager_create(server->wl_display, server->backend); + if (server->drm_lease_manager) { + server->drm_lease_request.notify = handle_drm_lease_request; + wl_signal_add(&server->drm_lease_manager->events.request, + &server->drm_lease_request); + } else { + sway_log(SWAY_DEBUG, "Failed to create wlr_drm_lease_device_v1"); + sway_log(SWAY_INFO, "VR will not be available"); + } +#endif + + server->export_dmabuf_manager_v1 = wlr_export_dmabuf_manager_v1_create(server->wl_display); + if (!server->export_dmabuf_manager_v1) { + sway_log(SWAY_ERROR, "Failed to create export dmabuf manager"); + return false; + } + server->screencopy_manager_v1 = wlr_screencopy_manager_v1_create(server->wl_display); + if (!server->screencopy_manager_v1) { + sway_log(SWAY_ERROR, "Failed to create screencopy manager"); + return false; + } + server->ext_image_copy_capture_manager_v1 = wlr_ext_image_copy_capture_manager_v1_create(server->wl_display, 1); + if (!server->ext_image_copy_capture_manager_v1) { + sway_log(SWAY_ERROR, "Failed to create ext image copy capture manager"); + return false; + } + if (!wlr_ext_output_image_capture_source_manager_v1_create(server->wl_display, 1)) { + sway_log(SWAY_ERROR, "Failed to create ext output image capture source manager"); + return false; + } + server->wlr_data_control_manager_v1 = wlr_data_control_manager_v1_create(server->wl_display); + if (!server->wlr_data_control_manager_v1) { + sway_log(SWAY_ERROR, "Failed to create data control manager"); + return false; + } + server->ext_data_control_manager_v1 = wlr_ext_data_control_manager_v1_create(server->wl_display, 1); + if (!server->ext_data_control_manager_v1) { + sway_log(SWAY_ERROR, "Failed to create ext data control manager"); + return false; + } + server->security_context_manager_v1 = wlr_security_context_manager_v1_create(server->wl_display); + if (!server->security_context_manager_v1) { + sway_log(SWAY_ERROR, "Failed to create security context manager"); + return false; + } + if (!wlr_viewporter_create(server->wl_display)) { + sway_log(SWAY_ERROR, "Failed to create viewporter"); + return false; + } + if (!wlr_single_pixel_buffer_manager_v1_create(server->wl_display)) { + sway_log(SWAY_ERROR, "Failed to create single pixel buffer manager"); + return false; + } + server->content_type_manager_v1 = + wlr_content_type_manager_v1_create(server->wl_display, 1); + if (!server->content_type_manager_v1) { + sway_log(SWAY_ERROR, "Failed to create content type manager"); + return false; + } + if (!wlr_fractional_scale_manager_v1_create(server->wl_display, 1)) { + sway_log(SWAY_ERROR, "Failed to create fractional scale manager"); + return false; + } + + server->ext_foreign_toplevel_image_capture_source_manager_v1 = + wlr_ext_foreign_toplevel_image_capture_source_manager_v1_create(server->wl_display, 1); + if (!server->ext_foreign_toplevel_image_capture_source_manager_v1) { + sway_log(SWAY_ERROR, "Failed to create ext foreign toplevel image capture source manager"); + return false; + } + server->new_foreign_toplevel_capture_request.notify = handle_new_foreign_toplevel_capture_request; + wl_signal_add(&server->ext_foreign_toplevel_image_capture_source_manager_v1->events.new_request, + &server->new_foreign_toplevel_capture_request); + + server->tearing_control_v1 = + wlr_tearing_control_manager_v1_create(server->wl_display, 1); + if (!server->tearing_control_v1) { + sway_log(SWAY_ERROR, "Failed to create tearing control manager"); + return false; + } + server->tearing_control_new_object.notify = handle_new_tearing_hint; + wl_signal_add(&server->tearing_control_v1->events.new_object, + &server->tearing_control_new_object); + wl_list_init(&server->tearing_controllers); + + struct wlr_xdg_foreign_registry *foreign_registry = + wlr_xdg_foreign_registry_create(server->wl_display); + if (!foreign_registry) { + sway_log(SWAY_ERROR, "Failed to create XDG foreign registry"); + return false; + } + if (!wlr_xdg_foreign_v1_create(server->wl_display, foreign_registry)) { + sway_log(SWAY_ERROR, "Failed to create XDG foreign v1"); + return false; + } + if (!wlr_xdg_foreign_v2_create(server->wl_display, foreign_registry)) { + sway_log(SWAY_ERROR, "Failed to create XDG foreign v2"); + return false; + } + + server->xdg_activation_v1 = wlr_xdg_activation_v1_create(server->wl_display); + if (!server->xdg_activation_v1) { + sway_log(SWAY_ERROR, "Failed to create XDG activation"); + return false; + } + server->xdg_activation_v1_request_activate.notify = + xdg_activation_v1_handle_request_activate; + wl_signal_add(&server->xdg_activation_v1->events.request_activate, + &server->xdg_activation_v1_request_activate); + server->xdg_activation_v1_new_token.notify = + xdg_activation_v1_handle_new_token; + wl_signal_add(&server->xdg_activation_v1->events.new_token, + &server->xdg_activation_v1_new_token); + + struct wlr_xdg_toplevel_tag_manager_v1 *xdg_toplevel_tag_manager_v1 = + wlr_xdg_toplevel_tag_manager_v1_create(server->wl_display, 1); + if (!xdg_toplevel_tag_manager_v1) { + sway_log(SWAY_ERROR, "Failed to create XDG toplevel tag manager"); + return false; + } + server->xdg_toplevel_tag_manager_v1_set_tag.notify = + xdg_toplevel_tag_manager_v1_handle_set_tag; + wl_signal_add(&xdg_toplevel_tag_manager_v1->events.set_tag, + &server->xdg_toplevel_tag_manager_v1_set_tag); + + struct wlr_cursor_shape_manager_v1 *cursor_shape_manager = + wlr_cursor_shape_manager_v1_create(server->wl_display, 2); + if (!cursor_shape_manager) { + sway_log(SWAY_ERROR, "Failed to create cursor shape manager"); + return false; + } + server->request_set_cursor_shape.notify = handle_request_set_cursor_shape; + wl_signal_add(&cursor_shape_manager->events.request_set_shape, &server->request_set_cursor_shape); + + if (server->renderer->features.input_color_transform) { + const enum wp_color_manager_v1_render_intent render_intents[] = { + WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL, + }; + size_t transfer_functions_len = 0; + enum wp_color_manager_v1_transfer_function *transfer_functions = + wlr_color_manager_v1_transfer_function_list_from_renderer(server->renderer, &transfer_functions_len); + size_t primaries_len = 0; + enum wp_color_manager_v1_primaries *primaries = + wlr_color_manager_v1_primaries_list_from_renderer(server->renderer, &primaries_len); + struct wlr_color_manager_v1 *cm = wlr_color_manager_v1_create( + server->wl_display, 2, &(struct wlr_color_manager_v1_options){ + .features = { + .parametric = true, + .set_mastering_display_primaries = true, + }, + .render_intents = render_intents, + .render_intents_len = sizeof(render_intents) / sizeof(render_intents[0]), + .transfer_functions = transfer_functions, + .transfer_functions_len = transfer_functions_len, + .primaries = primaries, + .primaries_len = primaries_len, + }); + free(transfer_functions); + free(primaries); + if (!cm) { + sway_log(SWAY_ERROR, "Failed to create color manager"); + return false; + } + wlr_scene_set_color_manager_v1(root->root_scene, cm); + } + + if (!wlr_color_representation_manager_v1_create_with_renderer( + server->wl_display, 1, server->renderer)) { + sway_log(SWAY_ERROR, "Failed to create color representation manager"); + return false; + } + + wl_list_init(&server->pending_launcher_ctxs); + + // Avoid using "wayland-0" as display socket + char name_candidate[16]; + for (unsigned int i = 1; i <= 32; ++i) { + snprintf(name_candidate, sizeof(name_candidate), "wayland-%u", i); + if (wl_display_add_socket(server->wl_display, name_candidate) >= 0) { + server->socket = strdup(name_candidate); + break; + } + } + + if (!server->socket) { + sway_log(SWAY_ERROR, "Unable to open wayland socket"); + wlr_backend_destroy(server->backend); + return false; + } + + server->headless_backend = wlr_headless_backend_create(server->wl_event_loop); + if (!server->headless_backend) { + sway_log(SWAY_ERROR, "Failed to create secondary headless backend"); + wlr_backend_destroy(server->backend); + return false; + } else { + wlr_multi_backend_add(server->backend, server->headless_backend); + } + + struct wlr_output *wlr_output = + wlr_headless_add_output(server->headless_backend, 800, 600); + wlr_output_set_name(wlr_output, "FALLBACK"); + root->fallback_output = output_create(wlr_output); + + // This may have been set already via -Dtxn-timeout + if (!server->txn_timeout_ms) { + server->txn_timeout_ms = 200; + } + + server->dirty_nodes = create_list(); + if (!server->dirty_nodes) { + sway_log(SWAY_ERROR, "Failed to create dirty nodes list"); + return false; + } + + server->input = input_manager_create(server); + if (!server->input) { + sway_log(SWAY_ERROR, "Failed to create input manager"); + return false; + } + input_manager_get_default_seat(); // create seat0 + + animation_manager_init(server); + + return true; +} + +void server_fini(struct sway_server *server) { + // remove listeners + wl_list_remove(&server->renderer_lost.link); + wl_list_remove(&server->new_output.link); + wl_list_remove(&server->layer_shell_surface.link); + wl_list_remove(&server->xdg_shell_toplevel.link); + wl_list_remove(&server->server_decoration.link); + wl_list_remove(&server->xdg_decoration.link); + wl_list_remove(&server->pointer_constraint.link); + wl_list_remove(&server->output_manager_apply.link); + wl_list_remove(&server->output_manager_test.link); + wl_list_remove(&server->output_power_manager_set_mode.link); +#if WLR_HAS_DRM_BACKEND + if (server->drm_lease_manager) { + wl_list_remove(&server->drm_lease_request.link); + } +#endif + wl_list_remove(&server->tearing_control_new_object.link); + wl_list_remove(&server->xdg_activation_v1_request_activate.link); + wl_list_remove(&server->xdg_activation_v1_new_token.link); + wl_list_remove(&server->xdg_toplevel_tag_manager_v1_set_tag.link); + wl_list_remove(&server->request_set_cursor_shape.link); + wl_list_remove(&server->new_foreign_toplevel_capture_request.link); + wl_list_remove(&server->workspace_manager_v1_commit.link); + input_manager_finish(server->input); + + // TODO: free sway-specific resources +#if WLR_HAS_XWAYLAND + if (server->xwayland.wlr_xwayland != NULL) { + wl_list_remove(&server->xwayland_surface.link); + wl_list_remove(&server->xwayland_ready.link); + wlr_xwayland_destroy(server->xwayland.wlr_xwayland); + } +#endif + wl_display_destroy_clients(server->wl_display); + wlr_backend_destroy(server->backend); + wl_display_destroy(server->wl_display); + list_free(server->dirty_nodes); + free(server->socket); +} + +bool server_start(struct sway_server *server) { +#if WLR_HAS_XWAYLAND + if (config->xwayland != XWAYLAND_MODE_DISABLED) { + sway_log(SWAY_DEBUG, "Initializing Xwayland (lazy=%d)", + config->xwayland == XWAYLAND_MODE_LAZY); + server->xwayland.wlr_xwayland = + wlr_xwayland_create(server->wl_display, server->compositor, + config->xwayland == XWAYLAND_MODE_LAZY); + if (!server->xwayland.wlr_xwayland) { + sway_log(SWAY_ERROR, "Failed to start Xwayland"); + unsetenv("DISPLAY"); + } else { + wl_signal_add(&server->xwayland.wlr_xwayland->events.new_surface, + &server->xwayland_surface); + server->xwayland_surface.notify = handle_xwayland_surface; + wl_signal_add(&server->xwayland.wlr_xwayland->events.ready, + &server->xwayland_ready); + server->xwayland_ready.notify = handle_xwayland_ready; + + setenv("DISPLAY", server->xwayland.wlr_xwayland->display_name, true); + + /* xcursor configured by the default seat */ + } + } +#endif + + if (config->primary_selection) { + wlr_primary_selection_v1_device_manager_create(server->wl_display); + } + + sway_log(SWAY_INFO, "Starting backend on wayland display '%s'", + server->socket); + if (!wlr_backend_start(server->backend)) { + sway_log(SWAY_ERROR, "Failed to start backend"); + wlr_backend_destroy(server->backend); + return false; + } + + return true; +} + +void server_run(struct sway_server *server) { + sway_log(SWAY_INFO, "Running compositor on wayland display '%s'", + server->socket); + wl_display_run(server->wl_display); +} diff --git a/sway/sway-bar.5.scd b/sway/sway-bar.5.scd new file mode 100644 index 0000000..a1733aa --- /dev/null +++ b/sway/sway-bar.5.scd @@ -0,0 +1,237 @@ +sway-bar(5) + +# NAME + +sway-bar - bar configuration file and commands + +# DESCRIPTION + +Sway allows configuring swaybar in the sway configuration file. + +# COMMANDS + +The following commands may only be used in the configuration file. + +*id* + Sets the ID of the bar. + +*swaybar_command* + Executes custom bar command. Default is _swaybar_. + +The following commands may be used either in the configuration file or at +runtime. + +*bindcode* [--release] + Executes _command_ when the mouse button has been pressed (or if _released_ + is given, when the button has been released). The buttons can be given as + an event code, which can be obtaining from *libinput debug-events*. To + disable the default behavior for a button, use the command _nop_. + +*bindsym* [--release] button[1-9]| + Executes _command_ when the mouse button has been pressed (or if _released_ + is given, when the button has been released). The buttons can be given as a + x11 button number or an event name, which can be obtained from *libinput + debug-events*. To disable the default behavior for a button, use the + command _nop_. + +*binding_mode_indicator* yes|no + Enable or disable binding mode indicator. Default is _yes_. + +*font* + Specifies the font to be used in the bar. _font_ should be specified as a + pango font description. For more information on pango font descriptions, + see https://docs.gtk.org/Pango/type_func.FontDescription.from_string.html#description + +*gaps* | | + Sets the gaps from the edge of the screen for the bar. Gaps can either be + set all at once, per direction, or per side. Note that only sides that + touch an edge of the screen can have gaps. For the side that does not + touch an edge of the screen, per-side outer gaps for workspaces may be of + use. + +*height* + Sets the height of the bar. Default height (0) will match the font size. + +*hidden_state* hide|show [] + Specifies the behaviour of the bar when it is in _hide_ mode. When the + hidden state is _hide_, then it is normally hidden, and only unhidden by + pressing the modifier key or in case of urgency hints. When the hidden + state is _show_, then it is permanently visible, drawn on top of the + currently visible workspace. Default is _hide_. + + For compatibility with i3, _bar hidden_state hide|show []_ is + supported along with the sway only _bar hidden_state hide|show_ + syntax. When using the i3 syntax, if _bar-id_ is omitted, the hidden_state + will be changed for all bars. Attempting to use _bar + hidden_state hide|show _ will result in an error due to + conflicting bar ids. + +*mode* dock|hide|invisible|overlay [] + Specifies the visibility of the bar. In _dock_ mode, it is permanently + visible at one edge of the screen. In _hide_ mode, it is hidden unless the + modifier key is pressed, though this behaviour depends on the hidden state. + In _invisible_ mode, it is permanently hidden. In _overlay_ mode, it is + permanently visible on top of other windows. (In _overlay_ mode the bar is + transparent to input events.) Default is _dock_. + + For compatibility with i3, _bar mode []_ syntax is supported + along with the sway only _bar mode _ syntax. When using the + i3 syntax, if _bar-id_ is omitted, the mode will be changed for all bars. + Attempting to use _bar mode _ will result in an + error due to conflicting bar ids. + +*modifier* |none + Specifies the modifier key that shows a hidden bar. Default is _Mod4_. + +*output* |\* + Restrict the bar to a certain output, can be specified multiple times. If + the output command is omitted, the bar will be displayed on all outputs. _\*_ + can be given at any point to reset it back to all outputs. + +*pango_markup* enabled|disabled + Enables or disables pango markup for status lines. This has no effect on + status lines using the i3bar JSON protocol. + +*position* top|bottom + Sets position of the bar. Default is _bottom_. + +*separator_symbol* + Specifies the separator symbol to separate blocks on the bar. + +*status_command* + Executes the bar _status command_ with _sh -c_. Each line of text printed + to stdout from this command will be displayed in the status area of the + bar. You may also use swaybar's JSON status line protocol. See + *swaybar-protocol*(7) for more information on the protocol + + If running this command via IPC, you can disable a running status command by + setting the command to a single dash: _swaybar bar bar-0 status\_command -_ + +*status_edge_padding* + Sets the padding that is used when the status line is at the right edge of + the bar. This value will be multiplied by the output scale. The default is + _3_. + +*status_padding* + Sets the vertical padding that is used for the status line. The default is + _1_. If _padding_ is _0_, blocks will be able to take up the full height of + the bar. This value will be multiplied by the output scale. + +*strip_workspace_name* yes|no + If set to _yes_, then workspace names will be omitted from the workspace + button and only the custom number will be shown. Default is _no_. + +*strip_workspace_numbers* yes|no + If set to _yes_, then workspace numbers will be omitted from the workspace + button and only the custom name will be shown. Default is _no_. + +*unbindcode* [--release] + Removes the binding with the given . + +*unbindsym* [--release] button[1-9]| + Removes the binding with the given