├── .gitignore ├── .gitlab-ci.yml ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── docs ├── index.md ├── meta │ ├── links-to-html.lua │ ├── pandoc.html.template │ ├── pandoc.man.template │ ├── shikane.metadata.yml │ └── styles.html ├── shikane.1.md ├── shikane.5.md └── shikanectl.1.md ├── flake.lock ├── flake.nix ├── nix-modules └── shikane.nix ├── scripts └── build-docs.sh └── src ├── bin ├── shikane.rs └── shikanectl.rs ├── client.rs ├── client └── args.rs ├── daemon.rs ├── daemon ├── ipc.rs ├── profile_manager.rs └── state_machine.rs ├── error.rs ├── execute.rs ├── ipc.rs ├── lib.rs ├── matching.rs ├── matching ├── comparator.rs ├── hopcroft_karp_map.rs ├── pairing.rs └── pipelined.rs ├── pipeline.rs ├── profile.rs ├── profile ├── convert.rs └── mode.rs ├── search.rs ├── search ├── field.rs ├── multi.rs ├── parser.rs ├── query.rs └── single.rs ├── settings.rs ├── util.rs ├── variant.rs ├── wl_backend.rs ├── wl_backend └── wl_store.rs ├── wlroots.rs └── wlroots ├── om_configuration.rs ├── om_head.rs ├── om_manager.rs ├── om_mode.rs └── wl_registry.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /build 3 | 4 | /result 5 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: 2 | name: pandoc/core:3.1.13 3 | entrypoint: ["/bin/sh", "-c"] 4 | before_script: 5 | - apk add bash 6 | pages: 7 | stage: deploy 8 | script: 9 | - scripts/build-docs.sh html 10 | - mkdir public/ 11 | - mv build/html/* public/ 12 | artifacts: 13 | paths: 14 | - public 15 | only: 16 | - tags 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). 6 | This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) 7 | regarding documented command line interfaces and configuration files. 8 | 9 | ## [Unreleased] 10 | 11 | ## [1.0.1] - 2024-05-28 12 | 13 | ### Added 14 | 15 | - Set `SHIKANE_LOG_TIME` environment variable to `1` to enable timestamps in logs. 16 | By default no timestamps are logged. 17 | - Document the `SHIKANE_LOG`, `SHIKANE_LOG_STYLES` and `SHIKANE_LOG_TIME` env vars. 18 | 19 | ### Changed 20 | 21 | - Change log filter to print state machine changes by default 22 | 23 | ### Fixed 24 | 25 | - daemon in oneshot mode not shutting down on NoVariantApplied state 26 | 27 | ## [1.0.0] - 2024-05-27 28 | 29 | ### Added 30 | 31 | #### CLI client `shikanectl` for controlling and querying the shikane daemon 32 | 33 | - `shikanectl export` export current display setup as shikane config 34 | - `shikanectl reload` instruct the daemon to reread the config file 35 | - `shikanectl switch` ad-hoc profile switching 36 | 37 | #### shikane daemon 38 | 39 | - Overhauled, more complex matching procedure which enables the generation of 40 | all possible profile variants based on connected displays and sorting those 41 | variants by exactness. 42 | - config: Add `search` field to replace the `match` field 43 | - Compare `search` *patterns* against specific display attributes 44 | - Define multiple `search`es per output 45 | - New search kind *substring matching* in `search`es 46 | - config: Add parsing of `position`s in the form `x,y` 47 | - config: Add parsing of `mode`s like `1920x1080@60Hz` 48 | - Selecting and setting of `best` and `preferred` modes 49 | - Optional `timeout` to wait after certain events from the compositor 50 | 51 | #### Meta 52 | 53 | - Use the [kanshi-converter snippet](https://gitlab.com/w0lff/shikane/-/snippets/3713247) 54 | to convert kanshi config to shikanes config.toml. 55 | - Testing for parts of the config parser (e.g. `search`, `mode`) 56 | - flake.nix for building shikane and documentation with [nix](https://nixos.org/) 57 | - [HTML documentation](https://w0lff.gitlab.io/shikane) 58 | - Automated deployment of HTML documentation using .gitlab-ci.yml 59 | 60 | ### Changed 61 | 62 | - Regex comparison behavior. 63 | Previously, display attributes were concatenated to a single string before 64 | being checked against a regex. Now, display attributes are independently 65 | compared with the regex. Refer to the 66 | [documentation](https://w0lff.gitlab.io/shikane/shikane.5.html) of the 67 | `search` field for usage instructions. 68 | - Parsing of regexes in config.toml. 69 | Previously ignored trailing slashes will now be seen as part of the regex. 70 | See below on how to migrate to the new `search` field syntax. 71 | - Update documentation to describe variants, `search`es and `mode` parsing. 72 | - Default log level to `warn` 73 | - Accept an empty config.toml file 74 | - Update MSRV to 1.70 75 | 76 | ### Deprecated 77 | 78 | - config: Defining a `position` as a table `{ x = 0, y = 0 }`. 79 | Use a string instead `"x,y"`. 80 | 81 | ### Removed 82 | 83 | - config: The `match` field has been replaced by the `search` field. 84 | 85 | #### Migration from [0.2.0] 86 | 87 | Use the commands below in order to migrate from the `match` field syntax 88 | to the `search` field syntax. 89 | 90 | **Note:** Due to the mentioned changes in how display attributes are supplied 91 | to regexes, your regexes might not work even after removing the trailing slashes. 92 | 93 | ```shell 94 | # First, remove the trailing slash from regexes in the toml file. 95 | sed -r -i 's#match.*=.*"/(.*)/"#match = "/\1"#' path/to/shikane/config.toml 96 | # Second, rename all match fields to search fields. 97 | sed -r -i 's#match.*=.*"(.*)"#search = "\1"#' path/to/shikane/config.toml 98 | ``` 99 | 100 | ## [0.2.0] - 2023-04-30 101 | 102 | ### Added 103 | 104 | - Support for `adaptive_sync` option 105 | 106 | ## [0.1.2] - 2023-04-29 107 | 108 | ### Fixed 109 | 110 | - docs: Missing version increment 111 | 112 | ## [0.1.1] - 2023-04-29 113 | 114 | ### Added 115 | 116 | - docs: Add acknowledgements, usage and feature comparison 117 | 118 | ## [0.1.0] - 2023-02-10 119 | 120 | ### Added 121 | 122 | - shikane daemon 123 | - documentation in man pages 124 | - MIT License 125 | 126 | 127 | [1.0.1]: https://gitlab.com/w0lff/shikane/-/compare/v1.0.0...v1.0.1 128 | [1.0.0]: https://gitlab.com/w0lff/shikane/-/compare/v0.2.0...v1.0.0 129 | [0.2.0]: https://gitlab.com/w0lff/shikane/-/compare/v0.1.2...v0.2.0 130 | [0.1.2]: https://gitlab.com/w0lff/shikane/-/compare/v0.1.1...v0.1.2 131 | [0.1.1]: https://gitlab.com/w0lff/shikane/-/compare/v0.1.0...v0.1.1 132 | [0.1.0]: https://gitlab.com/w0lff/shikane/releases/tag/v0.1.0 133 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shikane" 3 | version = "1.0.1" 4 | edition = "2021" 5 | authors = ["Hendrik Wolff "] 6 | repository = "https://gitlab.com/w0lff/shikane" 7 | description = "dynamic output configuration tool focusing on accuracy and determinism" 8 | license = "MIT" 9 | readme = "README.md" 10 | rust-version = "1.70" 11 | categories = ["command-line-utilities"] 12 | keywords = ["wayland", "wlroots", "sway"] 13 | 14 | [dependencies] 15 | calloop = "0.13.0" 16 | calloop-wayland-source = "0.3.0" 17 | clap = { version = "4", features = ["derive"] } 18 | env_logger = "0.10.2" 19 | hopcroft-karp = "0.2.1" 20 | itertools = "0.12.1" 21 | log = "0.4.21" 22 | regex = "1.10.4" 23 | ron = "0.8.1" 24 | serde = { version = "1.0.201", features = ["derive"] } 25 | snafu = "0.7.5" 26 | toml = { version = "0.5.11", features = ["preserve_order"] } 27 | wayland-client = { version = "=0.31.2", features = ["log"] } 28 | wayland-protocols-wlr = { version = "=0.2.0", features = ["client"] } 29 | xdg = "2.5.2" 30 | 31 | [dev-dependencies] 32 | rstest = "0.19.0" 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Hendrik Wolff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # shikane 2 | shikane (/ʃiˈkaːnə/) is a dynamic output configuration tool focusing on accuracy and determinism. 3 | 4 | It automatically detects and configures connected displays based on a set of 5 | profiles. Each profile specifies a set of outputs with additional parameters 6 | (e.g., mode, position, scale). A profile will be applied automatically if all 7 | specified outputs and modes can be *perfectly* matched to the currently 8 | connected displays and their capabilities. 9 | 10 | This is a Wayland equivalent for tools like [autorandr]. 11 | It aims to fully replace [kanshi], surpass its inaccuracies and add new features. 12 | shikane works with Wayland compositors supporting versions >=3 of the 13 | wlr-output-management protocol (e.g., compositors using wlroots v0.16). 14 | 15 | ## Features 16 | - generation of *all* compatible (display, output, mode)-combinations, ranked by exactness 17 | - specify multiple matching rules per output 18 | - restrict the matching to only certain display attributes 19 | - choose between regex, substring or full text based attribute matching 20 | - full cardinality matching algorithm 21 | - ad-hoc profile switching 22 | - export current display setup as shikane config.toml 23 | - state machine defined execution 24 | - execute commands, profile and display names are supplied as env vars 25 | - one-shot mode 26 | 27 | ## How the matching process works 28 | shikane selects possible **profile**s automatically at startup and when a change 29 | in the set of currently connected displays occurs. 30 | A **profile** is taken into consideration if every currently connected display 31 | can be matched to at least one **output** and no **output** is unmatched.\ 32 | A display matches an **output** if: 33 | 34 | - the **search** parameter matches against the properties of the display 35 | - AND the display supports the **mode** that is specified in the **output** 36 | table. 37 | 38 | After assembling a list of possible **profile**s shikane generates all variants 39 | of every **profile**. Once all variants have been verified and sorted by 40 | exactness, shikane tries to apply them one after the other until one succeeds 41 | or there are no variants left to try. 42 | 43 | Variants are slightly different versions of the same **profile**.\ 44 | For example, a given display has a set of supported modes: 1920x1080@60Hz and 45 | 1920x1080@50Hz. If the **mode** in the config.toml is specified as "1920x1080" 46 | both modes would fit the specification. Instead of choosing just one mode and 47 | using that, shikane takes both into account by generating two variants based on 48 | the same **profile**. One variant uses the 1920x1080@60Hz mode and the other 49 | variant uses the 1920x1080@50Hz mode.\ 50 | The same goes for the search parameter. If multiple 51 | (display,**output**,mode)-combinations are possible, shikane generates variants with 52 | all of them. 53 | 54 | ## Usage 55 | 1. Create your configuration file. 56 | See [configuration](#configuration) for a short overview 57 | or have a look at the man page `man 5 shikane` for more detailed information. 58 | 2. Start shikane. 59 | ```sh 60 | shikane 61 | ``` 62 | 63 | ### Using `shikanectl` to generate configurations 64 | 1. Start shikane. 65 | 2. Configure your outputs by hand with a (GUI) tool. 66 | 3. Export the current configuration. 67 | ```sh 68 | shikanectl export "room04" 69 | ``` 70 | 4. Append the printed [TOML] to shikanes config.toml. 71 | 5. Reload the config file. 72 | ```sh 73 | shikanectl reload 74 | ``` 75 | 76 | ## Documentation 77 | Documentation is provided as man pages: 78 | ```sh 79 | man 1 shikane 80 | man 5 shikane 81 | man 1 shikanectl 82 | ``` 83 | They are also [viewable online]. 84 | 85 | ## Installation 86 | Via your `$AURhelper` from the [AUR]: 87 | ```sh 88 | $AURhelper -S shikane 89 | ``` 90 | 91 | Via cargo from [crates.io] (without man pages): 92 | ```sh 93 | cargo install shikane 94 | ``` 95 | 96 | ## Building 97 | Dependencies: 98 | - a rust toolchain >=1.70 99 | - pandoc (for building the man pages) 100 | 101 | Building shikane: 102 | ```sh 103 | cargo build --release 104 | ``` 105 | 106 | Building the man pages: 107 | ```sh 108 | ./scripts/build-docs.sh man 109 | 110 | man -l build/man/shikane.1.gz 111 | man -l build/man/shikane.5.gz 112 | man -l build/man/shikanectl.1.gz 113 | ``` 114 | 115 | ## Configuration 116 | shikane uses the [TOML] file format for its configuration file 117 | and contains an array of **profile**s. Each **profile** is a table containing an 118 | array of **output** tables. The configuration file should be placed at 119 | `$XDG_CONFIG_HOME/shikane/config.toml`. 120 | 121 | ```toml 122 | [[profile]] 123 | name = "dual_foo" 124 | exec = ["notify-send shikane \"Profile $SHIKANE_PROFILE_NAME has been applied\""] 125 | [[profile.output]] 126 | # search for a matching serial number and model by full text comparison 127 | search = ["s=SERIAL123", "m=1Q2X5T"] 128 | enable = true 129 | mode = "1920x1080@50" 130 | position = "0,0" 131 | scale = 1.3 132 | 133 | [[profile.output]] 134 | search = "n/HDMI-[ABC]-[1-9]" # search for a matching name by regex 135 | enable = true 136 | exec = ["echo This is output $SHIKANE_OUTPUT_NAME"] 137 | mode = "2560x1440@75Hz" 138 | position = "1920,0" 139 | transform = "270" 140 | ``` 141 | 142 | ## Acknowledgements 143 | - [kanshi] being the inspiration and motivation 144 | - [wayland-rs] providing the wayland bindings 145 | 146 | ## License 147 | MIT 148 | 149 | 150 | [AUR]: https://aur.archlinux.org/packages/shikane 151 | [autorandr]: https://github.com/phillipberndt/autorandr 152 | [crates.io]: https://crates.io/crates/shikane 153 | [kanshi]: https://sr.ht/~emersion/kanshi 154 | [TOML]: https://toml.io 155 | [viewable online]: https://w0lff.gitlab.io/shikane 156 | [wayland-rs]: https://github.com/Smithay/wayland-rs 157 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # shikane man pages 2 | 3 | - [shikane.1](shikane.1.md) 4 | - [shikane.5](shikane.5.md) 5 | - [shikanectl.1](shikanectl.1.md) 6 | -------------------------------------------------------------------------------- /docs/meta/links-to-html.lua: -------------------------------------------------------------------------------- 1 | function Link(link) 2 | link.target = string.gsub(link.target, "%.md", ".html") 3 | return link 4 | end 5 | -------------------------------------------------------------------------------- /docs/meta/pandoc.html.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | $for(author-meta)$ 8 | 9 | $endfor$ 10 | $if(date-meta)$ 11 | 12 | $endif$ 13 | $if(keywords)$ 14 | 15 | $endif$ 16 | $if(description-meta)$ 17 | 18 | $endif$ 19 | $if(title-prefix)$$title-prefix$ – $endif$$pagetitle$ 20 | 23 | $for(css)$ 24 | 25 | $endfor$ 26 | $for(header-includes)$ 27 | $header-includes$ 28 | $endfor$ 29 | $if(math)$ 30 | $if(mathjax)$ 31 | 32 | $endif$ 33 | $math$ 34 | $endif$ 35 | 36 | 37 | $for(include-before)$ 38 | $include-before$ 39 | $endfor$ 40 | $if(title)$ 41 |
42 |

$title$

43 | $if(subtitle)$ 44 |

$subtitle$

45 | $endif$ 46 | $for(author)$ 47 |

$author$

48 | $endfor$ 49 | $if(date)$ 50 |

$date$

51 | $endif$ 52 | $if(abstract)$ 53 |
54 |
$abstract-title$
55 | $abstract$ 56 |
57 | $endif$ 58 |
59 | $endif$ 60 | $if(toc)$ 61 | 67 | $endif$ 68 | $body$ 69 | $for(include-after)$ 70 | $include-after$ 71 | $endfor$ 72 | 73 | 74 | -------------------------------------------------------------------------------- /docs/meta/pandoc.man.template: -------------------------------------------------------------------------------- 1 | $if(has-tables)$ 2 | '\" t 3 | $endif$ 4 | $if(pandoc-version)$ 5 | .\" Automatically generated by Pandoc $pandoc-version$ 6 | .\" 7 | $endif$ 8 | .\" Define V font for inline verbatim, using C font in formats 9 | .\" that render this, and otherwise B font. 10 | .ie "\f[CB]x\f[]"x" \{\ 11 | . ftr V B 12 | . ftr VI BI 13 | . ftr VB B 14 | . ftr VBI BI 15 | .\} 16 | .el \{\ 17 | . ftr V CR 18 | . ftr VI CI 19 | . ftr VB CB 20 | . ftr VBI CBI 21 | .\} 22 | $if(adjusting)$ 23 | .ad $adjusting$ 24 | $endif$ 25 | $if(header)$ 26 | .TH "$title/nowrap$" "$section/nowrap$" "$date/nowrap$" "$footer/nowrap$" "$header/nowrap$" 27 | $else$ 28 | .TH "$title/nowrap$" "$section/nowrap$" "$date/nowrap$" "$footer/nowrap$" 29 | $endif$ 30 | $if(hyphenate)$ 31 | .hy 32 | $else$ 33 | .nh 34 | $endif$ 35 | $for(header-includes)$ 36 | $header-includes$ 37 | $endfor$ 38 | $for(include-before)$ 39 | $include-before$ 40 | $endfor$ 41 | $body$ 42 | $for(include-after)$ 43 | $include-after$ 44 | $endfor$ 45 | $if(author)$ 46 | .SH AUTHORS 47 | $for(author)$$author$$sep$; $endfor$. 48 | $endif$ 49 | -------------------------------------------------------------------------------- /docs/meta/shikane.metadata.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # man pages 3 | adjusting: l 4 | 5 | # html 6 | maxwidth: 55em 7 | -------------------------------------------------------------------------------- /docs/meta/styles.html: -------------------------------------------------------------------------------- 1 | $if(document-css)$ 2 | html { 3 | $if(mainfont)$ 4 | font-family: $mainfont$; 5 | $endif$ 6 | $if(fontsize)$ 7 | font-size: $fontsize$; 8 | $endif$ 9 | $if(linestretch)$ 10 | line-height: $linestretch$; 11 | $endif$ 12 | color: $if(fontcolor)$$fontcolor$$else$#1a1a1a$endif$; 13 | background-color: $if(backgroundcolor)$$backgroundcolor$$else$#fdfdfd$endif$; 14 | } 15 | body { 16 | margin: 0 auto; 17 | max-width: $if(maxwidth)$$maxwidth$$else$36em$endif$; 18 | padding-left: $if(margin-left)$$margin-left$$else$50px$endif$; 19 | padding-right: $if(margin-right)$$margin-right$$else$50px$endif$; 20 | padding-top: $if(margin-top)$$margin-top$$else$50px$endif$; 21 | padding-bottom: $if(margin-bottom)$$margin-bottom$$else$50px$endif$; 22 | hyphens: auto; 23 | overflow-wrap: break-word; 24 | text-rendering: optimizeLegibility; 25 | font-kerning: normal; 26 | } 27 | @media (max-width: 600px) { 28 | body { 29 | font-size: 0.9em; 30 | padding: 12px; 31 | } 32 | h1 { 33 | font-size: 1.8em; 34 | } 35 | } 36 | @media print { 37 | html { 38 | background-color: $if(backgroundcolor)$$backgroundcolor$$else$white$endif$; 39 | } 40 | body { 41 | background-color: transparent; 42 | color: black; 43 | font-size: 12pt; 44 | } 45 | p, h2, h3 { 46 | orphans: 3; 47 | widows: 3; 48 | } 49 | h2, h3, h4 { 50 | page-break-after: avoid; 51 | } 52 | } 53 | p { 54 | margin: 1em 0; 55 | } 56 | a { 57 | color: $if(linkcolor)$$linkcolor$$else$#1a1a1a$endif$; 58 | } 59 | a:visited { 60 | color: $if(linkcolor)$$linkcolor$$else$#1a1a1a$endif$; 61 | } 62 | img { 63 | max-width: 100%; 64 | } 65 | svg { 66 | height: auto; 67 | max-width: 100%; 68 | } 69 | h1, h2, h3, h4, h5, h6 { 70 | margin-top: 1.4em; 71 | } 72 | h5, h6 { 73 | font-size: 1em; 74 | font-style: italic; 75 | } 76 | h6 { 77 | font-weight: normal; 78 | } 79 | ol, ul { 80 | padding-left: 1.7em; 81 | margin-top: 1em; 82 | } 83 | li > ol, li > ul { 84 | margin-top: 0; 85 | } 86 | blockquote { 87 | margin: 1em 0 1em 1.7em; 88 | padding-left: 1em; 89 | border-left: 2px solid #e6e6e6; 90 | color: #606060; 91 | } 92 | $if(abstract)$ 93 | div.abstract { 94 | margin: 2em 2em 2em 2em; 95 | text-align: left; 96 | font-size: 85%; 97 | } 98 | div.abstract-title { 99 | font-weight: bold; 100 | text-align: center; 101 | padding: 0; 102 | margin-bottom: 0.5em; 103 | } 104 | $endif$ 105 | code { 106 | font-family: $if(monofont)$$monofont$$else$Menlo, Monaco, Consolas, 'Lucida Console', monospace$endif$; 107 | $if(monobackgroundcolor)$ 108 | background-color: $monobackgroundcolor$; 109 | padding: .2em .4em; 110 | $endif$ 111 | font-size: 85%; 112 | margin: 0; 113 | hyphens: manual; 114 | } 115 | pre { 116 | margin: 1em 0; 117 | $if(monobackgroundcolor)$ 118 | background-color: $monobackgroundcolor$; 119 | padding: 1em; 120 | $endif$ 121 | overflow: auto; 122 | } 123 | pre code { 124 | padding: 0; 125 | overflow: visible; 126 | overflow-wrap: normal; 127 | } 128 | .sourceCode { 129 | background-color: transparent; 130 | overflow: visible; 131 | } 132 | hr { 133 | background-color: #1a1a1a; 134 | border: none; 135 | height: 1px; 136 | margin: 1em 0; 137 | } 138 | table { 139 | margin: 1em 0; 140 | border-collapse: collapse; 141 | width: 100%; 142 | overflow-x: auto; 143 | display: block; 144 | font-variant-numeric: lining-nums tabular-nums; 145 | } 146 | table caption { 147 | margin-bottom: 0.75em; 148 | } 149 | tbody { 150 | margin-top: 0.5em; 151 | border-top: 1px solid $if(fontcolor)$$fontcolor$$else$#1a1a1a$endif$; 152 | border-bottom: 1px solid $if(fontcolor)$$fontcolor$$else$#1a1a1a$endif$; 153 | } 154 | th { 155 | border-top: 1px solid $if(fontcolor)$$fontcolor$$else$#1a1a1a$endif$; 156 | padding: 0.25em 0.5em 0.25em 0.5em; 157 | } 158 | td { 159 | padding: 0.125em 0.5em 0.25em 0.5em; 160 | } 161 | header { 162 | margin-bottom: 4em; 163 | text-align: center; 164 | } 165 | #TOC li { 166 | list-style: none; 167 | } 168 | #TOC ul { 169 | padding-left: 1.3em; 170 | } 171 | #TOC > ul { 172 | padding-left: 0; 173 | } 174 | #TOC a:not(:hover) { 175 | text-decoration: none; 176 | } 177 | $endif$ 178 | code{white-space: pre-wrap;} 179 | span.smallcaps{font-variant: small-caps;} 180 | div.columns{display: flex; gap: min(4vw, 1.5em);} 181 | div.column{flex: auto; overflow-x: auto;} 182 | div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;} 183 | /* The extra [class] is a hack that increases specificity enough to 184 | override a similar rule in reveal.js */ 185 | ul.task-list[class]{list-style: none;} 186 | ul.task-list li input[type="checkbox"] { 187 | font-size: inherit; 188 | width: 0.8em; 189 | margin: 0 0.8em 0.2em -1.6em; 190 | vertical-align: middle; 191 | } 192 | $if(quotes)$ 193 | q { quotes: "“" "”" "‘" "’"; } 194 | $endif$ 195 | $if(displaymath-css)$ 196 | .display.math{display: block; text-align: center; margin: 0.5rem auto;} 197 | $endif$ 198 | $if(highlighting-css)$ 199 | /* CSS for syntax highlighting */ 200 | $highlighting-css$ 201 | $endif$ 202 | $if(csl-css)$ 203 | $styles.citations.html()$ 204 | $endif$ 205 | -------------------------------------------------------------------------------- /docs/shikane.1.md: -------------------------------------------------------------------------------- 1 | # NAME 2 | shikane - deterministic dynamic output configuration 3 | 4 | 5 | # SYNOPSIS 6 | **shikane** \ 7 | **shikane** \[**-hV**\] \[**-o**\] \[**-c** *file*\] \ 8 | **shikane** \[**\--oneshot**\] \[**\--config** *file*\] 9 | 10 | 11 | # DESCRIPTION 12 | shikane (/ʃiˈkaːnə/) is a dynamic output configuration tool focusing on accuracy and determinism. 13 | 14 | It automatically detects and configures connected displays based on a set of 15 | profiles. Each profile specifies a set of outputs with additional parameters 16 | (e.g., mode, position, scale). A profile will be applied automatically if all 17 | specified outputs and modes can be perfectly matched to the currently connected 18 | displays and their capabilities. 19 | (See **shikane**(5) for details.) 20 | 21 | This is a Wayland equivalent for tools like autorandr. 22 | It aims to fully replace kanshi, surpass its inaccuracies and add new features. 23 | shikane works with Wayland compositors supporting versions >=3 of the 24 | wlr-output-management protocol (e.g., compositors using wlroots v0.16). 25 | 26 | 27 | # OPTIONS 28 | **-h**, **\--help** 29 | 30 | : Print help information 31 | 32 | 33 | **-c**, **\--config** *file* 34 | 35 | : Path to a config *file* 36 | 37 | 38 | **-o**, **\--oneshot** 39 | 40 | : Enable oneshot mode 41 | 42 | Exit after a profile has been applied or if no profile was matched 43 | 44 | 45 | **-s**, **\--socket** *path* 46 | 47 | : Override the default path of the IPC socket 48 | 49 | 50 | **-T**, **\--timeout** *timeout* 51 | 52 | : Wait for *timeout* milliseconds before processing changes \[default: 0\] 53 | 54 | 55 | **-V**, **\--version** 56 | 57 | : Print version information 58 | 59 | 60 | # ENVIRONMENT 61 | **SHIKANE_LOG** 62 | 63 | : Controls at what log level shikane and its modules print messages to stderr. 64 | Available log levels are *error*, *warn*, *info*, *debug* and *trace*; 65 | sorted from highest to lowest priority. A lower log level includes messages 66 | from higher ones. Setting **SHIKANE_LOG** to *trace* will let you see 67 | everything. Setting it to *off* disables all logging. 68 | 69 | This variable allows filtering by modules and accepts a comma-separated 70 | list: **SHIKANE_LOG**=*shikane=warn*,*shikane::matching=debug* will only 71 | show *warn*ings and *error*s from shikane except for the matching module 72 | where log message of level *debug* and above are shown. 73 | 74 | Note: The logging output and filtering is not a stable interface and may be 75 | subject to change at any time. 76 | 77 | 78 | **SHIKANE_LOG_STYLE** 79 | 80 | : Controls if colors and styles with ANSI characters are used for log output. 81 | Possible values are *auto*, *always* and *never*. Defaults to *auto*. 82 | 83 | 84 | **SHIKANE_LOG_TIME** 85 | 86 | : Enables logging of prefixed timestamps if set to *1*. 87 | 88 | 89 | # FILES 90 | shikane reads its configuration from **\$XDG_CONFIG_HOME/shikane/config.toml** by 91 | default. The program exits with an error if no config *file* is found. 92 | The config file format is documented in **shikane**(5). 93 | 94 | 95 | # BUGS 96 | Hopefully less than 4. 97 | 98 | 99 | # AUTHORS 100 | Hendrik Wolff 101 | 102 | 103 | # SEE ALSO 104 | **shikane**(5), **shikanectl**(1) 105 | -------------------------------------------------------------------------------- /docs/shikanectl.1.md: -------------------------------------------------------------------------------- 1 | # NAME 2 | shikanectl - control the shikane daemon 3 | 4 | 5 | # SYNOPSIS 6 | **shikanectl** \[options\] \ 7 | 8 | 9 | # OPTIONS 10 | **-h**, **\--help** 11 | 12 | : Print help information 13 | 14 | 15 | **-s**, **\--socket** *socket* 16 | 17 | : Connect to the specified *socket* 18 | 19 | 20 | **-V**, **\--version** 21 | 22 | : Print version information 23 | 24 | 25 | # COMMANDS 26 | **reload** \[*file*\] 27 | 28 | : Reload the daemon configuration file, optionally by providing a different 29 | *file*. 30 | 31 | 32 | **switch** *name* 33 | 34 | : Use the given profile temporarily. 35 | 36 | 37 | **export** \[options\] *name* 38 | 39 | : Export the current display setup as shikane config. Include vendor, model 40 | and serial number in the searches by default. It is recommended to use a 41 | meaningful and unique *name* for the new profile. 42 | 43 | 44 | ## EXPORT OPTIONS 45 | **-d**, **\--description** 46 | 47 | : Include the description in the searches 48 | 49 | 50 | **-n**, **\--name** 51 | 52 | : Include the name in the searches 53 | 54 | 55 | **-m**, **\--model** 56 | 57 | : Include the model in the searches 58 | 59 | 60 | **-s**, **\--serial** 61 | 62 | : Include the serial number in the searches 63 | 64 | 65 | **-v**, **\--vendor** 66 | 67 | : Include the vendor in the searches 68 | 69 | 70 | # EXAMPLES 71 | Using `shikanectl export` to append the current output setup as a new profile 72 | to your existing configuration. Just replace the profile name with something 73 | meaningful and unique. 74 | ```shell 75 | shikanectl export "NEW_PROFILE_NAME" >> $XDG_CONFIG_HOME/shikane/config.toml 76 | ``` 77 | 78 | 79 | # BUGS 80 | Hopefully less than 4. 81 | 82 | 83 | # AUTHORS 84 | Hendrik Wolff 85 | 86 | 87 | # SEE ALSO 88 | **shikane**(1) 89 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "crane": { 4 | "inputs": { 5 | "nixpkgs": [ 6 | "nixpkgs" 7 | ] 8 | }, 9 | "locked": { 10 | "lastModified": 1716745752, 11 | "narHash": "sha256-8K1R9Yg4r08rYk86Yq+lu3E9L3uRUb4xMqYHgl0VGS0=", 12 | "owner": "ipetkov", 13 | "repo": "crane", 14 | "rev": "19ca94ec2d288de334ae932107816b4a97736cd8", 15 | "type": "github" 16 | }, 17 | "original": { 18 | "owner": "ipetkov", 19 | "repo": "crane", 20 | "type": "github" 21 | } 22 | }, 23 | "fenix": { 24 | "inputs": { 25 | "nixpkgs": [ 26 | "nixpkgs" 27 | ], 28 | "rust-analyzer-src": "rust-analyzer-src" 29 | }, 30 | "locked": { 31 | "lastModified": 1716704729, 32 | "narHash": "sha256-Yk0L1JdBTdC9ZtDreqcMMolOtTp0XnPjrACT8oTw2Wg=", 33 | "owner": "nix-community", 34 | "repo": "fenix", 35 | "rev": "aaa27b4cf3729b6562cd4dd65ba24eeda3731002", 36 | "type": "github" 37 | }, 38 | "original": { 39 | "owner": "nix-community", 40 | "repo": "fenix", 41 | "type": "github" 42 | } 43 | }, 44 | "flake-utils": { 45 | "inputs": { 46 | "systems": "systems" 47 | }, 48 | "locked": { 49 | "lastModified": 1710146030, 50 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 51 | "owner": "numtide", 52 | "repo": "flake-utils", 53 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 54 | "type": "github" 55 | }, 56 | "original": { 57 | "owner": "numtide", 58 | "repo": "flake-utils", 59 | "type": "github" 60 | } 61 | }, 62 | "nixpkgs": { 63 | "locked": { 64 | "lastModified": 1716715802, 65 | "narHash": "sha256-usk0vE7VlxPX8jOavrtpOqphdfqEQpf9lgedlY/r66c=", 66 | "owner": "NixOS", 67 | "repo": "nixpkgs", 68 | "rev": "e2dd4e18cc1c7314e24154331bae07df76eb582f", 69 | "type": "github" 70 | }, 71 | "original": { 72 | "owner": "NixOS", 73 | "ref": "nixpkgs-unstable", 74 | "repo": "nixpkgs", 75 | "type": "github" 76 | } 77 | }, 78 | "root": { 79 | "inputs": { 80 | "crane": "crane", 81 | "fenix": "fenix", 82 | "flake-utils": "flake-utils", 83 | "nixpkgs": "nixpkgs" 84 | } 85 | }, 86 | "rust-analyzer-src": { 87 | "flake": false, 88 | "locked": { 89 | "lastModified": 1716572615, 90 | "narHash": "sha256-mVUbarr4PNjERDk+uaoitPq7eL7De0ythZehezAzug8=", 91 | "owner": "rust-lang", 92 | "repo": "rust-analyzer", 93 | "rev": "a55e8bf09cdfc25066b77823cc98976a51af8a8b", 94 | "type": "github" 95 | }, 96 | "original": { 97 | "owner": "rust-lang", 98 | "ref": "nightly", 99 | "repo": "rust-analyzer", 100 | "type": "github" 101 | } 102 | }, 103 | "systems": { 104 | "locked": { 105 | "lastModified": 1681028828, 106 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 107 | "owner": "nix-systems", 108 | "repo": "default", 109 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 110 | "type": "github" 111 | }, 112 | "original": { 113 | "owner": "nix-systems", 114 | "repo": "default", 115 | "type": "github" 116 | } 117 | } 118 | }, 119 | "root": "root", 120 | "version": 7 121 | } 122 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 4 | crane.url = "github:ipetkov/crane"; 5 | crane.inputs.nixpkgs.follows = "nixpkgs"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | fenix = { 8 | url = "github:nix-community/fenix"; 9 | inputs.nixpkgs.follows = "nixpkgs"; 10 | }; 11 | }; 12 | 13 | outputs = { self, nixpkgs, crane, flake-utils, fenix, ... }@inputs: 14 | flake-utils.lib.eachDefaultSystem 15 | (system: 16 | let 17 | pkgs = import nixpkgs { 18 | inherit system; 19 | }; 20 | 21 | fenixChannel = fenix.packages.${system}.stable; 22 | 23 | fenixToolchain = (fenixChannel.withComponents [ 24 | "rustc" 25 | "cargo" 26 | "rustfmt" 27 | "clippy" 28 | "rust-analysis" 29 | "rust-src" 30 | "llvm-tools-preview" 31 | ]); 32 | craneLib = (crane.mkLib nixpkgs.legacyPackages.${system}).overrideToolchain fenixToolchain; 33 | shikane = pkgs.callPackage ./nix-modules/shikane.nix { inherit craneLib pkgs; }; 34 | in 35 | rec 36 | { 37 | packages = { 38 | default = shikane.default; 39 | shikane = shikane.shikane; 40 | shikane-docs = shikane.shikane-docs; 41 | }; 42 | 43 | devShells.default = pkgs.mkShell { 44 | nativeBuildInputs = (with packages.shikane; nativeBuildInputs ++ buildInputs) ++ [ fenixToolchain ]; 45 | RUST_SRC_PATH = "${fenixChannel.rust-src}/lib/rustlib/src/rust/library"; 46 | }; 47 | }) // { 48 | overlays.default = (final: prev: { 49 | inherit (self.packages.${prev.system}) 50 | shikane; 51 | }); 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /nix-modules/shikane.nix: -------------------------------------------------------------------------------- 1 | { craneLib, pkgs, ... }: 2 | 3 | let 4 | commonArgs = { 5 | src = craneLib.cleanCargoSource (craneLib.path ./..); 6 | cargoVendorDir = craneLib.vendorCargoDeps { cargoLock = ./../Cargo.lock; }; 7 | doCheck = false; 8 | }; 9 | 10 | cargoArtifacts = craneLib.buildDepsOnly (commonArgs // { }); 11 | 12 | shikane-clippy = craneLib.cargoClippy (commonArgs // { 13 | inherit cargoArtifacts; 14 | }); 15 | 16 | shikane = craneLib.buildPackage (commonArgs // { 17 | inherit cargoArtifacts; 18 | }); 19 | 20 | shikane-docs = pkgs.stdenv.mkDerivation { 21 | name = "shikane-docs"; 22 | src = ./..; 23 | nativeBuildInputs = with pkgs; [ pandoc installShellFiles ]; 24 | buildPhase = '' 25 | runHook preBuild 26 | bash scripts/build-docs.sh man ${shikane.version} 27 | bash scripts/build-docs.sh html ${shikane.version} 28 | runHook postBuild 29 | ''; 30 | installPhase = '' 31 | runHook preInstall 32 | installManPage build/man/* 33 | mkdir -p $out/share/doc/shikane/html 34 | mv build/html/* $out/share/doc/shikane/html 35 | mv README.md $out/share/doc/shikane/ 36 | mv CHANGELOG.md $out/share/doc/shikane/ 37 | mkdir -p $out/share/licenses/shikane/ 38 | mv LICENSE $out/share/licenses/shikane/ 39 | runHook postInstall 40 | ''; 41 | }; 42 | in 43 | { 44 | default = pkgs.symlinkJoin { 45 | name = "shikane"; 46 | paths = [ shikane shikane-docs ]; 47 | }; 48 | shikane = shikane; 49 | shikane-docs = shikane-docs; 50 | shikane-clippy = shikane-clippy; 51 | } 52 | -------------------------------------------------------------------------------- /scripts/build-docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | shikane_dir=$(readlink -f -- "$(dirname "$(readlink -f -- "$0")")"/../) 5 | docs="$shikane_dir/docs" 6 | meta="$docs/meta" 7 | out="$shikane_dir/build" 8 | cargo_version="$(grep -m 1 version "${shikane_dir}"/Cargo.toml | cut -d\" -f2)" 9 | manpages=("shikane.1" "shikane.5" "shikanectl.1") 10 | common_opts=( 11 | --standalone 12 | --from markdown 13 | --metadata-file "$meta/shikane.metadata.yml" 14 | -M "date=$(date +%F)" 15 | ) 16 | 17 | 18 | ## Build man pages from markdown files, gzip and write them to $out/man directory 19 | ## $1: shikane version included in the man pages 20 | buildman() { 21 | local version="$1" 22 | local out="$out/man" 23 | local man_opts=( 24 | "${common_opts[@]}" 25 | --to man 26 | --template "$meta/pandoc.man.template" 27 | -M "footer=shikane $version" 28 | ) 29 | 30 | mkdir -p "$out" 31 | for page in "${manpages[@]}"; do 32 | local page_section="${page/#*./}" 33 | local title="${page/%.*/}" 34 | local page_opts=( 35 | "${man_opts[@]}" 36 | -V section="$page_section" 37 | -V title="$title" 38 | "$docs/$page.md" 39 | -o "$out/$page" 40 | ) 41 | pandoc "${page_opts[@]}" 42 | gzip -9 -f "$out/$page" 43 | done 44 | } 45 | 46 | ## Build html man pages from markdown files and write them to $out/html directory 47 | ## $1: shikane version included in the html man pages 48 | buildhtml() { 49 | local version="$1" 50 | local out="$out/html" 51 | local html_opts=( 52 | "${common_opts[@]}" 53 | --embed-resources 54 | --to html 55 | --template "$meta/pandoc.html.template" 56 | --email-obfuscation=javascript 57 | -M title=shikane 58 | -M "subtitle=shikane $version" 59 | ) 60 | 61 | mkdir -p "$out" 62 | for page in "${manpages[@]}"; do 63 | local page_section="${page/#*./}" 64 | local title="${page/%.*/}" 65 | local page_opts=( 66 | "${html_opts[@]}" 67 | -V title="$title($page_section)" 68 | "$docs/$page.md" 69 | -o "$out/$page.html" 70 | ) 71 | pandoc "${page_opts[@]}" 72 | done 73 | pandoc "${html_opts[@]}" --lua-filter="$meta/links-to-html.lua" "$docs/index.md" -o "$out/index.html" 74 | } 75 | 76 | 77 | cleanman() { 78 | rm -rf "$out" 79 | } 80 | 81 | usage() { 82 | cat << EOF 83 | Usage: build-docs.sh 84 | 85 | Commands: 86 | man [VERSION] - build man pages [explicitly use string] 87 | html [VERSION] - build html pages [explicitly use string] 88 | clean - remove build artifacts 89 | EOF 90 | } 91 | 92 | 93 | if [[ $# -eq 1 || $# -eq 2 ]]; then 94 | case $1 in 95 | clean) 96 | cleanman 97 | exit 0;; 98 | man) 99 | if [[ $# -eq 2 ]]; then 100 | buildman "$2" 101 | else 102 | buildman "$cargo_version" 103 | fi 104 | exit 0 105 | ;; 106 | html) 107 | if [[ $# -eq 2 ]]; then 108 | buildhtml "$2" 109 | else 110 | buildhtml "$cargo_version" 111 | fi 112 | exit 0 113 | ;; 114 | *) 115 | usage 116 | exit 1;; 117 | esac 118 | fi 119 | 120 | usage 121 | exit 1 122 | -------------------------------------------------------------------------------- /src/bin/shikane.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | shikane::util::setup_logging(); 3 | shikane::daemon::daemon(None); 4 | } 5 | -------------------------------------------------------------------------------- /src/bin/shikanectl.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | shikane::util::setup_logging(); 3 | shikane::client::client(None); 4 | } 5 | -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | mod args; 2 | 3 | use std::collections::VecDeque; 4 | 5 | use clap::Parser; 6 | #[allow(unused_imports)] 7 | use log::{debug, error, info, trace, warn}; 8 | use snafu::{prelude::*, Location}; 9 | 10 | use crate::client::args::IncludeSearchFields; 11 | use crate::daemon::profile_manager::ProfileManager; 12 | use crate::error; 13 | use crate::ipc::{IpcRequest, IpcResponse, IpcStream}; 14 | use crate::matching::MatchReport; 15 | use crate::profile::{ConvertError, ConverterSettings, Profile}; 16 | use crate::wl_backend::WlHead; 17 | 18 | pub use self::args::ShikaneCtl; 19 | 20 | pub fn client(args: Option) { 21 | let args = match args { 22 | Some(args) => args, 23 | None => ShikaneCtl::parse(), 24 | }; 25 | if let Err(err) = run(args) { 26 | error!("{}", error::report(err.as_ref())) 27 | } 28 | } 29 | 30 | fn run(args: ShikaneCtl) -> Result<(), Box> { 31 | let mut ipc = match args.socket { 32 | Some(ref socket) => IpcStream::connect_to(socket)?, 33 | None => IpcStream::connect()?, 34 | }; 35 | 36 | let request: IpcRequest = args.cmd.clone().into(); 37 | ipc.send(&request)?; 38 | let response: IpcResponse = ipc.recv()?; 39 | trace!("{response:?}"); 40 | 41 | match response { 42 | IpcResponse::CurrentHeads(heads) => print_current_configuration(args, heads)?, 43 | IpcResponse::Error(err) => error!("{err}"), 44 | IpcResponse::Generic(s) => println!("{s}"), 45 | IpcResponse::MatchReports(reports) => print_match_reports(reports), 46 | IpcResponse::Success => {} 47 | } 48 | 49 | Ok(()) 50 | } 51 | 52 | fn print_current_configuration( 53 | args: ShikaneCtl, 54 | heads: VecDeque, 55 | ) -> Result<(), ClientError> { 56 | let (profile_name, search_fields) = match args.cmd { 57 | args::Command::Export(cmd_export) => { 58 | let sf = cmd_export 59 | .search_fields 60 | .unwrap_or(IncludeSearchFields::default()) 61 | .into(); 62 | (cmd_export.profile_name, sf) 63 | } 64 | _ => return CommandResponseMismatchCtx {}.fail(), 65 | }; 66 | 67 | let settings = ConverterSettings::default() 68 | .profile_name(profile_name) 69 | .include_search_fields(search_fields) 70 | .converter() 71 | .run(heads) 72 | .context(ConvertCtx)?; 73 | println!("{settings}"); 74 | Ok(()) 75 | } 76 | 77 | fn print_match_reports(reports: VecDeque) { 78 | let variants = ProfileManager::collect_variants_from_reports(&reports); 79 | let mut prev_profile: Option = None; 80 | println!("total valid variants: {}", variants.len()); 81 | for v in variants { 82 | match prev_profile { 83 | Some(ref profile) if *profile == v.profile => {} 84 | _ => { 85 | println!("{:?}", v.profile.name); 86 | prev_profile = Some(v.profile.clone()); 87 | } 88 | } 89 | println!( 90 | "\t(specificity, deviation): ({}, {})", 91 | v.specificity(), 92 | v.mode_deviation() 93 | ); 94 | } 95 | 96 | println!(); 97 | println!("[report specific values]"); 98 | for r in reports { 99 | println!("profile name: {:?}", r.profile.name); 100 | println!("\tunpaired heads: {}", r.unpaired_heads.len()); 101 | println!("\tunpaired outputs: {}", r.unpaired_outputs.len()); 102 | println!("\tunrelated pairings: {}", r.unrelated_pairings.len()); 103 | println!("\tinvalid subsets: {}", r.invalid_subsets.len()); 104 | } 105 | } 106 | 107 | #[derive(Debug, Snafu)] 108 | #[snafu(context(suffix(Ctx)))] 109 | pub enum ClientError { 110 | #[snafu(display("[{location}] Missing data from arguments for handling IPC response"))] 111 | CommandResponseMismatch { location: Location }, 112 | #[snafu(display("[{location}] Cannot convert current output configuration to TOML"))] 113 | Convert { 114 | source: ConvertError, 115 | location: Location, 116 | }, 117 | } 118 | -------------------------------------------------------------------------------- /src/client/args.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::{Args, Parser, Subcommand}; 4 | #[allow(unused_imports)] 5 | use log::{debug, error, info, trace, warn}; 6 | 7 | use crate::ipc::IpcRequest; 8 | use crate::search::SearchField; 9 | 10 | #[derive(Clone, Debug, Parser)] 11 | #[command(version)] 12 | pub struct ShikaneCtl { 13 | #[command(subcommand)] 14 | pub(super) cmd: Command, 15 | 16 | /// Connect to the specified socket 17 | #[clap(long, short)] 18 | pub(super) socket: Option, 19 | } 20 | 21 | #[derive(Clone, Debug, Subcommand)] 22 | pub enum Command { 23 | #[clap(subcommand, alias = "dbg", hide = true)] 24 | Debug(CmdDebug), 25 | Switch(CmdSwitch), 26 | Reload(CmdReload), 27 | Export(CmdExport), 28 | } 29 | 30 | /// Subcommand for debugging shikane and its configuration. 31 | /// This interface is not stable and could change any time. 32 | #[derive(Clone, Debug, Subcommand)] 33 | pub enum CmdDebug { 34 | /// Print the current state of the daemon state machine 35 | CurrentState, 36 | /// List all valid variants and report data 37 | ListReports, 38 | } 39 | 40 | /// Use the given profile temporarily 41 | #[derive(Clone, Debug, Args)] 42 | pub struct CmdSwitch { 43 | /// Name of the profile 44 | name: String, 45 | } 46 | 47 | /// Reload the configuration file 48 | #[derive(Clone, Debug, Args)] 49 | pub struct CmdReload { 50 | /// Use this file instead of the current config file 51 | file: Option, 52 | } 53 | 54 | /// Export the current display setup as shikane config. 55 | /// Include vendor, model and serial number in the searches by default. 56 | #[derive(Clone, Debug, Args)] 57 | pub struct CmdExport { 58 | #[command(flatten)] 59 | pub search_fields: Option, 60 | 61 | /// Name of the exported profile 62 | pub profile_name: String, 63 | } 64 | 65 | #[derive(Clone, Debug, Parser)] 66 | #[clap(group = clap::ArgGroup::new("include_search_fields").multiple(true))] 67 | pub struct IncludeSearchFields { 68 | /// Include the description in the searches 69 | #[arg(short, long, group = "include_search_fields")] 70 | description: bool, 71 | /// Include the name in the searches 72 | #[arg(short, long, group = "include_search_fields")] 73 | name: bool, 74 | /// Include the model in the searches 75 | #[arg(short, long, group = "include_search_fields")] 76 | model: bool, 77 | /// Include the serial number in the searches 78 | #[arg(short, long, group = "include_search_fields")] 79 | serial: bool, 80 | /// Include the vendor in the searches 81 | #[arg(short, long, group = "include_search_fields")] 82 | vendor: bool, 83 | } 84 | 85 | impl Default for IncludeSearchFields { 86 | fn default() -> Self { 87 | Self { 88 | description: false, 89 | name: false, 90 | model: true, 91 | serial: true, 92 | vendor: true, 93 | } 94 | } 95 | } 96 | 97 | impl From for Vec { 98 | fn from(value: IncludeSearchFields) -> Self { 99 | let mut sf = vec![]; 100 | if value.description { 101 | sf.push(SearchField::Description); 102 | } 103 | if value.name { 104 | sf.push(SearchField::Name); 105 | } 106 | if value.model { 107 | sf.push(SearchField::Model); 108 | } 109 | if value.serial { 110 | sf.push(SearchField::Serial); 111 | } 112 | if value.vendor { 113 | sf.push(SearchField::Vendor); 114 | } 115 | sf 116 | } 117 | } 118 | 119 | impl From for IpcRequest { 120 | fn from(cmd: Command) -> Self { 121 | match cmd { 122 | Command::Debug(c) => c.into(), 123 | Command::Switch(c) => Self::SwitchProfile(c.name), 124 | Command::Reload(c) => Self::ReloadConfig(c.file), 125 | Command::Export(_) => Self::CurrentHeads, 126 | } 127 | } 128 | } 129 | 130 | impl From for IpcRequest { 131 | fn from(value: CmdDebug) -> Self { 132 | match value { 133 | CmdDebug::CurrentState => Self::CurrentState, 134 | CmdDebug::ListReports => Self::MatchReports, 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/daemon.rs: -------------------------------------------------------------------------------- 1 | pub mod ipc; 2 | pub mod profile_manager; 3 | pub mod state_machine; 4 | 5 | use std::collections::VecDeque; 6 | use std::os::unix::net::UnixListener; 7 | use std::path::PathBuf; 8 | use std::time::Duration; 9 | 10 | use calloop::{timer::Timer, EventLoop, LoopHandle, LoopSignal, RegistrationToken}; 11 | use clap::Parser; 12 | #[allow(unused_imports)] 13 | use log::{debug, error, info, trace, warn}; 14 | use snafu::{prelude::*, Location}; 15 | 16 | use crate::daemon::state_machine::DaemonStateMachine; 17 | use crate::error; 18 | use crate::ipc::SocketBindCtx; 19 | use crate::settings::Settings; 20 | use crate::wl_backend::{WlBackend, WlBackendEvent}; 21 | use crate::wlroots::WlrootsBackend; 22 | 23 | type DSMWlroots = DaemonStateMachine; 24 | 25 | #[derive(Debug, Parser)] 26 | #[command(version)] 27 | pub struct ShikaneArgs { 28 | /// Path to config file 29 | #[arg(short, long, value_name = "PATH")] 30 | pub config: Option, 31 | 32 | /// Enable oneshot mode 33 | /// 34 | /// Exit after a profile has been applied or 35 | /// if no profile was matched 36 | #[arg(short, long)] 37 | pub oneshot: bool, 38 | 39 | /// IPC socket path 40 | #[arg(short, long, value_name = "PATH")] 41 | pub socket: Option, 42 | 43 | /// Apply profiles untested 44 | #[arg(short = 't', long, value_parser = clap::builder::BoolishValueParser::new(), action = clap::ArgAction::Set, default_value = "false", hide = true)] 45 | pub skip_tests: bool, 46 | 47 | /// Wait for TIMEOUT milliseconds before processing changes 48 | /// 49 | /// Usually you should not set this as it slows down shikane. 50 | #[arg(short = 'T', long, default_value_t = 0)] 51 | pub timeout: u64, 52 | } 53 | 54 | pub struct Shikane<'a, B: WlBackend> { 55 | dsm: DaemonStateMachine, 56 | event_queue: VecDeque, 57 | el_handle: LoopHandle<'a, Shikane<'a, B>>, 58 | loop_signal: LoopSignal, 59 | timeout_token: RegistrationToken, 60 | } 61 | 62 | pub fn daemon(args: Option) { 63 | let args = match args { 64 | Some(args) => args, 65 | None => ShikaneArgs::parse(), 66 | }; 67 | if let Err(err) = run(args) { 68 | error!("{}", error::report(err.as_ref())) 69 | } 70 | } 71 | 72 | fn run(args: ShikaneArgs) -> Result<(), Box> { 73 | let arg_socket_path = args.socket.clone(); 74 | let settings = Settings::from_args(args); 75 | 76 | let (wlroots_backend, wl_source) = WlrootsBackend::connect()?; 77 | let mut event_loop: EventLoop> = 78 | EventLoop::try_new().context(ELCreateCtx)?; 79 | let el_handle = event_loop.handle(); 80 | 81 | // wayland backend 82 | el_handle 83 | .insert_source(wl_source, move |_, event_queue, shikane| { 84 | let dispatch_result = event_queue.dispatch_pending(&mut shikane.dsm.backend); 85 | let n = match dispatch_result { 86 | Ok(n) => n, 87 | Err(ref err) => { 88 | error!("{}", error::report(&err)); 89 | return dispatch_result; 90 | } 91 | }; 92 | trace!("dispatched {n} wayland events"); 93 | 94 | let mut eq = shikane.dsm.backend.drain_event_queue(); 95 | shikane.event_queue.append(&mut eq); 96 | let mut timeout = Duration::from_millis(0); 97 | // delay processing only on change 98 | if shikane 99 | .event_queue 100 | .contains(&WlBackendEvent::AtomicChangeDone) 101 | { 102 | timeout = shikane.dsm.settings.timeout; 103 | trace!("delay processing by {:?}", timeout); 104 | } 105 | trace!( 106 | "inserting new {:?} timer, replacing old {:?}", 107 | timeout, 108 | shikane.timeout_token 109 | ); 110 | shikane.el_handle.remove(shikane.timeout_token); 111 | match insert_timer(&shikane.el_handle, timeout) { 112 | Ok(token) => shikane.timeout_token = token, 113 | Err(err) => error!("{}", error::report(&err)), 114 | } 115 | dispatch_result 116 | }) 117 | .context(InsertCtx)?; 118 | 119 | // IPC socket 120 | let socket_path = match arg_socket_path { 121 | Some(path) => path, 122 | None => crate::util::get_socket_path()?, 123 | }; 124 | clean_up_socket(&socket_path); 125 | trace!("Binding socket to {:?}", socket_path.to_string_lossy()); 126 | let listener = UnixListener::bind(&socket_path).context(SocketBindCtx { path: socket_path })?; 127 | let socket_event_source = 128 | calloop::generic::Generic::new(listener, calloop::Interest::READ, calloop::Mode::Level); 129 | el_handle 130 | .insert_source(socket_event_source, move |_, listener, shikane| { 131 | if let Err(err) = ipc::handle_listener(listener, &shikane.el_handle) { 132 | let err = error::report(&*err); 133 | warn!("{err}"); 134 | } 135 | Ok(calloop::PostAction::Continue) 136 | }) 137 | .context(InsertCtx)?; 138 | 139 | // initial timeout 140 | let timeout_token = insert_timer(&el_handle, settings.timeout)?; 141 | 142 | let dsm = DSMWlroots::new(wlroots_backend, settings); 143 | let loop_signal = event_loop.get_signal(); 144 | let mut shikane = Shikane { 145 | dsm, 146 | event_queue: Default::default(), 147 | el_handle, 148 | loop_signal, 149 | timeout_token, 150 | }; 151 | event_loop 152 | .run( 153 | std::time::Duration::from_millis(500), 154 | &mut shikane, 155 | |shikane| match shikane.dsm.backend.flush() { 156 | Ok(_) => {} 157 | Err(err) => { 158 | error!("backend error on flush: {}", error::report(&err)); 159 | shikane.loop_signal.stop() 160 | } 161 | }, 162 | ) 163 | .context(RunCtx)?; 164 | Ok(()) 165 | } 166 | 167 | fn clean_up_socket(socket_path: &PathBuf) { 168 | match std::fs::remove_file(socket_path) { 169 | Ok(_) => trace!("Deleted stale socket from previous run"), 170 | Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} 171 | Err(err) => { 172 | warn!("Cannot delete socket: {}", err) 173 | } 174 | } 175 | } 176 | 177 | fn insert_timer( 178 | el_handle: &LoopHandle>, 179 | duration: Duration, 180 | ) -> Result> { 181 | let timer = Timer::from_duration(duration); 182 | let timeout_token = el_handle 183 | .insert_source(timer, move |_instant, _, shikane| { 184 | trace!("processing event queue"); 185 | let eq = std::mem::take(&mut shikane.event_queue); 186 | let sm_shutdown = shikane.dsm.process_event_queue(eq); 187 | if sm_shutdown { 188 | trace!("stopping event loop"); 189 | shikane.loop_signal.stop(); 190 | } 191 | calloop::timer::TimeoutAction::Drop 192 | }) 193 | .context(InsertCtx)?; 194 | Ok(timeout_token) 195 | } 196 | 197 | #[derive(Debug, Snafu)] 198 | #[snafu(context(suffix(Ctx)))] 199 | #[snafu(visibility(pub(crate)))] 200 | pub(crate) enum EventLoopSetupError { 201 | #[snafu(display("[{location}] An error occurred while running the event loop"))] 202 | Run { 203 | source: calloop::Error, 204 | location: Location, 205 | }, 206 | #[snafu(display("[{location}] Cannot create new event loop"))] 207 | ELCreate { 208 | source: calloop::Error, 209 | location: Location, 210 | }, 211 | } 212 | 213 | #[derive(Debug, Snafu)] 214 | #[snafu(context(suffix(Ctx)))] 215 | #[snafu(visibility(pub(crate)))] 216 | pub(crate) enum EventLoopInsertError 217 | where 218 | T: 'static, 219 | { 220 | #[snafu(display("[{location}] Cannot insert event source into event loop"))] 221 | Insert { 222 | source: calloop::InsertError, 223 | location: Location, 224 | }, 225 | } 226 | -------------------------------------------------------------------------------- /src/daemon/ipc.rs: -------------------------------------------------------------------------------- 1 | use std::os::unix::net::UnixListener; 2 | use std::path::PathBuf; 3 | 4 | use calloop::LoopHandle; 5 | #[allow(unused_imports)] 6 | use log::{debug, error, info, trace, warn}; 7 | use snafu::prelude::*; 8 | 9 | use crate::daemon::InsertCtx; 10 | use crate::error; 11 | use crate::ipc::{IpcRequest, IpcResponse, IpcStream, SocketAcceptCtx}; 12 | use crate::search::SearchPattern; 13 | use crate::wl_backend::WlBackend; 14 | 15 | use super::profile_manager::{ProfileManager, Restriction}; 16 | use super::state_machine::{DSMState, DaemonStateMachine}; 17 | use super::Shikane; 18 | 19 | type Dsm = DaemonStateMachine; 20 | 21 | pub fn handle_listener( 22 | listener: &UnixListener, 23 | el_handle: &LoopHandle>, 24 | ) -> Result<(), Box> { 25 | let (stream, _) = listener.accept().context(SocketAcceptCtx)?; 26 | let ipc: IpcStream = stream.into(); 27 | let stream_event_source = ipc.into_event_source(); 28 | 29 | trace!("[EventLoop] Inserting IPC client"); 30 | el_handle 31 | .insert_source(stream_event_source, |_, stream, shikane| { 32 | // this is safe because the inner stream does not get dropped 33 | // also this source gets immediately removed at the end of this closure 34 | if let Err(err) = handle_client(unsafe { stream.get_mut() }, &mut shikane.dsm) { 35 | let err = error::report(err.as_ref()); 36 | warn!("IPC error({})", err); 37 | } 38 | Ok(calloop::PostAction::Remove) 39 | }) 40 | .context(InsertCtx)?; 41 | Ok(()) 42 | } 43 | 44 | fn handle_client( 45 | ipc: &mut IpcStream, 46 | state: &mut Dsm, 47 | ) -> Result<(), Box> { 48 | let request: IpcRequest = ipc.recv()?; 49 | let response = delegate_command(request, state); 50 | ipc.send(&response)?; 51 | Ok(()) 52 | } 53 | 54 | fn delegate_command(command: IpcRequest, state: &mut Dsm) -> IpcResponse { 55 | match command { 56 | IpcRequest::CurrentHeads => req_current_heads(state), 57 | IpcRequest::CurrentState => req_current_variant(state), 58 | IpcRequest::MatchReports => req_match_reports(state), 59 | IpcRequest::ReloadConfig(path) => req_reload_config(state, path), 60 | IpcRequest::SwitchProfile(pname) => req_switch_profile(state, pname), 61 | } 62 | } 63 | 64 | fn req_current_heads(state: &Dsm) -> IpcResponse { 65 | match state.backend.export_heads() { 66 | Some(heads) => IpcResponse::CurrentHeads(heads), 67 | None => IpcResponse::Error("no heads available".to_string()), 68 | } 69 | } 70 | 71 | fn req_current_variant(state: &Dsm) -> IpcResponse { 72 | match state.state() { 73 | s @ DSMState::VariantApplied(v) | s @ DSMState::VariantInProgress(v) => { 74 | IpcResponse::Generic(format!("{s}: {}", v.profile.name)) 75 | } 76 | s => IpcResponse::Generic(format!("{s}")), 77 | } 78 | } 79 | 80 | fn req_match_reports(state: &Dsm) -> IpcResponse { 81 | let reports = state.pm.reports().clone(); 82 | IpcResponse::MatchReports(reports) 83 | } 84 | 85 | fn req_reload_config(state: &mut Dsm, path: Option) -> IpcResponse { 86 | if let Err(err) = state.settings.reload_config(path) { 87 | return IpcResponse::Error(error::report(err.as_ref()).to_string()); 88 | } 89 | state.pm = ProfileManager::new(state.settings.profiles.clone()); 90 | state.simulate_change(); 91 | IpcResponse::Success 92 | } 93 | 94 | fn req_switch_profile(state: &mut Dsm, profile_name: String) -> IpcResponse { 95 | let restriction: Restriction = SearchPattern::Fulltext(profile_name.clone()).into(); 96 | if !state.pm.test_restriction(&restriction) { 97 | return IpcResponse::Error(format!("No matching profile found {:?}", profile_name)); 98 | } 99 | state.pm.restrict(restriction); 100 | state.simulate_change(); 101 | IpcResponse::Success 102 | } 103 | -------------------------------------------------------------------------------- /src/daemon/profile_manager.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashSet, VecDeque}; 2 | 3 | #[allow(unused_imports)] 4 | use log::{debug, error, info, trace, warn}; 5 | 6 | use crate::matching::{MatchReport, ProfileMatcher}; 7 | use crate::profile::Profile; 8 | use crate::search::SearchPattern; 9 | use crate::variant::ValidVariant; 10 | use crate::wl_backend::{LessEqWlHead, WlHead}; 11 | 12 | #[derive(Debug)] 13 | pub struct ProfileManager { 14 | profiles: VecDeque, 15 | variants: VecDeque, 16 | reports: VecDeque, 17 | restriction: Option, 18 | cached_heads: VecDeque, 19 | } 20 | 21 | #[derive(Clone, Debug)] 22 | pub struct Restriction { 23 | pattern: SearchPattern, 24 | } 25 | 26 | impl ProfileManager { 27 | pub fn new(profiles: VecDeque) -> Self { 28 | Self { 29 | profiles, 30 | variants: Default::default(), 31 | reports: Default::default(), 32 | restriction: Default::default(), 33 | cached_heads: Default::default(), 34 | } 35 | } 36 | pub fn set_profiles(&mut self, profiles: VecDeque) { 37 | self.profiles = profiles 38 | } 39 | pub fn reports(&self) -> &VecDeque { 40 | &self.reports 41 | } 42 | 43 | pub fn next_variant(&mut self) -> Option { 44 | self.variants.pop_front() 45 | } 46 | /// Restrict profile selection to a profile with a name that matches the restriction. 47 | pub fn restrict(&mut self, rest: Restriction) { 48 | self.restriction = Some(rest) 49 | } 50 | /// Lift a previously set restriction. 51 | pub fn lift_restriction(&mut self) -> Option { 52 | self.restriction.take() 53 | } 54 | pub fn test_restriction(&self, rest: &Restriction) -> bool { 55 | self.profiles 56 | .iter() 57 | .any(|p| rest.pattern.matches(&p.name).0) 58 | } 59 | 60 | /// Delete old variants and reports 61 | pub fn clear(&mut self) { 62 | self.variants.clear(); 63 | self.reports.clear(); 64 | self.clear_cached_heads(); 65 | } 66 | fn restricted_profiles(&self) -> VecDeque { 67 | if let Some(ref rest) = self.restriction { 68 | return self 69 | .profiles 70 | .iter() 71 | .filter(|p| rest.pattern.matches(&p.name).0) 72 | .cloned() 73 | .collect(); 74 | } 75 | self.profiles.clone() 76 | } 77 | pub fn generate_variants(&mut self, wl_heads: VecDeque) { 78 | self.cached_heads.clone_from(&wl_heads); 79 | let profiles: VecDeque = self.restricted_profiles(); 80 | self.lift_restriction(); 81 | 82 | for profile in profiles { 83 | let (len_heads, len_outputs) = (wl_heads.len(), profile.outputs.len()); 84 | if len_heads != len_outputs { 85 | continue; 86 | } 87 | debug!( 88 | "len(heads,outputs)=({},{}) profile.name={:?}", 89 | len_heads, len_outputs, profile.name 90 | ); 91 | if let Some(report) = ProfileMatcher::create_report(profile, wl_heads.clone()) { 92 | let (len, pname) = (report.valid_variants.len(), &report.profile.name); 93 | info!("len(valid variants)={} profile.name={:?}", len, pname); 94 | self.reports.push_back(report); 95 | } 96 | } 97 | self.variants = Self::collect_variants_from_reports(&self.reports); 98 | } 99 | 100 | pub fn collect_variants_from_reports( 101 | reports: &VecDeque, 102 | ) -> VecDeque { 103 | let mut variants: VecDeque<_> = reports 104 | .iter() 105 | .flat_map(|r| r.valid_variants.clone()) 106 | .collect(); 107 | variants.make_contiguous().sort_by(|a, b| { 108 | // sort specificity decreasingly and deviation increasingly 109 | (b.specificity(), a.mode_deviation()).cmp(&(a.specificity(), b.mode_deviation())) 110 | }); 111 | 112 | trace!("printing specificity and deviation of sorted valid variants"); 113 | variants.iter().for_each(|v| { 114 | trace!( 115 | "{}:(specificity, deviation):({}, {})", 116 | v.idx_str(), 117 | v.specificity(), 118 | v.mode_deviation() 119 | ) 120 | }); 121 | let (n, l) = (reports.len(), variants.len()); 122 | debug!("len(total valid variants over {n} reports)={l}",); 123 | variants 124 | } 125 | 126 | pub fn is_cache_outdated(&self, wl_heads: &VecDeque) -> bool { 127 | let le_head_a: HashSet = self.cached_heads.iter().map(LessEqWlHead).collect(); 128 | let le_head_b: HashSet = wl_heads.iter().map(LessEqWlHead).collect(); 129 | le_head_a != le_head_b 130 | } 131 | 132 | pub fn clear_cached_heads(&mut self) { 133 | self.cached_heads.clear() 134 | } 135 | } 136 | 137 | impl From for Restriction { 138 | fn from(pattern: SearchPattern) -> Self { 139 | Self { pattern } 140 | } 141 | } 142 | impl From for Restriction { 143 | fn from(r: regex::Regex) -> Self { 144 | Self { pattern: r.into() } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/daemon/state_machine.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | use std::fmt::Display; 3 | 4 | #[allow(unused_imports)] 5 | use log::{debug, error, info, trace, warn}; 6 | 7 | use crate::error; 8 | use crate::execute::CommandBuilder; 9 | use crate::settings::Settings; 10 | use crate::variant::{VSMInput, ValidVariant, VariantAction, VariantState}; 11 | use crate::wl_backend::{WlBackend, WlBackendEvent}; 12 | 13 | use super::profile_manager::ProfileManager; 14 | 15 | pub struct DaemonStateMachine { 16 | state: DSMState, 17 | skip_tests: bool, 18 | pub(crate) pm: ProfileManager, 19 | pub(crate) settings: Settings, 20 | pub(crate) backend: B, 21 | // true if the state machine encountered a shutdown 22 | encountered_shutdown: bool, 23 | } 24 | 25 | #[derive(Clone, Debug, Default)] 26 | pub enum DSMState { 27 | #[default] 28 | NoVariantApplied, 29 | VariantInProgress(ValidVariant), 30 | VariantApplied(ValidVariant), 31 | RestartAfterResponse, 32 | } 33 | 34 | impl DaemonStateMachine { 35 | pub fn new(backend: B, settings: Settings) -> Self { 36 | let state = Default::default(); 37 | info!("Initial daemon state: {}", state); 38 | Self { 39 | state, 40 | skip_tests: settings.skip_tests, 41 | pm: ProfileManager::new(settings.profiles.clone()), 42 | settings, 43 | backend, 44 | encountered_shutdown: false, 45 | } 46 | } 47 | 48 | /// The returned bool tells the caller if the state machine has been shutdown. 49 | /// It cannot and will not keep running once it returns true. 50 | #[must_use] 51 | pub fn process_event_queue(&mut self, eq: VecDeque) -> bool { 52 | if self.has_shutdown() { 53 | return self.has_shutdown(); 54 | } 55 | 56 | if eq.contains(&WlBackendEvent::NeededResourceFinished) { 57 | warn!("A needed resource finished. Shutting down."); 58 | self.state = self.shutdown(); 59 | return self.has_shutdown(); 60 | } 61 | 62 | for event in eq { 63 | self.advance(event); 64 | if self.has_shutdown() { 65 | return self.has_shutdown(); 66 | } 67 | } 68 | 69 | // should be false at this point 70 | self.has_shutdown() 71 | } 72 | pub fn advance(&mut self, event: WlBackendEvent) { 73 | debug!("Advancing daemon state with event: {event}"); 74 | self.state = self.next(event); 75 | info!("New daemon state: {}", self.state); 76 | } 77 | 78 | #[must_use] 79 | fn next(&mut self, event: WlBackendEvent) -> DSMState { 80 | use DSMState::*; 81 | use WlBackendEvent::*; 82 | match (self.state.clone(), event) { 83 | (NoVariantApplied, AtomicChangeDone) => self.restart(), 84 | (NoVariantApplied, NeededResourceFinished) => self.shutdown(), 85 | (NoVariantApplied, Succeeded | Failed | Cancelled) => self.warn_invalid(event), 86 | (VariantInProgress(v), event) => self.advance_variant(v, event), 87 | (VariantApplied(_), AtomicChangeDone) => self.restart(), 88 | (VariantApplied(_), NeededResourceFinished) => self.shutdown(), 89 | (VariantApplied(_), Succeeded | Failed | Cancelled) => self.warn_invalid(event), 90 | (RestartAfterResponse, AtomicChangeDone) => RestartAfterResponse, 91 | (RestartAfterResponse, NeededResourceFinished) => self.shutdown(), 92 | (RestartAfterResponse, Succeeded | Failed | Cancelled) => self.restart(), 93 | } 94 | } 95 | 96 | // This function is only called at [`DSMState::VariantInProgress`]. 97 | fn advance_variant(&mut self, mut v: ValidVariant, event: WlBackendEvent) -> DSMState { 98 | let input = match event { 99 | WlBackendEvent::AtomicChangeDone => VSMInput::AtomicChangeDone, 100 | WlBackendEvent::NeededResourceFinished => { 101 | v.discard(); 102 | return self.shutdown(); 103 | } 104 | WlBackendEvent::Succeeded => VSMInput::Succeeded, 105 | WlBackendEvent::Failed => VSMInput::Failed, 106 | WlBackendEvent::Cancelled => VSMInput::Cancelled, 107 | }; 108 | 109 | let action = v.state.advance(input); 110 | // Set the current variant and variant state as the variant in progress. 111 | // Do the action only with the advanced variant as they rely on the current DSM state! 112 | self.state = DSMState::VariantInProgress(v.clone()); 113 | self.do_action(action, v) 114 | } 115 | 116 | pub fn do_action(&mut self, action: VariantAction, variant: ValidVariant) -> DSMState { 117 | match action { 118 | VariantAction::Restart => self.restart(), 119 | VariantAction::TestVariant => { 120 | if let Err(err) = self.backend.test(&variant) { 121 | warn!("{}", error::report(&err)); 122 | } 123 | DSMState::VariantInProgress(variant) 124 | } 125 | VariantAction::ApplyVariant => { 126 | if let Err(err) = self.backend.apply(&variant) { 127 | warn!("{}", error::report(&err)); 128 | } 129 | DSMState::VariantInProgress(variant) 130 | } 131 | VariantAction::TryNextVariant => self.next_variant(), 132 | VariantAction::ExecCmd => { 133 | self.execute_variant_commands(&variant); 134 | if self.settings.oneshot { 135 | // No return here because the variant is applied. 136 | self.shutdown(); 137 | } 138 | DSMState::VariantApplied(variant) 139 | } 140 | VariantAction::Inert => self.state.clone(), 141 | } 142 | } 143 | 144 | pub fn next_variant(&mut self) -> DSMState { 145 | match self.pm.next_variant() { 146 | None => match self.settings.oneshot { 147 | true => self.shutdown(), 148 | false => DSMState::NoVariantApplied, 149 | }, 150 | Some(mut variant) => { 151 | let action = variant.start(self.skip_tests); 152 | self.do_action(action, variant) 153 | } 154 | } 155 | } 156 | 157 | pub fn restart(&mut self) -> DSMState { 158 | if let DSMState::VariantInProgress(v) = &self.state { 159 | if let VariantState::Testing | VariantState::Applying = v.state { 160 | return DSMState::RestartAfterResponse; 161 | } 162 | } 163 | 164 | if let Some(heads) = self.backend.export_heads() { 165 | // If there is no relevant change, don't restart. 166 | if !self.pm.is_cache_outdated(&heads) { 167 | debug!("No relevant change in heads detected. Not restarting."); 168 | return self.state.clone(); 169 | } 170 | // Else regenerate variants. 171 | self.pm.clear(); 172 | self.pm.generate_variants(heads); 173 | } 174 | self.next_variant() 175 | } 176 | 177 | pub fn shutdown(&mut self) -> DSMState { 178 | self.encountered_shutdown = true; 179 | DSMState::NoVariantApplied 180 | } 181 | 182 | pub fn has_shutdown(&self) -> bool { 183 | self.encountered_shutdown 184 | } 185 | 186 | pub fn state(&self) -> &DSMState { 187 | &self.state 188 | } 189 | 190 | pub fn simulate_change(&mut self) { 191 | debug!("simulating change"); 192 | self.pm.clear_cached_heads(); 193 | self.advance(WlBackendEvent::AtomicChangeDone); 194 | } 195 | 196 | fn execute_variant_commands(&self, variant: &ValidVariant) { 197 | let mut cmdb = CommandBuilder::new(variant.profile.name.clone()); 198 | cmdb.oneshot(self.settings.oneshot); 199 | if let Some(profile_commands) = &variant.profile.commands { 200 | cmdb.profile_commands(profile_commands.clone()) 201 | } 202 | for pairing in variant.pairings.iter() { 203 | if let Some(output_commands) = &pairing.output().commands { 204 | let head_name = pairing.wl_head().name().to_owned(); 205 | cmdb.insert_head_commands(head_name, output_commands.clone()); 206 | } 207 | } 208 | 209 | cmdb.execute(); 210 | } 211 | 212 | /// Prints a warning and does not advance itself, returns its current state. 213 | #[must_use] 214 | fn warn_invalid(&self, event: WlBackendEvent) -> DSMState { 215 | warn!("Received invalid input {event} at state {}", self.state); 216 | self.state.clone() 217 | } 218 | } 219 | 220 | impl Display for DSMState { 221 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 222 | match self { 223 | DSMState::NoVariantApplied => write!(f, "NoVariantApplied"), 224 | DSMState::VariantInProgress(v) => { 225 | write!(f, "VariantInProgress {}:{:?}", v.idx_str(), v.profile.name) 226 | } 227 | DSMState::VariantApplied(v) => { 228 | write!(f, "VariantApplied {}:{:?}", v.idx_str(), v.profile.name) 229 | } 230 | DSMState::RestartAfterResponse => write!(f, "RestartAfterResponse"), 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | #[must_use] 2 | pub(crate) fn report(error: &dyn snafu::Error) -> String { 3 | let sources = snafu::ChainCompat::new(error); 4 | let sources: Vec<&dyn snafu::Error> = sources.collect(); 5 | let sources = sources.iter().rev(); 6 | let mut s = String::new(); 7 | for (i, source) in sources.enumerate() { 8 | s = match i { 9 | 0 => format!("{source}"), 10 | _ => format!("{source} ({s})"), 11 | } 12 | } 13 | s 14 | } 15 | -------------------------------------------------------------------------------- /src/execute.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::process::Command; 3 | 4 | #[allow(unused_imports)] 5 | use log::{debug, error, info, trace, warn}; 6 | 7 | #[derive(Clone, Debug)] 8 | pub struct CommandBuilder { 9 | profile_name: String, 10 | profile_commands: Vec, 11 | heads: HashMap>, 12 | oneshot: bool, 13 | } 14 | 15 | impl CommandBuilder { 16 | pub fn new(profile_name: String) -> Self { 17 | Self { 18 | profile_name, 19 | profile_commands: Default::default(), 20 | heads: Default::default(), 21 | oneshot: false, 22 | } 23 | } 24 | pub fn oneshot(&mut self, oneshot: bool) { 25 | self.oneshot = oneshot 26 | } 27 | pub fn profile_commands(&mut self, profile_commands: Vec) { 28 | self.profile_commands = profile_commands 29 | } 30 | pub fn insert_head_commands(&mut self, head_name: String, output_commands: Vec) { 31 | self.heads.insert(head_name, output_commands); 32 | } 33 | 34 | pub fn execute(self) { 35 | let oneshot = self.oneshot; 36 | let mut cmd_list = self.create_command_list(); 37 | if cmd_list.is_empty() { 38 | return; 39 | } 40 | 41 | trace!("starting command executer thread"); 42 | let handle = std::thread::Builder::new() 43 | .name("command exec".into()) 44 | .spawn(move || { 45 | cmd_list.iter_mut().for_each(|(exec, cmd)| { 46 | execute_command(exec, cmd); 47 | }); 48 | }); 49 | 50 | if let Err(err) = handle { 51 | error!("cannot spawn thread {:?}", err); 52 | return; 53 | } 54 | 55 | // return immediately if we are running as a daemon 56 | if !oneshot { 57 | return; 58 | } 59 | 60 | if let Err(err) = handle.unwrap().join() { 61 | error!("cannot join thread {:?}", err) 62 | } 63 | } 64 | 65 | fn create_command_list(self) -> Vec<(String, Command)> { 66 | let mut cmd_list: Vec<(String, Command)> = vec![]; 67 | let env_vars = vec![("SHIKANE_PROFILE_NAME", self.profile_name.as_str())]; 68 | assemble_commands(&mut cmd_list, self.profile_commands, &env_vars); 69 | for (head_name, output_commands) in self.heads { 70 | let env_vars = vec![("SHIKANE_OUTPUT_NAME", head_name.as_str())]; 71 | assemble_commands(&mut cmd_list, output_commands, &env_vars); 72 | } 73 | cmd_list 74 | } 75 | } 76 | 77 | fn assemble_commands( 78 | cmd_list: &mut Vec<(String, Command)>, 79 | vexec: Vec, 80 | env_vars: &[(&str, &str)], 81 | ) { 82 | for cmd in vexec { 83 | let mut c = Command::new("sh"); 84 | c.arg("-c").arg(cmd.clone()); 85 | c.envs(env_vars.to_owned()); 86 | cmd_list.push((cmd, c)); 87 | } 88 | } 89 | 90 | fn execute_command(exec: &mut String, cmd: &mut Command) { 91 | debug!("[cmd] {:?}", exec); 92 | let output = match cmd.output() { 93 | Ok(o) => o, 94 | Err(err) => { 95 | error!("failed to spawn command: {:?} {}", exec, err); 96 | return; 97 | } 98 | }; 99 | if let Ok(stdout) = String::from_utf8(output.stdout) { 100 | trace!("[stdout] {:?}", stdout) 101 | } 102 | if let Ok(stderr) = String::from_utf8(output.stderr) { 103 | trace!("[stderr] {:?}", stderr) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/ipc.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | use std::io::{Read, Write}; 3 | use std::net::Shutdown; 4 | use std::os::fd::AsFd; 5 | use std::os::unix::net::UnixStream; 6 | use std::path::PathBuf; 7 | 8 | #[allow(unused_imports)] 9 | use log::{debug, error, info, trace, warn}; 10 | use serde::{de::DeserializeOwned, Deserialize, Serialize}; 11 | use snafu::{prelude::*, Location}; 12 | 13 | use crate::matching::MatchReport; 14 | use crate::wl_backend::WlHead; 15 | 16 | #[derive(Clone, Debug, Deserialize, Serialize)] 17 | pub(crate) enum IpcRequest { 18 | CurrentHeads, 19 | CurrentState, 20 | MatchReports, 21 | ReloadConfig(Option), 22 | SwitchProfile(String), 23 | } 24 | 25 | #[derive(Clone, Debug, Deserialize, Serialize)] 26 | pub(crate) enum IpcResponse { 27 | CurrentHeads(VecDeque), 28 | Error(String), 29 | Generic(String), 30 | MatchReports(VecDeque), 31 | Success, 32 | } 33 | 34 | #[derive(Debug)] 35 | pub(crate) struct IpcStream { 36 | stream: UnixStream, 37 | } 38 | 39 | impl IpcStream { 40 | pub(crate) fn connect() -> Result { 41 | let socket = get_socket_path()?; 42 | Self::connect_to(&socket) 43 | } 44 | 45 | pub(crate) fn connect_to(socket: &PathBuf) -> Result { 46 | let stream = UnixStream::connect(socket).context(SocketConnectCtx)?; 47 | Ok(Self::from(stream)) 48 | } 49 | 50 | pub(crate) fn recv(&mut self) -> Result { 51 | trace!("[reading data from socket]"); 52 | let mut buf = String::new(); 53 | self.stream 54 | .read_to_string(&mut buf) 55 | .context(SocketReadCtx)?; 56 | let data = ron::de::from_str(&buf).context(RonDeserializeCtx)?; 57 | self.shutdown_ipc_socket(Shutdown::Read)?; 58 | Ok(data) 59 | } 60 | 61 | pub(crate) fn send(&mut self, data: &D) -> Result<(), IpcError> { 62 | let buf = ron::ser::to_string(&data).context(RonSerializeCtx)?; 63 | trace!("[writing data to socket] {:?}", buf); 64 | self.stream 65 | .write_all(buf.as_bytes()) 66 | .context(SocketWriteCtx)?; 67 | self.shutdown_ipc_socket(Shutdown::Write)?; 68 | Ok(()) 69 | } 70 | 71 | pub(crate) fn shutdown_ipc_socket(&mut self, direction: Shutdown) -> Result<(), IpcError> { 72 | trace!("[shutdown socket] direction: {direction:?}"); 73 | self.stream 74 | .shutdown(direction) 75 | .context(ShutdownCtx { direction }) 76 | } 77 | 78 | pub(crate) fn into_event_source(self) -> calloop::generic::Generic { 79 | self.into() 80 | } 81 | } 82 | 83 | pub(crate) fn get_socket_path() -> Result { 84 | let wayland_display = "WAYLAND_DISPLAY"; 85 | let wayland_display = std::env::var("WAYLAND_DISPLAY").context(EnvVarCtx { 86 | var: wayland_display, 87 | })?; 88 | 89 | let xdg_dirs = xdg::BaseDirectories::new().context(BaseDirectoriesCtx)?; 90 | 91 | let path = format!("shikane-{wayland_display}.socket"); 92 | let path = xdg_dirs.place_runtime_file(path).context(SocketPathCtx)?; 93 | Ok(path) 94 | } 95 | 96 | impl From for calloop::generic::Generic { 97 | fn from(value: IpcStream) -> Self { 98 | calloop::generic::Generic::new(value, calloop::Interest::BOTH, calloop::Mode::Edge) 99 | } 100 | } 101 | 102 | impl AsFd for IpcStream { 103 | fn as_fd(&self) -> std::os::unix::prelude::BorrowedFd<'_> { 104 | self.stream.as_fd() 105 | } 106 | } 107 | 108 | impl From for IpcStream { 109 | fn from(value: UnixStream) -> Self { 110 | Self { stream: value } 111 | } 112 | } 113 | 114 | #[derive(Debug, Snafu)] 115 | #[snafu(context(suffix(Ctx)))] 116 | #[snafu(visibility(pub(crate)))] 117 | pub(crate) enum IpcError { 118 | // Socket 119 | #[snafu(display("[{location}] Failed to accept incoming socket connection"))] 120 | SocketAccept { 121 | source: std::io::Error, 122 | location: Location, 123 | }, 124 | #[snafu(display("[{location}] Cannot bind to socket {path:?}"))] 125 | SocketBind { 126 | source: std::io::Error, 127 | location: Location, 128 | path: PathBuf, 129 | }, 130 | #[snafu(display("[{location}] Cannot read from socket"))] 131 | SocketRead { 132 | source: std::io::Error, 133 | location: Location, 134 | }, 135 | #[snafu(display("[{location}] Cannot write to socket"))] 136 | SocketWrite { 137 | source: std::io::Error, 138 | location: Location, 139 | }, 140 | #[snafu(display("[{location}] Cannot shutdown stream for {direction:?} directon(s)"))] 141 | Shutdown { 142 | source: std::io::Error, 143 | location: Location, 144 | direction: Shutdown, 145 | }, 146 | // SerDe 147 | #[snafu(display("[{location}] Cannot serialize data to RON"))] 148 | RonSerialize { 149 | source: ron::error::Error, 150 | location: Location, 151 | }, 152 | #[snafu(display("[{location}] Cannot deserialize data from RON"))] 153 | RonDeserialize { 154 | source: ron::error::SpannedError, 155 | location: Location, 156 | }, 157 | } 158 | 159 | #[derive(Debug, Snafu)] 160 | #[snafu(context(suffix(Ctx)))] 161 | pub(crate) enum IpcSetupError { 162 | #[snafu(display("[{location}] Problem with XDG directories"))] 163 | BaseDirectories { 164 | source: xdg::BaseDirectoriesError, 165 | location: Location, 166 | }, 167 | #[snafu(display("[{location}] Cannot find environment variable {var}"))] 168 | EnvVar { 169 | source: std::env::VarError, 170 | location: Location, 171 | var: String, 172 | }, 173 | #[snafu(display("[{location}] Cannot place socket in XDG runtime directory"))] 174 | SocketPath { 175 | source: std::io::Error, 176 | location: Location, 177 | }, 178 | #[snafu(display("[{location}] Cannot connect to socket"))] 179 | SocketConnect { 180 | source: std::io::Error, 181 | location: Location, 182 | }, 183 | } 184 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod client; 2 | pub mod daemon; 3 | pub(crate) mod error; 4 | pub mod execute; 5 | pub(crate) mod ipc; 6 | pub mod matching; 7 | pub mod pipeline; 8 | pub mod profile; 9 | pub mod search; 10 | pub mod settings; 11 | pub mod util; 12 | pub mod variant; 13 | pub mod wl_backend; 14 | pub mod wlroots; 15 | -------------------------------------------------------------------------------- /src/matching.rs: -------------------------------------------------------------------------------- 1 | mod comparator; 2 | mod hopcroft_karp_map; 3 | mod pairing; 4 | mod pipelined; 5 | 6 | use std::collections::VecDeque; 7 | 8 | #[allow(unused_imports)] 9 | use log::{debug, error, info, trace, warn}; 10 | use serde::{Deserialize, Serialize}; 11 | 12 | use crate::error; 13 | use crate::pipeline::Pipeline; 14 | use crate::profile::{Output, Profile}; 15 | use crate::variant::ValidVariant; 16 | use crate::wl_backend::WlHead; 17 | 18 | pub use self::comparator::{Comparator, ComparatorInfo}; 19 | pub use self::hopcroft_karp_map::{Edge, HopcroftKarpMap}; 20 | pub use self::pairing::{ 21 | IntermediatePairing, IntermediatePairingWithMultipleModes, IntermediatePairingWithoutMode, 22 | Pairing, PairingWithMode, PairingWithoutMode, UnrelatedPairing, 23 | }; 24 | use self::pipelined::{MatchPipelineError, MatcherOutput}; 25 | 26 | pub const MAX_RR_DEVIATION: i32 = 500; 27 | 28 | pub struct ProfileMatcher; 29 | 30 | #[derive(Clone, Debug, Serialize, Deserialize)] 31 | pub struct MatchReport { 32 | // input 33 | pub(crate) profile: Profile, 34 | wl_heads: VecDeque, 35 | 36 | // from stage 1 37 | pub(crate) unpaired_heads: Vec, 38 | pub(crate) unpaired_outputs: Vec, 39 | pub(crate) unrelated_pairings: Vec, 40 | // from stage 2 41 | // contains lists of pairings where [pairing].len() != wl_heads.len() 42 | pub(crate) invalid_subsets: Vec>, 43 | // from stage 3 44 | pub(crate) valid_variants: VecDeque, 45 | } 46 | 47 | impl ProfileMatcher { 48 | pub fn create_report(profile: Profile, wl_heads: VecDeque) -> Option { 49 | let p = Pipeline::new(pipelined::Stage1) 50 | .add_pipe(pipelined::Stage2) 51 | .add_pipe(pipelined::Stage3); 52 | 53 | let input = pipelined::MatcherInput::new(wl_heads, profile.clone()); 54 | let result: Result = p.execute(input); 55 | 56 | match result { 57 | Ok(mo) => Some(mo.into_report()), 58 | Err(err) => { 59 | debug!( 60 | "matching error with profile {:?}: {}", 61 | profile.name, 62 | error::report(&err) 63 | ); 64 | if let MatchPipelineError::DifferentInputLength { ref input, .. } = err { 65 | warn!("This should not have happened: {}", error::report(&err)); 66 | warn!("Please report this :)"); 67 | warn!("Occured with the following parameters: {input:?}"); 68 | } 69 | None 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/matching/comparator.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | 3 | use itertools::Itertools; 4 | #[allow(unused_imports)] 5 | use log::{debug, error, info, trace, warn}; 6 | 7 | use crate::profile::{Mode, Output}; 8 | use crate::wl_backend::{WlHead, WlMode}; 9 | 10 | use super::{ 11 | IntermediatePairing, IntermediatePairingWithMultipleModes, IntermediatePairingWithoutMode, 12 | UnrelatedPairing, 13 | }; 14 | 15 | #[derive(Clone, Copy, Debug)] 16 | pub struct Comparator; 17 | 18 | #[derive(Clone, Debug)] 19 | pub struct ComparatorInfo { 20 | pub(crate) intermediate_pairings: Vec, 21 | pub(crate) unpaired_heads: Vec, 22 | pub(crate) unpaired_outputs: Vec, 23 | pub(crate) unrelated_pairings: Vec, 24 | } 25 | 26 | impl Comparator { 27 | pub(crate) fn collect_intermediate_pairings<'o, 'h>( 28 | outputs: impl Iterator + Clone, 29 | wl_heads: impl Iterator + Clone, 30 | ) -> ComparatorInfo { 31 | let results: Vec<_> = outputs 32 | .clone() 33 | .cartesian_product(wl_heads.clone()) 34 | .map(|(output, head)| Self::matcher(output, head)) 35 | .collect(); 36 | 37 | let mut intermediate_pairings = vec![]; 38 | let mut unrelated_pairings = vec![]; 39 | for res in results { 40 | match res { 41 | ComparatorResult::IntermediatePairing(ipair) => intermediate_pairings.push(ipair), 42 | ComparatorResult::UnrelatedParing(upair) => unrelated_pairings.push(upair), 43 | } 44 | } 45 | 46 | let unpaired_outputs: Vec<_> = outputs 47 | .filter(|o| { 48 | !intermediate_pairings 49 | .iter() 50 | .any(|ipair| ipair.output() == *o) 51 | }) 52 | .cloned() 53 | .collect(); 54 | let unpaired_heads: Vec<_> = wl_heads 55 | .filter(|h| { 56 | !intermediate_pairings 57 | .iter() 58 | .any(|ipair| ipair.matched_head() == *h) 59 | }) 60 | .cloned() 61 | .collect(); 62 | 63 | ComparatorInfo { 64 | intermediate_pairings, 65 | unpaired_heads, 66 | unpaired_outputs, 67 | unrelated_pairings, 68 | } 69 | } 70 | 71 | fn matcher(output: &Output, wl_head: &WlHead) -> ComparatorResult { 72 | debug!( 73 | "comparing output \"{}\" with head {:?}", 74 | output.search_pattern, 75 | wl_head.name() 76 | ); 77 | let search_result = output 78 | .search_pattern 79 | .clone() 80 | .query() 81 | .description(wl_head.description()) 82 | .name(wl_head.name()) 83 | .model(wl_head.model()) 84 | .serial(wl_head.serial_number()) 85 | .vendor(wl_head.make()) 86 | .run(); 87 | debug!("search_result.is_ok={}", search_result.is_ok()); 88 | 89 | // If we don't have to find a mode (no mode specified or custom mode), 90 | // we cannot run into the "unsupported mode" case. We can stop here. 91 | if output.mode.is_none() || output.mode.is_some_and(|m| m.is_custom()) { 92 | if !search_result.is_ok() { 93 | let mut upair = UnrelatedPairing::new(output.clone(), wl_head.clone()); 94 | upair.failed_search(search_result); 95 | return upair.into(); 96 | } 97 | let pair = 98 | IntermediatePairingWithoutMode::new(search_result, output.clone(), wl_head.clone()); 99 | return pair.into(); 100 | } 101 | 102 | // Unwrap is ok here because of early return 103 | let smode = output.mode.unwrap(); 104 | let matched_modes = collect_modes(wl_head.modes().clone(), &smode).unwrap(); 105 | 106 | if matched_modes.is_empty() || !search_result.is_ok() { 107 | let mut upair = UnrelatedPairing::new(output.clone(), wl_head.clone()); 108 | if !search_result.is_ok() { 109 | upair.failed_search(search_result); 110 | } 111 | if matched_modes.is_empty() { 112 | upair.unsupported_mode(smode); 113 | } 114 | return upair.into(); 115 | } 116 | 117 | IntermediatePairingWithMultipleModes::new( 118 | search_result, 119 | output.clone(), 120 | wl_head.clone(), 121 | matched_modes, 122 | ) 123 | .into() 124 | } 125 | } 126 | 127 | enum ComparatorResult { 128 | IntermediatePairing(IntermediatePairing), 129 | UnrelatedParing(UnrelatedPairing), 130 | } 131 | 132 | impl From for ComparatorResult { 133 | fn from(value: UnrelatedPairing) -> Self { 134 | Self::UnrelatedParing(value) 135 | } 136 | } 137 | impl From for ComparatorResult { 138 | fn from(value: IntermediatePairingWithoutMode) -> Self { 139 | Self::IntermediatePairing(value.into()) 140 | } 141 | } 142 | impl From for ComparatorResult { 143 | fn from(value: IntermediatePairingWithMultipleModes) -> Self { 144 | Self::IntermediatePairing(value.into()) 145 | } 146 | } 147 | 148 | /// Collect modes that match the given mode. 149 | fn collect_modes(mut modes: VecDeque, smode: &Mode) -> Option> { 150 | sort_modes(modes.make_contiguous()); 151 | modes.make_contiguous().reverse(); 152 | 153 | let best = modes.front().cloned(); 154 | let preferred = modes.iter().find(|m| m.preferred()).cloned(); 155 | let preferred = preferred.or(best.clone()).into_iter().collect(); 156 | let best = best.into_iter().collect(); 157 | 158 | match smode { 159 | Mode::Best => Some(best), 160 | Mode::Preferred => Some(preferred), 161 | Mode::WiHe(w, h) => Some( 162 | modes 163 | .into_iter() 164 | .filter(|m| m.width() == *w && m.height() == *h) 165 | .collect(), 166 | ), 167 | Mode::WiHeRe(w, h, r) => Some( 168 | modes 169 | .into_iter() 170 | .filter(|m| m.width() == *w && m.height() == *h && compare_mode_refresh(*r, m).0) 171 | .collect(), 172 | ), 173 | Mode::WiHeReCustom(_, _, _) => None, 174 | } 175 | } 176 | 177 | fn compare_mode_refresh(refresh: i32, mode: &WlMode) -> (bool, i32) { 178 | let diff: i32 = refresh.abs_diff(mode.refresh()) as i32; // difference in mHz 179 | trace!( 180 | "refresh: {refresh}mHz, monitor.refresh {}mHz, diff: {diff}mHz", 181 | mode.refresh() 182 | ); 183 | (diff <= super::MAX_RR_DEVIATION, diff) 184 | } 185 | 186 | fn sort_modes(modes: &mut [WlMode]) { 187 | modes.sort_by_key(|m| { 188 | let pixels = m.width() * m.height(); 189 | let w = m.width(); 190 | let h = m.height(); 191 | let r = m.refresh(); 192 | 193 | (pixels, w, h, r) 194 | }); 195 | } 196 | -------------------------------------------------------------------------------- /src/matching/hopcroft_karp_map.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::profile::Output; 4 | use crate::wl_backend::WlHead; 5 | 6 | use super::{ 7 | IntermediatePairing, IntermediatePairingWithMultipleModes, IntermediatePairingWithoutMode, 8 | }; 9 | 10 | /// Collect `Head`s that matched to `Output`s in a bipartite graph, feed it into the 11 | /// Hopcroft-Karp algorithm and return a maximum cardinality matching. 12 | /// 13 | /// The hopcroft_karp crate expects that the vertices are the same type and implement Hash. 14 | /// The crate chooses to panic if the graph is not bipartite. 15 | /// Neither `Output` nor `Head` need to implement Hash and instead of relying on a correct 16 | /// Hash implementation they will be mapped to a type that does: `isize`. 17 | /// This is why this adapter exists. 18 | /// 19 | /// It maps 20 | /// - every `Output` to a positive integer > 0 21 | /// - every `Head` to a negative integer < 0 22 | /// 23 | /// A graph that only contains edges between positive and negative integers is a bipartite graph. 24 | pub struct HopcroftKarpMap; 25 | 26 | impl HopcroftKarpMap { 27 | pub fn hkmap(edges: impl Iterator) -> impl Iterator 28 | where 29 | Output: PartialEq + Clone, 30 | Head: PartialEq + Clone, 31 | E: Edge, 32 | { 33 | let mut map = CombinedMap::default(); 34 | 35 | // Map (Output, Head) edges to integer tuples 36 | let mapped_edges = edges.map(|e| map.insert(e)).collect(); 37 | 38 | // Run Hopcroft-Karp on integer edges 39 | let matched_mapped_edges = hopcroft_karp::matching(&mapped_edges); 40 | 41 | // Map integers back to respective edge 42 | matched_mapped_edges 43 | .into_iter() 44 | .filter_map(move |me| map.pairs.remove(&me)) 45 | } 46 | } 47 | 48 | struct CombinedMap 49 | where 50 | Output: PartialEq, 51 | Head: PartialEq, 52 | // E: AsEdgeRef<'p, Output, Head>, 53 | E: Edge, 54 | { 55 | // maps positive integers to Output 56 | mapping_output: LeftMap, 57 | // maps negative integers to Head 58 | mapping_head: RightMap, 59 | // Contains edges (positive, negative) integers 60 | pairs: HashMap<(isize, isize), E>, 61 | } 62 | 63 | impl Default for CombinedMap 64 | where 65 | Output: PartialEq, 66 | Head: PartialEq, 67 | // E: AsEdgeRef, 68 | E: Edge, 69 | { 70 | fn default() -> Self { 71 | Self { 72 | mapping_output: Default::default(), 73 | mapping_head: Default::default(), 74 | pairs: Default::default(), 75 | } 76 | } 77 | } 78 | 79 | impl CombinedMap 80 | where 81 | Output: PartialEq + Clone, 82 | Head: PartialEq + Clone, 83 | E: Edge, 84 | { 85 | fn insert(&mut self, edge: E) -> (isize, isize) { 86 | let left = self.mapping_output.insert(edge.left().clone()); 87 | let right = self.mapping_head.insert(edge.right().clone()); 88 | self.pairs.insert((left, right), edge); 89 | (left, right) 90 | } 91 | } 92 | 93 | struct InnerMap { 94 | list: Vec, 95 | } 96 | 97 | /// Negative 98 | struct LeftMap { 99 | map: InnerMap, 100 | } 101 | 102 | /// Positive 103 | struct RightMap { 104 | map: InnerMap, 105 | } 106 | 107 | impl Default for InnerMap { 108 | fn default() -> Self { 109 | Self { list: vec![] } 110 | } 111 | } 112 | 113 | impl Default for RightMap { 114 | fn default() -> Self { 115 | Self { 116 | map: Default::default(), 117 | } 118 | } 119 | } 120 | impl Default for LeftMap { 121 | fn default() -> Self { 122 | Self { 123 | map: Default::default(), 124 | } 125 | } 126 | } 127 | 128 | impl LeftMap { 129 | fn insert(&mut self, value: T) -> isize { 130 | let index: usize = self.map.insert(value); 131 | let index: isize = -((index + 1) as isize); 132 | assert!(index < 0); 133 | index 134 | } 135 | } 136 | impl RightMap { 137 | fn insert(&mut self, value: T) -> isize { 138 | let index: usize = self.map.insert(value); 139 | let index: isize = (index + 1) as isize; 140 | assert!(index > 0); 141 | index 142 | } 143 | } 144 | 145 | impl InnerMap { 146 | fn insert(&mut self, value: T) -> usize { 147 | match self.list.iter().position(|t| *t == value) { 148 | Some(idx) => idx, 149 | None => { 150 | self.list.push(value); 151 | self.list.len() - 1 152 | } 153 | } 154 | } 155 | } 156 | 157 | /// Needed in the [`HopcroftKarpMap`]. 158 | /// 159 | /// This trait splits a type in two while keeping its inner information sealed and together. 160 | pub trait Edge { 161 | fn left(&self) -> &L; 162 | fn right(&self) -> &R; 163 | } 164 | 165 | impl Edge for IntermediatePairing { 166 | fn left(&self) -> &Output { 167 | match self { 168 | IntermediatePairing::WithMultipleModes(ip) => ip.left(), 169 | IntermediatePairing::WithoutMode(ip) => ip.left(), 170 | } 171 | } 172 | fn right(&self) -> &WlHead { 173 | match self { 174 | IntermediatePairing::WithMultipleModes(ip) => ip.right(), 175 | IntermediatePairing::WithoutMode(ip) => ip.right(), 176 | } 177 | } 178 | } 179 | 180 | impl Edge for IntermediatePairingWithMultipleModes { 181 | fn left(&self) -> &Output { 182 | &self.output 183 | } 184 | fn right(&self) -> &WlHead { 185 | &self.matched_head 186 | } 187 | } 188 | impl Edge for IntermediatePairingWithoutMode { 189 | fn left(&self) -> &Output { 190 | &self.output 191 | } 192 | fn right(&self) -> &WlHead { 193 | &self.matched_head 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/matching/pairing.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::profile::{Mode, Output}; 4 | use crate::search::SearchResult; 5 | use crate::wl_backend::{WlHead, WlMode}; 6 | 7 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 8 | pub enum Pairing { 9 | WithMode(PairingWithMode), 10 | WithoutMode(PairingWithoutMode), 11 | } 12 | 13 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 14 | pub struct PairingWithMode { 15 | pub(crate) search_result: SearchResult, 16 | pub(crate) output: Output, 17 | pub(crate) wl_head: WlHead, 18 | pub(crate) wl_mode: WlMode, 19 | } 20 | 21 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 22 | pub struct PairingWithoutMode { 23 | pub(crate) search_result: SearchResult, 24 | pub(crate) output: Output, 25 | pub(crate) wl_head: WlHead, 26 | } 27 | 28 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 29 | pub enum IntermediatePairing { 30 | WithMultipleModes(IntermediatePairingWithMultipleModes), 31 | WithoutMode(IntermediatePairingWithoutMode), 32 | } 33 | 34 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 35 | pub struct IntermediatePairingWithMultipleModes { 36 | pub(crate) search_result: SearchResult, 37 | pub(crate) output: Output, 38 | pub(crate) matched_head: WlHead, 39 | pub(crate) matched_modes: Vec, 40 | } 41 | 42 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 43 | pub struct IntermediatePairingWithoutMode { 44 | pub(crate) search_result: SearchResult, 45 | pub(crate) output: Output, 46 | pub(crate) matched_head: WlHead, 47 | } 48 | 49 | #[derive(Clone, Debug, Serialize, Deserialize)] 50 | pub struct UnrelatedPairing { 51 | pub(crate) output: Output, 52 | pub(crate) wl_head: WlHead, 53 | pub(crate) failed_search: Option, 54 | pub(crate) unsupported_mode: Option, 55 | } 56 | 57 | impl Pairing { 58 | pub fn output(&self) -> &Output { 59 | match self { 60 | Self::WithMode(p) => &p.output, 61 | Self::WithoutMode(p) => &p.output, 62 | } 63 | } 64 | pub fn custom_mode(&self) -> Option { 65 | match self { 66 | Self::WithMode(_) => None, 67 | Self::WithoutMode(p) => match p.output.mode.is_some_and(|m| m.is_custom()) { 68 | true => p.output.mode, 69 | false => None, 70 | }, 71 | } 72 | } 73 | pub fn wl_head(&self) -> &WlHead { 74 | match self { 75 | Self::WithMode(p) => &p.wl_head, 76 | Self::WithoutMode(p) => &p.wl_head, 77 | } 78 | } 79 | pub fn wl_mode(&self) -> Option<&WlMode> { 80 | match self { 81 | Self::WithMode(p) => Some(&p.wl_mode), 82 | Self::WithoutMode(_) => None, 83 | } 84 | } 85 | /// Return how much the refresh rate of the matched [`WlMode`] deviates from the specified [`Mode`]. 86 | /// Lower is better. 87 | pub fn mode_deviation(&self) -> u32 { 88 | match self { 89 | Pairing::WithMode(p) => { 90 | if let Some(o_refresh) = p.output.mode.and_then(|m| m.refresh()) { 91 | (p.wl_mode.refresh() - o_refresh.wrapping_abs()).unsigned_abs() 92 | } else { 93 | 0 94 | } 95 | } 96 | Pairing::WithoutMode(_) => 0, 97 | } 98 | } 99 | /// Return how specific the [`Output`] matches to the [`WlHead`]. 100 | /// Higher is better. 101 | pub fn specificity(&self) -> u64 { 102 | match self { 103 | Pairing::WithMode(p) => p.search_result.specificity(), 104 | Pairing::WithoutMode(p) => p.search_result.specificity(), 105 | } 106 | } 107 | } 108 | 109 | impl IntermediatePairing { 110 | pub fn output(&self) -> &Output { 111 | match self { 112 | IntermediatePairing::WithMultipleModes(ip) => &ip.output, 113 | IntermediatePairing::WithoutMode(ip) => &ip.output, 114 | } 115 | } 116 | pub fn matched_head(&self) -> &WlHead { 117 | match self { 118 | IntermediatePairing::WithMultipleModes(ip) => &ip.matched_head, 119 | IntermediatePairing::WithoutMode(ip) => &ip.matched_head, 120 | } 121 | } 122 | pub(super) fn expand(self) -> Vec { 123 | match self { 124 | IntermediatePairing::WithMultipleModes(ipair) => ipair.expand(), 125 | IntermediatePairing::WithoutMode(ipair) => vec![PairingWithoutMode { 126 | search_result: ipair.search_result, 127 | output: ipair.output, 128 | wl_head: ipair.matched_head, 129 | } 130 | .into()], 131 | } 132 | } 133 | } 134 | 135 | impl IntermediatePairingWithMultipleModes { 136 | pub(super) fn new( 137 | search_result: SearchResult, 138 | output: Output, 139 | matched_head: WlHead, 140 | matched_modes: Vec, 141 | ) -> Self { 142 | Self { 143 | search_result, 144 | output, 145 | matched_head, 146 | matched_modes, 147 | } 148 | } 149 | pub(super) fn expand(self) -> Vec { 150 | self.matched_modes 151 | .into_iter() 152 | .map(|m| { 153 | PairingWithMode { 154 | search_result: self.search_result.clone(), 155 | output: self.output.clone(), 156 | wl_head: self.matched_head.clone(), 157 | wl_mode: m, 158 | } 159 | .into() 160 | }) 161 | .collect() 162 | } 163 | } 164 | 165 | impl IntermediatePairingWithoutMode { 166 | pub(super) fn new(search_result: SearchResult, output: Output, matched_head: WlHead) -> Self { 167 | Self { 168 | search_result, 169 | output, 170 | matched_head, 171 | } 172 | } 173 | } 174 | 175 | impl UnrelatedPairing { 176 | pub(super) fn new(output: Output, wl_head: WlHead) -> Self { 177 | Self { 178 | output, 179 | wl_head, 180 | failed_search: Default::default(), 181 | unsupported_mode: Default::default(), 182 | } 183 | } 184 | pub(super) fn failed_search(&mut self, search_result: SearchResult) -> &mut Self { 185 | self.failed_search = Some(search_result); 186 | self 187 | } 188 | pub(super) fn unsupported_mode(&mut self, mode: Mode) -> &mut Self { 189 | self.unsupported_mode = Some(mode); 190 | self 191 | } 192 | } 193 | 194 | impl From for Pairing { 195 | fn from(value: PairingWithMode) -> Self { 196 | Self::WithMode(value) 197 | } 198 | } 199 | 200 | impl From for Pairing { 201 | fn from(value: PairingWithoutMode) -> Self { 202 | Self::WithoutMode(value) 203 | } 204 | } 205 | 206 | impl From for IntermediatePairing { 207 | fn from(value: IntermediatePairingWithMultipleModes) -> Self { 208 | Self::WithMultipleModes(value) 209 | } 210 | } 211 | 212 | impl From for IntermediatePairing { 213 | fn from(value: IntermediatePairingWithoutMode) -> Self { 214 | Self::WithoutMode(value) 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/matching/pipelined.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | 3 | use itertools::Itertools; 4 | #[allow(unused_imports)] 5 | use log::{debug, error, info, trace, warn}; 6 | use snafu::{prelude::*, Location}; 7 | 8 | use crate::pipeline::PipeStage; 9 | use crate::profile::{Output, Profile}; 10 | use crate::variant::ValidVariant; 11 | use crate::wl_backend::WlHead; 12 | 13 | use super::{ 14 | Comparator, ComparatorInfo, HopcroftKarpMap, IntermediatePairing, MatchReport, UnrelatedPairing, 15 | }; 16 | 17 | /// Create initial pairings 18 | pub struct Stage1; 19 | /// Send combinations of pairings through Hopcroft-Karp 20 | pub struct Stage2; 21 | /// Expand pairings to variants 22 | pub struct Stage3; 23 | 24 | /// Input for the matching pipeline 25 | #[derive(Clone, Debug)] 26 | pub struct MatcherInput { 27 | pub(super) wl_heads: VecDeque, 28 | size: usize, 29 | 30 | profile: Profile, 31 | outputs: Vec, 32 | } 33 | /// Data transfer between stage 1 and stage 2 34 | #[derive(Clone, Debug)] 35 | pub struct TransferOneTwo { 36 | initial: MatcherInput, 37 | 38 | intermediate_pairings: Vec, 39 | unpaired_heads: Vec, 40 | unpaired_outputs: Vec, 41 | unrelated_pairings: Vec, 42 | } 43 | /// Data transfer between stage 2 and stage 3 44 | #[derive(Clone, Debug)] 45 | pub struct TransferTwoThree { 46 | initial: MatcherInput, 47 | 48 | unpaired_heads: Vec, 49 | unpaired_outputs: Vec, 50 | unrelated_pairings: Vec, 51 | 52 | valid_subsets: Vec>, 53 | invalid_subsets: Vec>, 54 | } 55 | /// Output of the matching pipeline 56 | #[derive(Clone, Debug)] 57 | pub struct MatcherOutput { 58 | initial: MatcherInput, 59 | // from stage 1 60 | unpaired_heads: Vec, 61 | unpaired_outputs: Vec, 62 | unrelated_pairings: Vec, 63 | // from stage 2 64 | // contains lists of pairings where [pairing].len() != wl_heads.len() 65 | invalid_subsets: Vec>, 66 | // from stage 3 67 | valid_variants: VecDeque, 68 | } 69 | 70 | impl PipeStage for Stage1 { 71 | type Input = MatcherInput; 72 | type Output = TransferOneTwo; 73 | type Error = MatchPipelineError; 74 | 75 | fn process(input: Self::Input) -> Result { 76 | info!("stage 1"); 77 | debug!("profile: {}", input.profile.name); 78 | 79 | if input.outputs.len() != input.wl_heads.len() { 80 | return DifferentInputLengthCtx { input }.fail(); 81 | } 82 | let info = 83 | Comparator::collect_intermediate_pairings(input.outputs.iter(), input.wl_heads.iter()); 84 | 85 | let ipair_len = info.intermediate_pairings.len(); 86 | let valid_ipair_count = input.size <= ipair_len && ipair_len <= (input.size * input.size); 87 | debug!("len(unrelated pairs)={:?}", info.unrelated_pairings.len()); 88 | debug!("len(unpaired outputs)={:?}", info.unpaired_outputs.len()); 89 | debug!("len(unpaired heads)={:?}", info.unpaired_heads.len()); 90 | debug!("len(intermediate pairs)={:?}", ipair_len); 91 | debug!("len(ipairs) ∈ [size;(size*size)] -> {}", valid_ipair_count); 92 | if !valid_ipair_count { 93 | let transfer = input.enrich(info); 94 | return NotEnoughPairingsCtx { transfer }.fail(); 95 | } 96 | Ok(input.enrich(info)) 97 | } 98 | } 99 | 100 | impl PipeStage for Stage2 { 101 | type Input = TransferOneTwo; 102 | type Output = TransferTwoThree; 103 | type Error = MatchPipelineError; 104 | 105 | fn process(mut input: Self::Input) -> Result { 106 | info!("stage 2"); 107 | let (valid_subsets, invalid_subsets): (Vec>, Vec>) = 108 | std::mem::take(&mut input.intermediate_pairings) 109 | .into_iter() 110 | // Create k-element subsets of intermediate pairings, with k == wl_heads.len() 111 | .combinations(input.initial.size) 112 | .map(|ipairs| ipairs.into_iter()) 113 | .map(HopcroftKarpMap::hkmap) 114 | .map(|i| i.collect::>()) 115 | .partition(|ipairs| ipairs.len() == input.initial.size); 116 | 117 | if valid_subsets.is_empty() { 118 | let transfer = input.enrich(valid_subsets, invalid_subsets); 119 | return LowCardinalityCtx { transfer }.fail(); 120 | } 121 | 122 | Ok(input.enrich(valid_subsets, invalid_subsets)) 123 | } 124 | } 125 | 126 | impl PipeStage for Stage3 { 127 | type Input = TransferTwoThree; 128 | type Output = MatcherOutput; 129 | type Error = MatchPipelineError; 130 | 131 | fn process(mut input: Self::Input) -> Result { 132 | info!("stage 3"); 133 | let valid_variants: VecDeque = std::mem::take(&mut input.valid_subsets) 134 | .into_iter() 135 | .map(|ipairs| { 136 | ipairs 137 | .into_iter() 138 | .map(|ipair| ipair.expand()) 139 | .multi_cartesian_product() 140 | }) 141 | .enumerate() 142 | .flat_map(|(idx, pairs)| { 143 | pairs.into_iter().map(move |p| (idx, p)).enumerate().map( 144 | |(jdx, (idx, pairings))| ValidVariant { 145 | profile: input.initial.profile.clone(), 146 | pairings, 147 | state: Default::default(), 148 | index: (idx * input.initial.size + jdx), 149 | }, 150 | ) 151 | }) 152 | .collect(); 153 | 154 | Ok(input.enrich(valid_variants)) 155 | } 156 | } 157 | 158 | impl MatcherInput { 159 | pub fn new(wl_heads: VecDeque, profile: Profile) -> Self { 160 | let size = wl_heads.len(); 161 | Self { 162 | wl_heads, 163 | size, 164 | outputs: profile.outputs.clone(), 165 | profile, 166 | } 167 | } 168 | fn enrich(self, info: ComparatorInfo) -> TransferOneTwo { 169 | TransferOneTwo { 170 | initial: self, 171 | intermediate_pairings: info.intermediate_pairings, 172 | unpaired_heads: info.unpaired_heads, 173 | unpaired_outputs: info.unpaired_outputs, 174 | unrelated_pairings: info.unrelated_pairings, 175 | } 176 | } 177 | } 178 | impl TransferOneTwo { 179 | fn enrich( 180 | self, 181 | valid_subsets: Vec>, 182 | invalid_subsets: Vec>, 183 | ) -> TransferTwoThree { 184 | TransferTwoThree { 185 | initial: self.initial, 186 | unpaired_heads: self.unpaired_heads, 187 | unpaired_outputs: self.unpaired_outputs, 188 | unrelated_pairings: self.unrelated_pairings, 189 | valid_subsets, 190 | invalid_subsets, 191 | } 192 | } 193 | } 194 | impl TransferTwoThree { 195 | fn enrich(self, valid_variants: VecDeque) -> MatcherOutput { 196 | MatcherOutput { 197 | initial: self.initial, 198 | unpaired_heads: self.unpaired_heads, 199 | unpaired_outputs: self.unpaired_outputs, 200 | unrelated_pairings: self.unrelated_pairings, 201 | invalid_subsets: self.invalid_subsets, 202 | valid_variants, 203 | } 204 | } 205 | } 206 | impl MatcherOutput { 207 | pub(super) fn into_report(self) -> MatchReport { 208 | MatchReport { 209 | profile: self.initial.profile, 210 | wl_heads: self.initial.wl_heads, 211 | unpaired_heads: self.unpaired_heads, 212 | unpaired_outputs: self.unpaired_outputs, 213 | unrelated_pairings: self.unrelated_pairings, 214 | invalid_subsets: self.invalid_subsets, 215 | valid_variants: self.valid_variants, 216 | } 217 | } 218 | } 219 | 220 | /// Errors that may occur in the matching pipeline. 221 | #[derive(Debug, Snafu)] 222 | #[snafu(context(suffix(Ctx)))] 223 | pub enum MatchPipelineError { 224 | /// occurs in step 1 225 | #[snafu(display("[{location}] Mismatched count of outputs and heads"))] 226 | DifferentInputLength { 227 | input: MatcherInput, 228 | location: Location, 229 | }, 230 | /// occurs in step 1 231 | #[snafu(display("[{location}] Cannot find enough fitting pairs of outputs and heads"))] 232 | NotEnoughPairings { 233 | transfer: TransferOneTwo, 234 | location: Location, 235 | }, 236 | /// occurs in step 2 237 | #[snafu(display("[{location}] Unable to find a single full cardinality matching"))] 238 | LowCardinality { 239 | transfer: TransferTwoThree, 240 | location: Location, 241 | }, 242 | } 243 | -------------------------------------------------------------------------------- /src/pipeline.rs: -------------------------------------------------------------------------------- 1 | use core::marker::PhantomData; 2 | 3 | /// A trait representing a single stage in a [`Pipeline`]. 4 | pub trait PipeStage { 5 | type Input; 6 | type Output; 7 | type Error; 8 | fn process(input: Self::Input) -> Result; 9 | } 10 | 11 | /// An abstract processing pipeline, composed of one or multiple [`PipeStage`]s. 12 | pub struct Pipeline 13 | where 14 | P: PipeStage, 15 | { 16 | _pd: PhantomData<(I, O, E, P)>, 17 | } 18 | 19 | /// An internal intermediate stage, acting as glue between two [`PipeStage`]s. 20 | struct Intermediate 21 | where 22 | PAB: PipeStage, 23 | PBC: PipeStage, 24 | { 25 | _pd: PhantomData<(A, B, C, E, PAB, PBC)>, 26 | } 27 | 28 | impl Pipeline 29 | where 30 | PAB: PipeStage, 31 | { 32 | /// Constructs a new pipeline with the provided initial stage, and returns it. 33 | pub fn new(_pipe: PAB) -> Self { 34 | Self { _pd: PhantomData } 35 | } 36 | 37 | /// Executes the pipeline with the given input data, returning the output. 38 | pub fn execute(self, input: A) -> Result { 39 | PAB::process(input) 40 | } 41 | 42 | /// Adds a new stage to the pipeline, returning a new pipeline with the additional stage. 43 | pub fn add_pipe( 44 | self, 45 | _pipe: PBC, 46 | ) -> Pipeline> 47 | where 48 | PBC: PipeStage, 49 | { 50 | Pipeline::> { _pd: PhantomData } 51 | } 52 | } 53 | 54 | impl PipeStage for Intermediate 55 | where 56 | PAB: PipeStage, 57 | PBC: PipeStage, 58 | { 59 | type Input = A; 60 | type Output = C; 61 | type Error = E; 62 | 63 | fn process(input: Self::Input) -> Result { 64 | PBC::process(PAB::process(input)?) 65 | } 66 | } 67 | 68 | #[cfg(test)] 69 | mod test { 70 | use core::any::type_name; 71 | 72 | use super::*; 73 | 74 | #[allow(unused)] 75 | #[derive(Debug, PartialEq)] 76 | enum PipelineError { 77 | ProblemWithStep1, 78 | ProblemWithStep2(InnerErrorStep2), 79 | ProblemWithStep3, 80 | } 81 | 82 | #[derive(Debug, PartialEq)] 83 | struct InnerErrorStep2(usize); 84 | 85 | struct FirstStep; 86 | struct SecondStep; 87 | struct ThirdStep; 88 | struct FailingStep; 89 | struct RepeatingStep; 90 | 91 | #[derive(Debug, PartialEq, Eq)] 92 | struct A; 93 | #[derive(Debug, PartialEq, Eq)] 94 | struct B; 95 | #[derive(Debug, PartialEq, Eq)] 96 | struct C; 97 | #[derive(Debug, PartialEq, Eq)] 98 | struct D; 99 | 100 | impl PipeStage for FirstStep { 101 | type Input = A; 102 | type Output = B; 103 | type Error = PipelineError; 104 | 105 | fn process(input: Self::Input) -> Result { 106 | print!("got {:?}, ", input); 107 | println!("returning {}", type_name::()); 108 | Ok(B) 109 | } 110 | } 111 | 112 | impl PipeStage for SecondStep { 113 | type Input = B; 114 | type Output = C; 115 | type Error = PipelineError; 116 | 117 | fn process(input: Self::Input) -> Result { 118 | print!("got {:?}, ", input); 119 | println!("returning {}", type_name::()); 120 | Ok(C) 121 | } 122 | } 123 | 124 | impl PipeStage for ThirdStep { 125 | type Input = C; 126 | type Output = D; 127 | type Error = PipelineError; 128 | 129 | fn process(input: Self::Input) -> Result { 130 | print!("got {:?}, ", input); 131 | println!("returning {}", type_name::()); 132 | Ok(D) 133 | } 134 | } 135 | 136 | impl PipeStage for FailingStep { 137 | type Input = B; 138 | type Output = C; 139 | type Error = PipelineError; 140 | 141 | fn process(input: Self::Input) -> Result { 142 | print!("got {:?}, ", input); 143 | println!("returning {}", type_name::()); 144 | Err(PipelineError::ProblemWithStep2(InnerErrorStep2(1234))) 145 | } 146 | } 147 | 148 | impl PipeStage for RepeatingStep { 149 | type Input = A; 150 | type Output = A; 151 | type Error = PipelineError; 152 | 153 | fn process(input: Self::Input) -> Result { 154 | print!("got {:?}, ", input); 155 | println!("returning {}", type_name::()); 156 | Ok(A) 157 | } 158 | } 159 | 160 | #[test] 161 | fn test_pipe() { 162 | let p: Pipeline<_, _, _, _> = Pipeline::new(FirstStep) 163 | .add_pipe(SecondStep) 164 | .add_pipe(ThirdStep); 165 | let res = p.execute(A); 166 | println!("got {:?}", res); 167 | assert_eq!(res, Ok(D)) 168 | } 169 | 170 | #[test] 171 | fn test_pipe_error() { 172 | let p: Pipeline<_, _, _, _> = Pipeline::new(FirstStep) 173 | .add_pipe(FailingStep) 174 | .add_pipe(ThirdStep); 175 | let res = p.execute(A); 176 | assert!(res.is_err()); 177 | assert_eq!( 178 | res.unwrap_err(), 179 | PipelineError::ProblemWithStep2(InnerErrorStep2(1234)) 180 | ); 181 | } 182 | 183 | #[test] 184 | fn test_repeating_pipe() { 185 | let p: Pipeline<_, _, _, _> = Pipeline::new(RepeatingStep) 186 | .add_pipe(RepeatingStep) 187 | .add_pipe(RepeatingStep) 188 | .add_pipe(RepeatingStep) 189 | .add_pipe(RepeatingStep) 190 | .add_pipe(RepeatingStep) 191 | .add_pipe(RepeatingStep) 192 | .add_pipe(RepeatingStep) 193 | .add_pipe(RepeatingStep) 194 | .add_pipe(RepeatingStep) 195 | .add_pipe(RepeatingStep) 196 | .add_pipe(RepeatingStep) 197 | .add_pipe(RepeatingStep) 198 | .add_pipe(RepeatingStep) 199 | .add_pipe(RepeatingStep) 200 | .add_pipe(RepeatingStep) 201 | .add_pipe(RepeatingStep) 202 | .add_pipe(RepeatingStep) 203 | .add_pipe(RepeatingStep) 204 | .add_pipe(RepeatingStep) 205 | .add_pipe(RepeatingStep) 206 | .add_pipe(RepeatingStep) 207 | .add_pipe(RepeatingStep) 208 | .add_pipe(RepeatingStep) 209 | .add_pipe(RepeatingStep) 210 | .add_pipe(RepeatingStep) 211 | .add_pipe(RepeatingStep) 212 | .add_pipe(RepeatingStep) 213 | .add_pipe(RepeatingStep) 214 | .add_pipe(RepeatingStep) 215 | .add_pipe(RepeatingStep) 216 | .add_pipe(RepeatingStep) 217 | .add_pipe(RepeatingStep) 218 | .add_pipe(RepeatingStep) 219 | .add_pipe(RepeatingStep) 220 | .add_pipe(RepeatingStep) 221 | .add_pipe(RepeatingStep) 222 | .add_pipe(RepeatingStep) 223 | .add_pipe(RepeatingStep) 224 | .add_pipe(RepeatingStep) 225 | .add_pipe(RepeatingStep) 226 | .add_pipe(RepeatingStep) 227 | .add_pipe(RepeatingStep) 228 | .add_pipe(RepeatingStep) 229 | .add_pipe(RepeatingStep) 230 | .add_pipe(RepeatingStep) 231 | .add_pipe(RepeatingStep) 232 | .add_pipe(RepeatingStep) 233 | .add_pipe(RepeatingStep) 234 | .add_pipe(RepeatingStep) 235 | .add_pipe(RepeatingStep) 236 | .add_pipe(RepeatingStep) 237 | .add_pipe(RepeatingStep) 238 | .add_pipe(RepeatingStep) 239 | .add_pipe(RepeatingStep) 240 | .add_pipe(RepeatingStep) 241 | .add_pipe(RepeatingStep) 242 | .add_pipe(RepeatingStep) 243 | .add_pipe(RepeatingStep) 244 | .add_pipe(RepeatingStep) 245 | .add_pipe(RepeatingStep) 246 | .add_pipe(RepeatingStep) 247 | .add_pipe(RepeatingStep) 248 | .add_pipe(RepeatingStep) 249 | .add_pipe(RepeatingStep) 250 | .add_pipe(RepeatingStep) 251 | .add_pipe(RepeatingStep) 252 | .add_pipe(RepeatingStep) 253 | .add_pipe(RepeatingStep) 254 | .add_pipe(RepeatingStep) 255 | .add_pipe(RepeatingStep) 256 | .add_pipe(RepeatingStep) 257 | .add_pipe(RepeatingStep) 258 | .add_pipe(RepeatingStep) 259 | .add_pipe(RepeatingStep) 260 | .add_pipe(RepeatingStep) 261 | .add_pipe(RepeatingStep) 262 | .add_pipe(RepeatingStep) 263 | .add_pipe(RepeatingStep) 264 | .add_pipe(RepeatingStep) 265 | .add_pipe(RepeatingStep) 266 | .add_pipe(RepeatingStep) 267 | .add_pipe(RepeatingStep) 268 | .add_pipe(RepeatingStep) 269 | .add_pipe(RepeatingStep) 270 | .add_pipe(RepeatingStep) 271 | .add_pipe(RepeatingStep) 272 | .add_pipe(RepeatingStep) 273 | .add_pipe(RepeatingStep) 274 | .add_pipe(RepeatingStep) 275 | .add_pipe(RepeatingStep) 276 | .add_pipe(RepeatingStep) 277 | .add_pipe(RepeatingStep) 278 | .add_pipe(RepeatingStep) 279 | .add_pipe(RepeatingStep) 280 | .add_pipe(RepeatingStep) 281 | .add_pipe(RepeatingStep) 282 | .add_pipe(RepeatingStep) 283 | .add_pipe(RepeatingStep) 284 | .add_pipe(RepeatingStep) 285 | .add_pipe(RepeatingStep) 286 | .add_pipe(RepeatingStep) 287 | .add_pipe(RepeatingStep) 288 | .add_pipe(RepeatingStep) 289 | .add_pipe(RepeatingStep) 290 | .add_pipe(RepeatingStep) 291 | .add_pipe(RepeatingStep) 292 | .add_pipe(RepeatingStep) 293 | .add_pipe(RepeatingStep) 294 | .add_pipe(RepeatingStep) 295 | .add_pipe(RepeatingStep) 296 | .add_pipe(RepeatingStep) 297 | .add_pipe(RepeatingStep) 298 | .add_pipe(RepeatingStep) 299 | .add_pipe(RepeatingStep) 300 | .add_pipe(RepeatingStep) 301 | .add_pipe(RepeatingStep) 302 | .add_pipe(RepeatingStep) 303 | .add_pipe(RepeatingStep) 304 | .add_pipe(RepeatingStep) 305 | .add_pipe(RepeatingStep) 306 | .add_pipe(RepeatingStep) 307 | .add_pipe(RepeatingStep) 308 | .add_pipe(RepeatingStep) 309 | .add_pipe(RepeatingStep) 310 | .add_pipe(RepeatingStep) 311 | .add_pipe(RepeatingStep) 312 | .add_pipe(RepeatingStep) 313 | // .add_pipe(RepeatingStep) // Uncomment to hit recursion limit in rustc 314 | .add_pipe(RepeatingStep); 315 | let res = p.execute(A); 316 | assert_eq!(res, Ok(A)); 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /src/profile.rs: -------------------------------------------------------------------------------- 1 | mod convert; 2 | mod mode; 3 | 4 | use std::fmt::Display; 5 | use std::num::ParseIntError; 6 | use std::str::FromStr; 7 | 8 | use serde::de::{self, MapAccess, Visitor}; 9 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 10 | use snafu::prelude::*; 11 | 12 | use crate::search::Search; 13 | 14 | pub use self::convert::{ConvertError, Converter, ConverterSettings}; 15 | pub use self::mode::Mode; 16 | 17 | #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] 18 | pub struct Profile { 19 | pub name: String, 20 | #[serde(skip)] 21 | pub index: usize, 22 | #[serde(rename = "exec")] 23 | pub commands: Option>, 24 | // table must come last in toml 25 | #[serde(rename = "output")] 26 | pub outputs: Vec, 27 | } 28 | 29 | #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] 30 | pub struct Output { 31 | pub enable: bool, 32 | #[serde(rename = "search", alias = "match")] 33 | pub search_pattern: Search, 34 | #[serde(rename = "exec")] 35 | pub commands: Option>, 36 | pub mode: Option, 37 | pub position: Option, 38 | pub scale: Option, 39 | pub transform: Option, 40 | pub adaptive_sync: Option, 41 | } 42 | 43 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] 44 | pub struct PhysicalSize { 45 | pub width: i32, 46 | pub height: i32, 47 | } 48 | 49 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] 50 | pub struct Position { 51 | pub x: i32, 52 | pub y: i32, 53 | } 54 | 55 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 56 | pub enum AdaptiveSyncState { 57 | Disabled, 58 | Enabled, 59 | } 60 | 61 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)] 62 | pub enum Transform { 63 | #[serde(rename = "normal")] 64 | Normal, 65 | #[serde(rename = "90")] 66 | _90, 67 | #[serde(rename = "180")] 68 | _180, 69 | #[serde(rename = "270")] 70 | _270, 71 | #[serde(rename = "flipped")] 72 | Flipped, 73 | #[serde(rename = "flipped-90")] 74 | Flipped90, 75 | #[serde(rename = "flipped-180")] 76 | Flipped180, 77 | #[serde(rename = "flipped-270")] 78 | Flipped270, 79 | } 80 | 81 | impl Profile { 82 | pub fn new(name: String, outputs: Vec) -> Self { 83 | Self { 84 | name, 85 | outputs, 86 | commands: Default::default(), 87 | index: Default::default(), 88 | } 89 | } 90 | } 91 | 92 | impl Output { 93 | pub fn enabled(search_pattern: Search) -> Self { 94 | Self { 95 | enable: true, 96 | search_pattern, 97 | commands: Default::default(), 98 | mode: None, 99 | position: None, 100 | scale: None, 101 | transform: None, 102 | adaptive_sync: None, 103 | } 104 | } 105 | pub fn disabled(search_pattern: Search) -> Self { 106 | Self { 107 | enable: false, 108 | search_pattern, 109 | commands: Default::default(), 110 | mode: None, 111 | position: None, 112 | scale: None, 113 | transform: None, 114 | adaptive_sync: None, 115 | } 116 | } 117 | pub fn mode(&mut self, mode: Mode) { 118 | self.mode = Some(mode); 119 | } 120 | pub fn position(&mut self, position: Position) { 121 | self.position = Some(position); 122 | } 123 | pub fn scale(&mut self, scale: f64) { 124 | self.scale = Some(scale); 125 | } 126 | pub fn transform(&mut self, transform: Option) { 127 | self.transform = transform; 128 | } 129 | pub fn adaptive_sync(&mut self, adaptive_sync: AdaptiveSyncState) { 130 | self.adaptive_sync = Some(adaptive_sync) 131 | } 132 | } 133 | 134 | impl Display for Position { 135 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 136 | write!(f, "{},{}", self.x, self.y) 137 | } 138 | } 139 | 140 | impl Display for AdaptiveSyncState { 141 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 142 | match self { 143 | AdaptiveSyncState::Disabled => write!(f, "disabled"), 144 | AdaptiveSyncState::Enabled => write!(f, "enabled"), 145 | } 146 | } 147 | } 148 | 149 | impl Display for Transform { 150 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 151 | let s = match self { 152 | Transform::Normal => "normal", 153 | Transform::_90 => "90", 154 | Transform::_180 => "180", 155 | Transform::_270 => "270", 156 | Transform::Flipped => "flipped", 157 | Transform::Flipped90 => "flipped-90", 158 | Transform::Flipped180 => "flipped-180", 159 | Transform::Flipped270 => "flipped-270", 160 | }; 161 | write!(f, "{s}") 162 | } 163 | } 164 | 165 | #[derive(Debug, PartialEq, Snafu)] 166 | #[snafu(context(suffix(Ctx)))] 167 | pub enum ParsePositionError { 168 | #[snafu(display("Missing separator"))] 169 | Separator, 170 | #[snafu(display("Missing x coordinate"))] 171 | MissingX, 172 | #[snafu(display("Missing y coordinate"))] 173 | MissingY, 174 | #[snafu(display("Failed to parse x or y to i32"))] 175 | ParseInt { source: ParseIntError }, 176 | } 177 | 178 | impl FromStr for Position { 179 | type Err = ParsePositionError; 180 | 181 | fn from_str(s: &str) -> Result { 182 | let (x, y) = match s.split_once(',') { 183 | Some(split) => split, 184 | None => return SeparatorCtx {}.fail(), 185 | }; 186 | if x.is_empty() { 187 | return MissingXCtx {}.fail(); 188 | } 189 | if y.is_empty() { 190 | return MissingYCtx {}.fail(); 191 | } 192 | let x: i32 = x.parse().context(ParseIntCtx)?; 193 | let y: i32 = y.parse().context(ParseIntCtx)?; 194 | Ok(Self { x, y }) 195 | } 196 | } 197 | 198 | impl Serialize for Position { 199 | fn serialize(&self, serializer: S) -> Result 200 | where 201 | S: Serializer, 202 | { 203 | serializer.serialize_str(&self.to_string()) 204 | } 205 | } 206 | 207 | impl<'de> Deserialize<'de> for Position { 208 | fn deserialize(deserializer: D) -> Result 209 | where 210 | D: Deserializer<'de>, 211 | { 212 | struct PositionVisitor; 213 | 214 | #[derive(Serialize, Deserialize)] 215 | struct PositionToml { 216 | pub x: i32, 217 | pub y: i32, 218 | } 219 | 220 | impl<'de> Visitor<'de> for PositionVisitor { 221 | type Value = Position; 222 | 223 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 224 | formatter.write_str("string or struct") 225 | } 226 | 227 | fn visit_str(self, value: &str) -> Result 228 | where 229 | E: serde::de::Error, 230 | { 231 | FromStr::from_str(value).map_err(serde::de::Error::custom) 232 | } 233 | 234 | fn visit_map(self, map: M) -> Result 235 | where 236 | M: MapAccess<'de>, 237 | { 238 | let pos: PositionToml = 239 | Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))?; 240 | Ok(Position { x: pos.x, y: pos.y }) 241 | } 242 | } 243 | 244 | deserializer.deserialize_any(PositionVisitor) 245 | } 246 | } 247 | 248 | impl Serialize for AdaptiveSyncState { 249 | fn serialize(&self, serializer: S) -> Result 250 | where 251 | S: Serializer, 252 | { 253 | let b = matches!(self, AdaptiveSyncState::Enabled); 254 | serializer.serialize_bool(b) 255 | } 256 | } 257 | 258 | impl<'de> Deserialize<'de> for AdaptiveSyncState { 259 | fn deserialize(deserializer: D) -> Result 260 | where 261 | D: Deserializer<'de>, 262 | { 263 | let a_sync: bool = bool::deserialize(deserializer)?; 264 | match a_sync { 265 | true => Ok(AdaptiveSyncState::Enabled), 266 | false => Ok(AdaptiveSyncState::Disabled), 267 | } 268 | } 269 | } 270 | 271 | #[cfg(test)] 272 | mod tests { 273 | use rstest::rstest; 274 | 275 | use super::*; 276 | use ParsePositionError::*; 277 | 278 | fn pos(x: i32, y: i32) -> Position { 279 | Position { x, y } 280 | } 281 | fn int_error(s: &str) -> ParsePositionError { 282 | ParseInt { 283 | source: i32::from_str(s).unwrap_err(), 284 | } 285 | } 286 | 287 | #[derive(Debug, PartialEq, Deserialize, Serialize)] 288 | struct SimpleToml { 289 | pos: Position, 290 | } 291 | 292 | #[rstest] 293 | #[case("0,0", pos(0, 0))] 294 | #[case("1920,1080", pos(1920, 1080))] 295 | #[case("-1920,-1080", pos(-1920, -1080))] 296 | #[case("1000000,2", pos(1_000_000, 2))] 297 | fn serde_serialize_pos_string_ok(#[case] s: &str, #[case] pos: Position) { 298 | assert_eq!(Ok(format!("\"{}\"", s)), toml::to_string(&pos)); 299 | } 300 | 301 | #[rstest] 302 | #[case("0,0", pos(0, 0))] 303 | #[case("1920,1080", pos(1920, 1080))] 304 | #[case("-1920,-1080", pos(-1920, -1080))] 305 | #[case("1000000,2", pos(1_000_000, 2))] 306 | fn serde_deserialize_pos_string_ok(#[case] s: &str, #[case] pos: Position) { 307 | let toml_str = toml::from_str(&format!("pos = \"{}\"", s)); 308 | assert_eq!(toml_str, Ok(SimpleToml { pos })); 309 | } 310 | 311 | #[rstest] 312 | #[case("0,0", pos(0, 0))] 313 | #[case("1920,1080", pos(1920, 1080))] 314 | #[case("-1920,-1080", pos(-1920, -1080))] 315 | #[case("1000000,2", pos(1_000_000, 2))] 316 | fn serde_deserialize_pos_table_ok(#[case] s: &str, #[case] pos: Position) { 317 | let v: Vec<&str> = s.split(',').collect(); 318 | let (x, y) = (v[0], v[1]); 319 | let toml_str = format!("pos = {{ x = {x}, y = {y} }}"); 320 | assert_eq!(toml::from_str(&toml_str), Ok(SimpleToml { pos })); 321 | } 322 | 323 | #[rstest] 324 | #[case("0,0", pos(0, 0))] 325 | #[case("1920,1080", pos(1920, 1080))] 326 | #[case("-1920,-1080", pos(-1920, -1080))] 327 | #[case("1000000,2", pos(1_000_000, 2))] 328 | fn parse_position_from_str_ok(#[case] s: &str, #[case] pos: Position) { 329 | assert_eq!(Position::from_str(s), Ok(pos)) 330 | } 331 | 332 | #[rstest] 333 | #[case("", Separator)] 334 | #[case(",1080", MissingX)] 335 | #[case("1920,", MissingY)] 336 | #[case("-19201080", Separator)] 337 | #[case("50000000000,123", int_error("5000000000"))] 338 | #[case("-50000000000,123", int_error("-5000000000"))] 339 | #[case("30.0,123", int_error("30.0"))] 340 | fn parse_position_from_str_err(#[case] s: &str, #[case] err: ParsePositionError) { 341 | assert_eq!(Position::from_str(s), Err(err)) 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /src/profile/convert.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | use std::str::FromStr; 3 | 4 | use itertools::Itertools; 5 | use snafu::{prelude::*, Location}; 6 | 7 | use crate::profile::{Output, Profile}; 8 | use crate::search::{MultiSearch, ParseSingleSearchError, SearchField, SingleSearch}; 9 | use crate::settings::SettingsToml; 10 | use crate::wl_backend::WlHead; 11 | 12 | #[derive(Debug, Snafu)] 13 | #[snafu(context(suffix(Ctx)))] 14 | pub enum ConvertError { 15 | #[snafu(display("[{location}] Failed to create search from WlHead {head:?}"))] 16 | SingleSearch { 17 | source: ParseSingleSearchError, 18 | location: Location, 19 | head: Box, 20 | }, 21 | #[snafu(display("[{location}] Cannot serialize Settings to TOML"))] 22 | TomlSerialize { 23 | source: toml::ser::Error, 24 | location: Location, 25 | }, 26 | } 27 | 28 | #[derive(Clone, Debug, Default)] 29 | pub struct ConverterSettings { 30 | profile_name: String, 31 | included_search_fields: Vec, 32 | } 33 | 34 | pub struct Converter { 35 | settings: ConverterSettings, 36 | } 37 | 38 | impl ConverterSettings { 39 | pub fn profile_name(mut self, name: String) -> Self { 40 | self.profile_name = name; 41 | self 42 | } 43 | pub fn include_search_fields(mut self, fields: Vec) -> Self { 44 | self.included_search_fields = fields; 45 | self 46 | } 47 | pub fn converter(self) -> Converter { 48 | Converter { settings: self } 49 | } 50 | } 51 | impl Converter { 52 | pub fn run(&self, heads: VecDeque) -> Result { 53 | let mut outputs: Vec = vec![]; 54 | for head in heads { 55 | outputs.push(self.convert_head_to_output(head)?); 56 | } 57 | 58 | let p = Profile::new(self.settings.profile_name.clone(), outputs); 59 | let sc = SettingsToml { 60 | timeout: None, 61 | profiles: vec![p].into(), 62 | }; 63 | let settings_string = toml::to_string(&sc).context(TomlSerializeCtx)?; 64 | Ok(settings_string) 65 | } 66 | fn convert_head_to_output(&self, head: WlHead) -> Result { 67 | let ms = self 68 | .multi_search_from_head(&head) 69 | .context(SingleSearchCtx { head: head.clone() })?; 70 | let search_pattern = ms.into(); 71 | 72 | if !head.enabled() { 73 | return Ok(Output::disabled(search_pattern)); 74 | } 75 | 76 | let mut output = Output::enabled(search_pattern); 77 | if let Some(m) = head.current_mode().clone() { 78 | let mode = m.into(); 79 | output.mode(mode); 80 | } 81 | output.position(head.position()); 82 | output.transform(head.transform()); 83 | if head.scale() != 0.0 { 84 | output.scale(head.scale()); 85 | } 86 | if let Some(adaptive_sync) = head.adaptive_sync() { 87 | output.adaptive_sync(adaptive_sync); 88 | } 89 | Ok(output) 90 | } 91 | 92 | fn multi_search_from_head(&self, head: &WlHead) -> Result { 93 | let searches: Result, ParseSingleSearchError> = self 94 | .settings 95 | .included_search_fields 96 | .iter() 97 | .unique() 98 | .map(|field| match field { 99 | SearchField::Description => { 100 | SingleSearch::from_str(&format!("d={}", head.description())) 101 | } 102 | SearchField::Name => SingleSearch::from_str(&format!("n={}", head.name())), 103 | SearchField::Vendor => SingleSearch::from_str(&format!("v={}", head.make())), 104 | SearchField::Model => SingleSearch::from_str(&format!("m={}", head.model())), 105 | SearchField::Serial => { 106 | SingleSearch::from_str(&format!("s={}", head.serial_number())) 107 | } 108 | }) 109 | .collect(); 110 | Ok(MultiSearch::new(searches?)) 111 | } 112 | } 113 | 114 | #[cfg(test)] 115 | mod tests { 116 | use std::str::FromStr; 117 | 118 | use rstest::rstest; 119 | 120 | use crate::profile::{Mode, Output, Profile}; 121 | use crate::search::MultiSearch; 122 | use crate::search::SingleSearch; 123 | 124 | #[rstest] 125 | fn ser_multi_search() { 126 | let ss_serial = SingleSearch::from_str("=ab").unwrap(); 127 | let ss_model = SingleSearch::from_str("=cd").unwrap(); 128 | let ss_vendor = SingleSearch::from_str("=ef").unwrap(); 129 | let ss_description = SingleSearch::from_str("=gh").unwrap(); 130 | let ms = MultiSearch::new(vec![ss_serial, ss_model, ss_vendor, ss_description]); 131 | 132 | let output = Output { 133 | enable: true, 134 | search_pattern: ms.into(), 135 | commands: Default::default(), 136 | mode: Some(Mode::Best), 137 | position: None, 138 | scale: None, 139 | transform: None, 140 | adaptive_sync: None, 141 | }; 142 | 143 | let profile = Profile::new("foo".into(), vec![output]); 144 | let s = toml::to_string(&profile).unwrap(); 145 | let test = r#"name = "foo" 146 | 147 | [[output]] 148 | enable = true 149 | search = ["=ab", "=cd", "=ef", "=gh"] 150 | mode = "best" 151 | "#; 152 | println!("{}", s); 153 | assert_eq!(test, s); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/search.rs: -------------------------------------------------------------------------------- 1 | mod field; 2 | mod multi; 3 | mod parser; 4 | mod query; 5 | mod single; 6 | 7 | use std::fmt::Display; 8 | 9 | use serde::{Deserialize, Serialize}; 10 | 11 | pub use self::field::{FieldSet, FieldSetError, SearchField}; 12 | pub use self::multi::{MultiQuery, MultiSearch, MultiSearchResult}; 13 | pub use self::parser::ParseSingleSearchError; 14 | pub use self::query::SingleQuery; 15 | pub use self::single::{SingleSearch, SingleSearchResult}; 16 | 17 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 18 | pub enum SearchResult { 19 | Single(SingleSearchResult), 20 | Multi(MultiSearchResult), 21 | } 22 | 23 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 24 | #[serde(untagged)] 25 | pub enum Search { 26 | Single(SingleSearch), 27 | Multi(MultiSearch), 28 | } 29 | 30 | #[derive(Clone, Debug, PartialEq)] 31 | pub enum Query<'a> { 32 | Single(SingleQuery<'a>), 33 | Multi(MultiQuery<'a>), 34 | } 35 | 36 | #[derive(Clone, Copy, Debug, Default, PartialOrd, Ord, PartialEq, Eq)] 37 | pub enum SearchKind { 38 | Regex, 39 | Substring, 40 | #[default] 41 | Fulltext, 42 | } 43 | 44 | #[derive(Clone, Debug)] 45 | pub enum SearchPattern { 46 | Regex(regex::Regex), 47 | Substring(String), 48 | Fulltext(String), 49 | } 50 | 51 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] 52 | pub enum CompareMethod { 53 | // Try to match against at least one field 54 | #[default] 55 | AtleastOne, 56 | // The search has to match all given fields 57 | Exact, 58 | } 59 | 60 | impl Search { 61 | pub fn query<'a>(self) -> Query<'a> { 62 | match self { 63 | Search::Single(s) => s.query().into(), 64 | Search::Multi(s) => s.query().into(), 65 | } 66 | } 67 | } 68 | 69 | impl SearchResult { 70 | pub fn is_ok(&self) -> bool { 71 | match self { 72 | SearchResult::Single(sr) => sr.is_ok(), 73 | SearchResult::Multi(sr) => sr.is_ok(), 74 | } 75 | } 76 | pub fn specificity(&self) -> u64 { 77 | match self { 78 | SearchResult::Single(sr) => sr.specificity(), 79 | SearchResult::Multi(sr) => sr.specificity(), 80 | } 81 | } 82 | } 83 | 84 | impl<'a> Query<'a> { 85 | pub fn new(search: Search) -> Self { 86 | match search { 87 | Search::Single(search) => search.query().into(), 88 | Search::Multi(search) => search.query().into(), 89 | } 90 | } 91 | pub fn description(self, description: &'a str) -> Self { 92 | match self { 93 | Query::Single(q) => q.description(description).into(), 94 | Query::Multi(q) => q.description(description).into(), 95 | } 96 | } 97 | pub fn model(self, model: &'a str) -> Self { 98 | match self { 99 | Query::Single(q) => q.model(model).into(), 100 | Query::Multi(q) => q.model(model).into(), 101 | } 102 | } 103 | pub fn name(self, name: &'a str) -> Self { 104 | match self { 105 | Query::Single(q) => q.name(name).into(), 106 | Query::Multi(q) => q.name(name).into(), 107 | } 108 | } 109 | pub fn serial(self, serial: &'a str) -> Self { 110 | match self { 111 | Query::Single(q) => q.serial(serial).into(), 112 | Query::Multi(q) => q.serial(serial).into(), 113 | } 114 | } 115 | pub fn vendor(self, vendor: &'a str) -> Self { 116 | match self { 117 | Query::Single(q) => q.vendor(vendor).into(), 118 | Query::Multi(q) => q.vendor(vendor).into(), 119 | } 120 | } 121 | pub fn run(self) -> SearchResult { 122 | match self { 123 | Query::Single(q) => q.run().into(), 124 | Query::Multi(q) => q.run().into(), 125 | } 126 | } 127 | } 128 | 129 | impl SearchPattern { 130 | /// Compares the provided string with its contained pattern. 131 | /// 132 | /// Returns true if they match and how good they match. 133 | /// Higher is better. 134 | pub fn matches(&self, text: &str) -> (bool, u64) { 135 | let (is_matched, calculated_weight) = match self { 136 | SearchPattern::Regex(re) => { 137 | let b = re.is_match(text); 138 | (b, 1.0) 139 | } 140 | SearchPattern::Substring(s) => { 141 | let b = text.contains(s); 142 | (b, 1024.0 * (s.len() as f64 / text.len() as f64)) 143 | } 144 | SearchPattern::Fulltext(s) => { 145 | let b = text == s; 146 | (b, 1024.0) 147 | } 148 | }; 149 | if !is_matched { 150 | return (false, 0); 151 | } 152 | // scale the weight up by 1000 to remove the need for a float. 153 | // most relevant for substring matching. 154 | let weight = (calculated_weight * 1000.0).trunc() as u64; 155 | (is_matched, weight) 156 | } 157 | } 158 | 159 | impl SearchKind { 160 | pub fn as_char(&self) -> char { 161 | match self { 162 | SearchKind::Regex => '/', 163 | SearchKind::Substring => '%', 164 | SearchKind::Fulltext => '=', 165 | } 166 | } 167 | pub fn as_str(&self) -> &'static str { 168 | match self { 169 | SearchKind::Regex => "Regex", 170 | SearchKind::Substring => "Substring", 171 | SearchKind::Fulltext => "Fulltext", 172 | } 173 | } 174 | pub fn from_char(c: char) -> Option { 175 | match c { 176 | '/' => Some(SearchKind::Regex), 177 | '%' => Some(SearchKind::Substring), 178 | '=' => Some(SearchKind::Fulltext), 179 | _ => None, 180 | } 181 | } 182 | } 183 | 184 | impl SearchPattern { 185 | fn as_str(&self) -> &str { 186 | match self { 187 | SearchPattern::Regex(re) => re.as_str(), 188 | SearchPattern::Substring(s) => s, 189 | SearchPattern::Fulltext(s) => s, 190 | } 191 | } 192 | } 193 | 194 | impl PartialEq for SearchPattern { 195 | fn eq(&self, other: &Self) -> bool { 196 | match (self, other) { 197 | (SearchPattern::Regex(s), SearchPattern::Regex(o)) => s.as_str() == o.as_str(), 198 | (SearchPattern::Substring(s), SearchPattern::Substring(o)) => s == o, 199 | (SearchPattern::Fulltext(s), SearchPattern::Fulltext(o)) => s == o, 200 | (_, _) => false, 201 | } 202 | } 203 | } 204 | 205 | impl From for SearchPattern { 206 | fn from(r: regex::Regex) -> Self { 207 | Self::Regex(r) 208 | } 209 | } 210 | 211 | impl From for SearchResult { 212 | fn from(q: SingleSearchResult) -> Self { 213 | Self::Single(q) 214 | } 215 | } 216 | 217 | impl From for SearchResult { 218 | fn from(q: MultiSearchResult) -> Self { 219 | Self::Multi(q) 220 | } 221 | } 222 | 223 | impl From for Search { 224 | fn from(q: SingleSearch) -> Self { 225 | Self::Single(q) 226 | } 227 | } 228 | 229 | impl From for Search { 230 | fn from(q: MultiSearch) -> Self { 231 | Self::Multi(q) 232 | } 233 | } 234 | 235 | impl<'a> From> for Query<'a> { 236 | fn from(q: SingleQuery<'a>) -> Self { 237 | Self::Single(q) 238 | } 239 | } 240 | 241 | impl<'a> From> for Query<'a> { 242 | fn from(q: MultiQuery<'a>) -> Self { 243 | Self::Multi(q) 244 | } 245 | } 246 | 247 | impl Display for Search { 248 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 249 | match self { 250 | Search::Single(s) => s.fmt(f), 251 | Search::Multi(s) => s.fmt(f), 252 | } 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/search/field.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Default, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 6 | pub struct FieldSet { 7 | index: usize, 8 | set: [Option; Self::N], 9 | } 10 | 11 | #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] 12 | pub enum SearchField { 13 | Description, 14 | Name, 15 | Vendor, 16 | Model, 17 | Serial, 18 | } 19 | 20 | impl FieldSet { 21 | pub const N: usize = 5; 22 | 23 | pub fn new(field: SearchField) -> Self { 24 | let mut set: [_; Self::N] = Default::default(); 25 | set[0] = Some(field); 26 | Self { index: 1, set } 27 | } 28 | 29 | // fn acc(mut self, field: SearchField) -> Self { 30 | // if self.index < Self::N { 31 | // self.set[self.index] = Some(field); 32 | // self.index += 1; 33 | // } 34 | // self 35 | // } 36 | fn try_acc(mut self, field: SearchField) -> Result { 37 | self.try_insert(field)?; 38 | Ok(self) 39 | } 40 | 41 | fn check_insert(&self, field: SearchField) -> Result<(), FieldSetError> { 42 | if self.index >= Self::N { 43 | return Err(FieldSetError::Full); 44 | } 45 | let new = Some(field); 46 | if self.set.contains(&new) { 47 | return Err(FieldSetError::AlreadyInside(field)); 48 | } 49 | Ok(()) 50 | } 51 | 52 | pub fn try_insert(&mut self, field: SearchField) -> Result<(), FieldSetError> { 53 | self.check_insert(field)?; 54 | self.set[self.index] = Some(field); 55 | self.index += 1; 56 | Ok(()) 57 | } 58 | 59 | pub fn weight(&self, field: SearchField) -> Option { 60 | if let Some(exp) = self.set.iter().position(|sf| *sf == Some(field)) { 61 | Some(2_u16.pow(exp as u32)) 62 | } else if self.set.is_empty() { 63 | Some(2_u16.pow(field as u32)) 64 | } else { 65 | None 66 | } 67 | } 68 | 69 | pub fn iter(&self) -> impl Iterator + '_ { 70 | self.set.iter().filter_map(|f| *f) 71 | } 72 | pub fn contains(&self, field: SearchField) -> bool { 73 | self.set.contains(&Some(field)) 74 | } 75 | pub fn is_empty(&self) -> bool { 76 | self.set.iter().all(|f| f.is_none()) 77 | } 78 | pub fn len(&self) -> usize { 79 | self.set.iter().filter(|f| f.is_some()).count() 80 | } 81 | pub fn fill_default(&mut self) { 82 | self.set[0] = Some(SearchField::Description); 83 | self.set[1] = Some(SearchField::Name); 84 | self.set[2] = Some(SearchField::Vendor); 85 | self.set[3] = Some(SearchField::Model); 86 | self.set[4] = Some(SearchField::Serial); 87 | } 88 | } 89 | 90 | impl SearchField { 91 | pub fn as_char(&self) -> char { 92 | match self { 93 | SearchField::Description => 'd', 94 | SearchField::Model => 'm', 95 | SearchField::Name => 'n', 96 | SearchField::Serial => 's', 97 | SearchField::Vendor => 'v', 98 | } 99 | } 100 | pub fn as_str(&self) -> &'static str { 101 | match self { 102 | SearchField::Description => "Description", 103 | SearchField::Model => "Model", 104 | SearchField::Name => "Name", 105 | SearchField::Serial => "Serial", 106 | SearchField::Vendor => "Vendor", 107 | } 108 | } 109 | pub fn from_char(c: char) -> Option { 110 | match c { 111 | 'd' => Some(SearchField::Description), 112 | 'm' => Some(SearchField::Model), 113 | 'n' => Some(SearchField::Name), 114 | 's' => Some(SearchField::Serial), 115 | 'v' => Some(SearchField::Vendor), 116 | _ => None, 117 | } 118 | } 119 | } 120 | 121 | impl TryFrom<[Option; Self::N]> for FieldSet { 122 | type Error = FieldSetError; 123 | 124 | fn try_from(value: [Option; Self::N]) -> Result { 125 | value 126 | .into_iter() 127 | .flatten() 128 | .try_fold(Self::default(), |fs, field| fs.try_acc(field)) 129 | } 130 | } 131 | 132 | impl TryFrom> for FieldSet { 133 | type Error = FieldSetError; 134 | 135 | fn try_from(value: Vec) -> Result { 136 | value 137 | .into_iter() 138 | .try_fold(Self::default(), |fs, field| fs.try_acc(field)) 139 | } 140 | } 141 | 142 | #[derive(Clone, Debug, PartialEq, Eq)] 143 | pub enum FieldSetError { 144 | /// The given [SearchField] has already been inserted 145 | AlreadyInside(SearchField), 146 | /// There is no more space inside this [FieldSet] left 147 | Full, 148 | } 149 | 150 | impl std::error::Error for FieldSetError {} 151 | 152 | impl Display for FieldSetError { 153 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 154 | match self { 155 | FieldSetError::AlreadyInside(sf) => write!(f, "{sf} was already specified"), 156 | FieldSetError::Full => { 157 | write!(f, "Cannot specify more than {} search fields", FieldSet::N) 158 | } 159 | } 160 | } 161 | } 162 | 163 | impl Display for SearchField { 164 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 165 | write!(f, "{}", self.as_str()) 166 | } 167 | } 168 | 169 | impl Display for FieldSet { 170 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 171 | for field in self.set.into_iter().flatten() { 172 | write!(f, "{}", field.as_char())?; 173 | } 174 | Ok(()) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/search/multi.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use super::{SingleSearch, SingleSearchResult}; 6 | 7 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 8 | pub struct MultiSearchResult { 9 | searches: Vec, 10 | } 11 | 12 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 13 | #[serde(transparent)] 14 | pub struct MultiSearch { 15 | searches: Vec, 16 | } 17 | 18 | #[derive(Clone, Debug, PartialEq)] 19 | pub struct MultiQuery<'a> { 20 | search: MultiSearch, 21 | description: &'a str, 22 | model: &'a str, 23 | name: &'a str, 24 | serial: &'a str, 25 | vendor: &'a str, 26 | } 27 | 28 | impl MultiSearchResult { 29 | /// Returns `true` if all inner [`SingleSearchResult::is_ok()`]s return `true` too. 30 | pub fn is_ok(&self) -> bool { 31 | self.searches.iter().all(|ssr| ssr.is_ok()) 32 | } 33 | 34 | /// Returns the sum of all inner [`SingleSearchResult::specificity()`]. 35 | pub fn specificity(&self) -> u64 { 36 | self.searches.iter().map(|ssr| ssr.specificity()).sum() 37 | } 38 | } 39 | 40 | impl MultiSearch { 41 | pub fn new(searches: Vec) -> Self { 42 | Self { searches } 43 | } 44 | pub fn iter(&self) -> impl Iterator { 45 | self.searches.iter() 46 | } 47 | pub fn query<'a>(self) -> MultiQuery<'a> { 48 | MultiQuery::new(self) 49 | } 50 | } 51 | 52 | impl<'a> MultiQuery<'a> { 53 | pub fn new(search: MultiSearch) -> Self { 54 | Self { 55 | search, 56 | description: Default::default(), 57 | model: Default::default(), 58 | name: Default::default(), 59 | serial: Default::default(), 60 | vendor: Default::default(), 61 | } 62 | } 63 | pub fn description(mut self, description: &'a str) -> Self { 64 | self.description = description; 65 | self 66 | } 67 | pub fn model(mut self, model: &'a str) -> Self { 68 | self.model = model; 69 | self 70 | } 71 | pub fn name(mut self, name: &'a str) -> Self { 72 | self.name = name; 73 | self 74 | } 75 | pub fn serial(mut self, serial: &'a str) -> Self { 76 | self.serial = serial; 77 | self 78 | } 79 | pub fn vendor(mut self, vendor: &'a str) -> Self { 80 | self.vendor = vendor; 81 | self 82 | } 83 | pub fn run(self) -> MultiSearchResult { 84 | let mut ssrs: Vec = vec![]; 85 | for ss in self.search.searches { 86 | let ssr = ss 87 | .query() 88 | .description(self.description) 89 | .model(self.model) 90 | .name(self.name) 91 | .serial(self.serial) 92 | .vendor(self.vendor) 93 | .run(); 94 | ssrs.push(ssr); 95 | } 96 | MultiSearchResult { searches: ssrs } 97 | } 98 | } 99 | 100 | impl Display for MultiSearch { 101 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 102 | write!(f, "[")?; 103 | for (i, search) in self.searches.iter().enumerate() { 104 | if i != 0 { 105 | write!(f, ", ")?; 106 | } 107 | search.fmt(f)?; 108 | } 109 | write!(f, "]") 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/search/parser.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use itertools::Itertools; 4 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 5 | use snafu::prelude::*; 6 | 7 | use super::{ 8 | CompareMethod, FieldSet, FieldSetError, SearchField, SearchKind, SearchPattern, SingleSearch, 9 | }; 10 | 11 | #[derive(Debug, PartialEq, Snafu)] 12 | #[snafu(context(suffix(Ctx)))] 13 | pub enum ParseSingleSearchError { 14 | FieldSet { source: FieldSetError }, 15 | Regex { source: regex::Error }, 16 | } 17 | 18 | impl FromStr for SingleSearch { 19 | type Err = ParseSingleSearchError; 20 | 21 | fn from_str(s: &str) -> Result { 22 | let original_str = s.to_string(); 23 | let mut chars = s.chars().peekable(); 24 | let mut fieldset = FieldSet::default(); 25 | let kind = loop { 26 | if let Some((c, sf)) = chars 27 | .peeking_take_while(|c| { 28 | SearchField::from_char(*c).is_some() || SearchKind::from_char(*c).is_some() 29 | }) 30 | .next() 31 | .map(|c| (c, SearchField::from_char(c))) 32 | { 33 | if let Some(sf) = sf { 34 | fieldset.try_insert(sf).context(FieldSetCtx)?; 35 | } else if let Some(kind) = SearchKind::from_char(c) { 36 | break Some(kind); 37 | } else { 38 | break None; 39 | }; 40 | } else { 41 | break None; 42 | } 43 | }; 44 | 45 | let s: String; 46 | let search_kind: SearchKind; 47 | let method; 48 | if let Some(k) = kind { 49 | search_kind = k; 50 | if fieldset.is_empty() { 51 | fieldset.fill_default(); 52 | method = CompareMethod::AtleastOne; 53 | } else { 54 | method = CompareMethod::Exact; 55 | } 56 | s = chars.collect(); 57 | } else { 58 | // no search kind detected, default to full text search 59 | fieldset.fill_default(); 60 | search_kind = SearchKind::Fulltext; 61 | method = CompareMethod::AtleastOne; 62 | s = original_str; 63 | } 64 | 65 | let sp = match search_kind { 66 | SearchKind::Regex => SearchPattern::Regex(regex::Regex::new(&s).context(RegexCtx)?), 67 | SearchKind::Substring => SearchPattern::Substring(s), 68 | SearchKind::Fulltext => SearchPattern::Fulltext(s), 69 | }; 70 | Ok(SingleSearch::new(fieldset, search_kind, sp, method)) 71 | } 72 | } 73 | 74 | impl Serialize for SingleSearch { 75 | fn serialize(&self, serializer: S) -> Result 76 | where 77 | S: Serializer, 78 | { 79 | let s = format!("{}", self); 80 | serializer.serialize_str(&s) 81 | } 82 | } 83 | 84 | impl<'de> Deserialize<'de> for SingleSearch { 85 | fn deserialize(deserializer: D) -> Result 86 | where 87 | D: Deserializer<'de>, 88 | { 89 | let s = String::deserialize(deserializer)?; 90 | let ssearch = SingleSearch::from_str(&s).map_err(serde::de::Error::custom)?; 91 | Ok(ssearch) 92 | } 93 | } 94 | 95 | #[cfg(test)] 96 | mod tests { 97 | use std::str::FromStr; 98 | 99 | use rstest::rstest; 100 | 101 | use crate::search::SearchField as SF; 102 | use crate::search::SearchKind as SK; 103 | use crate::search::SingleSearch; 104 | use SF::Description as D; 105 | use SF::Model as M; 106 | use SF::Name as N; 107 | use SF::Serial as S; 108 | use SF::Vendor as V; 109 | use SK::Fulltext as Ft; 110 | use SK::Regex as Rx; 111 | use SK::Substring as Sstr; 112 | 113 | #[rstest] 114 | #[case("=DP-1", [D,N,V,M,S], Ft, 4)] // Ft 115 | #[case("=ab%cdef", [D,N,V,M,S], Ft, 7)] 116 | #[case("mvsnd=DP-1", [M,V,S,N,D], Ft, 4)] 117 | #[case("ms=DP-1", [M,S], Ft, 4)] 118 | #[case("s=%/DP-1", [S], Ft, 6)] 119 | #[case("s=/%DP-1", [S], Ft, 6)] 120 | #[case("%display", [D,N,V,M,S], Sstr, 7)] // Sstr 121 | #[case("d%display", [D], Sstr, 7)] 122 | #[case("m%=display", [M], Sstr, 8)] 123 | #[case("m%/display", [M], Sstr, 8)] 124 | #[case("m%=/display", [M], Sstr, 9)] 125 | #[case("/DP", [D,N,V,M,S], Rx, 2)] // Rx 126 | #[case("mv/company", [M,V], Rx, 7)] 127 | #[case("m/%=model", [M], Rx, 7)] 128 | #[case("m/=%model", [M], Rx, 7)] 129 | #[case("DP-1", [D,N,V,M,S], Ft, 4)] 130 | #[case("vsDP-1", [D,N,V,M,S], Ft, 6)] 131 | fn parse_single_search_from_str_ok( 132 | #[case] s: &str, 133 | #[case] fields: impl AsRef<[SF]>, 134 | #[case] kind: SK, 135 | #[case] pattern_len: usize, 136 | ) { 137 | let ssearch = SingleSearch::from_str(s); 138 | assert!(ssearch.is_ok()); 139 | let ssearch = ssearch.unwrap(); 140 | assert_eq!(ssearch.kind, kind); 141 | let sfields: Vec<_> = ssearch.fields.iter().collect(); 142 | assert_eq!(sfields, fields.as_ref()); 143 | assert_eq!(ssearch.pattern.as_str().len(), pattern_len); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/search/query.rs: -------------------------------------------------------------------------------- 1 | use super::{CompareMethod, SearchField, SingleSearch, SingleSearchResult}; 2 | 3 | #[derive(Clone, Debug, PartialEq)] 4 | pub struct SingleQuery<'a> { 5 | search: SingleSearch, 6 | description: &'a str, 7 | model: &'a str, 8 | name: &'a str, 9 | serial: &'a str, 10 | vendor: &'a str, 11 | } 12 | 13 | impl<'a> SingleQuery<'a> { 14 | pub fn new(search: SingleSearch) -> Self { 15 | Self { 16 | search, 17 | description: Default::default(), 18 | model: Default::default(), 19 | name: Default::default(), 20 | serial: Default::default(), 21 | vendor: Default::default(), 22 | } 23 | } 24 | pub fn description(mut self, description: &'a str) -> Self { 25 | self.description = description; 26 | self 27 | } 28 | pub fn model(mut self, model: &'a str) -> Self { 29 | self.model = model; 30 | self 31 | } 32 | pub fn name(mut self, name: &'a str) -> Self { 33 | self.name = name; 34 | self 35 | } 36 | pub fn serial(mut self, serial: &'a str) -> Self { 37 | self.serial = serial; 38 | self 39 | } 40 | pub fn vendor(mut self, vendor: &'a str) -> Self { 41 | self.vendor = vendor; 42 | self 43 | } 44 | pub fn run(self) -> SingleSearchResult { 45 | let mut matches = true; 46 | let mut satisfied_fields: Vec<(SearchField, u64)> = vec![]; 47 | 48 | let operation: fn(bool, bool) -> bool = match self.search.method { 49 | // match against at least 1 field, field has to be the left parameter 50 | CompareMethod::AtleastOne => |_, b| b, 51 | // match against all given fields 52 | CompareMethod::Exact => |a, b| a && b, 53 | }; 54 | for field in self.search.fields.iter() { 55 | let (b, weight) = match field { 56 | SearchField::Description => self.search.matches_description(self.description), 57 | SearchField::Model => self.search.matches_model(self.model), 58 | SearchField::Name => self.search.matches_name(self.name), 59 | SearchField::Serial => self.search.matches_serial(self.serial), 60 | SearchField::Vendor => self.search.matches_vendor(self.vendor), 61 | }; 62 | matches = operation(matches, b); 63 | if matches { 64 | // We can unwrap here because our input comes from another FieldSet 65 | satisfied_fields.push((field, weight)); 66 | } 67 | } 68 | 69 | SingleSearchResult::new(self.search, satisfied_fields) 70 | } 71 | } 72 | 73 | #[cfg(test)] 74 | mod test { 75 | use crate::search::{ 76 | CompareMethod, FieldSet, SearchField, SearchKind, SearchPattern, SingleSearch, 77 | }; 78 | 79 | use super::SingleQuery; 80 | 81 | #[test] 82 | fn single_search() { 83 | let ssearch = SingleSearch { 84 | fields: FieldSet::new(SearchField::Name), 85 | kind: SearchKind::Fulltext, 86 | pattern: SearchPattern::Fulltext(String::from("DP-1")), 87 | method: CompareMethod::Exact, 88 | }; 89 | let ssr = SingleQuery::new(ssearch) 90 | .name("DP-1") 91 | .model("generic model") 92 | .vendor("generic vendor") 93 | .run(); 94 | 95 | assert!(ssr.is_ok()); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/search/single.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | #[allow(unused_imports)] 4 | use log::{debug, error, info, trace, warn}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use super::{CompareMethod, FieldSet, SearchKind, SearchPattern}; 8 | 9 | use super::{SearchField, SingleQuery}; 10 | 11 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 12 | pub struct SingleSearchResult { 13 | search: SingleSearch, 14 | satisfied_fields: Vec<(SearchField, u64)>, 15 | } 16 | 17 | #[derive(Clone, Debug, PartialEq)] 18 | pub struct SingleSearch { 19 | pub(crate) fields: FieldSet, 20 | pub(crate) kind: SearchKind, 21 | pub(crate) pattern: SearchPattern, 22 | pub(crate) method: CompareMethod, 23 | } 24 | 25 | impl SingleSearchResult { 26 | pub fn new(search: SingleSearch, satisfied_fields: Vec<(SearchField, u64)>) -> Self { 27 | Self { 28 | search, 29 | satisfied_fields, 30 | } 31 | } 32 | 33 | pub fn is_ok(&self) -> bool { 34 | let ssfields: Vec = self.search.fields.iter().collect(); 35 | let satisfied_fields: Vec = 36 | self.satisfied_fields.iter().map(|f| f.0).collect(); 37 | trace!( 38 | "search_result.is_ok={}\nsearch_fields: {:?}\nsatisfied: {:?}", 39 | ssfields == satisfied_fields, 40 | ssfields, 41 | satisfied_fields, 42 | ); 43 | match self.search.method { 44 | CompareMethod::AtleastOne => !self.satisfied_fields.is_empty(), 45 | CompareMethod::Exact => ssfields == satisfied_fields, 46 | } 47 | } 48 | 49 | /// Return how specific the [`SingleSearch`] matched to its input from a [`SingleQuery`]. 50 | /// 51 | /// Each [`SearchField`] rests at a certain index. `(`[`FieldSet::N`]` - 1 - index)` is taken 52 | /// to the power of 2 and then multiplied by the weight the [`SearchPattern::matches`] function 53 | /// returned. 54 | pub fn specificity(&self) -> u64 { 55 | self.satisfied_fields 56 | .iter() 57 | .enumerate() 58 | .map(|(idx, (_sf, weight))| weight * 2u64.pow((FieldSet::N - 1 - idx) as u32)) 59 | .sum() 60 | } 61 | } 62 | 63 | impl SingleSearch { 64 | pub fn new( 65 | fields: FieldSet, 66 | kind: SearchKind, 67 | pattern: SearchPattern, 68 | method: CompareMethod, 69 | ) -> Self { 70 | Self { 71 | fields, 72 | kind, 73 | pattern, 74 | method, 75 | } 76 | } 77 | 78 | fn matches_field(&self, text: &str, field: SearchField) -> (bool, u64) { 79 | if !self.fields.contains(field) { 80 | return (false, 0); 81 | } 82 | self.pattern.matches(text) 83 | } 84 | pub fn matches_description(&self, text: &str) -> (bool, u64) { 85 | self.matches_field(text, SearchField::Description) 86 | } 87 | pub fn matches_model(&self, text: &str) -> (bool, u64) { 88 | self.matches_field(text, SearchField::Model) 89 | } 90 | pub fn matches_name(&self, text: &str) -> (bool, u64) { 91 | self.matches_field(text, SearchField::Name) 92 | } 93 | pub fn matches_serial(&self, text: &str) -> (bool, u64) { 94 | self.matches_field(text, SearchField::Serial) 95 | } 96 | pub fn matches_vendor(&self, text: &str) -> (bool, u64) { 97 | self.matches_field(text, SearchField::Vendor) 98 | } 99 | pub fn query<'a>(self) -> SingleQuery<'a> { 100 | SingleQuery::new(self) 101 | } 102 | } 103 | 104 | impl Display for SingleSearch { 105 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 106 | match self.method { 107 | CompareMethod::AtleastOne => {} 108 | CompareMethod::Exact => write!(f, "{}", self.fields)?, 109 | } 110 | write!(f, "{}", self.kind.as_char())?; 111 | write!(f, "{}", self.pattern.as_str()) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/settings.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | use std::path::PathBuf; 3 | use std::time::Duration; 4 | 5 | #[allow(unused_imports)] 6 | use log::{debug, error, info, trace, warn}; 7 | use serde::{Deserialize, Serialize}; 8 | use snafu::{prelude::*, Location}; 9 | use xdg::BaseDirectories; 10 | 11 | use crate::daemon::ShikaneArgs; 12 | use crate::error; 13 | use crate::profile::Profile; 14 | 15 | #[derive(Clone, Debug)] 16 | pub struct Settings { 17 | pub profiles: VecDeque, 18 | pub skip_tests: bool, 19 | pub oneshot: bool, 20 | pub timeout: Duration, 21 | pub config_path: PathBuf, 22 | } 23 | 24 | #[derive(Default, Debug, Serialize, Deserialize)] 25 | pub struct SettingsToml { 26 | pub timeout: Option, 27 | #[serde(default, rename = "profile")] 28 | pub profiles: VecDeque, 29 | } 30 | 31 | impl Settings { 32 | pub fn from_args(args: ShikaneArgs) -> Self { 33 | let (config, path) = match parse_settings_toml(args.config) { 34 | Ok(config) => config, 35 | Err(err) => { 36 | error!("{}", error::report(err.as_ref())); 37 | std::process::exit(1); 38 | } 39 | }; 40 | 41 | let timeout = config.timeout.unwrap_or(args.timeout); 42 | 43 | Self { 44 | profiles: config.profiles, 45 | skip_tests: args.skip_tests, 46 | oneshot: args.oneshot, 47 | timeout: Duration::from_millis(timeout), 48 | config_path: path, 49 | } 50 | } 51 | 52 | pub fn reload_config(&mut self, config: Option) -> Result<(), Box> { 53 | let config = config.unwrap_or(self.config_path.clone()); 54 | debug!("reloading config from {:?}", std::fs::canonicalize(&config)); 55 | let (config, path) = parse_settings_toml(Some(config))?; 56 | self.profiles = config.profiles; 57 | self.config_path = path; 58 | Ok(()) 59 | } 60 | } 61 | 62 | fn parse_settings_toml( 63 | config_path: Option, 64 | ) -> Result<(SettingsToml, PathBuf), Box> { 65 | let config_path = match config_path { 66 | None => { 67 | let xdg_dirs = BaseDirectories::with_prefix("shikane").context(BaseDirectoriesCtx)?; 68 | xdg_dirs 69 | .place_config_file("config.toml") 70 | .context(ConfigPathCtx)? 71 | } 72 | Some(path) => path, 73 | }; 74 | let s = std::fs::read_to_string(config_path.clone()).context(ReadConfigFileCtx)?; 75 | let mut config: SettingsToml = toml::from_str(&s).context(TomlDeserializeCtx)?; 76 | config 77 | .profiles 78 | .iter_mut() 79 | .enumerate() 80 | .for_each(|(idx, p)| p.index = idx); 81 | Ok((config, config_path)) 82 | } 83 | 84 | #[derive(Debug, Snafu)] 85 | #[snafu(context(suffix(Ctx)))] 86 | pub enum SettingsError { 87 | #[snafu(display("[{location}] Problem with XDG directories"))] 88 | BaseDirectories { 89 | source: xdg::BaseDirectoriesError, 90 | location: Location, 91 | }, 92 | #[snafu(display("[{location}] Cannot read config file"))] 93 | ReadConfigFile { 94 | source: std::io::Error, 95 | location: Location, 96 | }, 97 | #[snafu(display("[{location}] Cannot place config file in XDG config directory"))] 98 | ConfigPath { 99 | source: std::io::Error, 100 | location: Location, 101 | }, 102 | #[snafu(display("[{location}] Cannot deserialize settings from TOML"))] 103 | TomlDeserialize { 104 | source: toml::de::Error, 105 | location: Location, 106 | }, 107 | } 108 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused_imports)] 2 | use log::{debug, error, info, trace, warn}; 3 | use snafu::{prelude::*, Location}; 4 | 5 | const SHIKANE_LOG_DEFAULT: &str = "warn,shikane::variant=info,shikane::daemon::state_machine=info"; 6 | 7 | pub fn setup_logging() { 8 | let log_time: Option = 9 | match std::env::var("SHIKANE_LOG_TIME").is_ok_and(|value| value.trim() == "1") { 10 | true => Some(env_logger::TimestampPrecision::Millis), 11 | false => None, 12 | }; 13 | 14 | env_logger::Builder::from_env( 15 | env_logger::Env::new() 16 | .filter_or("SHIKANE_LOG", SHIKANE_LOG_DEFAULT) 17 | .write_style_or("SHIKANE_LOG_STYLE", "auto"), 18 | ) 19 | .format_timestamp(log_time) 20 | .init(); 21 | } 22 | 23 | pub(crate) fn get_socket_path() -> Result { 24 | let wayland_display = "WAYLAND_DISPLAY"; 25 | let wayland_display = std::env::var(wayland_display).context(EnvVarCtx { 26 | var: wayland_display, 27 | })?; 28 | 29 | let xdg_dirs = xdg::BaseDirectories::new().context(BaseDirectoriesCtx)?; 30 | 31 | let path = format!("shikane-{wayland_display}.socket"); 32 | let path = xdg_dirs.place_runtime_file(path).context(SocketPathCtx)?; 33 | Ok(path) 34 | } 35 | 36 | #[derive(Debug, Snafu)] 37 | #[snafu(context(suffix(Ctx)))] 38 | #[snafu(visibility(pub(crate)))] 39 | pub enum UtilError { 40 | #[snafu(display("[{location}] Problem with XDG directories"))] 41 | BaseDirectories { 42 | source: xdg::BaseDirectoriesError, 43 | location: Location, 44 | }, 45 | #[snafu(display("[{location}] Cannot find environment variable {var}"))] 46 | EnvVar { 47 | source: std::env::VarError, 48 | location: Location, 49 | var: String, 50 | }, 51 | #[snafu(display("[{location}] Cannot place socket in XDG runtime directory"))] 52 | SocketPath { 53 | source: std::io::Error, 54 | location: Location, 55 | }, 56 | } 57 | -------------------------------------------------------------------------------- /src/variant.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | #[allow(unused_imports)] 4 | use log::{debug, error, info, trace, warn}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use crate::matching::Pairing; 8 | use crate::profile::Profile; 9 | 10 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 11 | pub struct ValidVariant { 12 | pub profile: Profile, 13 | pub pairings: Vec, 14 | pub state: VariantState, 15 | pub index: usize, 16 | } 17 | 18 | #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] 19 | pub enum VariantState { 20 | /// Initial state 21 | #[default] 22 | Untested, 23 | Testing, 24 | Applying, 25 | /// Final state 26 | Applied, 27 | /// Final state 28 | Discarded, 29 | } 30 | 31 | /// variant state machine input 32 | #[derive(Copy, Clone, Debug)] 33 | pub enum VSMInput { 34 | Succeeded, 35 | Cancelled, 36 | Failed, 37 | AtomicChangeDone, 38 | } 39 | 40 | #[derive(Clone, Copy, Debug)] 41 | pub enum VariantAction { 42 | Restart, 43 | TestVariant, 44 | ApplyVariant, 45 | TryNextVariant, 46 | ExecCmd, 47 | Inert, 48 | } 49 | 50 | impl ValidVariant { 51 | #[must_use] 52 | pub fn start(&mut self, skip_tests: bool) -> VariantAction { 53 | self.state.reset(); 54 | if skip_tests { 55 | debug!("skipping test by simulating a successful test"); 56 | self.state = VariantState::Applying; 57 | info!("Initial variant state: {} (test skipped)", self.state); 58 | return VariantAction::ApplyVariant; 59 | } 60 | info!("Initial variant state: {}", self.state); 61 | self.state.advance(VSMInput::AtomicChangeDone) 62 | } 63 | 64 | pub fn discard(&mut self) { 65 | self.state = VariantState::Discarded; 66 | } 67 | 68 | pub fn mode_deviation(&self) -> u32 { 69 | self.pairings.iter().map(|p| p.mode_deviation()).sum() 70 | } 71 | 72 | pub fn specificity(&self) -> u64 { 73 | self.pairings.iter().map(|p| p.specificity()).sum::() / self.pairings.len() as u64 74 | } 75 | pub fn idx_str(&self) -> String { 76 | format!("{},{}", self.profile.index, self.index) 77 | } 78 | } 79 | 80 | impl VariantState { 81 | /// Advance itself by the given input, returning a [`DSMAction`] 82 | #[must_use] 83 | pub fn advance(&mut self, input: VSMInput) -> VariantAction { 84 | debug!("Advancing variant state with input: {input}"); 85 | let (new_state, action) = self.next(input); 86 | *self = new_state; 87 | info!("New variant state: {}", self); 88 | action 89 | } 90 | 91 | /// Consumes itself and an input, returns a new instance of itself and a [`DSMAction`]. 92 | #[must_use] 93 | pub fn next(self, input: VSMInput) -> (Self, VariantAction) { 94 | use VSMInput::*; 95 | use VariantAction::*; 96 | use VariantState::*; 97 | match (self, input) { 98 | (Untested, AtomicChangeDone) => (Testing, TestVariant), 99 | (Untested, _) => self.warn_invalid(input), 100 | (Testing, Succeeded) => (Applying, ApplyVariant), 101 | (Testing, Cancelled) => (Discarded, Restart), 102 | (Testing, Failed) => (Discarded, TryNextVariant), 103 | (Testing, AtomicChangeDone) => (self, Inert), 104 | (Applying, Succeeded) => (Applied, ExecCmd), 105 | (Applying, Cancelled) => (Discarded, Restart), 106 | (Applying, Failed) => (Discarded, TryNextVariant), 107 | (Applying, AtomicChangeDone) => (self, Inert), 108 | (Applied, AtomicChangeDone) => (Discarded, Restart), 109 | (Applied, _) => self.warn_invalid(input), 110 | (Discarded, _) => self.warn_invalid(input), 111 | } 112 | } 113 | 114 | /// Prints a warning and does not advance itself, returns itself and [`DSMAction::Inert`]. 115 | #[must_use] 116 | fn warn_invalid(self, input: VSMInput) -> (Self, VariantAction) { 117 | warn!("Received invalid input {input} at state {self}"); 118 | (self, VariantAction::Inert) 119 | } 120 | 121 | pub fn reset(&mut self) { 122 | *self = Default::default() 123 | } 124 | } 125 | 126 | impl Display for VSMInput { 127 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 128 | let text = match self { 129 | VSMInput::Succeeded => "Succeeded", 130 | VSMInput::Cancelled => "Cancelled", 131 | VSMInput::Failed => "Failed", 132 | VSMInput::AtomicChangeDone => "AtomicChangeDone", 133 | }; 134 | write!(f, "{text}") 135 | } 136 | } 137 | 138 | impl Display for VariantState { 139 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 140 | let text = match self { 141 | VariantState::Untested => "Untested", 142 | VariantState::Testing => "Testing", 143 | VariantState::Applying => "Applying", 144 | VariantState::Applied => "Applied", 145 | VariantState::Discarded => "Discarded", 146 | }; 147 | write!(f, "{text}") 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/wl_backend.rs: -------------------------------------------------------------------------------- 1 | mod wl_store; 2 | 3 | use std::collections::VecDeque; 4 | use std::fmt::Display; 5 | use std::hash::Hash; 6 | 7 | use serde::{Deserialize, Serialize}; 8 | use snafu::{prelude::*, Location}; 9 | use wayland_client::backend::WaylandError; 10 | 11 | use crate::profile::{AdaptiveSyncState, PhysicalSize, Position, Transform}; 12 | use crate::variant::ValidVariant; 13 | 14 | pub use self::wl_store::{ForeignId, WlStore}; 15 | 16 | #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] 17 | pub struct WlGenericId(usize); 18 | 19 | pub trait WlBackend { 20 | fn apply(&mut self, variant: &ValidVariant) -> Result<(), WlConfigurationError>; 21 | fn test(&mut self, variant: &ValidVariant) -> Result<(), WlConfigurationError>; 22 | 23 | fn drain_event_queue(&mut self) -> VecDeque; 24 | fn export_heads(&self) -> Option>; 25 | fn flush(&self) -> Result<(), WaylandError>; 26 | } 27 | 28 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 29 | pub struct WlHead { 30 | base: WlBaseHead, 31 | current_mode: Option, 32 | modes: VecDeque, 33 | pub(crate) id: WlGenericId, 34 | } 35 | 36 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 37 | pub struct WlMode { 38 | base: WlBaseMode, 39 | pub(crate) id: WlGenericId, 40 | } 41 | 42 | #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] 43 | pub struct WlBaseHead { 44 | pub name: String, 45 | pub description: String, 46 | pub size: PhysicalSize, 47 | pub enabled: bool, 48 | pub position: Position, 49 | pub transform: Option, 50 | pub scale: f64, 51 | pub make: String, 52 | pub model: String, 53 | pub serial_number: String, 54 | pub adaptive_sync: Option, 55 | } 56 | 57 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] 58 | pub struct WlBaseMode { 59 | pub width: i32, 60 | pub height: i32, 61 | pub refresh: i32, 62 | pub preferred: bool, 63 | } 64 | 65 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 66 | pub enum WlBackendEvent { 67 | AtomicChangeDone, 68 | NeededResourceFinished, 69 | Succeeded, 70 | Failed, 71 | Cancelled, 72 | } 73 | 74 | #[derive(Debug, Snafu)] 75 | #[snafu(context(suffix(Ctx)))] 76 | #[snafu(visibility(pub(crate)))] 77 | pub enum WlConfigurationError { 78 | #[snafu(display( 79 | "[{location}] Incompatible number of heads supplied: got {got}, have {have}" 80 | ))] 81 | HeadCountMismatch { 82 | location: Location, 83 | got: usize, 84 | have: usize, 85 | }, 86 | #[snafu(display("[{location}] Cannot configure a dead head, head name: {head_name}"))] 87 | DeadHead { 88 | location: Location, 89 | head_name: String, 90 | }, 91 | #[snafu(display("[{location}] Cannot configure a dead mode, mode: {mode}"))] 92 | DeadMode { 93 | location: Location, 94 | mode: WlBaseMode, 95 | }, 96 | #[snafu(display("[{location}] Cannot configure an unknown head, head: {head:?}"))] 97 | UnknownHead { 98 | location: Location, 99 | head: Box, 100 | }, 101 | #[snafu(display("[{location}] Cannot configure an unknown mode, mode: {mode}"))] 102 | UnknownMode { 103 | location: Location, 104 | mode: WlBaseMode, 105 | }, 106 | } 107 | 108 | impl WlHead { 109 | pub fn name(&self) -> &str { 110 | &self.base.name 111 | } 112 | pub fn description(&self) -> &str { 113 | &self.base.description 114 | } 115 | pub fn size(&self) -> PhysicalSize { 116 | self.base.size 117 | } 118 | pub fn modes(&self) -> &VecDeque { 119 | &self.modes 120 | } 121 | pub fn enabled(&self) -> bool { 122 | self.base.enabled 123 | } 124 | pub fn current_mode(&self) -> &Option { 125 | &self.current_mode 126 | } 127 | pub fn position(&self) -> Position { 128 | self.base.position 129 | } 130 | pub fn transform(&self) -> Option { 131 | self.base.transform 132 | } 133 | pub fn scale(&self) -> f64 { 134 | self.base.scale 135 | } 136 | pub fn make(&self) -> &str { 137 | &self.base.make 138 | } 139 | pub fn model(&self) -> &str { 140 | &self.base.model 141 | } 142 | pub fn serial_number(&self) -> &str { 143 | &self.base.serial_number 144 | } 145 | pub fn adaptive_sync(&self) -> Option { 146 | self.base.adaptive_sync 147 | } 148 | pub fn wl_base_head(&self) -> &WlBaseHead { 149 | &self.base 150 | } 151 | } 152 | 153 | impl WlMode { 154 | pub fn width(&self) -> i32 { 155 | self.base.width 156 | } 157 | pub fn height(&self) -> i32 { 158 | self.base.height 159 | } 160 | pub fn refresh(&self) -> i32 { 161 | self.base.refresh 162 | } 163 | pub fn preferred(&self) -> bool { 164 | self.base.preferred 165 | } 166 | pub fn wl_base_mode(&self) -> WlBaseMode { 167 | self.base 168 | } 169 | } 170 | 171 | impl Display for WlBaseMode { 172 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 173 | write!(f, "{}x{}@{}mHz", self.width, self.height, self.refresh) 174 | } 175 | } 176 | 177 | impl Display for WlBackendEvent { 178 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 179 | let text = match self { 180 | WlBackendEvent::AtomicChangeDone => "AtomicChangeDone", 181 | WlBackendEvent::NeededResourceFinished => "NeededResourceFinished", 182 | WlBackendEvent::Succeeded => "Succeeded", 183 | WlBackendEvent::Failed => "Failed", 184 | WlBackendEvent::Cancelled => "Cancelled", 185 | }; 186 | write!(f, "{text}") 187 | } 188 | } 189 | 190 | #[derive(Debug)] 191 | pub struct LessEqWlHead<'a>(pub &'a WlHead); 192 | 193 | impl<'a> PartialEq for LessEqWlHead<'a> { 194 | fn eq(&self, other: &Self) -> bool { 195 | let (a, b) = (self.0, other.0); 196 | a.id == b.id 197 | && a.serial_number() == b.serial_number() 198 | && a.model() == b.model() 199 | && a.make() == b.make() 200 | && a.description() == b.description() 201 | && a.name() == b.name() 202 | } 203 | } 204 | 205 | impl<'a> Eq for LessEqWlHead<'a> {} 206 | 207 | impl<'a> Hash for LessEqWlHead<'a> { 208 | fn hash(&self, state: &mut H) { 209 | self.0.id.hash(state); 210 | self.0.serial_number().hash(state); 211 | self.0.model().hash(state); 212 | self.0.make().hash(state); 213 | self.0.description().hash(state); 214 | self.0.name().hash(state); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/wl_backend/wl_store.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, VecDeque}; 2 | use std::hash::Hash; 3 | 4 | use snafu::{prelude::*, Location}; 5 | 6 | use crate::wl_backend::{WlGenericId, WlHead, WlMode}; 7 | 8 | use super::{WlBaseHead, WlBaseMode}; 9 | 10 | pub trait ForeignId { 11 | type Id: std::fmt::Debug + Clone + PartialEq + Eq + Hash; 12 | 13 | fn foreign_id(&self) -> Self::Id; 14 | } 15 | 16 | #[derive(Clone, Debug)] 17 | pub struct StoreHead 18 | where 19 | H: ForeignId, 20 | { 21 | pub base: WlBaseHead, 22 | pub(crate) current_mode: Option, 23 | modes: Vec, 24 | id: WlGenericId, 25 | pub(crate) foreign_head: H, 26 | _foreign_id: I, 27 | } 28 | 29 | #[derive(Clone, Debug)] 30 | pub struct StoreMode 31 | where 32 | M: ForeignId, 33 | { 34 | pub base: WlBaseMode, 35 | id: WlGenericId, 36 | pub(crate) foreign_mode: M, 37 | _foreign_id: I, 38 | } 39 | 40 | #[derive(Clone, Debug)] 41 | pub struct WlStore 42 | where 43 | H: ForeignId, 44 | M: ForeignId, 45 | { 46 | /// A Mapping from [`WlHead`]-Ids to [`WlHead`]s 47 | // heads: HashMap>>>, 48 | heads: HashMap>, 49 | /// A Mapping from [`WlMode`]-Ids to [`WlMode`]s 50 | // modes: HashMap>>>, 51 | modes: HashMap>, 52 | /// A Mapping from [`WlMode`]-Ids to [`WlHead`]-Ids 53 | /// 54 | /// The [`WlMode`] from the key-field belongs to the [`WlHead`] in the value-field 55 | mode_id_head_id: HashMap, 56 | store_key_to_foreign_key: HashMap, 57 | id_counter: usize, 58 | } 59 | 60 | impl Default for WlStore 61 | where 62 | H: ForeignId, 63 | M: ForeignId, 64 | { 65 | fn default() -> Self { 66 | Self { 67 | heads: Default::default(), 68 | modes: Default::default(), 69 | mode_id_head_id: Default::default(), 70 | store_key_to_foreign_key: Default::default(), 71 | id_counter: Default::default(), 72 | } 73 | } 74 | } 75 | 76 | impl WlStore 77 | where 78 | H: ForeignId, 79 | M: ForeignId, 80 | I: std::fmt::Debug + Clone + PartialEq + Eq + Hash, 81 | { 82 | pub fn export(&self) -> Result, WlStoreError> { 83 | self.heads 84 | .values() 85 | .map(|head| self.export_head(head)) 86 | .collect() 87 | } 88 | 89 | pub fn heads_count(&self) -> usize { 90 | self.heads.len() 91 | } 92 | 93 | pub fn head(&self, head_id: I) -> Result<&StoreHead, WlStoreError> { 94 | self.heads 95 | .get(&head_id) 96 | .context(HeadNotFoundCtx { head_id }) 97 | } 98 | pub fn mode(&self, mode_id: I) -> Result<&StoreMode, WlStoreError> { 99 | self.modes 100 | .get(&mode_id) 101 | .context(ModeNotFoundCtx { mode_id }) 102 | } 103 | pub fn mode_store_key( 104 | &self, 105 | mode_id: WlGenericId, 106 | ) -> Result<&StoreMode, WlStoreError> { 107 | let mode_id = self 108 | .store_key_to_foreign_key 109 | .get(&mode_id) 110 | .context(UnknownStoreKeyCtx { key: mode_id })?; 111 | self.mode(mode_id.clone()) 112 | } 113 | pub fn head_store_key( 114 | &self, 115 | head_id: WlGenericId, 116 | ) -> Result<&StoreHead, WlStoreError> { 117 | let head_id = self 118 | .store_key_to_foreign_key 119 | .get(&head_id) 120 | .context(UnknownStoreKeyCtx { key: head_id })?; 121 | self.head(head_id.clone()) 122 | } 123 | 124 | pub fn head_mut(&mut self, head_id: I) -> Result<&mut StoreHead, WlStoreError> { 125 | // let head_id = head_id.clone(); 126 | self.heads 127 | .get_mut(&head_id) 128 | .context(HeadNotFoundCtx { head_id }) 129 | } 130 | pub fn mode_mut(&mut self, mode_id: I) -> Result<&mut StoreMode, WlStoreError> { 131 | // let mode_id = mode_id.clone(); 132 | self.modes 133 | .get_mut(&mode_id) 134 | .context(ModeNotFoundCtx { mode_id }) 135 | } 136 | 137 | pub fn insert_head(&mut self, foreign_head: H) { 138 | let foreign_head_id = foreign_head.foreign_id(); 139 | let store_head = StoreHead { 140 | base: Default::default(), 141 | current_mode: Default::default(), 142 | modes: Default::default(), 143 | id: self.new_store_id(), 144 | foreign_head, 145 | _foreign_id: foreign_head_id.clone(), 146 | }; 147 | self.store_key_to_foreign_key 148 | .insert(store_head.id, foreign_head_id.clone()); 149 | self.heads.insert(foreign_head_id, store_head); 150 | } 151 | 152 | pub fn insert_mode(&mut self, foreign_head_id: I, foreign_mode: M) { 153 | let foreign_mode_id = foreign_mode.foreign_id(); 154 | let store_mode = StoreMode { 155 | base: Default::default(), 156 | id: self.new_store_id(), 157 | foreign_mode, 158 | _foreign_id: foreign_mode_id.clone(), 159 | }; 160 | self.store_key_to_foreign_key 161 | .insert(store_mode.id, foreign_mode_id.clone()); 162 | self.heads 163 | .entry(foreign_head_id.clone()) 164 | .and_modify(|head| head.modes.push(foreign_mode_id.clone())); 165 | 166 | self.mode_id_head_id 167 | .insert(foreign_mode_id.clone(), foreign_head_id); 168 | self.modes.insert(foreign_mode_id, store_mode); 169 | } 170 | /// This function removes all occurences of the provided `Id` of the mode in [`WlStore`]. 171 | pub fn remove_mode(&mut self, mode_id: &I) -> Result<(), WlStoreError> { 172 | // the Id of the head the mode belongs to 173 | let head_id = self.mode_id_head_id.remove(mode_id).ok_or( 174 | ModeNotFoundCtx { 175 | mode_id: mode_id.clone(), 176 | } 177 | .build(), 178 | )?; 179 | let head = self 180 | .heads 181 | .get_mut(&head_id) 182 | .ok_or(HeadNotFoundCtx { head_id }.build())?; 183 | 184 | if let Some(c_mode_id) = &head.current_mode { 185 | if *c_mode_id == *mode_id { 186 | head.current_mode = None; 187 | } 188 | } 189 | head.modes.retain(|id| *id != *mode_id); 190 | 191 | Ok(()) 192 | } 193 | pub fn remove_head(&mut self, head_id: &I) { 194 | self.heads.remove(head_id); 195 | } 196 | 197 | fn export_head(&self, head: &StoreHead) -> Result> { 198 | let StoreHead { 199 | base, 200 | current_mode: store_head_current_mode, 201 | modes, 202 | id, 203 | foreign_head: _, 204 | _foreign_id: _, 205 | } = head; 206 | 207 | let mut current_mode: Option = None; 208 | if let Some(current_mode_id) = store_head_current_mode { 209 | let store_mode = self.mode(current_mode_id.clone())?; 210 | 211 | current_mode = Some(WlMode { 212 | base: store_mode.base, 213 | id: store_mode.id, 214 | }); 215 | } 216 | 217 | let modes = modes 218 | .iter() 219 | .map(|mi| self.mode(mi.clone())) 220 | .filter_map(|r| r.ok()) 221 | .map(|m| WlMode { 222 | base: m.base, 223 | id: m.id, 224 | }) 225 | .collect(); 226 | 227 | let wl_head = WlHead { 228 | base: base.clone(), 229 | current_mode, 230 | modes, 231 | id: *id, 232 | }; 233 | Ok(wl_head) 234 | } 235 | 236 | // fn export_mode(&self, mode_id: &I) -> Result> { 237 | // let StoreMode { inner, wl_mode } = self.mode(mode_id)?; 238 | // Ok(tmode_from(*inner, wl_mode.clone())) 239 | // } 240 | 241 | fn new_store_id(&mut self) -> WlGenericId { 242 | self.id_counter += 1; 243 | WlGenericId(self.id_counter) 244 | } 245 | } 246 | 247 | #[derive(Debug, Snafu)] 248 | #[snafu(context(suffix(Ctx)))] 249 | #[snafu(visibility(pub(crate)))] 250 | pub enum WlStoreError { 251 | #[snafu(display("[{location}] Cannot find head in store: {head_id:?}"))] 252 | HeadNotFound { location: Location, head_id: I }, 253 | #[snafu(display("[{location}] Cannot find mode in store: {mode_id:?}"))] 254 | ModeNotFound { location: Location, mode_id: I }, 255 | #[snafu(display("[{location}] Cannot find store key: {key:?}"))] 256 | UnknownStoreKey { 257 | location: Location, 258 | key: WlGenericId, 259 | }, 260 | } 261 | -------------------------------------------------------------------------------- /src/wlroots.rs: -------------------------------------------------------------------------------- 1 | mod om_configuration; 2 | mod om_head; 3 | mod om_manager; 4 | mod om_mode; 5 | mod wl_registry; 6 | 7 | use std::collections::VecDeque; 8 | 9 | use calloop_wayland_source::WaylandSource; 10 | #[allow(unused_imports)] 11 | use log::{debug, error, info, trace, warn}; 12 | use snafu::{prelude::*, Location}; 13 | use wayland_client::backend::{ObjectId, WaylandError}; 14 | use wayland_client::globals::BindError; 15 | use wayland_client::protocol::wl_output::Transform as WlTransform; 16 | use wayland_client::{Connection, Proxy, QueueHandle}; 17 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_configuration_v1::ZwlrOutputConfigurationV1; 18 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_head_v1::AdaptiveSyncState as WlAdaptiveSyncState; 19 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_head_v1::ZwlrOutputHeadV1; 20 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_manager_v1::ZwlrOutputManagerV1; 21 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_mode_v1::ZwlrOutputModeV1; 22 | 23 | use crate::error; 24 | use crate::matching::Pairing; 25 | use crate::profile::{AdaptiveSyncState, Transform}; 26 | use crate::profile::{Mode, Output}; 27 | use crate::variant::ValidVariant; 28 | use crate::wl_backend::{ 29 | DeadHeadCtx, DeadModeCtx, ForeignId, HeadCountMismatchCtx, UnknownHeadCtx, UnknownModeCtx, 30 | WlBackend, WlBackendEvent, WlConfigurationError, WlHead, WlStore, 31 | }; 32 | 33 | type WlrootsStore = WlStore; 34 | 35 | #[derive(Debug)] 36 | pub struct WlrootsBackend { 37 | wl_store: WlrootsStore, 38 | wlr_output_manager: ZwlrOutputManagerV1, 39 | wlr_output_manager_serial: u32, 40 | connection: Connection, 41 | queue_handle: QueueHandle, 42 | event_queue: VecDeque, 43 | } 44 | 45 | impl WlBackend for WlrootsBackend { 46 | fn apply(&mut self, variant: &ValidVariant) -> Result<(), WlConfigurationError> { 47 | self.configure_variant(variant)?.apply(); 48 | Ok(()) 49 | } 50 | 51 | fn test(&mut self, variant: &ValidVariant) -> Result<(), WlConfigurationError> { 52 | self.configure_variant(variant)?.test(); 53 | Ok(()) 54 | } 55 | 56 | fn drain_event_queue(&mut self) -> VecDeque { 57 | std::mem::take(&mut self.event_queue) 58 | } 59 | 60 | fn export_heads(&self) -> Option> { 61 | self.export_wl_heads() 62 | } 63 | 64 | fn flush(&self) -> Result<(), WaylandError> { 65 | self.connection.flush() 66 | } 67 | } 68 | 69 | impl WlrootsBackend { 70 | pub fn connect() -> Result<(Self, WaylandSource), WlrootsBackendError> { 71 | let connection = Connection::connect_to_env().context(WaylandConnectionCtx)?; 72 | let (globals, event_queue) = 73 | wayland_client::globals::registry_queue_init(&connection).context(RegistryGlobalCtx)?; 74 | let queue_handle = event_queue.handle(); 75 | let wlr_output_manager: ZwlrOutputManagerV1 = globals 76 | .bind(&queue_handle, 3..=4, ()) 77 | .map_err(|e| match e { 78 | BindError::UnsupportedVersion => UnsupportedVersionCtx {}.build(), 79 | BindError::NotPresent => GlobalNotPresentCtx {}.build(), 80 | })?; 81 | 82 | let backend = Self { 83 | wl_store: Default::default(), 84 | wlr_output_manager, 85 | wlr_output_manager_serial: Default::default(), 86 | connection: connection.clone(), 87 | queue_handle, 88 | event_queue: Default::default(), 89 | }; 90 | 91 | Ok((backend, WaylandSource::new(connection, event_queue))) 92 | } 93 | 94 | fn export_wl_heads(&self) -> Option> { 95 | match self.wl_store.export() { 96 | Ok(heads) => Some(heads), 97 | Err(err) => { 98 | warn!("{}", error::report(&err)); 99 | None 100 | } 101 | } 102 | } 103 | 104 | pub fn queue_event(&mut self, event: WlBackendEvent) { 105 | self.event_queue.push_back(event) 106 | } 107 | } 108 | 109 | // impl for configuration 110 | impl WlrootsBackend { 111 | fn create_configuration(&mut self) -> ZwlrOutputConfigurationV1 { 112 | self.wlr_output_manager.create_configuration( 113 | self.wlr_output_manager_serial, 114 | &self.queue_handle, 115 | (), 116 | ) 117 | } 118 | 119 | fn configure_variant( 120 | &mut self, 121 | variant: &ValidVariant, 122 | ) -> Result { 123 | let got = variant.pairings.len(); 124 | let have = self.wl_store.heads_count(); 125 | if got != have { 126 | return HeadCountMismatchCtx { got, have }.fail(); 127 | } 128 | let wlr_conf = self.create_configuration(); 129 | for pair in variant.pairings.iter() { 130 | let res = configure_head_with_pairing( 131 | &wlr_conf, 132 | pair, 133 | &self.wl_store, 134 | &self.queue_handle, 135 | self.wlr_output_manager.version(), 136 | ); 137 | 138 | if let Err(err) = res { 139 | wlr_conf.destroy(); 140 | return Err(err); 141 | } 142 | } 143 | Ok(wlr_conf) 144 | } 145 | } 146 | 147 | fn configure_head_with_pairing( 148 | wlr_conf: &ZwlrOutputConfigurationV1, 149 | pairing: &Pairing, 150 | wl_store: &WlrootsStore, 151 | qh: &QueueHandle, 152 | wlr_om_version: u32, 153 | ) -> Result<(), WlConfigurationError> { 154 | let wl_head: &WlHead = pairing.wl_head(); 155 | let output: &Output = pairing.output(); 156 | let wlr_head: &ZwlrOutputHeadV1 = match wl_store.head_store_key(wl_head.id) { 157 | Ok(store_head) => &store_head.foreign_head, 158 | Err(err) => { 159 | warn!("{}", error::report(&err)); 160 | return UnknownHeadCtx { 161 | head: pairing.wl_head().wl_base_head().clone(), 162 | } 163 | .fail(); 164 | } 165 | }; 166 | // Cannot configure a head that is not alive 167 | if !wlr_head.is_alive() { 168 | let head_name = wl_head.name(); 169 | return DeadHeadCtx { head_name }.fail(); 170 | } 171 | 172 | // Disable the head if is disabled in the config 173 | if !output.enable { 174 | wlr_conf.disable_head(wlr_head); 175 | return Ok(()); 176 | } 177 | 178 | // Enable the head and set its properties 179 | let wlr_conf_head = wlr_conf.enable_head(wlr_head, qh, ()); 180 | 181 | // Mode 182 | if let Some(smode) = output.mode { 183 | if let Mode::WiHeReCustom(width, height, refresh) = smode { 184 | trace!("Setting Mode: {smode}"); 185 | wlr_conf_head.set_custom_mode(width, height, refresh); 186 | } else if let Some(wl_mode) = pairing.wl_mode() { 187 | let wl_base_mode = wl_mode.wl_base_mode(); 188 | let wlr_mode = match wl_store.mode_store_key(wl_mode.id) { 189 | Ok(store_mode) => &store_mode.foreign_mode, 190 | Err(err) => { 191 | warn!("{}", error::report(&err)); 192 | return UnknownModeCtx { mode: wl_base_mode }.fail(); 193 | } 194 | }; 195 | // Cannot configure a mode that is not alive 196 | if !wlr_mode.is_alive() { 197 | return DeadModeCtx { mode: wl_base_mode }.fail(); 198 | } 199 | trace!("Setting Mode: {smode} | {}", wl_base_mode); 200 | wlr_conf_head.set_mode(wlr_mode); 201 | } 202 | } 203 | 204 | // Position 205 | if let Some(pos) = output.position { 206 | trace!("Setting Position: {}", pos); 207 | wlr_conf_head.set_position(pos.x, pos.y); 208 | } 209 | 210 | // Scale 211 | if let Some(scale) = output.scale { 212 | trace!("Setting Scale: {}", scale); 213 | wlr_conf_head.set_scale(scale); 214 | } 215 | 216 | // Transform 217 | if let Some(transform) = output.transform { 218 | trace!("Setting Transform: {transform}"); 219 | wlr_conf_head.set_transform(transform.into()); 220 | } 221 | 222 | // Adaptive Sync 223 | if let Some(adaptive_sync) = output.adaptive_sync { 224 | if wlr_om_version >= 4 { 225 | trace!("Setting Adaptive Sync: {adaptive_sync}"); 226 | wlr_conf_head.set_adaptive_sync(adaptive_sync.into()); 227 | } else { 228 | let msg = format!("Cannot set adaptive_sync to {adaptive_sync}."); 229 | let msg = format!("{msg} wlr-output-management protocol version >= 4 needed."); 230 | warn!("{msg} Have version {wlr_om_version}"); 231 | } 232 | } 233 | 234 | Ok(()) 235 | } 236 | 237 | impl ForeignId for ZwlrOutputHeadV1 { 238 | type Id = ObjectId; 239 | fn foreign_id(&self) -> Self::Id { 240 | self.id() 241 | } 242 | } 243 | impl ForeignId for ZwlrOutputModeV1 { 244 | type Id = ObjectId; 245 | fn foreign_id(&self) -> Self::Id { 246 | self.id() 247 | } 248 | } 249 | 250 | impl From for WlAdaptiveSyncState { 251 | fn from(value: AdaptiveSyncState) -> Self { 252 | match value { 253 | AdaptiveSyncState::Disabled => Self::Disabled, 254 | AdaptiveSyncState::Enabled => Self::Enabled, 255 | } 256 | } 257 | } 258 | 259 | impl From for WlTransform { 260 | fn from(value: Transform) -> Self { 261 | match value { 262 | Transform::Normal => Self::Normal, 263 | Transform::_90 => Self::_90, 264 | Transform::_180 => Self::_180, 265 | Transform::_270 => Self::_270, 266 | Transform::Flipped => Self::Flipped, 267 | Transform::Flipped90 => Self::Flipped90, 268 | Transform::Flipped180 => Self::Flipped180, 269 | Transform::Flipped270 => Self::Flipped270, 270 | } 271 | } 272 | } 273 | 274 | #[derive(Debug, Snafu)] 275 | #[snafu(context(suffix(Ctx)))] 276 | #[snafu(visibility(pub(crate)))] 277 | pub enum WlrootsBackendError { 278 | #[snafu(display("[{location}] wlr-output-management protocol version < 3 is not supported"))] 279 | UnsupportedVersion { location: Location }, 280 | #[snafu(display("[{location}] wlr_output_manager global not present"))] 281 | GlobalNotPresent { location: Location }, 282 | #[snafu(display("[{location}] Failed to retrieve Wayland globals from registry"))] 283 | RegistryGlobal { 284 | source: wayland_client::globals::GlobalError, 285 | location: Location, 286 | }, 287 | #[snafu(display("[{location}] Cannot connect to Wayland server"))] 288 | WaylandConnection { 289 | source: wayland_client::ConnectError, 290 | location: Location, 291 | }, 292 | #[snafu(display("[{location}] Failed to flush connection to Wayland server"))] 293 | WaylandConnectionFlush { 294 | source: wayland_client::backend::WaylandError, 295 | location: Location, 296 | }, 297 | #[snafu(display("[{location}] Cannot get wayland object from specified ID"))] 298 | WaylandInvalidId { 299 | source: wayland_client::backend::InvalidId, 300 | location: Location, 301 | }, 302 | #[snafu(display("[{location}] Unable to release resources associated with destroyed mode"))] 303 | ReleaseOutputMode { location: Location }, 304 | } 305 | -------------------------------------------------------------------------------- /src/wlroots/om_configuration.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused_imports)] 2 | use log::{debug, error, info, trace, warn}; 3 | use wayland_client::{Connection, Dispatch, Proxy, QueueHandle}; 4 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_configuration_head_v1::ZwlrOutputConfigurationHeadV1; 5 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_configuration_v1::Event as OutputConfigurationEvent; 6 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_configuration_v1::ZwlrOutputConfigurationV1; 7 | 8 | use crate::wl_backend::WlBackendEvent; 9 | 10 | use super::WlrootsBackend; 11 | 12 | impl Dispatch for WlrootsBackend { 13 | fn event( 14 | backend: &mut Self, 15 | wlr_configuration: &ZwlrOutputConfigurationV1, 16 | event: ::Event, 17 | _: &(), 18 | _: &Connection, 19 | _: &QueueHandle, 20 | ) { 21 | trace!("[Event] {:?}", event); 22 | wlr_configuration.destroy(); 23 | match event { 24 | OutputConfigurationEvent::Succeeded => { 25 | backend.queue_event(WlBackendEvent::Succeeded); 26 | } 27 | OutputConfigurationEvent::Failed => { 28 | backend.queue_event(WlBackendEvent::Failed); 29 | } 30 | OutputConfigurationEvent::Cancelled => { 31 | backend.queue_event(WlBackendEvent::Cancelled); 32 | } 33 | unknown => warn!("[Event] unknown event received: {unknown:?}"), 34 | }; 35 | } 36 | } 37 | 38 | impl Dispatch for WlrootsBackend { 39 | fn event( 40 | _: &mut Self, 41 | _: &ZwlrOutputConfigurationHeadV1, 42 | event: ::Event, 43 | _: &(), 44 | _: &Connection, 45 | _: &QueueHandle, 46 | ) { 47 | trace!("[Event] {:?}", event); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/wlroots/om_head.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused_imports)] 2 | use log::{debug, error, info, trace, warn}; 3 | use wayland_client::protocol::wl_output::Transform as WlTransform; 4 | use wayland_client::{event_created_child, WEnum}; 5 | use wayland_client::{Connection, Dispatch, Proxy, QueueHandle}; 6 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_head_v1::AdaptiveSyncState as ZwlrAdaptiveSyncState; 7 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_head_v1::Event as ZwlrOutputHeadEvent; 8 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_head_v1::ZwlrOutputHeadV1; 9 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_head_v1::EVT_MODE_OPCODE; 10 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_manager_v1::ZwlrOutputManagerV1; 11 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_mode_v1::ZwlrOutputModeV1; 12 | 13 | use crate::error; 14 | use crate::profile::{AdaptiveSyncState, Transform}; 15 | 16 | use super::WlrootsBackend; 17 | 18 | impl Dispatch for WlrootsBackend { 19 | fn event( 20 | backend: &mut Self, 21 | wlr_head: &ZwlrOutputHeadV1, 22 | event: ::Event, 23 | _: &(), 24 | _: &Connection, 25 | _: &QueueHandle, 26 | ) { 27 | let head = match backend.wl_store.head_mut(wlr_head.id()) { 28 | Ok(head) => head, 29 | Err(err) => { 30 | warn!("{}", error::report(&err)); 31 | return; 32 | } 33 | }; 34 | 35 | // Update the properties of a head 36 | match event { 37 | ZwlrOutputHeadEvent::Name { name } => { 38 | trace!("[Event::Name] {:?}", name); 39 | head.base.name = name 40 | } 41 | ZwlrOutputHeadEvent::Description { description } => { 42 | trace!("[Event::Description] {:?}", description); 43 | head.base.description = description 44 | } 45 | ZwlrOutputHeadEvent::PhysicalSize { width, height } => { 46 | trace!( 47 | "[Event::PhysicalSize] width: {:?}, height: {:?}", 48 | width, 49 | height 50 | ); 51 | (head.base.size.width, head.base.size.height) = (width, height) 52 | } 53 | ZwlrOutputHeadEvent::Mode { mode } => { 54 | trace!("[Event::Mode] id: {:?}", mode.id()); 55 | backend.wl_store.insert_mode(wlr_head.id(), mode); 56 | } 57 | ZwlrOutputHeadEvent::Enabled { enabled } => { 58 | head.base.enabled = !matches!(enabled, 0); 59 | trace!("[Event::Enabled] {}", head.base.enabled); 60 | } 61 | ZwlrOutputHeadEvent::CurrentMode { mode } => { 62 | trace!("[Event::CurrentMode] id: {:?}", mode.id()); 63 | head.current_mode = Some(mode.id()) 64 | } 65 | ZwlrOutputHeadEvent::Position { x, y } => { 66 | trace!("[Event::Position] x: {:?}, y: {:?}", x, y); 67 | (head.base.position.x, head.base.position.y) = (x, y) 68 | } 69 | ZwlrOutputHeadEvent::Transform { transform } => { 70 | let event_prefix = "[Event::Transform]"; 71 | let transform = wenum_extract(event_prefix, transform) 72 | .and_then(|wlr_transform| transform_try_into(event_prefix, wlr_transform)); 73 | head.base.transform = transform 74 | } 75 | ZwlrOutputHeadEvent::Scale { scale } => { 76 | trace!("[Event::Scale] {:?}", scale); 77 | head.base.scale = scale 78 | } 79 | ZwlrOutputHeadEvent::Finished => { 80 | trace!("[Event::Finished]"); 81 | wlr_head.release(); 82 | backend.wl_store.remove_head(&wlr_head.id()); 83 | } 84 | ZwlrOutputHeadEvent::Make { make } => { 85 | trace!("[Event::Make] {:?}", make); 86 | head.base.make = make 87 | } 88 | ZwlrOutputHeadEvent::Model { model } => { 89 | trace!("[Event::Model] {:?}", model); 90 | head.base.model = model 91 | } 92 | ZwlrOutputHeadEvent::SerialNumber { serial_number } => { 93 | trace!("[Event::SerialNumber] {:?}", serial_number); 94 | head.base.serial_number = serial_number 95 | } 96 | ZwlrOutputHeadEvent::AdaptiveSync { state } => { 97 | let event_prefix = "[Event::AdaptiveSync]"; 98 | let ass = wenum_extract(event_prefix, state) 99 | .and_then(|wlr_ass| ass_try_into(event_prefix, wlr_ass)); 100 | head.base.adaptive_sync = ass; 101 | } 102 | unknown => { 103 | warn!("[Event] unknown event received {unknown:?}") 104 | } 105 | } 106 | } 107 | 108 | event_created_child!(WlrootsBackend, ZwlrOutputManagerV1, [ 109 | EVT_MODE_OPCODE => (ZwlrOutputModeV1, ()), 110 | ]); 111 | } 112 | 113 | fn wenum_extract(event_prefix: &str, wenum: WEnum) -> Option { 114 | let w_value_err = "The stored value does not match one defined by the protocol file"; 115 | match wenum.into_result() { 116 | Ok(inner) => { 117 | trace!("{event_prefix} {:?}", inner); 118 | Some(inner) 119 | } 120 | Err(err) => { 121 | warn!("{event_prefix} {w_value_err}: {:?}", err); 122 | None 123 | } 124 | } 125 | } 126 | 127 | fn ass_try_into(event_prefix: &str, wlr_ass: ZwlrAdaptiveSyncState) -> Option { 128 | match wlr_ass { 129 | ZwlrAdaptiveSyncState::Disabled => Some(AdaptiveSyncState::Disabled), 130 | ZwlrAdaptiveSyncState::Enabled => Some(AdaptiveSyncState::Enabled), 131 | unknown => { 132 | warn!("{event_prefix} unknown adaptive sync state: {unknown:?}"); 133 | None 134 | } 135 | } 136 | } 137 | 138 | fn transform_try_into(event_prefix: &str, wl_transform: WlTransform) -> Option { 139 | match wl_transform { 140 | WlTransform::Normal => Some(Transform::Normal), 141 | WlTransform::_90 => Some(Transform::_90), 142 | WlTransform::_180 => Some(Transform::_180), 143 | WlTransform::_270 => Some(Transform::_270), 144 | WlTransform::Flipped => Some(Transform::Flipped), 145 | WlTransform::Flipped90 => Some(Transform::Flipped90), 146 | WlTransform::Flipped180 => Some(Transform::Flipped180), 147 | WlTransform::Flipped270 => Some(Transform::Flipped270), 148 | unknown => { 149 | warn!("{event_prefix} unknown transform: {unknown:?}"); 150 | None 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/wlroots/om_manager.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused_imports)] 2 | use log::{debug, error, info, trace, warn}; 3 | use wayland_client::event_created_child; 4 | use wayland_client::{Connection, Dispatch, Proxy, QueueHandle}; 5 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_head_v1::ZwlrOutputHeadV1; 6 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_manager_v1::Event as ZwlrOutputManagerEvent; 7 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_manager_v1::ZwlrOutputManagerV1; 8 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_manager_v1::EVT_HEAD_OPCODE; 9 | 10 | use crate::wl_backend::WlBackendEvent; 11 | 12 | use super::WlrootsBackend; 13 | 14 | impl Dispatch for WlrootsBackend { 15 | fn event( 16 | backend: &mut Self, 17 | _: &ZwlrOutputManagerV1, 18 | event: ::Event, 19 | _: &(), 20 | _: &Connection, 21 | _: &QueueHandle, 22 | ) { 23 | match event { 24 | ZwlrOutputManagerEvent::Head { head } => { 25 | trace!("[Event::Head] id: {:?}", head.id()); 26 | backend.wl_store.insert_head(head) 27 | } 28 | ZwlrOutputManagerEvent::Done { serial } => { 29 | trace!("[Event::Done] serial: {}", serial); 30 | backend.wlr_output_manager_serial = serial; 31 | backend.queue_event(WlBackendEvent::AtomicChangeDone); 32 | } 33 | ZwlrOutputManagerEvent::Finished => { 34 | trace!("[Event::Finished]"); 35 | backend.wlr_output_manager_serial = 0; 36 | backend.queue_event(WlBackendEvent::NeededResourceFinished); 37 | } 38 | unknown => warn!("[Event] Unknown event received: {unknown:?}"), 39 | } 40 | } 41 | 42 | event_created_child!(WlrootsBackend, ZwlrOutputManagerV1, [ 43 | EVT_HEAD_OPCODE=> (ZwlrOutputHeadV1, ()), 44 | ]); 45 | } 46 | -------------------------------------------------------------------------------- /src/wlroots/om_mode.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused_imports)] 2 | use log::{debug, error, info, trace, warn}; 3 | use wayland_client::{Connection, Dispatch, Proxy, QueueHandle}; 4 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_mode_v1::Event as ZwlrOutputModeEvent; 5 | use wayland_protocols_wlr::output_management::v1::client::zwlr_output_mode_v1::ZwlrOutputModeV1; 6 | 7 | use crate::error; 8 | 9 | use super::WlrootsBackend; 10 | 11 | impl Dispatch for WlrootsBackend { 12 | fn event( 13 | backend: &mut Self, 14 | wlr_mode: &ZwlrOutputModeV1, 15 | event: ::Event, 16 | _: &(), 17 | _: &Connection, 18 | _: &QueueHandle, 19 | ) { 20 | let mode = match backend.wl_store.mode_mut(wlr_mode.id()) { 21 | Ok(mode) => mode, 22 | Err(err) => { 23 | warn!("{}", error::report(&err)); 24 | return; 25 | } 26 | }; 27 | 28 | // Update the properties of a mode 29 | match event { 30 | ZwlrOutputModeEvent::Size { width, height } => { 31 | trace!("[Event::Size] width: {:?}, height: {:?}", width, height); 32 | (mode.base.width, mode.base.height) = (width, height) 33 | } 34 | ZwlrOutputModeEvent::Refresh { refresh } => { 35 | trace!("[Event::Refresh] {:?}", refresh); 36 | mode.base.refresh = refresh 37 | } 38 | ZwlrOutputModeEvent::Preferred => { 39 | // I'm not sure if the server can change the preferation of a mode. 40 | trace!("[Event::Preferred]"); 41 | mode.base.preferred = true 42 | } 43 | ZwlrOutputModeEvent::Finished => { 44 | trace!("[Event::Finished]"); 45 | // After receiving the Finished event for a `ZwlrOutputModeV1` the mode must not be used anymore. 46 | wlr_mode.release(); 47 | // Thus removing the mode from the store. 48 | if let Err(err) = backend.wl_store.remove_mode(&wlr_mode.id()) { 49 | warn!("{:?}", error::report(&err)); 50 | } 51 | } 52 | _ => warn!("[Event] unknown event received: {:?}", event), 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/wlroots/wl_registry.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused_imports)] 2 | use log::{debug, error, info, trace, warn}; 3 | use wayland_client::globals::GlobalListContents; 4 | use wayland_client::{protocol::wl_registry, Connection, Dispatch, QueueHandle}; 5 | 6 | use super::WlrootsBackend; 7 | 8 | impl Dispatch for WlrootsBackend { 9 | fn event( 10 | _: &mut Self, 11 | _: &wl_registry::WlRegistry, 12 | _: wl_registry::Event, 13 | _: &GlobalListContents, 14 | _: &Connection, 15 | _: &QueueHandle, 16 | ) { 17 | } 18 | } 19 | --------------------------------------------------------------------------------