[
  {
    "path": ".clang-format",
    "content": "Language: Cpp\nStandard: Latest\nBasedOnStyle: LLVM\n\nColumnLimit: 100\nIndentWidth: 4\nTabWidth: 4\nUseTab: ForContinuationAndIndentation\n\nBreakBeforeBraces: Allman\n\nAccessModifierOffset: -4\n\nPointerAlignment: Left\n\nAlignAfterOpenBracket: AlwaysBreak\nBinPackArguments: false\nBinPackParameters: false\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: H-M-H\nliberapay: HMH\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build\n\non:\n  push:\n    branches: [ '*' ]\n    tags:\n      - v*\n  pull_request:\n    branches: [ master ]\n\njobs:\n  build-docker:\n    runs-on: ubuntu-latest\n    container: docker://hhmhh/weylus_build:latest\n    steps:\n    - uses: actions/checkout@v6\n    - uses: actions/cache@v5\n      with:\n        path: deps/dist*\n        key: ${{ runner.os }}-deps-${{ hashFiles('deps/*.sh', 'deps/*.patch') }}\n    - uses: actions/cache@v5\n      with:\n        path: |\n          ~/.cargo/registry\n          ~/.cargo/git\n          target\n        key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }}\n    - name: Build\n      run: ./docker_build.sh\n      shell: bash\n    - name: Artifacts1\n      uses: actions/upload-artifact@v6\n      with:\n        name: linux\n        path: packages/weylus-linux.zip\n    - name: Artifacts2\n      uses: actions/upload-artifact@v6\n      with:\n        name: linux-deb\n        path: packages/Weylus*.deb\n    - name: Artifacts3\n      uses: actions/upload-artifact@v6\n      with:\n        name: windows\n        path: packages/weylus-windows.zip\n    - name: Publish\n      uses: softprops/action-gh-release@v2\n      if: startsWith(github.ref, 'refs/tags/')\n      with:\n          files: |\n            packages/weylus-linux.zip\n            packages/Weylus*.deb\n            packages/weylus-windows.zip\n          prerelease: false\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n  build-docker-alpine:\n    runs-on: ubuntu-latest\n    container: docker://hhmhh/weylus_build_alpine:latest\n    steps:\n    - uses: actions/checkout@v6\n    - uses: actions/cache@v5\n      with:\n        path: deps/dist*\n        key: ${{ runner.os }}-alpine-deps-${{ hashFiles('deps/*.sh', 'deps/*.patch') }}\n    - uses: actions/cache@v5\n      with:\n        path: |\n          ~/.cargo/registry\n          ~/.cargo/git\n          target\n        key: ${{ runner.os }}-alpine-cargo-${{ hashFiles('Cargo.lock') }}\n    - name: Build\n      run: RUSTFLAGS='-C target-feature=-crt-static' cargo build --release && cd target/release && tar czf weylus-linux-alpine-musl.tar.gz weylus\n      shell: bash\n    - name: Artifacts1\n      uses: actions/upload-artifact@v6\n      with:\n        name: linux-alpine-musl\n        path: target/release/weylus-linux-alpine-musl.tar.gz\n    - name: Publish\n      uses: softprops/action-gh-release@v2\n      if: startsWith(github.ref, 'refs/tags/')\n      with:\n          files: |\n            target/release/weylus-linux-alpine-musl.tar.gz\n          prerelease: false\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n  build-macos:\n    strategy:\n      matrix:\n        os: [macos-latest, macos-15-intel] # -latest is for Apple Silicon, -15-intel is for Intel\n    runs-on: ${{ matrix.os }}\n    steps:\n    - uses: actions/checkout@v6\n    - uses: actions/cache@v5\n      with:\n        path: deps/dist\n        key: ${{ runner.os }}-deps-${{ hashFiles('deps/*.sh', 'deps/*.patch') }}\n    - uses: actions/cache@v5\n      with:\n        path: |\n          ~/.cargo/registry\n          ~/.cargo/git\n          target\n        key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }}\n    - name: Download deps\n      run: |\n        npm install -g typescript\n        brew install nasm\n        cargo install cargo-bundle\n      shell: bash\n    - name: Build\n      # run: MACOSX_DEPLOYMENT_TARGET=10.13 cargo bundle --release\n      run: cargo bundle --release\n    - name: Package\n      run: |\n        MACOS_BUILD_NAME=macos-$([ \"${{ matrix.os }}\" == \"macos-latest\" ] && echo \"arm\" || echo \"intel\")\n        echo \"MACOS_BUILD_NAME=$MACOS_BUILD_NAME\" >> $GITHUB_ENV\n        cd target/release/bundle/osx/ && zip -r ${MACOS_BUILD_NAME}.zip Weylus.app\n    - name: Artifacts\n      uses: actions/upload-artifact@v6\n      with:\n        name: ${{ env.MACOS_BUILD_NAME }}\n        path: target/release/bundle/osx/${{ env.MACOS_BUILD_NAME }}.zip\n    - name: ArtifactsDebug\n      if: failure()\n      uses: actions/upload-artifact@v6\n      with:\n        name: ${{ runner.os }}-ffbuild\n        path: |\n          deps/ffmpeg/ffbuild\n    - name: Debug via SSH\n      if: failure()\n      uses: luchihoratiu/debug-via-ssh@main\n      with:\n        NGROK_AUTH_TOKEN: ${{ secrets.NGROK_AUTH_TOKEN }}\n        SSH_PASS: ${{ secrets.SSH_PASS }}\n        NGROK_REGION: eu\n    - name: Publish\n      uses: softprops/action-gh-release@v2\n      if: startsWith(github.ref, 'refs/tags/')\n      with:\n          files: |\n            target/release/bundle/osx/macOS.zip\n          prerelease: false\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "/.vscode\n/target\n/deps/ffmpeg\n/deps/x264\n/deps/dist*\n/deps/nv-codec-headers\n/deps/libva\nc_helper/target\nc_helper/Cargo.lock\n*.js\n*.js.map\n*.tar.gz\n"
  },
  {
    "path": "CONTRIBUTORS",
    "content": "If you want to contribute to Weylus you have to agree to license your\ncontributions under the 3-Clause BSD License. To do so please add yourself to\nthe list of contributors below.\n\nList of Contributors:\n\n**************************\nRobert Schroll\nDaniel Rutz\nPhilipp Urlbauer\nOmegaRogue\n**************************\n\n3-Clause BSD License\n\nCopyright 2020-Present above Contributors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\nlist of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\nthis list of conditions and the following disclaimer in the documentation\nand/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its contributors\nmay be used to endorse or promote products derived from this software without\nspecific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[package]\nname = \"weylus\"\nversion = \"0.11.4\"\nauthors = [\"HMH <henry@freedesk.net>\"]\nlicense = \"AGPL-3.0-or-later\"\nedition = \"2021\"\ndescription = \"Use your iPad or Android tablet as graphic tablet.\"\n\n[dependencies]\nautopilot = { git = \"https://github.com/H-M-H/autopilot-rs.git\", rev = \"63eed09c715bfb665bb23172a3930a528e11691c\" }\nbitflags = { version = \"^2.6\", features = [\"serde\"] }\nbytes = \"1.7.1\"\nclap = { version = \"4.5.18\", features = [\"derive\"] }\nclap_complete = \"4.5.29\"\ndirs = \"^5.0\"\nfastwebsockets = { version = \"0.8.0\", features = [\"upgrade\", \"unstable-split\"] }\nfltk = { version = \"^1.5\", features = [\"use-wayland\"] }\nfltk-theme = \"^0.7.9\"\nhandlebars = \"^6.1\"\nhttp-body-util = \"0.1.2\"\nhyper = { version = \"^1.4\", features = [\"server\", \"http1\", \"http2\"] }\nhyper-util = { version = \"0.1.8\", features = [\"tokio\"] }\nimage = { version = \"^0.25\", features = [\"png\"], default-features = false }\nimage_autopilot = { package = \"image\", version = \"0.22.5\", features = [], default-features = false }\npercent-encoding = \"2.1.0\"\nqrcode = \"0.14.0\"\nrand = \"0.8.5\"\nserde = { version = \"^1.0\", features = [\"derive\"] }\nserde_json = \"^1.0\"\nsignal-hook = \"0.3.17\"\ntokio = { version = \"^1\", features = [\"fs\", \"macros\", \"rt-multi-thread\", \"sync\", \"net\"] }\ntoml = \"^0.9\"\ntracing = \"^0.1\"\ntracing-subscriber = { version = \"^0.3\", features = [\"ansi\", \"json\"], default-features = false }\nurl = \"^2.5\"\n\n[target.'cfg(windows)'.dependencies]\nwinapi = { version = \"0.3.9\", features = [\"d3d11\", \"d3dcommon\", \"dxgi\", \"dxgi1_2\", \"dxgitype\"] }\nwio = \"0.2.2\"\ncaptrs = \"^0.3.1\"\n\n[build-dependencies]\ncc = \"^1.1\"\nnum_cpus = \"^1.16\"\n\n[target.'cfg(target_os = \"linux\")'.dependencies]\ndbus = \"^0.9\"\ngstreamer = \"^0.24\"\ngstreamer-app = { version = \"^0.24\", features = [\"v1_16\"] }\ngstreamer-video = \"^0.24\"\n\n[target.'cfg(not(target_os = \"windows\"))'.dependencies]\npnet_datalink = \"^0.35\"\n\n[target.'cfg(target_os = \"macos\")'.dependencies]\ncore-foundation = \"^0.10\"\ncore-graphics = \"^0.24\"\n\n[features]\nbench = []\nffmpeg-system = []\nva-static = []\n\n[package.metadata.bundle]\nname = \"Weylus\"\nidentifier = \"io.github.h-m-h.weylus\"\n\n[package.metadata.deb]\nname = \"Weylus\"\nsection = \"graphics\"\npriority = \"optional\"\nassets = [\n    [\"target/release/weylus\", \"usr/bin/weylus\", \"755\"],\n    [\"weylus.desktop\", \"usr/share/applications/weylus.desktop\", \"755\"],\n    [\"Readme.md\", \"usr/share/doc/weylus/README\", \"644\"],\n]\n\n[profile.release]\nlto = true\nopt-level = 3\n"
  },
  {
    "path": "LICENSE",
    "content": "Weylus is licensed under the GNU Affero General Public License, version\n3 or later. Contributed work is licensed under the 3-Clause BSD License.\nSee CONTRIBUTORS for details.\n\n    Copyright (C) 2020-Present  H-M-H\n\n                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>.\n"
  },
  {
    "path": "Readme.md",
    "content": "# Weylus\n![Build](https://github.com/H-M-H/Weylus/workflows/Build/badge.svg)\n\nWeylus turns your tablet or smart phone into a graphic tablet/touch screen for your computer!\n\nWeylus in action with [Xournal++](https://github.com/xournalpp/xournalpp):\n\n![Weylus in action](In_action.gif)\n\n## Table of Contents\n* [Features](#features)\n* [Installation](#installation)\n    * [Packages](#packages)\n* [Running](#running)\n    * [Fullscreen](#fullscreen)\n    * [Keyboard Input](#keyboard-input)\n    * [Automation](#automation)\n    * [Linux](#linux)\n        * [Wayland](#wayland)\n        * [Hardware Acceleration](#hardware-acceleration)\n        * [Weylus as Second Screen](#weylus-as-second-screen)\n            * [Intel GPU on Xorg with Intel drivers](#intel-gpu-on-xorg-with-intel-drivers)\n            * [Dummy Plugs](#dummy-plugs)\n            * [Other Options](#other-options)\n        * [Encryption](#encryption)\n    * [macOS](#macos)\n        * [Hardware Acceleration](#hardware-acceleration-1)\n    * [Windows](#windows)\n        * [Hardware Acceleration](#hardware-acceleration-2)\n* [Building](#building)\n    * [Docker](#docker)\n* [How does this work?](#how-does-this-work)\n    * [Stylus/Touch](#stylustouch)\n    * [Screen mirroring & window capturing](#screen-mirroring--window-capturing)\n* [FAQ](#faq)\n\n## Features\n- Control your mouse with your tablet\n- Mirror your screen to your tablet\n- Send keyboard input using physical keyboards\n- Hardware accelerated video encoding\n\nThe above features are available on all Operating Systems but Weylus works best on Linux. Additional\nfeatures on Linux are:\n- Support for a stylus/pen (supports pressure and tilt)\n- Multi-touch: Try it with software that supports multi-touch, like Krita, and see for yourself!\n- Capturing specific windows and only drawing to them\n- Faster screen mirroring\n- Tablet as second screen\n\n## Installation\nJust grab the latest release for your OS from the\n[releases page](https://github.com/H-M-H/Weylus/releases) and install it on your computer. No apps\nexcept a modern browser (Firefox 80+, iOS/iPadOS 13+) are required on your tablet. **If you run\nLinux make sure to follow the instructions described [here](#linux) to enable uinput for features\nlike pressure sensitivity and multitouch!**\n\n### Packages\nAUR packages for Weylus are available here:\n- From source: [weylus](https://aur.archlinux.org/packages/weylus/)\n- Prebuilt binary: [weylus-bin](https://aur.archlinux.org/packages/weylus-bin/)\n\n## Running\nStart Weylus, preferably set an access code in the access code box and press the Start button. This\nwill start a webserver running on your computer. To control your computer with your tablet you need\nto open the url `http://<address of your computer>:<port set in the menu, default is 1701>`, if\npossible Weylus will display to you the url you need to open and show a QR code with the encoded\naddress. If you have a firewall running make sure to open a TCP port for the webserver (1701 by\ndefault) and the websocket connection (9001 by default).\n\nOn many Linux distributions this is done with ufw:\n```\nsudo ufw allow 1701/tcp\nsudo ufw allow 9001/tcp\n```\n\nPlease only run Weylus in networks you trust as there is no encryption to enable minimal latencies.\n\n### Fullscreen\nYou may want to add a bookmark to your home screen on your tablet as this enables running Weylus in\nfull screen mode (on iOS/iPadOS this needs to be done with Safari). If you are not on iOS/iPadOS\nthere is a button to toggle full screen mode.\n\n### Keyboard Input\nWeylus supports keyboard input for physical keyboards, so if you have a Bluetooth keyboard, just\nconnect it to your tablet and start typing. Due to technical limitations onscreen keyboards are not\nsupported.\n\n### Automation\nWeylus provides some features to make automation as convenient as possible. There is a command-line\ninterface; `--no-gui` for example starts Weylus in headless mode without a gui. For more options see\n`weylus --help`. If you want to run a specific script e.g., once a client connects to your computer\nyou can do so by parsing the log Weylus generates. You may want to enable more verbose logging by\nsetting the environment variable `WEYLUS_LOG_LEVEL` to `DEBUG` or `TRACE` as well as\n`WEYLUS_LOG_JSON` to `true` to enable easily parseable JSON logging.\n\n### Linux\nWeylus uses the `uinput` interface to simulate input events on Linux. **To enable stylus and\nmulti-touch support `/dev/uinput` needs to be writable by Weylus.** To make `/dev/uinput`\npermanently writable by your user, run:\n```sh\nsudo groupadd -r uinput\nsudo usermod -aG uinput $USER\necho 'KERNEL==\"uinput\", MODE=\"0660\", GROUP=\"uinput\", OPTIONS+=\"static_node=uinput\"' \\\n| sudo tee /etc/udev/rules.d/60-weylus.rules\n```\n\nThen, either reboot, or run\n\n```sh\nsudo udevadm control --reload\nsudo udevadm trigger\n```\n\nthen log out and log in again. To undo this, run:\n\n```sh\nsudo rm /etc/udev/rules.d/60-weylus.rules\n```\n\nThis allows your user to synthesize input events system-wide, even when another user is logged in.\nTherefore, untrusted users should not be added to the uinput group.\n\n#### Wayland\nWeylus offers experimental support for Wayland. Installing `pipewire` and `xdg-desktop-portal` as\nwell as one of:\n- `xdg-desktop-portal-gtk` for GNOME\n- `xdg-desktop-portal-kde` for KDE\n- `xdg-desktop-portal-wlr` for wlroots-based compositors like Sway\nis required.\n\nThere are still some things that do not work:\n- input mapping for windows\n- displaying proper window names\n- capturing the cursor\n\n#### Hardware Acceleration\nOn Linux Weylus supports hardware accelerated video encoding through the Video Acceleration API\n(VAAPI) or Nvidia's NVENC. By default hardware acceleration is disabled as quality and stability of\nthe hardware encoded video stream varies widely among different hardware and sufficient quality can\nnot be guaranteed. If VAAPI is used it is possible to select a specific driver by setting the\nenvironment variable `LIBVA_DRIVER_NAME`. You can find possible values with the command\n`ls /usr/lib/dri/ | sed -n 's/^\\(\\S*\\)_drv_video.so$/\\1/p'`. On some distributions the drivers may\nnot reside in `/usr/lib/dri` but for example in `/usr/lib/x86_64-linux-gnu/dri` and may not be found\nby Weylus. To force Weylus to search another directory for drivers, the environment variable\n`LIBVA_DRIVERS_PATH` can be set.\nAdditionally you can specify the VAAPI device to use by setting `WEYLUS_VAAPI_DEVICE`; by default\ndevices can be found in `/dev/dri`. On some systems this is not optional and this variable must be\nset. If VAAPI doesn't work out of the box for you, have a look into `/dev/dri`, often setting\n`WEYLUS_VAAPI_DEVICE=/dev/dri/renderD129` is already the solution. Note that you may need to install\nthe driver(s) first.\n\nNvidias NVENC is very fast but delivers a video stream of noticeably lower quality (at least on my\nGeForce GTX 1050 Mobile GPU) but more recent GPUs should provide higher quality. For this to work\nnvidia drivers need to be installed.\n\n#### Weylus as Second Screen\nThere are a few possibilities to use Weylus to turn your tablet into a second screen.\n\n##### Intel GPU on Xorg with Intel drivers\nIntel's drivers support creating virtual outputs that can be configured via xrandr.\n\nBut first a word of warning: The following configuration may break starting the X server. This means\nyou might end up without a graphical login or X may get stuck and just display a black screen. So\nmake sure you know what you are doing or are at least able to recover from a broken X server.\n\nYou will need to install the `xf86-video-intel` driver and create the file\n`/etc/X11/xorg.conf.d/20-intel.conf` with the following contents:\n```text\nSection \"Device\"\n    Identifier \"intelgpu0\"\n    Driver \"intel\"\n\n    # this adds two virtual monitors / devices\n    Option \"VirtualHeads\" \"2\"\n\n    # if your screen is flickering one of the following options might help\n    # Option \"TripleBuffer\" \"true\"\n    # Option \"TearFree\"     \"true\"\n    # Option \"DRI\"          \"false\"\nEndSection\n```\nAfter a reboot `xrandr` will show two additional monitors `VIRTUAL1` and `VIRTUAL2` and can be used\nto configure them. To activate `VIRTUAL1` with a screen size of 1112x834 and a refresh rate of 60\nfps the following commands can be used:\n```console\n> # this generates all input parameters xrandr needs\n> #from a given screen resolution and refresh rate\n> gtf 1112 834 60\n\n  # 1112x834 @ 60.00 Hz (GTF) hsync: 51.78 kHz; pclk: 75.81 MHz\n  Modeline \"1112x834_60.00\"  75.81  1112 1168 1288 1464  834 835 838 863  -HSync +Vsync\n> # setup the monitor\n> xrandr --newmode \"1112x834_60.00\"  75.81  1112 1168 1288 1464  834 835 838 863  -HSync +Vsync\n> xrandr --addmode VIRTUAL1 1112x834_60.00\n> xrandr --output VIRTUAL1 --mode 1112x834_60.00\n> # check if everything is in order\n> xrandr\n```\nNow you should be able to configure this monitor in your system setting like a regular second\nmonitor and for example set its position relative to your primary monitor.\n\nAfter setting up the virtual monitor start Weylus and select it in the capture menu. You may want to\nenable displaying the cursor in this case. That is it!\n\n##### Dummy Plugs\nWeylus detects if you use multiple monitors and you can select the one you want to mirror. So if you\nwant to use Weylus as a second screen you could just buy another monitor. Obviously this is\npointless as if you already bought that monitor, there is no need to use Weylus! This is where so\ncalled **HDMI/Displayport/VGA Dummy Plugs** come in handy. These are small devices that pretend to\nbe a monitor but only cost a fraction of the price of an actual monitor.\n\nOnce you have bought one and plugged it into your computer you can configure an additional screen\njust like you would do with an actual one and then use Weylus to mirror this virtual screen.\n\n##### Other Options\nThe following is untested/incomplete, feel free to do more research and open a pull request to\nexpand documentation on this!\n- On Wayland with sway there is `create_output` which can be used to [create headless\n  outputs](https://github.com/swaywm/sway/releases/tag/1.5), unfortunately it is not documented how\n  to actually do that: https://github.com/swaywm/sway/issues/5553\n- On Wayland with GNOME recently there has been added an option to [create virtual monitors with\n  mutter](https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/1698)\n\n#### Encryption\nBy default Weylus comes without encryption and should only be run on networks you trust. If this is\nnot the case it's strongly advised to set up a TLS proxy. One option is to use\n[hitch](https://hitch-tls.org/), an example script that sets up encryption is located at\n`weylus_tls.sh`.\nBut any TLS proxy should work just fine.\n\nNote that the mentioned script works by creating a self-signed certificate. This means your browser\nwill most likely display a scary looking but completely unfounded message telling you how incredibly\ndangerous it is to trust the certificate you yourself just created; this can be safely ignored!\n\nIn case you are using Firefox: There is a [bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1187666)\nthat prevents users from accepting self-signed certificates for websocket connections. A workaround\nis to directly open the websocket connection via the URL bar and accept the certificate there. After\naccepting the connection will of course fail as the browser expects https and not wss as protocol.\n\nSadly this solution is anything but frictionless and I am unhappy with the current state of affairs.\nThis is also another reason why encryption is not enabled by default, self-signed certificates are\njust too painful to handle nowadays. I'd gladly welcome any proposals to improve the situation!\n\n### macOS\nWeylus needs some permissions to work properly, make sure you enable:\n- Incoming connections\n- Screen capturing\n- Controlling your desktop\n\n#### Hardware Acceleration\nWeylus can make use of the Videotoolbox framework on macOS for hardware acceleration. In my tests\nthe video quality has been considerably worse than that using software encoding and thus\nVideotoolbox is disabled by default.\n\n### Windows\n\n#### Hardware Acceleration\nWeylus can make use of Nvidias NVENC as well as Microsoft's MediaFoundation for hardware accelerated\nvideo encoding. Due to widely varying quality it is disabled by default.\n\n## Building\nTo build Weylus you need to install Rust, Typescript, make, git, a C compiler, nasm and bash. `cargo\nbuild` builds the project. By default Weylus is build in debug mode, if you want a release build run\n`cargo build --release`. On Linux some additional dependencies are required to build Weylus. On\nDebian or Ubuntu they can be installed via:\n```sh\napt-get install -y libx11-dev libxext-dev libxft-dev libxinerama-dev libxcursor-dev libxrender-dev \\\nlibxfixes-dev libxtst-dev libxrandr-dev libxcomposite-dev libxi-dev libxv-dev autoconf libtool-bin \\\nnvidia-cuda-dev pkg-config libdrm-dev libpango1.0-dev libgstreamer1.0-dev \\\nlibgstreamer-plugins-base1.0-dev libdbus-1-dev\n```\n\nOn Fedora, they can be installed via:\n```sh\nsudo dnf install libXext-devel libXft-devel libXinerama-devel libXcursor-devel libXrender-devel \\\nlibXfixes-devel libXtst-devel libXrandr-devel libXcomposite-devel libXi-devel libXv-devel autoconf libtool \\\npkg-config libdrm-devel pango-devel gstreamer1-devel \\\ngstreamer1-plugins-base-devel dbus-devel nasm npm\n```\nAfter npm is installed, typescript must be installed by:\n```sh\nsudo npm install typescript -g\n```\n\nNote that building for the first time may take a while as by default ffmpeg needs to be built. On\nWindows only msvc is supported as C compiler; it is, however, possible to cross compile on Linux for\nWindows using minGW.\n\nIn case you do not want to build ffmpeg and libx264 via the supplied build script you can create the\ndirectory `deps/dist` yourself and copy static ffmpeg libraries built with support for libx264 and a\nstatic version of libx264 into `deps/dist/lib`. Additional `deps/dist/include` needs to be filled\nwith ffmpeg's include header files. For hardware acceleration to work ffmpeg needs to be built with\nadditional flags depending on your OS: Consult the variable `FFMPEG_EXTRA_ARGS` in `deps/build.sh`\nfor details. Furthermore, for VAAPI on Linux a static version of libva is required as well.\n\nThe build script will only try to build ffmpeg if the directory `deps/dist` does not exist.\n\nAlternatively passing `--features ffmpeg-system` to cargo will build Weylus using the system's\nversion of ffmpeg. This is disabled by default for compatibility reasons, on newer systems this\nshould not pose a problem and using the system libraries is advised.\n\n### Docker\nIt is also possible to build the Linux version inside a docker container. The Dockerfile used is\nlocated at [docker/Dockerfile](docker/Dockerfile). This is also how the official release is built.\nBuilding works like\nthis:\n```console\ndocker run -it hhmhh/weylus_build bash\nroot@f02164dbfa18:/# git clone https://github.com/H-M-H/Weylus\nCloning into 'Weylus'...\nremote: Enumerating objects: 10, done.\nremote: Counting objects: 100% (10/10), done.\nremote: Compressing objects: 100% (7/7), done.\nremote: Total 827 (delta 1), reused 6 (delta 0), pack-reused 817\nReceiving objects: 100% (827/827), 5.38 MiB | 7.12 MiB/s, done.\nResolving deltas: 100% (431/431), done.\nroot@f02164dbfa18:/# cd Weylus/\nroot@f02164dbfa18:/Weylus# cargo deb\n   Compiling\n   ...\n```\nOnce the build is finished you can for example copy the binary from the container to your file\nsystem like this:\n```sh\ndocker cp f02164dbfa18:/Weylus/target/release/weylus ~/some/path/weylus\n```\nThe .deb is located at `/Weylus/target/debian/`.  Please note that the container ID will most likely\nnot be `f02164dbfa18` if you run this yourself, replace it accordingly.\n\n## How does this work?\n### Stylus/Touch\nModern browsers expose so called\n[PointerEvents](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent) that can convey not\nonly mouse but additionally stylus/pen and touch information. Weylus sets up a webserver with the\ncorresponding javascript code to capture these events. The events are sent back to the server using\nwebsockets.\nWeylus then processes these events using either the generic OS independent backend, which only\nsupports controlling the mouse or on Linux the uinput backend can be used. It makes use of the\nuinput Linux kernel module which supports creating a wide range of input devices including mouse,\nstylus and touch input devices.\n\n### Screen mirroring & window capturing\nEither the generic backend is used which is less efficient and only captures the whole screen or on\nLinux xlib is used to connect to the X-server and do the necessary work of getting window\ninformation and capturing the window/screen. To make things fast the \"MIT-SHM - The MIT Shared\nMemory Extension\" is used to create shared memory images using `XShmCreateImage`. If Wayland instead\nof X11 is running, PipeWire and GStreamer is used to capture the screen. The images captured are\nthen encoded to a video stream using ffmpeg. Fragmented MP4 is used as container format to enable\nbrowsers to play the stream via the Media Source Extensions API. The video codec used is H.264 as\nthis is widely supported and allows very fast encoding as opposed to formats like AV1. To minimize\ndependencies ffmpeg is statically linked into Weylus.\n\n## FAQ\nQ: Why does the page not load on my tablet and instead I get a timeout?<br>\nA: There probably is some kind of firewall running, make sure the ports Weylus uses are opened.\n\nQ: Why do I get the error `ERROR Failed to create uinput device: CError: code...`?<br>\nA: uinput is probably misconfigured, have you made sure to follow all instructions and logged out\nand in again? You may also be running a very old kernel that does not support the required features.\nIn that case try to upgrade your system or use a newer one.\n\nQ: Why is the \"Capture\" drop down empty and the screen not mirrored?<br>\nA: It is possible that only the port for the webserver but not the websocket has been opened, check\nthat both ports have been opened.\n\nQ: Why can I not select any windows in the \"Capture\" drop down and only see the whole screen.<br>\nA: If you are running Weylus on MacOS or Windows this feature is unfortunately not implemented. On\nLinux it is possible that your window manager does not support\n[Extended Window Manager Hints](https://specifications.freedesktop.org/wm-spec/latest/) or that you\nneed to activate them first, like for XMonad.\n\nQ: Do I have to follow the instructions to setup Weylus as second screen too?<br>\nA: No, this is strictly optional.\n\nQ: Why am I unable to connect my tablet to the URL displayed by Weylus?<br>\nA: It is possible that your computer and WiFi connected tablet are on different networks, make sure\nthey are on the same network.\n\nQ: Why does this not run on Firefox for Android?<br>\nA: Actually it does, just make sure Firefox version 80+ is installed.\n\nQ: Why does this not run under Chrome on my iPad?<br>\nA: Chrome lacks some features for video streaming on iPadOS/iOS, try Firefox or Safari.\n\nQ: Why won't my cursor move in osu! ?<br>\nA: Try disabling raw input.\n\nQ: Can I use Weylus even if there is no WiFi?<br>\nA: Probably yes! Most tablets permit setting up a WiFi hotspot that can be used to connect your\ncomputer and tablet. Alternatively there is USB tethering too, which can be used to setup a peer to\npeer connection between your tablet and computer over USB. Another method for Android devices is to\nsetup a socket connection with\n[adb](https://developer.android.com/studio/command-line/adb#Enabling):\n```console\nadb reverse tcp:1701 tcp:1701\nadb reverse tcp:9001 tcp:9001\n```\nLike that you can connect from your Android device to Weylus with the URL: `http://127.0.0.1:1701`.\n\nWeylus only requires that your devices\nare connected via the Internet Protocol and that doesn't necessarily imply WiFi.\n\n---\n\n[![Packaging status](\nhttps://repology.org/badge/vertical-allrepos/weylus.svg\n)](https://repology.org/project/weylus/versions)\n"
  },
  {
    "path": "build.rs",
    "content": "use std::env;\nuse std::path::Path;\nuse std::process::Command;\n\nfn build_ffmpeg(dist_dir: &Path, enable_libnpp: bool) {\n    if dist_dir.exists() {\n        return;\n    }\n\n    Command::new(\"bash\")\n        .arg(Path::new(\"clean.sh\"))\n        .current_dir(\"deps\")\n        .status()\n        .expect(\"Failed to clean ffmpeg build!\");\n\n    if !Command::new(\"bash\")\n        .arg(Path::new(\"build.sh\"))\n        .current_dir(\"deps\")\n        .env(\"DIST\", dist_dir)\n        .env(\"ENABLE_LIBNPP\", if enable_libnpp { \"y\" } else { \"n\" })\n        .status()\n        .expect(\"Failed to run bash!\")\n        .success()\n    {\n        println!(\"cargo:warning=Failed to build ffmpeg!\");\n        std::process::exit(1);\n    }\n}\n\nfn main() {\n    let target_os = env::var(\"CARGO_CFG_TARGET_OS\").unwrap();\n\n    let dist_dir = Path::new(\"deps\")\n        .canonicalize()\n        .unwrap()\n        .join(format!(\"dist_{}\", target_os));\n\n    let enable_libnpp = env::var(\"I_AM_BUILDING_THIS_AT_HOME_AND_WANT_LIBNPP\").map_or(false, |v| {\n        [\"y\", \"yes\", \"true\", \"1\"].contains(&v.to_lowercase().as_str())\n    });\n\n    if env::var(\"CARGO_FEATURE_FFMPEG_SYSTEM\").is_err() {\n        build_ffmpeg(&dist_dir, enable_libnpp);\n    }\n\n    println!(\"cargo:rerun-if-changed=ts/lib.ts\");\n\n    #[cfg(not(target_os = \"windows\"))]\n    let mut tsc_command = Command::new(\"tsc\");\n\n    #[cfg(target_os = \"windows\")]\n    let mut tsc_command = Command::new(\"bash\");\n    #[cfg(target_os = \"windows\")]\n    tsc_command.args(&[\"-c\", \"tsc\"]);\n\n    let js_needs_update = || -> Result<bool, Box<dyn std::error::Error>> {\n        Ok(Path::new(\"ts/lib.ts\").metadata()?.modified()?\n            > Path::new(\"www/static/lib.js\").metadata()?.modified()?)\n    }()\n    .unwrap_or(true);\n\n    if js_needs_update {\n        match tsc_command.status() {\n            Err(err) => {\n                println!(\"cargo:warning=Failed to call tsc: {}\", err);\n                std::process::exit(1);\n            }\n            Ok(status) => {\n                if !status.success() {\n                    match status.code() {\n                        Some(code) => println!(\"cargo:warning=tsc failed with exitcode: {}\", code),\n                        None => println!(\"cargo:warning=tsc terminated by signal.\"),\n                    };\n                    std::process::exit(2);\n                }\n            }\n        }\n    }\n\n    println!(\"cargo:rerun-if-changed=lib/encode_video.c\");\n    let mut cc_video = cc::Build::new();\n    cc_video.file(\"lib/encode_video.c\");\n    cc_video.include(dist_dir.join(\"include\"));\n    if [\"linux\", \"windows\"].contains(&target_os.as_str()) {\n        cc_video.define(\"HAS_NVENC\", None);\n    }\n    if target_os == \"linux\" {\n        cc_video.define(\"HAS_VAAPI\", None);\n    }\n    if target_os == \"macos\" {\n        cc_video.define(\"HAS_VIDEOTOOLBOX\", None);\n    }\n    if target_os == \"windows\" {\n        cc_video.define(\"HAS_MEDIAFOUNDATION\", None);\n    }\n    if enable_libnpp {\n        cc_video.define(\"HAS_LIBNPP\", None);\n    }\n    cc_video.compile(\"video\");\n\n    println!(\"cargo:rerun-if-changed=lib/error.h\");\n    println!(\"cargo:rerun-if-changed=lib/error.c\");\n    println!(\"cargo:rerun-if-changed=lib/log.h\");\n    println!(\"cargo:rerun-if-changed=lib/log.c\");\n    cc::Build::new().file(\"lib/error.c\").compile(\"error\");\n    cc::Build::new().file(\"lib/log.c\").compile(\"log\");\n\n    let ffmpeg_link_kind =\n        // https://github.com/rust-lang/rust/pull/72785\n        // https://users.rust-lang.org/t/linking-on-windows-without-wholearchive/49846/3\n        if cfg!(target_os = \"windows\") ||\n            env::var(\"CARGO_FEATURE_FFMPEG_SYSTEM\").is_ok() {\n            \"dylib\"\n        } else {\n            \"static\"\n        };\n    println!(\"cargo:rustc-link-lib={}=avdevice\", ffmpeg_link_kind);\n    println!(\"cargo:rustc-link-lib={}=avformat\", ffmpeg_link_kind);\n    println!(\"cargo:rustc-link-lib={}=avfilter\", ffmpeg_link_kind);\n    println!(\"cargo:rustc-link-lib={}=avcodec\", ffmpeg_link_kind);\n    println!(\"cargo:rustc-link-lib={}=swresample\", ffmpeg_link_kind);\n    println!(\"cargo:rustc-link-lib={}=swscale\", ffmpeg_link_kind);\n    println!(\"cargo:rustc-link-lib={}=avutil\", ffmpeg_link_kind);\n    println!(\"cargo:rustc-link-lib={}=x264\", ffmpeg_link_kind);\n    if enable_libnpp {\n        if let Ok(lib_paths) = env::var(\"LIBRARY_PATH\") {\n            for lib_path in lib_paths.split(':') {\n                println!(\"cargo:rustc-link-search={}\", lib_path);\n            }\n        }\n        println!(\"cargo:rustc-link-lib=dylib=nppig\");\n        println!(\"cargo:rustc-link-lib=dylib=nppicc\");\n        println!(\"cargo:rustc-link-lib=dylib=nppc\");\n        println!(\"cargo:rustc-link-lib=dylib=nppidei\");\n        println!(\"cargo:rustc-link-lib=dylib=nppif\");\n    }\n    if env::var(\"CARGO_FEATURE_FFMPEG_SYSTEM\").is_err() {\n        println!(\n            \"cargo:rustc-link-search={}\",\n            dist_dir.join(\"lib\").to_string_lossy()\n        );\n    }\n\n    if target_os == \"linux\" {\n        linux();\n    }\n\n    if target_os == \"macos\" {\n        println!(\"cargo:rustc-link-lib=framework=VideoToolbox\");\n        println!(\"cargo:rustc-link-lib=framework=CoreMedia\");\n    }\n\n    if target_os == \"windows\" {\n        println!(\"cargo:rustc-link-lib=dylib=mfplat\");\n        println!(\"cargo:rustc-link-lib=dylib=mfuuid\");\n        println!(\"cargo:rustc-link-lib=dylib=ole32\");\n        println!(\"cargo:rustc-link-lib=dylib=strmiids\");\n        println!(\"cargo:rustc-link-lib=dylib=vfw32\");\n        println!(\"cargo:rustc-link-lib=dylib=shlwapi\");\n        println!(\"cargo:rustc-link-lib=dylib=bcrypt\");\n    }\n}\n\nfn linux() {\n    println!(\"cargo:rerun-if-changed=lib/linux/uniput.c\");\n    println!(\"cargo:rerun-if-changed=lib/linux/xcapture.c\");\n    println!(\"cargo:rerun-if-changed=lib/linux/xhelper.c\");\n    println!(\"cargo:rerun-if-changed=lib/linux/xhelper.h\");\n\n    cc::Build::new()\n        .file(\"lib/linux/uinput.c\")\n        .file(\"lib/linux/xcapture.c\")\n        .file(\"lib/linux/xhelper.c\")\n        .compile(\"linux\");\n\n    println!(\"cargo:rustc-link-lib=X11\");\n    println!(\"cargo:rustc-link-lib=Xext\");\n    println!(\"cargo:rustc-link-lib=Xrandr\");\n    println!(\"cargo:rustc-link-lib=Xfixes\");\n    println!(\"cargo:rustc-link-lib=Xcomposite\");\n    println!(\"cargo:rustc-link-lib=Xi\");\n    let va_link_kind = if env::var(\"CARGO_FEATURE_VA_STATIC\").is_ok() {\n        \"static\"\n    } else {\n        \"dylib\"\n    };\n    println!(\"cargo:rustc-link-lib={}=va\", va_link_kind);\n    println!(\"cargo:rustc-link-lib={}=va-drm\", va_link_kind);\n    println!(\"cargo:rustc-link-lib={}=va-x11\", va_link_kind);\n    println!(\"cargo:rustc-link-lib=drm\");\n    println!(\"cargo:rustc-link-lib=xcb-dri3\");\n    println!(\"cargo:rustc-link-lib=X11-xcb\");\n    println!(\"cargo:rustc-link-lib=xcb\");\n}\n"
  },
  {
    "path": "build_in_local_container.sh",
    "content": "#!/usr/bin/env sh\n\nset -ex\n\nrm -f docker/archive.tar.gz\ngit ls-files | tar Tczf - docker/archive.tar.gz\n\npodman run --replace -d --name weylus_build hhmhh/weylus_build:latest sleep infinity\npodman cp docker/archive.tar.gz weylus_build:/\npodman exec weylus_build sh -c \"mkdir /weylus && tar xf archive.tar.gz --directory=/weylus && cd weylus && ./docker_build.sh\"\n\npodman run --replace -d --name weylus_build_alpine hhmhh/weylus_build_alpine:latest sleep infinity\npodman cp docker/archive.tar.gz weylus_build_alpine:/\npodman exec weylus_build_alpine sh -c \"mkdir /weylus && tar xf archive.tar.gz --directory=/weylus && cd weylus && RUSTFLAGS='-C target-feature=-crt-static' cargo build --release\"\n"
  },
  {
    "path": "compile_flags.txt",
    "content": "-lX11\n-lXext\n-Wall\n-Wextra\n-Ideps/dist/include\n-DHAS_NVENC\n-DHAS_LIBNPP\n-DHAS_VAAPI\n"
  },
  {
    "path": "deps/awk.patch",
    "content": "diff --git a/configure b/configure\nindex 8569a60bf8..928b19db69 100755\n--- a/configure\n+++ b/configure\n@@ -4690,7 +4690,7 @@ probe_cc(){\n         else\n             _ident=$($_cc --version 2>/dev/null | head -n1 | tr -d '\\r')\n         fi\n-        _DEPCMD='$(DEP$(1)) $(DEP$(1)FLAGS) $($(1)DEP_FLAGS) $< 2>&1 | awk '\\''/including/ { sub(/^.*file: */, \"\"); gsub(/\\\\/, \"/\"); if (!match($$0, / /)) print \"$@:\", $$0 }'\\'' > $(@:.o=.d)'\n+        _DEPCMD='$(DEP$(1)) $(DEP$(1)FLAGS) $($(1)DEP_FLAGS) $< 2>&1 | awk -f ./msvc_dep.awk > $(@:.o=.d)'\n         _DEPFLAGS='$(CPPFLAGS) $(CFLAGS) -showIncludes -Zs'\n         _cflags_speed=\"-O2\"\n         _cflags_size=\"-O1\"\ndiff --git a/msvc_dep.awk b/msvc_dep.awk\nnew file mode 100644\nindex 0000000000..a791efe000\n--- /dev/null\n+++ b/msvc_dep.awk\n@@ -0,0 +1 @@\n+/including/ { sub(/^.*file: */, \"\"); gsub(/\\\\/, \"/\"); if (!match($$0, / /)) print \"$@:\", $$0 }\n"
  },
  {
    "path": "deps/build.sh",
    "content": "#!/usr/bin/env bash\n\nset -ex\n\nexport TARGET_OS=\"$CARGO_CFG_TARGET_OS\"\n\nif [ \"$OSTYPE\" == \"linux-gnu\" ]; then\n    export HOST_OS=\"linux\"\nfi\n\nif [[ \"$OSTYPE\" == \"darwin\"* ]]; then\n    export HOST_OS=\"macos\"\nfi\n\nif [ \"$OS\" == \"Windows_NT\" ]; then\n    export HOST_OS=\"windows\"\nfi\n\n[ -z \"$DIST\" ] && export DIST=\"$PWD/dist\"\n[ -z \"$TARGET_OS\" ] && export TARGET_OS=\"$HOST_OS\"\n\nexport NPROCS=\"$(nproc || echo 4)\"\n\n./download.sh\n\nif [ \"$TARGET_OS\" == \"windows\" ]; then\n    if [ \"$HOST_OS\" == \"linux\" ]; then\n        export CROSS_COMPILE=\"x86_64-w64-mingw32-\"\n        export FFMPEG_EXTRA_ARGS=\"--arch=x86_64 --target-os=mingw64 \\\n            --cross-prefix=x86_64-w64-mingw32- --enable-nvenc --enable-ffnvcodec \\\n            --enable-cuda-llvm --enable-mediafoundation --pkg-config=pkg-config --enable-d3d11va\"\n        export FFMPEG_CFLAGS=\"-I$DIST/include\"\n        export FFMPEG_LIBRARY_PATH=\"-L$DIST/lib\"\n    else\n        export CC=\"cl\"\n        export FFMPEG_EXTRA_ARGS=\"--toolchain=msvc --enable-nvenc --enable-ffnvcodec \\\n            --enable-cuda-llvm --enable-mediafoundation --enable-d3d11va\"\n        export FFMPEG_CFLAGS=\"-I$DIST/include\"\n        export FFMPEG_LIBRARY_PATH=\"-LIBPATH:$DIST/lib\"\n    fi\nelse\n    export FFMPEG_CFLAGS=\"-I$DIST/include\"\n    export FFMPEG_LIBRARY_PATH=\"-L$DIST/lib\"\n    if [ \"$TARGET_OS\" == \"linux\" ]; then\n        export FFMPEG_EXTRA_ARGS=\"--enable-nvenc \\\n            --enable-ffnvcodec \\\n            --enable-cuda-llvm \\\n            --enable-vaapi \\\n            --enable-libdrm \\\n            --enable-xlib\"\n    fi\n    if [ \"$TARGET_OS\" == \"macos\" ]; then\n        export FFMPEG_EXTRA_ARGS=\"--enable-videotoolbox\"\n    fi\nfi\n\nif [ \"$ENABLE_LIBNPP\" == \"y\" ]; then\n    export FFMPEG_EXTRA_ARGS=\"$FFMPEG_EXTRA_ARGS --enable-libnpp --enable-nonfree\"\nfi\n\nif [ \"$TARGET_OS\" == \"windows\" ] && [ \"$HOST_OS\" == \"linux\" ]; then\n    export X264_EXTRA_ARGS=\"--cross-prefix=x86_64-w64-mingw32- --host=x86_64-w64-mingw32\"\nfi\n./x264.sh\nif [ \"$TARGET_OS\" == \"linux\" ]; then\n    ./nv-codec-headers.sh\n    ./libva.sh\nfi\nif [ \"$TARGET_OS\" == \"windows\" ]; then\n    ./nv-codec-headers.sh\nfi\n./ffmpeg.sh\n\nif [ \"$TARGET_OS\" == \"windows\" ] && [ \"$HOST_OS\" == \"windows\" ]; then\n    cd \"$DIST/lib\"\n    for l in *.a; do\n        d=${l#lib}\n        cp \"$l\" \"${d%.a}.lib\"\n    done\n    cp libx264.lib x264.lib\nfi\n"
  },
  {
    "path": "deps/clean.sh",
    "content": "#!/usr/bin/env bash\n\nset -x\nfor d in ffmpeg x264 nv-codec-headers libva; do\n    test -d \"$d\" || continue\n    (cd \"$d\" && git clean -dfx && git reset --hard HEAD)\ndone\n"
  },
  {
    "path": "deps/clean_all.sh",
    "content": "#!/usr/bin/env bash\n\nset -ex\nrm -rf ffmpeg x264 nv-codec-headers libva dist*\n"
  },
  {
    "path": "deps/command_limit.patch",
    "content": "diff --git a/ffbuild/library.mak b/ffbuild/library.mak\nindex ad09f20..e63196d 100644\n--- a/ffbuild/library.mak\n+++ b/ffbuild/library.mak\n@@ -35,7 +35,9 @@ OBJS += $(SHLIBOBJS)\n endif\n $(SUBDIR)$(LIBNAME): $(OBJS) $(STLIBOBJS)\n \t$(RM) $@\n-\t$(AR) $(ARFLAGS) $(AR_O) $^\n+\t$(file >$@.ar.txt, $^)\n+\t$(AR) $(ARFLAGS) $(AR_O) @$@.ar.txt\n+\t$(RM) $@.ar.txt\n \t$(RANLIB) $@\n \n install-headers: install-lib$(NAME)-headers install-lib$(NAME)-pkgconfig\n"
  },
  {
    "path": "deps/download.sh",
    "content": "#!/usr/bin/env bash\n\nset -ex\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nsource \"$SCRIPT_DIR/refs.sh\"\nsource \"$SCRIPT_DIR/hashes.sh\"\n\nclone_at_commit() {\n    local url=\"$1\" dir=\"$2\" commit=\"$3\"\n    if [ ! -d \"$dir\" ]; then\n        git clone --filter=blob:none \"$url\" \"$dir\"\n        git -C \"$dir\" checkout \"$commit\"\n    fi\n}\n\nclone_at_commit \"$X264_URL\" x264 \"$X264_COMMIT\"\nclone_at_commit \"$FFMPEG_URL\" ffmpeg \"$FFMPEG_COMMIT\"\n\nif [ \"$TARGET_OS\" == \"linux\" ]; then\n    clone_at_commit \"$NV_CODEC_URL\" nv-codec-headers \"$NV_CODEC_COMMIT\"\n    clone_at_commit \"$LIBVA_URL\" libva \"$LIBVA_COMMIT\"\nfi\nif [ \"$TARGET_OS\" == \"windows\" ]; then\n    clone_at_commit \"$NV_CODEC_URL\" nv-codec-headers \"$NV_CODEC_COMMIT\"\nfi\n\nif [ \"$TARGET_OS\" == \"windows\" ] && [ \"$HOST_OS\" == \"windows\" ]; then\n    cd ffmpeg\n    git apply ../command_limit.patch\n    git apply ../awk.patch\nfi\n\n\n\n\n"
  },
  {
    "path": "deps/ffmpeg.sh",
    "content": "#!/usr/bin/env bash\n\nset -ex\n\ncd ffmpeg\nPKG_CONFIG_PATH=\"$DIST/lib/pkgconfig\" ./configure \\\n\t--prefix=\"$DIST\" \\\n\t--disable-debug \\\n\t--enable-static \\\n\t--disable-shared \\\n\t--enable-pic \\\n\t--enable-stripping \\\n\t--disable-programs \\\n\t--enable-gpl \\\n\t--enable-libx264 \\\n\t--disable-autodetect \\\n\t--extra-cflags=\"$FFMPEG_CFLAGS\" \\\n\t--extra-ldflags=\"$FFMPEG_LIBRARY_PATH\" \\\n\t$FFMPEG_EXTRA_ARGS\n\nmake -j$NPROCS\nmake install\n"
  },
  {
    "path": "deps/hashes.sh",
    "content": "#!/usr/bin/env bash\n\n# Generated by update_hashes.sh — do not edit manually.\n# Run ./update_hashes.sh to regenerate.\n\nX264_COMMIT=\"b35605ace3ddf7c1a5d67a2eb553f034aef41d55\"  # refs/heads/stable\nFFMPEG_COMMIT=\"a4044e04486d1136022498891088a90baf5b2775\"  # refs/tags/n8.0\nNV_CODEC_COMMIT=\"876af32a202d0de83bd1d36fe74ee0f7fcf86b0d\"  # HEAD\nLIBVA_COMMIT=\"e85b1569b738fd8866cb9fa2452319f7148d663f\"  # refs/tags/2.23.0\n"
  },
  {
    "path": "deps/libva.sh",
    "content": "#!/usr/bin/env bash\n\nset -ex\n\ncd libva\n\n# required to make ffmpeg's configure work\nsed -i -e \"s/-lva$/-lva -ldrm -ldl/\" pkgconfig/libva.pc.in\nsed -i -e 's/-lva-\\${display}$/-lva-\\${display} -lX11 -lXext -lXfixes -ldrm/' pkgconfig/libva-x11.pc.in\nsed -i -e 's/-lva-\\${display}$/-lva-\\${display} -ldrm/' pkgconfig/libva-drm.pc.in\n\n./autogen.sh --prefix=$(readlink -f \"$DIST\") \\\n    --enable-static=yes \\\n    --enable-shared=yes \\\n    --enable-drm \\\n    --enable-x11 \\\n    --enable-glx \\\n    --with-drivers-path=\"/usr/lib/dri\"\n\nmake -j$NPROCS\nmake install\n"
  },
  {
    "path": "deps/nv-codec-headers.sh",
    "content": "#!/usr/bin/env bash\n\nset -ex\n\ncd nv-codec-headers\nmake PREFIX=\"$DIST\"\nmake install PREFIX=\"$DIST\"\n"
  },
  {
    "path": "deps/refs.sh",
    "content": "#!/usr/bin/env bash\n\n# Dependency URLs and the branch/tag/ref to pin.\n# After editing, run ./update_hashes.sh to regenerate hashes.sh.\n\nX264_URL=\"https://code.videolan.org/videolan/x264.git\"\nX264_REF=\"refs/heads/stable\"\n\nFFMPEG_URL=\"https://code.ffmpeg.org/FFmpeg/FFmpeg.git\"\nFFMPEG_REF=\"refs/tags/n8.0\"\n\nNV_CODEC_URL=\"https://git.videolan.org/git/ffmpeg/nv-codec-headers.git\"\nNV_CODEC_REF=\"HEAD\"\n\nLIBVA_URL=\"https://github.com/intel/libva\"\nLIBVA_REF=\"refs/tags/2.23.0\"\n"
  },
  {
    "path": "deps/update_hashes.sh",
    "content": "#!/usr/bin/env bash\n\n# Resolves each ref in refs.sh to a commit hash and writes hashes.sh.\n\nset -e\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nsource \"$SCRIPT_DIR/refs.sh\"\n\nresolve() {\n    local name=\"$1\" url=\"$2\" ref=\"$3\"\n    local hash\n    hash=$(git ls-remote \"$url\" \"$ref\" | awk 'END { print $1 }')\n    if [ -z \"$hash\" ]; then\n        echo \"ERROR: could not resolve $name $ref from $url\" >&2\n        exit 1\n    fi\n    echo \"${name}_COMMIT=\\\"${hash}\\\"  # ${ref}\"\n}\n\ncat > \"$SCRIPT_DIR/hashes.sh\" <<EOF\n#!/usr/bin/env bash\n\n# Generated by update_hashes.sh — do not edit manually.\n# Run ./update_hashes.sh to regenerate.\n\n$(resolve X264      \"$X264_URL\"     \"$X264_REF\")\n$(resolve FFMPEG    \"$FFMPEG_URL\"   \"$FFMPEG_REF\")\n$(resolve NV_CODEC  \"$NV_CODEC_URL\" \"$NV_CODEC_REF\")\n$(resolve LIBVA     \"$LIBVA_URL\"    \"$LIBVA_REF\")\nEOF\n\necho \"hashes.sh updated.\"\n"
  },
  {
    "path": "deps/x264.sh",
    "content": "#!/usr/bin/env bash\n\nset -ex\n\ncd x264\n./configure \\\n\t--prefix=\"$DIST\" \\\n\t--exec-prefix=\"$DIST\" \\\n\t--enable-static \\\n\t--enable-pic \\\n\t--enable-strip \\\n\t--disable-cli \\\n\t--disable-opencl \\\n\t$X264_EXTRA_ARGS\n\nmake -j$NPROCS\nmake install\n"
  },
  {
    "path": "docker/Dockerfile",
    "content": "FROM debian:bullseye\nENV RUSTUP_HOME=\"/usr/local/rustup\" CARGO_HOME=\"/usr/local/cargo\" PATH=\"/usr/local/cargo/bin:$PATH\"\nRUN apt-get update && \\\n   apt-get install -y libx11-dev libxext-dev libxft-dev libxinerama-dev libxcursor-dev \\\n   libxrender-dev libxfixes-dev libgl1-mesa-dev libglu1-mesa-dev libxtst-dev cmake git curl \\\n   software-properties-common zip libssl-dev libxrandr-dev libxcomposite-dev libxi-dev \\\n   gcc g++ autoconf libtool-bin libxv-dev libdrm-dev libpango1.0-dev pkg-config mingw-w64 \\\n   libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libdbus-1-dev libxcb-dri3-dev clang  \\\n   libwayland-dev libxkbcommon-dev\nRUN apt-add-repository contrib\nRUN apt-add-repository non-free\nRUN apt-get update && apt-get install -y nvidia-cuda-dev\nRUN curl -Lo cmake.tar.gz https://github.com/Kitware/CMake/releases/download/v4.1.1/cmake-4.1.1.tar.gz && tar xf cmake.tar.gz\nRUN cd cmake-4.* && cmake . && make -j$(nproc) && make install\nRUN rm -rf cmake*\nRUN curl -LO \"https://www.nasm.us/pub/nasm/releasebuilds/2.16.03/nasm-2.16.03.tar.xz\" && \\\n    tar xf \"nasm-2.16.03.tar.xz\" && cd \"nasm-2.16.03\" && \\\n    ./configure --prefix=/usr && make -j$(nproc) && make install && cd .. && rm -rf \"nasm-2.16.03*\"\nRUN curl -sL https://deb.nodesource.com/setup_22.x | bash - && \\\n    apt-get install -y nodejs && \\\n    npm install -g typescript\nRUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \\\n    sh -s -- -y --default-toolchain stable-x86_64-unknown-linux-gnu\nRUN cargo install cargo-deb\nRUN rustup target add x86_64-pc-windows-gnu\n"
  },
  {
    "path": "docker/Dockerfile_alpine",
    "content": "FROM alpine:latest\nENV RUSTUP_HOME=\"/usr/local/rustup\" CARGO_HOME=\"/usr/local/cargo\" PATH=\"/usr/local/cargo/bin:$PATH\"\n\nRUN apk add --no-cache libx11-dev libxext-dev libxft-dev libxinerama-dev libxcursor-dev \\\n    libxrender-dev libxfixes-dev libxtst-dev libxrandr-dev libxcomposite-dev libxi-dev libxv-dev \\\n    autoconf libtool pkgconfig libdrm-dev pango-dev gst-plugins-base-dev gstreamer-dev dbus-libs \\\n    dbus-dev cmake build-base nasm npm ffmpeg-dev libva-dev curl git bash automake tar clang \\\n    wayland-dev libxkbcommon-dev\n\nRUN npm install --global typescript\n\nRUN ln -s /usr/bin/x86_64-alpine-linux-musl-gcc /usr/bin/musl-gcc\nRUN ln -s /usr/bin/x86_64-alpine-linux-musl-g++ /usr/bin/musl-g++\n\nRUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \\\n    sh -s -- -y --default-toolchain stable-x86_64-unknown-linux-musl\n"
  },
  {
    "path": "docker_build.sh",
    "content": "#!/usr/bin/env sh\n\nset -ex\n\n# cross compile windows version\ncargo build --target x86_64-pc-windows-gnu --release\n\n# cleanup cross compiled windows artifacts\n(cd deps && ./clean.sh)\n\n# build linux versions\ncargo deb --  --features=va-static\n\n# check if installing works\ndpkg -i target/debian/Weylus*.deb\ncp target/release/weylus target/release/weylus_va_static\n\n# build version with dynamic libva\ncargo build --release\n\nmkdir packages\n\nPKGDIR=\"$PWD/packages\"\n\n# package windows\n(\n  cd target/x86_64-pc-windows-gnu/release/\n  zip weylus-windows.zip weylus.exe\n  mv weylus-windows.zip \"$PKGDIR/\"\n)\n\n# package linux\n(\n  cp target/debian/Weylus*.deb \"$PKGDIR/\"\n  cp weylus.desktop target/release/\n  cd target/release/\n  zip weylus-linux.zip weylus weylus_va_static weylus.desktop\n  mv weylus-linux.zip \"$PKGDIR/\"\n)\n"
  },
  {
    "path": "lib/encode_video.c",
    "content": "#include <stdint.h>\n#include <stdlib.h>\n#include <string.h>\n\n#include <libavcodec/avcodec.h>\n#include <libavfilter/buffersink.h>\n#include <libavfilter/buffersrc.h>\n#include <libavformat/avformat.h>\n#include <libavformat/avio.h>\n#include <libavutil/buffer.h>\n#include <libavutil/dict.h>\n#include <libavutil/error.h>\n#include <libavutil/frame.h>\n#include <libavutil/hwcontext.h>\n#include <libavutil/imgutils.h>\n#include <libavutil/mem.h>\n#include <libavutil/opt.h>\n#include <libavutil/pixdesc.h>\n#include <libavutil/pixfmt.h>\n\n#include \"error.h\"\n#include \"log.h\"\n\n#ifdef HAS_VAAPI\n#include <libavutil/hwcontext_vaapi.h>\n#include <va/va.h>\n#endif\n\nconst AVRational TIME_BASE = (AVRational){1, 1000};\n\ntypedef struct ScaleContext\n{\n\tAVFilterGraph* filter_graph_scale;\n\tAVFilterContext* buffersink_scale_ctx;\n\tAVFilterContext* buffersrc_scale_ctx;\n\tAVFrame* frame_in;\n\tAVFrame* frame_out;\n} ScaleContext;\n\ntypedef struct Scalers\n{\n\tScaleContext bgr0;\n\tScaleContext rgb0;\n\tScaleContext rgb;\n\tAVBufferRef* hw_frames_ctx;\n\tAVFrame* frame_out;\n} Scalers;\n\ntypedef struct VideoContext\n{\n\tAVFormatContext* oc;\n\tAVCodecContext* c;\n\n\t// pointer to the frame to be encoded, one of frame_out in scalers.bgr0/rgb0/rgb\n\tAVFrame* frame;\n\n\tScalers scalers;\n\n\tAVBufferRef* hw_device_ctx;\n\n\tAVPacket* pkt;\n\tAVStream* st;\n\tint width_out;\n\tint height_out;\n\tint width_in;\n\tint height_in;\n\tvoid* buf;\n\tvoid* rust_ctx;\n\tint pts;\n\tint initialized;\n\tint frame_allocated;\n\tint try_vaapi;\n\tint try_nvenc;\n\tint try_videotoolbox;\n\tint try_mediafoundation;\n} VideoContext;\n\n// this is a rust function and lives in src/video.rs\nint write_video_packet(void* rust_ctx, const uint8_t* buf, int buf_size);\n\n#if defined(__clang__) || defined(__GNUC__)\nvoid log_callback(__attribute__((unused)) void* _ptr, int level, const char* fmt_orig, va_list args)\n#else\nvoid log_callback(void* _ptr, int level, const char* fmt_orig, va_list args)\n#endif\n{\n\tchar fmt[256] = {0};\n\tstrncpy(fmt, fmt_orig, sizeof(fmt) - 1);\n\tint done = 0;\n\t// strip whitespaces from end\n\tfor (int i = sizeof(fmt) - 1; i >= 0 && !done; --i)\n\t\tswitch (fmt[i])\n\t\t{\n\t\tcase ' ':\n\t\tcase '\\n':\n\t\tcase '\\t':\n\t\tcase '\\r':\n\t\t\tfmt[i] = '\\0';\n\t\t\tbreak;\n\t\tcase '\\0':\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tdone = 1;\n\t\t}\n\tchar buf[2048];\n\tvsnprintf(buf, sizeof(buf), fmt, args);\n\tswitch (level)\n\t{\n\tcase AV_LOG_FATAL:\n\tcase AV_LOG_ERROR:\n\tcase AV_LOG_PANIC:\n\t\tlog_error(\"%s\", buf);\n\t\tbreak;\n\tcase AV_LOG_INFO:\n\t\tlog_info(\"%s\", buf);\n\t\tbreak;\n\tcase AV_LOG_WARNING:\n\t\tlog_warn(\"%s\", buf);\n\t\tbreak;\n\tcase AV_LOG_QUIET:\n\t\tbreak;\n\tcase AV_LOG_VERBOSE:\n\t\tlog_debug(\"%s\", buf);\n\t\tbreak;\n\tcase AV_LOG_DEBUG:\n\t\tlog_trace(\"%s\", buf);\n\t\tbreak;\n\t}\n}\n\n// called in src/log.rs\nvoid init_ffmpeg_logger() { av_log_set_callback(log_callback); }\n\nvoid set_codec_params(VideoContext* ctx)\n{\n\t/* resolution must be a multiple of two */\n\tctx->c->width = ctx->width_out;\n\tctx->c->height = ctx->height_out;\n\tctx->c->time_base = TIME_BASE;\n\tctx->c->framerate = (AVRational){0, 1};\n\n\tctx->c->gop_size = 12;\n\t// no B-frames to reduce latency\n\tctx->c->max_b_frames = 0;\n\tif (ctx->oc->oformat->flags & AVFMT_GLOBALHEADER)\n\t\tctx->c->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;\n}\n\nvoid destroy_scale_ctx(ScaleContext* ctx)\n{\n\tavfilter_graph_free(&ctx->filter_graph_scale);\n\tif (ctx->frame_in)\n\t\tav_frame_free(&ctx->frame_in);\n}\n\nvoid init_scaler(\n\tScaleContext* ctx,\n\tint width_in,\n\tint height_in,\n\tint width_out,\n\tint height_out,\n\tenum AVPixelFormat pix_fmt_in,\n\tenum AVPixelFormat pix_fmt_out,\n\tAVBufferRef* hw_device_ctx,\n\tenum AVPixelFormat pix_fmt_sw_out,\n\tAVFrame* frame_out,\n\tError* err)\n{\n\tint ret = 0;\n\n\tctx->frame_in = av_frame_alloc();\n\tif (!ctx->frame_in)\n\t\tERROR(err, 1, \"Failed to allocate frame_in for scale filter!\");\n\n\tctx->frame_out = frame_out;\n\n\tctx->frame_in->format = pix_fmt_in;\n\tctx->frame_in->width = width_in;\n\tctx->frame_in->height = height_in;\n\tret = av_frame_get_buffer(ctx->frame_in, 0);\n\tif (ret)\n\t{\n\t\tdestroy_scale_ctx(ctx);\n\t\tERROR(\n\t\t\terr,\n\t\t\t1,\n\t\t\t\"Failed to allocate buffer for frame_in for scale filter: %s!\",\n\t\t\tav_err2str(ret));\n\t}\n\n\tchar args[512];\n\tconst AVFilter* buffersrc = avfilter_get_by_name(\"buffer\");\n\tconst AVFilter* buffersink = avfilter_get_by_name(\"buffersink\");\n\tAVFilterInOut* outputs = avfilter_inout_alloc();\n\tAVFilterInOut* inputs = avfilter_inout_alloc();\n\n\tctx->filter_graph_scale = avfilter_graph_alloc();\n\tif (!outputs || !inputs || !ctx->filter_graph_scale)\n\t{\n\t\tret = AVERROR(ENOMEM);\n\t\tgoto end;\n\t}\n\n\tavfilter_graph_set_auto_convert(ctx->filter_graph_scale, AVFILTER_AUTO_CONVERT_NONE);\n\n\t/* buffer video source: the decoded frames from the decoder will be inserted here. */\n\tsnprintf(\n\t\targs,\n\t\tsizeof(args),\n\t\t\"video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d\",\n\t\twidth_in,\n\t\theight_in,\n\t\tpix_fmt_in,\n\t\tTIME_BASE.num,\n\t\tTIME_BASE.den,\n\t\t1,\n\t\t1);\n\n\tret = avfilter_graph_create_filter(\n\t\t&ctx->buffersrc_scale_ctx, buffersrc, \"in\", args, NULL, ctx->filter_graph_scale);\n\tif (ret < 0)\n\t{\n\t\tlog_warn(\"Cannot create buffer source\");\n\t\tgoto end;\n\t}\n\n\t/* buffer video sink: to terminate the filter chain. */\n\tctx->buffersink_scale_ctx =\n\t\tavfilter_graph_alloc_filter(ctx->filter_graph_scale, buffersink, \"out\");\n\n\tif (ctx->buffersink_scale_ctx == NULL)\n\t{\n\t\tlog_warn(\"Cannot allocate buffer sink\");\n\t\tgoto end;\n\t}\n\n\tret = av_opt_set_array(\n\t\tctx->buffersink_scale_ctx,\n\t\t\"pixel_formats\",\n\t\tAV_OPT_SEARCH_CHILDREN,\n\t\t0,\n\t\t1,\n\t\tAV_OPT_TYPE_PIXEL_FMT,\n\t\t&pix_fmt_out);\n\tif (ret < 0)\n\t{\n\t\tlog_warn(\"Cannot set output pixel format: %s\", av_err2str(ret));\n\t\tgoto end;\n\t}\n\n\tret = avfilter_init_dict(ctx->buffersink_scale_ctx, NULL);\n\tif (ret < 0)\n\t{\n\t\tlog_warn(\"Cannot init buffer sink\");\n\t\tgoto end;\n\t}\n\n\toutputs->name = av_strdup(\"in\");\n\toutputs->filter_ctx = ctx->buffersrc_scale_ctx;\n\toutputs->pad_idx = 0;\n\toutputs->next = NULL;\n\n\tinputs->name = av_strdup(\"out\");\n\tinputs->filter_ctx = ctx->buffersink_scale_ctx;\n\tinputs->pad_idx = 0;\n\tinputs->next = NULL;\n\n\tswitch (pix_fmt_out)\n\t{\n\tcase AV_PIX_FMT_CUDA:\n\t\tif (pix_fmt_in == AV_PIX_FMT_RGB24)\n\t\t{\n\t\t\tsnprintf(\n\t\t\t\targs,\n\t\t\t\tsizeof(args),\n\t\t\t\t\"scale=w=%d:h=%d:flags=fast_bilinear,hwupload_cuda\",\n\t\t\t\twidth_out,\n\t\t\t\theight_out);\n\t\t}\n\t\telse\n\t\t{\n\t\t\tsnprintf(\n\t\t\t\targs,\n\t\t\t\tsizeof(args),\n#ifdef HAS_LIBNPP\n\t\t\t\t\"scale,format=nv12,hwupload_cuda,scale_npp=w=%d:h=%d:format=%s:interp_algo=nn\",\n#else\n\t\t\t\t\"hwupload_cuda,scale_cuda=w=%d:h=%d:format=%s:interp_algo=nearest\",\n#endif\n\t\t\t\twidth_out,\n\t\t\t\theight_out,\n\t\t\t\tav_get_pix_fmt_name(pix_fmt_sw_out));\n\t\t}\n\t\tbreak;\n\tcase AV_PIX_FMT_VAAPI:\n\t\tif (pix_fmt_in == AV_PIX_FMT_RGB24)\n\t\t\tsnprintf(\n\t\t\t\targs,\n\t\t\t\tsizeof(args),\n\t\t\t\t\"scale=w=%d:h=%d:flags=fast_bilinear,hwupload\",\n\t\t\t\twidth_out,\n\t\t\t\theight_out);\n\t\telse\n\t\t\tsnprintf(\n\t\t\t\targs,\n\t\t\t\tsizeof(args),\n\t\t\t\t\"hwupload,scale_vaapi=w=%d:h=%d:format=%s:mode=fast\",\n\t\t\t\twidth_out,\n\t\t\t\theight_out,\n\t\t\t\tav_get_pix_fmt_name(pix_fmt_sw_out));\n\t\tbreak;\n\tdefault:\n\t\tsnprintf(args, sizeof(args), \"scale=w=%d:h=%d:flags=fast_bilinear\", width_out, height_out);\n\t}\n\n\tif ((ret = avfilter_graph_parse_ptr(ctx->filter_graph_scale, args, &inputs, &outputs, NULL)) <\n\t\t0)\n\t{\n\t\tlog_warn(\"Failed to parse filter\");\n\t\tgoto end;\n\t}\n\n\tfor (unsigned int i = 0; i < ctx->filter_graph_scale->nb_filters; i++)\n\t{\n\t\tAVFilterContext* filt = ctx->filter_graph_scale->filters[i];\n\t\tif (strcmp(filt->filter->name, \"hwupload\") == 0)\n\t\t{\n\t\t\tfilt->hw_device_ctx = av_buffer_ref(hw_device_ctx);\n\t\t}\n\t}\n\n\tif ((ret = avfilter_graph_config(ctx->filter_graph_scale, NULL)) < 0)\n\t{\n\t\tlog_warn(\"Failed to configure filter graph\");\n\t\tgoto end;\n\t}\n\nend:\n\tavfilter_inout_free(&inputs);\n\tavfilter_inout_free(&outputs);\n\n\tif (ret != 0)\n\t{\n\t\tdestroy_scale_ctx(ctx);\n\t\tERROR(\n\t\t\terr,\n\t\t\t1,\n\t\t\t\"Setting up scale filter %s -> %s (sw: %s) failed!\",\n\t\t\tav_get_pix_fmt_name(pix_fmt_in),\n\t\t\tav_get_pix_fmt_name(pix_fmt_out),\n\t\t\tav_get_pix_fmt_name(pix_fmt_sw_out));\n\t}\n\telse\n\t{\n\t\tlog_debug(\n\t\t\t\"Scale filter set %s -> %s (sw: %s) up!\",\n\t\t\tav_get_pix_fmt_name(pix_fmt_in),\n\t\t\tav_get_pix_fmt_name(pix_fmt_out),\n\t\t\tav_get_pix_fmt_name(pix_fmt_sw_out));\n\t}\n}\n\nvoid destroy_scalers(Scalers* s)\n{\n\tdestroy_scale_ctx(&s->bgr0);\n\tdestroy_scale_ctx(&s->rgb0);\n\tdestroy_scale_ctx(&s->rgb);\n\tif (s->frame_out)\n\t\tav_frame_free(&s->frame_out);\n}\n\nvoid init_scalers(\n\tScalers* ctx,\n\tint width_in,\n\tint height_in,\n\tint width_out,\n\tint height_out,\n\tenum AVPixelFormat pix_fmt_out,\n\tenum AVPixelFormat pix_fmt_sw_out,\n\tAVBufferRef* hw_device_ctx,\n\tError* err)\n{\n\tint ret;\n\tctx->frame_out = av_frame_alloc();\n\tif (!ctx->frame_out)\n\t{\n\t\tdestroy_scalers(ctx);\n\t\tERROR(err, 1, \"Failed to allocate frame_out for scale filter!\");\n\t}\n\n\tif (hw_device_ctx != NULL)\n\t{\n\n\t\tAVBufferRef* hw_frames_ref;\n\t\tAVHWFramesContext* frames_ctx = NULL;\n\t\tif (!(hw_frames_ref = av_hwframe_ctx_alloc(hw_device_ctx)))\n\t\t{\n\t\t\tdestroy_scalers(ctx);\n\t\t\tERROR(err, 1, \"Failed to create HW frame context.\");\n\t\t}\n\t\tframes_ctx = (AVHWFramesContext*)(hw_frames_ref->data);\n\t\tframes_ctx->format = pix_fmt_out;\n\t\tframes_ctx->sw_format = pix_fmt_sw_out;\n\t\tframes_ctx->width = width_out;\n\t\tframes_ctx->height = height_out;\n\t\tframes_ctx->initial_pool_size = 20;\n\t\tif ((ret = av_hwframe_ctx_init(hw_frames_ref)) < 0)\n\t\t{\n\t\t\tav_buffer_unref(&hw_frames_ref);\n\t\t\tdestroy_scalers(ctx);\n\t\t\tERROR(\n\t\t\t\terr,\n\t\t\t\t1,\n\t\t\t\t\"Failed to initialize HW frame context.\"\n\t\t\t\t\"Error code: %s\",\n\t\t\t\tav_err2str(ret));\n\t\t}\n\n\t\tctx->hw_frames_ctx = av_buffer_ref(hw_frames_ref);\n\t\tret = av_hwframe_get_buffer(ctx->hw_frames_ctx, ctx->frame_out, 0);\n\t\tif (ret < 0)\n\t\t{\n\t\t\tav_buffer_unref(&hw_frames_ref);\n\t\t\tdestroy_scalers(ctx);\n\t\t\tERROR(\n\t\t\t\terr,\n\t\t\t\t1,\n\t\t\t\t\"Could not allocate video hardware frame data for scaling: %s\",\n\t\t\t\tav_err2str(ret));\n\t\t}\n\t\tav_buffer_unref(&hw_frames_ref);\n\t}\n\n\tenum AVPixelFormat pix_fmts[] = {AV_PIX_FMT_BGR0, AV_PIX_FMT_RGB0, AV_PIX_FMT_RGB24};\n\tScaleContext* scalers[] = {&ctx->bgr0, &ctx->rgb0, &ctx->rgb};\n\tfor (int i = 0; i < 3; i++)\n\t{\n\t\tinit_scaler(\n\t\t\tscalers[i],\n\t\t\twidth_in,\n\t\t\theight_in,\n\t\t\twidth_out,\n\t\t\theight_out,\n\t\t\tpix_fmts[i],\n\t\t\tpix_fmt_out,\n\t\t\thw_device_ctx,\n\t\t\tpix_fmt_sw_out,\n\t\t\tctx->frame_out,\n\t\t\terr);\n\t\tOK_OR_ABORT(err);\n\t}\n}\n\nvoid scale_frame(ScaleContext* ctx, Error* err)\n{\n\tint ret;\n\tif ((ret = av_buffersrc_add_frame_flags(\n\t\t\t\t   ctx->buffersrc_scale_ctx, ctx->frame_in, AV_BUFFERSRC_FLAG_KEEP_REF) < 0))\n\t{\n\t\tERROR(err, ret, \"Error adding frame to buffer source: %s.\", av_err2str(ret));\n\t}\n\n\tav_frame_unref(ctx->frame_out);\n\n\twhile (1)\n\t{\n\t\tint ret = av_buffersink_get_frame(ctx->buffersink_scale_ctx, ctx->frame_out);\n\t\tif (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)\n\t\t\tbreak;\n\t\tif (ret < 0)\n\t\t{\n\t\t\tERROR(err, ret, \"Error reading frame from buffer sink: %s.\", av_err2str(ret));\n\t\t}\n\t}\n}\n\nvoid open_video(VideoContext* ctx, Error* err)\n{\n\tif (ctx->width_out <= 1 || ctx->height_out <= 1)\n\t\tERROR(\n\t\t\terr,\n\t\t\t1,\n\t\t\t\"Invalid size for video: width = %d, height = %d\",\n\t\t\tctx->width_out,\n\t\t\tctx->height_out);\n\n\tconst AVCodec* codec;\n\tint ret;\n\n\tavformat_alloc_output_context2(&ctx->oc, NULL, \"mp4\", NULL);\n\tif (!ctx->oc)\n\t{\n\t\tERROR(err, 1, \"Could not find output format mp4.\");\n\t}\n\n\tint using_hw = 0;\n\n#ifdef HAS_VAAPI\n\tchar* vaapi_device = getenv(\"WEYLUS_VAAPI_DEVICE\");\n\n\tif (ctx->try_vaapi &&\n\t\tav_hwdevice_ctx_create(\n\t\t\t&ctx->hw_device_ctx, AV_HWDEVICE_TYPE_VAAPI, vaapi_device, NULL, 0) == 0)\n\t{\n\n\t\tif (ctx->hw_device_ctx)\n\t\t{\n\t\t\tAVHWFramesConstraints* cst =\n\t\t\t\tav_hwdevice_get_hwframe_constraints(ctx->hw_device_ctx, NULL);\n\t\t\tif (cst)\n\t\t\t{\n\t\t\t\tfor (enum AVPixelFormat* fmt = cst->valid_sw_formats; *fmt != AV_PIX_FMT_NONE;\n\t\t\t\t\t ++fmt)\n\t\t\t\t{\n\t\t\t\t\tlog_debug(\"VAAPI: valid pix_fmt: %s\", av_get_pix_fmt_name(*fmt));\n\t\t\t\t}\n\t\t\t\tav_hwframe_constraints_free(&cst);\n\t\t\t}\n\t\t}\n\n\t\tcodec = avcodec_find_encoder_by_name(\"h264_vaapi\");\n\t\tif (codec)\n\t\t{\n\t\t\tctx->c = avcodec_alloc_context3(codec);\n\t\t\tif (ctx->c)\n\t\t\t{\n\t\t\t\tError err = {0};\n\t\t\t\tinit_scalers(\n\t\t\t\t\t&ctx->scalers,\n\t\t\t\t\tctx->width_in,\n\t\t\t\t\tctx->height_in,\n\t\t\t\t\tctx->width_out,\n\t\t\t\t\tctx->height_out,\n\t\t\t\t\tAV_PIX_FMT_VAAPI,\n\t\t\t\t\tAV_PIX_FMT_NV12,\n\t\t\t\t\tctx->hw_device_ctx,\n\t\t\t\t\t&err);\n\t\t\t\tif (err.code)\n\t\t\t\t{\n\t\t\t\t\tlog_warn(\"Failed to initialize scaler: %s\", err.error_str);\n\t\t\t\t\tavcodec_free_context(&ctx->c);\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\tctx->c->pix_fmt = AV_PIX_FMT_VAAPI;\n\t\t\t\t\tctx->c->hw_frames_ctx = ctx->scalers.hw_frames_ctx;\n\t\t\t\t\tav_opt_set(ctx->c->priv_data, \"quality\", \"7\", 0);\n\t\t\t\t\tav_opt_set(ctx->c->priv_data, \"qp\", \"23\", 0);\n\t\t\t\t\tset_codec_params(ctx);\n\n\t\t\t\t\tif ((ret = avcodec_open2(ctx->c, codec, NULL) == 0))\n\t\t\t\t\t\tusing_hw = 1;\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\tlog_debug(\"Could not open codec: %s!\", av_err2str(ret));\n\t\t\t\t\t\tavcodec_free_context(&ctx->c);\n\t\t\t\t\t\tav_buffer_unref(&ctx->hw_device_ctx);\n\t\t\t\t\t\tdestroy_scalers(&ctx->scalers);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\telse\n\t\t\tav_buffer_unref(&ctx->hw_device_ctx);\n\t}\n#endif\n\n#ifdef HAS_MEDIAFOUNDATION\n\tif (ctx->try_mediafoundation && !using_hw)\n\t{\n\t\tcodec = avcodec_find_encoder_by_name(\"h264_mf\");\n\t\tif (codec)\n\t\t{\n\t\t\tctx->c = avcodec_alloc_context3(codec);\n\t\t\tif (ctx->c)\n\t\t\t{\n\t\t\t\tError err = {0};\n\t\t\t\tinit_scalers(\n\t\t\t\t\t&ctx->scalers,\n\t\t\t\t\tctx->width_in,\n\t\t\t\t\tctx->height_in,\n\t\t\t\t\tctx->width_out,\n\t\t\t\t\tctx->height_out,\n\t\t\t\t\tAV_PIX_FMT_NV12,\n\t\t\t\t\tAV_PIX_FMT_NV12,\n\t\t\t\t\tNULL,\n\t\t\t\t\t&err);\n\t\t\t\tif (err.code)\n\t\t\t\t{\n\t\t\t\t\tlog_warn(\"Failed to initialize scaler: %s\", err.error_str);\n\t\t\t\t\tavcodec_free_context(&ctx->c);\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\tctx->c->pix_fmt = AV_PIX_FMT_NV12;\n\t\t\t\t\tav_opt_set(ctx->c->priv_data, \"rate_control\", \"ld_vbr\", 0);\n\t\t\t\t\tav_opt_set(ctx->c->priv_data, \"scenario\", \"display_remoting\", 0);\n\t\t\t\t\tav_opt_set(ctx->c->priv_data, \"quality\", \"100\", 0);\n\t\t\t\t\tset_codec_params(ctx);\n\t\t\t\t\tint ret = avcodec_open2(ctx->c, codec, NULL);\n\t\t\t\t\tif (ret == 0)\n\t\t\t\t\t\tusing_hw = 1;\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\tlog_debug(\"Could not open codec: %s!\", av_err2str(ret));\n\t\t\t\t\t\tavcodec_free_context(&ctx->c);\n\t\t\t\t\t\tdestroy_scalers(&ctx->scalers);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\telse\n\t\t\t\tlog_debug(\"Could not allocate video codec context for 'h264_mf'!\");\n\t\t}\n\t\telse\n\t\t\tlog_debug(\"Codec 'h264_mf' not found!\");\n\t}\n#endif\n\n#ifdef HAS_NVENC\n\tif (ctx->try_nvenc && !using_hw &&\n\t\tav_hwdevice_ctx_create(&ctx->hw_device_ctx, AV_HWDEVICE_TYPE_CUDA, NULL, NULL, 0) == 0)\n\t{\n\t\tcodec = avcodec_find_encoder_by_name(\"h264_nvenc\");\n\t\tif (codec)\n\t\t{\n\t\t\tctx->c = avcodec_alloc_context3(codec);\n\t\t\tif (ctx->c)\n\t\t\t{\n\t\t\t\tError err = {0};\n\t\t\t\tinit_scalers(\n\t\t\t\t\t&ctx->scalers,\n\t\t\t\t\tctx->width_in,\n\t\t\t\t\tctx->height_in,\n\t\t\t\t\tctx->width_out,\n\t\t\t\t\tctx->height_out,\n\t\t\t\t\tAV_PIX_FMT_CUDA,\n#ifdef HAS_LIBNPP\n\t\t\t\t\tAV_PIX_FMT_NV12,\n#else\n\t\t\t\t\tAV_PIX_FMT_BGR0,\n#endif\n\t\t\t\t\tctx->hw_device_ctx,\n\t\t\t\t\t&err);\n\t\t\t\tif (err.code)\n\t\t\t\t{\n\t\t\t\t\tlog_warn(\"Failed to initialize scaler: %s\", err.error_str);\n\t\t\t\t\tavcodec_free_context(&ctx->c);\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\tctx->c->pix_fmt = AV_PIX_FMT_CUDA;\n\t\t\t\t\tctx->c->hw_frames_ctx = ctx->scalers.hw_frames_ctx;\n\t\t\t\t\tav_opt_set(ctx->c->priv_data, \"preset\", \"p1\", 0);\n\t\t\t\t\tav_opt_set(ctx->c->priv_data, \"zerolatency\", \"1\", 0);\n\t\t\t\t\tav_opt_set(ctx->c->priv_data, \"tune\", \"ull\", 0);\n\t\t\t\t\tav_opt_set(ctx->c->priv_data, \"rc\", \"cbr\", 0);\n\t\t\t\t\tav_opt_set(ctx->c->priv_data, \"cq\", \"21\", 0);\n\t\t\t\t\tav_opt_set(ctx->c->priv_data, \"delay\", \"0\", 0);\n\t\t\t\t\tset_codec_params(ctx);\n\n\t\t\t\t\tint ret = avcodec_open2(ctx->c, codec, NULL);\n\t\t\t\t\tif (ret == 0)\n\t\t\t\t\t\tusing_hw = 1;\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\tlog_debug(\"Could not open codec: %s!\", av_err2str(ret));\n\t\t\t\t\t\tavcodec_free_context(&ctx->c);\n\t\t\t\t\t\tdestroy_scalers(&ctx->scalers);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\telse\n\t\t\t\tlog_debug(\"Could not allocate video codec context for 'h264_nvenc'!\");\n\t\t}\n\t\telse\n\t\t\tlog_debug(\"Codec 'h264_nvenc' not found!\");\n\t}\n#endif\n\n#ifdef HAS_VIDEOTOOLBOX\n\tif (ctx->try_videotoolbox && !using_hw)\n\t{\n\t\tcodec = avcodec_find_encoder_by_name(\"h264_videotoolbox\");\n\t\tif (codec)\n\t\t{\n\t\t\tctx->c = avcodec_alloc_context3(codec);\n\t\t\tif (ctx->c)\n\t\t\t{\n\t\t\t\tError err = {0};\n\t\t\t\tinit_scalers(\n\t\t\t\t\t&ctx->scalers,\n\t\t\t\t\tctx->width_in,\n\t\t\t\t\tctx->height_in,\n\t\t\t\t\tctx->width_out,\n\t\t\t\t\tctx->height_out,\n\t\t\t\t\tAV_PIX_FMT_YUV420P,\n\t\t\t\t\tAV_PIX_FMT_YUV420P,\n\t\t\t\t\tctx->hw_device_ctx,\n\t\t\t\t\t&err);\n\t\t\t\tif (err.code)\n\t\t\t\t{\n\t\t\t\t\tlog_warn(\"Failed to initialize scaler: %s\", err.error_str);\n\t\t\t\t\tavcodec_free_context(&ctx->c);\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\tctx->c->pix_fmt = AV_PIX_FMT_YUV420P;\n\t\t\t\t\tav_opt_set(ctx->c->priv_data, \"realtime\", \"true\", 0);\n\t\t\t\t\tav_opt_set(ctx->c->priv_data, \"allow_sw\", \"true\", 0);\n\t\t\t\t\tav_opt_set(ctx->c->priv_data, \"profile\", \"extended\", 0);\n\t\t\t\t\tav_opt_set(ctx->c->priv_data, \"level\", \"5.2\", 0);\n\t\t\t\t\tset_codec_params(ctx);\n\t\t\t\t\tif (avcodec_open2(ctx->c, codec, NULL) == 0)\n\t\t\t\t\t\tusing_hw = 1;\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\tlog_debug(\"Could not open codec: %s!\", av_err2str(ret));\n\t\t\t\t\t\tavcodec_free_context(&ctx->c);\n\t\t\t\t\t\tdestroy_scalers(&ctx->scalers);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n#endif\n\n\tif (!using_hw)\n\t{\n\t\tcodec = avcodec_find_encoder_by_name(\"libx264\");\n\t\tif (!codec)\n\t\t{\n\t\t\tERROR(err, 1, \"Codec 'libx264' not found\");\n\t\t}\n\n\t\tctx->c = avcodec_alloc_context3(codec);\n\t\tif (!ctx->c)\n\t\t{\n\t\t\tERROR(err, 1, \"Could not allocate video codec context\");\n\t\t}\n\n\t\tinit_scalers(\n\t\t\t&ctx->scalers,\n\t\t\tctx->width_in,\n\t\t\tctx->height_in,\n\t\t\tctx->width_out,\n\t\t\tctx->height_out,\n\t\t\tAV_PIX_FMT_YUV420P,\n\t\t\tAV_PIX_FMT_YUV420P,\n\t\t\tNULL,\n\t\t\terr);\n\t\tif (err->code)\n\t\t{\n\t\t\tavcodec_free_context(&ctx->c);\n\t\t\treturn;\n\t\t}\n\n\t\tctx->c->pix_fmt = AV_PIX_FMT_YUV420P;\n\t\tav_opt_set(ctx->c->priv_data, \"preset\", \"ultrafast\", 0);\n\t\tav_opt_set(ctx->c->priv_data, \"tune\", \"zerolatency\", 0);\n\t\tav_opt_set(ctx->c->priv_data, \"crf\", \"23\", 0);\n\t\tset_codec_params(ctx);\n\n\t\tret = avcodec_open2(ctx->c, codec, NULL);\n\t\tif (ret < 0)\n\t\t{\n\t\t\tavcodec_free_context(&ctx->c);\n\t\t\tERROR(err, 1, \"Could not open codec: %s\", av_err2str(ret));\n\t\t}\n\t}\n\n\tctx->st = avformat_new_stream(ctx->oc, NULL);\n\tavcodec_parameters_from_context(ctx->st->codecpar, ctx->c);\n\n\tctx->pkt = av_packet_alloc();\n\tif (!ctx->pkt)\n\t\tERROR(err, 1, \"Failed to allocate packet\");\n\n\tint buf_size = 1024 * 1024;\n\tctx->buf = av_malloc(buf_size);\n\tctx->oc->pb = avio_alloc_context(\n\t\tctx->buf, buf_size, AVIO_FLAG_WRITE, ctx->rust_ctx, NULL, write_video_packet, NULL);\n\tif (!ctx->oc->pb)\n\t\tERROR(err, 1, \"Failed to allocate avio context\");\n\n\tAVDictionary* opt = NULL;\n\n\t// enable writing fragmented mp4\n\tav_dict_set(&opt, \"movflags\", \"frag_custom+empty_moov+default_base_moof\", 0);\n\tret = avformat_write_header(ctx->oc, &opt);\n\tif (ret < 0)\n\t\tlog_warn(\"Video: failed to write header!\");\n\tav_dict_free(&opt);\n\n\tif (av_pix_fmt_desc_get(ctx->c->pix_fmt)->flags & AV_PIX_FMT_FLAG_HWACCEL &&\n\t\tctx->c->hw_frames_ctx)\n\t{\n\t\tconst char* pix_fmt_sw =\n\t\t\tav_get_pix_fmt_name(((AVHWFramesContext*)ctx->c->hw_frames_ctx->data)->sw_format);\n\t\tlog_info(\n\t\t\t\"Video: %dx%d@%s pix_fmt: %s (%s)\",\n\t\t\tctx->width_out,\n\t\t\tctx->height_out,\n\t\t\tctx->c->codec->name,\n\t\t\tav_get_pix_fmt_name(ctx->c->pix_fmt),\n\t\t\tpix_fmt_sw);\n\t}\n\telse\n\t\tlog_info(\n\t\t\t\"Video: %dx%d@%s pix_fmt: %s\",\n\t\t\tctx->width_out,\n\t\t\tctx->height_out,\n\t\t\tctx->c->codec->name,\n\t\t\tav_get_pix_fmt_name(ctx->c->pix_fmt));\n\n\tctx->initialized = 1;\n}\n\nvoid destroy_video_encoder(VideoContext* ctx)\n{\n\tif (ctx->initialized)\n\t{\n\t\tav_write_trailer(ctx->oc);\n\t\tavio_context_free(&ctx->oc->pb);\n\t\tavformat_free_context(ctx->oc);\n\t\tavcodec_free_context(&ctx->c);\n\t\tav_packet_free(&ctx->pkt);\n\t\tav_free(ctx->buf);\n\t\tdestroy_scalers(&ctx->scalers);\n\t}\n\tif (ctx->hw_device_ctx)\n\t\tav_buffer_unref(&ctx->hw_device_ctx);\n\tfree(ctx);\n}\n\nvoid encode_video_frame(VideoContext* ctx, int millis, Error* err)\n{\n\tint ret;\n\tAVFrame* frame = ctx->frame;\n\tif (!frame)\n\t\tERROR(err, 1, \"Frame not initialized!\");\n\n\tframe->pts = millis;\n\n\tret = avcodec_send_frame(ctx->c, frame);\n\tif (ret < 0)\n\t\tERROR(err, 1, \"Error sending a frame for encoding: %s\", av_err2str(ret));\n\n\twhile (ret >= 0)\n\t{\n\t\tret = avcodec_receive_packet(ctx->c, ctx->pkt);\n\t\tif (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)\n\t\t\treturn;\n\t\telse if (ret < 0)\n\t\t{\n\t\t\tERROR(err, 1, \"Error during encoding\");\n\t\t}\n\n\t\tav_packet_rescale_ts(ctx->pkt, ctx->c->time_base, ctx->st->time_base);\n\t\tav_write_frame(ctx->oc, ctx->pkt);\n\t\tav_packet_unref(ctx->pkt);\n\n\t\t// new fragment on every frame for lowest latency\n\t\tav_write_frame(ctx->oc, NULL);\n\t}\n}\n\nVideoContext* init_video_encoder(\n\tvoid* rust_ctx,\n\tint width_in,\n\tint height_in,\n\tint width_out,\n\tint height_out,\n\tint try_vaapi,\n\tint try_nvenc,\n\tint try_videotoolbox,\n\tint try_mediafoundation)\n{\n\tVideoContext* ctx = malloc(sizeof(VideoContext));\n\tctx->rust_ctx = rust_ctx;\n\tctx->width_out = width_out - width_out % 2;\n\tctx->height_out = height_out - height_out % 2;\n\tctx->width_in = width_in;\n\tctx->height_in = height_in;\n\tctx->pts = 0;\n\tctx->initialized = 0;\n\tctx->frame_allocated = 0;\n\tctx->try_vaapi = try_vaapi;\n\tctx->try_nvenc = try_nvenc;\n\tctx->try_videotoolbox = try_videotoolbox;\n\tctx->try_mediafoundation = try_mediafoundation;\n\tctx->hw_device_ctx = NULL;\n\n\t// make sure all scalers are zero initialized so that destroy can always be called\n\tmemset(&ctx->scalers, 0, sizeof(Scalers));\n\treturn ctx;\n}\n\nvoid fill_bgr0(VideoContext* ctx, const void* data, int stride, Error* err)\n{\n\tctx->frame = NULL;\n\tScaleContext* scaler = &ctx->scalers.bgr0;\n\tscaler->frame_in->data[0] = (uint8_t*)data;\n\tscaler->frame_in->linesize[0] = stride;\n\n\tscale_frame(scaler, err);\n\tOK_OR_ABORT(err)\n\tctx->frame = scaler->frame_out;\n}\n\nvoid fill_rgb(VideoContext* ctx, const void* data, Error* err)\n{\n\tctx->frame = NULL;\n\tScaleContext* scaler = &ctx->scalers.rgb;\n\tctx->frame = NULL;\n\tscaler->frame_in->data[0] = (uint8_t*)data;\n\tscaler->frame_in->linesize[0] = ctx->width_in * 3;\n\n\tscale_frame(scaler, err);\n\tOK_OR_ABORT(err)\n\tctx->frame = scaler->frame_out;\n}\n\nvoid fill_rgb0(VideoContext* ctx, const void* data, Error* err)\n{\n\tctx->frame = NULL;\n\tScaleContext* scaler = &ctx->scalers.rgb0;\n\tscaler->frame_in->data[0] = (uint8_t*)data;\n\tscaler->frame_in->linesize[0] = ctx->width_in * 4;\n\n\tscale_frame(scaler, err);\n\tOK_OR_ABORT(err)\n\tctx->frame = scaler->frame_out;\n}\n"
  },
  {
    "path": "lib/error.c",
    "content": "#include \"error.h\"\n\nvoid fill_error(Error* err, int code, const char* fmt, ...)\n{\n\tif (!err)\n\t\treturn;\n\terr->code = code;\n\tva_list args;\n\tva_start(args, fmt);\n\tvsnprintf(err->error_str, sizeof(err->error_str), fmt, args);\n}\n"
  },
  {
    "path": "lib/error.h",
    "content": "#pragma once\n\n#include <stdarg.h>\n#include <stdio.h>\n\nstruct Error\n{\n\tint code;\n\tchar error_str[1024];\n};\n\ntypedef struct Error Error;\n\n#if defined(__clang__) || defined(__GNUC__)\n__attribute__((__format__ (__printf__, 3, 4)))\n#endif\nvoid fill_error(Error* err, int code, const char* fmt, ...);\n\n#define ERROR(err, code, fmt, ...)                                                                 \\\n\t{                                                                                              \\\n\t\tfill_error(err, code, fmt, ##__VA_ARGS__);                                                 \\\n\t\treturn;                                                                                    \\\n\t}\n\n#define OK_OR_ABORT(err)                                                                           \\\n\t{                                                                                              \\\n\t\tif (err->code)                                                                             \\\n\t\t\treturn;                                                                                \\\n\t}\n"
  },
  {
    "path": "lib/linux/uinput.c",
    "content": "#include <errno.h>\n#include <fcntl.h>\n#include <limits.h>\n#include <linux/input-event-codes.h>\n#include <linux/input.h>\n#include <linux/uinput.h>\n#include <signal.h>\n#include <stdint.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n#include <unistd.h>\n\n#include \"../error.h\"\n\n#define ABS_MAXVAL 65535\n\n#ifndef REL_WHEEL_HI_RES\n#define REL_WHEEL_HI_RES\t0x0b\n#endif\n#ifndef REL_HWHEEL_HI_RES\n#define REL_HWHEEL_HI_RES\t0x0c\n#endif\n\nvoid setup_abs(int fd, int code, int minimum, int maximum, int resolution, Error* err)\n{\n\tif (ioctl(fd, UI_SET_ABSBIT, code) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_ABSBIT, code %#x\", code);\n\n\tstruct uinput_abs_setup abs_setup;\n\tmemset(&abs_setup, 0, sizeof(abs_setup));\n\tabs_setup.code = code;\n\tabs_setup.absinfo.value = 0;\n\tabs_setup.absinfo.minimum = minimum;\n\tabs_setup.absinfo.maximum = maximum;\n\tabs_setup.absinfo.fuzz = 0;\n\tabs_setup.absinfo.flat = 0;\n\t// units/mm\n\tabs_setup.absinfo.resolution = resolution;\n\tif (ioctl(fd, UI_ABS_SETUP, &abs_setup) < 0)\n\t\tERROR(err, 1, \"error: UI_ABS_SETUP, code: %#x\", code);\n}\n\nvoid setup(int fd, const char* name, Error* err)\n{\n\n\tstruct uinput_setup setup;\n\tmemset(&setup, 0, sizeof(setup));\n\tstrncpy(setup.name, name, UINPUT_MAX_NAME_SIZE - 1);\n\tsetup.id.bustype = BUS_VIRTUAL;\n\tsetup.id.vendor = 0x1701;\n\tsetup.id.product = 0x1701;\n\tsetup.id.version = 0x0001;\n\tsetup.ff_effects_max = 0;\n\tif (ioctl(fd, UI_DEV_SETUP, &setup) < 0)\n\t\tERROR(err, 1, \"error: UI_DEV_SETUP\");\n}\n\nvoid init_keyboard(int fd, const char* name, Error* err)\n{\n\t// enable synchronization\n\tif (ioctl(fd, UI_SET_EVBIT, EV_SYN) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_EVBIT EV_SYN\");\n\n\t// enable keys\n\tif (ioctl(fd, UI_SET_EVBIT, EV_KEY) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_EVBIT EV_KEY\");\n\n\t// enable all the keys!\n\tfor (int keycode = KEY_ESC; keycode <= KEY_MICMUTE; ++keycode)\n\t\tif (ioctl(fd, UI_SET_KEYBIT, keycode) < 0)\n\t\t\tERROR(err, 1, \"error: ioctl UI_SET_KEYBIT %x\", keycode);\n\n\t// TODO: figure if scancodes are needed\n\t// if (ioctl(fd, UI_SET_EVBIT, EV_MSC) < 0)\n\t// \tERROR(err, 1, \"error: ioctl UI_SET_EVBIT EV_MSC\");\n\t// if (ioctl(fd, UI_SET_MSCBIT, MSC_SCAN) < 0)\n\t// \tERROR(err, 1, \"error: ioctl UI_SET_MSCBIT MSC_SCAN\");\n\n\tsetup(fd, name, err);\n\tOK_OR_ABORT(err);\n\n\tif (ioctl(fd, UI_DEV_CREATE) < 0)\n\t\tERROR(err, 1, \"error: ioctl\");\n}\n\nvoid init_mouse(int fd, const char* name, Error* err)\n{\n\t// enable synchronization\n\tif (ioctl(fd, UI_SET_EVBIT, EV_SYN) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_EVBIT EV_SYN\");\n\n\tif (ioctl(fd, UI_SET_PROPBIT, INPUT_PROP_DIRECT) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_PROPBIT INPUT_PROP_DIRECT\");\n\n\t// enable buttons\n\tif (ioctl(fd, UI_SET_EVBIT, EV_KEY) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_EVBIT EV_KEY\");\n\tif (ioctl(fd, UI_SET_KEYBIT, BTN_LEFT) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_KEYBIT BTN_LEFT\");\n\tif (ioctl(fd, UI_SET_KEYBIT, BTN_RIGHT) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_KEYBIT BTN_RIGHT\");\n\tif (ioctl(fd, UI_SET_KEYBIT, BTN_MIDDLE) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_KEYBIT BTN_MIDDLE\");\n\n\t// enable scrolling\n\tif (ioctl(fd, UI_SET_EVBIT, EV_REL) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_EVBIT EV_REL\");\n\tif (ioctl(fd, UI_SET_RELBIT, REL_WHEEL) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_RELBIT REL_WHEEL\");\n\tif (ioctl(fd, UI_SET_RELBIT, REL_HWHEEL) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_RELBIT REL_HWHEEL\");\n\tif (ioctl(fd, UI_SET_RELBIT, REL_WHEEL_HI_RES) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_RELBIT REL_WHEEL_HI_RES\");\n\tif (ioctl(fd, UI_SET_RELBIT, REL_HWHEEL_HI_RES) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_RELBIT REL_HWHEEL_HI_RES\");\n\n\t// setup sending timestamps\n\tif (ioctl(fd, UI_SET_EVBIT, EV_MSC) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_EVBIT EV_MSC\");\n\tif (ioctl(fd, UI_SET_MSCBIT, MSC_TIMESTAMP) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_MSCBIT MSC_TIMESTAMP\");\n\n\tif (ioctl(fd, UI_SET_EVBIT, EV_ABS) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_EVBIT EV_ABS\");\n\n\tsetup_abs(fd, ABS_X, 0, ABS_MAXVAL, 0, err);\n\tOK_OR_ABORT(err);\n\tsetup_abs(fd, ABS_Y, 0, ABS_MAXVAL, 0, err);\n\tOK_OR_ABORT(err);\n\n\tsetup(fd, name, err);\n\tOK_OR_ABORT(err);\n\n\tif (ioctl(fd, UI_DEV_CREATE) < 0)\n\t\tERROR(err, 1, \"error: ioctl\");\n}\n\nvoid init_stylus(int fd, const char* name, Error* err)\n{\n\t// enable synchronization\n\tif (ioctl(fd, UI_SET_EVBIT, EV_SYN) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_EVBIT EV_SYN\");\n\n\tif (ioctl(fd, UI_SET_PROPBIT, INPUT_PROP_DIRECT) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_PROPBIT INPUT_PROP_DIRECT\");\n\n\t// enable buttons\n\tif (ioctl(fd, UI_SET_EVBIT, EV_KEY) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_EVBIT EV_KEY\");\n\tif (ioctl(fd, UI_SET_KEYBIT, BTN_TOOL_PEN) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_KEYBIT BTN_TOOL_PEN\");\n\tif (ioctl(fd, UI_SET_KEYBIT, BTN_TOOL_RUBBER) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_KEYBIT BTN_TOOL_RUBBER\");\n\tif (ioctl(fd, UI_SET_KEYBIT, BTN_TOUCH) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_KEYBIT BTN_TOUCH\");\n\n\t// setup sending timestamps\n\tif (ioctl(fd, UI_SET_EVBIT, EV_MSC) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_EVBIT EV_MSC\");\n\tif (ioctl(fd, UI_SET_MSCBIT, MSC_TIMESTAMP) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_MSCBIT MSC_TIMESTAMP\");\n\n\tif (ioctl(fd, UI_SET_EVBIT, EV_ABS) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_EVBIT EV_ABS\");\n\n\tsetup_abs(fd, ABS_X, 0, ABS_MAXVAL, 12, err);\n\tOK_OR_ABORT(err);\n\tsetup_abs(fd, ABS_Y, 0, ABS_MAXVAL, 12, err);\n\tOK_OR_ABORT(err);\n\tsetup_abs(fd, ABS_PRESSURE, 0, ABS_MAXVAL, 12, err);\n\tOK_OR_ABORT(err);\n\tsetup_abs(fd, ABS_TILT_X, -90, 90, 12, err);\n\tOK_OR_ABORT(err);\n\tsetup_abs(fd, ABS_TILT_Y, -90, 90, 12, err);\n\tOK_OR_ABORT(err);\n\n\tsetup(fd, name, err);\n\tOK_OR_ABORT(err);\n\n\tif (ioctl(fd, UI_DEV_CREATE) < 0)\n\t\tERROR(err, 1, \"error: ioctl\");\n}\n\nvoid init_touch(int fd, const char* name, Error* err)\n{\n\t// enable synchronization\n\tif (ioctl(fd, UI_SET_EVBIT, EV_SYN) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_EVBIT EV_SYN\");\n\n\tif (ioctl(fd, UI_SET_PROPBIT, INPUT_PROP_DIRECT) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_PROPBIT INPUT_PROP_DIRECT\");\n\n\tif (ioctl(fd, UI_SET_EVBIT, EV_KEY) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_EVBIT EV_KEY\");\n\tif (ioctl(fd, UI_SET_KEYBIT, BTN_TOUCH) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_KEYBIT\");\n\tif (ioctl(fd, UI_SET_KEYBIT, BTN_TOOL_FINGER) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_KEYBIT\");\n\tif (ioctl(fd, UI_SET_KEYBIT, BTN_TOOL_DOUBLETAP) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_KEYBIT\");\n\tif (ioctl(fd, UI_SET_KEYBIT, BTN_TOOL_TRIPLETAP) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_KEYBIT\");\n\tif (ioctl(fd, UI_SET_KEYBIT, BTN_TOOL_QUADTAP) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_KEYBIT\");\n\tif (ioctl(fd, UI_SET_KEYBIT, BTN_TOOL_QUINTTAP) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_KEYBIT\");\n\n\t// setup sending timestamps\n\tif (ioctl(fd, UI_SET_EVBIT, EV_MSC) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_EVBIT EV_MSC\");\n\tif (ioctl(fd, UI_SET_MSCBIT, MSC_TIMESTAMP) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_MSCBIT MSC_TIMESTAMP\");\n\n\tif (ioctl(fd, UI_SET_EVBIT, EV_ABS) < 0)\n\t\tERROR(err, 1, \"error: ioctl UI_SET_EVBIT EV_ABS\");\n\n\tsetup_abs(fd, ABS_X, 0, ABS_MAXVAL, 200, err);\n\tOK_OR_ABORT(err);\n\tsetup_abs(fd, ABS_Y, 0, ABS_MAXVAL, 200, err);\n\tOK_OR_ABORT(err);\n\n\t// 5 fingers 5 multitouch slots.\n\tsetup_abs(fd, ABS_MT_SLOT, 0, 4, 0, err);\n\tOK_OR_ABORT(err);\n\tsetup_abs(fd, ABS_MT_TRACKING_ID, 0, 4, 0, err);\n\tOK_OR_ABORT(err);\n\tsetup_abs(fd, ABS_MT_POSITION_X, 0, ABS_MAXVAL, 200, err);\n\tOK_OR_ABORT(err);\n\tsetup_abs(fd, ABS_MT_POSITION_Y, 0, ABS_MAXVAL, 200, err);\n\tOK_OR_ABORT(err);\n\tsetup_abs(fd, ABS_MT_PRESSURE, 0, ABS_MAXVAL, 0, err);\n\tOK_OR_ABORT(err);\n\tsetup_abs(fd, ABS_MT_TOUCH_MAJOR, 0, ABS_MAXVAL, 12, err);\n\tOK_OR_ABORT(err);\n\tsetup_abs(fd, ABS_MT_TOUCH_MINOR, 0, ABS_MAXVAL, 12, err);\n\tOK_OR_ABORT(err);\n\t// PointerEvent only gives partial orientation of the touch ellipse\n\tsetup_abs(fd, ABS_MT_ORIENTATION, 0, 1, 0, err);\n\tOK_OR_ABORT(err);\n\n\tsetup(fd, name, err);\n\tOK_OR_ABORT(err);\n\n\tif (ioctl(fd, UI_DEV_CREATE) < 0)\n\t\tERROR(err, 1, \"error: ioctl\");\n}\n\nint init_uinput_keyboard(const char* name, Error* err)\n{\n\tint device;\n\n\tif ((device = open(\"/dev/uinput\", O_WRONLY | O_NONBLOCK)) < 0)\n\t\tfill_error(err, 101, \"error: failed to open /dev/uinput\");\n\telse\n\t{\n\t\tinit_keyboard(device, name, err);\n\t}\n\treturn device;\n}\n\nint init_uinput_stylus(const char* name, Error* err)\n{\n\tint device;\n\n\tif ((device = open(\"/dev/uinput\", O_WRONLY | O_NONBLOCK)) < 0)\n\t\tfill_error(err, 101, \"error: failed to open /dev/uinput\");\n\telse\n\t{\n\t\tinit_stylus(device, name, err);\n\t}\n\treturn device;\n}\n\nint init_uinput_mouse(const char* name, Error* err)\n{\n\tint device;\n\n\tif ((device = open(\"/dev/uinput\", O_WRONLY | O_NONBLOCK)) < 0)\n\t\tfill_error(err, 101, \"error: failed to open /dev/uinput\");\n\telse\n\t{\n\t\tinit_mouse(device, name, err);\n\t}\n\treturn device;\n}\n\nint init_uinput_touch(const char* name, Error* err)\n{\n\tint device;\n\n\tif ((device = open(\"/dev/uinput\", O_WRONLY | O_NONBLOCK)) < 0)\n\t\tfill_error(err, 101, \"error: failed to open /dev/uinput\");\n\telse\n\t{\n\t\tinit_touch(device, name, err);\n\t}\n\treturn device;\n}\n\nvoid destroy_uinput_device(int fd)\n{\n\tioctl(fd, UI_DEV_DESTROY);\n\tclose(fd);\n}\n\nvoid send_uinput_event(int device, int type, int code, int value, Error* err)\n{\n\tstruct input_event ev;\n\tev.type = type;\n\tev.code = code;\n\tev.value = value;\n\tif (write(device, &ev, sizeof(ev)) < 0)\n\t\tERROR(err, 1, \"error writing to device, filedescriptor: %d)\", device);\n}\n"
  },
  {
    "path": "lib/linux/uinput_info.md",
    "content": "# Some References on how to develop things using uinput\n\n- uinput: https://www.kernel.org/doc/html/latest/input/uinput.html\n- event codes: https://www.kernel.org/doc/html/latest/input/event-codes.html\n- multi-touch protocol: https://www.kernel.org/doc/html/latest/input/multi-touch-protocol.html\n\n## Other projects using uinput\n- https://github.com/rfc2822/GfxTablet\n- https://github.com/bsteinsbo/rpi_touch_driver\n\n## Debugging\n- libinput debug-events\n- evtest\n\n"
  },
  {
    "path": "lib/linux/xcapture.c",
    "content": "#include <X11/X.h>\n#include <X11/Xlib.h>\n#include <X11/Xutil.h>\n\n#include <X11/extensions/XShm.h>\n#include <X11/extensions/Xcomposite.h>\n#include <X11/extensions/Xfixes.h>\n#include <stdlib.h>\n#include <string.h>\n#include <sys/ipc.h>\n#include <sys/shm.h>\n\n#include <stdint.h>\n\n#include \"../error.h\"\n#include \"../log.h\"\n#include \"xhelper.h\"\n\nint clamp(int x, int lb, int ub)\n{\n\tif (x < lb)\n\t\treturn lb;\n\tif (x > ub)\n\t\treturn ub;\n\treturn x;\n}\n\nstruct CaptureContext\n{\n\tCapturable cap;\n\tXImage* ximg;\n\tXShmSegmentInfo shminfo;\n\tint has_xfixes;\n\tint has_offscreen;\n\tint wayland;\n\tBool last_img_return;\n};\n\ntypedef struct CaptureContext CaptureContext;\n\nstruct Image\n{\n\tchar* data;\n\tunsigned int width;\n\tunsigned int height;\n};\n\nvoid* start_capture(Capturable* cap, CaptureContext* ctx, Error* err)\n{\n\tif (XShmQueryExtension(cap->disp) != True)\n\t{\n\t\tfill_error(err, 1, \"XShmExtension is not available but required!\");\n\t\treturn NULL;\n\t}\n\n\tif (!ctx)\n\t{\n\t\tctx = malloc(sizeof(CaptureContext));\n\n\t\tint major, minor;\n\t\tBool pixmaps = False;\n\t\tXShmQueryVersion(cap->disp, &major, &minor, &pixmaps);\n\t\tctx->has_offscreen = pixmaps == True;\n\t\tif (ctx->has_offscreen && cap->type == WINDOW && cap->c.winfo.is_regular_window)\n\t\t{\n\t\t\tint event_base, error_base;\n\t\t\tctx->has_offscreen =\n\t\t\t\tXCompositeQueryExtension(cap->disp, &event_base, &error_base) == True;\n\t\t\tif (ctx->has_offscreen)\n\t\t\t\tXCompositeRedirectWindow(cap->disp, cap->c.winfo.win, False);\n\t\t}\n\t\tconst char* session_type = getenv(\"XDG_SESSION_TYPE\");\n\t\tif (session_type && strcmp(session_type, \"wayland\") == 0)\n\t\t\tctx->wayland = 1;\n\t\telse\n\t\t\tctx->wayland = 0;\n\t}\n\tctx->cap = *cap;\n\tctx->last_img_return = True;\n\n\tif (&ctx->cap != cap)\n\t\tstrncpy(ctx->cap.name, cap->name, sizeof(ctx->cap.name));\n\n\tint event_base, error_base;\n\tctx->has_xfixes = XFixesQueryExtension(cap->disp, &event_base, &error_base) == True;\n\n\tint x, y;\n\tunsigned int width, height;\n\tget_geometry(cap, &x, &y, &width, &height, err);\n\tctx->ximg = XShmCreateImage(\n\t\tcap->disp,\n\t\tDefaultVisualOfScreen(cap->screen),\n\t\tDefaultDepthOfScreen(cap->screen),\n\t\tZPixmap,\n\t\tNULL,\n\t\t&ctx->shminfo,\n\t\twidth,\n\t\theight);\n\n\tctx->shminfo.shmid =\n\t\tshmget(IPC_PRIVATE, ctx->ximg->bytes_per_line * ctx->ximg->height, IPC_CREAT | 0777);\n\tctx->shminfo.shmaddr = ctx->ximg->data = (char*)shmat(ctx->shminfo.shmid, 0, 0);\n\tctx->shminfo.readOnly = False;\n\tif (ctx->shminfo.shmid < 0)\n\t{\n\t\tfill_error(err, 1, \"Fatal shminfo error!\");\n\t\tfree(ctx);\n\t\treturn NULL;\n\t}\n\tif (!XShmAttach(cap->disp, &ctx->shminfo))\n\t{\n\t\tfill_error(err, 1, \"XShmAttach() failed\");\n\t\tfree(ctx);\n\t\treturn NULL;\n\t}\n\n\treturn ctx;\n}\n\nvoid stop_capture(CaptureContext* ctx, Error* err)\n{\n\tXShmDetach(ctx->cap.disp, &ctx->shminfo);\n\tXDestroyImage(ctx->ximg);\n\tif (shmdt(ctx->shminfo.shmaddr) != 0)\n\t{\n\t\tfill_error(err, 1, \"Failed to detach shared memory!\");\n\t}\n\tshmctl(ctx->shminfo.shmid, IPC_RMID, NULL);\n\tif (ctx->has_offscreen && ctx->cap.type == WINDOW && ctx->cap.c.winfo.is_regular_window)\n\t\tXCompositeUnredirectWindow(ctx->cap.disp, ctx->cap.c.winfo.win, False);\n\tfree(ctx);\n}\n\nvoid capture_screen(CaptureContext* ctx, struct Image* img, int capture_cursor, Error* err)\n{\n\tWindow root = DefaultRootWindow(ctx->cap.disp);\n\tint x, y;\n\tunsigned int width, height;\n\tget_geometry(&ctx->cap, &x, &y, &width, &height, err);\n\tOK_OR_ABORT(err);\n\t// if window resized, create new cap...\n\tif (width != (unsigned int)ctx->ximg->width || height != (unsigned int)ctx->ximg->height)\n\t{\n\t\tXShmDetach(ctx->cap.disp, &ctx->shminfo);\n\t\tXDestroyImage(ctx->ximg);\n\t\tshmdt(ctx->shminfo.shmaddr);\n\t\tshmctl(ctx->shminfo.shmid, IPC_RMID, NULL);\n\t\tCaptureContext* new_ctx = start_capture(&ctx->cap, ctx, err);\n\t\tif (!new_ctx)\n\t\t{\n\t\t\treturn;\n\t\t}\n\t}\n\n\tBool get_img_ret = False;\n\n\tswitch (ctx->cap.type)\n\t{\n\tcase WINDOW:\n\t{\n\t\tWindow* active_window;\n\t\tunsigned long size;\n\n\t\tint is_offscreen = ctx->cap.c.winfo.is_regular_window &&\n\t\t\t\t\t\t   (x < 0 || y < 0 || x + (int)width > ctx->cap.screen->width ||\n\t\t\t\t\t\t\ty + (int)height > ctx->cap.screen->height);\n\n\t\tactive_window =\n\t\t\t(Window*)get_property(ctx->cap.disp, root, XA_WINDOW, \"_NET_ACTIVE_WINDOW\", &size, err);\n\t\tif (!ctx->wayland && *active_window == ctx->cap.c.winfo.win && !is_offscreen)\n\t\t{\n\t\t\t// cap window within its root so menus are visible as strictly speaking menus do not\n\t\t\t// belong to the window itself ...\n\t\t\t// But don't do this on (X)Wayland as the root window is just black in that case.\n\t\t\tget_img_ret = XShmGetImage(ctx->cap.disp, root, ctx->ximg, x, y, 0x00ffffff);\n\t\t}\n\t\telse\n\t\t{\n\t\t\t// ... but only if it is the active window as we might be recording the wrong thing\n\t\t\t// otherwise. If it is not active just record the window itself.\n\t\t\t// also if pixmaps are supported use those as they support capturing windows even if\n\t\t\t// they are offscreen\n\t\t\tif (is_offscreen)\n\t\t\t{\n\t\t\t\tif (ctx->has_offscreen)\n\t\t\t\t{\n\t\t\t\t\tPixmap pm = XCompositeNameWindowPixmap(ctx->cap.disp, ctx->cap.c.winfo.win);\n\t\t\t\t\tget_img_ret = XShmGetImage(ctx->cap.disp, pm, ctx->ximg, 0, 0, 0x00ffffff);\n\t\t\t\t\tXFreePixmap(ctx->cap.disp, pm);\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t\tERROR(\n\t\t\t\t\t\terr,\n\t\t\t\t\t\t1,\n\t\t\t\t\t\t\"Can not capture window as it is off screen and Xcomposite is \"\n\t\t\t\t\t\t\"unavailable!\");\n\t\t\t}\n\t\t\telse\n\t\t\t\tget_img_ret =\n\t\t\t\t\tXShmGetImage(ctx->cap.disp, ctx->cap.c.winfo.win, ctx->ximg, 0, 0, 0x00ffffff);\n\t\t}\n\t\tfree(active_window);\n\t\tbreak;\n\t}\n\tcase RECT:\n\t\tget_img_ret = XShmGetImage(ctx->cap.disp, root, ctx->ximg, x, y, 0x00ffffff);\n\t\tbreak;\n\t}\n\n\tBool last_img_return = ctx->last_img_return;\n\tctx->last_img_return = get_img_ret;\n\t// only print an error once and do not repeat this message if consecutive calls to XShmGetImage\n\t// fail to avoid spamming the logs.\n\tif (get_img_ret != True)\n\t{\n\t\tif (last_img_return != get_img_ret)\n\t\t{\n\t\t\tERROR(err, 1, \"XShmGetImage failed!\");\n\t\t}\n\t\telse\n\t\t{\n\t\t\tERROR(err, 2, \"XShmGetImage failed!\");\n\t\t}\n\t}\n\n\t// capture cursor if requested and if XFixes is available\n\tif (capture_cursor && ctx->has_xfixes)\n\t{\n\t\tXFixesCursorImage* cursor_img = XFixesGetCursorImage(ctx->cap.disp);\n\t\tif (cursor_img != NULL)\n\t\t{\n\t\t\tuint32_t* data = (uint32_t*)ctx->ximg->data;\n\n\t\t\t// coordinates of cursor inside ximg\n\t\t\tint x0 = cursor_img->x - cursor_img->xhot - x;\n\t\t\tint y0 = cursor_img->y - cursor_img->yhot - y;\n\n\t\t\t// clamp part of cursor image to draw to the part of the cursor that is inside\n\t\t\t// the captured area\n\t\t\tint i0 = clamp(0, -x0, width - x0);\n\t\t\tint i1 = clamp(cursor_img->width, -x0, width - x0);\n\t\t\tint j0 = clamp(0, -y0, height - y0);\n\t\t\tint j1 = clamp(cursor_img->height, -y0, height - y0);\n\t\t\t// paint cursor image into captured image\n\t\t\tfor (int j = j0; j < j1; ++j)\n\t\t\t\tfor (int i = i0; i < i1; ++i)\n\t\t\t\t{\n\t\t\t\t\tuint32_t c_pixel = cursor_img->pixels[j * cursor_img->width + i];\n\t\t\t\t\tunsigned char a = (c_pixel & 0xff000000) >> 24;\n\t\t\t\t\tif (a)\n\t\t\t\t\t{\n\t\t\t\t\t\tuint32_t d_pixel = data[(j + y0) * width + i + x0];\n\n\t\t\t\t\t\tunsigned char c1 = (c_pixel & 0x00ff0000) >> 16;\n\t\t\t\t\t\tunsigned char c2 = (c_pixel & 0x0000ff00) >> 8;\n\t\t\t\t\t\tunsigned char c3 = (c_pixel & 0x000000ff) >> 0;\n\t\t\t\t\t\tunsigned char d1 = (d_pixel & 0x00ff0000) >> 16;\n\t\t\t\t\t\tunsigned char d2 = (d_pixel & 0x0000ff00) >> 8;\n\t\t\t\t\t\tunsigned char d3 = (d_pixel & 0x000000ff) >> 0;\n\t\t\t\t\t\t// colors from the cursor image are premultiplied with the alpha channel\n\t\t\t\t\t\tunsigned char f1 = c1 + d1 * (255 - a) / 255;\n\t\t\t\t\t\tunsigned char f2 = c2 + d2 * (255 - a) / 255;\n\t\t\t\t\t\tunsigned char f3 = c3 + d3 * (255 - a) / 255;\n\t\t\t\t\t\tdata[(j + y0) * width + i + x0] = (f1 << 16) | (f2 << 8) | (f3 << 0);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\tXFree(cursor_img);\n\t\t}\n\t\telse\n\t\t{\n\t\t\tlog_warn(\n\t\t\t\t\"Failed to obtain cursor image, XFixesGetCursorImage has returned a null pointer.\");\n\t\t}\n\t}\n\n\timg->width = ctx->ximg->width;\n\timg->height = ctx->ximg->height;\n\timg->data = ctx->ximg->data;\n}\n"
  },
  {
    "path": "lib/linux/xhelper.c",
    "content": "#include <X11/X.h>\n#include <X11/Xlib.h>\n#include <X11/extensions/XInput.h>\n#include <X11/extensions/XInput2.h>\n#include <X11/extensions/Xrandr.h>\n#include <X11/extensions/randr.h>\n#include <stdlib.h>\n#include <string.h>\n\n#include \"../error.h\"\n#include \"../log.h\"\n#include \"xhelper.h\"\n\nint x11_error_handler(Display* disp, XErrorEvent* err)\n{\n\tchar buf1[128], buf2[128], message_selector[64];\n\tXGetErrorText(disp, err->error_code, buf1, sizeof(buf1));\n\tsnprintf(message_selector, sizeof(message_selector), \"XRequest.%d\", err->request_code);\n\tXGetErrorDatabaseText(disp, \"\", message_selector, message_selector, buf2, sizeof(buf2));\n\tlog_debug(\"X11 error: %s: %s 0x%lx\", buf1, buf2, err->resourceid);\n\treturn 0;\n}\n\nvoid x11_set_error_handler()\n{\n\t// setting an error handler is required as otherwise xlib may just exit the process, even though\n\t// the error was recoverable.\n\tXSetErrorHandler(x11_error_handler);\n}\n\nint locale_to_utf8(char* src, char* dest, size_t size)\n{\n\ticonv_t icd = iconv_open(\"UTF-8//IGNORE\", \"\");\n\tsize_t src_size = size;\n\tsize_t outbytes_left = MAX_PROPERTY_VALUE_LEN - 1;\n\tint ret = iconv(icd, &src, &src_size, &dest, &outbytes_left);\n\ticonv_close(icd);\n\tif (ret < 0)\n\t{\n\t\treturn -1;\n\t}\n\tdest[src_size - 1 - outbytes_left] = '\\0';\n\treturn 0;\n}\n\nchar* get_property(\n\tDisplay* disp, Window win, Atom xa_prop_type, char* prop_name, unsigned long* size, Error* err)\n{\n\tAtom xa_prop_name;\n\tAtom xa_ret_type;\n\tint ret_format;\n\tunsigned long ret_nitems;\n\tunsigned long ret_bytes_after;\n\tunsigned long tmp_size;\n\tunsigned char* ret_prop;\n\tchar* ret;\n\n\txa_prop_name = XInternAtom(disp, prop_name, False);\n\n\t/* MAX_PROPERTY_VALUE_LEN / 4 explanation (XGetWindowProperty manpage):\n\t *\n\t * long_length = Specifies the length in 32-bit multiples of the\n\t *               data to be retrieved.\n\t */\n\tif (XGetWindowProperty(\n\t\t\tdisp,\n\t\t\twin,\n\t\t\txa_prop_name,\n\t\t\t0,\n\t\t\tMAX_PROPERTY_VALUE_LEN / 4,\n\t\t\tFalse,\n\t\t\txa_prop_type,\n\t\t\t&xa_ret_type,\n\t\t\t&ret_format,\n\t\t\t&ret_nitems,\n\t\t\t&ret_bytes_after,\n\t\t\t&ret_prop) != Success)\n\t{\n\t\tfill_error(err, 1, \"Cannot get %s property.\", prop_name);\n\t\treturn NULL;\n\t}\n\n\tif (xa_ret_type != xa_prop_type)\n\t{\n\t\tfill_error(err, 1, \"Invalid type of %s property.\", prop_name);\n\t\tXFree(ret_prop);\n\t\treturn NULL;\n\t}\n\n\t/* null terminate the result to make string handling easier */\n\ttmp_size = (ret_format / 8) * ret_nitems;\n\t/* Correct 64 Architecture implementation of 32 bit data */\n\tif (ret_format == 32)\n\t\ttmp_size *= sizeof(long) / 4;\n\tret = malloc(tmp_size + 1);\n\tmemcpy(ret, ret_prop, tmp_size);\n\tret[tmp_size] = '\\0';\n\n\tif (size)\n\t{\n\t\t*size = tmp_size;\n\t}\n\n\tXFree(ret_prop);\n\treturn ret;\n}\n\nchar* get_window_title(Display* disp, Window win, Error* err)\n{\n\tchar* title_utf8;\n\tchar* wm_name;\n\tchar* net_wm_name;\n\tError err_wm;\n\tError err_net_wm;\n\n\twm_name = get_property(disp, win, XA_STRING, \"WM_NAME\", NULL, &err_wm);\n\tnet_wm_name = get_property(\n\t\tdisp, win, XInternAtom(disp, \"UTF8_STRING\", False), \"_NET_WM_NAME\", NULL, &err_net_wm);\n\n\tif (net_wm_name)\n\t{\n\t\ttitle_utf8 = strdup(net_wm_name);\n\t}\n\telse\n\t{\n\t\tif (wm_name)\n\t\t{\n\t\t\ttitle_utf8 = malloc(MAX_PROPERTY_VALUE_LEN);\n\t\t\tif (locale_to_utf8(wm_name, title_utf8, MAX_PROPERTY_VALUE_LEN) != 0)\n\t\t\t{\n\t\t\t\tfill_error(err, 1, \"Failed to convert windowname to UTF-8!\");\n\t\t\t\tfree(title_utf8);\n\t\t\t\ttitle_utf8 = NULL;\n\t\t\t}\n\t\t}\n\t\telse\n\t\t{\n\t\t\tfill_error(\n\t\t\t\terr,\n\t\t\t\t1,\n\t\t\t\t\"Could not get window name: (%s) (%s)\",\n\t\t\t\terr_net_wm.error_str,\n\t\t\t\terr_wm.error_str);\n\t\t\ttitle_utf8 = NULL;\n\t\t}\n\t}\n\n\tfree(wm_name);\n\tfree(net_wm_name);\n\n\treturn title_utf8;\n}\n\nWindow* get_client_list(Display* disp, unsigned long* size, Error* err)\n{\n\tWindow* client_list;\n\tError err_net;\n\tError err_win;\n\tif ((client_list = (Window*)get_property(\n\t\t\t disp, DefaultRootWindow(disp), XA_WINDOW, \"_NET_CLIENT_LIST\", size, &err_net)) == NULL)\n\t{\n\t\tif ((client_list = (Window*)get_property(\n\t\t\t\t disp, DefaultRootWindow(disp), XA_CARDINAL, \"_WIN_CLIENT_LIST\", size, &err_win)) ==\n\t\t\tNULL)\n\t\t{\n\t\t\tfill_error(\n\t\t\t\terr,\n\t\t\t\t2,\n\t\t\t\t\"Cannot get client list properties. \"\n\t\t\t\t\"_NET_CLIENT_LIST: %s or _WIN_CLIENT_LIST: %s\",\n\t\t\t\terr_net.error_str,\n\t\t\t\terr_win.error_str);\n\t\t\treturn NULL;\n\t\t}\n\t}\n\n\treturn client_list;\n}\n\nint create_capturables(\n\tDisplay* disp, Capturable** capturables, int* num_monitors, int size, Error* err)\n{\n\tif (size <= 0)\n\t\treturn 0;\n\n\tint screen = DefaultScreen(disp);\n\tWindow root = RootWindow(disp, screen);\n\n\tint event_base, error_base, major, minor;\n\t*num_monitors = 0;\n\tXRRMonitorInfo* monitors = NULL;\n\tif (XRRQueryExtension(disp, &event_base, &error_base) && XRRQueryVersion(disp, &major, &minor))\n\t{\n\t\tmonitors = XRRGetMonitors(disp, root, True, num_monitors);\n\t\tif (*num_monitors < 0)\n\t\t{\n\t\t\t*num_monitors = 0;\n\t\t\tfill_error(err, 2, \"Failed to query monitor info via xrandr.\");\n\t\t}\n\t}\n\telse\n\t{\n\t\tfill_error(err, 2, \"Xrandr is unsupported on this X server.\");\n\t}\n\n\tWindow* client_list;\n\tunsigned long client_list_size;\n\n\tsize_t num_windows = ((client_list = get_client_list(disp, &client_list_size, err)) == NULL)\n\t\t\t\t\t\t\t ? 0\n\t\t\t\t\t\t\t : client_list_size / sizeof(Window);\n\n\tsize_t i = 0;\n\tCapturable* c = malloc(sizeof(Capturable));\n\tcapturables[i] = c;\n\tc->disp = disp;\n\tc->screen = ScreenOfDisplay(disp, screen);\n\tstrncpy(c->name, \"Desktop\", sizeof(c->name) - 1);\n\tc->type = WINDOW;\n\tc->c.winfo.win = root;\n\tc->c.winfo.is_regular_window = 0;\n\t++i;\n\n\tfor (; i < (size_t)*num_monitors + 1 && i < (size_t)size; ++i)\n\t{\n\t\tCapturable* c = malloc(sizeof(Capturable));\n\t\tcapturables[i] = c;\n\t\tXRRMonitorInfo* m = &monitors[i - 1];\n\t\tc->disp = disp;\n\t\tc->screen = ScreenOfDisplay(disp, screen);\n\t\tchar* name = XGetAtomName(disp, m->name);\n\t\tsnprintf(c->name, sizeof(c->name) - 1, \"Monitor: %s\", name);\n\t\tXFree(name);\n\t\tc->type = RECT;\n\t\tc->c.rinfo.x = m->x;\n\t\tc->c.rinfo.y = m->y;\n\t\tc->c.rinfo.width = m->width;\n\t\tc->c.rinfo.height = m->height;\n\t}\n\n\tfor (; i < num_windows + *num_monitors + 1 && i < (size_t)size; ++i)\n\t{\n\t\tsize_t j = i - *num_monitors - 1;\n\t\tchar* title_utf8 = get_window_title(disp, client_list[j], NULL);\n\t\tif (title_utf8 == NULL)\n\t\t{\n\t\t\ttitle_utf8 = malloc(32);\n\t\t\tsnprintf(title_utf8, 32, \"UNKNOWN %lu\", j);\n\t\t}\n\n\t\tCapturable* c = malloc(sizeof(Capturable));\n\t\tcapturables[i] = c;\n\t\tc->disp = disp;\n\t\tc->screen = ScreenOfDisplay(disp, screen);\n\t\tc->type = WINDOW;\n\t\tstrncpy(c->name, title_utf8, sizeof(c->name) - 1);\n\t\tc->c.winfo.win = client_list[j];\n\t\tc->c.winfo.is_regular_window = 1;\n\t\tfree(title_utf8);\n\t}\n\tfree(client_list);\n\tXRRFreeMonitors(monitors);\n\treturn i;\n}\n\nvoid* clone_capturable(Capturable* c)\n{\n\tCapturable* c2 = malloc(sizeof(Capturable));\n\t*c2 = *c;\n\tmemcpy(c2->name, c->name, sizeof(c2->name));\n\treturn c2;\n}\n\nvoid destroy_capturable(Capturable* c) { free(c); }\n\nvoid get_window_geometry(\n\tDisplay* disp,\n\tWindow win,\n\tint* x,\n\tint* y,\n\tunsigned int* width,\n\tunsigned int* height,\n\tError* err)\n{\n\tWindow junkroot;\n\tint junkx, junky;\n\tunsigned int bw, depth;\n\tif (!XGetGeometry(disp, win, &junkroot, &junkx, &junky, width, height, &bw, &depth))\n\t{\n\t\tERROR(err, 1, \"Failed to get window geometry!\");\n\t}\n\tXTranslateCoordinates(disp, win, junkroot, 0, 0, x, y, &junkroot);\n}\n\nvoid get_geometry(\n\tCapturable* cap, int* x, int* y, unsigned int* width, unsigned int* height, Error* err)\n{\n\tswitch (cap->type)\n\t{\n\tcase WINDOW:\n\t\tget_window_geometry(cap->disp, cap->c.winfo.win, x, y, width, height, err);\n\t\treturn;\n\tcase RECT:\n\t\t*x = cap->c.rinfo.x;\n\t\t*y = cap->c.rinfo.y;\n\t\t*width = cap->c.rinfo.width;\n\t\t*height = cap->c.rinfo.height;\n\t\treturn;\n\t}\n}\n\nvoid get_geometry_relative(\n\tCapturable* cap, float* x, float* y, float* width, float* height, Error* err)\n{\n\tint x_tmp, y_tmp;\n\tunsigned int width_tmp, height_tmp;\n\tget_geometry(cap, &x_tmp, &y_tmp, &width_tmp, &height_tmp, err);\n\tOK_OR_ABORT(err);\n\t*x = x_tmp / (float)cap->screen->width;\n\t*y = y_tmp / (float)cap->screen->height;\n\t*width = width_tmp / (float)cap->screen->width;\n\t*height = height_tmp / (float)cap->screen->height;\n}\n\nvoid client_msg(\n\tDisplay* disp,\n\tWindow win,\n\tchar* msg,\n\tunsigned long data0,\n\tunsigned long data1,\n\tunsigned long data2,\n\tunsigned long data3,\n\tunsigned long data4,\n\tError* err)\n{\n\tXEvent event;\n\tlong mask = SubstructureRedirectMask | SubstructureNotifyMask;\n\n\tevent.xclient.type = ClientMessage;\n\tevent.xclient.serial = 0;\n\tevent.xclient.send_event = True;\n\tevent.xclient.message_type = XInternAtom(disp, msg, False);\n\tevent.xclient.window = win;\n\tevent.xclient.format = 32;\n\tevent.xclient.data.l[0] = data0;\n\tevent.xclient.data.l[1] = data1;\n\tevent.xclient.data.l[2] = data2;\n\tevent.xclient.data.l[3] = data3;\n\tevent.xclient.data.l[4] = data4;\n\n\tif (!XSendEvent(disp, DefaultRootWindow(disp), False, mask, &event))\n\t{\n\t\tERROR(err, 1, \"Cannot send %s event.\", msg);\n\t}\n}\n\nvoid activate_window(Display* disp, WindowInfo* winfo, Error* err)\n{\n\t// do not activate windows like the root window or root windows of a screen\n\tif (!winfo->is_regular_window)\n\t\treturn;\n\n\tWindow* active_window = 0;\n\tunsigned long size;\n\n\tactive_window = (Window*)get_property(\n\t\tdisp, DefaultRootWindow(disp), XA_WINDOW, \"_NET_ACTIVE_WINDOW\", &size, err);\n\tif (*active_window == winfo->win)\n\t{\n\t\t// nothing to do window is active already\n\t\tfree(active_window);\n\t\treturn;\n\t}\n\tfree(active_window);\n\n\tunsigned long* desktop;\n\t/* desktop ID */\n\tif ((desktop = (unsigned long*)get_property(\n\t\t\t disp, winfo->win, XA_CARDINAL, \"_NET_WM_DESKTOP\", NULL, err)) == NULL)\n\t{\n\t\tif ((desktop = (unsigned long*)get_property(\n\t\t\t\t disp, winfo->win, XA_CARDINAL, \"_WIN_WORKSPACE\", NULL, err)) == NULL)\n\t\t{\n\t\t\tERROR(err, 1, \"Cannot find desktop ID of the window.\");\n\t\t}\n\t}\n\tclient_msg(disp, DefaultRootWindow(disp), \"_NET_CURRENT_DESKTOP\", *desktop, 0, 0, 0, 0, err);\n\tfree(desktop);\n\tOK_OR_ABORT(err);\n\n\tclient_msg(disp, winfo->win, \"_NET_ACTIVE_WINDOW\", 0, 0, 0, 0, 0, err);\n\tOK_OR_ABORT(err);\n\tXMapRaised(disp, winfo->win);\n}\n\nvoid capturable_before_input(Capturable* cap, Error* err)\n{\n\tswitch (cap->type)\n\t{\n\tcase WINDOW:\n\t\tactivate_window(cap->disp, &cap->c.winfo, err);\n\t\tbreak;\n\tcase RECT:\n\t\tbreak;\n\t}\n}\n\nconst char* get_capturable_name(Capturable* c) { return c->name; }\n\nvoid map_input_device_to_entire_screen(Display* disp, const char* device_name, int pen, Error* err)\n{\n\n\t// for some reason a device simualting a stylus does NOT create a single device in\n\t// XListInputDevices but actually two: One with the original name and the other one with\n\t// \"Pen (0)\" appended to it. The problem is that the original device does NOT permit setting\n\t// \"Coordinate Transformation Matrix\". This can only be done for the device with \"Pen (0)\"\n\t// appended. So this here is a dirty workaround assuming the configurable stylus/pen device is\n\t// always called original name + \"Pen\" + whatever.\n\tchar pen_name[256];\n\tif (pen)\n\t\tsnprintf(pen_name, sizeof(pen_name), \"%s Pen\", device_name);\n\tXID device_id;\n\tint num_devices = 0;\n\tXDeviceInfo* devices = XListInputDevices(disp, &num_devices);\n\n\tint found = 0;\n\tfor (int i = 0; i < num_devices; ++i)\n\t{\n\t\tif ((!pen && strcmp(device_name, devices[i].name) == 0) ||\n\t\t\t(pen && strncmp(pen_name, devices[i].name, strlen(pen_name)) == 0))\n\t\t{\n\t\t\tdevice_id = devices[i].id;\n\t\t\tfound = 1;\n\t\t\tbreak;\n\t\t}\n\t}\n\tXFreeDeviceList(devices);\n\n\tif (!found)\n\t\tERROR(err, 2, \"Device with name: %s not found!\", device_name);\n\n\tAtom prop_float, prop_matrix;\n\n\tunion\n\t{\n\t\tunsigned char* c;\n\t\tfloat* f;\n\t} data;\n\tint format_return;\n\tAtom type_return;\n\tunsigned long nitems;\n\tunsigned long bytes_after;\n\n\tint rc;\n\n\tprop_float = XInternAtom(disp, \"FLOAT\", False);\n\tprop_matrix = XInternAtom(disp, \"Coordinate Transformation Matrix\", False);\n\n\tif (!prop_float)\n\t{\n\t\tERROR(err, 1, \"Float atom not found. This server is too old.\");\n\t}\n\tif (!prop_matrix)\n\t{\n\t\tERROR(\n\t\t\terr,\n\t\t\t1,\n\t\t\t\"Coordinate transformation matrix not found. This \"\n\t\t\t\"server is too old.\");\n\t}\n\n\trc = XIGetProperty(\n\t\tdisp,\n\t\tdevice_id,\n\t\tprop_matrix,\n\t\t0,\n\t\t9,\n\t\tFalse,\n\t\tprop_float,\n\t\t&type_return,\n\t\t&format_return,\n\t\t&nitems,\n\t\t&bytes_after,\n\t\t&data.c);\n\tif (rc != Success || prop_float != type_return || format_return != 32 || nitems != 9 ||\n\t\tbytes_after != 0)\n\t{\n\t\tERROR(err, 1, \"Failed to retrieve current property values.\");\n\t}\n\n\tdata.f[0] = 1.0;\n\tdata.f[1] = 0.0;\n\tdata.f[2] = 0.0;\n\tdata.f[3] = 0.0;\n\tdata.f[4] = 1.0;\n\tdata.f[5] = 0.0;\n\tdata.f[6] = 0.0;\n\tdata.f[7] = 0.0;\n\tdata.f[8] = 1.0;\n\n\tXIChangeProperty(\n\t\tdisp, device_id, prop_matrix, prop_float, format_return, PropModeReplace, data.c, nitems);\n\n\tXFree(data.c);\n}\n"
  },
  {
    "path": "lib/linux/xhelper.h",
    "content": "#pragma once\n\n#include <X11/X.h>\n#include <X11/Xatom.h>\n#include <X11/Xlib.h>\n#include <X11/Xutil.h>\n\n#include <iconv.h>\n#include <malloc.h>\n#include <string.h>\n\n#include \"../error.h\"\n\n#define MAX_PROPERTY_VALUE_LEN 4096\n\ntypedef struct WindowInfo\n{\n\tWindow win;\n\tint is_regular_window;\n} WindowInfo;\n\ntypedef struct RectInfo\n{\n\tint x;\n\tint y;\n\tunsigned int width;\n\tunsigned int height;\n} RectInfo;\n\ntypedef enum CaptureType\n{\n\tWINDOW,\n\tRECT\n} CaptureType;\n\ntypedef struct Capturable\n{\n\tCaptureType type;\n\tchar name[128];\n\tDisplay* disp;\n\tScreen* screen;\n\tunion\n\t{\n\t\tWindowInfo winfo;\n\t\tRectInfo rinfo;\n\t} c;\n} Capturable;\n\nchar* get_property(\n\tDisplay* disp, Window win, Atom xa_prop_type, char* prop_name, unsigned long* size, Error* err);\n\nvoid get_geometry(\n\tCapturable* cap, int* x, int* y, unsigned int* width, unsigned int* height, Error* err);\n\nvoid get_geometry_relative(\n\tCapturable* cap, float* x, float* y, float* width, float* height, Error* err);\n"
  },
  {
    "path": "lib/log.c",
    "content": "#include \"log.h\"\n\n// rust functions living in log.rs\nvoid log_error_rust(const char*);\nvoid log_debug_rust(const char*);\nvoid log_info_rust(const char*);\nvoid log_trace_rust(const char*);\nvoid log_warn_rust(const char*);\n\nvoid log_error(const char* fmt, ...)\n{\n\tva_list args;\n\tva_start(args, fmt);\n\tchar buf[2048];\n\tvsnprintf(buf, sizeof(buf), fmt, args);\n\tlog_error_rust(buf);\n}\n\nvoid log_debug(const char* fmt, ...)\n{\n\tva_list args;\n\tva_start(args, fmt);\n\tchar buf[2048];\n\tvsnprintf(buf, sizeof(buf), fmt, args);\n\tlog_debug_rust(buf);\n}\n\nvoid log_info(const char* fmt, ...)\n{\n\tva_list args;\n\tva_start(args, fmt);\n\tchar buf[2048];\n\tvsnprintf(buf, sizeof(buf), fmt, args);\n\tlog_info_rust(buf);\n}\n\nvoid log_trace(const char* fmt, ...)\n{\n\tva_list args;\n\tva_start(args, fmt);\n\tchar buf[2048];\n\tvsnprintf(buf, sizeof(buf), fmt, args);\n\tlog_trace_rust(buf);\n}\n\nvoid log_warn(const char* fmt, ...)\n{\n\tva_list args;\n\tva_start(args, fmt);\n\tchar buf[2048];\n\tvsnprintf(buf, sizeof(buf), fmt, args);\n\tlog_warn_rust(buf);\n}\n"
  },
  {
    "path": "lib/log.h",
    "content": "#pragma once\n\n#include <stdarg.h>\n#include <stdio.h>\n\n#if defined(__clang__) || defined(__GNUC__)\n__attribute__((__format__ (__printf__, 1, 2)))\nvoid log_error(const char* fmt, ...);\n__attribute__((__format__ (__printf__, 1, 2)))\nvoid log_debug(const char* fmt, ...);\n__attribute__((__format__ (__printf__, 1, 2)))\nvoid log_info(const char* fmt, ...);\n__attribute__((__format__ (__printf__, 1, 2)))\nvoid log_trace(const char* fmt, ...);\n__attribute__((__format__ (__printf__, 1, 2)))\nvoid log_warn(const char* fmt, ...);\n#else\nvoid log_error(const char* fmt, ...);\nvoid log_debug(const char* fmt, ...);\nvoid log_info(const char* fmt, ...);\nvoid log_trace(const char* fmt, ...);\nvoid log_warn(const char* fmt, ...);\n#endif\n"
  },
  {
    "path": "src/capturable/captrs_capture.rs",
    "content": "use crate::capturable::{Capturable, Recorder};\nuse captrs::Capturer;\nuse std::boxed::Box;\nuse std::error::Error;\nuse winapi::shared::windef::RECT;\n\nuse super::Geometry;\n\n#[derive(Clone)]\npub struct CaptrsCapturable {\n    id: u8,\n    name: String,\n    screen: RECT,\n    virtual_screen: RECT,\n}\n\nimpl CaptrsCapturable {\n    pub fn new(id: u8, name: String, screen: RECT, virtual_screen: RECT) -> CaptrsCapturable {\n        CaptrsCapturable {\n            id,\n            name,\n            screen,\n            virtual_screen,\n        }\n    }\n}\n\nimpl Capturable for CaptrsCapturable {\n    fn name(&self) -> String {\n        format!(\"Desktop {} (captrs)\", self.name).into()\n    }\n    fn before_input(&mut self) -> Result<(), Box<dyn Error>> {\n        Ok(())\n    }\n    fn recorder(&self, _capture_cursor: bool) -> Result<Box<dyn Recorder>, Box<dyn Error>> {\n        Ok(Box::new(CaptrsRecorder::new(self.id)?))\n    }\n    fn geometry(&self) -> Result<Geometry, Box<dyn Error>> {\n        Ok(Geometry::VirtualScreen(\n            self.screen.left - self.virtual_screen.left,\n            self.screen.top - self.virtual_screen.top,\n            (self.screen.right - self.screen.left) as u32,\n            (self.screen.bottom - self.screen.top) as u32,\n            self.screen.left,\n            self.screen.top,\n        ))\n    }\n}\n#[derive(Debug)]\npub struct CaptrsError(String);\n\nimpl std::fmt::Display for CaptrsError {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        let Self(s) = self;\n        write!(f, \"{}\", s)\n    }\n}\n\nimpl Error for CaptrsError {}\npub struct CaptrsRecorder {\n    capturer: Capturer,\n}\n\nimpl CaptrsRecorder {\n    pub fn new(id: u8) -> Result<CaptrsRecorder, Box<dyn Error>> {\n        Ok(CaptrsRecorder {\n            capturer: Capturer::new(id.into())?,\n        })\n    }\n}\n\nimpl Recorder for CaptrsRecorder {\n    fn capture(&mut self) -> Result<crate::video::PixelProvider, Box<dyn Error>> {\n        self.capturer\n            .capture_store_frame()\n            .map_err(|_e| CaptrsError(\"Captrs failed to capture frame\".into()))?;\n        let (w, h) = self.capturer.geometry();\n        Ok(crate::video::PixelProvider::BGR0(\n            w as usize,\n            h as usize,\n            unsafe { std::mem::transmute(self.capturer.get_stored_frame().unwrap()) },\n        ))\n    }\n}\n"
  },
  {
    "path": "src/capturable/core_graphics.rs",
    "content": "use std::boxed::Box;\nuse std::error::Error;\nuse std::ffi::c_void;\nuse std::time::{Duration, Instant};\n\nuse core_foundation::{\n    array::CFArray,\n    base::{TCFType, ToVoid},\n    data::CFData,\n    dictionary::{CFDictionary, CFDictionaryRef},\n    number::{CFNumber, CFNumberRef},\n    string::{CFString, CFStringRef},\n};\nuse core_graphics::{\n    display,\n    display::{CGDisplay, CGRect},\n    image::CGImage,\n    window,\n    window::CGWindowID,\n};\n\nuse crate::capturable::{Capturable, Geometry, Recorder};\n\n#[derive(Debug)]\npub struct CGError(String);\n\nimpl std::fmt::Display for CGError {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        let Self(s) = self;\n        write!(f, \"{}\", s)\n    }\n}\n\nimpl Error for CGError {}\n\n#[derive(Clone)]\npub struct CGDisplayCapturable {\n    display: CGDisplay,\n}\n\nimpl CGDisplayCapturable {\n    pub fn new(display: CGDisplay) -> Self {\n        Self { display }\n    }\n}\n\nimpl Capturable for CGDisplayCapturable {\n    fn name(&self) -> String {\n        format!(\n            \"Monitor (CG, {}x{})\",\n            self.display.pixels_wide(),\n            self.display.pixels_high()\n        )\n    }\n    fn geometry(&self) -> Result<Geometry, Box<dyn Error>> {\n        let bounds = self.display.bounds();\n        let (x0, y0, w, h) = screen_coordsys()?;\n        Ok(Geometry::Relative(\n            (bounds.origin.x - x0) / w,\n            (bounds.origin.y - y0) / h,\n            bounds.size.width / w,\n            bounds.size.height / h,\n        ))\n    }\n    fn before_input(&mut self) -> Result<(), Box<dyn Error>> {\n        Ok(())\n    }\n    fn recorder(&self, capture_cursor: bool) -> Result<Box<dyn Recorder>, Box<dyn Error>> {\n        Ok(Box::new(RecorderCGDisplay::new(\n            self.display,\n            capture_cursor,\n        )))\n    }\n}\n\npub struct RecorderCGDisplay {\n    img_data: Option<CFData>,\n    display: CGDisplay,\n    capture_cursor: bool,\n}\n\nimpl RecorderCGDisplay {\n    pub fn new(display: CGDisplay, capture_cursor: bool) -> Self {\n        Self {\n            img_data: None,\n            display,\n            capture_cursor,\n        }\n    }\n}\n\nfn check_pixelformat(img: &CGImage) -> Result<(), Box<dyn Error>> {\n    // for now assume that the pixels are always in BGR0 format\n    // do some basic checks to verify this\n    if img.bits_per_pixel() != 32 {\n        Err(CGError(format!(\n            \"Only BGR0 with 32 bits per pixel is supported, not {} bits!\",\n            img.bits_per_pixel()\n        )))?\n    }\n    if img.bits_per_component() != 8 {\n        Err(CGError(format!(\n            \"Only BGR0 with 8 bits per component is supported, not {} bits!\",\n            img.bits_per_component()\n        )))?\n    }\n    Ok(())\n}\n\nimpl Recorder for RecorderCGDisplay {\n    fn capture(&mut self) -> Result<crate::video::PixelProvider, Box<dyn Error>> {\n        let img = if self.capture_cursor {\n            CGDisplay::screenshot(self.display.bounds(), 0, 0, 0)\n        } else {\n            self.display.image()\n        };\n        if let Some(img) = img {\n            check_pixelformat(&img)?;\n            let w = img.width() as usize;\n            let h = img.height() as usize;\n\n            // extract raw image data\n            self.img_data = Some(img.data());\n            Ok(crate::video::PixelProvider::BGR0S(\n                w,\n                h,\n                img.bytes_per_row(),\n                self.img_data.as_ref().unwrap().bytes(),\n            ))\n        } else {\n            Err(Box::new(CGError(\n                \"Failed to capture screen using CoreGraphics.\".into(),\n            )))\n        }\n    }\n}\n\n#[derive(Clone)]\npub struct CGWindowCapturable {\n    id: CGWindowID,\n    name: String,\n    cursor_id: CGWindowID,\n    bounds: CGRect,\n    geometry_relative: (f64, f64, f64, f64),\n    last_geometry_update: Instant,\n}\n\nimpl CGWindowCapturable {\n    fn update_geometry(&mut self) -> Result<(), Box<dyn Error>> {\n        if Instant::now() - self.last_geometry_update > Duration::from_secs(1) {\n            self.bounds = get_window_infos()\n                .iter()\n                .find(|w| w.id == self.id)\n                .ok_or_else(|| {\n                    CGError(format!(\n                        \"Could not find information for current window {}.\",\n                        self.id\n                    ))\n                })?\n                .bounds;\n            let (x0, y0, w, h) = screen_coordsys()?;\n            self.geometry_relative = (\n                (self.bounds.origin.x - x0) / w,\n                (self.bounds.origin.y - y0) / h,\n                self.bounds.size.width / w,\n                self.bounds.size.height / h,\n            );\n            self.last_geometry_update = Instant::now();\n        }\n        Ok(())\n    }\n}\n\nimpl Capturable for CGWindowCapturable {\n    fn name(&self) -> String {\n        self.name.clone()\n    }\n    fn geometry(&self) -> Result<Geometry, Box<dyn Error>> {\n        let (x, y, w, h) = self.geometry_relative;\n        Ok(Geometry::Relative(x, y, w, h))\n    }\n    fn before_input(&mut self) -> Result<(), Box<dyn Error>> {\n        self.update_geometry()\n    }\n    fn recorder(&self, capture_cursor: bool) -> Result<Box<dyn Recorder>, Box<dyn Error>> {\n        Ok(Box::new(RecorderCGWindow {\n            img_data: None,\n            capture_cursor,\n            win: self.clone(),\n        }))\n    }\n}\npub struct RecorderCGWindow {\n    img_data: Option<CFData>,\n    capture_cursor: bool,\n    win: CGWindowCapturable,\n}\n\nimpl Recorder for RecorderCGWindow {\n    fn capture(&mut self) -> Result<crate::video::PixelProvider, Box<dyn Error>> {\n        self.win.update_geometry()?;\n        let img = CGDisplay::screenshot_from_windows(\n            self.win.bounds,\n            if self.capture_cursor {\n                CFArray::from_copyable(&[\n                    self.win.cursor_id as *const c_void,\n                    self.win.id as *const c_void,\n                ])\n            } else {\n                CFArray::from_copyable(&[self.win.id as *const c_void])\n            },\n            0,\n        );\n        if let Some(img) = img {\n            check_pixelformat(&img)?;\n            let w = img.width() as usize;\n            let h = img.height() as usize;\n\n            // extract raw image data\n            self.img_data = Some(img.data());\n            Ok(crate::video::PixelProvider::BGR0S(\n                w,\n                h,\n                img.bytes_per_row(),\n                self.img_data.as_ref().unwrap().bytes(),\n            ))\n        } else {\n            Err(Box::new(CGError(\n                \"Failed to capture window using CoreGraphics.\".into(),\n            )))\n        }\n    }\n}\n\n#[derive(Debug)]\nstruct WindowInfo {\n    pub id: CGWindowID,\n    pub name: String,\n    pub bounds: CGRect,\n}\n\nfn get_window_infos() -> Vec<WindowInfo> {\n    let mut win_infos = vec![];\n    let wins = CGDisplay::window_list_info(\n        display::kCGWindowListExcludeDesktopElements | display::kCGWindowListOptionOnScreenOnly,\n        None,\n    );\n    if let Some(wins) = wins {\n        for w in wins.iter() {\n            let w: CFDictionary<*const c_void, *const c_void> =\n                unsafe { CFDictionary::wrap_under_get_rule(*w as CFDictionaryRef) };\n            let id = w.get(unsafe { window::kCGWindowNumber }.to_void());\n            let id = unsafe { CFNumber::wrap_under_get_rule(*id as CFNumberRef) }\n                .to_i64()\n                .unwrap() as CGWindowID;\n\n            let bounds = w.get(unsafe { window::kCGWindowBounds }.to_void());\n            let bounds = unsafe { CFDictionary::wrap_under_get_rule(*bounds as CFDictionaryRef) };\n            let bounds = CGRect::from_dict_representation(&bounds).unwrap();\n\n            let name = match w.find(unsafe { window::kCGWindowName }.to_void()) {\n                Some(n) => n,\n                None => continue,\n            };\n\n            let name = unsafe { CFString::wrap_under_get_rule(*name as CFStringRef) };\n            win_infos.push(WindowInfo {\n                id,\n                name: name.to_string(),\n                bounds,\n            });\n        }\n    }\n    win_infos\n}\n\npub fn screen_coordsys() -> Result<(f64, f64, f64, f64), Box<dyn Error>> {\n    let display_ids = CGDisplay::active_displays()\n        .map_err(|err| CGError(format!(\"Failed to obtain displays, CGError code: {}\", err)))?;\n    let rects: Vec<CGRect> = display_ids\n        .iter()\n        .map(|id| CGDisplay::new(*id).bounds())\n        .collect();\n    let mut x0 = 0.0;\n    let mut x1 = 0.0;\n    let mut y0 = 0.0;\n    let mut y1 = 0.0;\n    for r in rects.iter() {\n        let r_x0 = r.origin.x;\n        let r_x1 = r_x0 + r.size.width;\n        let r_y0 = r.origin.y;\n        let r_y1 = r_y0 + r.size.height;\n        x0 = f64::min(x0, r_x0);\n        x1 = f64::max(x1, r_x1);\n        y0 = f64::min(y0, r_y0);\n        y1 = f64::max(y1, r_y1);\n    }\n    Ok((x0, y0, x1 - x0, y1 - y0))\n}\n\npub fn get_displays() -> Result<Vec<CGDisplayCapturable>, Box<dyn Error>> {\n    let display_ids = CGDisplay::active_displays()\n        .map_err(|err| CGError(format!(\"Failed to obtain displays, CGError code: {}\", err)))?;\n    Ok(display_ids\n        .iter()\n        .map(|id| CGDisplayCapturable::new(CGDisplay::new(*id)))\n        .collect())\n}\n\npub fn get_windows() -> Result<Vec<CGWindowCapturable>, Box<dyn Error>> {\n    let window_infos = get_window_infos();\n    let cursor_id = window_infos\n        .iter()\n        .find(|w| w.name == \"Cursor\")\n        .ok_or_else(|| CGError(\"No Cursor found!\".into()))?\n        .id;\n    Ok(window_infos\n        .iter()\n        .filter(|w| w.id != cursor_id)\n        .map(|w| CGWindowCapturable {\n            id: w.id,\n            name: w.name.clone(),\n            cursor_id,\n            bounds: w.bounds,\n            geometry_relative: (0.0, 0.0, 1.0, 1.0),\n            last_geometry_update: Instant::now() - Duration::from_secs(2),\n        })\n        .collect())\n}\n"
  },
  {
    "path": "src/capturable/mod.rs",
    "content": "use std::boxed::Box;\nuse std::error::Error;\nuse tracing::warn;\n\n#[cfg(target_os = \"macos\")]\npub mod core_graphics;\n#[cfg(target_os = \"linux\")]\npub mod pipewire;\n#[cfg(target_os = \"linux\")]\n#[allow(dead_code)]\npub mod remote_desktop_dbus;\npub mod testsrc;\n\n#[cfg(target_os = \"windows\")]\npub mod captrs_capture;\n#[cfg(target_os = \"windows\")]\npub mod win_ctx;\n#[cfg(target_os = \"linux\")]\npub mod x11;\npub trait Recorder {\n    fn capture(&mut self) -> Result<crate::video::PixelProvider<'_>, Box<dyn Error>>;\n}\n\npub trait BoxCloneCapturable {\n    fn box_clone(&self) -> Box<dyn Capturable>;\n}\n\nimpl<T> BoxCloneCapturable for T\nwhere\n    T: Clone + Capturable + 'static,\n{\n    fn box_clone(&self) -> Box<dyn Capturable> {\n        Box::new(self.clone())\n    }\n}\n/// Relative: x, y, width, height of the Capturable as floats relative to the absolute size of the\n/// screen. For example x=0.5, y=0.0, width=0.5, height=1.0 means the right half of the screen.\n/// VirtualScreen: offset_x, offset_y, width, height for a capturable using a virtual screen. (Windows)\npub enum Geometry {\n    Relative(f64, f64, f64, f64),\n    #[cfg(target_os = \"windows\")]\n    VirtualScreen(i32, i32, u32, u32, i32, i32),\n}\n\npub trait Capturable: Send + BoxCloneCapturable {\n    /// Name of the Capturable, for example the window title, if it is a window.\n    fn name(&self) -> String;\n\n    /// Return Geometry of the Capturable.\n    fn geometry(&self) -> Result<Geometry, Box<dyn Error>>;\n\n    /// Callback that is called right before input is simulated.\n    /// Useful to focus the window on input.\n    fn before_input(&mut self) -> Result<(), Box<dyn Error>>;\n\n    /// Return a Recorder that can record the current capturable.\n    fn recorder(&self, capture_cursor: bool) -> Result<Box<dyn Recorder>, Box<dyn Error>>;\n}\n\nimpl Clone for Box<dyn Capturable> {\n    fn clone(&self) -> Self {\n        self.box_clone()\n    }\n}\n\npub fn get_capturables(\n    #[cfg(target_os = \"linux\")] wayland_support: bool,\n    #[cfg(target_os = \"linux\")] capture_cursor: bool,\n) -> Vec<Box<dyn Capturable>> {\n    let mut capturables: Vec<Box<dyn Capturable>> = vec![];\n    #[cfg(target_os = \"linux\")]\n    {\n        if wayland_support {\n            use crate::capturable::pipewire::get_capturables as get_capturables_pw;\n            match get_capturables_pw(capture_cursor) {\n                Ok(captrs) => {\n                    for c in captrs {\n                        capturables.push(Box::new(c));\n                    }\n                }\n                Err(err) => warn!(\n                    \"Failed to get list of capturables via dbus/pipewire: {}\",\n                    err\n                ),\n            }\n        }\n\n        use crate::capturable::x11::X11Context;\n        let x11ctx = X11Context::new();\n        if let Some(mut x11ctx) = x11ctx {\n            match x11ctx.capturables() {\n                Ok(captrs) => {\n                    for c in captrs {\n                        capturables.push(Box::new(c));\n                    }\n                }\n                Err(err) => warn!(\"Failed to get list of capturables via X11: {}\", err),\n            }\n        };\n    }\n\n    #[cfg(target_os = \"macos\")]\n    {\n        use crate::capturable::core_graphics::get_displays as get_displays_cg;\n        use crate::capturable::core_graphics::get_windows as get_windows_cg;\n        match get_displays_cg() {\n            Ok(captrs) => {\n                for c in captrs {\n                    capturables.push(Box::new(c));\n                }\n            }\n            Err(err) => warn!(\"Failed to get list of displays via CoreGraphics: {}\", err),\n        }\n\n        match get_windows_cg() {\n            Ok(mut captrs) => {\n                captrs.sort_by(|a, b| a.name().to_lowercase().cmp(&b.name().to_lowercase()));\n                for c in captrs {\n                    capturables.push(Box::new(c));\n                }\n            }\n            Err(err) => warn!(\"Failed to get list of windows via CoreGraphics: {}\", err),\n        }\n    }\n\n    #[cfg(target_os = \"windows\")]\n    {\n        use crate::capturable::captrs_capture::CaptrsCapturable;\n        use crate::capturable::win_ctx::WinCtx;\n        let winctx = WinCtx::new();\n        for (i, o) in winctx.get_outputs().iter().enumerate() {\n            let captr = CaptrsCapturable::new(\n                i as u8,\n                String::from_utf16_lossy(o.DeviceName.as_ref()),\n                o.DesktopCoordinates,\n                winctx.get_union_rect().clone(),\n            );\n            capturables.push(Box::new(captr));\n        }\n    }\n\n    if crate::log::get_log_level() >= tracing::Level::DEBUG {\n        for (width, height) in [\n            (200, 200),\n            (800, 600),\n            (1080, 720),\n            (1920, 1080),\n            (3840, 2160),\n            (15360, 2160),\n        ]\n        .iter()\n        {\n            use testsrc::PixelFormat;\n            for pixel_format in [PixelFormat::BGR0, PixelFormat::RGB0, PixelFormat::RGB] {\n                capturables.push(Box::new(testsrc::TestCapturable {\n                    width: *width,\n                    height: *height,\n                    pixel_format,\n                }));\n            }\n        }\n    }\n\n    capturables\n}\n"
  },
  {
    "path": "src/capturable/pipewire.rs",
    "content": "use std::collections::HashMap;\nuse std::error::Error;\nuse std::os::unix::io::AsRawFd;\nuse std::sync::{Arc, Mutex};\nuse std::time::Duration;\nuse tracing::{debug, trace, warn};\n\nuse dbus::{\n    arg::{OwnedFd, PropMap, RefArg, Variant},\n    blocking::{Proxy, SyncConnection},\n    message::{MatchRule, MessageType},\n    Message,\n};\n\nuse gstreamer as gst;\nuse gstreamer::prelude::*;\nuse gstreamer_app::AppSink;\n\nuse crate::capturable::{Capturable, Geometry, Recorder};\nuse crate::video::PixelProvider;\n\nuse crate::capturable::remote_desktop_dbus::{\n    OrgFreedesktopPortalRemoteDesktop, OrgFreedesktopPortalRequestResponse,\n    OrgFreedesktopPortalScreenCast,\n};\n\n#[derive(Debug, Clone, Copy)]\nstruct PwStreamInfo {\n    path: u64,\n    source_type: u64,\n}\n\n#[derive(Debug)]\npub struct DBusError(String);\n\nimpl std::fmt::Display for DBusError {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        let Self(s) = self;\n        write!(f, \"{}\", s)\n    }\n}\n\nimpl Error for DBusError {}\n\n#[derive(Debug)]\npub struct GStreamerError(String);\n\nimpl std::fmt::Display for GStreamerError {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        let Self(s) = self;\n        write!(f, \"{}\", s)\n    }\n}\n\nimpl Error for GStreamerError {}\n\n#[derive(Clone)]\npub struct PipeWireCapturable {\n    // connection needs to be kept alive for recording\n    dbus_conn: Arc<SyncConnection>,\n    fd: OwnedFd,\n    path: u64,\n    source_type: u64,\n}\n\nimpl PipeWireCapturable {\n    fn new(conn: Arc<SyncConnection>, fd: OwnedFd, stream: PwStreamInfo) -> Self {\n        Self {\n            dbus_conn: conn,\n            fd,\n            path: stream.path,\n            source_type: stream.source_type,\n        }\n    }\n}\n\nimpl std::fmt::Debug for PipeWireCapturable {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(\n            f,\n            \"PipeWireCapturable {{dbus: {}, fd: {}, path: {}, source_type: {}}}\",\n            self.dbus_conn.unique_name(),\n            self.fd.as_raw_fd(),\n            self.path,\n            self.source_type\n        )\n    }\n}\n\nimpl Capturable for PipeWireCapturable {\n    fn name(&self) -> String {\n        let type_str = match self.source_type {\n            1 => \"Desktop\",\n            2 => \"Window\",\n            _ => \"Unknown\",\n        };\n        format!(\"Pipewire {}, path: {}\", type_str, self.path)\n    }\n\n    fn geometry(&self) -> Result<Geometry, Box<dyn Error>> {\n        Ok(Geometry::Relative(0.0, 0.0, 1.0, 1.0))\n    }\n\n    fn before_input(&mut self) -> Result<(), Box<dyn Error>> {\n        Ok(())\n    }\n\n    fn recorder(&self, _capture_cursor: bool) -> Result<Box<dyn Recorder>, Box<dyn Error>> {\n        Ok(Box::new(PipeWireRecorder::new(self.clone())?))\n    }\n}\n\npub struct PipeWireRecorder {\n    buffer: Option<gst::MappedBuffer<gst::buffer::Readable>>,\n    buffer_cropped: Vec<u8>,\n    pix_fmt: String,\n    is_cropped: bool,\n    pipeline: gst::Pipeline,\n    appsink: AppSink,\n    width: usize,\n    height: usize,\n}\n\nimpl PipeWireRecorder {\n    pub fn new(capturable: PipeWireCapturable) -> Result<Self, Box<dyn Error>> {\n        let pipeline = gst::Pipeline::new();\n\n        let src = gst::ElementFactory::make(\"pipewiresrc\").build()?;\n        src.set_property(\"fd\", &capturable.fd.as_raw_fd());\n        src.set_property(\"path\", &format!(\"{}\", capturable.path));\n\n        // For some reason pipewire blocks on destruction of AppSink if this is not set to true,\n        // see: https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/982\n        src.set_property(\"always-copy\", &true);\n\n        let sink = gst::ElementFactory::make(\"appsink\").build()?;\n        sink.set_property(\"drop\", &true);\n        sink.set_property(\"max-buffers\", &1u32);\n\n        pipeline.add_many(&[&src, &sink])?;\n        src.link(&sink)?;\n        let appsink = sink\n            .dynamic_cast::<AppSink>()\n            .map_err(|_| GStreamerError(\"Sink element is expected to be an appsink!\".into()))?;\n        let mut caps = gst::Caps::new_empty();\n        caps.merge_structure(gst::structure::Structure::from_iter(\n            \"video/x-raw\",\n            [(\"format\", \"BGRx\".into())],\n        ));\n        caps.merge_structure(gst::structure::Structure::from_iter(\n            \"video/x-raw\",\n            [(\"format\", \"RGBx\".into())],\n        ));\n        appsink.set_caps(Some(&caps));\n\n        pipeline.set_state(gst::State::Playing)?;\n        Ok(Self {\n            pipeline,\n            appsink,\n            buffer: None,\n            pix_fmt: \"\".into(),\n            width: 0,\n            height: 0,\n            buffer_cropped: vec![],\n            is_cropped: false,\n        })\n    }\n}\n\nimpl Recorder for PipeWireRecorder {\n    fn capture(&mut self) -> Result<PixelProvider<'_>, Box<dyn Error>> {\n        if let Some(sample) = self\n            .appsink\n            .try_pull_sample(gst::ClockTime::from_mseconds(16))\n        {\n            let cap = sample.caps().unwrap().structure(0).unwrap();\n            let w: i32 = cap.value(\"width\")?.get()?;\n            let h: i32 = cap.value(\"height\")?.get()?;\n            self.pix_fmt = cap.value(\"format\")?.get()?;\n            let w = w as usize;\n            let h = h as usize;\n            let buf = sample\n                .buffer_owned()\n                .ok_or_else(|| GStreamerError(\"Failed to get owned buffer.\".into()))?;\n            let mut crop = buf\n                .meta::<gstreamer_video::VideoCropMeta>()\n                .map(|m| m.rect());\n            // only crop if necessary\n            if Some((0, 0, w as u32, h as u32)) == crop {\n                crop = None;\n            }\n            let buf = buf\n                .into_mapped_buffer_readable()\n                .map_err(|_| GStreamerError(\"Failed to map buffer.\".into()))?;\n            let buf_size = buf.size();\n            // BGRx is 4 bytes per pixel\n            if buf_size != (w * h * 4) {\n                // for some reason the width and height of the caps do not guarantee correct buffer\n                // size, so ignore those buffers, see:\n                // https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/985\n                trace!(\n                    \"Size of mapped buffer: {} does NOT match size of capturable {}x{}@BGRx, \\\n                    dropping it!\",\n                    buf_size,\n                    w,\n                    h\n                );\n            } else {\n                // Copy region specified by crop into self.buffer_cropped\n                // TODO: Figure out if ffmpeg provides a zero copy alternative\n                if let Some((x_off, y_off, w_crop, h_crop)) = crop {\n                    let x_off = x_off as usize;\n                    let y_off = y_off as usize;\n                    let w_crop = w_crop as usize;\n                    let h_crop = h_crop as usize;\n                    self.buffer_cropped.clear();\n                    let data = buf.as_slice();\n                    // BGRx is 4 bytes per pixel\n                    self.buffer_cropped.reserve(w_crop * h_crop * 4);\n                    for y in y_off..(y_off + h_crop) {\n                        let i = 4 * (w * y + x_off);\n                        self.buffer_cropped.extend(&data[i..i + 4 * w_crop]);\n                    }\n                    self.width = w_crop;\n                    self.height = h_crop;\n                } else {\n                    self.width = w;\n                    self.height = h;\n                }\n                self.is_cropped = crop.is_some();\n                self.buffer = Some(buf);\n            }\n        } else {\n            trace!(\"No new buffer available, falling back to previous one.\");\n        }\n        if self.buffer.is_none() {\n            return Err(Box::new(GStreamerError(\"No buffer available!\".into())));\n        }\n        let buf = if self.is_cropped {\n            self.buffer_cropped.as_slice()\n        } else {\n            self.buffer.as_ref().unwrap().as_slice()\n        };\n        match self.pix_fmt.as_str() {\n            \"BGRx\" => Ok(PixelProvider::BGR0(self.width, self.height, buf)),\n            \"RGBx\" => Ok(PixelProvider::RGB0(self.width, self.height, buf)),\n            _ => unreachable!(),\n        }\n    }\n}\n\nimpl Drop for PipeWireRecorder {\n    fn drop(&mut self) {\n        if let Err(err) = self.pipeline.set_state(gst::State::Null) {\n            warn!(\"Failed to stop GStreamer pipeline: {}.\", err);\n        }\n    }\n}\n\nfn handle_response<F>(\n    portal: Proxy<&SyncConnection>,\n    path: dbus::Path<'static>,\n    context: Arc<Mutex<CallBackContext>>,\n    mut f: F,\n) -> Result<dbus::channel::Token, dbus::Error>\nwhere\n    F: FnMut(\n            OrgFreedesktopPortalRequestResponse,\n            Proxy<&SyncConnection>,\n            &Message,\n            Arc<Mutex<CallBackContext>>,\n        ) -> Result<(), Box<dyn Error>>\n        + Send\n        + Sync\n        + 'static,\n{\n    let mut m = MatchRule::new();\n    m.path = Some(path);\n    m.msg_type = Some(MessageType::Signal);\n    m.sender = Some(\"org.freedesktop.portal.Desktop\".into());\n    m.interface = Some(\"org.freedesktop.portal.Request\".into());\n    portal\n        .connection\n        .add_match(m, move |r: OrgFreedesktopPortalRequestResponse, c, m| {\n            let portal = get_portal(c);\n            debug!(\"Response from DBus: response: {:?}, message: {:?}\", r, m);\n            match r.response {\n                0 => {}\n                1 => {\n                    context.lock().unwrap().failure = true;\n                    warn!(\"DBus response: User cancelled interaction.\");\n                    return true;\n                }\n                c => {\n                    context.lock().unwrap().failure = true;\n                    warn!(\"DBus response: Unknown error, code: {}.\", c);\n                    return true;\n                }\n            }\n            if let Err(err) = f(r, portal, m, context.clone()) {\n                context.lock().unwrap().failure = true;\n                warn!(\"Error requesting screen capture via dbus: {}\", err);\n            }\n            true\n        })\n}\n\nfn get_portal(conn: &SyncConnection) -> Proxy<'_, &SyncConnection> {\n    conn.with_proxy(\n        \"org.freedesktop.portal.Desktop\",\n        \"/org/freedesktop/portal/desktop\",\n        Duration::from_millis(1000),\n    )\n}\n\nfn streams_from_response(response: &OrgFreedesktopPortalRequestResponse) -> Vec<PwStreamInfo> {\n    (move || {\n        Some(\n            response\n                .results\n                .get(\"streams\")?\n                .as_iter()?\n                .next()?\n                .as_iter()?\n                .filter_map(|stream| {\n                    let mut itr = stream.as_iter()?;\n                    let path = itr.next()?.as_u64()?;\n                    let (keys, values): (Vec<(usize, &dyn RefArg)>, Vec<(usize, &dyn RefArg)>) =\n                        itr.next()?\n                            .as_iter()?\n                            .enumerate()\n                            .partition(|(i, _)| i % 2 == 0);\n                    let attributes = keys\n                        .iter()\n                        .filter_map(|(_, key)| Some(key.as_str()?.to_owned()))\n                        .zip(\n                            values\n                                .iter()\n                                .map(|(_, arg)| *arg)\n                                .collect::<Vec<&dyn RefArg>>(),\n                        )\n                        .collect::<HashMap<String, &dyn RefArg>>();\n                    Some(PwStreamInfo {\n                        path,\n                        source_type: attributes\n                            .get(\"source_type\")\n                            .map_or(Some(0), |v| v.as_u64())?,\n                    })\n                })\n                .collect::<Vec<PwStreamInfo>>(),\n        )\n    })()\n    .unwrap_or_default()\n}\n\n// mostly inspired by https://gitlab.gnome.org/snippets/19 and\n// https://gitlab.gnome.org/-/snippets/39\nstruct CallBackContext {\n    capture_cursor: bool,\n    session: dbus::Path<'static>,\n    streams: Vec<PwStreamInfo>,\n    fd: Option<OwnedFd>,\n    restore_token: Option<String>,\n    has_remote_desktop: bool,\n    failure: bool,\n}\n\nfn on_create_session_response(\n    r: OrgFreedesktopPortalRequestResponse,\n    portal: Proxy<&SyncConnection>,\n    _msg: &Message,\n    context: Arc<Mutex<CallBackContext>>,\n) -> Result<(), Box<dyn Error>> {\n    debug!(\"on_create_session_response\");\n    let session: dbus::Path = r\n        .results\n        .get(\"session_handle\")\n        .ok_or_else(|| {\n            DBusError(format!(\n                \"Failed to obtain session_handle from response: {:?}\",\n                r\n            ))\n        })?\n        .as_str()\n        .ok_or_else(|| DBusError(\"Failed to convert session_handle to string.\".into()))?\n        .to_string()\n        .into();\n\n    context.lock().unwrap().session = session.clone();\n    if context.lock().unwrap().has_remote_desktop {\n        select_devices(portal, context)\n    } else {\n        select_sources(portal, context)\n    }\n}\n\nfn select_devices(\n    portal: Proxy<&SyncConnection>,\n    context: Arc<Mutex<CallBackContext>>,\n) -> Result<(), Box<dyn Error>> {\n    let mut args: PropMap = HashMap::new();\n    let t: usize = rand::random();\n    args.insert(\n        \"handle_token\".to_string(),\n        Variant(Box::new(format!(\"weylus{t}\"))),\n    );\n\n    // TODO\n    //args.insert(\n    //    \"restore_token\".to_string(),\n    //    Variant(Box::new(format!(\"weylus{t}\"))),\n    //);\n\n    // persist modes:\n    // 0: Do not persist (default)\n    // 1: Permissions persist as long as the application is running\n    // 2: Permissions persist until explicitly revoked\n    args.insert(\"persist_mode\".to_string(), Variant(Box::new(2 as u32)));\n\n    // device types\n    // 1: KEYBOARD\n    // 2: POINTER\n    // 4: TOUCHSCREEN\n    let device_types = portal.available_device_types()?;\n    debug!(\"Available device types: {device_types}.\");\n    args.insert(\"types\".to_string(), Variant(Box::new(device_types)));\n\n    let path = portal.select_devices(context.lock().unwrap().session.clone(), args)?;\n    handle_response(portal, path, context, |_, portal, _, context| {\n        select_sources(portal, context)\n    })?;\n    Ok(())\n}\n\nfn select_sources(\n    portal: Proxy<&SyncConnection>,\n    context: Arc<Mutex<CallBackContext>>,\n) -> Result<(), Box<dyn Error>> {\n    debug!(\"select_sources\");\n    let mut args: PropMap = HashMap::new();\n\n    let t: usize = rand::random();\n    args.insert(\n        \"handle_token\".to_string(),\n        Variant(Box::new(format!(\"weylus{t}\"))),\n    );\n    // https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.ScreenCast.html#org-freedesktop-portal-screencast-selectsources\n    // allow multiple sources\n    args.insert(\"multiple\".into(), Variant(Box::new(true)));\n\n    // 1: MONITOR\n    // 2: WINDOW\n    // 4: VIRTUAL\n    let source_types = portal.available_source_types()?;\n    debug!(\"Available source types: {source_types}.\");\n    args.insert(\"types\".into(), Variant(Box::new(source_types)));\n\n    let capture_cursor = context.lock().unwrap().capture_cursor;\n    // 1: Hidden. The cursor is not part of the screen cast stream.\n    // 2: Embedded: The cursor is embedded as part of the stream buffers.\n    // 4: Metadata: The cursor is not part of the screen cast stream, but sent as PipeWire stream metadata.\n    let cursor_mode = if capture_cursor { 2u32 } else { 1u32 };\n\n    let is_plasma = std::env::var(\"DESKTOP_SESSION\").map_or(false, |s| s.contains(\"plasma\"));\n    if is_plasma && capture_cursor {\n        // Warn the user if capturing the cursor is tried on kde as this can crash\n        // kwin_wayland and tear down the plasma desktop, see:\n        // https://bugs.kde.org/show_bug.cgi?id=435042\n        warn!(\n            \"You are attempting to capture the cursor under KDE Plasma, this may crash your \\\n                    desktop, see https://bugs.kde.org/show_bug.cgi?id=435042 for details! \\\n                    You have been warned.\"\n        );\n    }\n    args.insert(\"cursor_mode\".into(), Variant(Box::new(cursor_mode)));\n\n    let path = portal.select_sources(context.lock().unwrap().session.clone(), args)?;\n    handle_response(portal, path, context, on_select_sources_response)?;\n    Ok(())\n}\n\nfn on_select_sources_response(\n    _r: OrgFreedesktopPortalRequestResponse,\n    portal: Proxy<&SyncConnection>,\n    _msg: &Message,\n    context: Arc<Mutex<CallBackContext>>,\n) -> Result<(), Box<dyn Error>> {\n    debug!(\"on_select_sources_response\");\n    let mut args: PropMap = HashMap::new();\n    let t: usize = rand::random();\n    args.insert(\n        \"handle_token\".to_string(),\n        Variant(Box::new(format!(\"weylus{t}\"))),\n    );\n    let path = if context.lock().unwrap().has_remote_desktop {\n        OrgFreedesktopPortalRemoteDesktop::start(\n            &portal,\n            context.lock().unwrap().session.clone(),\n            \"\",\n            args,\n        )?\n    } else {\n        OrgFreedesktopPortalScreenCast::start(\n            &portal,\n            context.lock().unwrap().session.clone(),\n            \"\",\n            args,\n        )?\n    };\n    handle_response(portal, path, context, on_start_response)?;\n    Ok(())\n}\n\nfn on_start_response(\n    r: OrgFreedesktopPortalRequestResponse,\n    portal: Proxy<&SyncConnection>,\n    _msg: &Message,\n    context: Arc<Mutex<CallBackContext>>,\n) -> Result<(), Box<dyn Error>> {\n    debug!(\"on_start_response\");\n    let mut context = context.lock().unwrap();\n    context.streams.append(&mut streams_from_response(&r));\n    let session = context.session.clone();\n    context\n        .fd\n        .replace(portal.open_pipe_wire_remote(session.clone(), HashMap::new())?);\n    if let Some(Some(t)) = r.results.get(\"restore_token\").map(|t| t.as_str()) {\n        context.restore_token = Some(t.to_string());\n    }\n    dbg!(&context.restore_token);\n    if context.has_remote_desktop {\n        debug!(\"Remote Desktop Session started\");\n    } else {\n        debug!(\"Screen Cast Session started\");\n    }\n    Ok(())\n}\n\nfn request_remote_desktop(\n    capture_cursor: bool,\n) -> Result<(SyncConnection, OwnedFd, Vec<PwStreamInfo>), Box<dyn Error>> {\n    let conn = SyncConnection::new_session()?;\n    let portal = get_portal(&conn);\n\n    // Disabled for KDE plasma due to https://bugs.kde.org/show_bug.cgi?id=484996\n    // List of supported DEs: https://wiki.archlinux.org/title/XDG_Desktop_Portal#List_of_backends_and_interfaces\n    let has_remote_desktop =\n        std::env::var(\"DESKTOP_SESSION\").map_or(false, |s| s.contains(\"gnome\"));\n\n    let context = CallBackContext {\n        capture_cursor,\n        session: Default::default(),\n        streams: Default::default(),\n        fd: None,\n        restore_token: None,\n        has_remote_desktop,\n        failure: false,\n    };\n    let context = Arc::new(Mutex::new(context));\n\n    let mut args: PropMap = HashMap::new();\n    let t1: usize = rand::random();\n    let t2: usize = rand::random();\n    args.insert(\n        \"session_handle_token\".to_string(),\n        Variant(Box::new(format!(\"weylus{t1}\"))),\n    );\n    args.insert(\n        \"handle_token\".to_string(),\n        Variant(Box::new(format!(\"weylus{t2}\"))),\n    );\n    let path = if has_remote_desktop {\n        OrgFreedesktopPortalRemoteDesktop::create_session(&portal, args)?\n    } else {\n        OrgFreedesktopPortalScreenCast::create_session(&portal, args)?\n    };\n    handle_response(portal, path, context.clone(), on_create_session_response)?;\n\n    // wait 3 minutes for user interaction\n    for _ in 0..1800 {\n        conn.process(Duration::from_millis(100))?;\n        let context = context.lock().unwrap();\n        // Once we got a file descriptor we are done!\n        if context.fd.is_some() {\n            break;\n        }\n\n        if context.failure {\n            break;\n        }\n    }\n    let context = context.lock().unwrap();\n    if context.fd.is_some() && !context.streams.is_empty() {\n        Ok((conn, context.fd.clone().unwrap(), context.streams.clone()))\n    } else {\n        Err(Box::new(DBusError(\n            \"Failed to obtain screen capture.\".into(),\n        )))\n    }\n}\n\npub fn get_capturables(capture_cursor: bool) -> Result<Vec<PipeWireCapturable>, Box<dyn Error>> {\n    let (conn, fd, streams) = request_remote_desktop(capture_cursor)?;\n    let conn = Arc::new(conn);\n    Ok(streams\n        .into_iter()\n        .map(|s| PipeWireCapturable::new(conn.clone(), fd.clone(), s))\n        .collect())\n}\n"
  },
  {
    "path": "src/capturable/remote_desktop_dbus.rs",
    "content": "// This code was autogenerated with `dbus-codegen-rust -c blocking -m None`, see https://github.com/diwic/dbus-rs\n// XML source:\n// https://github.com/flatpak/xdg-desktop-portal/raw/refs/tags/1.18.4/data/org.freedesktop.portal.Request.xml\n// https://github.com/flatpak/xdg-desktop-portal/raw/refs/tags/1.18.4/data/org.freedesktop.portal.ScreenCast.xml\n// https://github.com/flatpak/xdg-desktop-portal/raw/refs/tags/1.18.4/data/org.freedesktop.portal.RemoteDesktop.xml\nuse dbus;\n#[allow(unused_imports)]\nuse dbus::arg;\nuse dbus::blocking;\n\npub trait OrgFreedesktopPortalRequest {\n    fn close(&self) -> Result<(), dbus::Error>;\n}\n\n#[derive(Debug)]\npub struct OrgFreedesktopPortalRequestResponse {\n    pub response: u32,\n    pub results: arg::PropMap,\n}\n\nimpl arg::AppendAll for OrgFreedesktopPortalRequestResponse {\n    fn append(&self, i: &mut arg::IterAppend) {\n        arg::RefArg::append(&self.response, i);\n        arg::RefArg::append(&self.results, i);\n    }\n}\n\nimpl arg::ReadAll for OrgFreedesktopPortalRequestResponse {\n    fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {\n        Ok(OrgFreedesktopPortalRequestResponse {\n            response: i.read()?,\n            results: i.read()?,\n        })\n    }\n}\n\nimpl dbus::message::SignalArgs for OrgFreedesktopPortalRequestResponse {\n    const NAME: &'static str = \"Response\";\n    const INTERFACE: &'static str = \"org.freedesktop.portal.Request\";\n}\n\nimpl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref<Target = T>> OrgFreedesktopPortalRequest\n    for blocking::Proxy<'a, C>\n{\n    fn close(&self) -> Result<(), dbus::Error> {\n        self.method_call(\"org.freedesktop.portal.Request\", \"Close\", ())\n    }\n}\n\npub trait OrgFreedesktopPortalScreenCast {\n    fn create_session(&self, options: arg::PropMap) -> Result<dbus::Path<'static>, dbus::Error>;\n    fn select_sources(\n        &self,\n        session_handle: dbus::Path,\n        options: arg::PropMap,\n    ) -> Result<dbus::Path<'static>, dbus::Error>;\n    fn start(\n        &self,\n        session_handle: dbus::Path,\n        parent_window: &str,\n        options: arg::PropMap,\n    ) -> Result<dbus::Path<'static>, dbus::Error>;\n    fn open_pipe_wire_remote(\n        &self,\n        session_handle: dbus::Path,\n        options: arg::PropMap,\n    ) -> Result<arg::OwnedFd, dbus::Error>;\n    fn available_source_types(&self) -> Result<u32, dbus::Error>;\n    fn available_cursor_modes(&self) -> Result<u32, dbus::Error>;\n    fn version(&self) -> Result<u32, dbus::Error>;\n}\n\nimpl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref<Target = T>>\n    OrgFreedesktopPortalScreenCast for blocking::Proxy<'a, C>\n{\n    fn create_session(&self, options: arg::PropMap) -> Result<dbus::Path<'static>, dbus::Error> {\n        self.method_call(\n            \"org.freedesktop.portal.ScreenCast\",\n            \"CreateSession\",\n            (options,),\n        )\n        .and_then(|r: (dbus::Path<'static>,)| Ok(r.0))\n    }\n\n    fn select_sources(\n        &self,\n        session_handle: dbus::Path,\n        options: arg::PropMap,\n    ) -> Result<dbus::Path<'static>, dbus::Error> {\n        self.method_call(\n            \"org.freedesktop.portal.ScreenCast\",\n            \"SelectSources\",\n            (session_handle, options),\n        )\n        .and_then(|r: (dbus::Path<'static>,)| Ok(r.0))\n    }\n\n    fn start(\n        &self,\n        session_handle: dbus::Path,\n        parent_window: &str,\n        options: arg::PropMap,\n    ) -> Result<dbus::Path<'static>, dbus::Error> {\n        self.method_call(\n            \"org.freedesktop.portal.ScreenCast\",\n            \"Start\",\n            (session_handle, parent_window, options),\n        )\n        .and_then(|r: (dbus::Path<'static>,)| Ok(r.0))\n    }\n\n    fn open_pipe_wire_remote(\n        &self,\n        session_handle: dbus::Path,\n        options: arg::PropMap,\n    ) -> Result<arg::OwnedFd, dbus::Error> {\n        self.method_call(\n            \"org.freedesktop.portal.ScreenCast\",\n            \"OpenPipeWireRemote\",\n            (session_handle, options),\n        )\n        .and_then(|r: (arg::OwnedFd,)| Ok(r.0))\n    }\n\n    fn available_source_types(&self) -> Result<u32, dbus::Error> {\n        <Self as blocking::stdintf::org_freedesktop_dbus::Properties>::get(\n            self,\n            \"org.freedesktop.portal.ScreenCast\",\n            \"AvailableSourceTypes\",\n        )\n    }\n\n    fn available_cursor_modes(&self) -> Result<u32, dbus::Error> {\n        <Self as blocking::stdintf::org_freedesktop_dbus::Properties>::get(\n            self,\n            \"org.freedesktop.portal.ScreenCast\",\n            \"AvailableCursorModes\",\n        )\n    }\n\n    fn version(&self) -> Result<u32, dbus::Error> {\n        <Self as blocking::stdintf::org_freedesktop_dbus::Properties>::get(\n            self,\n            \"org.freedesktop.portal.ScreenCast\",\n            \"version\",\n        )\n    }\n}\n\npub trait OrgFreedesktopPortalRemoteDesktop {\n    fn create_session(&self, options: arg::PropMap) -> Result<dbus::Path<'static>, dbus::Error>;\n    fn select_devices(\n        &self,\n        session_handle: dbus::Path,\n        options: arg::PropMap,\n    ) -> Result<dbus::Path<'static>, dbus::Error>;\n    fn start(\n        &self,\n        session_handle: dbus::Path,\n        parent_window: &str,\n        options: arg::PropMap,\n    ) -> Result<dbus::Path<'static>, dbus::Error>;\n    fn notify_pointer_motion(\n        &self,\n        session_handle: dbus::Path,\n        options: arg::PropMap,\n        dx: f64,\n        dy: f64,\n    ) -> Result<(), dbus::Error>;\n    fn notify_pointer_motion_absolute(\n        &self,\n        session_handle: dbus::Path,\n        options: arg::PropMap,\n        stream: u32,\n        x_: f64,\n        y_: f64,\n    ) -> Result<(), dbus::Error>;\n    fn notify_pointer_button(\n        &self,\n        session_handle: dbus::Path,\n        options: arg::PropMap,\n        button: i32,\n        state: u32,\n    ) -> Result<(), dbus::Error>;\n    fn notify_pointer_axis(\n        &self,\n        session_handle: dbus::Path,\n        options: arg::PropMap,\n        dx: f64,\n        dy: f64,\n    ) -> Result<(), dbus::Error>;\n    fn notify_pointer_axis_discrete(\n        &self,\n        session_handle: dbus::Path,\n        options: arg::PropMap,\n        axis: u32,\n        steps: i32,\n    ) -> Result<(), dbus::Error>;\n    fn notify_keyboard_keycode(\n        &self,\n        session_handle: dbus::Path,\n        options: arg::PropMap,\n        keycode: i32,\n        state: u32,\n    ) -> Result<(), dbus::Error>;\n    fn notify_keyboard_keysym(\n        &self,\n        session_handle: dbus::Path,\n        options: arg::PropMap,\n        keysym: i32,\n        state: u32,\n    ) -> Result<(), dbus::Error>;\n    fn notify_touch_down(\n        &self,\n        session_handle: dbus::Path,\n        options: arg::PropMap,\n        stream: u32,\n        slot: u32,\n        x_: f64,\n        y_: f64,\n    ) -> Result<(), dbus::Error>;\n    fn notify_touch_motion(\n        &self,\n        session_handle: dbus::Path,\n        options: arg::PropMap,\n        stream: u32,\n        slot: u32,\n        x_: f64,\n        y_: f64,\n    ) -> Result<(), dbus::Error>;\n    fn notify_touch_up(\n        &self,\n        session_handle: dbus::Path,\n        options: arg::PropMap,\n        slot: u32,\n    ) -> Result<(), dbus::Error>;\n    fn connect_to_eis(\n        &self,\n        session_handle: dbus::Path,\n        options: arg::PropMap,\n    ) -> Result<arg::OwnedFd, dbus::Error>;\n    fn available_device_types(&self) -> Result<u32, dbus::Error>;\n    fn version(&self) -> Result<u32, dbus::Error>;\n}\n\nimpl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref<Target = T>>\n    OrgFreedesktopPortalRemoteDesktop for blocking::Proxy<'a, C>\n{\n    fn create_session(&self, options: arg::PropMap) -> Result<dbus::Path<'static>, dbus::Error> {\n        self.method_call(\n            \"org.freedesktop.portal.RemoteDesktop\",\n            \"CreateSession\",\n            (options,),\n        )\n        .and_then(|r: (dbus::Path<'static>,)| Ok(r.0))\n    }\n\n    fn select_devices(\n        &self,\n        session_handle: dbus::Path,\n        options: arg::PropMap,\n    ) -> Result<dbus::Path<'static>, dbus::Error> {\n        self.method_call(\n            \"org.freedesktop.portal.RemoteDesktop\",\n            \"SelectDevices\",\n            (session_handle, options),\n        )\n        .and_then(|r: (dbus::Path<'static>,)| Ok(r.0))\n    }\n\n    fn start(\n        &self,\n        session_handle: dbus::Path,\n        parent_window: &str,\n        options: arg::PropMap,\n    ) -> Result<dbus::Path<'static>, dbus::Error> {\n        self.method_call(\n            \"org.freedesktop.portal.RemoteDesktop\",\n            \"Start\",\n            (session_handle, parent_window, options),\n        )\n        .and_then(|r: (dbus::Path<'static>,)| Ok(r.0))\n    }\n\n    fn notify_pointer_motion(\n        &self,\n        session_handle: dbus::Path,\n        options: arg::PropMap,\n        dx: f64,\n        dy: f64,\n    ) -> Result<(), dbus::Error> {\n        self.method_call(\n            \"org.freedesktop.portal.RemoteDesktop\",\n            \"NotifyPointerMotion\",\n            (session_handle, options, dx, dy),\n        )\n    }\n\n    fn notify_pointer_motion_absolute(\n        &self,\n        session_handle: dbus::Path,\n        options: arg::PropMap,\n        stream: u32,\n        x_: f64,\n        y_: f64,\n    ) -> Result<(), dbus::Error> {\n        self.method_call(\n            \"org.freedesktop.portal.RemoteDesktop\",\n            \"NotifyPointerMotionAbsolute\",\n            (session_handle, options, stream, x_, y_),\n        )\n    }\n\n    fn notify_pointer_button(\n        &self,\n        session_handle: dbus::Path,\n        options: arg::PropMap,\n        button: i32,\n        state: u32,\n    ) -> Result<(), dbus::Error> {\n        self.method_call(\n            \"org.freedesktop.portal.RemoteDesktop\",\n            \"NotifyPointerButton\",\n            (session_handle, options, button, state),\n        )\n    }\n\n    fn notify_pointer_axis(\n        &self,\n        session_handle: dbus::Path,\n        options: arg::PropMap,\n        dx: f64,\n        dy: f64,\n    ) -> Result<(), dbus::Error> {\n        self.method_call(\n            \"org.freedesktop.portal.RemoteDesktop\",\n            \"NotifyPointerAxis\",\n            (session_handle, options, dx, dy),\n        )\n    }\n\n    fn notify_pointer_axis_discrete(\n        &self,\n        session_handle: dbus::Path,\n        options: arg::PropMap,\n        axis: u32,\n        steps: i32,\n    ) -> Result<(), dbus::Error> {\n        self.method_call(\n            \"org.freedesktop.portal.RemoteDesktop\",\n            \"NotifyPointerAxisDiscrete\",\n            (session_handle, options, axis, steps),\n        )\n    }\n\n    fn notify_keyboard_keycode(\n        &self,\n        session_handle: dbus::Path,\n        options: arg::PropMap,\n        keycode: i32,\n        state: u32,\n    ) -> Result<(), dbus::Error> {\n        self.method_call(\n            \"org.freedesktop.portal.RemoteDesktop\",\n            \"NotifyKeyboardKeycode\",\n            (session_handle, options, keycode, state),\n        )\n    }\n\n    fn notify_keyboard_keysym(\n        &self,\n        session_handle: dbus::Path,\n        options: arg::PropMap,\n        keysym: i32,\n        state: u32,\n    ) -> Result<(), dbus::Error> {\n        self.method_call(\n            \"org.freedesktop.portal.RemoteDesktop\",\n            \"NotifyKeyboardKeysym\",\n            (session_handle, options, keysym, state),\n        )\n    }\n\n    fn notify_touch_down(\n        &self,\n        session_handle: dbus::Path,\n        options: arg::PropMap,\n        stream: u32,\n        slot: u32,\n        x_: f64,\n        y_: f64,\n    ) -> Result<(), dbus::Error> {\n        self.method_call(\n            \"org.freedesktop.portal.RemoteDesktop\",\n            \"NotifyTouchDown\",\n            (session_handle, options, stream, slot, x_, y_),\n        )\n    }\n\n    fn notify_touch_motion(\n        &self,\n        session_handle: dbus::Path,\n        options: arg::PropMap,\n        stream: u32,\n        slot: u32,\n        x_: f64,\n        y_: f64,\n    ) -> Result<(), dbus::Error> {\n        self.method_call(\n            \"org.freedesktop.portal.RemoteDesktop\",\n            \"NotifyTouchMotion\",\n            (session_handle, options, stream, slot, x_, y_),\n        )\n    }\n\n    fn notify_touch_up(\n        &self,\n        session_handle: dbus::Path,\n        options: arg::PropMap,\n        slot: u32,\n    ) -> Result<(), dbus::Error> {\n        self.method_call(\n            \"org.freedesktop.portal.RemoteDesktop\",\n            \"NotifyTouchUp\",\n            (session_handle, options, slot),\n        )\n    }\n\n    fn connect_to_eis(\n        &self,\n        session_handle: dbus::Path,\n        options: arg::PropMap,\n    ) -> Result<arg::OwnedFd, dbus::Error> {\n        self.method_call(\n            \"org.freedesktop.portal.RemoteDesktop\",\n            \"ConnectToEIS\",\n            (session_handle, options),\n        )\n        .and_then(|r: (arg::OwnedFd,)| Ok(r.0))\n    }\n\n    fn available_device_types(&self) -> Result<u32, dbus::Error> {\n        <Self as blocking::stdintf::org_freedesktop_dbus::Properties>::get(\n            self,\n            \"org.freedesktop.portal.RemoteDesktop\",\n            \"AvailableDeviceTypes\",\n        )\n    }\n\n    fn version(&self) -> Result<u32, dbus::Error> {\n        <Self as blocking::stdintf::org_freedesktop_dbus::Properties>::get(\n            self,\n            \"org.freedesktop.portal.RemoteDesktop\",\n            \"version\",\n        )\n    }\n}\n"
  },
  {
    "path": "src/capturable/testsrc.rs",
    "content": "use crate::capturable::{Capturable, Geometry, Recorder};\nuse crate::video::PixelProvider;\nuse std::error::Error;\n\n#[derive(Debug, Clone, Copy)]\npub enum PixelFormat {\n    BGR0,\n    RGB0,\n    RGB,\n}\n\n#[derive(Debug, Clone, Copy)]\npub struct TestCapturable {\n    pub width: usize,\n    pub height: usize,\n    pub pixel_format: PixelFormat,\n}\n\nimpl TestCapturable {\n    fn pixel_size(&self) -> usize {\n        match self.pixel_format {\n            PixelFormat::BGR0 => 4,\n            PixelFormat::RGB0 => 4,\n            PixelFormat::RGB => 3,\n        }\n    }\n    fn set_default_pixel(&self, buf: &mut [u8], x: usize, y: usize) {\n        let w = self.width;\n        let i = x * 8 / w;\n        let pos = (x + y * w) * self.pixel_size();\n        let (pos_r, pos_g, pos_b) = match self.pixel_format {\n            PixelFormat::BGR0 => (pos + 2, pos + 1, pos),\n            PixelFormat::RGB0 | PixelFormat::RGB => (pos, pos + 1, pos + 2),\n        };\n        buf[pos_b] = if i & 1 != 0 { 255 } else { 0 };\n        buf[pos_g] = if i & 2 != 0 { 255 } else { 0 };\n        buf[pos_r] = if i & 4 != 0 { 255 } else { 0 };\n    }\n}\n\npub struct TestRecorder {\n    capturable: TestCapturable,\n    buf: Vec<u8>,\n    i: usize,\n}\n\nimpl TestRecorder {\n    fn new(capturable: TestCapturable) -> Self {\n        let mut buf = vec![0; capturable.width * capturable.height * capturable.pixel_size()];\n        let buf_ref = buf.as_mut();\n        for y in 0..capturable.height {\n            for x in 0..capturable.width {\n                capturable.set_default_pixel(buf_ref, x, y);\n            }\n        }\n        Self {\n            capturable,\n            buf,\n            i: 0,\n        }\n    }\n}\n\nimpl Capturable for TestCapturable {\n    fn name(&self) -> String {\n        format!(\n            \"Test Source {}x{}@{:?}\",\n            self.width, self.height, self.pixel_format\n        )\n    }\n    fn geometry(&self) -> Result<Geometry, Box<dyn Error>> {\n        Ok(Geometry::Relative(0.0, 0.0, 1.0, 1.0))\n    }\n    fn before_input(&mut self) -> Result<(), Box<dyn Error>> {\n        Ok(())\n    }\n    fn recorder(&self, _: bool) -> Result<Box<dyn Recorder>, Box<dyn Error>> {\n        Ok(Box::new(TestRecorder::new(*self)))\n    }\n}\n\nimpl Recorder for TestRecorder {\n    fn capture(&mut self) -> Result<PixelProvider<'_>, Box<dyn Error>> {\n        const N: usize = 120;\n        let dh = self.capturable.height / N;\n        let buf_ref = self.buf.as_mut();\n        let w = self.capturable.width;\n        for y in self.i * dh..(self.i + 1) * dh {\n            for x in 0..w {\n                self.capturable.set_default_pixel(buf_ref, x, y);\n            }\n        }\n        self.i = (self.i + 1) % N;\n        for y in self.i * dh..(self.i + 1) * dh {\n            for x in 0..w {\n                let pos = (x + y * w) * self.capturable.pixel_size();\n\n                let (pos_r, pos_g, pos_b) = match self.capturable.pixel_format {\n                    PixelFormat::BGR0 => (pos + 2, pos + 1, pos),\n                    PixelFormat::RGB0 | PixelFormat::RGB => (pos, pos + 1, pos + 2),\n                };\n                buf_ref[pos_b] = ((self.i + N * x / w) % N * 256 / N) as u8;\n                buf_ref[pos_g] = ((self.i + N * x / w + N / 3) % N * 256 / N) as u8;\n                buf_ref[pos_r] = ((self.i + N * x / w + 2 * N / 3) % N * 256 / N) as u8;\n            }\n        }\n        Ok(match self.capturable.pixel_format {\n            PixelFormat::BGR0 => PixelProvider::BGR0(\n                self.capturable.width,\n                self.capturable.height,\n                self.buf.as_slice(),\n            ),\n            PixelFormat::RGB0 => PixelProvider::RGB0(\n                self.capturable.width,\n                self.capturable.height,\n                self.buf.as_slice(),\n            ),\n            PixelFormat::RGB => PixelProvider::RGB(\n                self.capturable.width,\n                self.capturable.height,\n                self.buf.as_slice(),\n            ),\n        })\n    }\n}\n"
  },
  {
    "path": "src/capturable/win_ctx.rs",
    "content": "use std::mem::zeroed;\nuse std::{mem, ptr};\nuse winapi::shared::dxgi::{\n    CreateDXGIFactory1, IDXGIAdapter1, IDXGIFactory1, IDXGIOutput, IID_IDXGIFactory1,\n    DXGI_OUTPUT_DESC,\n};\n\nuse winapi::shared::windef::*;\nuse winapi::shared::winerror::*;\nuse winapi::um::winuser::*;\nuse wio::com::ComPtr;\n\n// from https://github.com/bryal/dxgcap-rs/blob/009b746d1c19c4c10921dd469eaee483db6aa002/src/lib.r\nfn hr_failed(hr: HRESULT) -> bool {\n    hr < 0\n}\n\nfn create_dxgi_factory_1() -> ComPtr<IDXGIFactory1> {\n    unsafe {\n        let mut factory = ptr::null_mut();\n        let hr = CreateDXGIFactory1(&IID_IDXGIFactory1, &mut factory);\n        if hr_failed(hr) {\n            panic!(\"Failed to create DXGIFactory1, {:x}\", hr)\n        } else {\n            ComPtr::from_raw(factory as *mut IDXGIFactory1)\n        }\n    }\n}\n\nfn get_adapter_outputs(adapter: &IDXGIAdapter1) -> Vec<ComPtr<IDXGIOutput>> {\n    let mut outputs = Vec::new();\n    for i in 0.. {\n        unsafe {\n            let mut output = ptr::null_mut();\n            if hr_failed(adapter.EnumOutputs(i, &mut output)) {\n                break;\n            } else {\n                let mut out_desc = zeroed();\n                (*output).GetDesc(&mut out_desc);\n                if out_desc.AttachedToDesktop != 0 {\n                    outputs.push(ComPtr::from_raw(output))\n                } else {\n                    break;\n                }\n            }\n        }\n    }\n    outputs\n}\n\n#[derive(Clone)]\npub struct WinCtx {\n    outputs: Vec<DXGI_OUTPUT_DESC>,\n    union_rect: RECT,\n}\n\nimpl WinCtx {\n    pub fn new() -> WinCtx {\n        let mut desktops: Vec<DXGI_OUTPUT_DESC> = Vec::new();\n        let mut union: RECT;\n        unsafe {\n            union = mem::zeroed();\n            let factory = create_dxgi_factory_1();\n            let mut adapter = ptr::null_mut();\n            if factory.EnumAdapters1(0, &mut adapter) != DXGI_ERROR_NOT_FOUND {\n                let adp = ComPtr::from_raw(adapter);\n                let outputs = get_adapter_outputs(&adp);\n                for o in outputs {\n                    let mut desc: DXGI_OUTPUT_DESC = mem::zeroed();\n                    o.GetDesc(ptr::addr_of_mut!(desc));\n                    desktops.push(desc);\n                    UnionRect(\n                        ptr::addr_of_mut!(union),\n                        ptr::addr_of!(union),\n                        ptr::addr_of!(desc.DesktopCoordinates),\n                    );\n                }\n            }\n        }\n        WinCtx {\n            outputs: desktops,\n            union_rect: union,\n        }\n    }\n    pub fn get_outputs(&self) -> &Vec<DXGI_OUTPUT_DESC> {\n        &self.outputs\n    }\n    pub fn get_union_rect(&self) -> &RECT {\n        &self.union_rect\n    }\n}\n"
  },
  {
    "path": "src/capturable/x11.rs",
    "content": "use crate::capturable::{Capturable, Geometry, Recorder};\nuse crate::cerror::CError;\nuse crate::video::PixelProvider;\nuse std::ffi::{CStr, CString};\nuse std::os::raw::{c_char, c_float, c_int, c_uint, c_void};\nuse std::slice::from_raw_parts;\nuse std::sync::Arc;\nuse std::{error::Error, fmt};\n\nuse tracing::debug;\n\nextern \"C\" {\n    fn XOpenDisplay(name: *const c_char) -> *mut c_void;\n    fn XCloseDisplay(disp: *mut c_void) -> c_int;\n    fn XInitThreads() -> c_int;\n    fn XLockDisplay(disp: *mut c_void);\n    fn XUnlockDisplay(disp: *mut c_void);\n\n    fn x11_set_error_handler();\n\n    fn create_capturables(\n        disp: *mut c_void,\n        handles: *mut *mut c_void,\n        num_monitors: *mut c_int,\n        size: c_int,\n        err: *mut CError,\n    ) -> c_int;\n\n    fn clone_capturable(handle: *const c_void) -> *mut c_void;\n    fn destroy_capturable(handle: *mut c_void);\n    fn get_capturable_name(handle: *const c_void) -> *const c_char;\n    fn capturable_before_input(handle: *mut c_void, err: *mut CError);\n    fn get_geometry_relative(\n        handle: *const c_void,\n        x: *mut c_float,\n        y: *mut c_float,\n        width: *mut c_float,\n        height: *mut c_float,\n        err: *mut CError,\n    );\n\n    fn map_input_device_to_entire_screen(\n        disp: *mut c_void,\n        device_name: *const c_char,\n        libinput: c_int,\n        err: *mut CError,\n    );\n    fn start_capture(handle: *const c_void, ctx: *mut c_void, err: *mut CError) -> *mut c_void;\n    fn capture_screen(\n        handle: *mut c_void,\n        img: *mut CImage,\n        capture_cursor: c_int,\n        err: *mut CError,\n    );\n    fn stop_capture(handle: *mut c_void, err: *mut CError);\n}\n\npub fn x11_init() {\n    unsafe {\n        XInitThreads();\n        x11_set_error_handler();\n    }\n}\n\npub struct X11Capturable {\n    handle: *mut c_void,\n    // keep a reference to the display so it is not closed while a capturable still exists\n    disp: Arc<XDisplay>,\n}\n\nimpl Clone for X11Capturable {\n    fn clone(&self) -> Self {\n        let handle = unsafe { clone_capturable(self.handle) };\n        Self {\n            handle,\n            disp: self.disp.clone(),\n        }\n    }\n}\n\nunsafe impl Send for X11Capturable {}\n\nimpl X11Capturable {\n    pub unsafe fn handle(&mut self) -> *mut c_void {\n        self.handle\n    }\n}\n\nimpl Capturable for X11Capturable {\n    fn name(&self) -> String {\n        unsafe {\n            CStr::from_ptr(get_capturable_name(self.handle))\n                .to_string_lossy()\n                .into()\n        }\n    }\n\n    fn geometry(&self) -> Result<Geometry, Box<dyn Error>> {\n        let mut x: c_float = 0.0;\n        let mut y: c_float = 0.0;\n        let mut width: c_float = 0.0;\n        let mut height: c_float = 0.0;\n        let mut err = CError::new();\n        self.disp.lock();\n        unsafe {\n            get_geometry_relative(\n                self.handle,\n                &mut x,\n                &mut y,\n                &mut width,\n                &mut height,\n                &mut err,\n            );\n        }\n        self.disp.unlock();\n        if err.is_err() {\n            return Err(Box::new(err));\n        }\n        Ok(Geometry::Relative(\n            x.into(),\n            y.into(),\n            width.into(),\n            height.into(),\n        ))\n    }\n\n    fn before_input(&mut self) -> Result<(), Box<dyn Error>> {\n        let mut err = CError::new();\n        self.disp.lock();\n        unsafe { capturable_before_input(self.handle, &mut err) };\n        self.disp.unlock();\n        if err.is_err() {\n            Err(Box::new(err))\n        } else {\n            Ok(())\n        }\n    }\n\n    fn recorder(&self, capture_cursor: bool) -> Result<Box<dyn Recorder>, Box<dyn Error>> {\n        match RecorderX11::new(self.clone(), capture_cursor) {\n            Ok(recorder) => Ok(Box::new(recorder)),\n            Err(err) => Err(Box::new(err)),\n        }\n    }\n}\n\nimpl fmt::Display for X11Capturable {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        write!(f, \"{}\", self.name())\n    }\n}\n\nimpl Drop for X11Capturable {\n    fn drop(&mut self) {\n        unsafe {\n            destroy_capturable(self.handle);\n        }\n    }\n}\n\nstruct XDisplay {\n    handle: *mut c_void,\n}\n\nimpl XDisplay {\n    pub fn new() -> Option<Self> {\n        let handle = unsafe { XOpenDisplay(std::ptr::null()) };\n        if handle.is_null() {\n            return None;\n        }\n        Some(Self { handle })\n    }\n\n    pub fn lock(&self) {\n        unsafe { XLockDisplay(self.handle) }\n    }\n\n    pub fn unlock(&self) {\n        unsafe { XUnlockDisplay(self.handle) }\n    }\n}\n\nimpl Drop for XDisplay {\n    fn drop(&mut self) {\n        self.lock();\n        unsafe { XCloseDisplay(self.handle) };\n        self.unlock();\n    }\n}\n\npub struct X11Context {\n    disp: Arc<XDisplay>,\n}\n\nimpl X11Context {\n    pub fn new() -> Option<Self> {\n        let disp = XDisplay::new()?;\n        Some(Self {\n            disp: Arc::new(disp),\n        })\n    }\n\n    pub fn capturables(&mut self) -> Result<Vec<X11Capturable>, CError> {\n        let mut err = CError::new();\n        let mut handles = [std::ptr::null_mut::<c_void>(); 128];\n        let mut num_monitors: c_int = 0;\n        self.disp.lock();\n        let size = unsafe {\n            create_capturables(\n                self.disp.handle,\n                handles.as_mut_ptr(),\n                &mut num_monitors,\n                handles.len() as c_int,\n                &mut err,\n            )\n        };\n        self.disp.unlock();\n        if err.is_err() {\n            if err.code() == 2 {\n                debug!(\"{}\", err);\n            } else {\n                return Err(err);\n            }\n        }\n        let mut capturables: Vec<X11Capturable> = handles[0..size as usize]\n            .iter()\n            .map(|handle| X11Capturable {\n                handle: *handle,\n                disp: self.disp.clone(),\n            })\n            .collect();\n        // The first capturable is always the whole desktop, after that there is num_monitors\n        // monitors and finally windows.\n        let win = &mut capturables[(num_monitors as usize + 1)..(size as usize)];\n        win.sort_by(|a, b| a.name().to_lowercase().cmp(&b.name().to_lowercase()));\n        Ok(capturables)\n    }\n\n    pub fn map_input_device_to_entire_screen(&mut self, device_name: &str, pen: bool) -> CError {\n        let mut err = CError::new();\n        let device_name_c_str = CString::new(device_name).unwrap();\n        self.disp.lock();\n        unsafe {\n            map_input_device_to_entire_screen(\n                self.disp.handle,\n                device_name_c_str.as_ptr(),\n                pen.into(),\n                &mut err,\n            )\n        };\n        self.disp.unlock();\n        if err.is_err() {\n            debug!(\"Failed to map input device to screen: {}\", &err);\n        }\n        err\n    }\n}\n\n#[repr(C)]\nstruct CImage {\n    data: *const u8,\n    width: c_uint,\n    height: c_uint,\n}\n\nimpl CImage {\n    pub fn new() -> Self {\n        Self {\n            data: std::ptr::null(),\n            width: 0,\n            height: 0,\n        }\n    }\n\n    pub fn size(&self) -> usize {\n        (self.width * self.height * 4) as usize\n    }\n\n    pub fn data(&self) -> &[u8] {\n        unsafe { from_raw_parts(self.data, self.size()) }\n    }\n}\n\npub struct RecorderX11 {\n    handle: *mut c_void,\n    // keep a reference to the capturable so it is not destroyed until we are done\n    #[allow(dead_code)]\n    capturable: X11Capturable,\n    img: CImage,\n    capture_cursor: bool,\n}\n\nimpl RecorderX11 {\n    pub fn new(mut capturable: X11Capturable, capture_cursor: bool) -> Result<Self, CError> {\n        let mut err = CError::new();\n        capturable.disp.lock();\n        let handle = unsafe { start_capture(capturable.handle(), std::ptr::null_mut(), &mut err) };\n        capturable.disp.unlock();\n        if err.is_err() {\n            Err(err)\n        } else {\n            Ok(Self {\n                handle,\n                capturable,\n                img: CImage::new(),\n                capture_cursor,\n            })\n        }\n    }\n}\n\nimpl Drop for RecorderX11 {\n    fn drop(&mut self) {\n        let mut err = CError::new();\n        self.capturable.disp.lock();\n        unsafe {\n            stop_capture(self.handle, &mut err);\n        }\n        self.capturable.disp.unlock();\n    }\n}\n\nimpl Recorder for RecorderX11 {\n    fn capture(&mut self) -> Result<PixelProvider<'_>, Box<dyn Error>> {\n        let mut err = CError::new();\n        self.capturable.disp.lock();\n        unsafe {\n            capture_screen(\n                self.handle,\n                &mut self.img,\n                self.capture_cursor.into(),\n                &mut err,\n            );\n        }\n        self.capturable.disp.unlock();\n        if err.is_err() {\n            self.img.data = std::ptr::null();\n            Err(err.into())\n        } else {\n            Ok(PixelProvider::BGR0(\n                self.img.width as usize,\n                self.img.height as usize,\n                self.img.data(),\n            ))\n        }\n    }\n}\n"
  },
  {
    "path": "src/cerror.rs",
    "content": "use std::error::Error;\nuse std::ffi::CStr;\nuse std::fmt;\n\nuse std::os::raw::{c_char, c_int};\n\n#[repr(C)]\npub struct CError {\n    code: c_int,\n    error_str: [c_char; 1024],\n}\n\npub enum CErrorCode {\n    NoError,\n    GenericError,\n    UInputNotAccessible,\n}\n\nimpl CError {\n    pub fn new() -> Self {\n        Self {\n            code: 0,\n            error_str: [0; 1024],\n        }\n    }\n\n    pub fn is_err(&self) -> bool {\n        self.code != 0\n    }\n\n    pub fn code(&self) -> i32 {\n        self.code as i32\n    }\n\n    pub fn to_enum(&self) -> CErrorCode {\n        match self.code {\n            0 => CErrorCode::NoError,\n            101 => CErrorCode::UInputNotAccessible,\n            _ => CErrorCode::GenericError,\n        }\n    }\n}\n\nimpl fmt::Display for CError {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        write!(f, \"CError: code: {} message: {}\", self.code, unsafe {\n            CStr::from_ptr(self.error_str.as_ptr()).to_string_lossy()\n        })\n    }\n}\n\nimpl fmt::Debug for CError {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        fmt::Display::fmt(self, f)\n    }\n}\n\nimpl Error for CError {}\n"
  },
  {
    "path": "src/config.rs",
    "content": "use std::net::IpAddr;\nuse std::{fs, path::PathBuf};\n\nuse clap::Parser;\nuse serde::{Deserialize, Serialize};\nuse tracing::{debug, warn};\n\n#[derive(clap::ValueEnum, Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]\npub enum ThemeType {\n    Aero,\n    AquaClassic,\n    Blue,\n    Classic,\n    Dark,\n    Greybird,\n    HighContrast,\n    Metro,\n}\n\nconst THEME_LIST: [ThemeType; 8] = [\n    ThemeType::Aero,\n    ThemeType::AquaClassic,\n    ThemeType::Blue,\n    ThemeType::Classic,\n    ThemeType::Dark,\n    ThemeType::Greybird,\n    ThemeType::HighContrast,\n    ThemeType::Metro,\n];\n\nimpl Default for ThemeType {\n    fn default() -> Self {\n        Self::Greybird\n    }\n}\n\nimpl ThemeType {\n    pub fn apply(&self) {\n        let theme = match self {\n            ThemeType::Classic => fltk_theme::ThemeType::Classic,\n            ThemeType::Aero => fltk_theme::ThemeType::Aero,\n            ThemeType::Metro => fltk_theme::ThemeType::Metro,\n            ThemeType::AquaClassic => fltk_theme::ThemeType::AquaClassic,\n            ThemeType::Greybird => fltk_theme::ThemeType::Greybird,\n            ThemeType::Blue => fltk_theme::ThemeType::Blue,\n            ThemeType::Dark => fltk_theme::ThemeType::Dark,\n            ThemeType::HighContrast => fltk_theme::ThemeType::HighContrast,\n        };\n        let theme = fltk_theme::WidgetTheme::new(theme);\n        theme.apply();\n    }\n\n    pub fn name(&self) -> String {\n        format!(\"{self:?}\")\n    }\n\n    pub fn to_index(&self) -> i32 {\n        THEME_LIST.iter().position(|th| th == self).unwrap() as i32\n    }\n\n    pub fn from_index(i: i32) -> Self {\n        let i = i.clamp(0, THEME_LIST.len() as i32 - 1) as usize;\n        THEME_LIST[i]\n    }\n\n    pub fn themes() -> &'static [ThemeType] {\n        &THEME_LIST\n    }\n}\n\n#[derive(Serialize, Deserialize, Parser, Debug, Clone)]\n#[command(version, about, long_about = None)]\npub struct Config {\n    #[arg(long, help = \"Access code\")]\n    pub access_code: Option<String>,\n    #[arg(long, default_value = \"0.0.0.0\", help = \"Bind address\")]\n    pub bind_address: IpAddr,\n    #[arg(long, default_value = \"1701\", help = \"Web port\")]\n    pub web_port: u16,\n    #[cfg(target_os = \"linux\")]\n    #[arg(\n        long,\n        help = \"Try to use hardware acceleration through the Video Acceleration API.\"\n    )]\n    pub try_vaapi: bool,\n    #[cfg(any(target_os = \"linux\", target_os = \"windows\"))]\n    #[arg(long, help = \"Try to use Nvidia's NVENC to encode the video via GPU.\")]\n    #[serde(default)]\n    pub try_nvenc: bool,\n    #[cfg(target_os = \"macos\")]\n    #[arg(\n        long,\n        help = \"Try to use hardware acceleration through the VideoToolbox API.\"\n    )]\n    #[serde(default)]\n    pub try_videotoolbox: bool,\n    #[cfg(target_os = \"windows\")]\n    #[arg(\n        long,\n        help = \"Try to use hardware acceleration through the MediaFoundation API.\"\n    )]\n    #[serde(default)]\n    pub try_mediafoundation: bool,\n    #[arg(long, help = \"Start Weylus server immediately on program start.\")]\n    #[serde(default)]\n    pub auto_start: bool,\n    #[arg(long, help = \"Gui Theme\")]\n    pub gui_theme: Option<ThemeType>,\n    #[arg(long, help = \"Run Weylus without gui and start immediately.\")]\n    #[serde(default)]\n    pub no_gui: bool,\n    #[cfg(target_os = \"linux\")]\n    #[arg(long, help = \"Wayland/PipeWire Support.\")]\n    #[serde(default)]\n    pub wayland_support: bool,\n\n    #[arg(long, help = \"Print template of index.html served by Weylus.\")]\n    #[serde(skip)]\n    pub print_index_html: bool,\n    #[arg(long, help = \"Print access.html served by Weylus.\")]\n    #[serde(skip)]\n    pub print_access_html: bool,\n    #[arg(long, help = \"Print style.css served by Weylus.\")]\n    #[serde(skip)]\n    pub print_style_css: bool,\n    #[arg(long, help = \"Print lib.js served by Weylus.\")]\n    #[serde(skip)]\n    pub print_lib_js: bool,\n\n    #[arg(\n        long,\n        help = \"Use custom template of index.html to be served by Weylus.\"\n    )]\n    #[serde(skip)]\n    pub custom_index_html: Option<PathBuf>,\n    #[arg(long, help = \"Use custom access.html to be served by Weylus.\")]\n    #[serde(skip)]\n    pub custom_access_html: Option<PathBuf>,\n    #[arg(long, help = \"Use custom style.css to be served by Weylus.\")]\n    #[serde(skip)]\n    pub custom_style_css: Option<PathBuf>,\n    #[arg(long, help = \"Use custom lib.js to be served by Weylus.\")]\n    #[serde(skip)]\n    pub custom_lib_js: Option<PathBuf>,\n\n    #[arg(long, help = \"Print shell completions for given shell.\")]\n    #[serde(skip)]\n    pub completions: Option<clap_complete::Shell>,\n}\n\npub fn read_config() -> Option<Config> {\n    if let Some(mut config_path) = dirs::config_dir() {\n        config_path.push(\"weylus\");\n        config_path.push(\"weylus.toml\");\n        match fs::read_to_string(&config_path) {\n            Ok(s) => match toml::from_str(&s) {\n                Ok(c) => Some(c),\n                Err(e) => {\n                    warn!(\"Failed to read configuration file: {}\", e);\n                    None\n                }\n            },\n            Err(err) => {\n                match err.kind() {\n                    std::io::ErrorKind::NotFound => {\n                        debug!(\"Failed to read configuration file: {}\", err)\n                    }\n                    _ => warn!(\"Failed to read configuration file: {}\", err),\n                }\n                None\n            }\n        }\n    } else {\n        None\n    }\n}\n\npub fn write_config(conf: &Config) {\n    match dirs::config_dir() {\n        Some(mut config_path) => {\n            config_path.push(\"weylus\");\n            if !config_path.exists() {\n                if let Err(err) = fs::create_dir_all(&config_path) {\n                    warn!(\"Failed create directory for configuration: {}\", err);\n                    return;\n                }\n            }\n            config_path.push(\"weylus.toml\");\n            if let Err(err) = fs::write(\n                config_path,\n                &toml::to_string_pretty(&conf).expect(\"Failed to encode config to toml.\"),\n            ) {\n                warn!(\"Failed to write configuration file: {}\", err);\n            }\n        }\n        None => {\n            warn!(\"Failed to find configuration directory!\");\n        }\n    }\n}\n\npub fn get_config() -> Config {\n    let args = std::env::args();\n    if let Some(mut config) = read_config() {\n        if args.len() > 1 {\n            config.update_from(args);\n        }\n        config\n    } else {\n        Config::parse()\n    }\n}\n"
  },
  {
    "path": "src/gui.rs",
    "content": "use std::cmp::min;\nuse std::io::Cursor;\nuse std::iter::Iterator;\nuse std::net::{IpAddr, SocketAddr};\nuse std::sync::atomic::AtomicBool;\n\nuse fltk::app;\nuse fltk::enums::{FrameType, LabelType};\nuse fltk::image::PngImage;\nuse fltk::menu::Choice;\nuse std::sync::{mpsc, Arc, Mutex};\nuse tracing::{error, info, warn};\n\nuse fltk::{\n    app::{awake_callback, App},\n    button::{Button, CheckButton},\n    frame::Frame,\n    input::{Input, IntInput},\n    output::Output,\n    prelude::*,\n    text::{TextBuffer, TextDisplay},\n    window::Window,\n};\n\n#[cfg(not(target_os = \"windows\"))]\nuse pnet_datalink as datalink;\n\nuse crate::config::{write_config, Config, ThemeType};\nuse crate::protocol::{CustomInputAreas, Rect};\nuse crate::web::Web2UiMessage::UInputInaccessible;\n\npub fn run(config: &Config, log_receiver: mpsc::Receiver<String>) {\n    let width = 200;\n    let height = 30;\n    let padding = 10;\n\n    let app = App::default().with_scheme(fltk::app::AppScheme::Gtk);\n    config.gui_theme.map(|th| th.apply());\n    let mut wind = Window::default()\n        .with_size(660, 600)\n        .center_screen()\n        .with_label(&format!(\"Weylus - {}\", env!(\"CARGO_PKG_VERSION\")));\n    wind.set_xclass(\"weylus\");\n    wind.set_callback(move |_win| app.quit());\n\n    let mut input_access_code = Input::default()\n        .with_pos(130, 30)\n        .with_size(width, height)\n        .with_label(\"Access code\");\n    input_access_code.set_tooltip(\n        \"Restrict who can control your computer with an access code. Note that this does NOT do \\\n        any kind of encryption and it is advised to only run Weylus inside trusted networks! Do \\\n        NOT reuse any of your passwords! If left blank, no code is required to access Weylus \\\n        remotely.\",\n    );\n    if let Some(code) = config.access_code.as_ref() {\n        input_access_code.set_value(code);\n    }\n\n    let mut input_bind_addr = Input::default()\n        .with_size(width, height)\n        .below_of(&input_access_code, padding)\n        .with_label(\"Bind Address\");\n    input_bind_addr.set_value(&config.bind_address.to_string());\n\n    let mut input_port = IntInput::default()\n        .with_size(width, height)\n        .below_of(&input_bind_addr, padding)\n        .with_label(\"Port\");\n    input_port.set_value(&config.web_port.to_string());\n\n    let mut check_auto_start = CheckButton::default()\n        .with_size(70, height)\n        .below_of(&input_port, padding + 5)\n        .with_label(\"Auto Start\");\n    check_auto_start.set_tooltip(\"Start Weylus server immediately on program start.\");\n    check_auto_start.set_checked(config.auto_start);\n\n    #[cfg(target_os = \"linux\")]\n    let mut check_wayland = CheckButton::default()\n        .with_size(70, height)\n        .right_of(&check_auto_start, 3 * padding)\n        .with_label(\"Wayland/\\nPipeWire\\nSupport\");\n    #[cfg(target_os = \"linux\")]\n    {\n        check_wayland.set_tooltip(\n            \"EXPERIMENTAL! This may crash your desktop! Enables screen \\\n        capturing for Wayland using PipeWire and GStreamer.\",\n        );\n        check_wayland.set_checked(config.wayland_support);\n    }\n\n    let mut label_hw_accel = Frame::default()\n        .with_size(width, height)\n        .below_of(&check_auto_start, padding)\n        .with_label(\"Try Hardware acceleration\");\n    label_hw_accel.set_tooltip(\n        \"On many systems video encoding can be done with hardware \\\n        acceleration. By default this is disabled as the quality and stability of video encoding \\\n        varies greatly among hardware and drivers. Currently this is only supported on Linux.\",\n    );\n\n    let mut check_native_hw_accel = CheckButton::default()\n        .with_size(70, height)\n        .below_of(&label_hw_accel, 0);\n\n    #[cfg(target_os = \"linux\")]\n    {\n        check_native_hw_accel.set_label(\"VAAPI\");\n        check_native_hw_accel\n            .set_tooltip(\"Try to use hardware acceleration through the Video Acceleration API.\");\n        check_native_hw_accel.set_checked(config.try_vaapi);\n    }\n\n    #[cfg(target_os = \"macos\")]\n    {\n        check_native_hw_accel.set_label(\"VideoToolbox\");\n        check_native_hw_accel\n            .set_tooltip(\"Try to use hardware acceleration through the VideoToolbox API.\");\n        check_native_hw_accel.set_checked(config.try_videotoolbox);\n    }\n\n    #[cfg(target_os = \"windows\")]\n    {\n        check_native_hw_accel.set_label(\"Media-\\nFoundation\");\n        check_native_hw_accel\n            .set_tooltip(\"Try to use hardware acceleration through the MediaFoundation API.\");\n        check_native_hw_accel.set_checked(config.try_mediafoundation);\n    }\n\n    let mut check_nvenc = CheckButton::default()\n        .with_size(70, height)\n        .right_of(&check_native_hw_accel, 2 * padding)\n        .with_label(\"NVENC\");\n    check_nvenc.set_tooltip(\"Try to use Nvidia's NVENC to encode the video via GPU.\");\n\n    #[cfg(any(target_os = \"linux\", target_os = \"windows\"))]\n    check_nvenc.set_checked(config.try_nvenc);\n\n    #[cfg(not(any(target_os = \"linux\", target_os = \"windows\")))]\n    {\n        check_nvenc.deactivate();\n        check_nvenc.hide();\n    }\n\n    let mut but_toggle = Button::default()\n        .with_size(width, height)\n        .below_of(&check_native_hw_accel, 2 * padding)\n        .with_label(\"Start\");\n\n    let mut output_server_addr = Output::default()\n        .with_size(500, height)\n        .below_of(&but_toggle, 3 * padding)\n        .with_label(\"Connect your\\ntablet to:\");\n    output_server_addr.hide();\n\n    let output_buf = TextBuffer::default();\n    let mut output = TextDisplay::default().with_size(600, 6 * height).with_pos(\n        30,\n        output_server_addr.y() + output_server_addr.height() + padding,\n    );\n    output.set_buffer(output_buf);\n    let output_buf = output.buffer().unwrap();\n\n    let mut choice_theme = Choice::default()\n        .with_size(width, height)\n        .right_of(&input_access_code, padding);\n\n    for theme in ThemeType::themes() {\n        choice_theme.add_choice(&theme.name());\n    }\n    choice_theme.set_value(config.gui_theme.unwrap_or(ThemeType::default()).to_index());\n\n    let mut qr_frame = Frame::default()\n        .with_size(235, 235)\n        .right_of(&input_bind_addr, padding);\n\n    qr_frame.hide();\n\n    wind.make_resizable(true);\n    wind.end();\n    wind.show();\n\n    let output_buf = Arc::new(Mutex::new(output_buf));\n\n    std::thread::spawn(move || {\n        while let Ok(log_message) = log_receiver.recv() {\n            let mut output_buf = output_buf.lock().unwrap();\n            output_buf.append(&log_message);\n        }\n    });\n\n    let mut weylus = crate::weylus::Weylus::new();\n    let mut is_server_running = false;\n    let auto_start = config.auto_start;\n    let config = Arc::new(Mutex::new(config.clone()));\n\n    {\n        let config = config.clone();\n        choice_theme.set_callback(move |c| {\n            let v = c.value();\n            if v >= 0 {\n                ThemeType::from_index(v).apply();\n                config.lock().unwrap().gui_theme = Some(ThemeType::from_index(v));\n                write_config(&config.lock().unwrap());\n            }\n        });\n    }\n\n    let mut toggle_server = move |but: &mut Button| {\n        if let Err(err) = || -> Result<(), Box<dyn std::error::Error>> {\n            let mut config = config.lock().unwrap();\n            if !is_server_running {\n                {\n                    let access_code_string = input_access_code.value();\n                    let access_code = match access_code_string.as_str() {\n                        \"\" => None,\n                        code => Some(code),\n                    };\n                    let bind_addr: IpAddr = input_bind_addr.value().parse()?;\n                    let web_port: u16 = input_port.value().parse()?;\n\n                    config.access_code = access_code.map(|s| s.to_string());\n                    config.web_port = web_port;\n                    config.bind_address = bind_addr;\n                    config.auto_start = check_auto_start.is_checked();\n                    config.gui_theme = Some(ThemeType::from_index(choice_theme.value()));\n                    #[cfg(target_os = \"linux\")]\n                    {\n                        config.try_vaapi = check_native_hw_accel.is_checked();\n                        config.wayland_support = check_wayland.is_checked();\n                    }\n                    #[cfg(any(target_os = \"linux\", target_os = \"windows\"))]\n                    {\n                        config.try_nvenc = check_nvenc.is_checked();\n                    }\n                    #[cfg(target_os = \"macos\")]\n                    {\n                        config.try_videotoolbox = check_native_hw_accel.is_checked();\n                    }\n                    #[cfg(target_os = \"windows\")]\n                    {\n                        config.try_mediafoundation = check_native_hw_accel.is_checked();\n                    }\n                }\n                if !weylus.start(&config, |message| match message {\n                    UInputInaccessible => awake_callback(move || {\n                        let w = 500;\n                        let h = 300;\n                        let mut pop_up = Window::default()\n                            .with_size(w, h)\n                            .center_screen()\n                            .with_label(\"Weylus - UInput inaccessible!\");\n                        pop_up.set_xclass(\"weylus\");\n\n                        let buf = TextBuffer::default();\n                        let mut pop_up_text = TextDisplay::default().with_size(w, h);\n                        pop_up_text.set_buffer(buf);\n                        pop_up_text.wrap_mode(fltk::text::WrapMode::AtBounds, 5);\n                        let mut buf = pop_up_text.buffer().unwrap();\n                        buf.set_text(std::include_str!(\"strings/uinput_error.txt\"));\n\n                        pop_up.end();\n                        pop_up.make_modal(true);\n                        pop_up.show();\n                    }),\n                }) {\n                    return Ok(());\n                }\n                is_server_running = true;\n\n                write_config(&config);\n\n                let mut web_sock = SocketAddr::new(config.bind_address, config.web_port);\n\n                #[cfg(not(target_os = \"windows\"))]\n                {\n                    if web_sock.ip().is_unspecified() {\n                        // try to guess an ip\n                        let mut ips = Vec::<IpAddr>::new();\n                        for iface in datalink::interfaces()\n                            .iter()\n                            .filter(|iface| iface.is_up() && !iface.is_loopback())\n                        {\n                            for ipnetw in &iface.ips {\n                                if (ipnetw.is_ipv4() && web_sock.ip().is_ipv4())\n                                    || (ipnetw.is_ipv6() && web_sock.ip().is_ipv6())\n                                {\n                                    // filtering ipv6 unicast requires nightly or more fiddling,\n                                    // lets wait for nightlies to stabilize...\n                                    ips.push(ipnetw.ip())\n                                }\n                            }\n                        }\n                        if !ips.is_empty() {\n                            web_sock.set_ip(ips[0]);\n                        }\n                        if ips.len() > 1 {\n                            info!(\"Found more than one IP address for browsers to connect to,\");\n                            info!(\"other urls are:\");\n                            for ip in &ips[1..] {\n                                info!(\"http://{}\", SocketAddr::new(*ip, config.web_port));\n                            }\n                        }\n                    }\n                }\n\n                #[cfg(not(target_os = \"windows\"))]\n                {\n                    use image::Luma;\n                    use qrcode::QrCode;\n                    let addr_string = format!(\"http://{}\", web_sock);\n                    output_server_addr.set_value(&addr_string);\n                    let mut url_string = addr_string;\n                    if let Some(access_code) = &config.access_code {\n                        url_string.push_str(\"?access_code=\");\n                        url_string.push_str(\n                            &percent_encoding::utf8_percent_encode(\n                                access_code,\n                                percent_encoding::NON_ALPHANUMERIC,\n                            )\n                            .to_string(),\n                        );\n                    }\n\n                    let cb = move |qr_frame: &mut Frame, _, _, w, h| {\n                        let code = QrCode::new(&url_string).unwrap();\n                        let img_buf = code.render::<Luma<u8>>().build();\n                        let image = image::DynamicImage::ImageLuma8(img_buf);\n                        let dims = min(w, h) as u32;\n                        let image =\n                            image.resize_exact(dims, dims, image::imageops::FilterType::Nearest);\n                        let mut buf = vec![];\n                        let mut cursor = Cursor::new(&mut buf);\n                        image\n                            .write_to(&mut cursor, image::ImageFormat::Png)\n                            .unwrap();\n                        let png = PngImage::from_data(&buf).unwrap();\n                        qr_frame.set_image(Some(png));\n                    };\n\n                    let x = qr_frame.x();\n                    let y = qr_frame.y();\n                    let w = qr_frame.width();\n                    let h = qr_frame.height();\n                    cb(&mut qr_frame, x, y, w, h);\n                    qr_frame.resize_callback(cb);\n                    qr_frame.show();\n                }\n                #[cfg(target_os = \"windows\")]\n                {\n                    if web_sock.ip().is_unspecified() {\n                        output_server_addr.set_value(\"http://<your ip address>\");\n                    } else {\n                        output_server_addr.set_value(&format!(\"http://{}\", web_sock.to_string()));\n                    }\n                }\n                output_server_addr.show();\n                but.set_label(\"Stop\");\n            } else {\n                weylus.stop();\n                but.set_label(\"Start\");\n                output_server_addr.hide();\n                qr_frame.resize_callback(|_, _, _, _, _| {});\n                qr_frame.hide();\n                is_server_running = false;\n            }\n            Ok(())\n        }() {\n            error!(\"{}\", err);\n        };\n    };\n\n    if auto_start {\n        toggle_server(&mut but_toggle);\n    }\n\n    but_toggle.set_callback(toggle_server);\n\n    app.run().expect(\"Failed to run Gui!\");\n\n    // TODO: Remove when https://github.com/fltk-rs/fltk-rs/issues/1480 is fixed\n    // this is required to drop the callback and do a graceful shutdown of the web server\n    but_toggle.set_callback(|_| ());\n}\n\nconst BORDER: i32 = 30;\nstatic WINCTX: Mutex<Option<InputAreaWindowContext>> = Mutex::new(None);\n\nstruct InputAreaWindowContext {\n    win: Window,\n    choice_mouse: Choice,\n    choice_touch: Choice,\n    choice_pen: Choice,\n    workspaces: Vec<Rect>,\n}\n\npub fn get_input_area(\n    no_gui: bool,\n    output_sender: std::sync::mpsc::Sender<crate::protocol::CustomInputAreas>,\n) {\n    // If no gui is running there is no event loop and windows can not be created.\n    // That's why we initialize the fltk app here one the first call.\n    if no_gui {\n        static GUI_INITIALIZED: AtomicBool = AtomicBool::new(false);\n\n        if !GUI_INITIALIZED.swap(true, std::sync::atomic::Ordering::Relaxed) {\n            std::thread::spawn(move || {\n                let _app = App::default().with_scheme(fltk::app::AppScheme::Gtk);\n                let mut winctx = create_custom_input_area_window();\n                custom_input_area_window_handle_events(&mut winctx.win, output_sender.clone());\n                show_overlay_window(&mut winctx);\n                WINCTX.lock().unwrap().replace(winctx);\n                loop {\n                    // calling wait_for ensures that the fltk event loop keeps running even if\n                    // there is no window shown\n                    if let Err(err) = app::wait_for(1.0) {\n                        warn!(\"Error waiting for fltk events: {err}.\");\n                    }\n                }\n            });\n        } else {\n            fltk::app::awake_callback(move || {\n                let mut winctx = WINCTX.lock().unwrap();\n                let winctx = winctx.as_mut().unwrap();\n                custom_input_area_window_handle_events(&mut winctx.win, output_sender.clone());\n                show_overlay_window(winctx);\n            });\n        }\n    } else {\n        fltk::app::awake_callback(move || {\n            let mut winctx = WINCTX.lock().unwrap();\n            if winctx.is_none() {\n                winctx.replace(create_custom_input_area_window());\n            }\n\n            let winctx = winctx.as_mut().unwrap();\n            custom_input_area_window_handle_events(&mut winctx.win, output_sender.clone());\n            show_overlay_window(winctx);\n        });\n    }\n}\n\nfn create_custom_input_area_window() -> InputAreaWindowContext {\n    let mut win = Window::default().with_size(600, 600).center_screen();\n    win.make_resizable(true);\n    win.set_border(false);\n    win.set_frame(FrameType::FlatBox);\n    win.set_color(fltk::enums::Color::from_rgb(240, 240, 240));\n    let mut frame = Frame::default()\n        .with_size(win.w() - 2 * BORDER, win.h() - 2 * BORDER)\n        .center_of_parent()\n        .with_label(\n            \"Press Enter to submit\\ncurrent selection as\\ncustom input area,\\nEscape to abort.\",\n        );\n    frame.set_label_type(LabelType::Normal);\n    frame.set_label_size(20);\n    frame.set_color(fltk::enums::Color::Black);\n    frame.set_frame(FrameType::BorderFrame);\n    frame.set_label_font(fltk::enums::Font::HelveticaBold);\n    let width = 200;\n    let height = 30;\n    let padding = 10;\n    let tool_tip = \"Some systems may have the input device mapped to a specific screen, this screen has to be selected here. Otherwise input mapping will be wrong. Selecting None disables any mapping.\";\n    let mut choice_mouse = Choice::default()\n        .with_size(width, height)\n        .with_pos(padding, 4 * padding)\n        .center_x(&frame)\n        .with_id(\"choice_mouse\")\n        .with_label(\"Map Mouse from:\");\n    choice_mouse.set_tooltip(tool_tip);\n    let mut choice_touch = Choice::default()\n        .with_size(width, height)\n        .below_of(&choice_mouse, padding)\n        .with_id(\"choice_touch\")\n        .with_label(\"Map Touch from:\");\n    choice_touch.set_tooltip(tool_tip);\n    let mut choice_pen = Choice::default()\n        .with_size(width, height)\n        .below_of(&choice_touch, padding)\n        .with_id(\"choice_pen\")\n        .with_label(\"Map Pen from:\");\n    choice_pen.set_tooltip(tool_tip);\n\n    frame.handle(|frame, event| match event {\n        fltk::enums::Event::Push => {\n            if app::event_clicks() {\n                if let Some(mut win) = frame.window() {\n                    win.fullscreen(!win.fullscreen_active());\n                }\n                true\n            } else {\n                false\n            }\n        }\n        _ => false,\n    });\n    win.resize_callback(move |_win, _x, _y, w, h| {\n        frame.resize(BORDER, BORDER, w - 2 * BORDER, h - 2 * BORDER)\n    });\n    win.end();\n    InputAreaWindowContext {\n        win,\n        choice_mouse,\n        choice_touch,\n        choice_pen,\n        workspaces: Vec::new(),\n    }\n}\n\nfn custom_input_area_window_handle_events(\n    win: &mut Window,\n    sender: std::sync::mpsc::Sender<crate::protocol::CustomInputAreas>,\n) {\n    #[derive(Debug)]\n    enum MouseFlags {\n        All,\n        Edge(bool, bool),\n        Corner(bool, bool),\n    }\n    fn get_mouse_flags(win: &Window, x: i32, y: i32) -> MouseFlags {\n        let dx0 = (win.x() - x).abs();\n        let dy0 = (win.y() - y).abs();\n        let dx1 = (win.x() + win.w() - x).abs();\n        let dy1 = (win.y() + win.h() - y).abs();\n        let dx = min(dx0, dx1);\n        let dy = min(dy0, dy1);\n        let d = min(dx, dy);\n        if d <= BORDER {\n            if dx <= BORDER && dy <= BORDER {\n                MouseFlags::Corner(dx0 <= dx1, dy0 <= dy1)\n            } else {\n                MouseFlags::Edge(dx <= dy, if dx <= dy { dx0 <= dx1 } else { dy0 <= dy1 })\n            }\n        } else {\n            MouseFlags::All\n        }\n    }\n    fn get_screen_coords_from_event_coords(win: &Window, (x, y): (i32, i32)) -> (i32, i32) {\n        (x + win.x(), y + win.y())\n    }\n    fn set_cursor(\n        win: &mut Window,\n        current_cursor: &mut fltk::enums::Cursor,\n        flags: Option<MouseFlags>,\n    ) {\n        let cursor = match flags {\n            Some(MouseFlags::All) => fltk::enums::Cursor::Move,\n            Some(MouseFlags::Edge(bx, by)) => match (bx, by) {\n                (true, true) => fltk::enums::Cursor::W,\n                (true, false) => fltk::enums::Cursor::E,\n                (false, true) => fltk::enums::Cursor::N,\n                (false, false) => fltk::enums::Cursor::S,\n            },\n            Some(MouseFlags::Corner(bx, by)) => match (bx, by) {\n                (true, true) => fltk::enums::Cursor::NWSE,\n                (true, false) => fltk::enums::Cursor::NESW,\n                (false, true) => fltk::enums::Cursor::NESW,\n                (false, false) => fltk::enums::Cursor::NWSE,\n            },\n            None => fltk::enums::Cursor::Default,\n        };\n        if *current_cursor != cursor {\n            *current_cursor = cursor;\n            win.set_cursor(cursor);\n        }\n    }\n\n    let mut drag_flags = MouseFlags::All;\n    let mut current_cursor = fltk::enums::Cursor::Default;\n    let mut x = 0;\n    let mut y = 0;\n    let mut win_x_drag_start = 0;\n    let mut win_y_drag_start = 0;\n    let mut win_w_drag_start = 0;\n    let mut win_h_drag_start = 0;\n    win.handle(move |win, event| {\n        match event {\n            fltk::enums::Event::Move => {\n                let (x, y) = get_screen_coords_from_event_coords(&win, app::event_coords());\n                let flags = get_mouse_flags(&win, x, y);\n                set_cursor(win, &mut current_cursor, Some(flags));\n                true\n            }\n            fltk::enums::Event::Leave => {\n                win.set_cursor(fltk::enums::Cursor::Default);\n                true\n            }\n            fltk::enums::Event::Push => {\n                (x, y) = get_screen_coords_from_event_coords(&win, app::event_coords());\n                win_x_drag_start = win.x();\n                win_y_drag_start = win.y();\n                win_w_drag_start = win.w();\n                win_h_drag_start = win.h();\n                drag_flags = get_mouse_flags(&win, x, y);\n                true\n            }\n            fltk::enums::Event::Drag => {\n                if win.opacity() == 1.0 {\n                    win.set_opacity(0.5);\n                }\n                let (x_new, y_new) = get_screen_coords_from_event_coords(&win, app::event_coords());\n                let dx = x_new - x;\n                let dy = y_new - y;\n                match drag_flags {\n                    MouseFlags::All => win.set_pos(win_x_drag_start + dx, win_y_drag_start + dy),\n                    MouseFlags::Edge(bx, by) => match (bx, by) {\n                        (true, true) => win.resize(\n                            win_x_drag_start + dx,\n                            win_y_drag_start,\n                            win_w_drag_start - dx,\n                            win_h_drag_start,\n                        ),\n                        (true, false) => win.resize(\n                            win_x_drag_start,\n                            win_y_drag_start,\n                            win_w_drag_start + dx,\n                            win_h_drag_start,\n                        ),\n                        (false, true) => win.resize(\n                            win_x_drag_start,\n                            win_y_drag_start + dy,\n                            win_w_drag_start,\n                            win_h_drag_start - dy,\n                        ),\n                        (false, false) => win.resize(\n                            win_x_drag_start,\n                            win_y_drag_start,\n                            win_w_drag_start,\n                            win_h_drag_start + dy,\n                        ),\n                    },\n                    MouseFlags::Corner(bx, by) => match (bx, by) {\n                        (true, true) => win.resize(\n                            win_x_drag_start + dx,\n                            win_y_drag_start + dy,\n                            win_w_drag_start - dx,\n                            win_h_drag_start - dy,\n                        ),\n\n                        (true, false) => win.resize(\n                            win_x_drag_start + dx,\n                            win_y_drag_start,\n                            win_w_drag_start - dx,\n                            win_h_drag_start + dy,\n                        ),\n                        (false, true) => win.resize(\n                            win_x_drag_start,\n                            win_y_drag_start + dy,\n                            win_w_drag_start + dx,\n                            win_h_drag_start - dy,\n                        ),\n                        (false, false) => win.resize(\n                            win_x_drag_start,\n                            win_y_drag_start,\n                            win_w_drag_start + dx,\n                            win_h_drag_start + dy,\n                        ),\n                    },\n                }\n                true\n            }\n            fltk::enums::Event::Released => {\n                if win.opacity() != 1.0 {\n                    win.set_opacity(1.0);\n                }\n                true\n            }\n            fltk::enums::Event::KeyDown => match app::event_key() {\n                fltk::enums::Key::Enter => {\n                    fn relative_rect(win: &Window, workspace: &Rect) -> Rect {\n                        // clamp rect to workspace and ensure it has non-zero area\n                        let mut rect = crate::protocol::Rect {\n                            x: (win.x() as f64 - workspace.x).min(workspace.w) / workspace.w,\n                            y: (win.y() as f64 - workspace.y).min(workspace.h) / workspace.h,\n                            w: win.w().max(1) as f64 / workspace.w,\n                            h: win.h().max(1) as f64 / workspace.h,\n                        };\n                        rect.w = rect.w.min(1.0 - rect.x);\n                        rect.h = rect.h.min(1.0 - rect.y);\n                        rect\n                    }\n                    win.set_cursor(fltk::enums::Cursor::Default);\n                    win.hide();\n                    let mut areas = CustomInputAreas::default();\n                    let workspaces = WINCTX.lock().unwrap().as_ref().unwrap().workspaces.clone();\n                    for (name, area) in [\n                        (\"choice_mouse\", &mut areas.mouse),\n                        (\"choice_touch\", &mut areas.touch),\n                        (\"choice_pen\", &mut areas.pen),\n                    ] {\n                        let c: Choice = fltk::app::widget_from_id(name).unwrap();\n                        match c.value() {\n                            0 => (),\n                            v @ 1.. if (v as usize) <= workspaces.len() => {\n                                let workspace = workspaces[v as usize - 1];\n                                *area = Some(relative_rect(win, &workspace))\n                            }\n                            v => warn!(\"Unexpected value in {name}: {v}!\"),\n                        }\n                    }\n                    sender.send(areas).unwrap();\n                    true\n                }\n                fltk::enums::Key::Escape => {\n                    win.set_cursor(fltk::enums::Cursor::Default);\n                    win.hide();\n                    true\n                }\n                _ => false,\n            },\n            _ => false,\n        }\n    });\n}\n\nfn show_overlay_window(winctx: &mut InputAreaWindowContext) {\n    let win = &mut winctx.win;\n    if win.shown() {\n        return;\n    }\n    let screens = fltk::app::Screen::all_screens();\n    winctx.workspaces.clear();\n    winctx.workspaces.push(get_full_workspace_rect());\n    for screen in &screens {\n        let fltk::draw::Rect { x, y, w, h } = screen.work_area();\n        winctx.workspaces.push(Rect {\n            x: x as f64,\n            y: y as f64,\n            w: w as f64,\n            h: h as f64,\n        });\n    }\n    for c in [\n        &mut winctx.choice_mouse,\n        &mut winctx.choice_touch,\n        &mut winctx.choice_pen,\n    ] {\n        let v = c.value();\n        c.clear();\n        c.add_choice(\"None\");\n        c.add_choice(\"Full Workspace\");\n        for screen in &screens {\n            c.add_choice(&format!(\n                \"Screen {n} at {w}x{h}+{x}+{y}\",\n                n = screen.n,\n                w = screen.w(),\n                h = screen.h(),\n                x = screen.x(),\n                y = screen.y()\n            ));\n        }\n        if v >= 0 && (v as usize) < 2 + screens.len() {\n            c.set_value(v);\n        } else {\n            c.set_value(0);\n        }\n    }\n    if win.fullscreen_active() {\n        win.set_size(600, 600);\n        let n = win.screen_num();\n        let screen = app::Screen::new(n).unwrap();\n        win.set_pos(\n            screen.x() + (screen.w() - 600) / 2,\n            screen.y() + (screen.h() - 600) / 2,\n        );\n    }\n    win.show();\n    win.set_on_top();\n    win.set_visible_focus();\n}\n\npub fn get_full_workspace_rect() -> Rect {\n    let mut rect = Rect::default();\n    for screen in fltk::app::Screen::all_screens() {\n        let fltk::draw::Rect { x, y, w, h } = screen.work_area();\n        rect.x = (x as f64).min(rect.x);\n        rect.y = (y as f64).min(rect.y);\n        rect.w = ((x + w) as f64).max(rect.w);\n        rect.h = ((y + h) as f64).max(rect.h);\n    }\n    rect\n}\n"
  },
  {
    "path": "src/input/autopilot_device.rs",
    "content": "use autopilot::geometry::Size;\nuse autopilot::mouse;\nuse autopilot::mouse::ScrollDirection;\nuse autopilot::screen::size as screen_size;\n\nuse tracing::warn;\n\nuse crate::input::device::{InputDevice, InputDeviceType};\nuse crate::protocol::{Button, KeyboardEvent, KeyboardEventType, PointerEvent, WheelEvent};\n\nuse crate::capturable::{Capturable, Geometry};\n\npub struct AutoPilotDevice {\n    capturable: Box<dyn Capturable>,\n}\n\nimpl AutoPilotDevice {\n    pub fn new(capturable: Box<dyn Capturable>) -> Self {\n        Self { capturable }\n    }\n}\n\nimpl InputDevice for AutoPilotDevice {\n    fn send_wheel_event(&mut self, event: &WheelEvent) {\n        match event.dy {\n            1..=i32::MAX => mouse::scroll(ScrollDirection::Up, 1),\n            i32::MIN..=-1 => mouse::scroll(ScrollDirection::Down, 1),\n            0 => {}\n        }\n    }\n\n    fn send_pointer_event(&mut self, event: &PointerEvent) {\n        if !event.is_primary {\n            return;\n        }\n        if let Err(err) = self.capturable.before_input() {\n            warn!(\"Failed to activate window, sending no input ({})\", err);\n            return;\n        }\n        let (x_rel, y_rel, width_rel, height_rel) = match self.capturable.geometry().unwrap() {\n            Geometry::Relative(x, y, width, height) => (x, y, width, height),\n            #[cfg(target_os = \"windows\")]\n            _ => {\n                warn!(\"Failed to get window geometry, sending no input\");\n                return;\n            }\n        };\n        #[cfg(not(target_os = \"macos\"))]\n        let Size { width, height } = screen_size();\n        #[cfg(target_os = \"macos\")]\n        let (_, _, width, height) = match crate::capturable::core_graphics::screen_coordsys() {\n            Ok(bounds) => bounds,\n            Err(err) => {\n                warn!(\"Could not determine global coordinate system: {}\", err);\n                return;\n            }\n        };\n        if let Err(err) = mouse::move_to(autopilot::geometry::Point::new(\n            (event.x * width_rel + x_rel) * width,\n            (event.y * height_rel + y_rel) * height,\n        )) {\n            warn!(\"Could not move mouse: {}\", err);\n        }\n        match event.button {\n            Button::PRIMARY => {\n                mouse::toggle(mouse::Button::Left, event.buttons.contains(event.button))\n            }\n            Button::AUXILARY => {\n                mouse::toggle(mouse::Button::Middle, event.buttons.contains(event.button))\n            }\n            Button::SECONDARY => {\n                mouse::toggle(mouse::Button::Right, event.buttons.contains(event.button))\n            }\n            _ => (),\n        }\n    }\n\n    fn send_keyboard_event(&mut self, event: &KeyboardEvent) {\n        use autopilot::key::{Character, Code, KeyCode};\n\n        let state = match event.event_type {\n            KeyboardEventType::UP => false,\n            KeyboardEventType::DOWN => true,\n            // autopilot doesn't handle this, so just do nothing\n            KeyboardEventType::REPEAT => return,\n        };\n\n        fn map_key(code: &str) -> Option<KeyCode> {\n            match code {\n                \"Escape\" => Some(KeyCode::Escape),\n                \"Enter\" => Some(KeyCode::Return),\n                \"Backspace\" => Some(KeyCode::Backspace),\n                \"Tab\" => Some(KeyCode::Tab),\n                \"Space\" => Some(KeyCode::Space),\n                \"CapsLock\" => Some(KeyCode::CapsLock),\n                \"F1\" => Some(KeyCode::F1),\n                \"F2\" => Some(KeyCode::F2),\n                \"F3\" => Some(KeyCode::F3),\n                \"F4\" => Some(KeyCode::F4),\n                \"F5\" => Some(KeyCode::F5),\n                \"F6\" => Some(KeyCode::F6),\n                \"F7\" => Some(KeyCode::F7),\n                \"F8\" => Some(KeyCode::F8),\n                \"F9\" => Some(KeyCode::F9),\n                \"F10\" => Some(KeyCode::F10),\n                \"F11\" => Some(KeyCode::F11),\n                \"F12\" => Some(KeyCode::F12),\n                \"F13\" => Some(KeyCode::F13),\n                \"F14\" => Some(KeyCode::F14),\n                \"F15\" => Some(KeyCode::F15),\n                \"F16\" => Some(KeyCode::F16),\n                \"F17\" => Some(KeyCode::F17),\n                \"F18\" => Some(KeyCode::F18),\n                \"F19\" => Some(KeyCode::F19),\n                \"F20\" => Some(KeyCode::F20),\n                \"F21\" => Some(KeyCode::F21),\n                \"F22\" => Some(KeyCode::F22),\n                \"F23\" => Some(KeyCode::F23),\n                \"F24\" => Some(KeyCode::F24),\n                \"Home\" => Some(KeyCode::Home),\n                \"ArrowUp\" => Some(KeyCode::UpArrow),\n                \"PageUp\" => Some(KeyCode::PageUp),\n                \"ArrowLeft\" => Some(KeyCode::LeftArrow),\n                \"ArrowRight\" => Some(KeyCode::RightArrow),\n                \"End\" => Some(KeyCode::End),\n                \"ArrowDown\" => Some(KeyCode::DownArrow),\n                \"PageDown\" => Some(KeyCode::PageDown),\n                \"Delete\" => Some(KeyCode::Delete),\n                \"ControlLeft\" | \"ControlRight\" => Some(KeyCode::Control),\n                \"AltLeft\" | \"AltRight\" => Some(KeyCode::Alt),\n                \"MetaLeft\" | \"MetaRight\" => Some(KeyCode::Meta),\n                \"ShiftLeft\" | \"ShiftRight\" => Some(KeyCode::Shift),\n                _ => None,\n            }\n        }\n        let key = map_key(&event.code);\n        let mut flags = Vec::new();\n        if event.ctrl {\n            flags.push(autopilot::key::Flag::Control);\n        }\n        if event.alt {\n            flags.push(autopilot::key::Flag::Alt);\n        }\n        if event.meta {\n            flags.push(autopilot::key::Flag::Meta);\n        }\n        if event.shift {\n            flags.push(autopilot::key::Flag::Shift);\n        }\n        match key {\n            Some(key) => autopilot::key::toggle(&Code(key), state, &flags, 0),\n            None => {\n                for c in event.key.chars() {\n                    autopilot::key::toggle(&Character(c), state, &flags, 0);\n                }\n            }\n        }\n    }\n\n    fn set_capturable(&mut self, capturable: Box<dyn Capturable>) {\n        self.capturable = capturable;\n    }\n\n    fn device_type(&self) -> InputDeviceType {\n        InputDeviceType::AutoPilotDevice\n    }\n}\n"
  },
  {
    "path": "src/input/autopilot_device_win.rs",
    "content": "use winapi::shared::minwindef::DWORD;\nuse winapi::shared::windef::{HWND, POINT};\nuse winapi::um::winuser::*;\n\nuse tracing::warn;\n\nuse crate::input::autopilot_device::AutoPilotDevice;\nuse crate::input::device::{InputDevice, InputDeviceType};\nuse crate::protocol::{\n    Button, KeyboardEvent, PointerEvent, PointerEventType, PointerType, WheelEvent,\n};\n\nuse crate::capturable::{Capturable, Geometry};\n\npub struct WindowsInput {\n    capturable: Box<dyn Capturable>,\n    autopilot_device: AutoPilotDevice,\n    pointer_device_handle: *mut HSYNTHETICPOINTERDEVICE__,\n    touch_device_handle: *mut HSYNTHETICPOINTERDEVICE__,\n    multitouch_map: std::collections::HashMap<i64, POINTER_TYPE_INFO>,\n}\n\nimpl WindowsInput {\n    pub fn new(capturable: Box<dyn Capturable>) -> Self {\n        unsafe {\n            InitializeTouchInjection(5, TOUCH_FEEDBACK_DEFAULT);\n            Self {\n                capturable: capturable.clone(),\n                autopilot_device: AutoPilotDevice::new(capturable),\n                pointer_device_handle: CreateSyntheticPointerDevice(PT_PEN, 1, 1),\n                touch_device_handle: CreateSyntheticPointerDevice(PT_TOUCH, 5, 1),\n                multitouch_map: std::collections::HashMap::new(),\n            }\n        }\n    }\n}\n\nimpl InputDevice for WindowsInput {\n    fn send_wheel_event(&mut self, event: &WheelEvent) {\n        unsafe { mouse_event(MOUSEEVENTF_WHEEL, 0, 0, event.dy as DWORD, 0) };\n    }\n\n    fn send_pointer_event(&mut self, event: &PointerEvent) {\n        if let Err(err) = self.capturable.before_input() {\n            warn!(\"Failed to activate window, sending no input ({})\", err);\n            return;\n        }\n        let Geometry::VirtualScreen(offset_x, offset_y, width, height, left, top) =\n            self.capturable.geometry().unwrap()\n        else {\n            unreachable!()\n        };\n\n        let (x, y) = (\n            (event.x * width as f64) as i32 + offset_x,\n            (event.y * height as f64) as i32 + offset_y,\n        );\n        let mut pointer_flags = match event.event_type {\n            PointerEventType::DOWN => {\n                POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT | POINTER_FLAG_DOWN\n            }\n            PointerEventType::MOVE | PointerEventType::OVER | PointerEventType::ENTER => {\n                POINTER_FLAG_INRANGE | POINTER_FLAG_UPDATE\n            }\n            PointerEventType::UP => POINTER_FLAG_UP,\n            PointerEventType::CANCEL | PointerEventType::LEAVE | PointerEventType::OUT => {\n                POINTER_FLAG_INRANGE | POINTER_FLAG_UPDATE | POINTER_FLAG_CANCELED\n            }\n        };\n        let button_change_type = match event.buttons {\n            Button::PRIMARY => {\n                pointer_flags |= POINTER_FLAG_INCONTACT;\n                POINTER_CHANGE_FIRSTBUTTON_DOWN\n            }\n            Button::SECONDARY => POINTER_CHANGE_SECONDBUTTON_DOWN,\n            Button::AUXILARY => POINTER_CHANGE_THIRDBUTTON_DOWN,\n            Button::NONE => POINTER_CHANGE_NONE,\n            _ => POINTER_CHANGE_NONE,\n        };\n        if event.is_primary {\n            pointer_flags |= POINTER_FLAG_PRIMARY;\n        }\n        match event.pointer_type {\n            PointerType::Pen => {\n                unsafe {\n                    let mut pointer_type_info = POINTER_TYPE_INFO {\n                        type_: PT_PEN,\n                        u: std::mem::zeroed(),\n                    };\n                    *pointer_type_info.u.penInfo_mut() = POINTER_PEN_INFO {\n                        pointerInfo: POINTER_INFO {\n                            pointerType: PT_PEN,\n                            pointerId: event.pointer_id as u32,\n                            frameId: 0,\n                            pointerFlags: pointer_flags,\n                            sourceDevice: 0 as *mut winapi::ctypes::c_void, //maybe use syntheticPointerDeviceHandle here but works with 0\n                            hwndTarget: 0 as HWND,\n                            ptPixelLocation: POINT { x: x, y: y },\n                            ptHimetricLocation: POINT { x: 0, y: 0 },\n                            ptPixelLocationRaw: POINT { x: x, y: y },\n                            ptHimetricLocationRaw: POINT { x: 0, y: 0 },\n                            dwTime: 0,\n                            historyCount: 1,\n                            InputData: 0,\n                            dwKeyStates: 0,\n                            PerformanceCount: 0,\n                            ButtonChangeType: button_change_type,\n                        },\n                        penFlags: PEN_FLAG_NONE,\n                        penMask: PEN_MASK_PRESSURE\n                            | PEN_MASK_ROTATION\n                            | PEN_MASK_TILT_X\n                            | PEN_MASK_TILT_Y,\n                        pressure: (event.pressure * 1024f64) as u32,\n                        rotation: event.twist as u32,\n                        tiltX: event.tilt_x,\n                        tiltY: event.tilt_y,\n                    };\n                    InjectSyntheticPointerInput(self.pointer_device_handle, &pointer_type_info, 1);\n                }\n            }\n            PointerType::Touch => {\n                unsafe {\n                    let mut pointer_type_info = POINTER_TYPE_INFO {\n                        type_: PT_TOUCH,\n                        u: std::mem::zeroed(),\n                    };\n\n                    let mut pointer_touch_info: POINTER_TOUCH_INFO = std::mem::zeroed();\n                    pointer_touch_info.pointerInfo = std::mem::zeroed();\n                    pointer_touch_info.pointerInfo.pointerType = PT_TOUCH;\n                    pointer_touch_info.pointerInfo.pointerFlags = pointer_flags;\n                    pointer_touch_info.pointerInfo.pointerId = event.pointer_id as u32; //event.pointer_id as u32; Using the actual pointer id causes errors in the touch injection\n                    pointer_touch_info.pointerInfo.ptPixelLocation = POINT { x, y };\n                    pointer_touch_info.touchFlags = TOUCH_FLAG_NONE;\n                    pointer_touch_info.touchMask = TOUCH_MASK_PRESSURE;\n                    pointer_touch_info.pressure = (event.pressure * 1024f64) as u32;\n\n                    pointer_touch_info.pointerInfo.ButtonChangeType = button_change_type;\n\n                    *pointer_type_info.u.touchInfo_mut() = pointer_touch_info;\n                    self.multitouch_map\n                        .insert(event.pointer_id, pointer_type_info);\n                    let len = self.multitouch_map.len();\n\n                    let mut pointer_type_info_vec: Vec<POINTER_TYPE_INFO> = Vec::new();\n                    for (_i, info) in self.multitouch_map.iter().enumerate() {\n                        pointer_type_info_vec.push(*info.1);\n                    }\n                    let b: Box<[POINTER_TYPE_INFO]> = pointer_type_info_vec.into_boxed_slice();\n                    let m: *mut POINTER_TYPE_INFO = Box::into_raw(b) as _;\n\n                    InjectSyntheticPointerInput(self.touch_device_handle, m, len as u32);\n\n                    match event.event_type {\n                        PointerEventType::DOWN\n                        | PointerEventType::MOVE\n                        | PointerEventType::OVER\n                        | PointerEventType::ENTER => {}\n\n                        PointerEventType::UP\n                        | PointerEventType::CANCEL\n                        | PointerEventType::LEAVE\n                        | PointerEventType::OUT => {\n                            self.multitouch_map.remove(&event.pointer_id);\n                        }\n                    }\n                }\n            }\n            PointerType::Mouse => {\n                let mut dw_flags = 0;\n\n                let (screen_x, screen_y) = (\n                    (event.x * width as f64) as i32 + left,\n                    (event.y * height as f64) as i32 + top,\n                );\n\n                match event.event_type {\n                    PointerEventType::DOWN => match event.buttons {\n                        Button::PRIMARY => {\n                            dw_flags |= MOUSEEVENTF_LEFTDOWN;\n                        }\n                        Button::SECONDARY => {\n                            dw_flags |= MOUSEEVENTF_RIGHTDOWN;\n                        }\n                        Button::AUXILARY => {\n                            dw_flags |= MOUSEEVENTF_MIDDLEDOWN;\n                        }\n                        _ => {}\n                    },\n                    PointerEventType::MOVE | PointerEventType::OVER | PointerEventType::ENTER => {\n                        unsafe { SetCursorPos(screen_x, screen_y) };\n                    }\n                    PointerEventType::UP => match event.button {\n                        Button::PRIMARY => {\n                            dw_flags |= MOUSEEVENTF_LEFTUP;\n                        }\n                        Button::SECONDARY => {\n                            dw_flags |= MOUSEEVENTF_RIGHTUP;\n                        }\n                        Button::AUXILARY => {\n                            dw_flags |= MOUSEEVENTF_MIDDLEUP;\n                        }\n                        _ => {}\n                    },\n                    PointerEventType::CANCEL | PointerEventType::LEAVE | PointerEventType::OUT => {\n                        dw_flags |= MOUSEEVENTF_LEFTUP;\n                    }\n                }\n                unsafe { mouse_event(dw_flags, 0 as u32, 0 as u32, 0, 0) };\n            }\n            PointerType::Unknown => todo!(),\n        }\n    }\n\n    fn send_keyboard_event(&mut self, event: &KeyboardEvent) {\n        self.autopilot_device.send_keyboard_event(event);\n    }\n\n    fn set_capturable(&mut self, capturable: Box<dyn Capturable>) {\n        self.capturable = capturable;\n    }\n\n    fn device_type(&self) -> InputDeviceType {\n        InputDeviceType::WindowsInput\n    }\n}\n"
  },
  {
    "path": "src/input/device.rs",
    "content": "use crate::capturable::Capturable;\nuse crate::protocol::{KeyboardEvent, PointerEvent, WheelEvent};\n\n#[derive(PartialEq, Eq)]\npub enum InputDeviceType {\n    AutoPilotDevice,\n    UInputDevice,\n    #[cfg(target_os = \"windows\")]\n    WindowsInput,\n}\n\npub trait InputDevice {\n    fn send_wheel_event(&mut self, event: &WheelEvent);\n    fn send_pointer_event(&mut self, event: &PointerEvent);\n    fn send_keyboard_event(&mut self, event: &KeyboardEvent);\n    fn set_capturable(&mut self, capturable: Box<dyn Capturable>);\n    fn device_type(&self) -> InputDeviceType;\n}\n"
  },
  {
    "path": "src/input/mod.rs",
    "content": "pub mod autopilot_device;\npub mod device;\n\n#[cfg(target_os = \"windows\")]\npub mod autopilot_device_win;\n#[cfg(target_os = \"linux\")]\npub mod uinput_device;\n#[cfg(target_os = \"linux\")]\n#[allow(dead_code)]\npub mod uinput_keys;\n"
  },
  {
    "path": "src/input/uinput_device.rs",
    "content": "use std::cmp::Ordering;\nuse std::ffi::CString;\nuse std::os::raw::{c_char, c_int};\nuse std::time::{Duration, Instant};\n\nuse crate::capturable::x11::X11Context;\nuse crate::capturable::{Capturable, Geometry};\nuse crate::input::device::{InputDevice, InputDeviceType};\nuse crate::protocol::{\n    Button, KeyboardEvent, KeyboardEventType, KeyboardLocation, PointerEvent, PointerEventType,\n    PointerType, Rect, WheelEvent,\n};\n\nuse crate::cerror::CError;\n\nuse tracing::{debug, warn};\n\nextern \"C\" {\n    fn init_uinput_keyboard(name: *const c_char, err: *mut CError) -> c_int;\n    fn init_uinput_stylus(name: *const c_char, err: *mut CError) -> c_int;\n    fn init_uinput_mouse(name: *const c_char, err: *mut CError) -> c_int;\n    fn init_uinput_touch(name: *const c_char, err: *mut CError) -> c_int;\n    fn destroy_uinput_device(fd: c_int);\n    fn send_uinput_event(device: c_int, typ: c_int, code: c_int, value: c_int, err: *mut CError);\n}\n\nstruct MultiTouch {\n    id: i64,\n}\n\npub struct UInputDevice {\n    keyboard_fd: c_int,\n    stylus_fd: c_int,\n    mouse_fd: c_int,\n    touch_fd: c_int,\n    touches: [Option<MultiTouch>; 5],\n    tool_pen_active: bool,\n    pen_touching: bool,\n    last_pen_event: Instant,\n    capturable: Box<dyn Capturable>,\n    geometry: Rect,\n    name_mouse_device: String,\n    name_stylus_device: String,\n    name_touch_device: String,\n    num_mouse_mapping_tries: usize,\n    num_stylus_mapping_tries: usize,\n    num_touch_mapping_tries: usize,\n    x11ctx: Option<X11Context>,\n}\n\nimpl UInputDevice {\n    pub fn new(capturable: Box<dyn Capturable>, id: &Option<String>) -> Result<Self, CError> {\n        let mut suffix = String::new();\n        if let Some(id) = id {\n            suffix = format!(\" - {}\", id);\n        }\n        let mut err = CError::new();\n        let name_stylus = format!(\"Weylus Stylus{}\", suffix);\n        let name_stylus_c_str = CString::new(name_stylus.as_bytes()).unwrap();\n        let stylus_fd = unsafe { init_uinput_stylus(name_stylus_c_str.as_ptr(), &mut err) };\n        if err.is_err() {\n            return Err(err);\n        }\n\n        let name_mouse = format!(\"Weylus Mouse{}\", suffix);\n        let name_mouse_c_str = CString::new(name_mouse.as_bytes()).unwrap();\n        let mouse_fd = unsafe { init_uinput_mouse(name_mouse_c_str.as_ptr(), &mut err) };\n        if err.is_err() {\n            unsafe { destroy_uinput_device(stylus_fd) };\n            return Err(err);\n        }\n\n        let name_touch = format!(\"Weylus Touch{}\", suffix);\n        let name_touch_c_str = CString::new(name_touch.as_bytes()).unwrap();\n        let touch_fd = unsafe { init_uinput_touch(name_touch_c_str.as_ptr(), &mut err) };\n        if err.is_err() {\n            unsafe {\n                destroy_uinput_device(stylus_fd);\n                destroy_uinput_device(mouse_fd);\n            }\n            return Err(err);\n        }\n\n        let name_keyboard = format!(\"Weylus Keyboard{}\", suffix);\n        let name_keyboard_c_str = CString::new(name_keyboard.as_bytes()).unwrap();\n        let keyboard_fd = unsafe { init_uinput_keyboard(name_keyboard_c_str.as_ptr(), &mut err) };\n        if err.is_err() {\n            unsafe {\n                destroy_uinput_device(stylus_fd);\n                destroy_uinput_device(mouse_fd);\n                destroy_uinput_device(touch_fd);\n            }\n            return Err(err);\n        }\n\n        Ok(Self {\n            keyboard_fd,\n            stylus_fd,\n            mouse_fd,\n            touch_fd,\n            touches: Default::default(),\n            tool_pen_active: false,\n            pen_touching: false,\n            last_pen_event: Instant::now(),\n            capturable,\n            geometry: Rect::default(),\n            name_mouse_device: name_mouse,\n            name_touch_device: name_touch,\n            name_stylus_device: name_stylus,\n            num_mouse_mapping_tries: 0,\n            num_stylus_mapping_tries: 0,\n            num_touch_mapping_tries: 0,\n            x11ctx: X11Context::new(),\n        })\n    }\n\n    fn transform_x(&self, x: f64) -> i32 {\n        let x = (x * self.geometry.w + self.geometry.x) * ABS_MAX;\n        x as i32\n    }\n\n    fn transform_y(&self, y: f64) -> i32 {\n        let y = (y * self.geometry.h + self.geometry.y) * ABS_MAX;\n        y as i32\n    }\n\n    fn transform_pressure(&self, p: f64) -> i32 {\n        (p * ABS_MAX) as i32\n    }\n\n    fn transform_touch_size(&self, s: f64) -> i32 {\n        (s * ABS_MAX) as i32\n    }\n\n    fn find_slot(&self, id: i64) -> Option<usize> {\n        self.touches\n            .iter()\n            .enumerate()\n            .find_map(|(slot, mt)| match mt {\n                Some(mt) => {\n                    if mt.id == id {\n                        Some(slot)\n                    } else {\n                        None\n                    }\n                }\n                _ => None,\n            })\n    }\n\n    fn send(&self, fd: c_int, typ: c_int, code: c_int, value: c_int) {\n        let mut err = CError::new();\n        unsafe {\n            send_uinput_event(fd, typ, code, value, &mut err);\n        }\n        if err.is_err() {\n            warn!(\"{}\", err);\n        }\n    }\n}\n\nimpl Drop for UInputDevice {\n    fn drop(&mut self) {\n        unsafe {\n            destroy_uinput_device(self.keyboard_fd);\n            destroy_uinput_device(self.stylus_fd);\n            destroy_uinput_device(self.mouse_fd);\n            destroy_uinput_device(self.touch_fd);\n        };\n    }\n}\n\n// Event Types\nconst ET_SYNC: c_int = 0x00;\nconst ET_KEY: c_int = 0x01;\nconst ET_RELATIVE: c_int = 0x02;\nconst ET_ABSOLUTE: c_int = 0x03;\nconst ET_MSC: c_int = 0x04;\n\n// Event Codes\nconst EC_SYNC_REPORT: c_int = 0;\n\nconst EC_KEY_MOUSE_LEFT: c_int = 0x110;\nconst EC_KEY_MOUSE_RIGHT: c_int = 0x111;\nconst EC_KEY_MOUSE_MIDDLE: c_int = 0x112;\nconst EC_KEY_TOOL_PEN: c_int = 0x140;\nconst EC_KEY_TOOL_RUBBER: c_int = 0x141;\nconst EC_KEY_TOUCH: c_int = 0x14a;\nconst EC_KEY_TOOL_FINGER: c_int = 0x145;\nconst EC_KEY_TOOL_DOUBLETAP: c_int = 0x14d;\nconst EC_KEY_TOOL_TRIPLETAP: c_int = 0x14e;\nconst EC_KEY_TOOL_QUADTAP: c_int = 0x14f; /* Four fingers on trackpad */\nconst EC_KEY_TOOL_QUINTTAP: c_int = 0x148; /* Five fingers on trackpad */\n//const EC_RELATIVE_X: c_int = 0x00;\n//const EC_RELATIVE_Y: c_int = 0x01;\n\nconst EC_REL_HWHEEL: c_int = 0x06;\nconst EC_REL_WHEEL: c_int = 0x08;\nconst EC_REL_WHEEL_HI_RES: c_int = 0x0b;\nconst EC_REL_HWHEEL_HI_RES: c_int = 0x0c;\n\nconst EC_ABSOLUTE_X: c_int = 0x00;\nconst EC_ABSOLUTE_Y: c_int = 0x01;\nconst EC_ABSOLUTE_PRESSURE: c_int = 0x18;\nconst EC_ABSOLUTE_TILT_X: c_int = 0x1a;\nconst EC_ABSOLUTE_TILT_Y: c_int = 0x1b;\nconst EC_ABS_MT_SLOT: c_int = 0x2f; /* MT slot being modified */\nconst EC_ABS_MT_TOUCH_MAJOR: c_int = 0x30; /* Major axis of touching ellipse */\nconst EC_ABS_MT_TOUCH_MINOR: c_int = 0x31; /* Minor axis (omit if circular) */\nconst EC_ABS_MT_ORIENTATION: c_int = 0x34; /* Ellipse orientation */\nconst EC_ABS_MT_POSITION_X: c_int = 0x35; /* Center X touch position */\nconst EC_ABS_MT_POSITION_Y: c_int = 0x36; /* Center Y touch position */\nconst EC_ABS_MT_TRACKING_ID: c_int = 0x39; /* Unique ID of initiated contact */\nconst EC_ABS_MT_PRESSURE: c_int = 0x3a; /* Pressure on contact area */\n\nconst EC_MSC_TIMESTAMP: c_int = 0x05;\n\n// This is choosen somewhat arbitrarily\n// describes maximum value for ABS_X, ABS_Y, ABS_...\n// This corresponds to PointerEvent values of 1.0\nconst ABS_MAX: f64 = 65535.0;\n\n// This specifies how many times it should be attempted to map the input devices created via uinput\n// to the entire screen and not only a single monitor. Actually this is a workaround because\n// apparently it is impossible to set the correct mapping in a sane way. The reason is that X needs\n// some time to register new input devices, which makes it impossible to configure them right after\n// creation as the devices won't be available for configuration at that time. This means one has to\n// wait an unspecified amount of time until the devices show up. But just sleeping for example 3\n// seconds does not solve the issue either because the input device for the stylus does not show up\n// if there has not been any input. As a matter of fact things are even more compilcated as for\n// some reason the stylus device created via uinput creates two devices for X. One can not be\n// mapped to the screen (this is the device that shows up with out the need to send actual inputs\n// via uinput) and another one that can be mapped to the screen. But this is the device that\n// requires sending inputs via uinput first other wise it does not show up. This is why this crude\n// method of just setting the mapping forcefully on the first MAX_SCREEN_MAPPING_TRIES input events\n// has been choosen. If anyone knows a better solution: PLEASE FIX THIS!\nconst MAX_SCREEN_MAPPING_TRIES: usize = 100;\n\nimpl InputDevice for UInputDevice {\n    fn send_wheel_event(&mut self, event: &WheelEvent) {\n        if let Err(err) = self.capturable.before_input() {\n            warn!(\"Failed to activate window, sending no input ({})\", err);\n            return;\n        }\n\n        fn direction(d: i32) -> i32 {\n            match d.cmp(&0) {\n                Ordering::Equal => 0,\n                Ordering::Less => -1,\n                Ordering::Greater => 1,\n            }\n        }\n\n        self.send(\n            self.mouse_fd,\n            ET_RELATIVE,\n            EC_REL_WHEEL,\n            direction(event.dy),\n        );\n        self.send(\n            self.mouse_fd,\n            ET_RELATIVE,\n            EC_REL_HWHEEL,\n            direction(event.dx),\n        );\n        self.send(self.mouse_fd, ET_RELATIVE, EC_REL_WHEEL_HI_RES, event.dy);\n        self.send(self.mouse_fd, ET_RELATIVE, EC_REL_HWHEEL_HI_RES, event.dx);\n\n        self.send(\n            self.mouse_fd,\n            ET_MSC,\n            EC_MSC_TIMESTAMP,\n            (event.timestamp % (i32::MAX as u64 + 1)) as i32,\n        );\n        self.send(self.mouse_fd, ET_SYNC, EC_SYNC_REPORT, 0);\n    }\n\n    fn send_pointer_event(&mut self, event: &PointerEvent) {\n        if let Err(err) = self.capturable.before_input() {\n            warn!(\"Failed to activate window, sending no input ({})\", err);\n            return;\n        }\n        let (x, y, width, height) = match self.capturable.geometry().unwrap() {\n            Geometry::Relative(x, y, width, height) => (x, y, width, height),\n        };\n        self.geometry.x = x;\n        self.geometry.y = y;\n        self.geometry.w = width;\n        self.geometry.h = height;\n        match event.pointer_type {\n            PointerType::Touch => {\n                if self.num_touch_mapping_tries < MAX_SCREEN_MAPPING_TRIES {\n                    if let Some(x11ctx) = &mut self.x11ctx {\n                        // Mapping input does not work on XWayland as xinput list does not expose\n                        // device names and thus we can not identify the devices created by uinput\n                        if let Ok(session_type) = std::env::var(\"XDG_SESSION_TYPE\") {\n                            if session_type != \"wayland\" {\n                                x11ctx.map_input_device_to_entire_screen(\n                                    &self.name_touch_device,\n                                    false,\n                                );\n                            }\n                        } else {\n                            x11ctx\n                                .map_input_device_to_entire_screen(&self.name_touch_device, false);\n                        }\n                    }\n                    self.num_touch_mapping_tries += 1;\n                }\n\n                // This is a workaround for browsers that send events when the pen is hovering but\n                // do not send an event when the pen leaves the hovering range. If the pen is left\n                // in this state touch rejection may stay active and touch won't work.\n                // Therefore, we manually remove the pen after a short delay.\n                if self.tool_pen_active\n                    && !self.pen_touching\n                    && (Instant::now() - self.last_pen_event) > Duration::from_millis(50)\n                {\n                    self.tool_pen_active = false;\n                    self.send(self.stylus_fd, ET_KEY, EC_KEY_TOUCH, 0);\n                    self.send(self.stylus_fd, ET_KEY, EC_KEY_TOOL_PEN, 0);\n                    self.send(self.stylus_fd, ET_KEY, EC_KEY_TOOL_RUBBER, 0);\n                    self.send(self.stylus_fd, ET_ABSOLUTE, EC_ABSOLUTE_PRESSURE, 0);\n                    self.send(\n                        self.stylus_fd,\n                        ET_MSC,\n                        EC_MSC_TIMESTAMP,\n                        (event.timestamp % (i32::MAX as u64 + 1)) as i32,\n                    );\n                    self.send(self.stylus_fd, ET_SYNC, EC_SYNC_REPORT, 0);\n                }\n                match event.event_type {\n                    PointerEventType::DOWN\n                    | PointerEventType::MOVE\n                    | PointerEventType::OVER\n                    | PointerEventType::ENTER => {\n                        let slot: usize;\n                        // check if this event is already assigned to one of our 10 multitouch slots\n                        if let Some(s) = self.find_slot(event.pointer_id) {\n                            slot = s;\n                        } else {\n                            // this event is not assigned to a slot, lets try to do so now\n                            // find the first unused slot\n                            if let Some(s) =\n                                self.touches\n                                    .iter()\n                                    .enumerate()\n                                    .find_map(|(slot, mt)| match mt {\n                                        None => Some(slot),\n                                        Some(_) => None,\n                                    })\n                            {\n                                slot = s;\n                                self.touches[slot] = Some(MultiTouch {\n                                    id: event.pointer_id,\n                                })\n                            } else {\n                                // out of slots, do nothing\n                                return;\n                            }\n                        };\n                        self.send(self.touch_fd, ET_ABSOLUTE, EC_ABS_MT_SLOT, slot as i32);\n                        self.send(\n                            self.touch_fd,\n                            ET_ABSOLUTE,\n                            EC_ABS_MT_TRACKING_ID,\n                            slot as i32,\n                        );\n\n                        if let PointerEventType::DOWN = event.event_type {\n                            self.send(self.touch_fd, ET_KEY, EC_KEY_TOUCH, 1);\n                            match slot {\n                                1 => self.send(self.touch_fd, ET_KEY, EC_KEY_TOOL_FINGER, 0),\n                                2 => self.send(self.touch_fd, ET_KEY, EC_KEY_TOOL_DOUBLETAP, 0),\n                                3 => self.send(self.touch_fd, ET_KEY, EC_KEY_TOOL_TRIPLETAP, 0),\n                                4 => self.send(self.touch_fd, ET_KEY, EC_KEY_TOOL_QUADTAP, 0),\n                                _ => self.send(self.touch_fd, ET_KEY, EC_KEY_TOOL_QUINTTAP, 0),\n                            }\n                            match slot {\n                                1 => self.send(self.touch_fd, ET_KEY, EC_KEY_TOOL_DOUBLETAP, 1),\n                                2 => self.send(self.touch_fd, ET_KEY, EC_KEY_TOOL_TRIPLETAP, 1),\n                                3 => self.send(self.touch_fd, ET_KEY, EC_KEY_TOOL_QUADTAP, 1),\n                                4 => self.send(self.touch_fd, ET_KEY, EC_KEY_TOOL_QUINTTAP, 1),\n                                _ => self.send(self.touch_fd, ET_KEY, EC_KEY_TOOL_FINGER, 1),\n                            }\n                        }\n                        self.send(\n                            self.touch_fd,\n                            ET_ABSOLUTE,\n                            EC_ABS_MT_PRESSURE,\n                            self.transform_pressure(event.pressure),\n                        );\n                        let major: i32;\n                        let minor: i32;\n                        let orientation = if event.height >= event.width {\n                            major = self.transform_touch_size(event.height);\n                            minor = self.transform_touch_size(event.width);\n                            0\n                        } else {\n                            major = self.transform_touch_size(event.width);\n                            minor = self.transform_touch_size(event.height);\n                            1\n                        };\n                        self.send(self.touch_fd, ET_ABSOLUTE, EC_ABS_MT_TOUCH_MAJOR, major);\n                        self.send(self.touch_fd, ET_ABSOLUTE, EC_ABS_MT_TOUCH_MINOR, minor);\n                        self.send(\n                            self.touch_fd,\n                            ET_ABSOLUTE,\n                            EC_ABS_MT_ORIENTATION,\n                            orientation,\n                        );\n                        self.send(\n                            self.touch_fd,\n                            ET_ABSOLUTE,\n                            EC_ABS_MT_POSITION_X,\n                            self.transform_x(event.x),\n                        );\n                        self.send(\n                            self.touch_fd,\n                            ET_ABSOLUTE,\n                            EC_ABS_MT_POSITION_Y,\n                            self.transform_y(event.y),\n                        );\n                        self.send(\n                            self.touch_fd,\n                            ET_ABSOLUTE,\n                            EC_ABSOLUTE_X,\n                            self.transform_x(event.x),\n                        );\n                        self.send(\n                            self.touch_fd,\n                            ET_ABSOLUTE,\n                            EC_ABSOLUTE_Y,\n                            self.transform_y(event.y),\n                        );\n                        self.send(\n                            self.touch_fd,\n                            ET_MSC,\n                            EC_MSC_TIMESTAMP,\n                            (event.timestamp % (i32::MAX as u64 + 1)) as i32,\n                        );\n                        self.send(self.touch_fd, ET_SYNC, EC_SYNC_REPORT, 0);\n                    }\n                    PointerEventType::CANCEL\n                    | PointerEventType::UP\n                    | PointerEventType::LEAVE\n                    | PointerEventType::OUT => {\n                        // remove from slot\n                        if let Some(slot) = self.find_slot(event.pointer_id) {\n                            self.send(self.touch_fd, ET_ABSOLUTE, EC_ABS_MT_SLOT, slot as i32);\n                            self.send(self.touch_fd, ET_ABSOLUTE, EC_ABS_MT_TRACKING_ID, -1);\n                            self.send(self.touch_fd, ET_KEY, EC_KEY_TOUCH, 0);\n                            self.send(self.touch_fd, ET_KEY, EC_KEY_TOOL_FINGER, 0);\n                            match slot {\n                                1 => self.send(self.touch_fd, ET_KEY, EC_KEY_TOOL_DOUBLETAP, 0),\n                                2 => self.send(self.touch_fd, ET_KEY, EC_KEY_TOOL_TRIPLETAP, 0),\n                                3 => self.send(self.touch_fd, ET_KEY, EC_KEY_TOOL_QUADTAP, 0),\n                                4 => self.send(self.touch_fd, ET_KEY, EC_KEY_TOOL_QUINTTAP, 0),\n                                _ => (),\n                            }\n                            self.send(\n                                self.touch_fd,\n                                ET_MSC,\n                                EC_MSC_TIMESTAMP,\n                                (event.timestamp % (i32::MAX as u64 + 1)) as i32,\n                            );\n                            self.send(self.touch_fd, ET_SYNC, EC_SYNC_REPORT, 0);\n                            self.touches[slot] = None;\n                        }\n                    }\n                };\n            }\n            PointerType::Pen => {\n                self.last_pen_event = Instant::now();\n                if self.num_stylus_mapping_tries < MAX_SCREEN_MAPPING_TRIES {\n                    if let Some(x11ctx) = &mut self.x11ctx {\n                        // Mapping input does not work on XWayland as xinput list does not expose\n                        // device names and thus we can not identify the devices created by uinput\n                        if let Ok(session_type) = std::env::var(\"XDG_SESSION_TYPE\") {\n                            if session_type != \"wayland\" {\n                                x11ctx.map_input_device_to_entire_screen(\n                                    &self.name_stylus_device,\n                                    true,\n                                );\n                            }\n                        } else {\n                            x11ctx\n                                .map_input_device_to_entire_screen(&self.name_stylus_device, true);\n                        }\n                    }\n                    self.num_touch_mapping_tries += 1;\n                }\n                match event.event_type {\n                    PointerEventType::DOWN\n                    | PointerEventType::MOVE\n                    | PointerEventType::OVER\n                    | PointerEventType::ENTER => {\n                        if let PointerEventType::DOWN = event.event_type {\n                            self.pen_touching = true;\n                            self.send(self.stylus_fd, ET_KEY, EC_KEY_TOUCH, 1);\n                        }\n                        if !self.tool_pen_active && !event.buttons.contains(Button::ERASER) {\n                            self.send(self.stylus_fd, ET_KEY, EC_KEY_TOOL_PEN, 1);\n                            self.send(self.stylus_fd, ET_KEY, EC_KEY_TOOL_RUBBER, 0);\n                            self.tool_pen_active = true;\n                        }\n                        if let Button::ERASER = event.button {\n                            self.send(self.stylus_fd, ET_KEY, EC_KEY_TOOL_PEN, 0);\n                            self.send(self.stylus_fd, ET_KEY, EC_KEY_TOOL_RUBBER, 1);\n                            self.tool_pen_active = false;\n                        }\n                        self.send(\n                            self.stylus_fd,\n                            ET_ABSOLUTE,\n                            EC_ABSOLUTE_X,\n                            self.transform_x(event.x),\n                        );\n                        self.send(\n                            self.stylus_fd,\n                            ET_ABSOLUTE,\n                            EC_ABSOLUTE_Y,\n                            self.transform_y(event.y),\n                        );\n                        self.send(\n                            self.stylus_fd,\n                            ET_ABSOLUTE,\n                            EC_ABSOLUTE_PRESSURE,\n                            if self.pen_touching {\n                                self.transform_pressure(event.pressure)\n                            } else {\n                                0\n                            },\n                        );\n                        self.send(\n                            self.stylus_fd,\n                            ET_ABSOLUTE,\n                            EC_ABSOLUTE_TILT_X,\n                            event.tilt_x,\n                        );\n                        self.send(\n                            self.stylus_fd,\n                            ET_ABSOLUTE,\n                            EC_ABSOLUTE_TILT_Y,\n                            event.tilt_y,\n                        );\n                    }\n                    PointerEventType::UP\n                    | PointerEventType::CANCEL\n                    | PointerEventType::LEAVE\n                    | PointerEventType::OUT => {\n                        self.send(self.stylus_fd, ET_KEY, EC_KEY_TOUCH, 0);\n                        self.send(self.stylus_fd, ET_KEY, EC_KEY_TOOL_PEN, 0);\n                        self.send(self.stylus_fd, ET_KEY, EC_KEY_TOOL_RUBBER, 0);\n                        self.send(self.stylus_fd, ET_ABSOLUTE, EC_ABSOLUTE_PRESSURE, 0);\n                        self.tool_pen_active = false;\n                        self.pen_touching = false;\n                    }\n                }\n                self.send(\n                    self.stylus_fd,\n                    ET_MSC,\n                    EC_MSC_TIMESTAMP,\n                    (event.timestamp % (i32::MAX as u64 + 1)) as i32,\n                );\n                self.send(self.stylus_fd, ET_SYNC, EC_SYNC_REPORT, 0);\n            }\n            PointerType::Mouse | PointerType::Unknown => {\n                if self.num_mouse_mapping_tries < MAX_SCREEN_MAPPING_TRIES {\n                    if let Some(x11ctx) = &mut self.x11ctx {\n                        // Mapping input does not work on XWayland as xinput list does not expose\n                        // device names and thus we can not identify the devices created by uinput\n                        if let Ok(session_type) = std::env::var(\"XDG_SESSION_TYPE\") {\n                            if session_type != \"wayland\" {\n                                x11ctx.map_input_device_to_entire_screen(\n                                    &self.name_mouse_device,\n                                    false,\n                                );\n                            }\n                        } else {\n                            x11ctx\n                                .map_input_device_to_entire_screen(&self.name_mouse_device, false);\n                        }\n                    }\n                    self.num_touch_mapping_tries += 1;\n                }\n                match event.event_type {\n                    PointerEventType::DOWN\n                    | PointerEventType::MOVE\n                    | PointerEventType::OVER\n                    | PointerEventType::ENTER => {\n                        if let PointerEventType::DOWN = event.event_type {\n                            match event.button {\n                                Button::PRIMARY => {\n                                    self.send(self.mouse_fd, ET_KEY, EC_KEY_MOUSE_LEFT, 1)\n                                }\n                                Button::SECONDARY => {\n                                    self.send(self.mouse_fd, ET_KEY, EC_KEY_MOUSE_RIGHT, 1)\n                                }\n                                Button::AUXILARY => {\n                                    self.send(self.mouse_fd, ET_KEY, EC_KEY_MOUSE_MIDDLE, 1)\n                                }\n                                _ => (),\n                            }\n                        }\n                        self.send(\n                            self.mouse_fd,\n                            ET_ABSOLUTE,\n                            EC_ABSOLUTE_X,\n                            self.transform_x(event.x),\n                        );\n                        self.send(\n                            self.mouse_fd,\n                            ET_ABSOLUTE,\n                            EC_ABSOLUTE_Y,\n                            self.transform_y(event.y),\n                        );\n                    }\n                    PointerEventType::UP\n                    | PointerEventType::CANCEL\n                    | PointerEventType::LEAVE\n                    | PointerEventType::OUT => match event.button {\n                        Button::PRIMARY => self.send(self.mouse_fd, ET_KEY, EC_KEY_MOUSE_LEFT, 0),\n                        Button::SECONDARY => {\n                            self.send(self.mouse_fd, ET_KEY, EC_KEY_MOUSE_RIGHT, 0)\n                        }\n                        Button::AUXILARY => {\n                            self.send(self.mouse_fd, ET_KEY, EC_KEY_MOUSE_MIDDLE, 0)\n                        }\n                        _ => (),\n                    },\n                }\n                self.send(\n                    self.mouse_fd,\n                    ET_MSC,\n                    EC_MSC_TIMESTAMP,\n                    (event.timestamp % (i32::MAX as u64 + 1)) as i32,\n                );\n                self.send(self.mouse_fd, ET_SYNC, EC_SYNC_REPORT, 0);\n            }\n        }\n    }\n\n    fn send_keyboard_event(&mut self, event: &KeyboardEvent) {\n        use crate::input::uinput_keys::*;\n        if let Err(err) = self.capturable.before_input() {\n            warn!(\"Failed to activate window, sending no input ({})\", err);\n            return;\n        }\n        fn map_key(code: &str, location: &KeyboardLocation) -> c_int {\n            match (code, location) {\n                (\"Escape\", _) => KEY_ESC,\n                (\"Digit0\", KeyboardLocation::NUMPAD) => KEY_KP0,\n                (\"Digit1\", KeyboardLocation::NUMPAD) => KEY_KP1,\n                (\"Digit2\", KeyboardLocation::NUMPAD) => KEY_KP2,\n                (\"Digit3\", KeyboardLocation::NUMPAD) => KEY_KP3,\n                (\"Digit4\", KeyboardLocation::NUMPAD) => KEY_KP4,\n                (\"Digit5\", KeyboardLocation::NUMPAD) => KEY_KP5,\n                (\"Digit6\", KeyboardLocation::NUMPAD) => KEY_KP6,\n                (\"Digit7\", KeyboardLocation::NUMPAD) => KEY_KP7,\n                (\"Digit8\", KeyboardLocation::NUMPAD) => KEY_KP8,\n                (\"Digit9\", KeyboardLocation::NUMPAD) => KEY_KP9,\n                (\"Minus\", KeyboardLocation::NUMPAD) => KEY_KPMINUS,\n                (\"Equal\", KeyboardLocation::NUMPAD) => KEY_KPEQUAL,\n                (\"Enter\", KeyboardLocation::NUMPAD) => KEY_KPENTER,\n                (\"Digit0\", _) => KEY_0,\n                (\"Digit1\", _) => KEY_1,\n                (\"Digit2\", _) => KEY_2,\n                (\"Digit3\", _) => KEY_3,\n                (\"Digit4\", _) => KEY_4,\n                (\"Digit5\", _) => KEY_5,\n                (\"Digit6\", _) => KEY_6,\n                (\"Digit7\", _) => KEY_7,\n                (\"Digit8\", _) => KEY_8,\n                (\"Digit9\", _) => KEY_9,\n                (\"Minus\", _) => KEY_MINUS,\n                (\"Equal\", _) => KEY_EQUAL,\n                (\"Enter\", _) => KEY_ENTER,\n                (\"Backspace\", _) => KEY_BACKSPACE,\n                (\"Tab\", _) => KEY_TAB,\n                (\"KeyA\", _) => KEY_A,\n                (\"KeyB\", _) => KEY_B,\n                (\"KeyC\", _) => KEY_C,\n                (\"KeyD\", _) => KEY_D,\n                (\"KeyE\", _) => KEY_E,\n                (\"KeyF\", _) => KEY_F,\n                (\"KeyG\", _) => KEY_G,\n                (\"KeyH\", _) => KEY_H,\n                (\"KeyI\", _) => KEY_I,\n                (\"KeyJ\", _) => KEY_J,\n                (\"KeyK\", _) => KEY_K,\n                (\"KeyL\", _) => KEY_L,\n                (\"KeyM\", _) => KEY_M,\n                (\"KeyN\", _) => KEY_N,\n                (\"KeyO\", _) => KEY_O,\n                (\"KeyP\", _) => KEY_P,\n                (\"KeyQ\", _) => KEY_Q,\n                (\"KeyR\", _) => KEY_R,\n                (\"KeyS\", _) => KEY_S,\n                (\"KeyT\", _) => KEY_T,\n                (\"KeyU\", _) => KEY_U,\n                (\"KeyV\", _) => KEY_V,\n                (\"KeyW\", _) => KEY_W,\n                (\"KeyX\", _) => KEY_X,\n                (\"KeyY\", _) => KEY_Y,\n                (\"KeyZ\", _) => KEY_Z,\n                (\"BracketLeft\", _) => KEY_LEFTBRACE,\n                (\"BracketRight\", _) => KEY_RIGHTBRACE,\n                (\"Semicolon\", _) => KEY_SEMICOLON,\n                (\"Quote\", _) => KEY_APOSTROPHE,\n                (\"Backquote\", _) => KEY_GRAVE,\n                (\"Backslash\", _) => KEY_BACKSLASH,\n                (\"Comma\", _) => KEY_COMMA,\n                (\"Period\", _) => KEY_DOT,\n                (\"Slash\", _) => KEY_SLASH,\n                (\"Space\", _) => KEY_SPACE,\n                (\"CapsLock\", _) => KEY_CAPSLOCK,\n                (\"NumpadMultiply\", _) => KEY_KPASTERISK,\n                (\"F1\", _) => KEY_F1,\n                (\"F2\", _) => KEY_F2,\n                (\"F3\", _) => KEY_F3,\n                (\"F4\", _) => KEY_F4,\n                (\"F5\", _) => KEY_F5,\n                (\"F6\", _) => KEY_F6,\n                (\"F7\", _) => KEY_F7,\n                (\"F8\", _) => KEY_F8,\n                (\"F9\", _) => KEY_F9,\n                (\"F10\", _) => KEY_F10,\n                (\"F11\", _) => KEY_F11,\n                (\"F12\", _) => KEY_F12,\n                (\"F13\", _) => KEY_F13,\n                (\"F14\", _) => KEY_F14,\n                (\"F15\", _) => KEY_F15,\n                (\"F16\", _) => KEY_F16,\n                (\"F17\", _) => KEY_F17,\n                (\"F18\", _) => KEY_F18,\n                (\"F19\", _) => KEY_F19,\n                (\"F20\", _) => KEY_F20,\n                (\"F21\", _) => KEY_F21,\n                (\"F22\", _) => KEY_F22,\n                (\"F23\", _) => KEY_F23,\n                (\"F24\", _) => KEY_F24,\n                (\"NumLock\", _) => KEY_NUMLOCK,\n                (\"ScrollLock\", _) => KEY_SCROLLLOCK,\n                (\"Numpad0\", _) => KEY_KP0,\n                (\"Numpad1\", _) => KEY_KP1,\n                (\"Numpad2\", _) => KEY_KP2,\n                (\"Numpad3\", _) => KEY_KP3,\n                (\"Numpad4\", _) => KEY_KP4,\n                (\"Numpad5\", _) => KEY_KP5,\n                (\"Numpad6\", _) => KEY_KP6,\n                (\"Numpad7\", _) => KEY_KP7,\n                (\"Numpad8\", _) => KEY_KP8,\n                (\"Numpad9\", _) => KEY_KP9,\n                (\"NumpadSubtract\", _) => KEY_KPMINUS,\n                (\"NumpadAdd\", _) => KEY_KPPLUS,\n                // (\"NumpadDecimal\", _) => ?,\n                (\"IntlBackslash\", _) => KEY_102ND,\n                (\"IntlRo\", _) => KEY_RO,\n                (\"NumpadEnter\", _) => KEY_KPENTER,\n                (\"NumpadDivide\", _) => KEY_KPSLASH,\n                (\"NumpadEqual\", _) => KEY_KPEQUAL,\n                (\"NumpadComma\", _) => KEY_KPCOMMA,\n                (\"NumpadParenLeft\", _) => KEY_KPLEFTPAREN,\n                (\"NumpadParenRight\", _) => KEY_KPRIGHTPAREN,\n                // (\"NumpadChangeSign\", _) => ?,\n                // (\"Convert\", _) => ?,\n                (\"KanaMode\", _) => KEY_KATAKANA,\n                // (\"NonConvert\", _) => ?,\n                (\"PrintScreen\", _) => KEY_SYSRQ,\n                (\"Home\", _) => KEY_HOME,\n                (\"ArrowUp\", _) => KEY_UP,\n                (\"PageUp\", _) => KEY_PAGEUP,\n                (\"ArrowLeft\", _) => KEY_LEFT,\n                (\"ArrowRight\", _) => KEY_RIGHT,\n                (\"End\", _) => KEY_END,\n                (\"ArrowDown\", _) => KEY_DOWN,\n                (\"PageDown\", _) => KEY_PAGEDOWN,\n                (\"Insert\", _) => KEY_INSERT,\n                (\"Delete\", _) => KEY_DELETE,\n                (\"VolumeMute\", _) | (\"AudioVolumeMute\", _) => KEY_MUTE,\n                (\"VolumeDown\", _) | (\"AudioVolumeDown\", _) => KEY_VOLUMEDOWN,\n                (\"VolumeUp\", _) | (\"AudioVolumeUp\", _) => KEY_VOLUMEUP,\n                (\"Pause\", _) => KEY_PAUSE,\n\n                (\"Lang1\", _) => KEY_HANGUEL,\n                (\"Lang2\", _) => KEY_HANJA,\n                (\"IntlYen\", _) => KEY_YEN,\n                (\"OSLeft\", _) => KEY_LEFTMETA,\n                (\"OSRight\", _) => KEY_RIGHTMETA,\n                (\"ContextMenu\", _) => KEY_MENU,\n                // (\"BrowserStop\", _) => ?,\n                (\"Cancel\", _) => KEY_CANCEL,\n                (\"Again\", _) => KEY_AGAIN,\n                (\"Props\", _) => KEY_PROPS,\n                (\"Undo\", _) => KEY_UNDO,\n                // (\"Select\", _) => ?,\n                (\"Copy\", _) => KEY_COPY,\n                (\"Open\", _) => KEY_OPEN,\n                (\"Paste\", _) => KEY_PASTE,\n                (\"Find\", _) => KEY_FIND,\n                (\"Cut\", _) => KEY_CUT,\n                (\"Help\", _) => KEY_HELP,\n                // (\"LaunchApp2\", _) => ?,\n                // (\"LaunchApp1\", _) => ,\n                (\"LaunchMail\", _) => KEY_MAIL,\n                // (\"BrowserFavorites\", _) => ?,\n                // (\"BrowserBack\", _) => ?,\n                // (\"BrowserForward\", _) => ?,\n                (\"Eject\", _) => KEY_EJECTCD,\n                (\"MediaTrackNext\", _) => KEY_NEXTSONG,\n                (\"MediaPlayPause\", _) => KEY_PLAYPAUSE,\n                (\"MediaTrackPrevious\", _) => KEY_PREVIOUSSONG,\n                (\"MediaStop\", _) => KEY_STOPCD,\n                (\"MediaSelect\", _) | (\"LaunchMediaPlayer\", _) => KEY_MEDIA,\n                // (\"BrowserHome\", _) => ?,\n                // (\"BrowserRefresh\", _) => ?,\n                // (\"BrowserSearch\", _) => ?,\n                (\"Power\", _) => KEY_POWER,\n                (\"Sleep\", _) => KEY_SLEEP,\n                (\"WakeUp\", _) => KEY_WAKEUP,\n                (\"ControlLeft\", _) => KEY_LEFTCTRL,\n                (\"ControlRight\", _) => KEY_RIGHTCTRL,\n                (\"AltLeft\", _) => KEY_LEFTALT,\n                (\"AltRight\", _) => KEY_RIGHTALT,\n                (\"MetaLeft\", _) => KEY_LEFTMETA,\n                (\"MetaRight\", _) => KEY_RIGHTMETA,\n                (\"ShiftLeft\", _) => KEY_LEFTSHIFT,\n                (\"ShiftRight\", _) => KEY_RIGHTSHIFT,\n                _ => KEY_UNKNOWN,\n            }\n        }\n\n        let key_code: c_int = map_key(&event.code, &event.location);\n        let state: c_int = match event.event_type {\n            KeyboardEventType::UP => 0,\n            KeyboardEventType::DOWN => 1,\n            KeyboardEventType::REPEAT => 2,\n        };\n\n        if key_code == KEY_UNKNOWN {\n            if let KeyboardEventType::DOWN = event.event_type {\n                if !event.key.is_empty() {\n                    // If the key is unknow try inserting the unicode character directly\n                    // to do so use CTRL + SHIFT + U + UTF16 HEX of the unicode point.\n                    let unicode_keys = event\n                        .key\n                        .encode_utf16()\n                        .map(|b| format!(\"{:X}\", b))\n                        .collect::<Vec<String>>()\n                        .concat();\n\n                    debug!(\n                        \"Got unknown key: {} code: {}, trying to insert unicode using ctrl + \\\n                        shift + u + {}!\",\n                        event.code, event.key, unicode_keys\n                    );\n\n                    self.send(self.keyboard_fd, ET_KEY, KEY_LEFTCTRL, 1);\n                    self.send(self.keyboard_fd, ET_KEY, KEY_LEFTSHIFT, 1);\n                    self.send(self.keyboard_fd, ET_KEY, KEY_U, 1);\n                    self.send(self.keyboard_fd, ET_SYNC, EC_SYNC_REPORT, 0);\n                    for c in unicode_keys.chars() {\n                        let key_code = if c.is_alphabetic() {\n                            map_key(&format!(\"Key{}\", c), &KeyboardLocation::STANDARD)\n                        } else {\n                            map_key(&format!(\"Digit{}\", c), &KeyboardLocation::STANDARD)\n                        };\n\n                        self.send(self.keyboard_fd, ET_KEY, key_code, 1);\n                        self.send(self.keyboard_fd, ET_SYNC, EC_SYNC_REPORT, 0);\n                        self.send(self.keyboard_fd, ET_KEY, key_code, 0);\n                        self.send(self.keyboard_fd, ET_SYNC, EC_SYNC_REPORT, 0);\n                    }\n                    self.send(self.keyboard_fd, ET_KEY, KEY_LEFTCTRL, 0);\n                    self.send(self.keyboard_fd, ET_KEY, KEY_LEFTSHIFT, 0);\n                    self.send(self.keyboard_fd, ET_KEY, KEY_U, 0);\n                    self.send(self.keyboard_fd, ET_SYNC, EC_SYNC_REPORT, 0);\n                }\n            } else {\n                debug!(\n                    \"Got unknow key: code: {} key: {}, ignoring event.\",\n                    event.code, event.key\n                );\n            }\n            return;\n        }\n\n        if event.ctrl {\n            self.send(self.keyboard_fd, ET_KEY, KEY_LEFTCTRL, state);\n            self.send(self.keyboard_fd, ET_SYNC, EC_SYNC_REPORT, 0);\n        }\n        if event.alt {\n            self.send(self.keyboard_fd, ET_KEY, KEY_LEFTALT, state);\n            self.send(self.keyboard_fd, ET_SYNC, EC_SYNC_REPORT, 0);\n        }\n        if event.meta {\n            self.send(self.keyboard_fd, ET_KEY, KEY_LEFTMETA, state);\n            self.send(self.keyboard_fd, ET_SYNC, EC_SYNC_REPORT, 0);\n        }\n        if event.shift {\n            self.send(self.keyboard_fd, ET_KEY, KEY_LEFTSHIFT, state);\n            self.send(self.keyboard_fd, ET_SYNC, EC_SYNC_REPORT, 0);\n        }\n\n        self.send(self.keyboard_fd, ET_KEY, key_code, state);\n        self.send(self.keyboard_fd, ET_SYNC, EC_SYNC_REPORT, 0);\n    }\n\n    fn set_capturable(&mut self, capturable: Box<dyn Capturable>) {\n        self.capturable = capturable;\n    }\n\n    fn device_type(&self) -> InputDeviceType {\n        InputDeviceType::UInputDevice\n    }\n}\n"
  },
  {
    "path": "src/input/uinput_keys.rs",
    "content": "use std::os::raw::c_int;\n\npub const KEY_ESC: c_int = 1;\npub const KEY_1: c_int = 2;\npub const KEY_2: c_int = 3;\npub const KEY_3: c_int = 4;\npub const KEY_4: c_int = 5;\npub const KEY_5: c_int = 6;\npub const KEY_6: c_int = 7;\npub const KEY_7: c_int = 8;\npub const KEY_8: c_int = 9;\npub const KEY_9: c_int = 10;\npub const KEY_0: c_int = 11;\npub const KEY_MINUS: c_int = 12;\npub const KEY_EQUAL: c_int = 13;\npub const KEY_BACKSPACE: c_int = 14;\npub const KEY_TAB: c_int = 15;\npub const KEY_Q: c_int = 16;\npub const KEY_W: c_int = 17;\npub const KEY_E: c_int = 18;\npub const KEY_R: c_int = 19;\npub const KEY_T: c_int = 20;\npub const KEY_Y: c_int = 21;\npub const KEY_U: c_int = 22;\npub const KEY_I: c_int = 23;\npub const KEY_O: c_int = 24;\npub const KEY_P: c_int = 25;\npub const KEY_LEFTBRACE: c_int = 26;\npub const KEY_RIGHTBRACE: c_int = 27;\npub const KEY_ENTER: c_int = 28;\npub const KEY_LEFTCTRL: c_int = 29;\npub const KEY_A: c_int = 30;\npub const KEY_S: c_int = 31;\npub const KEY_D: c_int = 32;\npub const KEY_F: c_int = 33;\npub const KEY_G: c_int = 34;\npub const KEY_H: c_int = 35;\npub const KEY_J: c_int = 36;\npub const KEY_K: c_int = 37;\npub const KEY_L: c_int = 38;\npub const KEY_SEMICOLON: c_int = 39;\npub const KEY_APOSTROPHE: c_int = 40;\npub const KEY_GRAVE: c_int = 41;\npub const KEY_LEFTSHIFT: c_int = 42;\npub const KEY_BACKSLASH: c_int = 43;\npub const KEY_Z: c_int = 44;\npub const KEY_X: c_int = 45;\npub const KEY_C: c_int = 46;\npub const KEY_V: c_int = 47;\npub const KEY_B: c_int = 48;\npub const KEY_N: c_int = 49;\npub const KEY_M: c_int = 50;\npub const KEY_COMMA: c_int = 51;\npub const KEY_DOT: c_int = 52;\npub const KEY_SLASH: c_int = 53;\npub const KEY_RIGHTSHIFT: c_int = 54;\npub const KEY_KPASTERISK: c_int = 55;\npub const KEY_LEFTALT: c_int = 56;\npub const KEY_SPACE: c_int = 57;\npub const KEY_CAPSLOCK: c_int = 58;\npub const KEY_F1: c_int = 59;\npub const KEY_F2: c_int = 60;\npub const KEY_F3: c_int = 61;\npub const KEY_F4: c_int = 62;\npub const KEY_F5: c_int = 63;\npub const KEY_F6: c_int = 64;\npub const KEY_F7: c_int = 65;\npub const KEY_F8: c_int = 66;\npub const KEY_F9: c_int = 67;\npub const KEY_F10: c_int = 68;\npub const KEY_NUMLOCK: c_int = 69;\npub const KEY_SCROLLLOCK: c_int = 70;\npub const KEY_KP7: c_int = 71;\npub const KEY_KP8: c_int = 72;\npub const KEY_KP9: c_int = 73;\npub const KEY_KPMINUS: c_int = 74;\npub const KEY_KP4: c_int = 75;\npub const KEY_KP5: c_int = 76;\npub const KEY_KP6: c_int = 77;\npub const KEY_KPPLUS: c_int = 78;\npub const KEY_KP1: c_int = 79;\npub const KEY_KP2: c_int = 80;\npub const KEY_KP3: c_int = 81;\npub const KEY_KP0: c_int = 82;\npub const KEY_KPDOT: c_int = 83;\n\npub const KEY_ZENKAKUHANKAKU: c_int = 85;\npub const KEY_102ND: c_int = 86;\npub const KEY_F11: c_int = 87;\npub const KEY_F12: c_int = 88;\npub const KEY_RO: c_int = 89;\npub const KEY_KATAKANA: c_int = 90;\npub const KEY_HIRAGANA: c_int = 91;\npub const KEY_HENKAN: c_int = 92;\npub const KEY_KATAKANAHIRAGANA: c_int = 93;\npub const KEY_MUHENKAN: c_int = 94;\npub const KEY_KPJPCOMMA: c_int = 95;\npub const KEY_KPENTER: c_int = 96;\npub const KEY_RIGHTCTRL: c_int = 97;\npub const KEY_KPSLASH: c_int = 98;\npub const KEY_SYSRQ: c_int = 99;\npub const KEY_RIGHTALT: c_int = 100;\npub const KEY_LINEFEED: c_int = 101;\npub const KEY_HOME: c_int = 102;\npub const KEY_UP: c_int = 103;\npub const KEY_PAGEUP: c_int = 104;\npub const KEY_LEFT: c_int = 105;\npub const KEY_RIGHT: c_int = 106;\npub const KEY_END: c_int = 107;\npub const KEY_DOWN: c_int = 108;\npub const KEY_PAGEDOWN: c_int = 109;\npub const KEY_INSERT: c_int = 110;\npub const KEY_DELETE: c_int = 111;\npub const KEY_MACRO: c_int = 112;\npub const KEY_MUTE: c_int = 113;\npub const KEY_VOLUMEDOWN: c_int = 114;\npub const KEY_VOLUMEUP: c_int = 115;\npub const KEY_POWER: c_int = 116; /* SC System Power Down */\npub const KEY_KPEQUAL: c_int = 117;\npub const KEY_KPPLUSMINUS: c_int = 118;\npub const KEY_PAUSE: c_int = 119;\npub const KEY_SCALE: c_int = 120; /* AL Compiz Scale (Expose) */\n\npub const KEY_KPCOMMA: c_int = 121;\npub const KEY_HANGEUL: c_int = 122;\npub const KEY_HANGUEL: c_int = KEY_HANGEUL;\npub const KEY_HANJA: c_int = 123;\npub const KEY_YEN: c_int = 124;\npub const KEY_LEFTMETA: c_int = 125;\npub const KEY_RIGHTMETA: c_int = 126;\npub const KEY_COMPOSE: c_int = 127;\n\npub const KEY_STOP: c_int = 128; /* AC Stop */\npub const KEY_AGAIN: c_int = 129;\npub const KEY_PROPS: c_int = 130; /* AC Properties */\npub const KEY_UNDO: c_int = 131; /* AC Undo */\npub const KEY_FRONT: c_int = 132;\npub const KEY_COPY: c_int = 133; /* AC Copy */\npub const KEY_OPEN: c_int = 134; /* AC Open */\npub const KEY_PASTE: c_int = 135; /* AC Paste */\npub const KEY_FIND: c_int = 136; /* AC Search */\npub const KEY_CUT: c_int = 137; /* AC Cut */\npub const KEY_HELP: c_int = 138; /* AL Integrated Help Center */\npub const KEY_MENU: c_int = 139; /* Menu (show menu) */\npub const KEY_CALC: c_int = 140; /* AL Calculator */\npub const KEY_SETUP: c_int = 141;\npub const KEY_SLEEP: c_int = 142; /* SC System Sleep */\npub const KEY_WAKEUP: c_int = 143; /* System Wake Up */\npub const KEY_FILE: c_int = 144; /* AL Local Machine Browser */\npub const KEY_SENDFILE: c_int = 145;\npub const KEY_DELETEFILE: c_int = 146;\npub const KEY_XFER: c_int = 147;\npub const KEY_PROG1: c_int = 148;\npub const KEY_PROG2: c_int = 149;\npub const KEY_WWW: c_int = 150; /* AL Internet Browser */\npub const KEY_MSDOS: c_int = 151;\npub const KEY_COFFEE: c_int = 152; /* AL Terminal Lock/Screensaver */\npub const KEY_SCREENLOCK: c_int = KEY_COFFEE;\npub const KEY_ROTATE_DISPLAY: c_int = 153; /* Display orientation for e.g. tablets */\npub const KEY_DIRECTION: c_int = KEY_ROTATE_DISPLAY;\npub const KEY_CYCLEWINDOWS: c_int = 154;\npub const KEY_MAIL: c_int = 155;\npub const KEY_BOOKMARKS: c_int = 156; /* AC Bookmarks */\npub const KEY_COMPUTER: c_int = 157;\npub const KEY_BACK: c_int = 158; /* AC Back */\npub const KEY_FORWARD: c_int = 159; /* AC Forward */\npub const KEY_CLOSECD: c_int = 160;\npub const KEY_EJECTCD: c_int = 161;\npub const KEY_EJECTCLOSECD: c_int = 162;\npub const KEY_NEXTSONG: c_int = 163;\npub const KEY_PLAYPAUSE: c_int = 164;\npub const KEY_PREVIOUSSONG: c_int = 165;\npub const KEY_STOPCD: c_int = 166;\npub const KEY_RECORD: c_int = 167;\npub const KEY_REWIND: c_int = 168;\npub const KEY_PHONE: c_int = 169; /* Media Select Telephone */\npub const KEY_ISO: c_int = 170;\npub const KEY_CONFIG: c_int = 171; /* AL Consumer Control Configuration */\npub const KEY_HOMEPAGE: c_int = 172; /* AC Home */\npub const KEY_REFRESH: c_int = 173; /* AC Refresh */\npub const KEY_EXIT: c_int = 174; /* AC Exit */\npub const KEY_MOVE: c_int = 175;\npub const KEY_EDIT: c_int = 176;\npub const KEY_SCROLLUP: c_int = 177;\npub const KEY_SCROLLDOWN: c_int = 178;\npub const KEY_KPLEFTPAREN: c_int = 179;\npub const KEY_KPRIGHTPAREN: c_int = 180;\npub const KEY_NEW: c_int = 181; /* AC New */\npub const KEY_REDO: c_int = 182; /* AC Redo/Repeat */\n\npub const KEY_F13: c_int = 183;\npub const KEY_F14: c_int = 184;\npub const KEY_F15: c_int = 185;\npub const KEY_F16: c_int = 186;\npub const KEY_F17: c_int = 187;\npub const KEY_F18: c_int = 188;\npub const KEY_F19: c_int = 189;\npub const KEY_F20: c_int = 190;\npub const KEY_F21: c_int = 191;\npub const KEY_F22: c_int = 192;\npub const KEY_F23: c_int = 193;\npub const KEY_F24: c_int = 194;\n\npub const KEY_PLAYCD: c_int = 200;\npub const KEY_PAUSECD: c_int = 201;\npub const KEY_PROG3: c_int = 202;\npub const KEY_PROG4: c_int = 203;\npub const KEY_DASHBOARD: c_int = 204; /* AL Dashboard */\npub const KEY_SUSPEND: c_int = 205;\npub const KEY_CLOSE: c_int = 206; /* AC Close */\npub const KEY_PLAY: c_int = 207;\npub const KEY_FASTFORWARD: c_int = 208;\npub const KEY_BASSBOOST: c_int = 209;\npub const KEY_PRINT: c_int = 210; /* AC Print */\npub const KEY_HP: c_int = 211;\npub const KEY_CAMERA: c_int = 212;\npub const KEY_SOUND: c_int = 213;\npub const KEY_QUESTION: c_int = 214;\npub const KEY_EMAIL: c_int = 215;\npub const KEY_CHAT: c_int = 216;\npub const KEY_SEARCH: c_int = 217;\npub const KEY_CONNECT: c_int = 218;\npub const KEY_FINANCE: c_int = 219; /* AL Checkbook/Finance */\npub const KEY_SPORT: c_int = 220;\npub const KEY_SHOP: c_int = 221;\npub const KEY_ALTERASE: c_int = 222;\npub const KEY_CANCEL: c_int = 223; /* AC Cancel */\npub const KEY_BRIGHTNESSDOWN: c_int = 224;\npub const KEY_BRIGHTNESSUP: c_int = 225;\npub const KEY_MEDIA: c_int = 226;\n\npub const KEY_SWITCHVIDEOMODE: c_int = 227; /* Cycle between available video outputs (Monitor/LCD/TV-out/etc) */\npub const KEY_KBDILLUMTOGGLE: c_int = 228;\npub const KEY_KBDILLUMDOWN: c_int = 229;\npub const KEY_KBDILLUMUP: c_int = 230;\n\npub const KEY_SEND: c_int = 231; /* AC Send */\npub const KEY_REPLY: c_int = 232; /* AC Reply */\npub const KEY_FORWARDMAIL: c_int = 233; /* AC Forward Msg */\npub const KEY_SAVE: c_int = 234; /* AC Save */\npub const KEY_DOCUMENTS: c_int = 235;\n\npub const KEY_BATTERY: c_int = 236;\n\npub const KEY_BLUETOOTH: c_int = 237;\npub const KEY_WLAN: c_int = 238;\npub const KEY_UWB: c_int = 239;\n\npub const KEY_UNKNOWN: c_int = 240;\n\npub const KEY_VIDEO_NEXT: c_int = 241; /* drive next video source */\npub const KEY_VIDEO_PREV: c_int = 242; /* drive previous video source */\npub const KEY_BRIGHTNESS_CYCLE: c_int = 243; /* brightness up, after max is min */\npub const KEY_BRIGHTNESS_AUTO: c_int = 244; /* Set Auto Brightness: manual brightness control is off, rely on ambient */\npub const KEY_BRIGHTNESS_ZERO: c_int = KEY_BRIGHTNESS_AUTO;\npub const KEY_DISPLAY_OFF: c_int = 245; /* display device to off state */\n\npub const KEY_WWAN: c_int = 246; /* Wireless WAN (LTE, UMTS, GSM, etc.) */\npub const KEY_WIMAX: c_int = KEY_WWAN;\npub const KEY_RFKILL: c_int = 247; /* Key that controls all radios */\n\npub const KEY_MICMUTE: c_int = 248; /* Mute / unmute the microphone */\n"
  },
  {
    "path": "src/log.rs",
    "content": "use std::ffi::CStr;\nuse std::io::Write;\nuse std::os::raw::c_char;\nuse std::sync::mpsc;\nuse tracing::{debug, error, info, trace, warn};\nuse tracing_subscriber::layer::SubscriberExt;\n\nextern \"C\" {\n    fn init_ffmpeg_logger();\n}\n\nstruct GuiTracingWriter {\n    gui_sender: mpsc::SyncSender<String>,\n}\n\nimpl Write for GuiTracingWriter {\n    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {\n        self.gui_sender\n            .try_send(String::from_utf8_lossy(buf).trim_start().into())\n            .ok();\n        Ok(buf.len())\n    }\n    fn flush(&mut self) -> std::io::Result<()> {\n        Ok(())\n    }\n}\n\nstruct GuiTracingWriterFactory {\n    sender: mpsc::SyncSender<String>,\n}\n\nimpl<'a> tracing_subscriber::fmt::MakeWriter<'a> for GuiTracingWriterFactory {\n    type Writer = GuiTracingWriter;\n    fn make_writer(&'a self) -> Self::Writer {\n        Self::Writer {\n            gui_sender: self.sender.clone(),\n        }\n    }\n}\n\npub fn get_log_level() -> tracing::Level {\n    #[cfg(debug_assertions)]\n    let mut level = tracing::Level::DEBUG;\n\n    #[cfg(not(debug_assertions))]\n    let mut level = tracing::Level::INFO;\n\n    if let Ok(var) = std::env::var(\"WEYLUS_LOG_LEVEL\") {\n        let l: Result<tracing::Level, _> = var.parse();\n        if let Ok(l) = l {\n            level = l;\n        }\n    }\n    level\n}\n\npub fn setup_logging(sender: mpsc::SyncSender<String>) {\n    if std::env::var(\"WEYLUS_LOG_JSON\").is_ok() {\n        let logger = tracing_subscriber::fmt()\n            .json()\n            .with_max_level(get_log_level())\n            .with_writer(std::io::stdout)\n            .finish()\n            .with(\n                tracing_subscriber::fmt::Layer::default()\n                    .with_ansi(false)\n                    .without_time()\n                    .with_target(false)\n                    .compact()\n                    .with_writer(GuiTracingWriterFactory { sender }),\n            );\n        tracing::subscriber::set_global_default(logger).expect(\"Failed to setup logger!\");\n    } else {\n        let logger = tracing_subscriber::fmt()\n            .with_max_level(get_log_level())\n            .with_writer(std::io::stderr)\n            .finish()\n            .with(\n                tracing_subscriber::fmt::Layer::default()\n                    .with_ansi(false)\n                    .without_time()\n                    .with_target(false)\n                    .compact()\n                    .with_writer(GuiTracingWriterFactory { sender }),\n            );\n        tracing::subscriber::set_global_default(logger).expect(\"Failed to setup logger!\");\n    }\n    unsafe {\n        init_ffmpeg_logger();\n    }\n}\n\n#[no_mangle]\nfn log_error_rust(msg: *const c_char) {\n    let msg = unsafe { CStr::from_ptr(msg) }.to_string_lossy();\n    error!(\"{}\", msg);\n}\n\n#[no_mangle]\nfn log_debug_rust(msg: *const c_char) {\n    let msg = unsafe { CStr::from_ptr(msg) }.to_string_lossy();\n    debug!(\"{}\", msg);\n}\n\n#[no_mangle]\nfn log_info_rust(msg: *const c_char) {\n    let msg = unsafe { CStr::from_ptr(msg) }.to_string_lossy();\n    info!(\"{}\", msg);\n}\n\n#[no_mangle]\nfn log_trace_rust(msg: *const c_char) {\n    let msg = unsafe { CStr::from_ptr(msg) }.to_string_lossy();\n    trace!(\"{}\", msg);\n}\n\n#[no_mangle]\nfn log_warn_rust(msg: *const c_char) {\n    let msg = unsafe { CStr::from_ptr(msg) }.to_string_lossy();\n    warn!(\"{}\", msg);\n}\n"
  },
  {
    "path": "src/main.rs",
    "content": "#![cfg_attr(feature = \"bench\", feature(test))]\n#[cfg(feature = \"bench\")]\nextern crate test;\n\n#[macro_use]\nextern crate bitflags;\n\nuse clap::CommandFactory;\nuse clap_complete::generate;\n#[cfg(unix)]\nuse signal_hook::iterator::Signals;\n#[cfg(unix)]\nuse signal_hook::{consts::TERM_SIGNALS, low_level::signal_name};\nuse tracing::{error, info, warn};\n\nuse std::sync::mpsc;\n\nuse config::{get_config, Config};\n\nmod capturable;\nmod cerror;\nmod config;\nmod gui;\nmod input;\nmod log;\nmod protocol;\nmod video;\nmod web;\nmod websocket;\nmod weylus;\n\nfn main() {\n    let (sender, receiver) = mpsc::sync_channel::<String>(100);\n\n    log::setup_logging(sender);\n\n    let conf = get_config();\n\n    if let Some(shell) = conf.completions {\n        generate(\n            shell,\n            &mut Config::command(),\n            \"weylus\",\n            &mut std::io::stdout(),\n        );\n        return;\n    }\n\n    if conf.print_index_html {\n        print!(\"{}\", web::INDEX_HTML);\n        return;\n    }\n    if conf.print_access_html {\n        print!(\"{}\", web::ACCESS_HTML);\n        return;\n    }\n    if conf.print_style_css {\n        print!(\"{}\", web::STYLE_CSS);\n        return;\n    }\n    if conf.print_lib_js {\n        print!(\"{}\", web::LIB_JS);\n        return;\n    }\n\n    #[cfg(target_os = \"linux\")]\n    {\n        // make sure XInitThreads is called before any threading is done\n        crate::capturable::x11::x11_init();\n\n        if let Err(err) = gstreamer::init() {\n            error!(\n                \"Failed to initialize gstreamer, screen capturing will most likely not work \\\n                 on Wayland: {}\",\n                err\n            );\n        }\n    }\n\n    if conf.no_gui {\n        let mut weylus = crate::weylus::Weylus::new();\n        weylus.start(&conf, |msg| match msg {\n            web::Web2UiMessage::UInputInaccessible => {\n                warn!(std::include_str!(\"strings/uinput_error.txt\"))\n            }\n        });\n        #[cfg(unix)]\n        {\n            let mut signals = Signals::new(TERM_SIGNALS).unwrap();\n            for sig in signals.forever() {\n                info!(\n                    \"Shutting down after receiving signal {signame} ({sig})...\",\n                    signame = signal_name(sig).unwrap_or(\"UNKNOWN SIGNAL\")\n                );\n                std::thread::spawn(move || {\n                    for sig in signals.forever() {\n                        warn!(\n                            \"Received second signal {signame} ({sig}) while shutting down \\\n                            gracefully, proceeding with forceful shutdown...\",\n                            signame = signal_name(sig).unwrap_or(\"UNKNOWN SIGNAL\")\n                        );\n                        std::process::exit(1);\n                    }\n                });\n                weylus.stop();\n                break;\n            }\n        }\n        #[cfg(not(unix))]\n        {\n            loop {\n                std::thread::park();\n            }\n        }\n    } else {\n        gui::run(&conf, receiver);\n    }\n}\n\n#[cfg(feature = \"bench\")]\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use capturable::{Capturable, Recorder};\n    use test::Bencher;\n\n    #[cfg(target_os = \"linux\")]\n    #[bench]\n    fn bench_capture_x11(b: &mut Bencher) {\n        let mut x11ctx = capturable::x11::X11Context::new().unwrap();\n        let root = x11ctx.capturables().unwrap().remove(0);\n        let mut r = root.recorder(false).unwrap();\n        b.iter(|| {\n            r.capture().unwrap();\n        });\n    }\n\n    #[cfg(target_os = \"linux\")]\n    #[bench]\n    fn bench_video_x11(b: &mut Bencher) {\n        let mut x11ctx = capturable::x11::X11Context::new().unwrap();\n        let root = x11ctx.capturables().unwrap().remove(0);\n        let mut r = root.recorder(false).unwrap();\n        let (width, height) = r.capture().unwrap().size();\n\n        let opts = video::EncoderOptions {\n            try_vaapi: true,\n            try_nvenc: true,\n            try_videotoolbox: false,\n            try_mediafoundation: false,\n        };\n        let mut encoder =\n            video::VideoEncoder::new(width, height, width, height, |_| {}, opts).unwrap();\n        b.iter(|| encoder.encode(r.capture().unwrap()));\n    }\n\n    #[cfg(target_os = \"linux\")]\n    #[bench]\n    fn bench_capture_wayland(b: &mut Bencher) {\n        gstreamer::init().unwrap();\n        let root = capturable::pipewire::get_capturables(false)\n            .unwrap()\n            .remove(0);\n        let mut r = root.recorder(false).unwrap();\n        let _ = r.capture();\n        b.iter(|| {\n            r.capture().unwrap();\n        });\n    }\n\n    #[cfg(target_os = \"linux\")]\n    #[bench]\n    fn bench_video_wayland(b: &mut Bencher) {\n        gstreamer::init().unwrap();\n        let root = capturable::pipewire::get_capturables(false)\n            .unwrap()\n            .remove(0);\n        let mut r = root.recorder(false).unwrap();\n        let (width, height) = r.capture().unwrap().size();\n\n        let opts = video::EncoderOptions {\n            try_vaapi: true,\n            try_nvenc: true,\n            try_videotoolbox: false,\n            try_mediafoundation: false,\n        };\n        let mut encoder =\n            video::VideoEncoder::new(width, height, width, height, |_| {}, opts).unwrap();\n        b.iter(|| encoder.encode(r.capture().unwrap()));\n    }\n\n    #[cfg(target_os = \"linux\")]\n    #[bench]\n    fn bench_video_vaapi(b: &mut Bencher) {\n        const WIDTH: usize = 1920;\n        const HEIGHT: usize = 1080;\n        const N: usize = 60;\n        let mut bufs = vec![vec![0u8; SIZE]; N];\n        for i in 0..N {\n            for j in 0..SIZE {\n                bufs[i][j] = ((i * SIZE + j) % 256) as u8;\n            }\n        }\n\n        let opts = video::EncoderOptions {\n            try_vaapi: true,\n            try_nvenc: false,\n            try_videotoolbox: false,\n            try_mediafoundation: false,\n        };\n        let mut encoder =\n            video::VideoEncoder::new(WIDTH, HEIGHT, WIDTH, HEIGHT, |_| {}, opts).unwrap();\n        const SIZE: usize = WIDTH * HEIGHT * 4;\n        let mut i = 0;\n        b.iter(|| {\n            encoder.encode(video::PixelProvider::BGR0(WIDTH, HEIGHT, &bufs[i % N]));\n            i += 1;\n        });\n    }\n\n    #[cfg(target_os = \"linux\")]\n    #[bench]\n    fn bench_video_x264(b: &mut Bencher) {\n        const WIDTH: usize = 1920;\n        const HEIGHT: usize = 1080;\n        const N: usize = 60;\n        let mut bufs = vec![vec![0u8; SIZE]; N];\n        for i in 0..N {\n            for j in 0..SIZE {\n                bufs[i][j] = ((i * SIZE + j) % 256) as u8;\n            }\n        }\n\n        let opts = video::EncoderOptions {\n            try_vaapi: false,\n            try_nvenc: false,\n            try_videotoolbox: false,\n            try_mediafoundation: false,\n        };\n        let mut encoder =\n            video::VideoEncoder::new(WIDTH, HEIGHT, WIDTH, HEIGHT, |_| {}, opts).unwrap();\n        const SIZE: usize = WIDTH * HEIGHT * 4;\n        let mut i = 0;\n        b.iter(|| {\n            encoder.encode(video::PixelProvider::BGR0(WIDTH, HEIGHT, &bufs[i % N]));\n            i += 1;\n        });\n    }\n\n    #[cfg(target_os = \"linux\")]\n    #[bench]\n    fn bench_video_nvenc(b: &mut Bencher) {\n        const WIDTH: usize = 1920;\n        const HEIGHT: usize = 1080;\n        const N: usize = 60;\n        let mut bufs = vec![vec![0u8; SIZE]; N];\n        for i in 0..N {\n            for j in 0..SIZE {\n                bufs[i][j] = ((i * SIZE + j) % 256) as u8;\n            }\n        }\n\n        let opts = video::EncoderOptions {\n            try_vaapi: false,\n            try_nvenc: true,\n            try_videotoolbox: false,\n            try_mediafoundation: false,\n        };\n        let mut encoder =\n            video::VideoEncoder::new(WIDTH, HEIGHT, WIDTH, HEIGHT, |_| {}, opts).unwrap();\n        const SIZE: usize = WIDTH * HEIGHT * 4;\n        let mut i = 0;\n        b.iter(|| {\n            encoder.encode(video::PixelProvider::BGR0(WIDTH, HEIGHT, &bufs[i % N]));\n            i += 1;\n        });\n    }\n}\n"
  },
  {
    "path": "src/protocol.rs",
    "content": "use serde::{Deserialize, Deserializer, Serialize};\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct ClientConfiguration {\n    #[cfg(target_os = \"linux\")]\n    pub uinput_support: bool,\n    pub capturable_id: usize,\n    pub capture_cursor: bool,\n    pub max_width: usize,\n    pub max_height: usize,\n    pub client_name: Option<String>,\n    pub frame_rate: f64,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub enum MessageInbound {\n    PointerEvent(PointerEvent),\n    WheelEvent(WheelEvent),\n    KeyboardEvent(KeyboardEvent),\n    GetCapturableList,\n    Config(ClientConfiguration),\n    PauseVideo,\n    ResumeVideo,\n    RestartVideo,\n    ChooseCustomInputAreas,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub enum MessageOutbound {\n    CapturableList(Vec<String>),\n    NewVideo,\n    ConfigOk,\n    CustomInputAreas(CustomInputAreas),\n    ConfigError(String),\n    Error(String),\n}\n\n#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]\npub struct Rect {\n    pub x: f64,\n    pub y: f64,\n    pub w: f64,\n    pub h: f64,\n}\n\nimpl Default for Rect {\n    fn default() -> Self {\n        Self {\n            x: 0.0,\n            y: 0.0,\n            w: 1.0,\n            h: 1.0,\n        }\n    }\n}\n\n#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Default)]\npub struct CustomInputAreas {\n    pub mouse: Option<Rect>,\n    pub touch: Option<Rect>,\n    pub pen: Option<Rect>,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub enum PointerType {\n    #[serde(rename = \"\")]\n    Unknown,\n    #[serde(rename = \"mouse\")]\n    Mouse,\n    #[serde(rename = \"pen\")]\n    Pen,\n    #[serde(rename = \"touch\")]\n    Touch,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub enum PointerEventType {\n    #[serde(rename = \"pointerdown\")]\n    DOWN,\n    #[serde(rename = \"pointerup\")]\n    UP,\n    #[serde(rename = \"pointercancel\")]\n    CANCEL,\n    #[serde(rename = \"pointermove\")]\n    MOVE,\n    #[serde(rename = \"pointerover\")]\n    OVER,\n    #[serde(rename = \"pointerenter\")]\n    ENTER,\n    #[serde(rename = \"pointerleave\")]\n    LEAVE,\n    #[serde(rename = \"pointerout\")]\n    OUT,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub enum KeyboardEventType {\n    #[serde(rename = \"down\")]\n    DOWN,\n    #[serde(rename = \"up\")]\n    UP,\n    #[serde(rename = \"repeat\")]\n    REPEAT,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub enum KeyboardLocation {\n    STANDARD,\n    LEFT,\n    RIGHT,\n    NUMPAD,\n}\n\nfn location_from<'de, D: Deserializer<'de>>(deserializer: D) -> Result<KeyboardLocation, D::Error> {\n    let code: u8 = Deserialize::deserialize(deserializer)?;\n    match code {\n        0 => Ok(KeyboardLocation::STANDARD),\n        1 => Ok(KeyboardLocation::LEFT),\n        2 => Ok(KeyboardLocation::RIGHT),\n        3 => Ok(KeyboardLocation::NUMPAD),\n        _ => Err(serde::de::Error::custom(\n            \"Failed to parse keyboard location code.\",\n        )),\n    }\n}\n\nbitflags! {\n    #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)]\n    pub struct Button: u8 {\n        const NONE = 0b0000_0000;\n        const PRIMARY = 0b0000_0001;\n        const SECONDARY = 0b0000_0010;\n        const AUXILARY = 0b0000_0100;\n        const FOURTH = 0b0000_1000;\n        const FIFTH = 0b0001_0000;\n        const ERASER = 0b0010_0000;\n    }\n}\n\nfn button_from<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Button, D::Error> {\n    let bits: u8 = Deserialize::deserialize(deserializer)?;\n    Button::from_bits(bits).map_or(\n        Err(serde::de::Error::custom(\"Failed to parse button code.\")),\n        Ok,\n    )\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct KeyboardEvent {\n    pub event_type: KeyboardEventType,\n    pub code: String,\n    pub key: String,\n    #[serde(deserialize_with = \"location_from\")]\n    pub location: KeyboardLocation,\n    pub alt: bool,\n    pub ctrl: bool,\n    pub shift: bool,\n    pub meta: bool,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct PointerEvent {\n    pub event_type: PointerEventType,\n    pub pointer_id: i64,\n    pub timestamp: u64,\n    pub is_primary: bool,\n    pub pointer_type: PointerType,\n    #[serde(deserialize_with = \"button_from\")]\n    pub button: Button,\n    #[serde(deserialize_with = \"button_from\")]\n    pub buttons: Button,\n    pub x: f64,\n    pub y: f64,\n    // pub movement_x: f64,\n    // pub movement_y: f64,\n    pub pressure: f64,\n    pub tilt_x: i32,\n    pub tilt_y: i32,\n    pub twist: i32,\n    pub width: f64,\n    pub height: f64,\n}\n\n#[derive(Serialize, Deserialize, Debug)]\npub struct WheelEvent {\n    pub dx: i32,\n    pub dy: i32,\n    pub timestamp: u64,\n}\n\npub trait WeylusSender {\n    type Error: std::error::Error;\n    fn send_message(&mut self, message: MessageOutbound) -> Result<(), Self::Error>;\n    fn send_video(&mut self, bytes: &[u8]) -> Result<(), Self::Error>;\n}\n\npub trait WeylusReceiver: Iterator<Item = Result<MessageInbound, Self::Error>> {\n    type Error: std::error::Error;\n}\n"
  },
  {
    "path": "src/strings/uinput_error.txt",
    "content": "Weylus uses the uinput interface to simulate input events on Linux. To enable stylus and multi-touch support /dev/uinput needs to be writable by Weylus. To make /dev/uinput permanently writable by your user, run the following inside a terminal:\n\nsudo groupadd -r uinput\nsudo usermod -aG uinput $USER\necho 'KERNEL==\"uinput\", MODE=\"0660\", GROUP=\"uinput\", OPTIONS+=\"static_node=uinput\"' | sudo tee /etc/udev/rules.d/60-weylus.rules\n\nThen, either reboot, or run\n\nsudo udevadm control --reload\nsudo udevadm trigger\n\nthen log out and log in again. To undo this, run:\n\nsudo rm /etc/udev/rules.d/60-weylus.rules\n\nThis allows your user to synthesize input events system-wide, even when another user is logged in. Therefore, untrusted users should not be added to the uinput group.\n\nIf you do not want to make use of this feature uncheck \"Enable uinput\" in your browser. Like this Weylus will only simulate a mouse.\n"
  },
  {
    "path": "src/video.rs",
    "content": "use std::os::raw::{c_int, c_uchar, c_void};\nuse std::time::Instant;\n\nuse tracing::warn;\n\nuse crate::cerror::CError;\n\nextern \"C\" {\n    fn init_video_encoder(\n        rust_ctx: *mut c_void,\n        width_in: c_int,\n        height_in: c_int,\n        width_out: c_int,\n        height_out: c_int,\n        try_vaapi: c_int,\n        try_nvenc: c_int,\n        try_videotoolbox: c_int,\n        try_mediafoundation: c_int,\n    ) -> *mut c_void;\n    fn open_video(handle: *mut c_void, err: *mut CError);\n    fn destroy_video_encoder(handle: *mut c_void);\n    fn encode_video_frame(handle: *mut c_void, micros: c_int, err: *mut CError);\n\n    fn fill_rgb(ctx: *mut c_void, data: *const u8, err: *mut CError);\n    fn fill_rgb0(ctx: *mut c_void, data: *const u8, err: *mut CError);\n    fn fill_bgr0(ctx: *mut c_void, data: *const u8, stride: c_int, err: *mut CError);\n}\n\n// this is used as callback in lib/encode_video.c via ffmpegs AVIOContext\n#[no_mangle]\nfn write_video_packet(video_encoder: *mut c_void, buf: *const c_uchar, buf_size: c_int) -> c_int {\n    let video_encoder = unsafe { (video_encoder as *mut VideoEncoder).as_mut().unwrap() };\n    (video_encoder.write_data)(unsafe {\n        std::slice::from_raw_parts(buf as *const u8, buf_size as usize)\n    });\n    0\n}\n\npub enum PixelProvider<'a> {\n    // 8 bits per color\n    RGB(usize, usize, &'a [u8]),\n    RGB0(usize, usize, &'a [u8]),\n    BGR0(usize, usize, &'a [u8]),\n    // width, height, stride\n    BGR0S(usize, usize, usize, &'a [u8]),\n}\n\nimpl<'a> PixelProvider<'a> {\n    pub fn size(&self) -> (usize, usize) {\n        match self {\n            PixelProvider::RGB(w, h, _) => (*w, *h),\n            PixelProvider::RGB0(w, h, _) => (*w, *h),\n            PixelProvider::BGR0(w, h, _) => (*w, *h),\n            PixelProvider::BGR0S(w, h, _, _) => (*w, *h),\n        }\n    }\n}\n\n#[derive(Clone, Copy)]\npub struct EncoderOptions {\n    pub try_vaapi: bool,\n    pub try_nvenc: bool,\n    pub try_videotoolbox: bool,\n    pub try_mediafoundation: bool,\n}\n\npub struct VideoEncoder {\n    handle: *mut c_void,\n    width_in: usize,\n    height_in: usize,\n    width_out: usize,\n    height_out: usize,\n    write_data: Box<dyn FnMut(&[u8])>,\n    start_time: Instant,\n}\n\nimpl VideoEncoder {\n    pub fn new(\n        width_in: usize,\n        height_in: usize,\n        width_out: usize,\n        height_out: usize,\n        mut write_data: impl FnMut(&[u8]) + 'static,\n        options: EncoderOptions,\n    ) -> Result<Box<Self>, CError> {\n        let mut video_encoder = Box::new(Self {\n            handle: std::ptr::null_mut(),\n            width_in,\n            height_in,\n            width_out,\n            height_out,\n            write_data: Box::new(move |data| write_data(data)),\n            start_time: Instant::now(),\n        });\n        let handle = unsafe {\n            init_video_encoder(\n                video_encoder.as_mut() as *mut _ as *mut c_void,\n                width_in as c_int,\n                height_in as c_int,\n                width_out as c_int,\n                height_out as c_int,\n                options.try_vaapi.into(),\n                options.try_nvenc.into(),\n                options.try_videotoolbox.into(),\n                options.try_mediafoundation.into(),\n            )\n        };\n        video_encoder.handle = handle;\n\n        let mut err = CError::new();\n        unsafe { open_video(video_encoder.handle, &mut err) };\n        if err.is_err() {\n            return Err(err);\n        }\n        Ok(video_encoder)\n    }\n\n    pub fn encode(&mut self, pixel_provider: PixelProvider) {\n        let mut err = CError::new();\n        match pixel_provider {\n            PixelProvider::BGR0(w, _, bgr0) => unsafe {\n                fill_bgr0(self.handle, bgr0.as_ptr(), (w * 4) as c_int, &mut err);\n            },\n            PixelProvider::BGR0S(_, _, stride, bgr0) => unsafe {\n                fill_bgr0(self.handle, bgr0.as_ptr(), stride as c_int, &mut err);\n            },\n            PixelProvider::RGB(_, _, rgb) => unsafe {\n                fill_rgb(self.handle, rgb.as_ptr(), &mut err);\n            },\n            PixelProvider::RGB0(_, _, rgb) => unsafe {\n                fill_rgb0(self.handle, rgb.as_ptr(), &mut err);\n            },\n        }\n        if err.is_err() {\n            warn!(\"Failed to fill video frame: {}\", err);\n            return;\n        }\n        unsafe {\n            encode_video_frame(\n                self.handle,\n                (Instant::now() - self.start_time).as_millis() as c_int,\n                &mut err,\n            );\n        }\n        if err.is_err() {\n            warn!(\"Failed to encode video frame: {}\", err);\n            return;\n        }\n    }\n\n    pub fn check_size(\n        &self,\n        width_in: usize,\n        height_in: usize,\n        width_out: usize,\n        height_out: usize,\n    ) -> bool {\n        (self.width_in == width_in)\n            && (self.height_in == height_in)\n            && (self.width_out == width_out)\n            && (self.height_out == height_out)\n    }\n}\n\nimpl Drop for VideoEncoder {\n    fn drop(&mut self) {\n        if !self.handle.is_null() {\n            unsafe { destroy_video_encoder(self.handle) }\n        }\n    }\n}\n"
  },
  {
    "path": "src/web.rs",
    "content": "use bytes::Bytes;\nuse fastwebsockets::upgrade;\nuse handlebars::Handlebars;\nuse http_body_util::combinators::BoxBody;\nuse http_body_util::{BodyExt, Full};\nuse hyper::body::Incoming;\nuse hyper::server::conn::http1;\nuse hyper::service::service_fn;\nuse hyper::{Method, Request, Response, StatusCode};\nuse hyper_util::rt::TokioIo;\nuse serde::Serialize;\nuse std::collections::HashMap;\nuse std::convert::Infallible;\nuse std::net::SocketAddr;\nuse std::path::PathBuf;\nuse std::sync::atomic::{AtomicUsize, Ordering};\nuse std::sync::Arc;\nuse std::time::Duration;\nuse tokio::net::TcpListener;\nuse tokio::sync::{mpsc, oneshot};\nuse tracing::{debug, error, info, warn};\n\nuse crate::websocket::{weylus_websocket_channel, WeylusClientConfig, WeylusClientHandler};\n\n#[derive(Debug)]\npub enum WebStartUpMessage {\n    Start,\n    Error,\n}\n\npub enum Web2UiMessage {\n    UInputInaccessible,\n}\n\npub const INDEX_HTML: &str = std::include_str!(\"../www/templates/index.html\");\npub const ACCESS_HTML: &str = std::include_str!(\"../www/static/access_code.html\");\npub const STYLE_CSS: &str = std::include_str!(\"../www/static/style.css\");\npub const LIB_JS: &str = std::include_str!(\"../www/static/lib.js\");\n\n#[derive(Serialize)]\nstruct IndexTemplateContext {\n    access_code: Option<String>,\n    uinput_enabled: bool,\n    capture_cursor_enabled: bool,\n    log_level: String,\n    enable_custom_input_areas: bool,\n}\n\nfn response_from_str(s: &str, content_type: &str) -> Response<Full<Bytes>> {\n    Response::builder()\n        .status(StatusCode::OK)\n        .header(\"content-type\", content_type)\n        .body(s.to_string().into())\n        .unwrap()\n}\n\nfn response_not_found() -> Response<Full<Bytes>> {\n    Response::builder()\n        .status(StatusCode::NOT_FOUND)\n        .header(\"content-type\", \"text/html; charset=utf-8\")\n        .body(\"Not found!\".into())\n        .unwrap()\n}\n\nasync fn response_from_path_or_default(\n    path: Option<&PathBuf>,\n    default: &str,\n    content_type: &str,\n) -> Response<Full<Bytes>> {\n    match path {\n        Some(path) => match tokio::fs::read_to_string(path).await {\n            Ok(s) => response_from_str(&s, content_type),\n            Err(err) => {\n                warn!(\"Failed to load file: {}\", err);\n                response_from_str(default, content_type)\n            }\n        },\n        None => response_from_str(default, content_type),\n    }\n}\n\nasync fn serve(\n    addr: SocketAddr,\n    mut req: Request<Incoming>,\n    context: Arc<Context<'_>>,\n    sender_ui: mpsc::Sender<Web2UiMessage>,\n    num_clients: Arc<AtomicUsize>,\n    semaphore_websocket_shutdown: Arc<tokio::sync::Semaphore>,\n    notify_disconnect: Arc<tokio::sync::Notify>,\n) -> Result<Response<BoxBody<Bytes, Infallible>>, hyper::Error> {\n    debug!(\"Got request: {:?}\", req);\n    let mut authed = false;\n    if let Some(access_code) = &context.web_config.access_code {\n        if req.method() == Method::GET && (req.uri().path() == \"/\" || req.uri().path() == \"/ws\") {\n            use url::form_urlencoded;\n            if let Some(query) = req.uri().query() {\n                let params = form_urlencoded::parse(query.as_bytes())\n                    .into_owned()\n                    .collect::<HashMap<String, String>>();\n                if let Some(code) = params.get(\"access_code\") {\n                    if code == access_code {\n                        authed = true;\n                        debug!(address = ?addr, \"Web-Client authenticated.\");\n                    }\n                }\n            }\n        }\n    } else {\n        authed = true;\n    }\n    if req.method() != Method::GET {\n        return Ok(response_not_found().map(|r| r.boxed()));\n    }\n    match req.uri().path() {\n        \"/\" => {\n            if !authed {\n                return Ok(response_from_path_or_default(\n                    context.web_config.custom_access_html.as_ref(),\n                    ACCESS_HTML,\n                    \"text/html; charset=utf-8\",\n                )\n                .await\n                .map(|r| r.boxed()));\n            }\n            let config = IndexTemplateContext {\n                access_code: context.web_config.access_code.clone(),\n                uinput_enabled: cfg!(target_os = \"linux\"),\n                capture_cursor_enabled: cfg!(not(target_os = \"windows\")),\n                log_level: crate::log::get_log_level().to_string(),\n                enable_custom_input_areas: context.web_config.enable_custom_input_areas,\n            };\n\n            let html = if let Some(path) = context.web_config.custom_index_html.as_ref() {\n                let mut reg = Handlebars::new();\n                if let Err(err) = reg.register_template_file(\"index\", path) {\n                    warn!(\"Failed to register template from path: {}\", err);\n                    context.templates.render(\"index\", &config)\n                } else {\n                    reg.render(\"index\", &config)\n                }\n            } else {\n                context.templates.render(\"index\", &config)\n            };\n\n            match html {\n                Ok(html) => {\n                    Ok(response_from_str(&html, \"text/html; charset=utf-8\").map(|r| r.boxed()))\n                }\n                Err(err) => {\n                    error!(\"Failed to render index template: {}\", err);\n                    Ok(response_not_found().map(|r| r.boxed()))\n                }\n            }\n        }\n        \"/ws\" => {\n            if !authed {\n                return Ok(Response::builder()\n                    .status(StatusCode::UNAUTHORIZED)\n                    .body(\"unauthorized\".to_string().boxed())\n                    .unwrap());\n            }\n\n            let (response, fut) = upgrade::upgrade(&mut req).unwrap();\n            num_clients.fetch_add(1, Ordering::Relaxed);\n\n            let config = context.weylus_client_config.clone();\n            tokio::spawn(async move {\n                match fut.await {\n                    Ok(ws) => {\n                        let (sender, receiver) =\n                            weylus_websocket_channel(ws, semaphore_websocket_shutdown);\n                        std::thread::spawn(move || {\n                            let client = WeylusClientHandler::new(\n                                sender,\n                                receiver,\n                                || {\n                                    if let Err(err) =\n                                        sender_ui.blocking_send(Web2UiMessage::UInputInaccessible)\n                                    {\n                                        warn!(\n                                            \"Failed to send message 'UInputInaccessible': {err}.\"\n                                        );\n                                    }\n                                },\n                                config,\n                            );\n                            client.run();\n                            num_clients.fetch_sub(1, Ordering::Relaxed);\n                            notify_disconnect.notify_waiters();\n                        });\n                    }\n                    Err(err) => {\n                        eprintln!(\"Error in websocket connection: {}\", err);\n                        num_clients.fetch_sub(1, Ordering::Relaxed);\n                        notify_disconnect.notify_waiters();\n                    }\n                }\n            });\n\n            Ok(response.map(|r| r.boxed()))\n        }\n        \"/style.css\" => Ok(response_from_path_or_default(\n            context.web_config.custom_style_css.as_ref(),\n            STYLE_CSS,\n            \"text/css; charset=utf-8\",\n        )\n        .await\n        .map(|r| r.boxed())),\n        \"/lib.js\" => Ok(response_from_path_or_default(\n            context.web_config.custom_lib_js.as_ref(),\n            LIB_JS,\n            \"text/javascript; charset=utf-8\",\n        )\n        .await\n        .map(|r| r.boxed())),\n        _ => Ok(response_not_found().map(|r| r.boxed())),\n    }\n}\n\n#[derive(Clone)]\npub struct WebServerConfig {\n    pub bind_addr: SocketAddr,\n    pub access_code: Option<String>,\n    pub custom_index_html: Option<PathBuf>,\n    pub custom_access_html: Option<PathBuf>,\n    pub custom_style_css: Option<PathBuf>,\n    pub custom_lib_js: Option<PathBuf>,\n    pub enable_custom_input_areas: bool,\n}\n\nstruct Context<'a> {\n    web_config: WebServerConfig,\n    weylus_client_config: WeylusClientConfig,\n    templates: Handlebars<'a>,\n}\n\npub fn run(\n    sender_ui: tokio::sync::mpsc::Sender<Web2UiMessage>,\n    sender_startup: oneshot::Sender<WebStartUpMessage>,\n    notify_shutdown: Arc<tokio::sync::Notify>,\n    web_server_config: WebServerConfig,\n    weylus_client_config: WeylusClientConfig,\n) -> std::thread::JoinHandle<()> {\n    let mut templates = Handlebars::new();\n    templates\n        .register_template_string(\"index\", INDEX_HTML)\n        .unwrap();\n\n    let context = Context {\n        web_config: web_server_config,\n        weylus_client_config,\n        templates,\n    };\n    std::thread::spawn(move || run_server(context, sender_ui, sender_startup, notify_shutdown))\n}\n\n#[tokio::main]\nasync fn run_server(\n    context: Context<'static>,\n    sender_ui: tokio::sync::mpsc::Sender<Web2UiMessage>,\n    sender_startup: oneshot::Sender<WebStartUpMessage>,\n    notify_shutdown: Arc<tokio::sync::Notify>,\n) {\n    let addr = context.web_config.bind_addr;\n\n    let listener = match TcpListener::bind(addr).await {\n        Ok(listener) => listener,\n        Err(err) => {\n            error!(\"Failed to bind to socket: {err}.\");\n            sender_startup.send(WebStartUpMessage::Error).unwrap();\n            return;\n        }\n    };\n\n    sender_startup.send(WebStartUpMessage::Start).unwrap();\n\n    let context = Arc::new(context);\n\n    let broadcast_shutdown = Arc::new(tokio::sync::Notify::new());\n\n    let num_clients = Arc::new(AtomicUsize::new(0));\n    let notify_disconnect = Arc::new(tokio::sync::Notify::new());\n    let semaphore_websocket_shutdown = Arc::new(tokio::sync::Semaphore::new(0));\n\n    loop {\n        let (tcp, remote_address) = tokio::select! {\n            res = listener.accept() => {\n                match res {\n                    Ok(conn) => conn,\n                    Err(err) => {\n                        warn!(\"Connection failed: {err}.\");\n                        continue;\n                    }\n                }\n            },\n            _ = notify_shutdown.notified() => {\n                info!(\"Webserver is shutting down.\");\n                broadcast_shutdown.notify_waiters();\n                break;\n            }\n        };\n\n        debug!(address = ?remote_address, \"Client connected.\");\n\n        let io = TokioIo::new(tcp);\n\n        let sender_ui = sender_ui.clone();\n        let broadcast_shutdown = broadcast_shutdown.clone();\n        let context = context.clone();\n        let num_clients = num_clients.clone();\n        let semaphore_websocket_shutdown = semaphore_websocket_shutdown.clone();\n        let notify_disconnect = notify_disconnect.clone();\n\n        tokio::task::spawn(async move {\n            let conn = http1::Builder::new().serve_connection(\n                io,\n                service_fn({\n                    move |req| {\n                        let context = context.clone();\n                        let num_clients = num_clients.clone();\n                        let semaphore_websocket_shutdown = semaphore_websocket_shutdown.clone();\n                        let notify_disconnect = notify_disconnect.clone();\n                        serve(\n                            remote_address,\n                            req,\n                            context,\n                            sender_ui.clone(),\n                            num_clients,\n                            semaphore_websocket_shutdown,\n                            notify_disconnect,\n                        )\n                    }\n                }),\n            );\n\n            let conn = conn.with_upgrades();\n\n            tokio::select! {\n                conn = conn => match conn {\n                    Ok(_) => (),\n                    Err(err) => {\n                        warn!(\"Error polling connection ({remote_address}): {err}.\")\n                    }\n                },\n                _ = broadcast_shutdown.notified() => {\n                    info!(\"Closing connection to: {remote_address}.\");\n                }\n            }\n        });\n    }\n\n    semaphore_websocket_shutdown.add_permits(num_clients.load(Ordering::Relaxed));\n\n    loop {\n        let remaining_clients = num_clients.load(Ordering::Relaxed);\n        if remaining_clients == 0 {\n            break;\n        } else {\n            debug!(\"Waiting for remaining clients ({remaining_clients}) to disconnect.\");\n        }\n        tokio::select! {\n            _ = notify_disconnect.notified() => (),\n            _ = tokio::time::sleep(Duration::from_secs(1)) => {\n                semaphore_websocket_shutdown.add_permits(num_clients.load(Ordering::Relaxed));\n            },\n        }\n    }\n}\n"
  },
  {
    "path": "src/websocket.rs",
    "content": "use fastwebsockets::{FragmentCollectorRead, Frame, OpCode, WebSocket, WebSocketError};\nuse hyper::upgrade::Upgraded;\nuse hyper_util::rt::TokioIo;\nuse std::convert::Infallible;\nuse std::sync::mpsc::RecvTimeoutError;\nuse std::sync::{mpsc, Arc};\nuse std::thread::{spawn, JoinHandle};\nuse std::time::{Duration, Instant};\nuse tokio::sync::mpsc::channel;\nuse tracing::{error, trace, warn};\n\nuse crate::capturable::{get_capturables, Capturable, Recorder};\nuse crate::input::device::{InputDevice, InputDeviceType};\nuse crate::protocol::{\n    ClientConfiguration, KeyboardEvent, MessageInbound, MessageOutbound, PointerEvent,\n    WeylusReceiver, WeylusSender, WheelEvent,\n};\n\nuse crate::cerror::CErrorCode;\nuse crate::video::{EncoderOptions, VideoEncoder};\n\nstruct VideoConfig {\n    capturable: Box<dyn Capturable>,\n    capture_cursor: bool,\n    max_width: usize,\n    max_height: usize,\n    frame_rate: f64,\n}\n\nenum VideoCommands {\n    Start(VideoConfig),\n    Pause,\n    Resume,\n    Restart,\n}\n\nfn send_message<S>(sender: &mut S, message: MessageOutbound)\nwhere\n    S: WeylusSender,\n{\n    if let Err(err) = sender.send_message(message) {\n        warn!(\"Failed to send message to client: {err}\");\n    }\n}\n\npub struct WeylusClientHandler<S, R, FnUInput> {\n    sender: S,\n    receiver: Option<R>,\n    video_sender: mpsc::Sender<VideoCommands>,\n    input_device: Option<Box<dyn InputDevice>>,\n    capturables: Vec<Box<dyn Capturable>>,\n    on_uinput_inaccessible: FnUInput,\n    config: WeylusClientConfig,\n    #[cfg(target_os = \"linux\")]\n    capture_cursor: bool,\n    client_name: Option<String>,\n    video_thread: JoinHandle<()>,\n}\n\n#[derive(Clone, Copy)]\npub struct WeylusClientConfig {\n    pub encoder_options: EncoderOptions,\n    #[cfg(target_os = \"linux\")]\n    pub wayland_support: bool,\n    pub no_gui: bool,\n}\n\nimpl<S, R, FnUInput> WeylusClientHandler<S, R, FnUInput> {\n    pub fn new(\n        sender: S,\n        receiver: R,\n        on_uinput_inaccessible: FnUInput,\n        config: WeylusClientConfig,\n    ) -> Self\n    where\n        R: WeylusReceiver,\n        S: WeylusSender + Clone + Send + Sync + 'static,\n    {\n        let (video_sender, video_receiver) = mpsc::channel::<VideoCommands>();\n        let video_thread = {\n            let sender = sender.clone();\n            // offload creating the videostream to another thread to avoid blocking the thread that\n            // is receiving messages from the websocket\n            spawn(move || handle_video(video_receiver, sender, config.encoder_options))\n        };\n\n        Self {\n            sender,\n            receiver: Some(receiver),\n            video_sender,\n            input_device: None,\n            capturables: vec![],\n            on_uinput_inaccessible,\n            config,\n            #[cfg(target_os = \"linux\")]\n            capture_cursor: false,\n            client_name: None,\n            video_thread,\n        }\n    }\n\n    pub fn run(mut self)\n    where\n        R: WeylusReceiver,\n        S: WeylusSender + Clone + Send + Sync + 'static,\n        FnUInput: Fn(),\n    {\n        for message in self.receiver.take().unwrap() {\n            match message {\n                Ok(message) => {\n                    trace!(\"Received message: {message:?}\");\n                    match message {\n                        MessageInbound::PointerEvent(event) => self.process_pointer_event(&event),\n                        MessageInbound::WheelEvent(event) => self.process_wheel_event(&event),\n                        MessageInbound::KeyboardEvent(event) => self.process_keyboard_event(&event),\n                        MessageInbound::GetCapturableList => self.send_capturable_list(),\n                        MessageInbound::Config(config) => self.update_config(config),\n                        MessageInbound::PauseVideo => {\n                            self.video_sender.send(VideoCommands::Pause).unwrap()\n                        }\n                        MessageInbound::ResumeVideo => {\n                            self.video_sender.send(VideoCommands::Resume).unwrap()\n                        }\n                        MessageInbound::RestartVideo => {\n                            self.video_sender.send(VideoCommands::Restart).unwrap()\n                        }\n                        MessageInbound::ChooseCustomInputAreas => {\n                            let (sender, receiver) = std::sync::mpsc::channel();\n                            crate::gui::get_input_area(self.config.no_gui, sender);\n                            let mut sender = self.sender.clone();\n                            spawn(move || {\n                                while let Ok(areas) = receiver.recv() {\n                                    send_message(\n                                        &mut sender,\n                                        MessageOutbound::CustomInputAreas(areas),\n                                    );\n                                }\n                            });\n                        }\n                    }\n                }\n                Err(err) => {\n                    warn!(\"Failed to read message {err}!\");\n                    self.send_message(MessageOutbound::Error(\n                        \"Failed to read message!\".to_string(),\n                    ));\n                }\n            }\n        }\n\n        drop(self.video_sender);\n        if let Err(err) = self.video_thread.join() {\n            warn!(\"Failed to join video thread: {err:?}\");\n        }\n    }\n\n    fn send_message(&mut self, message: MessageOutbound)\n    where\n        S: WeylusSender,\n    {\n        send_message(&mut self.sender, message)\n    }\n\n    fn process_wheel_event(&mut self, event: &WheelEvent) {\n        match &mut self.input_device {\n            Some(i) => i.send_wheel_event(event),\n            None => warn!(\"Input device is not initalized, can not process WheelEvent!\"),\n        }\n    }\n\n    fn process_pointer_event(&mut self, event: &PointerEvent) {\n        if self.input_device.is_some() {\n            self.input_device\n                .as_mut()\n                .unwrap()\n                .send_pointer_event(event)\n        } else {\n            warn!(\"Input device is not initalized, can not process PointerEvent!\");\n        }\n    }\n\n    fn process_keyboard_event(&mut self, event: &KeyboardEvent) {\n        if self.input_device.is_some() {\n            self.input_device\n                .as_mut()\n                .unwrap()\n                .send_keyboard_event(event)\n        } else {\n            warn!(\"Input device is not initalized, can not process KeyboardEvent!\");\n        }\n    }\n\n    fn send_capturable_list(&mut self)\n    where\n        S: WeylusSender,\n    {\n        let mut windows = Vec::<String>::new();\n        self.capturables = get_capturables(\n            #[cfg(target_os = \"linux\")]\n            self.config.wayland_support,\n            #[cfg(target_os = \"linux\")]\n            self.capture_cursor,\n        );\n        self.capturables.iter().for_each(|c| {\n            windows.push(c.name());\n        });\n        self.send_message(MessageOutbound::CapturableList(windows));\n    }\n\n    fn update_config(&mut self, config: ClientConfiguration)\n    where\n        S: WeylusSender,\n        FnUInput: Fn(),\n    {\n        let client_name_changed = if self.client_name != config.client_name {\n            self.client_name = config.client_name;\n            true\n        } else {\n            false\n        };\n        if config.capturable_id < self.capturables.len() {\n            let capturable = self.capturables[config.capturable_id].clone();\n\n            #[cfg(target_os = \"linux\")]\n            {\n                self.capture_cursor = config.capture_cursor;\n            }\n\n            #[cfg(target_os = \"linux\")]\n            if config.uinput_support {\n                if self.input_device.as_ref().map_or(true, |d| {\n                    client_name_changed || d.device_type() != InputDeviceType::UInputDevice\n                }) {\n                    let device = crate::input::uinput_device::UInputDevice::new(\n                        capturable.clone(),\n                        &self.client_name,\n                    );\n                    match device {\n                        Ok(d) => self.input_device = Some(Box::new(d)),\n                        Err(e) => {\n                            error!(\"Failed to create uinput device: {}\", e);\n                            if let CErrorCode::UInputNotAccessible = e.to_enum() {\n                                (self.on_uinput_inaccessible)();\n                            }\n                            self.send_message(MessageOutbound::ConfigError(\n                                \"Failed to create uinput device!\".to_string(),\n                            ));\n                            return;\n                        }\n                    }\n                } else if let Some(d) = self.input_device.as_mut() {\n                    d.set_capturable(capturable.clone());\n                }\n            } else if self.input_device.as_ref().map_or(true, |d| {\n                d.device_type() != InputDeviceType::AutoPilotDevice\n            }) {\n                self.input_device = Some(Box::new(\n                    crate::input::autopilot_device::AutoPilotDevice::new(capturable.clone()),\n                ));\n            } else if let Some(d) = self.input_device.as_mut() {\n                d.set_capturable(capturable.clone());\n            }\n\n            #[cfg(target_os = \"macos\")]\n            if self.input_device.is_none() {\n                self.input_device = Some(Box::new(\n                    crate::input::autopilot_device::AutoPilotDevice::new(capturable.clone()),\n                ));\n            } else {\n                self.input_device\n                    .as_mut()\n                    .map(|d| d.set_capturable(capturable.clone()));\n            }\n            #[cfg(target_os = \"windows\")]\n            if self.input_device.is_none() {\n                self.input_device = Some(Box::new(\n                    crate::input::autopilot_device_win::WindowsInput::new(capturable.clone()),\n                ));\n            } else {\n                self.input_device\n                    .as_mut()\n                    .map(|d| d.set_capturable(capturable.clone()));\n            }\n\n            self.video_sender\n                .send(VideoCommands::Start(VideoConfig {\n                    capturable,\n                    capture_cursor: config.capture_cursor,\n                    max_width: config.max_width,\n                    max_height: config.max_height,\n                    frame_rate: config.frame_rate,\n                }))\n                .unwrap();\n        } else {\n            error!(\"Got invalid id for capturable: {}\", config.capturable_id);\n            self.send_message(MessageOutbound::ConfigError(\n                \"Invalid id for capturable!\".to_string(),\n            ));\n        }\n    }\n}\n\nfn handle_video<S: WeylusSender + Clone + 'static>(\n    receiver: mpsc::Receiver<VideoCommands>,\n    mut sender: S,\n    encoder_options: EncoderOptions,\n) {\n    const EFFECTIVE_INIFINITY: Duration = Duration::from_secs(3600 * 24 * 365 * 200);\n\n    let mut recorder: Option<Box<dyn Recorder>> = None;\n    let mut video_encoder: Option<Box<VideoEncoder>> = None;\n\n    let mut max_width = 1920;\n    let mut max_height = 1080;\n    let mut frame_duration = EFFECTIVE_INIFINITY;\n    let mut last_frame = Instant::now();\n    let mut paused = false;\n\n    loop {\n        let now = Instant::now();\n        let elapsed = now - last_frame;\n        let frames_passed = (elapsed.as_secs_f64() / frame_duration.as_secs_f64()) as u32;\n        let next_frame = last_frame + (frames_passed + 1) * frame_duration;\n        let timeout = next_frame - now;\n        last_frame = next_frame;\n\n        if frames_passed > 0 {\n            trace!(\"Dropped {frames_passed} frame(s)!\");\n        }\n\n        match receiver.recv_timeout(if paused { EFFECTIVE_INIFINITY } else { timeout }) {\n            Ok(VideoCommands::Start(config)) => {\n                #[allow(unused_assignments)]\n                {\n                    // gstpipewire can not handle setting a pipeline's state to Null after another\n                    // pipeline has been created and its state has been set to Play.\n                    // This line makes sure that there always is only a single recorder and thus\n                    // single pipeline in this thread by forcing rust to call the destructor of the\n                    // current pipeline here, right before creating a new pipeline.\n                    // See: https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/986\n                    //\n                    // This shouldn't affect other Recorder trait objects.\n                    recorder = None;\n                }\n                match config.capturable.recorder(config.capture_cursor) {\n                    Ok(r) => {\n                        recorder = Some(r);\n                        max_width = config.max_width;\n                        max_height = config.max_height;\n                        send_message(&mut sender, MessageOutbound::ConfigOk);\n                    }\n                    Err(err) => {\n                        warn!(\"Failed to init screen cast: {}!\", err);\n                        send_message(\n                            &mut sender,\n                            MessageOutbound::Error(\"Failed to init screen cast!\".into()),\n                        )\n                    }\n                }\n                last_frame = Instant::now();\n\n                // The Duration type can not handle infinity, if the frame rate is set to 0 we just\n                // set the duration between two frames to a very long one, which is effectively\n                // infinity.\n                let d = 1.0 / config.frame_rate;\n                frame_duration = if d.is_finite() {\n                    Duration::from_secs_f64(d)\n                } else {\n                    EFFECTIVE_INIFINITY\n                };\n                frame_duration = frame_duration.min(EFFECTIVE_INIFINITY);\n            }\n            Ok(VideoCommands::Pause) => {\n                paused = true;\n            }\n            Ok(VideoCommands::Resume) => {\n                paused = false;\n            }\n            Ok(VideoCommands::Restart) => {\n                video_encoder = None;\n            }\n            Err(RecvTimeoutError::Timeout) => {\n                if recorder.is_none() {\n                    warn!(\"Screen capture not initalized, can not send video frame!\");\n                    continue;\n                }\n                let pixel_data = recorder.as_mut().unwrap().capture();\n                if let Err(err) = pixel_data {\n                    warn!(\"Error capturing screen: {}\", err);\n                    continue;\n                }\n                let pixel_data = pixel_data.unwrap();\n                let (width_in, height_in) = pixel_data.size();\n                let scale =\n                    (max_width as f64 / width_in as f64).min(max_height as f64 / height_in as f64);\n                // limit video to 4K\n                let scale_max = (3840.0 / width_in as f64).min(2160.0 / height_in as f64);\n                let scale = scale.min(scale_max);\n                let mut width_out = width_in;\n                let mut height_out = height_in;\n                if scale < 1.0 {\n                    width_out = (width_out as f64 * scale) as usize;\n                    height_out = (height_out as f64 * scale) as usize;\n                }\n                // video encoder is not setup or setup for encoding the wrong size: restart it\n                if video_encoder.is_none()\n                    || !video_encoder\n                        .as_ref()\n                        .unwrap()\n                        .check_size(width_in, height_in, width_out, height_out)\n                {\n                    send_message(&mut sender, MessageOutbound::NewVideo);\n                    let mut sender = sender.clone();\n                    let res = VideoEncoder::new(\n                        width_in,\n                        height_in,\n                        width_out,\n                        height_out,\n                        move |data| {\n                            if let Err(err) = sender.send_video(data) {\n                                warn!(\"Failed to send video frame: {err}!\");\n                            }\n                        },\n                        encoder_options,\n                    );\n                    match res {\n                        Ok(r) => video_encoder = Some(r),\n                        Err(e) => {\n                            warn!(\"{}\", e);\n                            continue;\n                        }\n                    };\n                }\n                let video_encoder = video_encoder.as_mut().unwrap();\n                video_encoder.encode(pixel_data);\n            }\n            // stop thread once the channel is closed\n            Err(RecvTimeoutError::Disconnected) => return,\n        };\n    }\n}\n\npub struct WsWeylusReceiver {\n    recv: tokio::sync::mpsc::Receiver<MessageInbound>,\n}\n\nimpl Iterator for WsWeylusReceiver {\n    type Item = Result<MessageInbound, Infallible>;\n\n    fn next(&mut self) -> Option<Self::Item> {\n        self.recv.blocking_recv().map(Ok)\n    }\n}\n\nimpl WeylusReceiver for WsWeylusReceiver {\n    type Error = Infallible;\n}\n\npub enum WsMessage {\n    Frame(Frame<'static>),\n    Video(Vec<u8>),\n    MessageOutbound(MessageOutbound),\n}\n\nunsafe impl Send for WsMessage {}\n\n#[derive(Clone)]\npub struct WsWeylusSender {\n    sender: tokio::sync::mpsc::Sender<WsMessage>,\n}\n\nimpl WeylusSender for WsWeylusSender {\n    type Error = tokio::sync::mpsc::error::SendError<WsMessage>;\n\n    fn send_message(&mut self, message: MessageOutbound) -> Result<(), Self::Error> {\n        self.sender\n            .blocking_send(WsMessage::MessageOutbound(message))\n    }\n\n    fn send_video(&mut self, bytes: &[u8]) -> Result<(), Self::Error> {\n        self.sender.blocking_send(WsMessage::Video(bytes.to_vec()))\n    }\n}\n\npub fn weylus_websocket_channel(\n    websocket: WebSocket<TokioIo<Upgraded>>,\n    semaphore_shutdown: Arc<tokio::sync::Semaphore>,\n) -> (WsWeylusSender, WsWeylusReceiver) {\n    let (rx, mut tx) = websocket.split(|ws| tokio::io::split(ws));\n\n    let mut rx = FragmentCollectorRead::new(rx);\n\n    let (sender_inbound, receiver_inbound) = channel::<MessageInbound>(32);\n    let (sender_outbound, mut receiver_outbound) = channel::<WsMessage>(32);\n\n    {\n        let sender_outbound = sender_outbound.clone();\n        tokio::spawn(async move {\n            let mut send_fn = |frame| async {\n                if let Err(err) = sender_outbound.send(WsMessage::Frame(frame)).await {\n                    warn!(\"Failed to send websocket frame while receiving fragmented frame: {err}.\")\n                };\n                Ok(())\n            };\n\n            loop {\n                let fut = rx.read_frame::<_, WebSocketError>(&mut send_fn);\n\n                let frame = tokio::select! {\n                    _ = semaphore_shutdown.acquire() => break,\n                    frame = fut => match frame {\n                        Ok(frame) => frame,\n                        Err(err) => {\n                            warn!(\"Invalid websocket frame: {err}.\");\n                            break;\n                        },\n                    },\n                };\n                match frame.opcode {\n                    OpCode::Close => break,\n                    OpCode::Text => match serde_json::from_slice(&frame.payload) {\n                        Ok(msg) => {\n                            if let Err(err) = sender_inbound.send(msg).await {\n                                warn!(\"Failed to forward inbound message to WeylusClientHandler: {err}.\");\n                            }\n                        }\n                        Err(err) => warn!(\"Failed to parse message: {err}\"),\n                    },\n                    _ => {}\n                }\n            }\n        });\n    }\n\n    tokio::spawn(async move {\n        loop {\n            let msg = if let Some(msg) = receiver_outbound.recv().await {\n                msg\n            } else {\n                break;\n            };\n\n            match msg {\n                WsMessage::Frame(frame) => {\n                    if let Err(err) = tx.write_frame(frame).await {\n                        if let WebSocketError::ConnectionClosed = err {\n                            break;\n                        }\n                        warn!(\"Failed to send frame: {err}\");\n                    }\n                }\n                WsMessage::Video(data) => {\n                    if let Err(err) = tx.write_frame(Frame::binary(data.into())).await {\n                        if let WebSocketError::ConnectionClosed = err {\n                            break;\n                        }\n                        warn!(\"Failed to send video frame: {err}\");\n                    }\n                }\n                WsMessage::MessageOutbound(msg) => {\n                    let json_string = serde_json::to_string(&msg).unwrap();\n                    let data = json_string.as_bytes();\n                    if let Err(err) = tx.write_frame(Frame::text(data.into())).await {\n                        if let WebSocketError::ConnectionClosed = err {\n                            break;\n                        }\n                        warn!(\"Failed to send outbound message: {err}\");\n                    }\n                }\n            }\n        }\n    });\n\n    (\n        WsWeylusSender {\n            sender: sender_outbound,\n        },\n        WsWeylusReceiver {\n            recv: receiver_inbound,\n        },\n    )\n}\n"
  },
  {
    "path": "src/weylus.rs",
    "content": "use std::net::SocketAddr;\nuse std::sync::Arc;\nuse tracing::error;\n\nuse crate::config::Config;\nuse crate::video::EncoderOptions;\nuse crate::web::{Web2UiMessage, WebServerConfig, WebStartUpMessage};\nuse crate::websocket::WeylusClientConfig;\n\npub struct Weylus {\n    notify_shutdown: Arc<tokio::sync::Notify>,\n    web_thread: Option<std::thread::JoinHandle<()>>,\n}\n\nimpl Weylus {\n    pub fn new() -> Self {\n        Self {\n            notify_shutdown: Arc::new(tokio::sync::Notify::new()),\n            web_thread: None,\n        }\n    }\n\n    pub fn start(\n        &mut self,\n        config: &Config,\n        mut on_web_message: impl FnMut(Web2UiMessage) + Send + 'static,\n    ) -> bool {\n        let encoder_options = EncoderOptions {\n            #[cfg(target_os = \"linux\")]\n            try_vaapi: config.try_vaapi,\n            #[cfg(not(target_os = \"linux\"))]\n            try_vaapi: false,\n\n            #[cfg(any(target_os = \"linux\", target_os = \"windows\"))]\n            try_nvenc: config.try_nvenc,\n            #[cfg(not(any(target_os = \"linux\", target_os = \"windows\")))]\n            try_nvenc: false,\n\n            #[cfg(target_os = \"macos\")]\n            try_videotoolbox: config.try_videotoolbox,\n            #[cfg(not(target_os = \"macos\"))]\n            try_videotoolbox: false,\n\n            #[cfg(target_os = \"windows\")]\n            try_mediafoundation: config.try_mediafoundation,\n            #[cfg(not(target_os = \"windows\"))]\n            try_mediafoundation: false,\n        };\n\n        let (sender_ui, mut receiver_ui) = tokio::sync::mpsc::channel(100);\n        let (sender_startup, receiver_startup) = tokio::sync::oneshot::channel();\n\n        let web_thread = crate::web::run(\n            sender_ui,\n            sender_startup,\n            self.notify_shutdown.clone(),\n            WebServerConfig {\n                bind_addr: SocketAddr::new(config.bind_address, config.web_port),\n                access_code: config.access_code.clone(),\n                custom_index_html: config.custom_index_html.clone(),\n                custom_access_html: config.custom_access_html.clone(),\n                custom_style_css: config.custom_style_css.clone(),\n                custom_lib_js: config.custom_lib_js.clone(),\n                #[cfg(target_os = \"linux\")]\n                enable_custom_input_areas: config.wayland_support,\n                #[cfg(not(target_os = \"linux\"))]\n                enable_custom_input_areas: false,\n            },\n            WeylusClientConfig {\n                encoder_options,\n                #[cfg(target_os = \"linux\")]\n                wayland_support: config.wayland_support,\n                no_gui: config.no_gui,\n            },\n        );\n\n        match receiver_startup.blocking_recv() {\n            Ok(WebStartUpMessage::Start) => (),\n            Ok(WebStartUpMessage::Error) => {\n                if web_thread.join().is_err() {\n                    error!(\"Webserver thread panicked.\");\n                }\n                return false;\n            }\n            Err(err) => {\n                error!(\"Error communicating with webserver thread: {}\", err);\n                if web_thread.join().is_err() {\n                    error!(\"Webserver thread panicked.\");\n                }\n                return false;\n            }\n        }\n        self.web_thread = Some(web_thread);\n        std::thread::spawn(move || {\n            while let Some(msg) = receiver_ui.blocking_recv() {\n                on_web_message(msg);\n            }\n        });\n        true\n    }\n\n    pub fn stop(&mut self) {\n        self.notify_shutdown.notify_one();\n        self.wait();\n    }\n\n    fn wait(&mut self) {\n        if let Some(t) = self.web_thread.take() {\n            if t.join().is_err() {\n                error!(\"Web thread panicked.\");\n            }\n        }\n    }\n}\n\nimpl Drop for Weylus {\n    fn drop(&mut self) {\n        self.stop();\n    }\n}\n"
  },
  {
    "path": "ts/lib.ts",
    "content": "interface Window {\n    ManagedMediaSource: any;\n}\n\nenum LogLevel {\n    ERROR = 0,\n    WARN,\n    INFO,\n    DEBUG,\n    TRACE,\n}\n\nlet log_pre: HTMLPreElement;\nlet log_level: LogLevel = LogLevel.ERROR;\nlet no_log_messages: boolean = true;\n\nlet fps_out: HTMLOutputElement;\nlet frame_count = 0;\nlet last_fps_calc: number = performance.now();\n\nlet check_video: HTMLInputElement;\n\nfunction run(level: string) {\n    window.onload = () => {\n        log_pre = document.getElementById(\"log\") as HTMLPreElement;\n        log_pre.textContent = \"\";\n        log_level = LogLevel[level];\n        fps_out = document.getElementById(\"fps\") as HTMLOutputElement;\n        check_video = document.getElementById(\"enable_video\") as HTMLInputElement;\n        window.addEventListener(\"error\", (e: ErrorEvent | Event | UIEvent) => {\n            if ((e as ErrorEvent).error) {\n                let err = e as ErrorEvent;\n                log(LogLevel.ERROR, err.filename + \":L\" + err.lineno + \":\" + err.colno + \": \" + err.message + \" Error object: \" + JSON.stringify(err.error));\n            } else if ((e as Event | UIEvent).target) {\n                let ev = e as Event;\n                let src = (e.target as any).src;\n                if (ev.target instanceof HTMLVideoElement)\n                    log(LogLevel.ERROR, \"Failed to decode video, try reducing resolution or disabling hardware acceleration and reload the page. Error src: \" + src);\n                else\n                    log(LogLevel.ERROR, \"Failed to obtain resource, target: \" + ev.target + \" type: \" + ev.type + \" src: \" + src + \" Error object: \" + JSON.stringify(ev));\n            } else {\n                log(LogLevel.WARN, \"Got unknown event: \" + JSON.stringify(e));\n            }\n            return false;\n        }, true)\n        init();\n    };\n}\n\nfunction log(level: LogLevel, msg: string) {\n    if (level > log_level)\n        return;\n\n    if (level == LogLevel.TRACE)\n        console.trace(msg);\n    else if (level == LogLevel.DEBUG)\n        console.debug(msg);\n    else if (level == LogLevel.INFO)\n        console.info(msg);\n    else if (level == LogLevel.WARN)\n        console.warn(msg);\n    else if (level == LogLevel.ERROR)\n        console.error(msg);\n\n    if (no_log_messages) {\n        no_log_messages = false;\n        document.getElementById(\"log_section\").classList.remove(\"hide\");\n    }\n    log_pre.textContent += LogLevel[level] + \": \" + msg + \"\\n\";\n}\n\nfunction frame_rate_scale(x: number) {\n    return Math.pow(x / 100, 1.5);\n}\n\nfunction frame_rate_scale_inv(x: number) {\n    return 100 * Math.pow(x, 2 / 3);\n}\n\n\nfunction calc_max_video_resolution(scale: number) {\n    return [\n        Math.round(scale * window.innerWidth * window.devicePixelRatio),\n        Math.round(scale * window.innerHeight * window.devicePixelRatio)\n    ];\n}\n\nfunction fresh_canvas() {\n    let canvas_old = document.getElementById(\"canvas\");\n    let canvas = document.createElement(\"canvas\");\n    canvas.id = canvas_old.id;\n    canvas_old.classList.forEach((cls) => canvas.classList.add(cls));\n    canvas_old.replaceWith(canvas);\n    return canvas;\n}\n\nclass Rect {\n    x: number;\n    y: number;\n    w: number;\n    h: number;\n}\n\nclass CustomInputAreas {\n    mouse: Rect;\n    touch: Rect;\n    pen: Rect;\n}\n\nclass Settings {\n    webSocket: WebSocket;\n    checks: Map<string, HTMLInputElement>;\n    capturable_select: HTMLSelectElement;\n    frame_rate_input: HTMLInputElement;\n    frame_rate_output: HTMLOutputElement;\n    scale_video_input: HTMLInputElement;\n    scale_video_output: HTMLOutputElement;\n    range_min_pressure: HTMLInputElement;\n    check_aggressive_seek: HTMLInputElement;\n    client_name_input: HTMLInputElement;\n    visible: boolean;\n    custom_input_areas: CustomInputAreas;\n    settings: HTMLElement;\n\n    constructor(webSocket: WebSocket) {\n        this.webSocket = webSocket;\n        this.checks = new Map<string, HTMLInputElement>();\n        this.capturable_select = document.getElementById(\"window\") as HTMLSelectElement;\n        this.frame_rate_input = document.getElementById(\"frame_rate\") as HTMLInputElement;\n        this.frame_rate_input.min = frame_rate_scale_inv(0).toString();\n        this.frame_rate_input.max = frame_rate_scale_inv(120).toString();\n        this.frame_rate_output = this.frame_rate_input.nextElementSibling as HTMLOutputElement;\n        this.scale_video_input = document.getElementById(\"scale_video\") as HTMLInputElement;\n        this.scale_video_output = this.scale_video_input.nextElementSibling as HTMLOutputElement;\n        this.range_min_pressure = document.getElementById(\"min_pressure\") as HTMLInputElement;\n        this.client_name_input = document.getElementById(\"client_name\") as HTMLInputElement;\n        this.frame_rate_input.oninput = () => {\n            this.frame_rate_output.value = Math.round(frame_rate_scale(this.frame_rate_input.valueAsNumber)).toString();\n        }\n        this.scale_video_input.oninput = () => {\n            let [w, h] = calc_max_video_resolution(this.scale_video_input.valueAsNumber)\n            this.scale_video_output.value = w + \"x\" + h\n        }\n        this.visible = true;\n\n        // Settings UI\n        this.settings = document.getElementById(\"settings\");\n        this.settings.onclick = (e) => e.stopPropagation();\n        let handle = document.getElementById(\"handle\");\n\n        // Settings elements\n        this.settings.querySelectorAll(\"input[type=checkbox]\").forEach(\n            (elem, _key, _parent) => this.checks.set(elem.id, elem as HTMLInputElement)\n        );\n\n        this.load_settings();\n\n        // event handling\n\n        // client only\n        handle.onclick = () => { this.toggle() };\n        this.checks.get(\"lefty\").onchange = (e) => {\n            if ((e.target as HTMLInputElement).checked)\n                this.settings.classList.add(\"lefty\");\n            else\n                this.settings.classList.remove(\"lefty\");\n            this.save_settings();\n        }\n\n        document.getElementById(\"vanish\").onclick = () => {\n            this.settings.classList.add(\"vanish\");\n        }\n\n        this.checks.get(\"stretch\").onchange = () => {\n            stretch_video();\n            this.save_settings();\n        };\n\n        this.checks.get(\"enable_debug_overlay\").onchange = (e) => {\n            let enabled = (e.target as HTMLInputElement).checked;\n            if (enabled) {\n                debug_overlay.classList.remove(\"hide\");\n            } else {\n                debug_overlay.classList.add(\"hide\");\n            }\n            this.save_settings();\n        };\n\n        this.check_aggressive_seek = this.checks.get(\"aggressive_seeking\");\n        this.check_aggressive_seek.onchange = () => {\n            this.save_settings();\n        };\n\n        this.checks.get(\"enable_video\").onchange = (e) => {\n            let enabled = (e.target as HTMLInputElement).checked;\n            document.getElementById(\"video\").classList.toggle(\"vanish\", !enabled);\n            document.getElementById(\"canvas\").classList.toggle(\"vanish\", enabled);\n            this.save_settings();\n            if (enabled) {\n                this.webSocket.send('\"ResumeVideo\"');\n            } else {\n                this.webSocket.send('\"PauseVideo\"');\n            }\n        }\n\n        let upd_pointer = () => {\n            this.save_settings();\n            new PointerHandler(this.webSocket);\n        }\n        this.checks.get(\"enable_mouse\").onchange = upd_pointer;\n        this.checks.get(\"enable_stylus\").onchange = upd_pointer;\n        this.checks.get(\"enable_touch\").onchange = upd_pointer;\n\n        this.checks.get(\"energysaving\").onchange = (e) => {\n            this.save_settings();\n            this.toggle_energysaving((e.target as HTMLInputElement).checked);\n        };\n\n        this.checks.get(\"enable_custom_input_areas\").onchange = () => {\n            this.save_settings();\n        };\n\n        this.frame_rate_input.onchange = () => this.save_settings();\n        this.range_min_pressure.onchange = () => this.save_settings();\n\n        // server\n        let upd_server_config = () => { this.save_settings(); this.send_server_config() };\n        this.checks.get(\"uinput_support\").onchange = upd_server_config;\n        this.checks.get(\"capture_cursor\").onchange = upd_server_config;\n        this.scale_video_input.onchange = upd_server_config;\n        this.client_name_input.onchange = upd_server_config;\n        this.frame_rate_input.onchange = upd_server_config;\n\n        document.getElementById(\"refresh\").onclick = () => this.webSocket.send('\"GetCapturableList\"');\n        document.getElementById(\"custom_input_areas\").onclick = () => {\n            this.webSocket.send('\"ChooseCustomInputAreas\"');\n        };\n        this.capturable_select.onchange = () => this.send_server_config();\n    }\n\n    send_server_config() {\n        let config = new Object(null);\n        config[\"capturable_id\"] = Number(this.capturable_select.value);\n        for (const key of [\n            \"uinput_support\",\n            \"capture_cursor\"])\n            config[key] = this.checks.get(key).checked;\n        let [w, h] = calc_max_video_resolution(this.scale_video_input.valueAsNumber);\n        config[\"max_width\"] = w;\n        config[\"max_height\"] = h;\n        config[\"frame_rate\"] = frame_rate_scale(this.frame_rate_input.valueAsNumber);\n        if (this.client_name_input.value)\n            config[\"client_name\"] = this.client_name_input.value;\n        this.webSocket.send(JSON.stringify({ \"Config\": config }));\n    }\n\n    save_settings() {\n        let settings = Object(null);\n        for (const [key, elem] of this.checks.entries())\n            settings[key] = elem.checked;\n        settings[\"frame_rate\"] = frame_rate_scale(this.frame_rate_input.valueAsNumber).toString();\n        settings[\"scale_video\"] = this.scale_video_input.value;\n        settings[\"min_pressure\"] = this.range_min_pressure.value;\n        settings[\"custom_input_areas\"] = this.custom_input_areas;\n        settings[\"client_name\"] = this.client_name_input.value;\n        localStorage.setItem(\"settings\", JSON.stringify(settings));\n    }\n\n    load_settings() {\n        let settings_string = localStorage.getItem(\"settings\");\n        if (settings_string === null) {\n            this.frame_rate_input.value = frame_rate_scale_inv(30).toString();\n            this.frame_rate_output.value = (30).toString();\n            let [w, h] = calc_max_video_resolution(this.scale_video_input.valueAsNumber)\n            this.scale_video_output.value = w + \"x\" + h;\n            return;\n        }\n        try {\n            let settings = JSON.parse(settings_string);\n            for (const [key, elem] of this.checks.entries()) {\n                if (typeof settings[key] === \"boolean\")\n                    elem.checked = settings[key];\n            }\n            let upd_limit = settings[\"frame_rate\"];\n            if (upd_limit)\n                this.frame_rate_input.value = frame_rate_scale_inv(upd_limit).toString();\n            else\n                this.frame_rate_input.value = frame_rate_scale_inv(30).toString();\n            this.frame_rate_output.value = Math.round(frame_rate_scale(this.frame_rate_input.valueAsNumber)).toString();\n\n            let scale_video = settings[\"scale_video\"];\n            if (scale_video)\n                this.scale_video_input.value = scale_video;\n            let [w, h] = calc_max_video_resolution(this.scale_video_input.valueAsNumber);\n            this.scale_video_output.value = w + \"x\" + h;\n\n            let min_pressure = settings[\"min_pressure\"];\n            if (min_pressure)\n                this.range_min_pressure.value = min_pressure;\n\n            this.custom_input_areas = settings[\"custom_input_areas\"];\n\n            if (this.checks.get(\"lefty\").checked) {\n                this.settings.classList.add(\"lefty\");\n            }\n\n            if (!this.checks.get(\"enable_video\").checked || this.checks.get(\"energysaving\").checked) {\n                this.checks.get(\"enable_video\").checked = false;\n                if (this.checks.get(\"energysaving\").checked)\n                    this.checks.get(\"enable_video\").disabled = true;\n                document.getElementById(\"video\").classList.add(\"vanish\");\n                document.getElementById(\"canvas\").classList.remove(\"vanish\");\n            }\n\n            if (this.checks.get(\"energysaving\").checked) {\n                this.toggle_energysaving(true);\n            }\n\n            if (this.checks.get(\"enable_debug_overlay\").checked) {\n                debug_overlay.classList.remove(\"hide\");\n            }\n\n\n            if (document.getElementById(\"custom_input_areas\").classList.contains(\"hide\")) {\n                this.checks.get(\"enable_custom_input_areas\").checked = false;\n            }\n\n            let client_name = settings[\"client_name\"];\n            if (client_name)\n                this.client_name_input.value = client_name;\n\n        } catch {\n            log(LogLevel.DEBUG, \"Failed to load settings.\")\n            return;\n        }\n    }\n\n    stretched_video() {\n        return this.checks.get(\"stretch\").checked\n    }\n\n    pointer_types() {\n        let ptrs = [];\n        if (this.checks.get(\"enable_mouse\").checked)\n            ptrs.push(\"mouse\");\n        if (this.checks.get(\"enable_stylus\").checked)\n            ptrs.push(\"pen\");\n        if (this.checks.get(\"enable_touch\").checked)\n            ptrs.push(\"touch\");\n        return ptrs;\n    }\n\n    toggle() {\n        this.settings.classList.toggle(\"hide\");\n        this.visible = !this.visible;\n    }\n\n    onCapturableList(window_names: string[]) {\n        let current_selection = undefined;\n        if (this.capturable_select.selectedOptions[0])\n            current_selection = this.capturable_select.selectedOptions[0].textContent;\n        let new_index: number;\n        this.capturable_select.innerText = \"\";\n        window_names.forEach((name, i) => {\n            let option = document.createElement(\"option\");\n            option.value = String(i);\n            option.innerText = name;\n            this.capturable_select.appendChild(option);\n            if (name === current_selection)\n                new_index = i;\n        });\n        if (new_index !== undefined)\n            this.capturable_select.value = String(new_index);\n        else if (current_selection)\n            // Can't find the window, so don't select anything\n            this.capturable_select.value = \"\";\n    }\n\n    toggle_energysaving(energysaving: boolean) {\n        let canvas = fresh_canvas();\n        if (energysaving) {\n            let ctx = canvas.getContext(\"2d\");\n            ctx.fillStyle = \"#000\";\n            ctx.fillRect(0, 0, canvas.width, canvas.height);\n        }\n\n        if (energysaving) {\n            this.checks.get(\"enable_video\").checked = false;\n            this.checks.get(\"enable_video\").disabled = true;\n            this.checks.get(\"enable_video\").dispatchEvent(new Event(\"change\"));\n        } else\n            this.checks.get(\"enable_video\").disabled = false;\n        if (settings)\n            new PointerHandler(this.webSocket);\n    }\n\n    video_enabled(): boolean {\n        return this.checks.get(\"enable_video\").checked;\n    }\n}\n\nlet settings: Settings;\nlet debug_overlay: HTMLElement;\nlet last_pointer_data: Object;\n\nclass PEvent {\n    event_type: string;\n    pointer_id: number;\n    timestamp: number;\n    is_primary: boolean;\n    pointer_type: string;\n    button: number;\n    buttons: number;\n    x: number;\n    y: number;\n    // movement_x: number;\n    // movement_y: number;\n    pressure: number;\n    tilt_x: number;\n    tilt_y: number;\n    twist: number;\n    width: number;\n    height: number;\n\n    constructor(eventType: string, event: PointerEvent, targetRect: DOMRect) {\n        let diag_len = Math.sqrt(targetRect.width * targetRect.width + targetRect.height * targetRect.height)\n        this.event_type = eventType.toString();\n        this.pointer_id = event.pointerId;\n        this.timestamp = Math.round(event.timeStamp * 1000);\n        this.is_primary = event.isPrimary;\n        this.pointer_type = event.pointerType;\n        let btn = event.button;\n        // for some reason the secondary and auxiliary buttons are ordered differently for\n        // the button and buttons properties\n        if (btn == 2)\n            btn = 1;\n        else if (btn == 1)\n            btn = 2;\n        this.button = (btn < 0 ? 0 : 1 << btn);\n        this.buttons = event.buttons;\n        let x_offset = 0;\n        let y_offset = 0;\n        let x_scale = 1;\n        let y_scale = 1;\n        if (settings.checks.get(\"enable_custom_input_areas\").checked) {\n            let custom_input_area: Rect = null;\n            if (event.pointerType == \"mouse\") {\n                custom_input_area = settings.custom_input_areas.mouse;\n            } else if (event.pointerType == \"touch\") {\n                custom_input_area = settings.custom_input_areas.touch;\n            } else if (event.pointerType == \"pen\") {\n                custom_input_area = settings.custom_input_areas.pen;\n            }\n            if (custom_input_area) {\n                x_scale = custom_input_area.w;\n                y_scale = custom_input_area.h;\n                x_offset = custom_input_area.x;\n                y_offset = custom_input_area.y;\n            }\n        }\n        this.x = (event.clientX - targetRect.left) / targetRect.width * x_scale + x_offset;\n        this.y = (event.clientY - targetRect.top) / targetRect.height * y_scale + y_offset;\n        // this.movement_x = event.movementX ? event.movementX : 0;\n        // this.movement_y = event.movementY ? event.movementY : 0;\n        this.pressure = Math.max(event.pressure, settings.range_min_pressure.valueAsNumber);\n        this.tilt_x = event.tiltX;\n        this.tilt_y = event.tiltY;\n        this.width = event.width / diag_len;\n        this.height = event.height / diag_len;\n        this.twist = event.twist;\n    }\n}\n\nclass WEvent {\n    dx: number;\n    dy: number;\n    timestamp: number;\n\n    constructor(event: WheelEvent) {\n        /* The WheelEvent can have different scrolling modes that affect how much scrolling\n         * should be done. Unfortunately there is not always a way to accurately convert the scroll\n         * distance into pixels. Thus the following is a guesstimate and scales the WheelEvent's\n         * deltaX/Y values accordingly.\n         */\n        let scale = 1;\n        switch (event.deltaMode) {\n            case 0x01: // DOM_DELTA_LINE\n                scale = 10;\n                break;\n            case 0x02: // DOM_DELTA_PAGE\n                scale = 1000;\n                break;\n            default: // DOM_DELTA_PIXEL\n        }\n        this.dx = Math.round(scale * event.deltaX);\n        this.dy = Math.round(scale * event.deltaY);\n        this.timestamp = Math.round(event.timeStamp * 1000);\n    }\n}\n\n// in milliseconds\nconst fade_time = 5000;\n\nconst vs_source = `\n  attribute vec3 aVertex;\n  uniform float uTime;\n  varying lowp vec4 vColor;\n\n  void main() {\n    float dt = uTime - aVertex[2];\n    gl_Position = vec4(aVertex[0], aVertex[1], 1.0, 1.0);\n    vColor = vec4(0.0, 170.0/255.0, 1.0, 1.0) * max(1.0 - dt/${fade_time}.0, 0.0);\n  }\n`;\n\nconst fs_source = `\n  varying lowp vec4 vColor;\n\n  void main() {\n    gl_FragColor = vColor;\n  }\n`;\n\nclass Painter {\n    canvas: HTMLCanvasElement;\n    gl: WebGLRenderingContext;\n\n    /* Store lines currently being drawn.\n     *\n     * Keys are pointerIds, values are an array of the last position (x, y), thickness and event\n     * time and another array with vertices to be used by webgl. Each vertex is made of 3 floats, x\n     * and y coordinates and the event time. Vertices always come in pairs of two. Two such vertices\n     * describe the edges of the line to be drawn with regard to it's thickness. TRIANGLE_STRIP is\n     * then used to connect them and draw an actual line with some thickness depending on the\n     * pressure applied.\n     */\n    lines_active: Map<number, [[number, number, number, number], number[]]>\n\n    // Array of vertices that are not actively drawn anymore and do not need updates, except\n    // removing them after they faded away.\n    lines_old: number[][];\n    vertex_attr: GLint;\n    vertex_buffer: WebGLBuffer;\n    time_attr: WebGLUniformLocation;\n    initialized: boolean;\n\n    constructor(canvas: HTMLCanvasElement) {\n        this.canvas = canvas;\n        canvas.width = window.innerWidth * window.devicePixelRatio;\n        canvas.height = window.innerHeight * window.devicePixelRatio;\n        this.gl = canvas.getContext(\"webgl\");\n        if (this.gl) {\n            this.lines_active = new Map();\n            this.lines_old = [];\n            this.setupWebGL();\n        }\n    }\n\n    loadShader(type, source): WebGLShader {\n        let gl = this.gl;\n        const shader = gl.createShader(type);\n        gl.shaderSource(shader, source);\n        gl.compileShader(shader);\n        if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n            log(LogLevel.WARN, \"Failed to compile shaders: \" + gl.getShaderInfoLog(shader));\n            gl.deleteShader(shader);\n            return null;\n        }\n        return shader;\n    }\n\n    setupWebGL() {\n        let gl = this.gl;\n        gl.enable(gl.BLEND);\n        gl.clearColor(0, 0, 0, 0);\n        gl.clear(gl.COLOR_BUFFER_BIT);\n\n        const vertex_shader = this.loadShader(gl.VERTEX_SHADER, vs_source);\n        const fragment_shader = this.loadShader(gl.FRAGMENT_SHADER, fs_source);\n        if (!vertex_shader || !fragment_shader)\n            return;\n        const shader_program = gl.createProgram();\n        gl.attachShader(shader_program, vertex_shader);\n        gl.attachShader(shader_program, fragment_shader);\n        gl.linkProgram(shader_program);\n\n        if (!gl.getProgramParameter(shader_program, gl.LINK_STATUS)) {\n            log(LogLevel.WARN, \"Unable to initialize the shader program: \" + gl.getProgramInfoLog(shader_program));\n            return;\n        }\n        this.vertex_attr = gl.getAttribLocation(shader_program, \"aVertex\");\n        this.time_attr = gl.getUniformLocation(shader_program, \"uTime\");\n        this.vertex_buffer = gl.createBuffer();\n        gl.bindBuffer(gl.ARRAY_BUFFER, this.vertex_buffer);\n        gl.vertexAttribPointer(this.vertex_attr, 3, gl.FLOAT, false, 0, 0);\n        gl.enableVertexAttribArray(this.vertex_attr);\n        gl.useProgram(shader_program);\n        this.initialized = true;\n        requestAnimationFrame(() => this.render());\n    }\n\n    render() {\n        // only do work if necessary\n        if (!check_video.checked && (this.lines_active.size > 0 || this.lines_old.length > 0)) {\n            if (this.lines_old.length > 0) {\n                if (performance.now() - this.lines_old[0][this.lines_old[0].length - 1] > fade_time)\n                    this.lines_old.shift();\n            }\n            let gl = this.gl;\n            gl.viewport(0, 0, this.canvas.width, this.canvas.height);\n            gl.clear(gl.COLOR_BUFFER_BIT);\n            gl.uniform1f(this.time_attr, performance.now());\n            gl.bindBuffer(gl.ARRAY_BUFFER, this.vertex_buffer);\n            for (let vertices of this.lines_old) {\n                gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.DYNAMIC_DRAW);\n                gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertices.length / 3)\n            }\n            for (let [_, vertices] of this.lines_active.values()) {\n                // sometimes there are no linesegments because there has been only a single\n                // PointerEvent\n                if (vertices.length == 0)\n                    continue;\n                gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.DYNAMIC_DRAW);\n                gl.drawArrays(gl.TRIANGLE_STRIP, 0, vertices.length / 3)\n            }\n        }\n        requestAnimationFrame(() => this.render());\n    }\n\n    appendEventToLine(event: PointerEvent) {\n        let line = this.lines_active.get(event.pointerId);\n        if (!line) {\n            line = [null, []];\n            this.lines_active.set(event.pointerId, line)\n        }\n        let max_pixels = Math.max(this.canvas.width, this.canvas.height);\n        let x = event.clientX * window.devicePixelRatio / this.canvas.width * 2 - 1;\n        let y = 1 - event.clientY * window.devicePixelRatio / this.canvas.height * 2;\n        let delta = event.pressure + 0.4;\n        let t = performance.now();\n        // to draw a line segment, there has to be some previous position\n        if (line[0]) {\n            let [x0, y0, delta0, t0] = line[0];\n            // get vector perpendicular to the linesegment to calculate quadrangel around the\n            // segment with appropriate thickness\n            let dx = (y - y0);\n            let dy = -(x - x0);\n            let dd = Math.sqrt(dx ** 2 + dy ** 2);\n            if (dd == 0) {\n                return;\n            }\n            dx = dx / dd * max_pixels / this.canvas.width * 0.004;\n            dy = dy / dd * max_pixels / this.canvas.height * 0.004;\n\n            if (line[1].length == 0)\n                line[1].push(\n                    x0 + delta0 * dx, y0 + delta0 * dy, t0, x0 - delta0 * dx, y0 - delta0 * dy, t0,\n                );\n            line[1].push(\n                x + delta * dx, y + delta * dy, t, x - delta * dx, y - delta * dy, t\n            )\n        }\n        line[0] = [x, y, delta, t];\n    }\n\n    onstart(event: PointerEvent) {\n        this.appendEventToLine(event);\n    }\n\n    onmove(event: PointerEvent) {\n        if (this.lines_active.has(event.pointerId)) {\n            const events = typeof event.getCoalescedEvents === 'function' ? event.getCoalescedEvents() : [event];\n            for (const e of events) {\n                this.appendEventToLine(e);\n            }\n        }\n    }\n\n    onstop(event: PointerEvent) {\n        let lines = this.lines_active.get(event.pointerId);\n        if (lines) {\n            if (lines[1].length > 0)\n                this.lines_old.push(lines[1]);\n            this.lines_active.delete(event.pointerId);\n        }\n    }\n}\n\nclass PointerHandler {\n    webSocket: WebSocket;\n    pointerTypes: string[];\n\n    constructor(webSocket: WebSocket) {\n        let video = document.getElementById(\"video\");\n        let canvas = document.getElementById(\"canvas\");\n        this.webSocket = webSocket;\n        this.pointerTypes = settings.pointer_types();\n\n        video.onpointerdown = (e) => this.onEvent(e, \"pointerdown\");\n        video.onpointerup = (e) => this.onEvent(e, \"pointerup\");\n        video.onpointercancel = (e) => this.onEvent(e, \"pointercancel\");\n        video.onpointermove = (e) => this.onEvent(e, \"pointermove\");\n        video.onpointerout = (e) => this.onEvent(e, \"pointerout\");\n        video.onpointerleave = (e) => this.onEvent(e, \"pointerleave\");\n        video.onpointerenter = (e) => this.onEvent(e, \"pointerenter\");\n        video.onpointerover = (e) => this.onEvent(e, \"pointerover\");\n\n        let painter: Painter;\n        if (!settings.checks.get(\"energysaving\").checked)\n            painter = new Painter(canvas as HTMLCanvasElement);\n\n        if (painter && painter.initialized) {\n            canvas.onpointerdown = (e) => { this.onEvent(e, \"pointerdown\"); painter.onstart(e); };\n            canvas.onpointerup = (e) => { this.onEvent(e, \"pointerup\"); painter.onstop(e); };\n            canvas.onpointercancel = (e) => { this.onEvent(e, \"pointercancel\"); painter.onstop(e); };\n            canvas.onpointermove = (e) => { this.onEvent(e, \"pointermove\"); painter.onmove(e); };\n            canvas.onpointerout = (e) => { this.onEvent(e, \"pointerout\"); painter.onstop(e); };\n            canvas.onpointerleave = (e) => { this.onEvent(e, \"pointerleave\"); painter.onstop(e); };\n            canvas.onpointerenter = (e) => { this.onEvent(e, \"pointerenter\"); painter.onmove(e); };\n            canvas.onpointerover = (e) => { this.onEvent(e, \"pointerover\"); painter.onmove(e); };\n        } else {\n            canvas.onpointerdown = (e) => this.onEvent(e, \"pointerdown\");\n            canvas.onpointerup = (e) => this.onEvent(e, \"pointerup\");\n            canvas.onpointercancel = (e) => this.onEvent(e, \"pointercancel\");\n            canvas.onpointermove = (e) => this.onEvent(e, \"pointermove\");\n            canvas.onpointerout = (e) => this.onEvent(e, \"pointerout\");\n            canvas.onpointerleave = (e) => this.onEvent(e, \"pointerleave\");\n            canvas.onpointerenter = (e) => this.onEvent(e, \"pointerenter\");\n            canvas.onpointerover = (e) => this.onEvent(e, \"pointerover\");\n        }\n\n        // This is a workaround for the following Safari/WebKit bug:\n        // https://bugs.webkit.org/show_bug.cgi?id=217430\n        // I have no idea why this works but it does.\n        video.ontouchmove = (e) => e.preventDefault();\n        canvas.ontouchmove = (e) => e.preventDefault();\n\n        for (let elem of [video, canvas]) {\n            elem.onwheel = (e) => {\n                this.webSocket.send(JSON.stringify({ \"WheelEvent\": new WEvent(e) }));\n            }\n        }\n    }\n\n    onEvent(event: PointerEvent, event_type: string) {\n        if (settings.checks.get(\"enable_debug_overlay\").checked) {\n            let props = [\n                \"altKey\",\n                \"altitudeAngle\",\n                \"azimuthAngle\",\n                \"button\",\n                \"buttons\",\n                \"clientX\",\n                \"clientY\",\n                \"ctrlKey\",\n                \"height\",\n                \"isPrimary\",\n                \"metaKey\",\n                \"movementX\",\n                \"movementY\",\n                \"offsetX\",\n                \"offsetY\",\n                \"pageX\",\n                \"pageY\",\n                \"pointerId\",\n                \"pointerType\",\n                \"pressure\",\n                \"screenX\",\n                \"screenY\",\n                \"shiftKey\",\n                \"tangentialPressure\",\n                \"tiltX\",\n                \"tiltY\",\n                \"timeStamp\",\n                \"twist\",\n                \"type\",\n                \"width\",\n                \"x\",\n                \"y\",\n            ];\n            if (!last_pointer_data) {\n                last_pointer_data = {};\n                for (let prop of props) {\n                    let span_id = `prop_${prop}_span`;\n                    let span = document.getElementById(span_id);\n                    span = document.createElement(\"span\");\n                    span.id = span_id;\n                    debug_overlay.appendChild(span);\n                    debug_overlay.appendChild(document.createElement(\"br\"));\n                }\n            }\n            for (let prop of props) {\n                let span_id = `prop_${prop}_span`;\n                let span = document.getElementById(span_id);\n                let v = event[prop];\n                span.textContent = `${prop}: ${v}`;\n                if (last_pointer_data[prop] == v) {\n                    span.classList.remove(\"updated\");\n                } else {\n                    span.classList.add(\"updated\");\n                    last_pointer_data[prop] = v;\n                }\n            }\n        }\n        if (this.pointerTypes.includes(event.pointerType)) {\n            let rect = (event.target as HTMLElement).getBoundingClientRect();\n            const events = event_type === \"pointermove\" && typeof event.getCoalescedEvents === 'function' ? event.getCoalescedEvents() : [event];\n            for (let event of events) {\n                this.webSocket.send(\n                    JSON.stringify(\n                        {\n                            \"PointerEvent\": new PEvent(\n                                event_type,\n                                event,\n                                rect\n                            )\n                        }\n                    )\n                );\n            }\n            if (settings.visible) {\n                settings.toggle();\n            }\n        }\n    }\n}\n\nclass KEvent {\n    event_type: string;\n    code: string;\n    key: string;\n    location: number;\n    alt: boolean;\n    ctrl: boolean;\n    shift: boolean;\n    meta: boolean;\n\n    constructor(event_type: string, event: KeyboardEvent) {\n        this.event_type = event_type;\n        this.code = event.code;\n        this.key = event.key;\n        this.location = event.location;\n        this.alt = event.altKey;\n        this.ctrl = event.ctrlKey;\n        this.shift = event.shiftKey;\n        this.meta = event.metaKey;\n    }\n}\n\nclass KeyboardHandler {\n    webSocket: WebSocket;\n\n    constructor(webSocket: WebSocket) {\n        this.webSocket = webSocket;\n\n        let d = document;\n        let s = document.getElementById(\"settings\")\n\n        // Consume all KeyboardEvents, except the settings menu is open.\n        // this avoids making the main/video/canvas element focusable by using\n        // a tabindex which interferes with PointerEvent than can be considered\n        // hovering.\n\n        function settings_hidden() {\n            return s.classList.contains(\"hide\") || s.classList.contains(\"vanish\");\n        }\n\n        d.onkeydown = (e) => {\n            if (!settings_hidden())\n                return true;\n            if (e.repeat)\n                return this.onEvent(e, \"repeat\");\n            return this.onEvent(e, \"down\");\n        };\n        d.onkeyup = (e) => {\n            if (!settings_hidden())\n                return true;\n            return this.onEvent(e, \"up\");\n        };\n        d.onkeypress = (e) => {\n            if (!settings_hidden())\n                return true;\n            e.preventDefault();\n            e.stopPropagation();\n            return false;\n        };\n    }\n\n    onEvent(event: KeyboardEvent, event_type: string) {\n        this.webSocket.send(JSON.stringify({ \"KeyboardEvent\": new KEvent(event_type, event) }));\n        event.preventDefault();\n        event.stopPropagation();\n        return false;\n    }\n}\n\nfunction frame_rate_stats() {\n    let t = performance.now();\n    let fps = Math.round(frame_count / (t - last_fps_calc) * 10000) / 10;\n    fps_out.value = fps.toString();\n    frame_count = 0;\n    last_fps_calc = t;\n    setTimeout(() => frame_rate_stats(), 1500);\n}\n\nfunction handle_messages(\n    webSocket: WebSocket,\n    video: HTMLVideoElement,\n    onConfigOk: Function,\n    onConfigError: Function,\n    onCapturableList: Function,\n) {\n    let mediaSource: MediaSource = null;\n    let sourceBuffer: SourceBuffer = null;\n    let queue = [];\n    const MAX_BUFFER_LENGTH = 20;  // In seconds\n    function upd_buf() {\n        if (sourceBuffer == null)\n            return;\n        if (!sourceBuffer.updating && queue.length > 0 && mediaSource.readyState == \"open\") {\n            let buffer_length = 0;\n            if (sourceBuffer.buffered.length) {\n                // Assume only one time range...\n                buffer_length = sourceBuffer.buffered.end(0) - sourceBuffer.buffered.start(0);\n            }\n            if (buffer_length > MAX_BUFFER_LENGTH) {\n                sourceBuffer.remove(0, sourceBuffer.buffered.end(0) - MAX_BUFFER_LENGTH / 2);\n                // This will trigger updateend when finished\n            } else {\n                try {\n                    sourceBuffer.appendBuffer(queue.shift());\n                } catch (err) {\n                    log(LogLevel.DEBUG, \"Error appending to sourceBuffer:\" + err);\n                    // Drop everything, and try to pick up the stream again\n                    if (sourceBuffer.updating)\n                        sourceBuffer.abort();\n                    sourceBuffer.remove(0, Infinity);\n                }\n            }\n        }\n    }\n    webSocket.onmessage = (event: MessageEvent) => {\n        if (typeof event.data == \"string\") {\n            let msg = JSON.parse(event.data);\n            if (typeof msg == \"string\") {\n                if (msg == \"NewVideo\") {\n                    let MS = window.ManagedMediaSource ? window.ManagedMediaSource : window.MediaSource;\n                    mediaSource = new MS();\n                    sourceBuffer = null;\n                    video.src = URL.createObjectURL(mediaSource);\n                    mediaSource.addEventListener(\"sourceopen\", (_) => {\n                        let mimeType = 'video/mp4; codecs=\"avc1.4D403D\"';\n                        if (!MS.isTypeSupported(mimeType))\n                            mimeType = \"video/mp4\";\n                        sourceBuffer = mediaSource.addSourceBuffer(mimeType);\n                        sourceBuffer.addEventListener(\"updateend\", upd_buf);\n                        // try to recover from errors by restarting the video\n                        if (sourceBuffer.onerror)\n                            sourceBuffer.onerror = () => settings.send_server_config();\n                    })\n                } else if (msg == \"ConfigOk\") {\n                    onConfigOk();\n                }\n            } else if (typeof msg == \"object\") {\n                if (\"CapturableList\" in msg)\n                    onCapturableList(msg[\"CapturableList\"]);\n                else if (\"Error\" in msg)\n                    alert(msg[\"Error\"]);\n                else if (\"ConfigError\" in msg) {\n                    onConfigError(msg[\"ConfigError\"]);\n                } else if (\"CustomInputAreas\" in msg) {\n                    settings.custom_input_areas = msg[\"CustomInputAreas\"];\n                    settings.checks.get(\"enable_custom_input_areas\").checked = true;\n                    settings.save_settings();\n                }\n            }\n\n            return;\n        }\n\n        // not a string -> got a video frame\n        queue.push(event.data);\n        upd_buf();\n        frame_count += 1;\n\n        // only seek if there is data available, some browsers choke otherwise\n        if (video.seekable.length > 0) {\n            let seek_time = video.seekable.end(video.seekable.length - 1);\n            if (video.readyState >= (settings.check_aggressive_seek.checked ? 3 : 4)\n                // but make sure to catch up if the video is more than 3 seconds behind\n                || seek_time - video.currentTime > 3) {\n                if (isFinite(seek_time))\n                    video.currentTime = seek_time;\n                else\n                    log(LogLevel.WARN, \"Failed to seek to end of video.\")\n            }\n\n        }\n    }\n}\n\nfunction check_apis() {\n    let apis = [\n        {\n            attrs: [\"MediaSource\", \"ManagedMediaSource\"],\n            msg: \"This browser doesn't support MSE/MMS required to playback video stream, try upgrading!\"\n        },\n        {\n            attrs: [\"PointerEvent\"],\n            msg: \"This browser doesn't support PointerEvents, input will not work, try upgrading!\"\n        },\n    ];\n\n    outer:\n    for (let d of apis) {\n        for (let attr of d.attrs) {\n            if (attr in window) {\n                continue outer;\n            }\n        }\n        log(LogLevel.ERROR, d.msg);\n    }\n}\n\nfunction init() {\n    check_apis();\n\n    let protocol = document.location.protocol == \"https:\" ? \"wss://\" : \"ws://\";\n    let webSocket = new WebSocket(\n        protocol + window.location.hostname + \":\" +\n        window.location.port + \"/ws\" + window.location.search\n    );\n    webSocket.binaryType = \"arraybuffer\";\n\n    debug_overlay = document.getElementById(\"debug_overlay\");\n    settings = new Settings(webSocket);\n\n    let video = document.getElementById(\"video\") as HTMLVideoElement;\n    let canvas = document.getElementById(\"canvas\") as HTMLCanvasElement;\n\n    video.oncontextmenu = function(event) {\n        event.preventDefault();\n        event.stopPropagation();\n        return false;\n    };\n    canvas.oncontextmenu = function(event) {\n        event.preventDefault();\n        event.stopPropagation();\n        return false;\n    };\n\n    let toggle_fullscreen_btn = document.getElementById(\"fullscreen\") as HTMLButtonElement;\n\n    if (document.exitFullscreen) {\n        toggle_fullscreen_btn.onclick = () => {\n            if (!document.fullscreenElement)\n                document.body.requestFullscreen({ navigationUI: \"hide\" });\n            else\n                document.exitFullscreen();\n        }\n    } else {\n        // if document.exitFullscreen is not present we are probably running on iOS/iPadOS.\n        // As input is broken in fullscreen mode on these, do not offer fullscreen in the first\n        // place.\n        toggle_fullscreen_btn.parentElement.removeChild(toggle_fullscreen_btn);\n    }\n\n    let handle_disconnect = (msg: string) => {\n        document.body.onclick = video.onclick = (e) => {\n            e.stopPropagation();\n            if (window.confirm(msg + \" Reload page?\"))\n                location.reload();\n        }\n    }\n    webSocket.onerror = () => handle_disconnect(\"Lost connection.\");\n    webSocket.onclose = () => handle_disconnect(\"Connection closed.\");\n    window.onresize = () => {\n        stretch_video();\n        canvas.width = window.innerWidth * window.devicePixelRatio;\n        canvas.height = window.innerHeight * window.devicePixelRatio;\n        let [w, h] = calc_max_video_resolution(settings.scale_video_input.valueAsNumber);\n        settings.scale_video_output.value = w + \"x\" + h;\n        settings.send_server_config();\n    }\n    video.controls = false;\n    video.disableRemotePlayback = true;\n    video.onloadeddata = () => stretch_video();\n    let is_connected = false;\n    handle_messages(webSocket, video, () => {\n        if (!is_connected) {\n            new KeyboardHandler(webSocket);\n            new PointerHandler(webSocket);\n            is_connected = true;\n        }\n    },\n        (err) => alert(err),\n        (window_names) => settings.onCapturableList(window_names)\n    );\n    window.onunload = () => { webSocket.close(); }\n    webSocket.onopen = function(event) {\n        webSocket.send('\"GetCapturableList\"');\n        if (!settings.video_enabled())\n            webSocket.send('\"PauseVideo\"');\n\n        settings.send_server_config();\n\n        document.onvisibilitychange = () => {\n            if (document.hidden) {\n                webSocket.send('\"PauseVideo\"');\n            } else if (settings.video_enabled()) {\n                webSocket.send('\"ResumeVideo\"');\n            }\n        };\n    }\n    frame_rate_stats();\n}\n\n// object-fit: fill; <-- this is unfortunately not supported on iOS, so we use the following\n// workaround\nfunction stretch_video() {\n    let video = document.getElementById(\"video\") as HTMLVideoElement;\n    if (settings.stretched_video()) {\n        video.style.transform = \"scaleX(\" + document.body.clientWidth / video.clientWidth + \") scaleY(\" + document.body.clientHeight / video.clientHeight + \")\";\n    } else {\n        let scale = Math.min(document.body.clientWidth / video.clientWidth, document.body.clientHeight / video.clientHeight);\n        video.style.transform = \"scale(\" + scale + \")\";\n    }\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es2020\",\n    \"lib\": [\"es2020\", \"dom\", \"es6\"],\n    \"module\": \"commonjs\",\n    \"outDir\": \"www/static\",\n    \"sourceMap\": false\n  },\n  \"include\": [\n    \"ts/**/*.ts\"\n  ]\n}\n"
  },
  {
    "path": "weylus.desktop",
    "content": "[Desktop Entry]\nType=Application\nName=Weylus\nComment=Use your tablet as graphic tablet/touch screen on your computer.\nTryExec=weylus\nExec=weylus\nTerminal=false\nIcon=input-tablet\nCategories=Office;Graphics;Education;Presentation;\nStartupWMClass=weylus\n"
  },
  {
    "path": "weylus_tls.sh",
    "content": "#!/usr/bin/env sh\n\nfunction die {\n    # cleanup to ensure restarting this script doesn't fail because\n    # of ports that are still in use\n    kill $(jobs -p) > /dev/null 2>&1\n    exit $1\n}\n\n# generate certificate if it doesn't exist yet\nif [ ! -e weylus.pem ]\nthen\n    openssl req -batch -newkey rsa:4096 -sha256 -keyout weylus.key -nodes -x509 -days 365 \\\n        -subj=\"/CN=Weylus\" -out weylus.crt\n\n    # combine into a pem file as this is everything hitch needs\n    cat weylus.key weylus.crt > weylus.pem\n    rm weylus.key weylus.crt\nfi\n\n# WEYLUS can be used to determine which version of Weylus to run\n# If unset, try ./weylus and then weylus from path. If both fail,\n# read the path to Weylus from stdin.\nif [ -z \"$WEYLUS\" ]\nthen\n    if [ -e weylus ]\n    then\n        WEYLUS=./weylus\n    else\n        if which weylus > /dev/null 2>&1\n        then\n            WEYLUS=weylus\n        else\n            echo \"Please specify path to weylus.\"\n            echo -n \"> \"\n            read -r WEYLUS\n        fi\n    fi\nfi\n\nif [ -z \"$ACCESS_CODE\" ]\nthen\n    # generate access code if none is given\n    ACCESS_CODE=\"$(openssl rand -base64 12)\"\n    echo \"Autogenerated access code: $ACCESS_CODE\"\nfi\n\n# cleanup on CTRL+C\ntrap die SIGINT\n\n# The TLS proxy will be set up as follows: Proxy all incoming traffic from\n# port 1701 to 1702 on which the actual instance of Weylus is running.\n\n# start Weylus listening only on the local interface\n$WEYLUS --bind-address \"127.0.0.1\" \\\n    --web-port \"1702\" \\\n    --access-code \"$ACCESS_CODE\" \\\n    --no-gui &\n\n# start the proxy\nhitch --frontend=\"[0.0.0.0]:1701\" --backend=\"[127.0.0.1]:1702\" \\\n    --daemon=off --tls-protos=\"TLSv1.2 TLSv1.3\" \"weylus.pem\" &\n\nwait\n"
  },
  {
    "path": "www/static/access_code.html",
    "content": "<!DOCTYPE html>\n<html>\n\t<head>\n\t\t<meta name=\"apple-mobile-web-app-capable\" content=\"yes\" />\n\t\t<meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\">\n\t\t<meta name=\"mobile-web-app-capable\" content=\"yes\">\n\t\t<title>Weylus</title>\n\t\t<link rel=\"stylesheet\" href=\"style.css\">\n\t</head>\n\t<body>\n\t\t<div class=\"container\">\n\t\t\t<form action=\"/\">\n\t\t\t\t<label for=\"access_code\">Access code:</label><br>\n\t\t\t\t<input type=\"text\" id=\"access_code\" name=\"access_code\"><br>\n\t\t\t\t<input type=\"submit\" value=\"Login\">\n\t\t\t</form>\n\t\t</div>\n\t</body>\n</html>\n"
  },
  {
    "path": "www/static/style.css",
    "content": "@media (prefers-color-scheme: dark) {\n    :root {\n        --color: #ddd;\n        --background-color-1: #303030;\n        --background-color-2: #202020;\n    }\n}\n@media (prefers-color-scheme: light) {\n    :root {\n        --color: #111;\n        --background-color-1: #eee;\n        --background-color-2: #fff;\n    }\n}\nmain {\n    touch-action: none;\n    user-select: none;\n    -webkit-touch-callout: none;\n    -webkit-user-select: none;\n    -khtml-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n}\nbody, html, main {\n    width: 100%;\n    height: 100%;\n    margin: 0;\n    padding: 0;\n    overflow: hidden;\n    display: flex;\n    color: var(--color);\n    background: var(--background-color-2);\n}\nvideo {\n    max-width: 100%;\n    max-height: 100%;\n    width: auto;\n    height: auto;\n    display: block;\n    margin: auto auto;\n}\ncanvas {\n    position: absolute;\n    width: 100%;\n    height: 100%;\n}\ninput[type='text'] {\n    touch-action: auto !important;\n    user-select: text;\n    -webkit-touch-callout: default;\n    -webkit-user-select: text;\n    -khtml-user-select: text;\n    -moz-user-select: text;\n    -ms-user-select: text;\n}\ninput[type='range']::-webkit-slider-runnable-track, input[type=\"range\"]::-moz-range-track {\n      background-color: #00aaff;\n}\n.container {\n    width: 100%;\n    height: 100%;\n    display: flex;\n    justify-content: center;\n    align-items: center;\n}\n#settings, #handle, #debug_overlay {\n    color: var(--color);\n    background: var(--background-color-1);\n}\n#settings_scroll {\n    overflow: auto;\n    width: 100%;\n    height: 100%;\n}\n#settings {\n    width: 16em;\n    display: block;\n    position: absolute;\n    right: 0;\n    top: 0;\n    bottom: 0;\n    transform: none;\n    padding: 1em;\n    font-family: sans-serif;\n    transition: transform 0.5s, opacity 0.5s;\n}\n#settings.lefty {\n    left: 0;\n    right: auto;\n}\n#settings.hide, #settings.vanish {\n    transform: translateX(100%);\n    opacity: 40%;\n}\n#settings.hide.lefty, #settings.vanish.lefty {\n    transform: translateX(-100%);\n    opacity: 40%;\n}\n#handle {\n    position: absolute;\n    top: 0;\n    left: -1em;\n    width: 1em;\n    border-radius: 0 0 0 0.25em;\n    padding: 0.2em 0 0.1em 0;\n    font-size: 150%;\n    text-align:center;\n}\n.lefty #handle {\n    left: auto;\n    right: -1em;\n    border-radius: 0 0 0.25em 0;\n}\n.vanish #handle, video.vanish, canvas.vanish {\n    display: none !important;\n}\n#settings h2 {\n    font-size: 1.2em;\n    margin: 0;\n    text-align: center;\n}\n#settings h3 {\n    font-size: 1em;\n    margin-top: 0.2;\n    text-align: center;\n}\n#settings section {\n    margin-bottom: 1em;\n}\n#settings section label, section button {\n    display: block;\n    margin-top: 0.5em;\n}\n#settings section.hide, section label.hide, section button.hide, #debug_overlay.hide {\n    display: none !important;\n}\nselect {\n    width: 15em;\n}\n#displayoptions {\n    display: flex;\n    flex-direction: row;\n}\n.lefty #displayoptions {\n    flex-direction: row-reverse;\n}\n#displayoptions label {\n    flex-grow: 1;\n}\nlabel input:disabled + span {\n    color: #888;\n}\n#frame_update_limit {\n    width: auto;\n}\n#lefty {\n    display: none;\n}\n#leftylabel::before {\n    content: \"❮ \";\n}\n.lefty #leftylabel {\n    text-align: right;\n}\n.lefty #leftylabel::before {\n    content: \"\";\n}\n.lefty #leftylabel::after {\n    content: \" ❯\";\n}\n#vanish {\n    text-align: right;\n}\n.lefty #vanish {\n    text-align: left;\n}\n#log {\n    max-width: 15em;\n    overflow-x: auto;\n    touch-action: pan-x pan-y;\n    user-select: text;\n    -webkit-touch-callout: default;\n    -webkit-user-select: text;\n    -khtml-user-select: text;\n    -moz-user-select: text;\n    -ms-user-select: text;\n}\n#debug_overlay {\n    --padding: 3px;\n    position: absolute;\n    top:0;\n    bottom: 0;\n    left: 0;\n    right: 0;\n    margin: auto;\n    width: fit-content;\n    height: fit-content;\n    z-index: 1;\n    pointer-events: none;\n    opacity: 75%;\n    padding: var(--padding);\n    border: 2px solid var(--color);\n    white-space: pre;\n    font-size: small;\n    text-align: center;\n}\n#debug_overlay span.updated {\n    color: darkgreen;\n}\n"
  },
  {
    "path": "www/templates/index.html",
    "content": "<!DOCTYPE html>\n<html>\n\n<head>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\" />\n    <meta name=\"apple-mobile-web-app-capable\" content=\"yes\" />\n    <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\">\n    <meta name=\"mobile-web-app-capable\" content=\"yes\">\n    <title>Weylus</title>\n    <link rel=\"stylesheet\" href=\"style.css\">\n    <script src=\"lib.js\"></script>\n    <script>\n        run(\"{{log_level}}\");\n    </script>\n</head>\n\n<body>\n    <main id=\"main\">\n        <video id=\"video\" autoplay muted defaultMuted playsinline disablePictureInPicture></video>\n        <canvas id=\"canvas\" class=\"vanish\"></canvas>\n        <div id=\"debug_overlay\" class=\"hide\"></div>\n    </main>\n    <div id=\"settings\">\n        <div id=\"handle\">⠿</div>\n        <div id=\"settings_scroll\">\n            <h2>Settings</h2>\n            <section>\n                <label for=\"window\">Capture:</label>\n                <select id=\"window\"></select>\n                <button id=\"refresh\">Refresh List</button>\n            </section>\n            <h3>Video</h3>\n            <section>\n                <label><input type=\"checkbox\" id=\"enable_video\" checked /> <span>Enable Video</span></label>\n                <label><input type=\"checkbox\" id=\"energysaving\" /> <span>Energy Saving (no video, black\n                        screen)</span></label>\n                <label><input type=\"checkbox\" id=\"stretch\" checked /> <span>Stretch Video</span></label>\n                <button id=\"fullscreen\">Toggle Fullscreen</button>\n                <label {{#if (not capture_cursor_enabled)}}class=\"hide\" {{/if}}>\n                    <input type=\"checkbox\" id=\"capture_cursor\" />\n                    <span>Capture Cursor</span>\n                </label>\n                <label><input type=\"checkbox\" id=\"aggressive_seeking\" checked /> <span>Lower Latency<br>(possibly\n                        choppy)</span></label>\n                <label>Max Video Resolution: <br><input type=\"range\" id=\"scale_video\" min=\"0.1\" max=\"2\" step=\"0.01\"\n                        value=\"1.8\" /><output></output></label>\n                <label>Frame Rate: <br><input type=\"range\" id=\"frame_rate\" value=\"0\" /><output>30</output> fps</label>\n            </section>\n            <h3>Input</h3>\n            <section>\n                <label><input type=\"checkbox\" id=\"enable_mouse\" checked /> <span>Enable Mouse</span></label>\n                <label><input type=\"checkbox\" id=\"enable_stylus\" checked /> <span>Enable Stylus</span></label>\n                <label><input type=\"checkbox\" id=\"enable_touch\" checked /> <span>Enable Touch</span></label>\n                <label {{#if (not uinput_enabled)}}class=\"hide\" {{/if}}>\n                    <input type=\"checkbox\" id=\"uinput_support\" checked />\n                    <span>Enable uinput</span>\n                </label>\n                <label>Min pressure to generate: <br><input type=\"range\" id=\"min_pressure\" min=\"0\" max=\"1\" step=\"0.01\"\n                        value=\"0\" /></label>\n                <button id=\"custom_input_areas\" {{#if (not enable_custom_input_areas)}}class=\"hide\" {{/if}}>Custom Input\n                    Area</button>\n                <label {{#if (not enable_custom_input_areas)}}class=\"hide\" {{/if}}><input type=\"checkbox\"\n                        id=\"enable_custom_input_areas\" /> <span>Enable Custom Input\n                        Area</span></label>\n            </section>\n            <section {{#if (not uinput_enabled)}}class=\"hide\" {{/if}}>\n                <label><span>Client Name:</span><br><input type=\"text\" id=\"client_name\" /><br><span>Optional, useful to\n                        distinguish multiple devices.</span></label>\n            </section>\n            <section id=\"displayoptions\">\n                <label id=\"leftylabel\"><input type=\"checkbox\" id=\"lefty\" />Swap</label>\n                <label id=\"vanish\">Hide until Reload</label>\n            </section>\n            <section id=\"stats_section\">\n                <label><span>FPS (receiving): </span><output id=\"fps\">0</output></label>\n            </section>\n            <section id=\"debug_section\">\n                <label><input type=\"checkbox\" id=\"enable_debug_overlay\" /> <span>Debug\n                        Overlay</span></label>\n            </section>\n            <section id=\"log_section\" class=\"hide\">\n                <label>Log\n                    <pre class=\"log\" id=\"log\" />\n                </label>\n            </section>\n        </div>\n    </div>\n</body>\n\n</html>\n"
  }
]