├── .builds ├── alpine.yml └── nix.yml ├── .clang-format ├── .editorconfig ├── .envrc ├── .github └── workflows │ ├── arch.yaml │ └── nix.yaml ├── .gitignore ├── .gitmodules ├── .rustfmt.toml ├── LICENSE ├── Makefile ├── README.md ├── build.zig ├── docs ├── next.1.scd └── nextctl.1.scd ├── flake.lock ├── flake.nix ├── next.desktop ├── next ├── Config.zig ├── Server.zig ├── control │ ├── border.zig │ ├── command.zig │ ├── csd.zig │ ├── cursor.zig │ ├── exit.zig │ ├── inputs.zig │ ├── outputs.zig │ └── spawn.zig ├── desktop │ ├── Decoration.zig │ ├── DecorationManager.zig │ ├── Output.zig │ ├── OutputLayout.zig │ ├── Wallpaper.zig │ ├── Window.zig │ ├── XdgPopup.zig │ └── XdgToplevel.zig ├── global │ └── Control.zig ├── input │ ├── Cursor.zig │ ├── InputManager.zig │ ├── Keyboard.zig │ └── Seat.zig ├── next.zig └── utils │ ├── allocator.zig │ ├── c.zig │ └── wlr_log.c ├── nextctl-go ├── Makefile ├── cmd │ └── nextctl │ │ └── nextctl.go ├── go.mod ├── go.sum └── pkg │ └── next_control │ └── gen.go ├── nextctl-rs ├── Cargo.lock ├── Cargo.toml ├── Makefile ├── build.rs └── src │ ├── main.rs │ └── wayland │ └── mod.rs ├── nextctl.zig ├── nextctl ├── Makefile ├── build.zig ├── compile_flags.txt ├── include │ └── nextctl.h ├── protocols │ └── next-control-v1.xml └── src │ └── nextctl.c ├── protocols └── next-control-v1.xml ├── rust-toolchain.toml ├── scdoc.zig ├── scenefx.nix └── sniff.json /.builds/alpine.yml: -------------------------------------------------------------------------------- 1 | image: alpine/edge 2 | packages: 3 | - cairo-dev 4 | - cargo 5 | - clang 6 | - clang-dev 7 | - eudev-dev 8 | - expat-dev 9 | - go 10 | - hwdata 11 | - libdrm 12 | - libevdev-dev 13 | - libffi-dev 14 | - libinput-dev 15 | - libjpeg-turbo-dev 16 | - libseat-dev 17 | - libxkbcommon-dev 18 | - mesa-dev 19 | - meson 20 | - pixman-dev 21 | - rustfmt 22 | - scdoc 23 | - tar 24 | - wayland-protocols 25 | - wget 26 | - xcb-util-image-dev 27 | - xcb-util-renderutil-dev 28 | - xcb-util-wm-dev 29 | - xwayland 30 | sources: 31 | - https://github.com/wlrfx/scenefx 32 | - https://git.sr.ht/~shinyzenith/NextWM 33 | - https://gitlab.freedesktop.org/wayland/wayland.git 34 | - https://gitlab.freedesktop.org/wlroots/wlroots.git 35 | tasks: 36 | - install-deps: | 37 | ZIG_VERSION=0.11.0 38 | WAYLAND_VERSION=1.21.0 39 | WLROOTS_VERSION=0.16.0 40 | 41 | cd wayland 42 | git checkout $WAYLAND_VERSION 43 | meson setup build -Ddocumentation=false -Dtests=false --prefix /usr 44 | sudo ninja -C build install 45 | cd .. 46 | 47 | cd wlroots 48 | git checkout $WLROOTS_VERSION 49 | meson setup build --auto-features=enabled -Drenderers=gles2 -Dexamples=false -Dwerror=false -Db_ndebug=false -Dxcb-errors=disabled --prefix /usr 50 | sudo ninja -C build install 51 | cd .. 52 | 53 | cd scenefx 54 | meson setup build --auto-features enabled --reconfigure -Dwerror=false -Dexamples=false 55 | sudo ninja -C build install 56 | cd .. 57 | 58 | wget -nv https://ziglang.org/download/$ZIG_VERSION/zig-linux-x86_64-$ZIG_VERSION.tar.xz 59 | tar -xvf zig-linux-x86_64-$ZIG_VERSION.tar.xz 1>/dev/null 60 | sudo mv ./zig-linux-x86_64-$ZIG_VERSION/zig /usr/bin 61 | sudo mv ./zig-linux-x86_64-$ZIG_VERSION/lib /usr/lib/zig 62 | 63 | - build: | 64 | cd NextWM; zig build 65 | 66 | - build-xwayland: | 67 | cd NextWM; zig build -Dxwayland -Dxwayland-lazy 68 | 69 | - build-rs: | 70 | make -C NextWM/nextctl-rs 71 | 72 | - build-go: | 73 | make -C NextWM/nextctl-go 74 | 75 | - fmt: | 76 | make check -C NextWM 77 | -------------------------------------------------------------------------------- /.builds/nix.yml: -------------------------------------------------------------------------------- 1 | image: nixos/unstable 2 | 3 | repositories: 4 | nixpkgs: https://nixos.org/channels/nixpkgs-unstable 5 | 6 | environment: 7 | NIX_CONFIG: "experimental-features = nix-command flakes" 8 | 9 | sources: 10 | - https://git.sr.ht/~shinyzenith/NextWM 11 | 12 | tasks: 13 | - build: | 14 | cd NextWM; nix develop --command zig build 15 | 16 | - build-xwayland: | 17 | cd NextWM; nix develop --command zig build -Dxwayland -Dxwayland-lazy 18 | 19 | - build-rs: | 20 | cd NextWM; nix develop --command make -C ./nextctl-rs 21 | 22 | - build-go: | 23 | cd NextWM; nix develop --command make -C ./nextctl-go 24 | 25 | - fmt: | 26 | cd NextWM; nix develop --command make check 27 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | # 3 | # .clang-format 4 | # 5 | # Created by: Aakash Sen Sharma, July 2022 6 | # Copyright: (C) 2022, Aakash Sen Sharma & Contributors 7 | --- 8 | AccessModifierOffset: -4 9 | AlignAfterOpenBracket: Align 10 | AlignConsecutiveAssignments: false 11 | AlignConsecutiveDeclarations: false 12 | AlignEscapedNewlines: Left 13 | AlignOperands: true 14 | AlignTrailingComments: false 15 | AllowAllParametersOfDeclarationOnNextLine: false 16 | AllowShortBlocksOnASingleLine: false 17 | AllowShortCaseLabelsOnASingleLine: false 18 | AllowShortFunctionsOnASingleLine: None 19 | AllowShortIfStatementsOnASingleLine: false 20 | AllowShortLoopsOnASingleLine: false 21 | AlwaysBreakAfterDefinitionReturnType: None 22 | AlwaysBreakAfterReturnType: None 23 | AlwaysBreakBeforeMultilineStrings: false 24 | AlwaysBreakTemplateDeclarations: false 25 | BinPackArguments: true 26 | BinPackParameters: true 27 | BraceWrapping: 28 | AfterClass: false 29 | AfterControlStatement: false 30 | AfterEnum: false 31 | AfterFunction: false 32 | AfterNamespace: true 33 | AfterObjCDeclaration: false 34 | AfterStruct: false 35 | AfterUnion: false 36 | AfterExternBlock: false 37 | BeforeCatch: false 38 | BeforeElse: false 39 | IndentBraces: false 40 | SplitEmptyFunction: true 41 | SplitEmptyRecord: true 42 | SplitEmptyNamespace: true 43 | BreakBeforeBinaryOperators: None 44 | BreakBeforeBraces: Custom 45 | BreakBeforeInheritanceComma: false 46 | BreakBeforeTernaryOperators: false 47 | BreakConstructorInitializersBeforeComma: false 48 | BreakConstructorInitializers: BeforeComma 49 | BreakAfterJavaFieldAnnotations: false 50 | BreakStringLiterals: false 51 | ColumnLimit: 90 52 | CommentPragmas: '^ IWYU pragma:' 53 | CompactNamespaces: false 54 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 55 | ConstructorInitializerIndentWidth: 4 56 | ContinuationIndentWidth: 4 57 | Cpp11BracedListStyle: false 58 | DerivePointerAlignment: false 59 | DisableFormat: false 60 | ExperimentalAutoDetectBinPacking: false 61 | FixNamespaceComments: false 62 | 63 | IncludeBlocks: Preserve 64 | IncludeCategories: 65 | - Regex: '.*' 66 | Priority: 1 67 | IncludeIsMainRegex: '(Test)?$' 68 | IndentCaseLabels: false 69 | IndentGotoLabels: false 70 | IndentPPDirectives: None 71 | IndentWidth: 4 72 | IndentWrappedFunctionNames: false 73 | JavaScriptQuotes: Leave 74 | JavaScriptWrapImports: true 75 | KeepEmptyLinesAtTheStartOfBlocks: false 76 | MacroBlockBegin: '' 77 | MacroBlockEnd: '' 78 | MaxEmptyLinesToKeep: 1 79 | NamespaceIndentation: None 80 | ObjCBinPackProtocolList: Auto 81 | ObjCBlockIndentWidth: 4 82 | ObjCSpaceAfterProperty: true 83 | ObjCSpaceBeforeProtocolList: true 84 | 85 | # Taken from git's rules 86 | PenaltyBreakAssignment: 10 87 | PenaltyBreakBeforeFirstCallParameter: 30 88 | PenaltyBreakComment: 10 89 | PenaltyBreakFirstLessLess: 0 90 | PenaltyBreakString: 10 91 | PenaltyExcessCharacter: 100 92 | PenaltyReturnTypeOnItsOwnLine: 60 93 | 94 | PointerAlignment: Right 95 | ReflowComments: false 96 | SortIncludes: false 97 | SortUsingDeclarations: false 98 | SpaceAfterCStyleCast: false 99 | SpaceAfterTemplateKeyword: true 100 | SpaceBeforeAssignmentOperators: true 101 | SpaceBeforeCtorInitializerColon: true 102 | SpaceBeforeInheritanceColon: true 103 | SpaceBeforeParens: ControlStatementsExceptForEachMacros 104 | SpaceBeforeRangeBasedForLoopColon: true 105 | SpaceInEmptyParentheses: false 106 | SpacesBeforeTrailingComments: 1 107 | SpacesInAngles: false 108 | SpacesInContainerLiterals: false 109 | SpacesInCStyleCastParentheses: false 110 | SpacesInParentheses: false 111 | SpacesInSquareBrackets: false 112 | Standard: Cpp03 113 | TabWidth: 4 114 | UseTab: Always 115 | ... 116 | 117 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = lf 4 | insert_final_newline = true 5 | trim_trailing_whitespace = true 6 | indent_style = space 7 | indent_size = 4 8 | 9 | [*.xml, *.nix] 10 | indent_size = 2 11 | 12 | [Makefile] 13 | indent_style = tab 14 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.github/workflows/arch.yaml: -------------------------------------------------------------------------------- 1 | name: Build NextWM 2 | 3 | on: [push, pull_request] 4 | jobs: 5 | build: 6 | name: "Build NextWM" 7 | runs-on: ubuntu-latest 8 | container: 9 | image: archlinux 10 | steps: 11 | - name: Get required pacman pkgs 12 | run: | 13 | sed -i 's/SigLevel = Required DatabaseOptional/SigLevel = Optional TrustAll/' /etc/pacman.conf 14 | pacman --noconfirm --noprogressbar -Syyu 15 | pacman --noconfirm --noprogressbar -Sy wayland-protocols xorg-xwayland make git pkgconf scdoc clang rustup go meson wget libdrm libinput mesa libffi expat hwdata libxkbcommon xcb-util-image xcb-util-renderutil xcb-util-wm pixman libevdev seatd libxcb xcb-proto xcb-util-errors cairo libjpeg-turbo 16 | rustup install stable 17 | rustup default stable 18 | 19 | - name: Installing dependencies 20 | run: | 21 | ZIG_VERSION=0.11.0 22 | WAYLAND_VERSION=1.21.0 23 | WLROOTS_VERSION=0.16.0 24 | 25 | git clone https://gitlab.freedesktop.org/wayland/wayland.git 26 | cd wayland 27 | git checkout $WAYLAND_VERSION 28 | meson setup build -Ddocumentation=false -Dtests=false --prefix /usr 29 | ninja -C build install 30 | cd .. 31 | 32 | git clone https://gitlab.freedesktop.org/wlroots/wlroots.git 33 | cd wlroots 34 | git checkout $WLROOTS_VERSION 35 | meson setup build --auto-features=enabled -Drenderers=gles2 -Dexamples=false -Dwerror=false -Db_ndebug=false --prefix /usr 36 | ninja -C build install 37 | cd .. 38 | 39 | git clone https://github.com/wlrfx/scenefx.git 40 | cd scenefx 41 | meson setup build --auto-features enabled --reconfigure -Dwerror=false -Dexamples=false 42 | ninja -C build install 43 | cd .. 44 | 45 | 46 | wget -nv https://ziglang.org/download/$ZIG_VERSION/zig-linux-x86_64-$ZIG_VERSION.tar.xz 47 | tar -xvf zig-linux-x86_64-$ZIG_VERSION.tar.xz 1>/dev/null 48 | mv ./zig-linux-x86_64-$ZIG_VERSION/zig /usr/bin 49 | mv ./zig-linux-x86_64-$ZIG_VERSION/lib /usr/lib/zig 50 | 51 | - name: Checkout NextWM 52 | uses: actions/checkout@v3 53 | with: 54 | submodules: recursive 55 | 56 | - name: Build NextWM 57 | run: | 58 | zig build 59 | 60 | - name: Build NextWM-Xwayland 61 | run: | 62 | zig build -Dxwayland -Dxwayland-lazy 63 | 64 | - name: Build Nextctl-rs 65 | run: | 66 | make -C ./nextctl-rs 67 | 68 | - name: Build Nextctl-go 69 | run: | 70 | make BUILD_FLAGS="-buildvcs=false" -C ./nextctl-go 71 | 72 | - name: Formatting Check 73 | run: | 74 | make check 75 | -------------------------------------------------------------------------------- /.github/workflows/nix.yaml: -------------------------------------------------------------------------------- 1 | name: Build NextWM (nix) 2 | 3 | on: [push, pull_request] 4 | jobs: 5 | nix: 6 | name: "Build NextWM" 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout NextWM 10 | uses: actions/checkout@v3 11 | with: 12 | submodules: recursive 13 | 14 | - name: install nix 15 | uses: cachix/install-nix-action@v20 16 | with: 17 | install_url: https://nixos.org/nix/install 18 | extra_nix_config: | 19 | auto-optimise-store = true 20 | access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} 21 | experimental-features = nix-command flakes 22 | 23 | - name: Build NextWM 24 | run: | 25 | nix develop --command zig build 26 | 27 | - name: Build NextWM-Xwayland 28 | run: | 29 | nix develop --command zig build -Dxwayland -Dxwayland-lazy 30 | 31 | - name: Build Nextctl-rs 32 | run: | 33 | nix develop --command make -C ./nextctl-rs 34 | 35 | - name: Build Nextctl-go 36 | run: | 37 | nix develop --command make BUILD_FLAGS="-buildvcs=false" -C ./nextctl-go 38 | 39 | - name: Formatting Check 40 | run: | 41 | nix develop --command make check 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gz 2 | *.o 3 | next-protocols.pc 4 | next_control_v1.rs 5 | nextctl-go/nextctl 6 | nextctl-go/pkg/next_control/next_control_v1.go 7 | nextctl/src/next-control-v1.c 8 | nextctl/include/next-control-v1.h 9 | target/ 10 | zig-cache 11 | zig-out 12 | .direnv 13 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/zig-wayland"] 2 | path = deps/zig-wayland 3 | url = https://github.com/ifreund/zig-wayland 4 | ignore = dirty 5 | [submodule "deps/zig-xkbcommon"] 6 | path = deps/zig-xkbcommon 7 | url = https://github.com/ifreund/zig-xkbcommon 8 | ignore = dirty 9 | [submodule "protocols/wlr-protocols"] 10 | path = protocols/wlr-protocols 11 | url = https://gitlab.freedesktop.org/wlroots/wlr-protocols 12 | ignore = dirty 13 | [submodule "deps/zig-clap"] 14 | path = deps/zig-clap 15 | url = https://github.com/Hejsil/zig-clap 16 | ignore = dirty 17 | [submodule "deps/zig-pixman"] 18 | path = deps/zig-pixman 19 | url = https://github.com/ifreund/zig-pixman 20 | [submodule "deps/zig-wlroots"] 21 | path = deps/zig-wlroots 22 | url = https://github.com/wlrfx/zig-wlroots 23 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition="2021" 2 | newline_style = "Unix" 3 | use_field_init_shorthand = true 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Aakash Sen Sharma. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX:=/usr 2 | BUILD_FLAGS:= 3 | 4 | build: 5 | zig build $(BUILD_FLAGS) 6 | 7 | install: 8 | zig build $(BUILD_FLAGS) --prefix $(PREFIX) 9 | 10 | check: 11 | $(MAKE) -C ./nextctl -s $@ 12 | $(MAKE) -C ./nextctl-rs -s $@ 13 | $(MAKE) -C ./nextctl-go -s $@ 14 | 15 | zig fmt --check next/ 16 | zig fmt --check *.zig 17 | 18 | uninstall: 19 | $(RM) $(PREFIX)/bin/next 20 | $(RM) $(PREFIX)/bin/nextctl 21 | $(RM) $(PREFIX)/share/man/man1/next.1.gz 22 | $(RM) $(PREFIX)/share/man/man1/nextctl.1.gz 23 | $(RM) $(PREFIX)/share/wayland-sessions/next.desktop 24 | $(RM) $(PREFIX)/share/next-protocols/next-control-v1.xml 25 | $(RM) $(PREFIX)/share/pkgconfig/next-protocols.pc 26 | 27 | clean: 28 | $(MAKE) -C ./nextctl-go -s $@ 29 | $(MAKE) -C ./nextctl-rs -s $@ 30 | 31 | $(RM) -r ./nextctl/zig-cache ./nextctl/zig-out 32 | $(RM) -r zig-cache zig-out 33 | $(RM) ./docs/*.gz 34 | $(RM) -r ./deps/scenefx/build 35 | $(RM) *.pc 36 | 37 | 38 | .PHONY: build clean install uninstall check 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NextWM 2 | 3 | Manual tiling wayland compositor written with wlroots aimed to be a bspwm clone. 4 | 5 | Note: NextWM is still a work in progress project. It won't be useable anytime soon, but when it is I will be the first one to spam screenshots of it in the readme. 6 | 7 |

8 | 9 | 10 | 11 |

12 | 13 | 14 | ## License: 15 | 16 | The entire project is licensed as BSD-2 "Simplified" unless stated otherwise in the file header. 17 | 18 | ## Aim 19 | 20 | I want to learn how to write wlroots compositors with this project. 21 | 22 | ## Why multiple implementations of Nextctl? 23 | 24 | Since this project is meant to teach others, why not show people how wayland clients are written in different languages :) ? 25 | 26 | ## To-Do 27 | 28 | - Compress man pages using zig stdlib. 29 | - Simple inbuilt bar. 30 | - Toplevel location data export? 31 | - focused_wlr_output and focused_toplevel data export? 32 | 33 | ## Building 34 | 35 | Note: All Nextctl implementations are exactly identical. 36 | 37 | ### Build Flags 38 | - `-Dxwayland` flag enables Xwayland supoprt. 39 | - `-Dxwayland-lazy` lazy load Xwayland (might have slightly worse xwayland startup times but reduces resource consumption). 40 | - `-Dnextctl-rs` Compile the Rust version of Nextctl (Default is C codebase). 41 | - `-Dnextctl-go` Compile the Go version of Nextctl (Default is C codebase). 42 | 43 | ### Depedencies 44 | 45 | 1. `cargo` (Optional. Required if you build Rust implementation of Nextctl) * 46 | 1. `go` 1.18 (Optional. Required if you build Go implementation of Nextctl) * 47 | 1. `libevdev` 48 | 1. `libinput` 49 | 1. `make` * 50 | 1. `pixman` 51 | 1. `pkg-config` * 52 | 1. `scdoc` (Optional. If scdoc binary is not found, man pages are not generated.) * 53 | 1. `wayland-protocols` * 54 | 1. `wayland` 55 | 1. `wlroots` 0.16 56 | 1. `scenefx` (Currently chasing master as there's no tagged release.) 57 | 1. `xkbcommon` 58 | 1. `xwayland` (Optional. Required if you want Xwayland support.) 59 | 1. `zig` 0.11.0 * 60 | 61 | _\* Compile-time dependencies_ 62 | 63 | ## Steps 64 | 65 | ```bash 66 | git clone --recursive https://git.sr.ht/~shinyzenith/NextWM 67 | sudo make install 68 | ``` 69 | 70 | ## Keybind handling 71 | 72 | Consider using the compositors in-built key mapper or [swhkd](https://github.com/shinyzenith/swhkd) if you're looking for a sxhkd like experience. 73 | 74 | ## Contributing: 75 | 76 | Send patches to: 77 | [~shinyzenith/NextWM@lists.sr.ht](https://lists.sr.ht/~shinyzenith/NextWM) 78 | 79 | ## Bug tracker: 80 | 81 | https://todo.sr.ht/~shinyzenith/NextWM 82 | 83 | ## Support 84 | 85 | - https://matrix.to/#/#waycrate-tools:matrix.org 86 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | // 3 | // build.zig 4 | // 5 | // Created by: Aakash Sen Sharma, May 2023 6 | // Copyright: (C) 2022, Aakash Sen Sharma & Contributors 7 | 8 | const std = @import("std"); 9 | 10 | const NextctlStep = @import("nextctl.zig"); 11 | const Scdoc = @import("scdoc.zig"); 12 | const Nextctl = @import("nextctl.zig"); 13 | const Scanner = @import("deps/zig-wayland/build.zig").Scanner; 14 | 15 | const version = "0.1.0-dev"; 16 | 17 | pub fn build(builder: *std.Build) !void { 18 | const target = builder.standardTargetOptions(.{}); 19 | const optimize = builder.standardOptimizeOption(.{}); 20 | 21 | const xwayland = builder.option(bool, "xwayland", "Set to true to enable XWayland features.") orelse false; 22 | const xwayland_lazy = builder.option(bool, "xwayland-lazy", "Set to true to enable XWayland lazy initialization.") orelse false; 23 | 24 | const nextctl_rs = builder.option(bool, "nextctl-rs", "If enabled, rust version is built, else C.") orelse false; 25 | const nextctl_go = builder.option(bool, "nextctl-go", "If enabled, go version is built, else C.") orelse false; 26 | 27 | const exe = builder.addExecutable(.{ 28 | .name = "next", 29 | .root_source_file = .{ .path = "next/next.zig" }, 30 | .target = target, 31 | .optimize = optimize, 32 | }); 33 | 34 | exe.addCSourceFile(.{ 35 | .file = .{ .path = "./next/utils/wlr_log.c" }, 36 | .flags = &.{ "-std=c18", "-O3" }, 37 | }); // Zig doesn't have good var arg support 38 | 39 | const options = builder.addOptions(); 40 | options.addOption([]const u8, "version", version); 41 | options.addOption(bool, "xwayland_lazy", xwayland_lazy); 42 | options.addOption(bool, "xwayland", xwayland); 43 | 44 | exe.addOptions("build_options", options); 45 | 46 | const scanner = Scanner.create(builder, .{}); 47 | scanner.addCSource(exe); // TODO: remove: https://github.com/ziglang/zig/issues/131 48 | 49 | generate_protocol_files(scanner); 50 | 51 | // Packages: 52 | { 53 | const wayland = builder.createModule( 54 | .{ .source_file = scanner.result }, 55 | ); 56 | exe.addModule("wayland", wayland); 57 | 58 | const xkbcommon = builder.createModule(.{ 59 | .source_file = .{ .path = "deps/zig-xkbcommon/src/xkbcommon.zig" }, 60 | }); 61 | exe.addModule("xkbcommon", xkbcommon); 62 | 63 | const pixman = builder.createModule(.{ 64 | .source_file = .{ .path = "deps/zig-pixman/pixman.zig" }, 65 | }); 66 | exe.addModule("pixman", pixman); 67 | 68 | const clap = builder.createModule(.{ 69 | .source_file = .{ .path = "deps/zig-clap/clap.zig" }, 70 | }); 71 | exe.addModule("clap", clap); 72 | 73 | const wlroots = builder.createModule(.{ 74 | .source_file = .{ .path = "deps/zig-wlroots/src/wlroots.zig" }, 75 | .dependencies = &.{ 76 | .{ .name = "wayland", .module = wayland }, 77 | .{ .name = "xkbcommon", .module = xkbcommon }, 78 | .{ .name = "pixman", .module = pixman }, 79 | }, 80 | }); 81 | exe.addModule("wlroots", wlroots); 82 | } 83 | 84 | // Links: 85 | { 86 | exe.linkLibC(); 87 | exe.linkSystemLibrary("cairo"); 88 | exe.linkSystemLibrary("libdrm"); 89 | exe.linkSystemLibrary("libevdev"); 90 | exe.linkSystemLibrary("libinput"); 91 | exe.linkSystemLibrary("libturbojpeg"); 92 | exe.linkSystemLibrary("libjpeg"); 93 | exe.linkSystemLibrary("pixman-1"); 94 | exe.linkSystemLibrary("wayland-server"); 95 | exe.linkSystemLibrary("scenefx"); 96 | exe.linkSystemLibrary("wlroots"); 97 | exe.linkSystemLibrary("xkbcommon"); 98 | } 99 | builder.installArtifact(exe); 100 | 101 | // Scdoc installation 102 | { 103 | if (blk: { 104 | _ = builder.findProgram(&.{"scdoc"}, &.{}) catch |err| switch (err) { 105 | error.FileNotFound => break :blk false, 106 | else => return err, 107 | }; 108 | break :blk true; 109 | }) { 110 | try Scdoc.build(builder, "./docs/"); 111 | } 112 | } 113 | 114 | // Nextctl Installation 115 | { 116 | const build_type: NextctlStep.BuildType = blk: { 117 | if (nextctl_rs and nextctl_go) { 118 | @panic("Please choose only 1 Nextctl Implementation."); 119 | } else if (nextctl_rs) { 120 | break :blk .rust; 121 | } else if (nextctl_go) { 122 | break :blk .go; 123 | } else { 124 | break :blk .c; 125 | } 126 | }; 127 | 128 | const nextctl = try NextctlStep.init(builder, build_type, version); 129 | try nextctl.install(); 130 | } 131 | 132 | // Pkgconfig installation. 133 | { 134 | const write_file = std.Build.Step.WriteFile.create(builder); 135 | const pkgconfig_file = write_file.add("next-protocols.pc", builder.fmt( 136 | \\prefix={s} 137 | \\datadir=${{prefix}}/share 138 | \\pkgdatadir=${{datadir}}/next-protocols 139 | \\ 140 | \\Name: next-protocols 141 | \\URL: https://git.sr.ht/~shinyzenith/nextwm 142 | \\Description: protocol files for NextWM 143 | \\Version: {s} 144 | , .{ builder.install_prefix, version })); 145 | 146 | builder.installFile("protocols/next-control-v1.xml", "share/next-protocols/next-control-v1.xml"); 147 | builder.getInstallStep().dependOn(&builder.addInstallFile( 148 | pkgconfig_file, 149 | "share/pkgconfig/next-protocols.pc", 150 | ).step); 151 | } 152 | 153 | builder.installFile("./next.desktop", "share/wayland-sessions/next.desktop"); 154 | } 155 | 156 | fn generate_protocol_files(scanner: *Scanner) void { 157 | scanner.addSystemProtocol("stable/xdg-shell/xdg-shell.xml"); 158 | scanner.addSystemProtocol("staging/ext-session-lock/ext-session-lock-v1.xml"); 159 | scanner.addSystemProtocol("unstable/pointer-constraints/pointer-constraints-unstable-v1.xml"); 160 | scanner.addSystemProtocol("unstable/pointer-gestures/pointer-gestures-unstable-v1.xml"); 161 | scanner.addSystemProtocol("unstable/xdg-decoration/xdg-decoration-unstable-v1.xml"); 162 | 163 | scanner.addCustomProtocol("protocols/next-control-v1.xml"); 164 | scanner.addCustomProtocol("protocols/wlr-protocols/unstable/wlr-layer-shell-unstable-v1.xml"); 165 | scanner.addCustomProtocol("protocols/wlr-protocols/unstable/wlr-output-power-management-unstable-v1.xml"); 166 | 167 | // Generating the bindings we require, we need to manually update this. 168 | scanner.generate("wl_compositor", 4); 169 | scanner.generate("wl_subcompositor", 1); 170 | scanner.generate("wl_shm", 1); 171 | scanner.generate("wl_output", 4); 172 | scanner.generate("wl_seat", 7); 173 | scanner.generate("wl_data_device_manager", 3); 174 | scanner.generate("xdg_wm_base", 2); 175 | 176 | scanner.generate("zwlr_layer_shell_v1", 4); 177 | scanner.generate("zwlr_output_power_manager_v1", 1); 178 | scanner.generate("zwp_pointer_constraints_v1", 1); 179 | scanner.generate("zwp_pointer_gestures_v1", 3); 180 | scanner.generate("zxdg_decoration_manager_v1", 1); 181 | 182 | scanner.generate("ext_session_lock_manager_v1", 1); 183 | 184 | scanner.generate("next_control_v1", 1); 185 | } 186 | -------------------------------------------------------------------------------- /docs/next.1.scd: -------------------------------------------------------------------------------- 1 | next(1) "git.sr.ht/~shinyzenith/NextWM" "General Commands Manual" 2 | 3 | # NAME 4 | 5 | NextWM - Wayland compositing window manager. 6 | 7 | # SYNOPSIS 8 | 9 | *next* [_flags_] 10 | 11 | # DESCRIPTION 12 | 13 | *NextWM* is a wayland compositing window manager written in Zig. 14 | 15 | # OPTIONS 16 | 17 | TODO 18 | 19 | # AUTHORS 20 | 21 | Maintained by Shinyzenith . 22 | For more information about development, see . 23 | -------------------------------------------------------------------------------- /docs/nextctl.1.scd: -------------------------------------------------------------------------------- 1 | nextctl(1) "git.sr.ht/~shinyzenith/NextWM" "General Commands Manual" 2 | 3 | # NAME 4 | 5 | nextctl 6 | 7 | # SYNOPSIS 8 | 9 | *nextctl* [args] 10 | 11 | # DESCRIPTION 12 | 13 | *nextctl* is a command line tool to control NextWM. 14 | 15 | # OPTIONS 16 | 17 | *-h* 18 | Print the help text and exit. 19 | 20 | # AUTHORS 21 | 22 | Maintained by Shinyzenith . 23 | For more information about development, see . 24 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1698266953, 6 | "narHash": "sha256-jf72t7pC8+8h8fUslUYbWTX5rKsRwOzRMX8jJsGqDXA=", 7 | "owner": "nixos", 8 | "repo": "nixpkgs", 9 | "rev": "75a52265bda7fd25e06e3a67dee3f0354e73243c", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "nixos", 14 | "ref": "nixpkgs-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "NextWM devel"; 3 | 4 | inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; }; 5 | 6 | outputs = { self, nixpkgs, ... }: 7 | let 8 | pkgsFor = system: 9 | import nixpkgs { 10 | inherit system; 11 | overlays = [ ]; 12 | }; 13 | 14 | targetSystems = [ "aarch64-linux" "x86_64-linux" ]; 15 | in { 16 | devShells = nixpkgs.lib.genAttrs targetSystems (system: 17 | let pkgs = pkgsFor system; 18 | in { 19 | default = pkgs.mkShell { 20 | name = "NextWM-devel"; 21 | 22 | nativeBuildInputs = with pkgs; [ 23 | # Compilers 24 | cargo 25 | go 26 | rustc 27 | scdoc 28 | zig 29 | 30 | # Libs 31 | cairo 32 | hwdata 33 | libGL 34 | libdrm 35 | libevdev 36 | libinput 37 | libjpeg 38 | libxkbcommon 39 | mesa 40 | pixman 41 | stdenv 42 | udev 43 | wayland 44 | wayland-protocols 45 | wlroots_0_16 46 | (callPackage ./scenefx.nix { }) 47 | 48 | # Tools 49 | clang-tools 50 | cmake 51 | gdb 52 | gnumake 53 | gopls 54 | meson 55 | ninja 56 | pkg-config 57 | rust-analyzer 58 | rustfmt 59 | strace 60 | valgrind 61 | wayland-scanner 62 | zls 63 | ]; 64 | }; 65 | }); 66 | }; 67 | } 68 | -------------------------------------------------------------------------------- /next.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Next 3 | Comment=Manual tiling compositor written in zig. 4 | Exec=next 5 | DesktopNames=Next 6 | Type=Application 7 | -------------------------------------------------------------------------------- /next/Config.zig: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | // 3 | // next/Config.zig 4 | // 5 | // Created by: Aakash Sen Sharma, May 2022 6 | // Copyright: (C) 2022, Aakash Sen Sharma & Contributors 7 | 8 | const Self = @This(); 9 | 10 | const std = @import("std"); 11 | const allocator = @import("utils/allocator.zig").allocator; 12 | const log = std.log.scoped(.Config); 13 | 14 | const Window = @import("desktop/Window.zig"); 15 | 16 | pub const CursorWarpMode = enum { 17 | disabled, 18 | @"on-output-change", 19 | }; 20 | 21 | // Titles and app-id's of toplevels that should render client side decorations. 22 | csd_app_ids: std.StringHashMapUnmanaged(void) = .{}, 23 | csd_titles: std.StringHashMapUnmanaged(void) = .{}, 24 | 25 | cursor_hide_when_typing: bool = false, 26 | warp_cursor: CursorWarpMode = .disabled, 27 | 28 | // Red - default border color. 29 | border_color: [4]f32 = .{ 1, 0, 0, 1 }, 30 | 31 | repeat_rate: i32 = 100, 32 | repeat_delay: i32 = 300, 33 | 34 | border_width: u8 = 2, 35 | 36 | //TODO: make these configurable 37 | toplevel_corner_radius: c_int = 20, 38 | toplevel_opacity: f32 = 1, // Ranges from 0 to 1 39 | toplevel_box_shadow_color: [4]f32 = .{ 0.0, 0.0, 0.0, 1.0 }, 40 | 41 | focus_is_sloppy: bool = true, 42 | 43 | pub fn init() Self { 44 | log.debug("Initialized compositor config", .{}); 45 | const self = .{}; 46 | errdefer self.deinit(); 47 | 48 | return self; 49 | } 50 | 51 | pub fn csdAllowed(self: Self, window: *Window) bool { 52 | if (self.csd_app_ids.contains(std.mem.sliceTo(window.getAppId(), 0))) { 53 | return true; 54 | } 55 | 56 | if (self.csd_titles.contains(std.mem.sliceTo(window.getTitle(), 0))) { 57 | return true; 58 | } 59 | return false; 60 | } 61 | 62 | pub fn deinit(self: *Self) void { 63 | log.debug("Destroying server configuration allocations", .{}); 64 | 65 | var app_id_it = self.csd_app_ids.keyIterator(); 66 | while (app_id_it.next()) |key| allocator.free(key.*); 67 | 68 | var title_it = self.csd_titles.keyIterator(); 69 | while (title_it.next()) |key| allocator.free(key.*); 70 | 71 | self.csd_app_ids.deinit(allocator); 72 | self.csd_titles.deinit(allocator); 73 | } 74 | -------------------------------------------------------------------------------- /next/Server.zig: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | // 3 | // next/Server.zig 4 | // 5 | // Created by: Aakash Sen Sharma, May 2022 6 | // Copyright: (C) 2022, Aakash Sen Sharma & Contributors 7 | 8 | const Self = @This(); 9 | 10 | const allocator = @import("utils/allocator.zig").allocator; 11 | const build_options = @import("build_options"); 12 | const c = @import("utils/c.zig"); 13 | const log = std.log.scoped(.Server); 14 | const std = @import("std"); 15 | 16 | const wl = @import("wayland").server.wl; 17 | const wlr = @import("wlroots"); 18 | 19 | const Config = @import("Config.zig"); 20 | const Control = @import("global/Control.zig"); 21 | const Cursor = @import("input/Cursor.zig"); 22 | const DecorationManager = @import("desktop/DecorationManager.zig"); 23 | const InputManager = @import("input/InputManager.zig"); 24 | const Keyboard = @import("input/Keyboard.zig"); 25 | const Output = @import("desktop/Output.zig"); 26 | const OutputLayout = @import("desktop/OutputLayout.zig"); 27 | const Seat = @import("input/Seat.zig"); 28 | const Window = @import("desktop/Window.zig"); 29 | const XdgToplevel = @import("desktop/XdgToplevel.zig"); 30 | 31 | const default_cursor_size = 24; 32 | 33 | wl_server: *wl.Server, 34 | wl_event_loop: *wl.EventLoop, 35 | wlr_backend: *wlr.Backend, 36 | wlr_headless_backend: *wlr.Backend, 37 | wlr_renderer: *wlr.Renderer, 38 | wlr_allocator: *wlr.Allocator, 39 | wlr_scene: *wlr.Scene, 40 | wlr_compositor: *wlr.Compositor, 41 | 42 | wlr_foreign_toplevel_manager: *wlr.ForeignToplevelManagerV1, 43 | 44 | config: Config, 45 | control: Control, 46 | decoration_manager: DecorationManager, 47 | input_manager: InputManager, 48 | output_layout: OutputLayout, 49 | seat: Seat, 50 | 51 | // Layers 52 | layer_bg: *wlr.SceneTree, 53 | layer_fullscreen: *wlr.SceneTree, 54 | layer_bottom: *wlr.SceneTree, 55 | layer_float: *wlr.SceneTree, 56 | layer_nofocus: *wlr.SceneTree, 57 | layer_overlay: *wlr.SceneTree, 58 | layer_tile: *wlr.SceneTree, 59 | layer_top: *wlr.SceneTree, 60 | 61 | sigint_cb: *wl.EventSource, 62 | sigterm_cb: *wl.EventSource, 63 | sigkill_cb: *wl.EventSource, 64 | sigabrt_cb: *wl.EventSource, 65 | sigquit_cb: *wl.EventSource, 66 | 67 | wlr_output_manager: *wlr.OutputManagerV1, 68 | new_output: wl.Listener(*wlr.Output), 69 | 70 | outputs: std.ArrayListUnmanaged(*Output), 71 | keyboards: std.ArrayListUnmanaged(*Keyboard), 72 | cursors: std.ArrayListUnmanaged(*Cursor), 73 | 74 | wlr_xdg_shell: *wlr.XdgShell, 75 | new_xdg_surface: wl.Listener(*wlr.XdgSurface), 76 | mapped_windows: std.ArrayListUnmanaged(*Window), 77 | pending_windows: std.ArrayListUnmanaged(*Window), 78 | 79 | wlr_layer_shell: *wlr.LayerShellV1, 80 | new_layer_surface: wl.Listener(*wlr.LayerSurfaceV1), 81 | 82 | wlr_xwayland: if (build_options.xwayland) *wlr.Xwayland else void, 83 | new_xwayland_surface: if (build_options.xwayland) wl.Listener(*wlr.XwaylandSurface) else void, 84 | 85 | wlr_power_manager: *wlr.OutputPowerManagerV1, 86 | set_mode: wl.Listener(*wlr.OutputPowerManagerV1.event.SetMode), 87 | 88 | wlr_cursor: *wlr.Cursor, 89 | wlr_xcursor_manager: *wlr.XcursorManager, 90 | 91 | wlr_xdg_activation_v1: *wlr.XdgActivationV1, 92 | request_activate: wl.Listener(*wlr.XdgActivationV1.event.RequestActivate), 93 | 94 | pub fn init(self: *Self) !void { 95 | logInfo(); 96 | 97 | self.wl_server = try wl.Server.create(); 98 | errdefer self.wl_server.destroy(); 99 | 100 | self.wl_event_loop = self.wl_server.getEventLoop(); 101 | self.sigabrt_cb = try self.wl_event_loop.addSignal(*wl.Server, std.os.SIG.ABRT, terminateCb, self.wl_server); 102 | self.sigint_cb = try self.wl_event_loop.addSignal(*wl.Server, std.os.SIG.INT, terminateCb, self.wl_server); 103 | self.sigquit_cb = try self.wl_event_loop.addSignal(*wl.Server, std.os.SIG.QUIT, terminateCb, self.wl_server); 104 | self.sigterm_cb = try self.wl_event_loop.addSignal(*wl.Server, std.os.SIG.TERM, terminateCb, self.wl_server); 105 | 106 | // NOTE: This frees itself when the server is destroyed. 107 | self.wlr_backend = try wlr.Backend.autocreate(self.wl_server); 108 | 109 | // Created when no ouputs are available. 110 | // NOTE: This frees itself when server is destroyed. 111 | self.wlr_headless_backend = try wlr.Backend.createHeadless(self.wl_server); 112 | 113 | self.wlr_renderer = try wlr.Renderer.autocreate(self.wlr_backend); 114 | errdefer self.wlr_renderer.destroy(); 115 | 116 | self.wlr_allocator = try wlr.Allocator.autocreate(self.wlr_backend, self.wlr_renderer); 117 | errdefer self.wlr_allocator.destroy(); 118 | 119 | self.wlr_compositor = try wlr.Compositor.create(self.wl_server, self.wlr_renderer); 120 | _ = try wlr.Subcompositor.create(self.wl_server); 121 | 122 | //NOTE: This has to be initialized as one of the first wayland globals inorder to not crash on middle-button paste in gtk3...ugh. 123 | _ = try wlr.PrimarySelectionDeviceManagerV1.create(self.wl_server); 124 | 125 | self.wlr_foreign_toplevel_manager = try wlr.ForeignToplevelManagerV1.create(self.wl_server); 126 | 127 | self.wlr_scene = try wlr.Scene.create(); 128 | 129 | try self.output_layout.init(); 130 | 131 | self.wlr_output_manager = try wlr.OutputManagerV1.create(self.wl_server); 132 | _ = try wlr.XdgOutputManagerV1.create(self.wl_server, self.output_layout.wlr_output_layout); 133 | 134 | self.wlr_cursor = try wlr.Cursor.create(); 135 | errdefer self.wlr_cursor.destroy(); 136 | 137 | self.wlr_xdg_activation_v1 = try wlr.XdgActivationV1.create(self.wl_server); 138 | 139 | self.wlr_xcursor_manager = try wlr.XcursorManager.create(null, default_cursor_size); 140 | errdefer self.wlr_xcursor_manager.destroy(); 141 | 142 | try self.wlr_xcursor_manager.load(1); 143 | 144 | self.wlr_xdg_shell = try wlr.XdgShell.create(self.wl_server, 5); 145 | self.wlr_layer_shell = try wlr.LayerShellV1.create(self.wl_server); 146 | self.wlr_power_manager = try wlr.OutputPowerManagerV1.create(self.wl_server); 147 | 148 | try self.seat.init(); 149 | 150 | // Creating toplevel layers: 151 | self.layer_bg = try self.wlr_scene.tree.createSceneTree(); 152 | self.layer_fullscreen = try self.wlr_scene.tree.createSceneTree(); // Used for direct_scanout. 153 | self.layer_bottom = try self.wlr_scene.tree.createSceneTree(); 154 | self.layer_float = try self.wlr_scene.tree.createSceneTree(); 155 | self.layer_nofocus = try self.wlr_scene.tree.createSceneTree(); 156 | self.layer_overlay = try self.wlr_scene.tree.createSceneTree(); 157 | self.layer_tile = try self.wlr_scene.tree.createSceneTree(); 158 | self.layer_top = try self.wlr_scene.tree.createSceneTree(); 159 | 160 | if (build_options.xwayland) { 161 | self.wlr_xwayland = try wlr.Xwayland.create(self.wl_server, self.wlr_compositor, build_options.xwayland_lazy); 162 | self.wlr_xwayland.setSeat(self.seat.wlr_seat); 163 | } 164 | 165 | try self.wlr_renderer.initServer(self.wl_server); 166 | try self.wlr_scene.attachOutputLayout(self.output_layout.wlr_output_layout); 167 | self.wlr_cursor.attachOutputLayout(self.output_layout.wlr_output_layout); 168 | 169 | // NOTE: These all free themselves when wlr_server is destroy. 170 | _ = try wlr.DataControlManagerV1.create(self.wl_server); 171 | _ = try wlr.DataDeviceManager.create(self.wl_server); 172 | _ = try wlr.ExportDmabufManagerV1.create(self.wl_server); 173 | _ = try wlr.GammaControlManagerV1.create(self.wl_server); 174 | _ = try wlr.ScreencopyManagerV1.create(self.wl_server); 175 | _ = try wlr.SinglePixelBufferManagerV1.create(self.wl_server); 176 | _ = try wlr.Viewporter.create(self.wl_server); 177 | 178 | try self.control.init(); 179 | try self.decoration_manager.init(); 180 | try self.input_manager.init(); 181 | self.config = Config.init(); 182 | 183 | self.new_output.setNotify(newOutput); 184 | self.wlr_backend.events.new_output.add(&self.new_output); 185 | 186 | self.new_xdg_surface.setNotify(newXdgSurface); 187 | self.wlr_xdg_shell.events.new_surface.add(&self.new_xdg_surface); 188 | 189 | self.set_mode.setNotify(setMode); 190 | self.wlr_power_manager.events.set_mode.add(&self.set_mode); 191 | 192 | self.new_layer_surface.setNotify(newLayerSurface); 193 | self.wlr_layer_shell.events.new_surface.add(&self.new_layer_surface); 194 | 195 | self.request_activate.setNotify(requestActivate); 196 | self.wlr_xdg_activation_v1.events.request_activate.add(&self.request_activate); 197 | 198 | if (build_options.xwayland) { 199 | self.new_xwayland_surface.setNotify(newXwaylandSurface); 200 | self.wlr_xwayland.events.new_surface.add(&self.new_xwayland_surface); 201 | } 202 | } 203 | 204 | pub fn start(self: *Self) !void { 205 | var buf: [11]u8 = undefined; 206 | const socket = try self.wl_server.addSocketAuto(&buf); 207 | 208 | if (c.setenv("WAYLAND_DISPLAY", socket.ptr, 1) < 0) return error.SetenvError; 209 | if (build_options.xwayland) if (c.setenv("DISPLAY", self.wlr_xwayland.display_name, 1) < 0) return error.SetenvError; 210 | 211 | try self.wlr_backend.start(); 212 | log.info("Starting NextWM on {s}", .{socket}); 213 | 214 | if (build_options.xwayland) { 215 | log.info("Xwayland initialized at {s}", .{self.wlr_xwayland.display_name}); 216 | } 217 | } 218 | 219 | pub fn deinit(self: *Self) void { 220 | log.info("Cleaning up server resources", .{}); 221 | self.sigabrt_cb.remove(); 222 | self.sigint_cb.remove(); 223 | self.sigquit_cb.remove(); 224 | self.sigterm_cb.remove(); 225 | 226 | if (build_options.xwayland) { 227 | self.wlr_xwayland.destroy(); 228 | } 229 | self.wl_server.destroyClients(); 230 | 231 | self.wlr_backend.destroy(); 232 | self.wlr_renderer.destroy(); 233 | self.wlr_allocator.destroy(); 234 | 235 | for (self.mapped_windows.items) |item| { 236 | allocator.destroy(item); 237 | } 238 | self.mapped_windows.deinit(allocator); 239 | 240 | for (self.pending_windows.items) |item| { 241 | allocator.destroy(item); 242 | } 243 | self.pending_windows.deinit(allocator); 244 | 245 | for (self.outputs.items) |output| { 246 | output.deinit_wallpaper(); 247 | allocator.destroy(output); 248 | } 249 | self.outputs.deinit(allocator); 250 | 251 | for (self.cursors.items) |item| { 252 | allocator.destroy(item); 253 | } 254 | self.cursors.deinit(allocator); 255 | 256 | for (self.keyboards.items) |item| { 257 | allocator.destroy(item); 258 | } 259 | self.keyboards.deinit(allocator); 260 | 261 | self.wlr_scene.tree.node.destroy(); 262 | 263 | self.wlr_cursor.destroy(); 264 | self.wlr_xcursor_manager.destroy(); 265 | self.output_layout.deinit(); 266 | 267 | self.seat.deinit(); 268 | 269 | // Destroy the server. 270 | self.wl_server.destroy(); 271 | self.config.deinit(); 272 | log.info("Exiting NextWM...", .{}); 273 | } 274 | 275 | // This is called to gracefully handle signals. 276 | fn terminateCb(_: c_int, wl_server: *wl.Server) c_int { 277 | log.info("Terminating event loop.", .{}); 278 | wl_server.terminate(); 279 | return 0; 280 | } 281 | 282 | fn requestActivate( 283 | _: *wl.Listener(*wlr.XdgActivationV1.event.RequestActivate), 284 | event: *wlr.XdgActivationV1.event.RequestActivate, 285 | ) void { 286 | log.debug("Signal: wlr_xdg_activation_v1_request_activate", .{}); 287 | // Handle xwayland and subsurfaces 288 | var window: ?*Window = null; 289 | if (event.surface.isXdgSurface()) { 290 | if (wlr.XdgSurface.fromWlrSurface(event.surface)) |xdg_surface| { 291 | if (xdg_surface.role == .toplevel) { 292 | window = @as(*Window, @ptrFromInt(xdg_surface.data)); 293 | } 294 | } 295 | } 296 | 297 | //TODO: Focus on the window and warp the cursor to it's center? 298 | //if (window) |win| { 299 | //} 300 | } 301 | 302 | // Callback that gets triggered on existence of a new output. 303 | fn newOutput(_: *wl.Listener(*wlr.Output), wlr_output: *wlr.Output) void { 304 | log.debug("Signal: wlr_backend_new_output", .{}); 305 | const output = allocator.create(Output) catch { 306 | std.log.err("Failed to allocate new output", .{}); 307 | return; 308 | }; 309 | errdefer allocator.destroy(output); 310 | 311 | // Instantiate the output struct. 312 | // TODO: Reminder to use this pattern and clean up similar memory leaks. 313 | // TODO: use destroy instead of free for objects on heap, check all calls of free or destroy. 314 | output.init(wlr_output) catch |err| { 315 | log.err("Failed to create output: {s}", .{@errorName(err)}); 316 | allocator.destroy(output); 317 | return; 318 | }; 319 | } 320 | 321 | fn newXdgSurface(listener: *wl.Listener(*wlr.XdgSurface), xdg_surface: *wlr.XdgSurface) void { 322 | const self = @fieldParentPtr(Self, "new_xdg_surface", listener); 323 | log.debug("Signal: wlr_xdg_shell_new_surface", .{}); 324 | if (xdg_surface.role == .toplevel) { 325 | if (self.seat.focused_output) |output| { 326 | XdgToplevel.init(output, xdg_surface) catch { 327 | log.err("Failed to allocate memory", .{}); 328 | xdg_surface.resource.postNoMemory(); 329 | return; 330 | }; 331 | } else { 332 | log.err("No focused wlr_output found. Skipping xdg_toplevel.", .{}); 333 | return; 334 | } 335 | } 336 | } 337 | 338 | // This callback is called when a new layer surface is created. 339 | pub fn newLayerSurface(_: *wl.Listener(*wlr.LayerSurfaceV1), wlr_layer_surface: *wlr.LayerSurfaceV1) void { 340 | log.debug("Signal: wlr_layer_shell_new_surface", .{}); 341 | log.debug( 342 | "New layer surface: namespace {s}, layer {s}, anchor {b:0>4}, size {},{}, margin {},{},{},{}, exclusive_zone {}", 343 | .{ 344 | wlr_layer_surface.namespace, 345 | @tagName(wlr_layer_surface.pending.layer), 346 | @as(u32, @bitCast(wlr_layer_surface.pending.anchor)), 347 | wlr_layer_surface.pending.desired_width, 348 | wlr_layer_surface.pending.desired_height, 349 | wlr_layer_surface.pending.margin.top, 350 | wlr_layer_surface.pending.margin.right, 351 | wlr_layer_surface.pending.margin.bottom, 352 | wlr_layer_surface.pending.margin.left, 353 | wlr_layer_surface.pending.exclusive_zone, 354 | }, 355 | ); 356 | // TODO: Finish this. 357 | } 358 | 359 | // This callback is called when a new xwayland surface is created. 360 | pub fn newXwaylandSurface(listener: *wl.Listener(*wlr.XwaylandSurface), xwayland_surface: *wlr.XwaylandSurface) void { 361 | _ = @fieldParentPtr(Self, "new_xwayland_surface", listener); 362 | log.debug("Signal: wlr_xwayland_new_surface", .{}); 363 | 364 | if (xwayland_surface.override_redirect) { 365 | // TODO: Create override_redirect surface. 366 | } else { 367 | //TODO: Create Xwayland window. 368 | } 369 | } 370 | 371 | // Callback that gets triggered on existence of a new output. 372 | fn setMode(listener: *wl.Listener(*wlr.OutputPowerManagerV1.event.SetMode), event: *wlr.OutputPowerManagerV1.event.SetMode) void { 373 | const self = @fieldParentPtr(Self, "set_mode", listener); 374 | const mode = event.mode == .on; 375 | const state = if (mode) "Enabling" else "Disabling"; 376 | log.debug("Signal: wlr_output_power_manager_v1_set_mode", .{}); 377 | log.debug( 378 | "{s} output {s}", 379 | .{ 380 | state, 381 | event.output.name, 382 | }, 383 | ); 384 | 385 | event.output.enable(mode); 386 | event.output.commit() catch { 387 | log.err("Output commit failed: {s}", .{event.output.name}); 388 | return; 389 | }; 390 | 391 | // If the commit didn't fail then go ahead and edit the output_layout :) 392 | switch (event.mode) { 393 | .on => { 394 | if (self.output_layout.wlr_output_layout.get(event.output)) |_| { 395 | log.debug("Output is already in output_layout", .{}); 396 | } else { 397 | log.debug("Adding output to output_layout", .{}); 398 | self.output_layout.wlr_output_layout.addAuto(event.output); 399 | } 400 | }, 401 | .off => { 402 | if (self.output_layout.wlr_output_layout.get(event.output)) |_| { 403 | self.output_layout.wlr_output_layout.remove(event.output); 404 | } else { 405 | log.debug("Output is already absent from the output_layout, not removing", .{}); 406 | } 407 | }, 408 | _ => {}, 409 | } 410 | } 411 | 412 | fn logInfo() void { 413 | const uname_info = std.os.uname(); 414 | log.info("System name: {s}", .{uname_info.sysname}); 415 | log.info("Host name: {s}", .{uname_info.nodename}); 416 | log.info("Release Info: {s}", .{uname_info.release}); 417 | log.info("Version Info: {s}", .{uname_info.version}); 418 | } 419 | -------------------------------------------------------------------------------- /next/control/border.zig: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | // 3 | // next/control/border.zig 4 | // 5 | // Created by: Aakash Sen Sharma, May 2022 6 | // Copyright: (C) 2022, Aakash Sen Sharma & Contributors 7 | 8 | const std = @import("std"); 9 | const server = &@import("../next.zig").server; 10 | const allocator = @import("../utils/allocator.zig").allocator; 11 | 12 | const wlr = @import("wlroots"); 13 | 14 | const Error = @import("command.zig").Error; 15 | 16 | pub fn setWidth( 17 | args: []const [:0]const u8, 18 | out: *?[]const u8, 19 | ) Error!void { 20 | if (args.len < 2) return Error.NotEnoughArguments; 21 | if (args.len > 2) return Error.TooManyArguments; 22 | 23 | server.config.border_width = std.fmt.parseInt(u8, args[1], 10) catch { 24 | out.* = try std.fmt.allocPrint(allocator, "Failed to parse border-width from range 0-255\n", .{}); 25 | return; 26 | }; 27 | 28 | for (server.mapped_windows.items) |window| { 29 | switch (window.backend) { 30 | .xdg_toplevel => |*xdg_toplevel| { 31 | var geom: wlr.Box = undefined; 32 | xdg_toplevel.xdg_surface.getGeometry(&geom); 33 | _ = xdg_toplevel.resize(geom.x, geom.y, geom.width, geom.height); 34 | }, 35 | } 36 | } 37 | } 38 | 39 | pub fn setColor( 40 | args: []const [:0]const u8, 41 | out: *?[]const u8, 42 | ) Error!void { 43 | if (args.len < 2) return Error.NotEnoughArguments; 44 | if (args.len > 2) return Error.TooManyArguments; 45 | 46 | server.config.border_color = parseRgba(args[1]) catch |err| { 47 | out.* = try std.fmt.allocPrint(allocator, "Failed to parse border-color: {s}\n", .{@errorName(err)}); 48 | return; 49 | }; 50 | 51 | for (server.mapped_windows.items) |window| { 52 | switch (window.backend) { 53 | .xdg_toplevel => |xdg_toplevel| { 54 | for (xdg_toplevel.borders) |border| { 55 | border.setColor(&server.config.border_color); 56 | } 57 | }, 58 | } 59 | } 60 | } 61 | 62 | pub fn parseRgba(_: []const u8) ![4]f32 { 63 | //TODO: Finish this eventually. 64 | return [_]f32{ 1, 0, 0, 1 }; 65 | } 66 | -------------------------------------------------------------------------------- /next/control/command.zig: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | // 3 | // next/control/command.zig 4 | // 5 | // Created by: Aakash Sen Sharma, May 2022 6 | // Copyright: (C) 2022, Aakash Sen Sharma & Contributors 7 | 8 | const std = @import("std"); 9 | const assert = std.debug.assert; 10 | 11 | const border = @import("border.zig"); 12 | const csd = @import("csd.zig"); 13 | const cursor = @import("cursor.zig"); 14 | const exit = @import("exit.zig"); 15 | const inputs = @import("inputs.zig"); 16 | const outputs = @import("outputs.zig"); 17 | const spawn = @import("spawn.zig"); 18 | 19 | pub const Error = error{ 20 | NoCommand, 21 | NotEnoughArguments, 22 | OutOfMemory, 23 | TooManyArguments, 24 | UnknownCommand, 25 | UnknownOption, 26 | OSError, 27 | }; 28 | 29 | // zig fmt: off 30 | const commands = std.ComptimeStringMap( 31 | *const fn ([]const [:0]const u8, *?[]const u8) Error!void, 32 | .{ 33 | .{ "spawn", spawn.spawnCmd }, 34 | .{ "wallpaper", outputs.setWallpaper }, 35 | //TODO: We should change *-* style commands to subcommand based systems. Eg: `border width set 3` / `border color set focused ...` 36 | .{ "border-width", border.setWidth }, 37 | // TODO: This is just a catch all. We will create border-focused, border-unfocused, etc soon. 38 | .{ "border-color", border.setColor }, 39 | .{ "csd", csd.csdToggle }, 40 | .{ "exit", exit.exitRiver }, 41 | //TODO: We should change *-* style commands to subcommand based systems. Eg: `list inputs` / `list outputs` 42 | .{ "list-inputs", inputs.listInputs }, 43 | .{ "list-outputs", outputs.listOutputs }, 44 | .{ "set-repeat-rate", inputs.setRepeat }, 45 | .{ "warp-cursor", cursor.warpCursor }, 46 | .{ "hide-cursor", cursor.hideCursor }, 47 | .{ "sloppy-focus", cursor.setSloppyFocus }, 48 | }, 49 | ); 50 | // zig fmt: on 51 | 52 | pub fn run( 53 | args: []const [:0]const u8, 54 | out: *?[]const u8, 55 | ) Error!void { 56 | assert(out.* == null); 57 | if (args.len == 0) return Error.NoCommand; 58 | const func = commands.get(args[0]) orelse return Error.UnknownCommand; 59 | try func(args, out); 60 | } 61 | 62 | pub fn errToMsg(err: Error) [:0]const u8 { 63 | return switch (err) { 64 | Error.NoCommand => "No command provided\n", 65 | Error.NotEnoughArguments => "Not enough arguments provided\n", 66 | Error.OutOfMemory => "Out of memory\n", 67 | Error.TooManyArguments => "Too many arguments\n", 68 | Error.UnknownOption => "Unknown option\n", 69 | Error.UnknownCommand => "Unknown command\n", 70 | Error.OSError => "OS error\n", 71 | }; 72 | } 73 | -------------------------------------------------------------------------------- /next/control/csd.zig: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | // 3 | // next/control/csd.zig 4 | // 5 | // Created by: Aakash Sen Sharma, May 2022 6 | // Copyright: (C) 2022, Aakash Sen Sharma & Contributors 7 | 8 | const allocator = @import("../utils/allocator.zig").allocator; 9 | const assert = std.debug.assert; 10 | const server = &@import("../next.zig").server; 11 | const std = @import("std"); 12 | 13 | const Error = @import("command.zig").Error; 14 | const Window = @import("../desktop/Window.zig"); 15 | 16 | const FilterKind = enum { 17 | @"app-id", 18 | title, 19 | }; 20 | 21 | const FilterState = enum { 22 | add, 23 | remove, 24 | }; 25 | 26 | pub fn csdToggle( 27 | args: []const [:0]const u8, 28 | _: *?[]const u8, 29 | ) Error!void { 30 | if (args.len > 4) return Error.TooManyArguments; 31 | if (args.len < 4) return Error.NotEnoughArguments; 32 | 33 | const state = std.meta.stringToEnum(FilterState, args[1]) orelse return Error.UnknownOption; 34 | const kind = std.meta.stringToEnum(FilterKind, args[2]) orelse return Error.UnknownOption; 35 | const map = switch (kind) { 36 | .@"app-id" => &server.config.csd_app_ids, 37 | .title => &server.config.csd_titles, 38 | }; 39 | const key = args[3]; 40 | switch (state) { 41 | .add => { 42 | const gop = try map.getOrPut(allocator, key); 43 | if (gop.found_existing) return; 44 | errdefer assert(map.remove(key)); 45 | gop.key_ptr.* = try allocator.dupe(u8, key); 46 | }, 47 | .remove => { 48 | if (map.fetchRemove(key)) |kv| allocator.free(kv.key); 49 | }, 50 | } 51 | 52 | var decoration_it = server.decoration_manager.decorations.first; 53 | while (decoration_it) |decoration_node| : (decoration_it = decoration_node.next) { 54 | const xdg_toplevel_decoration = decoration_node.data.xdg_toplevel_decoration; 55 | const window = @as(*Window, @ptrFromInt(xdg_toplevel_decoration.surface.data)); 56 | 57 | const window_data: []const u8 = switch (kind) { 58 | .@"app-id" => std.mem.sliceTo(window.getAppId(), 0), 59 | .title => std.mem.sliceTo(window.getTitle(), 0), 60 | }; 61 | 62 | if (std.mem.eql(u8, key, window_data)) { 63 | switch (state) { 64 | .add => { 65 | _ = xdg_toplevel_decoration.setMode(.client_side); 66 | }, 67 | .remove => { 68 | _ = xdg_toplevel_decoration.setMode(.server_side); 69 | }, 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /next/control/cursor.zig: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | // 3 | // next/control/cursor.zig 4 | // 5 | // Created by: Aakash Sen Sharma, May 2022 6 | // Copyright: (C) 2022, Aakash Sen Sharma & Contributors 7 | 8 | const std = @import("std"); 9 | const server = &@import("../next.zig").server; 10 | const allocator = @import("../utils/allocator.zig").allocator; 11 | 12 | const Error = @import("command.zig").Error; 13 | const CursorWarpMode = @import("../Config.zig").CursorWarpMode; 14 | 15 | pub fn hideCursor( 16 | args: []const [:0]const u8, 17 | out: *?[]const u8, 18 | ) Error!void { 19 | if (args.len > 3) return Error.TooManyArguments; 20 | if (args.len < 3) return Error.NotEnoughArguments; 21 | 22 | if (std.mem.eql(u8, "when-typing", args[1])) { 23 | if (std.mem.eql(u8, "true", args[2])) { 24 | server.config.cursor_hide_when_typing = true; 25 | } else if (std.mem.eql(u8, "false", args[2])) { 26 | server.config.cursor_hide_when_typing = false; 27 | } else { 28 | out.* = try std.fmt.allocPrint(allocator, "Invalid cursor state provided.", .{}); 29 | } 30 | } else { 31 | return Error.UnknownOption; 32 | } 33 | server.input_manager.hideCursor(); 34 | } 35 | 36 | pub fn warpCursor( 37 | args: []const [:0]const u8, 38 | out: *?[]const u8, 39 | ) !void { 40 | if (args.len > 2) return Error.TooManyArguments; 41 | if (args.len < 2) return Error.NotEnoughArguments; 42 | 43 | const state = std.meta.stringToEnum(CursorWarpMode, args[1]); 44 | if (state) |s| { 45 | server.config.warp_cursor = s; 46 | return; 47 | } else { 48 | out.* = try std.fmt.allocPrint(allocator, "Failed to parse provided state.\n", .{}); 49 | } 50 | } 51 | 52 | pub fn setSloppyFocus( 53 | args: []const [:0]const u8, 54 | out: *?[]const u8, 55 | ) !void { 56 | if (args.len < 2) return Error.NotEnoughArguments; 57 | if (args.len > 2) return Error.TooManyArguments; 58 | 59 | if (std.mem.eql(u8, "true", args[1])) { 60 | server.config.focus_is_sloppy = true; 61 | } else if (std.mem.eql(u8, "false", args[1])) { 62 | server.config.focus_is_sloppy = false; 63 | } else if (std.mem.eql(u8, "toggle", args[1])) { 64 | server.config.focus_is_sloppy = !server.config.focus_is_sloppy; 65 | } else { 66 | out.* = try std.fmt.allocPrint(allocator, "Failed to parse focus state: {s}\n", .{args[1]}); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /next/control/exit.zig: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | // 3 | // next/control/exit.zig 4 | // 5 | // Created by: Aakash Sen Sharma, May 2022 6 | // Copyright: (C) 2022, Aakash Sen Sharma & Contributors 7 | 8 | const server = &@import("../next.zig").server; 9 | 10 | const Error = @import("command.zig").Error; 11 | 12 | pub fn exitRiver( 13 | args: []const [:0]const u8, 14 | _: *?[]const u8, 15 | ) Error!void { 16 | if (args.len > 1) return Error.TooManyArguments; 17 | server.wl_server.terminate(); 18 | } 19 | -------------------------------------------------------------------------------- /next/control/inputs.zig: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | // 3 | // next/control/inputs.zig 4 | // 5 | // Created by: Aakash Sen Sharma, May 2022 6 | // Copyright: (C) 2022, Aakash Sen Sharma & Contributors 7 | 8 | const std = @import("std"); 9 | const server = &@import("../next.zig").server; 10 | const allocator = @import("../utils/allocator.zig").allocator; 11 | 12 | const Error = @import("command.zig").Error; 13 | 14 | pub fn listInputs( 15 | args: []const [:0]const u8, 16 | out: *?[]const u8, 17 | ) Error!void { 18 | if (args.len > 1) return Error.TooManyArguments; 19 | 20 | var output = std.ArrayList(u8).init(allocator); 21 | const writer = output.writer(); 22 | 23 | for (server.keyboards.items) |device| { 24 | try writer.print("Keyboard: {s}\n", .{ 25 | device.wlr_input_device.name, 26 | }); 27 | } 28 | for (server.cursors.items) |device| { 29 | try writer.print("Pointer: {s}\n", .{ 30 | device.wlr_input_device.name, 31 | }); 32 | } 33 | out.* = try output.toOwnedSlice(); 34 | } 35 | 36 | pub fn setRepeat( 37 | args: []const [:0]const u8, 38 | out: *?[]const u8, 39 | ) !void { 40 | if (args.len < 3) return Error.NotEnoughArguments; 41 | if (args.len > 3) return Error.TooManyArguments; 42 | 43 | const rate = std.fmt.parseInt(i32, args[1], 10) catch { 44 | out.* = try std.fmt.allocPrint(allocator, "Failed to parse repeat-rate\n", .{}); 45 | return; 46 | }; 47 | const delay = std.fmt.parseInt(i32, args[2], 10) catch { 48 | out.* = try std.fmt.allocPrint(allocator, "Failed to parse repeat-delay\n", .{}); 49 | return; 50 | }; 51 | 52 | server.config.repeat_rate = rate; 53 | server.config.repeat_delay = delay; 54 | 55 | for (server.keyboards.items) |keyboard| { 56 | keyboard.wlr_input_device.toKeyboard().setRepeatInfo(rate, delay); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /next/control/outputs.zig: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | // 3 | // next/control/outputs.zig 4 | // 5 | // Created by: Aakash Sen Sharma, May 2022 6 | // Copyright: (C) 2022, Aakash Sen Sharma & Contributors 7 | 8 | const std = @import("std"); 9 | const server = &@import("../next.zig").server; 10 | const allocator = @import("../utils/allocator.zig").allocator; 11 | const log = std.log.scoped(.OutputControl); 12 | 13 | const Error = @import("command.zig").Error; 14 | 15 | const WallpaperKind = enum { 16 | unset, 17 | set, 18 | }; 19 | const WallpaperMode = @import("../desktop/Output.zig").WallpaperMode; 20 | 21 | pub fn listOutputs( 22 | args: []const [:0]const u8, 23 | out: *?[]const u8, 24 | ) Error!void { 25 | if (args.len > 1) return Error.TooManyArguments; 26 | 27 | var data = std.ArrayList(u8).init(allocator); 28 | const writer = data.writer(); 29 | 30 | for (server.outputs.items) |output| { 31 | const geometry = output.getGeometry(); 32 | //TODO: See what is dpmsStatus 33 | //TODO: Add model, serial, scale, transform, focused_status, vrr, refresh rate 34 | try writer.print("{s}\n\t{d}x{d} at {d},{d}\n\tScale: {d}\n\tName: {s}\n\tDescription: {s}\n\tMake: {s}\n\tEnabled: {d}\n\tVRR: {d}\n", .{ 35 | output.wlr_output.name, 36 | geometry.width, 37 | geometry.height, 38 | geometry.x, 39 | geometry.y, 40 | output.wlr_output.scale, 41 | output.getName(), 42 | output.getDescription(), 43 | output.getMake(), 44 | @intFromBool(output.wlr_output.enabled), 45 | @intFromBool(output.wlr_output.adaptive_sync_status == .enabled), 46 | }); 47 | } 48 | out.* = try data.toOwnedSlice(); 49 | } 50 | 51 | pub fn setWallpaper( 52 | args: []const [:0]const u8, 53 | out: *?[]const u8, 54 | ) Error!void { 55 | const state = std.meta.stringToEnum(WallpaperKind, args[1]) orelse return Error.UnknownOption; 56 | 57 | if (state == .unset) { 58 | if (args.len > 3) return error.TooManyArguments; 59 | if (args.len < 3) return error.NotEnoughArguments; 60 | } else { 61 | if (args.len > 5) return error.TooManyArguments; 62 | if (args.len < 5) return error.NotEnoughArguments; 63 | } 64 | 65 | for (server.outputs.items) |output| { 66 | if (std.mem.eql(u8, output.getName(), std.mem.sliceTo(args[2], 0))) { 67 | switch (state) { 68 | .set => { 69 | output.has_wallpaper = true; 70 | output.wallpaper_mode = std.meta.stringToEnum(WallpaperMode, args[3]) orelse return Error.UnknownOption; 71 | output.wallpaper_path = allocator.dupe(u8, std.mem.sliceTo(args[4], 0)) catch return Error.OutOfMemory; 72 | output.init_wallpaper_rendering() catch |err| { 73 | log.err("Wallpaper setting failed: {s}", .{@errorName(err)}); 74 | out.* = try std.fmt.allocPrint(allocator, "Wallpaper render failed: {s}\n", .{@errorName(err)}); 75 | }; 76 | }, 77 | 78 | .unset => { 79 | output.deinit_wallpaper(); 80 | output.has_wallpaper = false; 81 | }, 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /next/control/spawn.zig: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | // 3 | // next/control/spawn.zig 4 | // 5 | // Created by: Aakash Sen Sharma, July 2023 6 | // Copyright: (C) 2023, Aakash Sen Sharma & Contributors 7 | 8 | const std = @import("std"); 9 | const os = std.os; 10 | const allocator = @import("../utils/allocator.zig").allocator; 11 | const log = std.log.scoped(.SpawnControl); 12 | const c = @import("../utils/c.zig"); 13 | 14 | const Error = @import("command.zig").Error; 15 | 16 | pub fn spawnCmd( 17 | args: []const [:0]const u8, 18 | out: *?[]const u8, 19 | ) Error!void { 20 | if (args.len < 2) return Error.NotEnoughArguments; 21 | if (args.len > 2) return Error.TooManyArguments; 22 | 23 | log.debug("Attempting to run shell command: {s}", .{args[1]}); 24 | 25 | const cmd = [_:null]?[*:0]const u8{ "/bin/sh", "-c", args[1], null }; 26 | const pid = os.fork() catch |err| { 27 | log.err("Fork failed: {s}", .{@errorName(err)}); 28 | 29 | out.* = try std.fmt.allocPrint(allocator, "fork failed!", .{}); 30 | return Error.OSError; 31 | }; 32 | 33 | if (pid == 0) { 34 | if (c.setsid() < 0) unreachable; 35 | if (os.system.sigprocmask(os.SIG.SETMASK, &os.empty_sigset, null) < 0) unreachable; 36 | const sig_dfl = os.Sigaction{ 37 | .handler = .{ .handler = os.SIG.DFL }, 38 | .mask = os.empty_sigset, 39 | .flags = 0, 40 | }; 41 | os.sigaction(os.SIG.PIPE, &sig_dfl, null) catch |err| { 42 | log.err("Sigaction failed: {s}", .{@errorName(err)}); 43 | return Error.OSError; 44 | }; 45 | 46 | const pid2 = os.fork() catch c._exit(1); 47 | if (pid2 == 0) os.execveZ("/bin/sh", &cmd, std.c.environ) catch c._exit(1); 48 | 49 | c._exit(0); 50 | } 51 | 52 | const exit_code = os.waitpid(pid, 0); 53 | if (!os.W.IFEXITED(exit_code.status) or 54 | (os.W.IFEXITED(exit_code.status) and os.W.EXITSTATUS(exit_code.status) != 0)) 55 | { 56 | out.* = try std.fmt.allocPrint(allocator, "Fork failed", .{}); 57 | return Error.OSError; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /next/desktop/Decoration.zig: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | // 3 | // next/desktop/Decoration.zig 4 | // 5 | // Created by: Aakash Sen Sharma, May 2022 6 | // Copyright: (C) 2022, Aakash Sen Sharma & Contributors 7 | 8 | const Self = @This(); 9 | 10 | const allocator = @import("../utils/allocator.zig").allocator; 11 | const log = std.log.scoped(.Decoration); 12 | const server = &@import("../next.zig").server; 13 | const std = @import("std"); 14 | 15 | const wl = @import("wayland").server.wl; 16 | const wlr = @import("wlroots"); 17 | 18 | const Window = @import("Window.zig"); 19 | 20 | xdg_toplevel_decoration: *wlr.XdgToplevelDecorationV1, 21 | 22 | destroy: wl.Listener(*wlr.XdgToplevelDecorationV1) = wl.Listener(*wlr.XdgToplevelDecorationV1).init(handleDestroy), 23 | request_mode: wl.Listener(*wlr.XdgToplevelDecorationV1) = wl.Listener(*wlr.XdgToplevelDecorationV1).init(requestMode), 24 | 25 | pub fn init(self: *Self, xdg_toplevel_decoration: *wlr.XdgToplevelDecorationV1) void { 26 | self.* = .{ .xdg_toplevel_decoration = xdg_toplevel_decoration }; 27 | log.debug("Intializing a toplevel decoration", .{}); 28 | 29 | xdg_toplevel_decoration.events.destroy.add(&self.destroy); 30 | xdg_toplevel_decoration.events.request_mode.add(&self.request_mode); 31 | 32 | requestMode(&self.request_mode, self.xdg_toplevel_decoration); 33 | } 34 | 35 | fn handleDestroy(listener: *wl.Listener(*wlr.XdgToplevelDecorationV1), _: *wlr.XdgToplevelDecorationV1) void { 36 | const self = @fieldParentPtr(Self, "destroy", listener); 37 | log.debug("Signal: wlr_xdg_toplevel_decoration_destroy", .{}); 38 | 39 | self.destroy.link.remove(); 40 | self.request_mode.link.remove(); 41 | 42 | const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self); 43 | server.decoration_manager.decorations.remove(node); 44 | allocator.destroy(node); 45 | } 46 | 47 | fn requestMode(listener: *wl.Listener(*wlr.XdgToplevelDecorationV1), _: *wlr.XdgToplevelDecorationV1) void { 48 | const self = @fieldParentPtr(Self, "request_mode", listener); 49 | log.debug("Signal: wlr_xdg_toplevel_decoration_request_mode", .{}); 50 | 51 | const window = @as(*Window, @ptrFromInt(self.xdg_toplevel_decoration.surface.data)); 52 | 53 | if (server.config.csdAllowed(window)) { 54 | _ = self.xdg_toplevel_decoration.setMode(.client_side); 55 | } else { 56 | _ = self.xdg_toplevel_decoration.setMode(.server_side); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /next/desktop/DecorationManager.zig: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | // 3 | // next/desktop/DecorationManager.zig 4 | // 5 | // Created by: Aakash Sen Sharma, May 2022 6 | // Copyright: (C) 2022, Aakash Sen Sharma & Contributors 7 | 8 | const Self = @This(); 9 | 10 | const allocator = @import("../utils/allocator.zig").allocator; 11 | const log = std.log.scoped(.DecorationManager); 12 | const server = &@import("../next.zig").server; 13 | const std = @import("std"); 14 | 15 | const wl = @import("wayland").server.wl; 16 | const wlr = @import("wlroots"); 17 | 18 | const Decoration = @import("Decoration.zig"); 19 | 20 | decorations: std.TailQueue(Decoration) = .{}, 21 | 22 | xdg_decoration_manager: *wlr.XdgDecorationManagerV1, 23 | 24 | new_toplevel_decoration: wl.Listener(*wlr.XdgToplevelDecorationV1) = wl.Listener(*wlr.XdgToplevelDecorationV1).init(newToplevelDecoration), 25 | 26 | pub fn init(self: *Self) !void { 27 | self.* = .{ 28 | .xdg_decoration_manager = try wlr.XdgDecorationManagerV1.create(server.wl_server), 29 | }; 30 | log.debug("Initializing DecorationManager", .{}); 31 | self.xdg_decoration_manager.events.new_toplevel_decoration.add(&self.new_toplevel_decoration); 32 | } 33 | 34 | fn newToplevelDecoration(listener: *wl.Listener(*wlr.XdgToplevelDecorationV1), xdg_toplevel_decoration: *wlr.XdgToplevelDecorationV1) void { 35 | log.debug("Signal: wlr_xdg_decoration_manager_new_toplevel_decoration", .{}); 36 | 37 | const self = @fieldParentPtr(Self, "new_toplevel_decoration", listener); 38 | const decoration = allocator.create(std.TailQueue(Decoration).Node) catch { 39 | xdg_toplevel_decoration.resource.postNoMemory(); 40 | return; 41 | }; 42 | decoration.data.init(xdg_toplevel_decoration); 43 | self.decorations.append(decoration); 44 | } 45 | -------------------------------------------------------------------------------- /next/desktop/Output.zig: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | // 3 | // next/desktop/Output.zig 4 | // 5 | // Created by: Aakash Sen Sharma, May 2022 6 | // Copyright: (C) 2022, Aakash Sen Sharma & Contributors 7 | 8 | const Self = @This(); 9 | 10 | const allocator = @import("../utils/allocator.zig").allocator; 11 | const log = std.log.scoped(.Output); 12 | const os = std.os; 13 | const server = &@import("../next.zig").server; 14 | const c = @import("../utils/c.zig"); 15 | const std = @import("std"); 16 | 17 | const wl = @import("wayland").server.wl; 18 | const wlr = @import("wlroots"); 19 | 20 | const Server = @import("../Server.zig"); 21 | const Wallpaper = @import("Wallpaper.zig"); 22 | 23 | pub const WallpaperMode = enum { 24 | fit, 25 | stretch, 26 | }; 27 | 28 | server: *Server, 29 | 30 | wlr_output: *wlr.Output, 31 | 32 | background_image_surface: ?*c.cairo_surface_t = null, 33 | 34 | has_wallpaper: bool = false, 35 | wallpaper: ?*Wallpaper = null, 36 | wallpaper_path: ?[]const u8 = null, 37 | wallpaper_mode: ?WallpaperMode = null, 38 | 39 | frame: wl.Listener(*wlr.Output) = wl.Listener(*wlr.Output).init(handleFrame), 40 | destroy: wl.Listener(*wlr.Output) = wl.Listener(*wlr.Output).init(handleDestroy), 41 | 42 | // This callback prepares the output object to accept listeners. 43 | pub fn init(self: *Self, wlr_output: *wlr.Output) !void { 44 | log.debug("Initializing output device", .{}); 45 | 46 | // Configure the output detected by the backend to use our allocator and renderer. 47 | if (!wlr_output.initRender(server.wlr_allocator, server.wlr_renderer)) { 48 | return error.OutputInitRenderFailed; 49 | } 50 | 51 | // Some backends don't have modes. DRM+KMS does, and we need to set a mode before using the target. 52 | if (wlr_output.preferredMode()) |preferred_mode| { 53 | wlr_output.setMode(preferred_mode); 54 | wlr_output.enable(true); 55 | // If preferred_mode setting failed then we iterate over all possible modes and attempt to set one. 56 | // If we set one succesfully then we break the loop! 57 | wlr_output.commit() catch { 58 | var iterator = wlr_output.modes.iterator(.forward); 59 | while (iterator.next()) |mode| { 60 | if (mode == preferred_mode) continue; 61 | wlr_output.setMode(mode); 62 | wlr_output.commit() catch continue; 63 | break; 64 | } 65 | }; 66 | } 67 | 68 | //TODO: self.wlr_output.enableAdaptiveSync(true); if config states it. 69 | 70 | self.* = .{ 71 | .server = server, 72 | .wlr_output = wlr_output, 73 | }; 74 | 75 | self.wlr_output.data = @intFromPtr(&self); 76 | 77 | // Add a callback for the frame event from the output struct. 78 | self.wlr_output.events.frame.add(&self.frame); 79 | 80 | // Add the new output to the output_layout for automatic layout management by wlroots. 81 | self.server.output_layout.wlr_output_layout.addAuto(self.wlr_output); 82 | self.server.outputs.append(allocator, self) catch { 83 | return error.OOM; 84 | }; 85 | 86 | const output_title = std.fmt.allocPrintZ(allocator, "nextwm - {s}", .{self.wlr_output.name}) catch |e| { 87 | log.err("Failed to allocate output name, skipping setting custom output name: {s}", .{@errorName(e)}); 88 | return; 89 | }; 90 | defer allocator.free(output_title); 91 | 92 | if (self.wlr_output.isWl()) { 93 | self.wlr_output.wlSetTitle(output_title); 94 | } else if (wlr.config.has_x11_backend and self.wlr_output.isX11()) { 95 | self.wlr_output.x11SetTitle(output_title); 96 | } 97 | 98 | // If focused_output is null, we become the new focused_output :) 99 | if (self.server.seat.focused_output) |_| {} else { 100 | self.server.seat.focusOutput(self); 101 | } 102 | } 103 | 104 | fn configure_node_decoration(self: *Self, node: *wlr.SceneNode) void { 105 | if (!node.enabled) { 106 | return; 107 | } 108 | 109 | if (node.type == .buffer) { 110 | const scene_buffer = wlr.SceneBuffer.fromNode(node); 111 | const scene_surface = wlr.SceneSurface.fromBuffer(scene_buffer) orelse return; 112 | 113 | var xdg_surface: *wlr.XdgSurface = undefined; 114 | if (wlr.Surface.isXdgSurface(scene_surface.surface)) { 115 | xdg_surface = wlr.XdgSurface.fromWlrSurface(scene_surface.surface) orelse return; 116 | } 117 | 118 | if (xdg_surface.role == .toplevel) { 119 | scene_buffer.setOpacity(self.server.config.toplevel_opacity); 120 | 121 | if (!wlr.Surface.isSubsurface(xdg_surface.surface)) { 122 | var shadow_data = wlr.ShadowData.getDefault(); 123 | shadow_data.enabled = true; 124 | shadow_data.color.* = self.server.config.toplevel_box_shadow_color; 125 | 126 | ////TODO: Configurable blur_sigma for shadow ;) 127 | scene_buffer.setShadowData(shadow_data); 128 | scene_buffer.setCornerRadius(self.server.config.toplevel_corner_radius); 129 | } 130 | } 131 | } else if (node.type == .tree) { 132 | const tree = node.parent orelse return; 133 | var it = tree.children.safeIterator(.forward); 134 | while (it.next()) |scene_node| { 135 | self.configure_node_decoration(scene_node); 136 | } 137 | } 138 | } 139 | 140 | pub fn init_wallpaper_rendering(self: *Self) !void { 141 | // We do some cleanup first. 142 | const wallpaper_path = allocator.dupe(u8, self.wallpaper_path.?) catch return error.OOM; 143 | 144 | self.deinit_wallpaper(); 145 | self.wallpaper_path = wallpaper_path; 146 | 147 | const image_surface = try Wallpaper.cairo_load_image(self.wallpaper_path.?); 148 | errdefer c.cairo_surface_destroy(image_surface); 149 | 150 | const output_geometry = self.getGeometry(); 151 | 152 | self.background_image_surface = try Wallpaper.cairo_surface_transform_apply(image_surface, self.wallpaper_mode.?, output_geometry.width, output_geometry.height); 153 | 154 | const data = c.cairo_image_surface_get_data(self.background_image_surface); 155 | const stride = c.cairo_image_surface_get_stride(self.background_image_surface); 156 | 157 | self.wallpaper = allocator.create(Wallpaper) catch { 158 | log.debug("Failed to allocate memory", .{}); 159 | log.debug("Skipping wallpaper setting", .{}); 160 | return error.OOM; 161 | }; 162 | 163 | try self.wallpaper.?.cairo_buffer_create(@as(c_int, @intCast(output_geometry.width)), @as(c_int, @intCast(output_geometry.height)), @as(usize, @intCast(stride)), data); 164 | errdefer allocator.destroy(self.wallpaper.?); 165 | 166 | self.wallpaper.?.scene_buffer = try self.server.layer_bg.createSceneBuffer(&self.wallpaper.?.base_buffer); 167 | self.wallpaper.?.scene_buffer.?.node.setPosition(@as(c_int, @intCast(output_geometry.x)), @as(c_int, @intCast(output_geometry.y))); 168 | } 169 | 170 | pub fn deinit_wallpaper(self: *Self) void { 171 | if (self.wallpaper) |wallpaper| { 172 | if (wallpaper.scene_buffer) |scene_buffer| { 173 | scene_buffer.node.destroy(); 174 | } 175 | 176 | if (!wallpaper.base_buffer.dropped) { 177 | log.warn("Wallpaper deinit called before wlr_buffer_destroy", .{}); 178 | wallpaper.base_buffer.drop(); 179 | } 180 | 181 | allocator.destroy(wallpaper); 182 | if (self.wallpaper_path) |path| { 183 | allocator.free(path); 184 | self.wallpaper_path = null; 185 | } 186 | self.wallpaper = null; 187 | } 188 | 189 | if (self.background_image_surface) |surface| { 190 | c.cairo_surface_destroy(surface); 191 | self.background_image_surface = null; 192 | } 193 | } 194 | 195 | // This callback is called everytime an output is ready to display a frame. 196 | fn handleFrame(listener: *wl.Listener(*wlr.Output), _: *wlr.Output) void { // Get the parent struct, Output. 197 | const self = @fieldParentPtr(Self, "frame", listener); 198 | log.debug("Signal: wlr_output_frame", .{}); 199 | 200 | // Get the scene output with respect to the wlr.Output object that's being passed. 201 | const scene_output = self.server.wlr_scene.getSceneOutput(self.wlr_output).?; 202 | 203 | // Pass decoration data to the scene nodes. 204 | self.configure_node_decoration(&scene_output.scene.tree.node); 205 | 206 | // Commit the output to the scene. 207 | _ = scene_output.commit(); 208 | 209 | // Get current time as the FrameDone event requires it. 210 | var now: os.timespec = undefined; 211 | os.clock_gettime(os.CLOCK.MONOTONIC, &now) catch { 212 | log.err("CLOCK_MONOTONIC not supported", .{}); 213 | return; 214 | }; 215 | 216 | // Send the FrameDone event. 217 | scene_output.sendFrameDone(&now); 218 | } 219 | 220 | // This callback is called everytime an output is unplugged. 221 | fn handleDestroy(listener: *wl.Listener(*wlr.Output), _: *wlr.Output) void { 222 | // Get the parent struct, Output. 223 | const self = @fieldParentPtr(Self, "destroy", listener); 224 | log.debug("Signal: wlr_output_destroy", .{}); 225 | 226 | self.deinit(); 227 | } 228 | 229 | pub fn deinit(self: *Self) void { 230 | log.err("Deinitializing output: {s}", .{self.getName()}); 231 | 232 | //TODO: Check if all such cases are handled in server cleanup and then in the handleDestroy. 233 | // Free all wallpaper surfaces and cairo objects. 234 | self.deinit_wallpaper(); 235 | 236 | // Remove the output from the global compositor output layout. 237 | self.server.output_layout.wlr_output_layout.remove(self.wlr_output); 238 | 239 | // Find index of self from outputs and remove it. 240 | if (std.mem.indexOfScalar(*Self, self.server.outputs.items, self)) |i| { 241 | log.err("Removing output from server output array", .{}); 242 | 243 | allocator.destroy(self.server.outputs.swapRemove(i)); 244 | } 245 | 246 | //TODO: Move closed monitors clients to focused one. 247 | if (self.server.seat.focused_output) |focused_output| { 248 | if (focused_output.wlr_output == self.wlr_output) { 249 | // Unset focused output. It will be reset in output_layout_change as destroying a monitor does emit that event. 250 | self.server.seat.focused_output = null; 251 | } 252 | } 253 | } 254 | 255 | // Helper to get X, Y coordinates and the width and height of the output. 256 | pub fn getGeometry(self: *Self) struct { width: u64, height: u64, x: u64, y: u64 } { 257 | var width: c_int = undefined; 258 | var height: c_int = undefined; 259 | var x: f64 = undefined; 260 | var y: f64 = undefined; 261 | 262 | self.server.output_layout.wlr_output_layout.outputCoords(self.wlr_output, &x, &y); 263 | self.wlr_output.effectiveResolution(&width, &height); 264 | 265 | return .{ 266 | .width = @as(u64, @intCast(width)), 267 | .height = @as(u64, @intCast(height)), 268 | .x = @as(u64, @intFromFloat(x)), 269 | .y = @as(u64, @intFromFloat(y)), 270 | }; 271 | } 272 | 273 | pub fn getDescription(self: *Self) [*:0]const u8 { 274 | if (self.wlr_output.description) |description| { 275 | return description; 276 | } else { 277 | return ""; 278 | } 279 | } 280 | 281 | pub fn getMake(self: *Self) [*:0]const u8 { 282 | if (self.wlr_output.make) |make| { 283 | return make; 284 | } else { 285 | return ""; 286 | } 287 | } 288 | 289 | pub fn getName(self: *Self) []const u8 { 290 | const name = self.wlr_output.name; 291 | return std.mem.sliceTo(name, 0); 292 | } 293 | -------------------------------------------------------------------------------- /next/desktop/OutputLayout.zig: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | // 3 | // next/desktop/OutputLayout.zig 4 | // 5 | // Created by: Aakash Sen Sharma, January 2023 6 | // Copyright: (C) 2023, Aakash Sen Sharma & Contributors 7 | 8 | const Self = @This(); 9 | 10 | const log = std.log.scoped(.OutputLayout); 11 | const server = &@import("../next.zig").server; 12 | const Server = @import("../Server.zig"); 13 | const std = @import("std"); 14 | 15 | const wl = @import("wayland").server.wl; 16 | const wlr = @import("wlroots"); 17 | 18 | server: *Server, 19 | 20 | wlr_output_layout: *wlr.OutputLayout, 21 | 22 | layout_change: wl.Listener(*wlr.OutputLayout) = wl.Listener(*wlr.OutputLayout).init(layoutChange), 23 | 24 | pub fn init(self: *Self) !void { 25 | self.* = .{ 26 | .server = server, 27 | .wlr_output_layout = try wlr.OutputLayout.create(), 28 | }; 29 | 30 | self.wlr_output_layout.events.change.add(&self.layout_change); 31 | } 32 | 33 | pub fn deinit(self: *Self) void { 34 | self.wlr_output_layout.destroy(); 35 | } 36 | 37 | fn layoutChange(listener: *wl.Listener(*wlr.OutputLayout), _: *wlr.OutputLayout) void { 38 | const self = @fieldParentPtr(Self, "layout_change", listener); 39 | log.debug("Signal: wlr_output_layout_layout_change", .{}); 40 | 41 | // Everytime an output is resized / added / removed, we redo wallpaper rendering. 42 | // This is probably not efficient but without it in nested sessions, if wlr_output is resized, the wallpaper doesn't get resized accordingly. 43 | for (self.server.outputs.items) |output| { 44 | if (self.server.seat.focused_output) |_| {} else { 45 | self.server.seat.focusOutput(output); 46 | } 47 | 48 | if (output.has_wallpaper) { 49 | output.init_wallpaper_rendering() catch |err| { 50 | log.err("Error occured: {s}", .{@errorName(err)}); 51 | log.err("Skipping wallpaper setting.", .{}); 52 | }; 53 | } 54 | } 55 | 56 | //TODO: Finish this! 57 | //TODO: Take windows from deactivated monitors and rearrange them. 58 | //TODO: When a monitor mode changes, reorient it's child windows. 59 | //TODO: Handle layout changes. 60 | } 61 | -------------------------------------------------------------------------------- /next/desktop/Wallpaper.zig: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | // 3 | // next/desktop/Wallpaper.zig 4 | // 5 | // Created by: Aakash Sen Sharma, July 2023 6 | // Copyright: (C) 2023, Aakash Sen Sharma & Contributors 7 | 8 | const Self = @This(); 9 | 10 | const builtin = @import("builtin"); 11 | const std = @import("std"); 12 | const log = std.log.scoped(.Wallpaper); 13 | const os = std.os; 14 | const c = @import("../utils/c.zig"); 15 | const allocator = @import("../utils/allocator.zig").allocator; 16 | 17 | const wl = @import("wayland").server.wl; 18 | const wlr = @import("wlroots"); 19 | const endianess = builtin.target.cpu.arch.endian(); 20 | 21 | const WallpaperMode = @import("Output.zig").WallpaperMode; 22 | 23 | base_buffer: wlr.Buffer, 24 | data: [*c]u8, 25 | stride: usize, 26 | scene_buffer: ?*wlr.SceneBuffer = null, 27 | 28 | const jpeg_magics = [_][4]u8{ 29 | [_]u8{ 0xFF, 0xD8, 0xFF, 0xD8 }, 30 | [_]u8{ 0xFF, 0xD8, 0xFF, 0xE0 }, 31 | [_]u8{ 0xFF, 0xD8, 0xFF, 0xE1 }, 32 | }; 33 | 34 | const png_magic = [_]u8{ 0x89, 0x50, 0x4E, 0x47 }; 35 | 36 | pub fn cairo_load_image(path: []const u8) !*c.cairo_surface_t { 37 | log.debug("Loading image: {s}", .{path}); 38 | var magic: [4]u8 = undefined; 39 | 40 | const fd = try os.open(path, os.linux.O.RDONLY, undefined); 41 | defer os.close(fd); 42 | 43 | const bytes_read = try os.read(fd, &magic); 44 | if (bytes_read != 4) { 45 | return error.FailedToReadMagic; 46 | } 47 | 48 | for (jpeg_magics) |jpeg_magic| { 49 | if (std.mem.eql(u8, &magic, &jpeg_magic)) { 50 | log.info("Mimetype jpg detected", .{}); 51 | return cairo_load_jpg(path); 52 | } 53 | } 54 | 55 | if (std.mem.eql(u8, &magic, &png_magic)) { 56 | log.info("Mimetype png detected", .{}); 57 | return cairo_load_png(path); 58 | } 59 | return error.InvalidImageFormat; 60 | } 61 | 62 | fn cairo_load_png(path: []const u8) !*c.cairo_surface_t { 63 | return c.cairo_image_surface_create_from_png(path.ptr).?; 64 | } 65 | 66 | fn cairo_load_jpg(path: []const u8) !*c.cairo_surface_t { 67 | var jpeg_error: c.jpeg_error_mgr = undefined; 68 | var jpeg_info: c.jpeg_decompress_struct = undefined; 69 | 70 | const fd = @as(c_int, @intCast(try std.os.open(path, 0 | std.os.O.RDONLY, undefined))); 71 | defer _ = std.os.close(fd); 72 | 73 | const fd_stat = try std.os.fstat(fd); 74 | 75 | var buf: [*c]u8 = @as([*c]u8, @ptrCast(c.malloc(@as(c_ulong, @intCast(fd_stat.size))) orelse return error.OOM)); 76 | if (c.read(fd, buf, @as(usize, @intCast(fd_stat.size))) < fd_stat.size) { 77 | return error.failedToReadAllBytes; 78 | } 79 | 80 | jpeg_info.err = c.jpeg_std_error(&jpeg_error); 81 | c.jpeg_create_decompress(&jpeg_info); 82 | c.jpeg_mem_src(&jpeg_info, buf, @as(c_ulong, @intCast(fd_stat.size))); 83 | 84 | _ = c.jpeg_read_header(&jpeg_info, 1); 85 | 86 | log.info("System is {s} endian", .{@tagName(endianess)}); 87 | 88 | if (endianess == .Little) { 89 | log.info("Using JCS_EXT_BGRA format", .{}); 90 | jpeg_info.out_color_space = c.JCS_EXT_BGRA; 91 | } else { 92 | log.info("Using JCS_EXT_ARGB format", .{}); 93 | jpeg_info.out_color_space = c.JCS_EXT_ARGB; 94 | } 95 | 96 | _ = c.jpeg_start_decompress(&jpeg_info); 97 | 98 | var surface = c.cairo_image_surface_create(c.CAIRO_FORMAT_RGB24, @as(c_int, @intCast(jpeg_info.output_width)), @as(c_int, @intCast(jpeg_info.output_height))); 99 | if (c.cairo_surface_status(surface) != c.CAIRO_STATUS_SUCCESS) { 100 | c.jpeg_destroy_decompress(&jpeg_info); 101 | return error.cairoSurfaceCreationFailed; 102 | } 103 | 104 | while (jpeg_info.output_scanline < jpeg_info.output_height) { 105 | var row_address = c.cairo_image_surface_get_data(surface) + (jpeg_info.output_scanline * @as(c_uint, @intCast(c.cairo_image_surface_get_stride(surface)))); 106 | 107 | _ = c.jpeg_read_scanlines(&jpeg_info, &row_address, 1); 108 | } 109 | 110 | c.cairo_surface_mark_dirty(surface); 111 | _ = c.jpeg_finish_decompress(&jpeg_info); 112 | c.jpeg_destroy_decompress(&jpeg_info); 113 | _ = c.cairo_surface_set_mime_data(surface, c.CAIRO_MIME_TYPE_JPEG, buf, @as(c_ulong, @intCast(fd_stat.size)), c.free, buf); 114 | 115 | return surface.?; 116 | } 117 | 118 | pub fn cairo_surface_transform_apply(image_surface: *c.cairo_surface_t, transform: WallpaperMode, width: u64, height: u64) !*c.cairo_surface_t { 119 | const surface = c.cairo_image_surface_create(c.CAIRO_FORMAT_ARGB32, @as(c_int, @intCast(width)), @as(c_int, @intCast(height))) orelse return error.cairoCreateImageSurfaceFailed; 120 | 121 | const cairo = c.cairo_create(surface) orelse return error.cairoCtxFailed; 122 | defer c.cairo_destroy(cairo); 123 | defer c.cairo_surface_destroy(image_surface); 124 | 125 | const image_width = @as(f64, @floatFromInt(c.cairo_image_surface_get_width(image_surface))); 126 | const image_height = @as(f64, @floatFromInt(c.cairo_image_surface_get_height(image_surface))); 127 | 128 | switch (transform) { 129 | .fit => { 130 | _ = c.cairo_rectangle(cairo, 0, 0, @as(f64, @floatFromInt(width)), @as(f64, @floatFromInt(height))); 131 | c.cairo_clip(cairo); 132 | const width_ratio: f64 = @as(f64, @floatFromInt(width)) / image_width; 133 | if (width_ratio * image_height >= @as(f64, @floatFromInt(height))) { 134 | c.cairo_scale(cairo, width_ratio, width_ratio); 135 | } else { 136 | const height_ratio = @as(f64, @floatFromInt(height)) / image_height; 137 | c.cairo_translate(cairo, @divFloor(-(image_width * height_ratio - @as(f64, @floatFromInt(width))), 2), 0); 138 | c.cairo_scale(cairo, height_ratio, height_ratio); 139 | } 140 | }, 141 | .stretch => { 142 | c.cairo_scale( 143 | cairo, 144 | @as(f64, @floatFromInt(width)) / image_width, 145 | @as(f64, @floatFromInt(height)) / image_height, 146 | ); 147 | }, 148 | } 149 | c.cairo_set_source_surface(cairo, image_surface, 0, 0); 150 | c.cairo_paint(cairo); 151 | c.cairo_restore(cairo); 152 | 153 | c.cairo_surface_flush(surface); 154 | 155 | return surface; 156 | } 157 | 158 | pub fn cairo_buffer_create(self: *Self, width: c_int, height: c_int, stride: usize, data: [*c]u8) !void { 159 | const cairo_buffer_impl = wlr.Buffer.Impl{ 160 | .destroy = cairo_handle_destroy, 161 | .get_dmabuf = undefined, 162 | .get_shm = undefined, 163 | .begin_data_ptr_access = cairo_data_access, 164 | .end_data_ptr_access = cairo_end_data_access, 165 | }; 166 | 167 | self.base_buffer.init(&cairo_buffer_impl, width, height); 168 | 169 | self.data = data; 170 | self.stride = stride; 171 | } 172 | 173 | pub fn cairo_data_access(buffer: *wlr.Buffer, _: u32, data: **anyopaque, format: *u32, stride: *usize) callconv(.C) bool { 174 | const self = @fieldParentPtr(Self, "base_buffer", buffer); 175 | log.debug("Buffer data accessed", .{}); 176 | 177 | data.* = self.data; 178 | stride.* = self.stride; 179 | format.* = c.DRM_FORMAT_ARGB8888; 180 | 181 | return true; 182 | } 183 | pub fn cairo_end_data_access(_: *wlr.Buffer) callconv(.C) void { 184 | log.debug("Buffer data access ended", .{}); 185 | } 186 | pub fn cairo_handle_destroy(buffer: *wlr.Buffer) callconv(.C) void { 187 | const self = @fieldParentPtr(Self, "base_buffer", buffer); 188 | log.debug("Buffer destroyed", .{}); 189 | 190 | if (!self.base_buffer.dropped) { 191 | self.base_buffer.drop(); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /next/desktop/Window.zig: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | // 3 | // next/desktop/Window.zig 4 | // 5 | // Created by: Aakash Sen Sharma, May 2022 6 | // Copyright: (C) 2022, Aakash Sen Sharma & Contributors 7 | 8 | const Self = @This(); 9 | 10 | const allocator = @import("../utils/allocator.zig").allocator; 11 | const log = std.log.scoped(.Window); 12 | const server = &@import("../next.zig").server; 13 | const std = @import("std"); 14 | 15 | const wlr = @import("wlroots"); 16 | 17 | const Server = @import("../Server.zig"); 18 | const Output = @import("Output.zig"); 19 | const XdgToplevel = @import("XdgToplevel.zig"); 20 | 21 | const Backend = union(enum) { 22 | xdg_toplevel: XdgToplevel, 23 | }; 24 | 25 | server: *Server, 26 | output: *Output, 27 | 28 | wlr_foreign_toplevel_handle: *wlr.ForeignToplevelHandleV1 = undefined, 29 | backend: Backend, 30 | 31 | pub fn init(self: *Self, output: *Output, backend: Backend) !void { 32 | log.debug("Initializing window", .{}); 33 | // TODO: Set toplevel tags here. 34 | self.* = .{ 35 | .output = output, 36 | .backend = backend, 37 | .wlr_foreign_toplevel_handle = try wlr.ForeignToplevelHandleV1.create(server.wlr_foreign_toplevel_manager), 38 | .server = server, 39 | }; 40 | 41 | server.pending_windows.append(allocator, self) catch { 42 | log.err("Failed to allocate memory", .{}); 43 | return; 44 | }; 45 | 46 | switch (self.backend) { 47 | .xdg_toplevel => |xdg_toplevel| xdg_toplevel.xdg_surface.data = @intFromPtr(self), 48 | } 49 | } 50 | 51 | pub fn getAppId(self: *Self) [*:0]const u8 { 52 | log.debug("Surface AppID was requested", .{}); 53 | return switch (self.backend) { 54 | .xdg_toplevel => |xdg_toplevel| xdg_toplevel.getAppId(), 55 | }; 56 | } 57 | 58 | pub fn getTitle(self: *Self) [*:0]const u8 { 59 | log.debug("Surface Title was requested", .{}); 60 | return switch (self.backend) { 61 | .xdg_toplevel => |xdg_toplevel| xdg_toplevel.getTitle(), 62 | }; 63 | } 64 | 65 | pub fn notifyAppId(self: *Self, app_id: [*:0]const u8) void { 66 | log.debug("AppID data propagated to ftm handle", .{}); 67 | self.wlr_foreign_toplevel_handle.setAppId(app_id); 68 | } 69 | 70 | pub fn notifyTitle(self: *Self, title: [*:0]const u8) void { 71 | log.debug("Title data propagated to ftm handle", .{}); 72 | self.wlr_foreign_toplevel_handle.setTitle(title); 73 | } 74 | 75 | pub fn setMon(self: *Self, output: *Output) void { 76 | if (output == self.output) return; 77 | switch (self.backend) { 78 | .xdg_toplevel => |xdg_toplevel| { 79 | // TODO: Is this performant? 80 | xdg_toplevel.xdg_surface.surface.sendLeave(self.output.wlr_output); 81 | xdg_toplevel.xdg_surface.surface.sendEnter(output.wlr_output); 82 | }, 83 | } 84 | self.output = output; 85 | } 86 | 87 | // Called by backend specific implementation on destroy event. 88 | /// Free window and toplevel on destroy. 89 | pub fn handleDestroy(self: *Self) void { 90 | switch (self.backend) { 91 | .xdg_toplevel => |*xdg_toplevel| { 92 | for (xdg_toplevel.borders) |border| { 93 | border.node.destroy(); 94 | } 95 | xdg_toplevel.scene_tree.node.destroy(); 96 | }, 97 | } 98 | if (std.mem.indexOfScalar(*Self, self.server.mapped_windows.items, self)) |i| { 99 | log.warn("Window destroyed before unmap event.", .{}); 100 | self.wlr_foreign_toplevel_handle.destroy(); 101 | 102 | const window = self.server.mapped_windows.swapRemove(i); 103 | allocator.destroy(window); 104 | allocator.destroy(self); 105 | } else { 106 | if (std.mem.indexOfScalar(*Self, server.pending_windows.items, self)) |i| { 107 | self.wlr_foreign_toplevel_handle.destroy(); 108 | 109 | const window = self.server.pending_windows.swapRemove(i); 110 | allocator.destroy(window); 111 | allocator.destroy(self); 112 | } 113 | } 114 | 115 | // Set the focus to the window below the cursor :) 116 | const window_below_cursor = windowAt(server.wlr_cursor.x, server.wlr_cursor.y) orelse return; 117 | server.seat.setFocus(window_below_cursor.window, window_below_cursor.surface); 118 | } 119 | 120 | const WindowAt = struct { 121 | window: *Self, 122 | surface: *wlr.Surface, 123 | sx: f64, 124 | sy: f64, 125 | }; 126 | 127 | pub fn windowAt( 128 | lx: f64, 129 | ly: f64, 130 | ) ?WindowAt { 131 | var sx: f64 = undefined; 132 | var sy: f64 = undefined; 133 | 134 | if (server.wlr_scene.tree.node.at(lx, ly, &sx, &sy)) |node| { 135 | if (node.type != .buffer) return null; 136 | 137 | const scene_buffer = wlr.SceneBuffer.fromNode(node); 138 | const scene_surface = wlr.SceneSurface.fromBuffer(scene_buffer) orelse return null; 139 | 140 | var it: ?*wlr.SceneTree = node.parent; 141 | while (it) |n| : (it = n.node.parent) { 142 | if (@as(?*Self, @ptrFromInt(n.node.data))) |window| { 143 | return WindowAt{ 144 | .window = window, 145 | .surface = scene_surface.surface, 146 | .sx = sx, 147 | .sy = sy, 148 | }; 149 | } 150 | } 151 | } 152 | return null; 153 | } 154 | -------------------------------------------------------------------------------- /next/desktop/XdgPopup.zig: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | // 3 | // next/desktop/XdgPopup.zig 4 | // 5 | // Created by: Aakash Sen Sharma, May 2022 6 | // Copyright: (C) 2022, Aakash Sen Sharma & Contributors 7 | 8 | const Self = @This(); 9 | 10 | const allocator = @import("../utils/allocator.zig").allocator; 11 | const log = std.log.scoped(.XdgPopup); 12 | const server = &@import("../next.zig").server; 13 | const std = @import("std"); 14 | 15 | const wl = @import("wayland").server.wl; 16 | const wlr = @import("wlroots"); 17 | 18 | const XdgToplevel = @import("XdgToplevel.zig"); 19 | const Server = @import("../Server.zig"); 20 | const ParentSurface = union(enum) { 21 | xdg_toplevel: *XdgToplevel, 22 | xdg_popup: *Self, 23 | }; 24 | 25 | wlr_xdg_popup: *wlr.XdgPopup, 26 | parent: ParentSurface, 27 | mapped: bool = false, 28 | 29 | popups: std.ArrayListUnmanaged(*Self) = .{}, 30 | server: *Server, 31 | 32 | output_box: wlr.Box = undefined, 33 | 34 | //TODO: Handle SubSurfaces 35 | map: wl.Listener(*wlr.XdgSurface) = wl.Listener(*wlr.XdgSurface).init(handleMap), 36 | unmap: wl.Listener(*wlr.XdgSurface) = wl.Listener(*wlr.XdgSurface).init(handleUnmap), 37 | surface_destroy: wl.Listener(*wlr.XdgSurface) = wl.Listener(*wlr.XdgSurface).init(handleDestroy), 38 | new_popup: wl.Listener(*wlr.XdgPopup) = wl.Listener(*wlr.XdgPopup).init(handleNewPopup), 39 | 40 | commit: wl.Listener(*wlr.Surface) = wl.Listener(*wlr.Surface).init(handleCommit), 41 | 42 | pub fn create(xdg_popup: *wlr.XdgPopup, parent: ParentSurface) ?*Self { 43 | const self: *Self = allocator.create(Self) catch { 44 | std.log.err("Failed to allocate memory", .{}); 45 | xdg_popup.resource.postNoMemory(); 46 | return null; 47 | }; 48 | self.* = .{ 49 | .parent = parent, 50 | .wlr_xdg_popup = xdg_popup, 51 | .server = server, 52 | }; 53 | 54 | if (xdg_popup.base.data == 0) xdg_popup.base.data = @intFromPtr(self); 55 | 56 | switch (parent) { 57 | .xdg_toplevel => |xdg_toplevel| { 58 | var box: wlr.Box = undefined; 59 | xdg_popup.base.getGeometry(&box); 60 | 61 | var lx: f64 = 0; 62 | var ly: f64 = 0; 63 | self.server.output_layout.wlr_output_layout.closestPoint( 64 | null, 65 | @as(f64, @floatFromInt(xdg_toplevel.geometry.x + box.x)), 66 | @as(f64, @floatFromInt(xdg_toplevel.geometry.y + box.y)), 67 | &lx, 68 | &ly, 69 | ); 70 | 71 | var width: c_int = undefined; 72 | var height: c_int = undefined; 73 | if (self.server.output_layout.wlr_output_layout.outputAt(lx, ly)) |output| { 74 | output.effectiveResolution(&width, &height); 75 | } else { 76 | log.warn("Failed to find output for xdg_popup", .{}); 77 | allocator.destroy(self); 78 | return null; 79 | } 80 | self.output_box = wlr.Box{ 81 | .x = box.x - @as(c_int, @intFromFloat(lx)), 82 | .y = box.y - @as(c_int, @intFromFloat(ly)), 83 | .width = width, 84 | .height = height, 85 | }; 86 | }, 87 | 88 | // Nested popup! 89 | .xdg_popup => |parent_popup| { 90 | self.output_box = parent_popup.output_box; 91 | }, 92 | } 93 | xdg_popup.unconstrainFromBox(&self.output_box); 94 | return self; 95 | } 96 | 97 | pub fn destroy(self: *Self) void { 98 | self.surface_destroy.link.remove(); 99 | self.map.link.remove(); 100 | self.unmap.link.remove(); 101 | self.new_popup.link.remove(); 102 | self.new_popup.link.remove(); 103 | 104 | if (self.mapped) self.commit.link.remove(); 105 | 106 | self.destroyPopups(); 107 | allocator.destroy(self); 108 | } 109 | 110 | pub fn destroyPopups(self: *Self) void { 111 | for (self.popups.items) |popup| { 112 | popup.wlr_xdg_popup.destroy(); 113 | allocator.destroy(popup); 114 | } 115 | self.popups.deinit(allocator); 116 | } 117 | 118 | fn handleMap(listener: *wl.Listener(*wlr.XdgSurface), _: *wlr.XdgSurface) void { 119 | const self = @fieldParentPtr(Self, "map", listener); 120 | log.debug("Signal: wlr_xdg_popup_map", .{}); 121 | 122 | self.wlr_xdg_popup.base.surface.events.commit.add(&self.commit); 123 | self.mapped = true; 124 | } 125 | 126 | fn handleCommit(_: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void { 127 | log.debug("Signal: wlr_xdg_popup_commit", .{}); 128 | } 129 | 130 | fn handleUnmap(_: *wl.Listener(*wlr.XdgSurface), _: *wlr.XdgSurface) void { 131 | log.debug("Signal: wlr_xdg_popup_unmap", .{}); 132 | } 133 | 134 | fn handleNewPopup(listener: *wl.Listener(*wlr.XdgPopup), wlr_xdg_popup: *wlr.XdgPopup) void { 135 | const self = @fieldParentPtr(Self, "new_popup", listener); 136 | log.debug("Signal: wlr_xdg_popup_new_popup", .{}); 137 | 138 | if (Self.create(wlr_xdg_popup, self.parent)) |popup| { 139 | self.popups.append(allocator, popup) catch { 140 | log.err("Failed to allocate memory", .{}); 141 | return; 142 | }; 143 | } else { 144 | log.err("Failed to create new_popup", .{}); 145 | } 146 | } 147 | 148 | fn handleDestroy(listener: *wl.Listener(*wlr.XdgSurface), _: *wlr.XdgSurface) void { 149 | const self = @fieldParentPtr(Self, "surface_destroy", listener); 150 | log.debug("Signal: wlr_xdg_popup_destroy", .{}); 151 | 152 | self.destroy(); 153 | } 154 | -------------------------------------------------------------------------------- /next/desktop/XdgToplevel.zig: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | // 3 | // next/desktop/XdgToplevel.zig 4 | // 5 | // Created by: Aakash Sen Sharma, May 2022 6 | // Copyright: (C) 2022, Aakash Sen Sharma & Contributors 7 | 8 | const Self = @This(); 9 | 10 | const allocator = @import("../utils/allocator.zig").allocator; 11 | const log = std.log.scoped(.XdgToplevel); 12 | const server = &@import("../next.zig").server; 13 | const std = @import("std"); 14 | 15 | const wl = @import("wayland").server.wl; 16 | const wlr = @import("wlroots"); 17 | 18 | const Window = @import("Window.zig"); 19 | const Output = @import("Output.zig"); 20 | const XdgPopup = @import("XdgPopup.zig"); 21 | 22 | window: *Window, 23 | 24 | xdg_surface: *wlr.XdgSurface, 25 | 26 | borders: [4]*wlr.SceneRect = undefined, 27 | popups: std.ArrayListUnmanaged(*XdgPopup) = .{}, 28 | 29 | geometry: wlr.Box = undefined, 30 | draw_borders: bool = true, 31 | scene_tree: *wlr.SceneTree = undefined, 32 | scene_surface: *wlr.SceneTree = undefined, 33 | 34 | map: wl.Listener(void) = wl.Listener(void).init(handleMap), 35 | unmap: wl.Listener(void) = wl.Listener(void).init(handleUnmap), 36 | destroy: wl.Listener(void) = wl.Listener(void).init(handleDestroy), 37 | set_app_id: wl.Listener(void) = wl.Listener(void).init(setAppId), 38 | new_popup: wl.Listener(*wlr.XdgPopup) = wl.Listener(*wlr.XdgPopup).init(newPopup), 39 | set_title: wl.Listener(void) = wl.Listener(void).init(setTitle), 40 | 41 | // Listeners that are only active while the window is mapped. 42 | commit: wl.Listener(*wlr.Surface) = wl.Listener(*wlr.Surface).init(handleCommit), 43 | 44 | pub fn init(focused_output: *Output, xdg_surface: *wlr.XdgSurface) error{OutOfMemory}!void { 45 | log.debug("New xdg_shell toplevel received: title=\"{s}\" app_id=\"{s}\"", .{ 46 | getXdgSurfaceTitle(xdg_surface), 47 | getXdgSurfaceAppID(xdg_surface), 48 | }); 49 | 50 | const window = allocator.create(Window) catch { 51 | log.err("Failed to allocate memory", .{}); 52 | return; 53 | }; 54 | errdefer allocator.destroy(window); 55 | 56 | window.init(focused_output, .{ .xdg_toplevel = .{ 57 | .window = window, 58 | .xdg_surface = xdg_surface, 59 | } }) catch { 60 | allocator.destroy(window); 61 | log.err("Failed to create a window out of the toplevel", .{}); 62 | return; 63 | }; 64 | 65 | xdg_surface.events.map.add(&window.backend.xdg_toplevel.map); 66 | xdg_surface.events.unmap.add(&window.backend.xdg_toplevel.unmap); 67 | xdg_surface.events.destroy.add(&window.backend.xdg_toplevel.destroy); 68 | xdg_surface.events.new_popup.add(&window.backend.xdg_toplevel.new_popup); 69 | 70 | xdg_surface.role_data.toplevel.events.set_app_id.add(&window.backend.xdg_toplevel.set_app_id); 71 | xdg_surface.role_data.toplevel.events.set_title.add(&window.backend.xdg_toplevel.set_title); 72 | 73 | _ = xdg_surface.role_data.toplevel.setWmCapabilities(.{ .fullscreen = true }); 74 | // Maybe eventually we'll support these? 75 | // TODO: xdg_surface.events.map.new_subsurface(&self.new_subsurface); 76 | // TODO: Handle existing subsurfaces. 77 | } 78 | 79 | pub fn handleMap(listener: *wl.Listener(void)) void { 80 | const self = @fieldParentPtr(Self, "map", listener); 81 | log.debug("Signal: wlr_xdg_surface_map", .{}); 82 | 83 | // TODO: Check if view wants to be fullscreen then make it fullscreen. 84 | 85 | // Setting some struct fields we will need later 86 | self.scene_tree = server.layer_tile.createSceneTree() catch return; 87 | self.scene_tree.node.setEnabled(true); 88 | 89 | // TODO: Remember to set data to ref to Window for proper Node detection during cursor motion processing. 90 | self.scene_tree.node.data = @intFromPtr(self.window); 91 | 92 | // TODO: This should handle our resize logic primarily 93 | self.xdg_surface.surface.events.commit.add(&self.commit); 94 | 95 | self.scene_surface = self.scene_tree.createSceneXdgSurface(self.xdg_surface) catch return; 96 | self.xdg_surface.getGeometry(&self.geometry); 97 | 98 | inline for (0..4) |i| { 99 | self.borders[i] = self.scene_tree.createSceneRect(0, 0, &server.config.border_color) catch return; 100 | } 101 | 102 | // If the client can have csd then why draw servide side borders? 103 | if (server.config.csdAllowed(self.window)) { 104 | self.draw_borders = false; 105 | } else { 106 | _ = self.xdg_surface.role_data.toplevel.setTiled(.{ .top = true, .bottom = true, .left = true, .right = true }); 107 | } 108 | 109 | self.window.wlr_foreign_toplevel_handle = wlr.ForeignToplevelHandleV1.create(server.wlr_foreign_toplevel_manager) catch { 110 | log.err("Failed to create foreign_toplevel_handle", .{}); 111 | return; 112 | }; 113 | 114 | // Setup ftm handle listeners. 115 | // TODO: Some more toplevel handle listeners go here 116 | self.window.wlr_foreign_toplevel_handle.setTitle(self.getTitle()); 117 | self.window.wlr_foreign_toplevel_handle.setAppId(self.getAppId()); 118 | 119 | // TODO: This segfaults probably because we infer wlr_output from the seats currently focused output which is initially undefined 120 | // TODO: Fix me 121 | // self.window.wlr_foreign_toplevel_handle.outputEnter(self.window.output.wlr_output); 122 | 123 | // Find index of self from pending_windows and remove it. 124 | if (std.mem.indexOfScalar(*Window, server.pending_windows.items, self.window)) |i| { 125 | _ = server.pending_windows.swapRemove(i); 126 | } 127 | 128 | // Appending should happen regardless of us finding the window in pending_windows. 129 | server.mapped_windows.append(allocator, self.window) catch { 130 | log.err("Failed to allocate memory.", .{}); 131 | self.xdg_surface.resource.getClient().postNoMemory(); 132 | return; 133 | }; 134 | log.debug("Window '{s}' mapped", .{self.getTitle()}); 135 | 136 | //TODO: Remove this 137 | _ = self.xdg_surface.role_data.toplevel.setActivated(true); 138 | const keyboard = server.seat.wlr_seat.getKeyboard() orelse return; 139 | server.seat.wlr_seat.keyboardNotifyEnter( 140 | self.xdg_surface.role_data.toplevel.base.surface, 141 | &keyboard.keycodes, 142 | keyboard.num_keycodes, 143 | &keyboard.modifiers, 144 | ); 145 | } 146 | 147 | pub fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void { 148 | const self = @fieldParentPtr(Self, "commit", listener); 149 | log.debug("Signal: wlr_xdg_surface_commit", .{}); 150 | 151 | //TODO: Instead of constantly resizing on each commit, check if a resize is needed by setting a flag 152 | // 153 | // Or check if the surface wants a resize by checking it's geometry? Look at what others do here. 154 | 155 | const geom = self.window.output.getGeometry(); 156 | self.window.backend.xdg_toplevel.resize( 157 | @as(c_int, @intCast(geom.x)), 158 | @as(c_int, @intCast(geom.y)), 159 | @as(c_int, @intCast(geom.width)), 160 | @as(c_int, @intCast(geom.height)), 161 | ); 162 | } 163 | 164 | pub fn newPopup(listener: *wl.Listener(*wlr.XdgPopup), xdg_popup: *wlr.XdgPopup) void { 165 | const self = @fieldParentPtr(Self, "new_popup", listener); 166 | log.debug("Signal: wlr_xdg_surface_new_popup", .{}); 167 | 168 | if (XdgPopup.create(xdg_popup, .{ .xdg_toplevel = self })) |popup| { 169 | self.popups.append(allocator, popup) catch return; 170 | } 171 | } 172 | 173 | pub fn handleUnmap(listener: *wl.Listener(void)) void { 174 | const self = @fieldParentPtr(Self, "unmap", listener); 175 | log.debug("Signal: wlr_xdg_surface_unmap", .{}); 176 | 177 | if (std.mem.indexOfScalar(*Window, server.mapped_windows.items, self.window)) |i| { 178 | _ = server.mapped_windows.swapRemove(i); 179 | } 180 | if (server.seat.wlr_seat.keyboard_state.focused_surface) |focused_surface| { 181 | if (focused_surface == self.xdg_surface.surface) { 182 | server.seat.wlr_seat.keyboardClearFocus(); 183 | } 184 | } 185 | if (server.seat.wlr_seat.pointer_state.focused_surface) |focused_surface| { 186 | if (focused_surface == self.xdg_surface.surface) { 187 | server.seat.wlr_seat.pointerClearFocus(); 188 | } 189 | } 190 | } 191 | 192 | pub fn handleDestroy(listener: *wl.Listener(void)) void { 193 | const self = @fieldParentPtr(Self, "destroy", listener); 194 | log.debug("Signal: wlr_xdg_surface_destroy", .{}); 195 | 196 | self.window.handleDestroy(); 197 | } 198 | 199 | pub fn resize(self: *Self, x: c_int, y: c_int, width: c_int, height: c_int) void { 200 | const border_width = server.config.border_width; 201 | self.geometry.x = x; 202 | self.geometry.y = y; 203 | self.geometry.width = width; 204 | self.geometry.height = height; 205 | 206 | if (self.draw_borders) { 207 | // If borders are meant to be drawn then add that to the geometry width 208 | self.geometry.width += 2 * border_width; 209 | self.geometry.height += 2 * border_width; 210 | 211 | self.borders[0].setSize(self.geometry.width, border_width); 212 | 213 | self.borders[1].setSize(self.geometry.width, border_width); 214 | self.borders[1].node.setPosition(0, self.geometry.height - border_width); 215 | 216 | self.borders[2].setSize(border_width, self.geometry.height - 2 * border_width); 217 | self.borders[2].node.setPosition(0, border_width); 218 | 219 | self.borders[3].setSize(border_width, self.geometry.height - 2 * border_width); 220 | self.borders[3].node.setPosition(self.geometry.width - border_width, border_width); 221 | } 222 | self.scene_tree.node.setPosition(self.geometry.x, self.geometry.y); 223 | self.scene_surface.node.setPosition(border_width, border_width); 224 | 225 | self.updateSize(width, height); 226 | } 227 | 228 | /// If the passed width and height are the same then this function is no-op 229 | pub fn updateSize(self: *Self, width: i32, height: i32) void { 230 | if (self.geometry.width == width and self.geometry.height == height) { 231 | return; 232 | } 233 | 234 | _ = self.xdg_surface.role_data.toplevel.setSize(width, height); 235 | } 236 | 237 | pub fn setAppId(listener: *wl.Listener(void)) void { 238 | const self = @fieldParentPtr(Self, "set_app_id", listener); 239 | log.debug("Signal: wlr_xdg_toplevel_set_app_id", .{}); 240 | 241 | self.window.notifyAppId(self.getAppId()); 242 | } 243 | 244 | pub fn setTitle(listener: *wl.Listener(void)) void { 245 | const self = @fieldParentPtr(Self, "set_title", listener); 246 | log.debug("Signal: wlr_xdg_toplevel_set_title", .{}); 247 | 248 | self.window.notifyTitle(self.getTitle()); 249 | } 250 | 251 | pub fn getTitle(self: Self) [*:0]const u8 { 252 | return getXdgSurfaceTitle(self.xdg_surface); 253 | } 254 | 255 | pub fn getAppId(self: Self) [*:0]const u8 { 256 | return getXdgSurfaceAppID(self.xdg_surface); 257 | } 258 | 259 | fn getXdgSurfaceTitle(xdg_surface: *wlr.XdgSurface) [*:0]const u8 { 260 | if (xdg_surface.role_data.toplevel.title) |title| { 261 | return title; 262 | } else { 263 | return ""; 264 | } 265 | } 266 | fn getXdgSurfaceAppID(xdg_surface: *wlr.XdgSurface) [*:0]const u8 { 267 | if (xdg_surface.role_data.toplevel.app_id) |app_id| { 268 | return app_id; 269 | } else { 270 | return ""; 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /next/global/Control.zig: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | // 3 | // next/global/Control.zig 4 | // 5 | // Created by: Aakash Sen Sharma, May 2022 6 | // Copyright: (C) 2022, Aakash Sen Sharma & Contributors 7 | 8 | const Self = @This(); 9 | 10 | const std = @import("std"); 11 | const allocator = @import("../utils/allocator.zig").allocator; 12 | const command = @import("../control/command.zig"); 13 | const server = &@import("../next.zig").server; 14 | 15 | const wayland = @import("wayland"); 16 | const next = wayland.server.next; 17 | const wl = wayland.server.wl; 18 | 19 | const ArgMap = std.AutoHashMap(struct { client: *wl.Client, id: u32 }, std.ArrayListUnmanaged([:0]const u8)); 20 | 21 | global: *wl.Global, 22 | server_destroy: wl.Listener(*wl.Server) = wl.Listener(*wl.Server).init(serverDestroy), 23 | args: ArgMap, 24 | 25 | pub fn init(self: *Self) !void { 26 | self.* = .{ 27 | .global = try wl.Global.create(server.wl_server, next.ControlV1, 1, *Self, self, bind), 28 | .args = ArgMap.init(allocator), 29 | }; 30 | 31 | server.wl_server.addDestroyListener(&self.server_destroy); 32 | } 33 | 34 | fn serverDestroy(listener: *wl.Listener(*wl.Server), _: *wl.Server) void { 35 | const self = @fieldParentPtr(Self, "server_destroy", listener); 36 | self.global.destroy(); 37 | self.args.deinit(); 38 | } 39 | 40 | fn bind(client: *wl.Client, self: *Self, version: u32, id: u32) void { 41 | const control = next.ControlV1.create(client, version, id) catch { 42 | client.postNoMemory(); 43 | return; 44 | }; 45 | self.args.putNoClobber(.{ .client = client, .id = id }, .{}) catch { 46 | client.destroy(); 47 | client.postNoMemory(); 48 | return; 49 | }; 50 | control.setHandler(*Self, handleRequest, handleDestroy, self); 51 | } 52 | 53 | fn handleRequest(control: *next.ControlV1, request: next.ControlV1.Request, self: *Self) void { 54 | switch (request) { 55 | .destroy => control.destroy(), 56 | .add_argument => |add_argument| { 57 | const slice = allocator.dupeZ(u8, std.mem.sliceTo(add_argument.argument, 0)) catch { 58 | control.getClient().postNoMemory(); 59 | return; 60 | }; 61 | 62 | const arg_map = self.args.getPtr(.{ .client = control.getClient(), .id = control.getId() }).?; 63 | arg_map.append(allocator, slice) catch { 64 | control.getClient().postNoMemory(); 65 | allocator.free(slice); 66 | return; 67 | }; 68 | }, 69 | .run_command => |run_command| { 70 | const args = self.args.getPtr(.{ .client = control.getClient(), .id = control.getId() }).?; 71 | defer { 72 | for (args.items) |arg| allocator.free(arg); 73 | args.items.len = 0; 74 | } 75 | 76 | const callback = next.CommandCallbackV1.create( 77 | control.getClient(), 78 | control.getVersion(), 79 | run_command.callback, 80 | ) catch { 81 | control.getClient().postNoMemory(); 82 | return; 83 | }; 84 | 85 | var output: ?[]const u8 = null; 86 | defer if (output) |s| allocator.free(s); 87 | command.run(args.items, &output) catch |err| { 88 | const failure_message = switch (err) { 89 | command.Error.OutOfMemory => { 90 | callback.getClient().postNoMemory(); 91 | return; 92 | }, 93 | else => command.errToMsg(err), 94 | }; 95 | callback.sendFailure(failure_message); 96 | return; 97 | }; 98 | 99 | const success_message: [:0]const u8 = blk: { 100 | if (output) |s| { 101 | break :blk allocator.dupeZ(u8, s) catch { 102 | callback.getClient().postNoMemory(); 103 | return; 104 | }; 105 | } else break :blk ""; 106 | }; 107 | 108 | defer if (output != null) allocator.free(success_message); 109 | callback.sendSuccess(success_message); 110 | }, 111 | } 112 | } 113 | 114 | fn handleDestroy(control: *next.ControlV1, self: *Self) void { 115 | var args = self.args.fetchRemove( 116 | .{ .client = control.getClient(), .id = control.getId() }, 117 | ).?.value; 118 | for (args.items) |arg| allocator.free(arg); 119 | args.deinit(allocator); 120 | } 121 | -------------------------------------------------------------------------------- /next/input/Cursor.zig: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | // 3 | // next/input/Cursor.zig 4 | // 5 | // Created by: Aakash Sen Sharma, July 2022 6 | // Copyright: (C) 2022, Aakash Sen Sharma & Contributors 7 | 8 | const Self = @This(); 9 | 10 | const std = @import("std"); 11 | const allocator = @import("../utils/allocator.zig").allocator; 12 | const server = &@import("../next.zig").server; 13 | const c = @import("../utils/c.zig"); 14 | const log = std.log.scoped(.Cursor); 15 | 16 | const wl = @import("wayland").server.wl; 17 | const wlr = @import("wlroots"); 18 | 19 | const Server = @import("../Server.zig"); 20 | const Window = @import("../desktop/Window.zig"); 21 | 22 | server: *Server, 23 | 24 | wlr_input_device: *wlr.InputDevice, 25 | wlr_pointer: *wlr.Pointer, 26 | 27 | axis: wl.Listener(*wlr.Pointer.event.Axis) = wl.Listener(*wlr.Pointer.event.Axis).init(handleAxis), 28 | button: wl.Listener(*wlr.Pointer.event.Button) = wl.Listener(*wlr.Pointer.event.Button).init(handleButton), 29 | frame: wl.Listener(*wlr.Cursor) = wl.Listener(*wlr.Cursor).init(handleFrame), 30 | motion: wl.Listener(*wlr.Pointer.event.Motion) = wl.Listener(*wlr.Pointer.event.Motion).init(handleMotion), 31 | motion_absolute: wl.Listener(*wlr.Pointer.event.MotionAbsolute) = wl.Listener(*wlr.Pointer.event.MotionAbsolute).init(handleMotionAbsolute), 32 | 33 | pointer_destroy: wl.Listener(*wlr.InputDevice) = wl.Listener(*wlr.InputDevice).init(pointerDestroy), 34 | 35 | pub fn init(self: *Self, device: *wlr.InputDevice) !void { 36 | log.debug("Initializing pointer device", .{}); 37 | 38 | self.* = .{ 39 | .server = server, 40 | .wlr_input_device = device, 41 | .wlr_pointer = device.toPointer(), 42 | }; 43 | 44 | if (self.wlr_input_device.isLibinput()) { 45 | //TODO: More libinput config_required. 46 | // This is probably generic for any kind of device and can be a global function in the InputManager / InputConfig namespace. 47 | // 48 | //const libinput_device = @ptrCast(*c.libinput_device, self.wlr_input_device.getLibinputDevice().?); 49 | 50 | //if (c.libinput_device_config_tap_get_finger_count(libinput_device) > 0) { 51 | //c.libinput_device_config_tap_set_enabled(libinput_device, 1); 52 | //c.libinput_device_config_tap_set_drag_enabled(libinput_device, 1); 53 | //c.libinput_device_config_tap_set_drag_lock_enabled(libinput_device, 1); 54 | //c.libinput_device_config_tap_set_button_map(libinput_device, c.LIBINPUT_CONFIG_TAP_MAP_LRM); 55 | //} 56 | 57 | //if (c.libinput_device_config_scroll_has_natural_scroll(libinput_device)) { 58 | //c.libinput_device_config_scroll_set_natural_scroll_enabled(libinput_device, 0); 59 | //} 60 | 61 | //if (c.libinput_device_config_dwt_is_available(libinput_device)) { 62 | //c.libinput_device_config_dwt_set_enabled(libinput_device, 1); 63 | //} 64 | } 65 | 66 | // These listeners only need to be registered once against the wlr_cursor. 67 | if (self.server.cursors.items.len == 0) { 68 | self.server.wlr_cursor.events.axis.add(&self.axis); 69 | self.server.wlr_cursor.events.button.add(&self.button); 70 | self.server.wlr_cursor.events.frame.add(&self.frame); 71 | self.server.wlr_cursor.events.motion.add(&self.motion); 72 | self.server.wlr_cursor.events.motion_absolute.add(&self.motion_absolute); 73 | } 74 | 75 | self.server.cursors.append(allocator, self) catch { 76 | return error.OOM; 77 | }; 78 | //TODO: We should modify the pointer libinput object here to support features such as tap to click. 79 | 80 | self.server.wlr_cursor.attachInputDevice(self.wlr_input_device); 81 | 82 | self.wlr_input_device.events.destroy.add(&self.pointer_destroy); 83 | } 84 | 85 | fn pointerDestroy(listener: *wl.Listener(*wlr.InputDevice), input_device: *wlr.InputDevice) void { 86 | const self = @fieldParentPtr(Self, "pointer_destroy", listener); 87 | log.debug("Signal: wlr_input_device_destroy (pointer)", .{}); 88 | 89 | if (std.mem.indexOfScalar(*Self, self.server.cursors.items, self)) |i| { 90 | const cursor = self.server.cursors.swapRemove(i); 91 | allocator.destroy(cursor); 92 | } 93 | 94 | server.input_manager.setSeatCapabilities(); 95 | server.wlr_cursor.detachInputDevice(input_device); 96 | } 97 | 98 | // NOTE: Do we need anything else here? 99 | pub fn handleAxis(listener: *wl.Listener(*wlr.Pointer.event.Axis), event: *wlr.Pointer.event.Axis) void { 100 | const self = @fieldParentPtr(Self, "axis", listener); 101 | log.debug("Signal: wlr_pointer_axis", .{}); 102 | 103 | self.server.seat.wlr_seat.pointerNotifyAxis( 104 | event.time_msec, 105 | event.orientation, 106 | event.delta, 107 | event.delta_discrete, 108 | event.source, 109 | ); 110 | } 111 | 112 | // TODO: Handle custom button bindings. 113 | pub fn handleButton(listener: *wl.Listener(*wlr.Pointer.event.Button), event: *wlr.Pointer.event.Button) void { 114 | const self = @fieldParentPtr(Self, "button", listener); 115 | log.debug("Signal: wlr_pointer_button", .{}); 116 | 117 | _ = self.server.seat.wlr_seat.pointerNotifyButton(event.time_msec, event.button, event.state); 118 | 119 | if (Window.windowAt(self.server.wlr_cursor.x, self.server.wlr_cursor.y)) |window_data| { 120 | self.server.seat.setFocus(window_data.window, window_data.surface); 121 | } 122 | } 123 | 124 | pub fn handleFrame(listener: *wl.Listener(*wlr.Cursor), _: *wlr.Cursor) void { 125 | const self = @fieldParentPtr(Self, "frame", listener); 126 | log.debug("Signal: wlr_cursor_frame", .{}); 127 | 128 | self.server.seat.wlr_seat.pointerNotifyFrame(); 129 | } 130 | 131 | //TODO: Finish this. 132 | pub fn handleMotion(listener: *wl.Listener(*wlr.Pointer.event.Motion), event: *wlr.Pointer.event.Motion) void { 133 | const self = @fieldParentPtr(Self, "motion", listener); 134 | log.debug("Signal: wlr_cursor_motion", .{}); 135 | 136 | self.server.wlr_cursor.move(event.device, event.delta_x, event.delta_y); 137 | self.processCursorMotion(event.time_msec); 138 | } 139 | 140 | pub fn handleMotionAbsolute(listener: *wl.Listener(*wlr.Pointer.event.MotionAbsolute), event: *wlr.Pointer.event.MotionAbsolute) void { 141 | const self = @fieldParentPtr(Self, "motion_absolute", listener); 142 | log.debug("Signal: wlr_cursor_motion_absolute", .{}); 143 | 144 | self.server.wlr_cursor.warpAbsolute(event.device, event.x, event.y); 145 | self.processCursorMotion(event.time_msec); 146 | } 147 | 148 | pub fn processCursorMotion(self: *Self, time_msec: u32) void { 149 | 150 | //TODO: Handle cursor modes like move, resize, etc 151 | // for now we do very basic handling of a passthrough mode. 152 | 153 | // Passthrough mode. 154 | if (Window.windowAt(self.server.wlr_cursor.x, self.server.wlr_cursor.y)) |window_data| { 155 | self.server.seat.wlr_seat.pointerNotifyEnter(window_data.surface, window_data.sx, window_data.sy); 156 | self.server.seat.wlr_seat.pointerNotifyMotion(time_msec, window_data.sx, window_data.sy); 157 | 158 | if (self.server.config.focus_is_sloppy) { 159 | self.server.seat.setFocus(window_data.window, window_data.surface); 160 | } 161 | } else { 162 | self.server.wlr_xcursor_manager.setCursorImage("left_ptr", self.server.wlr_cursor); 163 | self.server.seat.wlr_seat.pointerClearFocus(); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /next/input/InputManager.zig: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | // 3 | // next/input/InputManager.zig 4 | // 5 | // Created by: Aakash Sen Sharma, July 2022 6 | // Copyright: (C) 2022, Aakash Sen Sharma & Contributors 7 | 8 | const Self = @This(); 9 | 10 | const allocator = @import("../utils/allocator.zig").allocator; 11 | const log = std.log.scoped(.InputManager); 12 | const server = &@import("../next.zig").server; 13 | const std = @import("std"); 14 | 15 | const wl = @import("wayland").server.wl; 16 | const wlr = @import("wlroots"); 17 | 18 | const Server = @import("../Server.zig"); 19 | const Cursor = @import("Cursor.zig"); 20 | const Keyboard = @import("Keyboard.zig"); 21 | 22 | server: *Server, 23 | 24 | wlr_idle: *wlr.Idle, 25 | wlr_input_inhibit_manager: *wlr.InputInhibitManager, 26 | wlr_pointer_constraints: *wlr.PointerConstraintsV1, 27 | wlr_relative_pointer_manager: *wlr.RelativePointerManagerV1, 28 | wlr_virtual_pointer_manager: *wlr.VirtualPointerManagerV1, 29 | wlr_virtual_keyboard_manager: *wlr.VirtualKeyboardManagerV1, 30 | 31 | new_input: wl.Listener(*wlr.InputDevice) = wl.Listener(*wlr.InputDevice).init(newInput), 32 | 33 | pub fn init(self: *Self) !void { 34 | self.* = .{ 35 | .server = server, 36 | .wlr_idle = try wlr.Idle.create(server.wl_server), 37 | .wlr_input_inhibit_manager = try wlr.InputInhibitManager.create(server.wl_server), 38 | .wlr_pointer_constraints = try wlr.PointerConstraintsV1.create(server.wl_server), 39 | .wlr_relative_pointer_manager = try wlr.RelativePointerManagerV1.create(server.wl_server), 40 | .wlr_virtual_pointer_manager = try wlr.VirtualPointerManagerV1.create(server.wl_server), 41 | .wlr_virtual_keyboard_manager = try wlr.VirtualKeyboardManagerV1.create(server.wl_server), 42 | }; 43 | 44 | self.server.wlr_backend.events.new_input.add(&self.new_input); 45 | } 46 | 47 | fn newInput(listener: *wl.Listener(*wlr.InputDevice), input_device: *wlr.InputDevice) void { 48 | const self = @fieldParentPtr(Self, "new_input", listener); 49 | log.debug("Signal: wlr_backend_new_input", .{}); 50 | 51 | switch (input_device.type) { 52 | .keyboard => { 53 | const keyboard = allocator.create(Keyboard) catch { 54 | log.debug("Failed to allocate memory", .{}); 55 | return; 56 | }; 57 | errdefer allocator.destroy(keyboard); 58 | 59 | keyboard.init(input_device) catch |err| { 60 | log.err("Failed to initialize keyboard device: {s}", .{@errorName(err)}); 61 | allocator.destroy(keyboard); 62 | return; 63 | }; 64 | }, 65 | .pointer => { 66 | const pointer = allocator.create(Cursor) catch { 67 | log.debug("Failed to allocate memory", .{}); 68 | return; 69 | }; 70 | errdefer allocator.destroy(pointer); 71 | 72 | pointer.init(input_device) catch |err| { 73 | log.err("Failed to initialize cursor device: {s}", .{@errorName(err)}); 74 | allocator.destroy(pointer); 75 | return; 76 | }; 77 | }, 78 | else => { 79 | return; 80 | }, 81 | } 82 | 83 | self.setSeatCapabilities(); 84 | } 85 | 86 | /// Make sure to use this function after making all changes to &server.cursors and &server.keyboards array-lists. 87 | pub fn setSeatCapabilities(self: *Self) void { 88 | const has_keyboard = (self.server.keyboards.items.len > 0); 89 | const has_pointer = (self.server.cursors.items.len > 0); 90 | 91 | log.debug("Setting seat capabilities: Pointer->{} Keyboard->{}", .{ has_pointer, has_keyboard }); 92 | self.server.seat.wlr_seat.setCapabilities(.{ 93 | .pointer = has_pointer, 94 | .keyboard = has_keyboard, 95 | }); 96 | } 97 | 98 | pub fn hideCursor(self: *Self) void { 99 | //TODO: Check if any buttons are currently pressed then don't hide the cursor. 100 | // if (self.pressed_count > 0) return; 101 | // self.hidden = true; 102 | //TODO: Check rivers implementation. 103 | self.server.wlr_cursor.setImage(null, 0, 0, 0, 0, 0, 0); 104 | self.server.seat.wlr_seat.pointerNotifyClearFocus(); 105 | } 106 | -------------------------------------------------------------------------------- /next/input/Keyboard.zig: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | // 3 | // next/input/Keyboard.zig 4 | // 5 | // Created by: Aakash Sen Sharma, July 2022 6 | // Copyright: (C) 2022, Aakash Sen Sharma & Contributors 7 | 8 | const Self = @This(); 9 | 10 | const allocator = @import("../utils/allocator.zig").allocator; 11 | const log = std.log.scoped(.Keyboard); 12 | const server = &@import("../next.zig").server; 13 | const std = @import("std"); 14 | 15 | const wl = @import("wayland").server.wl; 16 | const wlr = @import("wlroots"); 17 | const xkb = @import("xkbcommon"); 18 | 19 | const Server = @import("../Server.zig"); 20 | 21 | server: *Server, 22 | 23 | wlr_input_device: *wlr.InputDevice, 24 | wlr_keyboard: *wlr.Keyboard, 25 | 26 | key: wl.Listener(*wlr.Keyboard.event.Key) = wl.Listener(*wlr.Keyboard.event.Key).init(handleKey), 27 | modifiers: wl.Listener(*wlr.Keyboard) = wl.Listener(*wlr.Keyboard).init(handleModifiers), 28 | 29 | destroy: wl.Listener(*wlr.InputDevice) = wl.Listener(*wlr.InputDevice).init(handleDestroy), 30 | 31 | pub fn init(self: *Self, device: *wlr.InputDevice) !void { 32 | log.debug("Initializing keyboard device", .{}); 33 | self.* = .{ 34 | .server = server, 35 | .wlr_input_device = device, 36 | .wlr_keyboard = device.toKeyboard(), 37 | }; 38 | self.server.keyboards.append(allocator, self) catch { 39 | log.err("Failed to allocate memory", .{}); 40 | return; 41 | }; 42 | 43 | const context = xkb.Context.new(.no_flags) orelse return error.XkbContextNewFailed; 44 | defer context.unref(); 45 | 46 | const keymap = xkb.Keymap.newFromNames(context, null, .no_flags) orelse return error.XkbKeymapCreationFailed; 47 | defer keymap.unref(); 48 | 49 | _ = self.wlr_keyboard.setKeymap(keymap); 50 | self.wlr_keyboard.setRepeatInfo(self.server.config.repeat_rate, self.server.config.repeat_delay); 51 | 52 | self.server.seat.wlr_seat.setKeyboard(self.wlr_keyboard); 53 | 54 | self.wlr_keyboard.events.key.add(&self.key); 55 | self.wlr_keyboard.events.modifiers.add(&self.modifiers); 56 | self.wlr_input_device.events.destroy.add(&self.destroy); 57 | } 58 | 59 | fn handleKey(listener: *wl.Listener(*wlr.Keyboard.event.Key), event: *wlr.Keyboard.event.Key) void { 60 | const self = @fieldParentPtr(Self, "key", listener); 61 | log.debug("Signal: wlr_keyboard_key", .{}); 62 | 63 | self.server.input_manager.wlr_idle.notifyActivity(self.server.seat.wlr_seat); 64 | 65 | // Translate libinput keycode -> xkbcommon 66 | const keycode = event.keycode + 8; 67 | //const modifiers = self.wlr_keyboard.getModifiers(); 68 | 69 | const xkb_state = self.wlr_keyboard.xkb_state orelse return; 70 | const keysyms = xkb_state.keyGetSyms(keycode); 71 | 72 | for (keysyms) |sym| { 73 | // Check if the sym is a modifier. 74 | if (!(@intFromEnum(sym) >= xkb.Keysym.Shift_L and @intFromEnum(sym) <= xkb.Keysym.Hyper_R)) { 75 | //TODO: Hide while typing. 76 | } 77 | } 78 | 79 | for (keysyms) |sym| { 80 | if (!(event.state == .released) and handleCompositorBindings(sym)) return; 81 | } 82 | 83 | self.server.seat.wlr_seat.setKeyboard(self.wlr_input_device.toKeyboard()); 84 | self.server.seat.wlr_seat.keyboardNotifyKey(event.time_msec, event.keycode, event.state); 85 | } 86 | 87 | fn handleModifiers(listener: *wl.Listener(*wlr.Keyboard), _: *wlr.Keyboard) void { 88 | const self = @fieldParentPtr(Self, "modifiers", listener); 89 | const wlr_keyboard = self.wlr_input_device.toKeyboard(); 90 | 91 | self.server.seat.wlr_seat.setKeyboard(self.wlr_input_device.toKeyboard()); 92 | self.server.seat.wlr_seat.keyboardNotifyModifiers(&wlr_keyboard.modifiers); 93 | } 94 | 95 | fn handleDestroy(listener: *wl.Listener(*wlr.InputDevice), _: *wlr.InputDevice) void { 96 | const self = @fieldParentPtr(Self, "destroy", listener); 97 | log.debug("Signal: wlr_input_device_destroy (keyboard)", .{}); 98 | 99 | if (std.mem.indexOfScalar(*Self, self.server.keyboards.items, self)) |i| { 100 | const keyboard = self.server.keyboards.swapRemove(i); 101 | allocator.destroy(keyboard); 102 | } 103 | 104 | server.input_manager.setSeatCapabilities(); 105 | } 106 | 107 | fn handleCompositorBindings(keysym: xkb.Keysym) bool { 108 | switch (@intFromEnum(keysym)) { 109 | xkb.Keysym.XF86Switch_VT_1...xkb.Keysym.XF86Switch_VT_12 => { 110 | if (server.wlr_backend.isMulti()) { 111 | if (server.wlr_backend.getSession()) |session| { 112 | session.changeVt(@intFromEnum(keysym) - xkb.Keysym.XF86Switch_VT_1 + 1) catch { 113 | log.err("Failed to switch VT.", .{}); 114 | }; 115 | } 116 | } 117 | return true; 118 | }, 119 | xkb.Keysym.Escape => { 120 | server.wl_server.terminate(); 121 | return true; 122 | }, 123 | xkb.Keysym.Delete => { 124 | var output: ?[]const u8 = null; 125 | @import("../control/spawn.zig").spawnCmd( 126 | &[_][:0]const u8{ "", "alacritty" }, 127 | &output, 128 | ) catch {}; 129 | if (output) |out| { 130 | log.err("Spawn command returned: {s}", .{out}); 131 | allocator.free(out); 132 | } 133 | 134 | return true; 135 | }, 136 | else => return false, 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /next/input/Seat.zig: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | // 3 | // next/input/Seat.zig 4 | // 5 | // Created by: Aakash Sen Sharma, July 2022 6 | // Copyright: (C) 2022, Aakash Sen Sharma & Contributors 7 | 8 | const Self = @This(); 9 | 10 | const std = @import("std"); 11 | const allocator = @import("../utils/allocator.zig").allocator; 12 | const log = std.log.scoped(.Seat); 13 | 14 | const wl = @import("wayland").server.wl; 15 | const wlr = @import("wlroots"); 16 | const server = &@import("../next.zig").server; 17 | const Server = @import("../Server.zig"); 18 | const Window = @import("../desktop/Window.zig"); 19 | const Output = @import("../desktop/Output.zig"); 20 | 21 | const default_seat_name: [*:0]const u8 = "next-seat0"; 22 | const FocusTarget = union(enum) { 23 | window: *Window, 24 | none: void, 25 | }; 26 | 27 | server: *Server, 28 | wlr_seat: *wlr.Seat, 29 | 30 | // Flag to denote any on-going pointer-drags. 31 | pointer_drag: bool = false, 32 | 33 | // Currently focused wl_output 34 | focused_output: ?*Output = null, 35 | 36 | // Currently focused window. 37 | focused_window: FocusTarget = .none, 38 | 39 | request_set_selection: wl.Listener(*wlr.Seat.event.RequestSetSelection) = wl.Listener(*wlr.Seat.event.RequestSetSelection).init(requestSetSelection), 40 | request_set_primary_selection: wl.Listener(*wlr.Seat.event.RequestSetPrimarySelection) = wl.Listener(*wlr.Seat.event.RequestSetPrimarySelection).init(requestSetPrimarySelection), 41 | request_start_drag: wl.Listener(*wlr.Seat.event.RequestStartDrag) = wl.Listener(*wlr.Seat.event.RequestStartDrag).init(requestStartDrag), 42 | 43 | request_set_cursor: wl.Listener(*wlr.Seat.event.RequestSetCursor) = wl.Listener(*wlr.Seat.event.RequestSetCursor).init(requestSetCursor), 44 | 45 | pub fn init(self: *Self) !void { 46 | const seat = try wlr.Seat.create(server.wl_server, default_seat_name); 47 | self.* = .{ 48 | .wlr_seat = seat, 49 | .server = server, 50 | }; 51 | 52 | self.wlr_seat.events.request_set_cursor.add(&self.request_set_cursor); 53 | self.wlr_seat.events.request_set_primary_selection.add(&self.request_set_primary_selection); 54 | self.wlr_seat.events.request_set_selection.add(&self.request_set_selection); 55 | } 56 | 57 | pub fn deinit(self: *Self) void { 58 | self.wlr_seat.destroy(); 59 | } 60 | 61 | // Callback that gets triggered when the server seat wants to set a selection. 62 | pub fn requestSetSelection(listener: *wl.Listener(*wlr.Seat.event.RequestSetSelection), event: *wlr.Seat.event.RequestSetSelection) void { 63 | const self = @fieldParentPtr(Self, "request_set_selection", listener); 64 | log.debug("Signal: wlr_seat_request_set_selection", .{}); 65 | 66 | self.wlr_seat.setSelection(event.source, event.serial); 67 | } 68 | 69 | // Callback that gets triggered when the server seat wants to set the primary selection. 70 | pub fn requestSetPrimarySelection(listener: *wl.Listener(*wlr.Seat.event.RequestSetPrimarySelection), event: *wlr.Seat.event.RequestSetPrimarySelection) void { 71 | const self = @fieldParentPtr(Self, "request_set_primary_selection", listener); 72 | log.debug("Signal: wlr_seat_request_set_primary_selection", .{}); 73 | 74 | self.wlr_seat.setPrimarySelection(event.source, event.serial); 75 | } 76 | 77 | pub fn requestStartDrag(listener: *wl.Listener(*wlr.Seat.event.RequestStartDrag), event: *wlr.Seat.event.RequestStartDrag) void { 78 | const self = @fieldParentPtr(Self, "request_start_drag", listener); 79 | log.debug("Signal: wlr_seat_request_start_drag", .{}); 80 | 81 | if (!self.wlr_seat.validatePointerGrabSerial(event.origin, event.serial)) { 82 | log.err("Failed to validate pointer serial {}", .{event.serial}); 83 | if (event.drag.source) |source| source.destroy(); 84 | return; 85 | } 86 | 87 | if (self.pointer_drag) { 88 | log.debug("Ignoring drag request, another pointer drag is currently in progress", .{}); 89 | return; 90 | } 91 | 92 | log.debug("Starting pointer drag", .{}); 93 | self.wlr_seat.startPointerDrag(event.drag, event.serial); 94 | } 95 | 96 | pub fn setFocus(self: *Self, window: *Window, surface: *wlr.Surface) void { 97 | //TODO: On switching tty's keyboards are destroyed and hence leave the surface, when we switch back to the compositor, the keyboard is recreated 98 | //TODO: and events are sent apart from enter event, which is a violation of wayland protocol, so fix that. 99 | //TODO: https://github.com/riverwm/river/commit/d4b2f2b0fc5766c8ae14a6f42fe76d058bfb3505 100 | // If currently focused surface is a layer then we don't want other apps to get the focus :) 101 | // 102 | // if (self.focused == .layer) return; 103 | 104 | switch (self.focused_window) { 105 | .window => |focused_window| { 106 | if (focused_window == window) return; 107 | }, 108 | else => {}, 109 | } 110 | 111 | if (window.output != self.focused_output) self.focusOutput(window.output); 112 | if (self.wlr_seat.keyboard_state.focused_surface) |prev_surface| { 113 | if (prev_surface == surface) return; 114 | if (prev_surface.isXdgSurface()) { 115 | const xdg_surface = wlr.XdgSurface.fromWlrSurface(prev_surface) orelse return; 116 | _ = xdg_surface.role_data.toplevel.setActivated(false); 117 | } 118 | } 119 | 120 | switch (window.backend) { 121 | .xdg_toplevel => |xdg_toplevel| { 122 | xdg_toplevel.scene_tree.node.raiseToTop(); 123 | _ = xdg_toplevel.xdg_surface.role_data.toplevel.setActivated(true); 124 | }, 125 | } 126 | 127 | const wlr_keyboard = self.wlr_seat.getKeyboard() orelse return; 128 | self.wlr_seat.keyboardNotifyEnter( 129 | surface, 130 | &wlr_keyboard.keycodes, 131 | wlr_keyboard.num_keycodes, 132 | &wlr_keyboard.modifiers, 133 | ); 134 | } 135 | 136 | pub fn focusOutput(self: *Self, output: *Output) void { 137 | if (self.focused_output) |focused_output| { 138 | if (focused_output == output) { 139 | log.err("Attempted to focus on already focused_output. Skipping.", .{}); 140 | return; 141 | } 142 | } 143 | self.focused_output = output; 144 | log.debug("Focusing on output.", .{}); 145 | // TODO: finish this. 146 | 147 | if (self.server.config.warp_cursor == .@"on-output-change") { 148 | var layout_box: wlr.Box = undefined; 149 | self.server.output_layout.wlr_output_layout.getBox(self.focused_output.?.wlr_output, &layout_box); 150 | 151 | if (!layout_box.containsPoint(self.server.wlr_cursor.x, self.server.wlr_cursor.y)) { 152 | const geometry = output.getGeometry(); 153 | 154 | const lx = @as(f32, @floatFromInt(layout_box.x + @as(i32, @intCast(geometry.width / 2)))); 155 | const ly = @as(f32, @floatFromInt(layout_box.y + @as(i32, @intCast(geometry.height / 2)))); 156 | if (!self.server.wlr_cursor.warp(null, lx, ly)) { 157 | log.err("Failed to warp cursor on output change", .{}); 158 | } 159 | } 160 | } 161 | } 162 | 163 | // Callback that gets triggered when a client wants to set the cursor image. 164 | pub fn requestSetCursor(listener: *wl.Listener(*wlr.Seat.event.RequestSetCursor), event: *wlr.Seat.event.RequestSetCursor) void { 165 | const self = @fieldParentPtr(Self, "request_set_cursor", listener); 166 | log.debug("Signal: wlr_seat_request_set_cursor", .{}); 167 | 168 | // Check if the client request to set the cursor is the currently focused surface. 169 | const focused_client = self.server.seat.wlr_seat.pointer_state.focused_client; 170 | if (focused_client == event.seat_client) { 171 | log.debug("Focused toplevel set the cursor surface", .{}); 172 | self.server.wlr_cursor.setSurface(event.surface, event.hotspot_x, event.hotspot_y); 173 | } else { 174 | log.debug("Non-focused toplevel attempted to set the cursor surface. Request denied", .{}); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /next/next.zig: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | // 3 | // next/next.zig 4 | // 5 | // Created by: Aakash Sen Sharma, May 2022 6 | // Copyright: (C) 2022, Aakash Sen Sharma & Contributors 7 | 8 | const allocator = @import("utils/allocator.zig").allocator; 9 | const build_options = @import("build_options"); 10 | const c = @import("utils/c.zig"); 11 | const clap = @import("clap"); 12 | const fs = std.fs; 13 | const io = std.io; 14 | const mem = std.mem; 15 | const os = std.os; 16 | const std = @import("std"); 17 | 18 | const wlr = @import("wlroots"); 19 | 20 | const Server = @import("Server.zig"); 21 | const stderr = io.getStdErr().writer(); 22 | const stdout = io.getStdOut().writer(); 23 | 24 | // Server is a public global as we import it in some other files. 25 | pub var server: Server = undefined; 26 | 27 | // Tell std.log to leave log_level filtering to us. 28 | pub const log_level: std.log.Level = .debug; 29 | pub var runtime_log_level: std.log.Level = .err; 30 | 31 | pub fn main() anyerror!void { 32 | const params = comptime clap.parseParamsComptime( 33 | \\-h, --help print this help message and exit. 34 | \\-v, --version print the version number and exit. 35 | \\-c, --command run `sh -c ` on startup. 36 | \\-d, --debug set log level to debug mode. 37 | \\-l, --level set the log level: error, warnings, or info. 38 | ); 39 | 40 | var res = clap.parse(clap.Help, ¶ms, clap.parsers.default, .{}) catch |err| { 41 | try stderr.print("Failed to parse arguments: {s}\n", .{@errorName(err)}); 42 | return; 43 | }; 44 | var args = res.args; 45 | defer res.deinit(); 46 | 47 | if (args.help != 0) { 48 | try stderr.writeAll("Usage: next [options]\n"); 49 | return clap.help(stderr, clap.Help, ¶ms, .{}); 50 | } 51 | 52 | if (args.version != 0) { 53 | try stdout.print("Next version: {s}\n", .{build_options.version}); 54 | return; 55 | } 56 | 57 | if (args.debug != 0) { 58 | runtime_log_level = .debug; 59 | } 60 | 61 | if (args.level) |level| { 62 | if (mem.eql(u8, level, std.log.Level.err.asText())) { 63 | runtime_log_level = .err; 64 | } else if (mem.eql(u8, level, std.log.Level.warn.asText())) { 65 | runtime_log_level = .warn; 66 | } else if (mem.eql(u8, level, std.log.Level.info.asText())) { 67 | runtime_log_level = .info; 68 | } else if (mem.eql(u8, level, std.log.Level.debug.asText())) { 69 | runtime_log_level = .debug; 70 | } else { 71 | std.log.err("Invalid log level '{s}'", .{level}); 72 | return; 73 | } 74 | } 75 | 76 | const startup_command = blk: { 77 | if (args.command) |command| { 78 | break :blk try allocator.dupeZ(u8, command); 79 | } else { 80 | if (os.getenv("XDG_CONFIG_HOME")) |xdg_config_home| { 81 | break :blk try fs.path.joinZ(allocator, &.{ xdg_config_home, "next/nextrc" }); 82 | } else if (os.getenv("HOME")) |home| { 83 | break :blk try fs.path.joinZ(allocator, &.{ home, ".config/next/nextrc" }); 84 | } else { 85 | return; 86 | } 87 | } 88 | }; 89 | defer allocator.free(startup_command); 90 | 91 | // X_OK -> executable bit. 92 | os.accessZ(startup_command, os.X_OK) catch |err| { 93 | if (err == error.PermissionDenied) { 94 | // R_OK -> readable bit 95 | if (os.accessZ(startup_command, os.R_OK)) { 96 | // If the file is readable but cannot be executed then it must not have the execution bit set. 97 | std.log.err("Failed to run nextrc file: {s}\nPlease mark the file executable with the following command:\n chmod +x {s}", .{ startup_command, startup_command }); 98 | return; 99 | } else |_| {} 100 | std.log.err("Failed to run nextrc file: {s}\n{s}", .{ startup_command, @errorName(err) }); 101 | return; 102 | } 103 | }; 104 | 105 | // Wayland requires XDG_RUNTIME_DIR to be set in order for proper functioning. 106 | if (os.getenv("XDG_RUNTIME_DIR") == null) { 107 | std.log.err("XDG_RUNTIME_DIR has not been set.", .{}); 108 | return; 109 | } 110 | 111 | // TODO: Remove this entirely when zig gets good var-arg support. 112 | wlr_fmt_log(switch (runtime_log_level) { 113 | .debug => .debug, 114 | .info => .info, 115 | .warn, .err => .err, 116 | }); 117 | 118 | // Ignore SIGPIPE so the compositor doesn't get killed when attempting to write to a read-end-closed socket. 119 | // TODO: Remove this handler entirely: https://github.com/ziglang/zig/pull/11982 120 | const sig_ign = os.Sigaction{ 121 | .handler = .{ .handler = os.SIG.IGN }, 122 | .mask = os.empty_sigset, 123 | .flags = 0, 124 | }; 125 | try os.sigaction(os.SIG.PIPE, &sig_ign, null); 126 | 127 | std.log.scoped(.Next).info("Initializing server", .{}); 128 | try server.init(); 129 | defer server.deinit(); 130 | 131 | try server.start(); 132 | 133 | const pid = try os.fork(); 134 | if (pid == 0) { 135 | var errno = os.errno(c.setsid()); 136 | if (@intFromEnum(errno) != 0) { 137 | std.log.err("Setsid syscall failed: {any}", .{errno}); 138 | } 139 | 140 | // SET_MASK sets the blocked signal to an empty signal set in this case. 141 | errno = os.errno(os.system.sigprocmask(os.SIG.SETMASK, &os.empty_sigset, null)); 142 | if (@intFromEnum(errno) != 0) { 143 | std.log.err("Sigprocmask syscall failed: {any}", .{errno}); 144 | } 145 | 146 | // Setting default handler for sigpipe. 147 | const sig_dfl = os.Sigaction{ 148 | .handler = .{ .handler = os.SIG.DFL }, 149 | .mask = os.empty_sigset, 150 | .flags = 0, 151 | }; 152 | try os.sigaction(os.SIG.PIPE, &sig_dfl, null); 153 | 154 | // NOTE: it's convention for the first element in the argument vector to be same as the invoking binary. 155 | // Read https://man7.org/linux/man-pages/man2/execve.2.html for more info. 156 | const c_argv = [_:null]?[*:0]const u8{ "/bin/sh", "-c", startup_command, null }; 157 | os.execveZ("/bin/sh", &c_argv, std.c.environ) catch os.exit(1); 158 | } 159 | 160 | // Sending sigterm to a negative pid traverses down it's child list and sends sigterm to each of them. 161 | // Read https://man7.org/linux/man-pages/man2/kill.2.html for more info. 162 | defer os.kill(-pid, os.SIG.TERM) catch |err| { 163 | std.log.err("Failed to kill init-child: {d} {s}", .{ pid, @errorName(err) }); 164 | }; 165 | 166 | // Run the server! 167 | std.log.info("Running NextWM event loop", .{}); 168 | server.wl_server.run(); 169 | } 170 | 171 | extern fn wlr_fmt_log(importance: wlr.log.Importance) void; 172 | export fn wlr_log_callback(importance: wlr.log.Importance, ptr: [*:0]const u8, len: usize) void { 173 | switch (importance) { 174 | .err => log(.err, .Wlroots, "{s}", .{ptr[0..len]}), 175 | .info => log(.info, .Wlroots, "{s}", .{ptr[0..len]}), 176 | .debug => log(.debug, .Wlroots, "{s}", .{ptr[0..len]}), 177 | .silent, .last => unreachable, 178 | } 179 | } 180 | 181 | // Custom logging function. 182 | pub fn log( 183 | comptime level: std.log.Level, 184 | comptime scope: @TypeOf(.EnumLiteral), 185 | comptime format: []const u8, 186 | args: anytype, 187 | ) void { 188 | if (@intFromEnum(level) > @intFromEnum(runtime_log_level)) return; 189 | 190 | const level_txt = comptime toUpper(level.asText()); 191 | const scope_txt = "[" ++ @tagName(scope) ++ "] "; 192 | 193 | stderr.print(scope_txt ++ "(" ++ level_txt ++ ") " ++ format ++ "\n", args) catch {}; 194 | } 195 | 196 | fn toUpper(comptime string: []const u8) *const [string.len:0]u8 { 197 | comptime { 198 | var tmp: [string.len:0]u8 = undefined; 199 | for (&tmp, 0..) |*char, i| char.* = std.ascii.toUpper(string[i]); 200 | return &tmp; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /next/utils/allocator.zig: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | // 3 | // next/utils/allocator.zig 4 | // 5 | // Created by: Aakash Sen Sharma, July 2022 6 | // Copyright: (C) 2022, Aakash Sen Sharma & Contributors 7 | 8 | const std = @import("std"); 9 | 10 | // Global allocator to be used across the project. 11 | pub const allocator = std.heap.c_allocator; 12 | -------------------------------------------------------------------------------- /next/utils/c.zig: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | // 3 | // next/utils/c.zig 4 | // 5 | // Created by: Aakash Sen Sharma, May 2022 6 | // Copyright: (C) 2022, Aakash Sen Sharma & Contributors 7 | 8 | pub usingnamespace @cImport({ 9 | @cDefine("_POSIX_C_SOURCE", "200809L"); 10 | @cInclude("stdlib.h"); 11 | @cInclude("unistd.h"); 12 | @cInclude("libinput.h"); 13 | @cInclude("cairo.h"); 14 | @cInclude("stdio.h"); 15 | @cInclude("jpeglib.h"); 16 | @cInclude("drm_fourcc.h"); 17 | }); 18 | -------------------------------------------------------------------------------- /next/utils/wlr_log.c: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | // 3 | // next/utils/wlr_log.c 4 | // 5 | // Created by: Aakash Sen Sharma, August 2022 6 | // Copyright: (C) 2022, Aakash Sen Sharma & Contributors 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | 14 | #define BUFFER_SIZE 1024 15 | 16 | void wlr_log_callback(enum wlr_log_importance importance, const char *ptr, size_t len); 17 | static void callback(enum wlr_log_importance importance, const char *fmt, va_list args) { 18 | char buffer[BUFFER_SIZE]; 19 | 20 | va_list args_copy; 21 | va_copy(args_copy, args); 22 | 23 | const int length = vsnprintf(buffer, BUFFER_SIZE, fmt, args); 24 | 25 | if (length + 1 <= BUFFER_SIZE) { 26 | wlr_log_callback(importance, buffer, length); 27 | } else { 28 | char *allocated_buffer = malloc(length + 1); 29 | if (allocated_buffer != NULL) { 30 | const int length2 = vsnprintf(allocated_buffer, length + 1, fmt, args_copy); 31 | assert(length2 == length); 32 | wlr_log_callback(importance, allocated_buffer, length); 33 | free(allocated_buffer); 34 | } 35 | } 36 | 37 | va_end(args_copy); 38 | } 39 | 40 | void wlr_fmt_log(enum wlr_log_importance importance) { 41 | wlr_log_init(importance, callback); 42 | } 43 | -------------------------------------------------------------------------------- /nextctl-go/Makefile: -------------------------------------------------------------------------------- 1 | BINARY:=nextctl 2 | FMT_REQUIRED:=$(shell gofmt -l $(shell find . -type f -iname *.go)) 3 | BUILD_FLAGS:=-ldflags "-s -w" 4 | 5 | all: build 6 | 7 | build: 8 | go mod tidy 9 | $(MAKE) generate -s 10 | go build $(BUILD_FLAGS) ./cmd/$(BINARY)/ 11 | 12 | generate: 13 | go generate -x ./... 14 | 15 | check: 16 | $(MAKE) generate -s 17 | @echo $(FMT_REQUIRED) 18 | @test -z $(FMT_REQUIRED) 19 | go vet ./... 20 | 21 | clean: 22 | go clean 23 | $(RM) -f $(BINARY) 24 | $(RM) -f pkg/next_control/next_control_v1.go 25 | 26 | .PHONY: all build clean check 27 | -------------------------------------------------------------------------------- /nextctl-go/cmd/nextctl/nextctl.go: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: BSD 2-Clause "Simplified" License 3 | * 4 | * nextctl.go 5 | * 6 | * Created by: Aakash Sen Sharma, December 2022 7 | * Copyright: (C) 2022, Aakash Sen Sharma & Contributors 8 | */ 9 | 10 | package main 11 | 12 | import ( 13 | "fmt" 14 | "os" 15 | 16 | . "git.sr.ht/~shinyzenith/nextctl/pkg/next_control" 17 | "github.com/rajveermalviya/go-wayland/wayland/client" 18 | ) 19 | 20 | const EXIT_FAILURE = 1 21 | const EXIT_SUCCESS = 0 22 | const VERSION = "0.1.0-dev" 23 | 24 | type Nextctl struct { 25 | display *client.Display 26 | registry *client.Registry 27 | next_control *NextControlV1 28 | } 29 | 30 | const usage = `Usage: nextctl 31 | -h, --help Print this help message and exit. 32 | 33 | -v, --version Print the version number and exit. 34 | 35 | Complete documentation for recognized commands can be found in 36 | the nextctl(1) man page.` 37 | 38 | func main() { 39 | for _, arg := range os.Args[1:] { 40 | if arg == "-h" || arg == "--help" { 41 | fmt.Fprintln(os.Stderr, usage) 42 | os.Exit(0) 43 | } else if arg == "-v" || arg == "--version" { 44 | fmt.Fprintln(os.Stderr, "Nextctl version: ", VERSION) 45 | os.Exit(0) 46 | } 47 | } 48 | 49 | Nextctl := &Nextctl{display: nil, registry: nil, next_control: nil} 50 | 51 | if display, err := client.Connect(""); err != nil { 52 | fmt.Fprintln(os.Stderr, "ERROR: Cannot connect to wayland display.") 53 | os.Exit(EXIT_FAILURE) 54 | } else { 55 | Nextctl.display = display 56 | } 57 | 58 | Nextctl.registry, _ = Nextctl.display.GetRegistry() 59 | Nextctl.registry.SetGlobalHandler(Nextctl.GlobalHandler) 60 | 61 | Nextctl.DisplayDispatch() 62 | 63 | if Nextctl.next_control == nil { 64 | fmt.Fprintln(os.Stderr, "ERROR: Compositor doesn't implement NextControlV1.") 65 | os.Exit(EXIT_FAILURE) 66 | } 67 | 68 | for _, arg := range os.Args[1:] { 69 | _ = Nextctl.next_control.AddArgument(arg) 70 | } 71 | callback, _ := Nextctl.next_control.RunCommand() 72 | callback.SetSuccessHandler(Nextctl.NextSuccessHandler) 73 | callback.SetFailureHandler(Nextctl.NextFailureHandler) 74 | Nextctl.DisplayDispatch() 75 | 76 | os.Exit(EXIT_SUCCESS) 77 | } 78 | 79 | func (Nextctl *Nextctl) DisplayDispatch() { 80 | callback, err := Nextctl.display.Sync() 81 | if err != nil { 82 | fmt.Fprintln(os.Stderr, "ERROR: wayland dispatch failed.") 83 | os.Exit(EXIT_FAILURE) 84 | } 85 | defer func() { 86 | if err := callback.Destroy(); err != nil { 87 | fmt.Fprintln(os.Stderr, "ERROR: wayland dispatch failed.") 88 | os.Exit(EXIT_FAILURE) 89 | } 90 | }() 91 | 92 | callback_done := false 93 | callback.SetDoneHandler(func(_ client.CallbackDoneEvent) { 94 | callback_done = true 95 | }) 96 | 97 | for !callback_done { 98 | Nextctl.display.Context().Dispatch() 99 | } 100 | } 101 | 102 | func (Nextctl *Nextctl) GlobalHandler(wl_global client.RegistryGlobalEvent) { 103 | if wl_global.Interface == "next_control_v1" { 104 | next_control := NewNextControlV1(Nextctl.display.Context()) 105 | err := Nextctl.registry.Bind(wl_global.Name, wl_global.Interface, wl_global.Version, next_control) 106 | if err != nil { 107 | fmt.Fprintln(os.Stderr, "ERROR: wayland dispatch failed.") 108 | os.Exit(EXIT_FAILURE) 109 | } 110 | Nextctl.next_control = next_control 111 | } 112 | } 113 | 114 | func (Nextctl *Nextctl) NextSuccessHandler(success_event NextCommandCallbackV1SuccessEvent) { 115 | fmt.Fprintln(os.Stdout, success_event.Output) 116 | } 117 | 118 | func (Nextctl *Nextctl) NextFailureHandler(failure_event NextCommandCallbackV1FailureEvent) { 119 | fmt.Fprint(os.Stderr, "ERROR: ", failure_event.FailureMessage) 120 | if failure_event.FailureMessage == "Unknown command\n" || failure_event.FailureMessage == "No command provided\n" { 121 | fmt.Fprintln(os.Stderr, usage) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /nextctl-go/go.mod: -------------------------------------------------------------------------------- 1 | module git.sr.ht/~shinyzenith/nextctl 2 | 3 | go 1.18 4 | 5 | require github.com/rajveermalviya/go-wayland/wayland v0.0.0-20230130181619-0ad78d1310b2 6 | 7 | require golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 // indirect 8 | -------------------------------------------------------------------------------- /nextctl-go/go.sum: -------------------------------------------------------------------------------- 1 | github.com/rajveermalviya/go-wayland/wayland v0.0.0-20230130181619-0ad78d1310b2 h1:tRhbehjSCwQSZL7A2AoZlKrDYhZzaPIAcpnhfaUc0Tw= 2 | github.com/rajveermalviya/go-wayland/wayland v0.0.0-20230130181619-0ad78d1310b2/go.mod h1:PXhW/GoWcMBeiZ39ZdgoMs/xduJEEUE+kxUBB2Kwd+M= 3 | golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 h1:kwrAHlwJ0DUBZwQ238v+Uod/3eZ8B2K5rYsUHBQvzmI= 4 | golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 5 | -------------------------------------------------------------------------------- /nextctl-go/pkg/next_control/gen.go: -------------------------------------------------------------------------------- 1 | package next_control_v1 2 | 3 | //go:generate go run github.com/rajveermalviya/go-wayland/cmd/go-wayland-scanner@latest -i ../../../protocols/next-control-v1.xml -o next_control_v1.go -pkg next_control_v1 4 | -------------------------------------------------------------------------------- /nextctl-rs/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "autocfg" 7 | version = "1.1.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 10 | 11 | [[package]] 12 | name = "bitflags" 13 | version = "1.3.2" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 16 | 17 | [[package]] 18 | name = "cc" 19 | version = "1.0.73" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" 22 | 23 | [[package]] 24 | name = "cfg-if" 25 | version = "1.0.0" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 28 | 29 | [[package]] 30 | name = "downcast-rs" 31 | version = "1.2.0" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" 34 | 35 | [[package]] 36 | name = "libc" 37 | version = "0.2.126" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" 40 | 41 | [[package]] 42 | name = "memoffset" 43 | version = "0.6.5" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" 46 | dependencies = [ 47 | "autocfg", 48 | ] 49 | 50 | [[package]] 51 | name = "nextctl-rs" 52 | version = "0.1.0-dev" 53 | dependencies = [ 54 | "wayland-client", 55 | "wayland-commons", 56 | "wayland-scanner", 57 | ] 58 | 59 | [[package]] 60 | name = "nix" 61 | version = "0.22.3" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "e4916f159ed8e5de0082076562152a76b7a1f64a01fd9d1e0fea002c37624faf" 64 | dependencies = [ 65 | "bitflags", 66 | "cc", 67 | "cfg-if", 68 | "libc", 69 | "memoffset", 70 | ] 71 | 72 | [[package]] 73 | name = "once_cell" 74 | version = "1.13.0" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" 77 | 78 | [[package]] 79 | name = "pkg-config" 80 | version = "0.3.25" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" 83 | 84 | [[package]] 85 | name = "proc-macro2" 86 | version = "1.0.42" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "c278e965f1d8cf32d6e0e96de3d3e79712178ae67986d9cf9151f51e95aac89b" 89 | dependencies = [ 90 | "unicode-ident", 91 | ] 92 | 93 | [[package]] 94 | name = "quote" 95 | version = "1.0.20" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" 98 | dependencies = [ 99 | "proc-macro2", 100 | ] 101 | 102 | [[package]] 103 | name = "smallvec" 104 | version = "1.9.0" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" 107 | 108 | [[package]] 109 | name = "unicode-ident" 110 | version = "1.0.2" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" 113 | 114 | [[package]] 115 | name = "wayland-client" 116 | version = "0.29.4" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "91223460e73257f697d9e23d401279123d36039a3f7a449e983f123292d4458f" 119 | dependencies = [ 120 | "bitflags", 121 | "downcast-rs", 122 | "libc", 123 | "nix", 124 | "wayland-commons", 125 | "wayland-scanner", 126 | "wayland-sys", 127 | ] 128 | 129 | [[package]] 130 | name = "wayland-commons" 131 | version = "0.29.4" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "94f6e5e340d7c13490eca867898c4cec5af56c27a5ffe5c80c6fc4708e22d33e" 134 | dependencies = [ 135 | "nix", 136 | "once_cell", 137 | "smallvec", 138 | "wayland-sys", 139 | ] 140 | 141 | [[package]] 142 | name = "wayland-scanner" 143 | version = "0.29.4" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "39a1ed3143f7a143187156a2ab52742e89dac33245ba505c17224df48939f9e0" 146 | dependencies = [ 147 | "proc-macro2", 148 | "quote", 149 | "xml-rs", 150 | ] 151 | 152 | [[package]] 153 | name = "wayland-sys" 154 | version = "0.29.4" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "d9341df79a8975679188e37dab3889bfa57c44ac2cb6da166f519a81cbe452d4" 157 | dependencies = [ 158 | "pkg-config", 159 | ] 160 | 161 | [[package]] 162 | name = "xml-rs" 163 | version = "0.8.4" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" 166 | -------------------------------------------------------------------------------- /nextctl-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | description = "Command line tool to control NextWM" 3 | name = "nextctl-rs" 4 | version = "0.1.0-dev" 5 | edition = "2021" 6 | authors = [ "Shinyzenith ", ] 7 | 8 | [build-dependencies] 9 | wayland-scanner = "0.29.4" 10 | 11 | [dependencies] 12 | wayland-client = "0.29.4" 13 | wayland-commons = "0.29.4" 14 | 15 | [[bin]] 16 | name = "nextctl" 17 | path = "src/main.rs" 18 | -------------------------------------------------------------------------------- /nextctl-rs/Makefile: -------------------------------------------------------------------------------- 1 | BUILD_FLAGS = --release 2 | 3 | all: build 4 | 5 | build: 6 | cargo build $(BUILD_FLAGS) 7 | 8 | check: 9 | cargo check 10 | cargo fmt -- --check 11 | 12 | clean: 13 | cargo clean 14 | 15 | .PHONY: all build protocols clean check 16 | -------------------------------------------------------------------------------- /nextctl-rs/build.rs: -------------------------------------------------------------------------------- 1 | extern crate wayland_scanner; 2 | use std::path::Path; 3 | 4 | pub fn main() { 5 | let out_dir = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/src/wayland/")); 6 | wayland_scanner::generate_code( 7 | "../protocols/next-control-v1.xml", 8 | out_dir.join("next_control_v1.rs"), 9 | wayland_scanner::Side::Client, 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /nextctl-rs/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{env, error::Error, process::exit}; 2 | use wayland_client::{Display, GlobalManager}; 3 | 4 | mod wayland; 5 | use crate::wayland::next_control_v1::next_command_callback_v1::Event; 6 | use crate::wayland::next_control_v1::next_control_v1::NextControlV1; 7 | 8 | fn main() -> Result<(), Box> { 9 | let mut args = env::args(); 10 | args.next(); 11 | for flag in args { 12 | match flag.as_str() { 13 | "-h" | "--help" => { 14 | print_usage(); 15 | exit(0); 16 | } 17 | "-v" | "--version" => { 18 | eprintln!(env!("CARGO_PKG_VERSION")); 19 | exit(0) 20 | } 21 | _ => {} 22 | } 23 | } 24 | let display: Display = match Display::connect_to_env() { 25 | Ok(x) => x, 26 | Err(_) => { 27 | eprintln!("ERROR: Cannot connect to wayland display."); 28 | exit(1); 29 | } 30 | }; 31 | let mut event_queue = display.create_event_queue(); 32 | let attached_display = display.attach(event_queue.token()); 33 | 34 | let globals = GlobalManager::new(&attached_display); 35 | if event_queue 36 | .sync_roundtrip(&mut (), |_, _, _| unreachable!()) 37 | .is_err() 38 | { 39 | eprintln!("ERROR: wayland dispatch failed."); 40 | exit(1); 41 | }; 42 | 43 | let next_control = match globals.instantiate_exact::(1) { 44 | Ok(x) => x, 45 | Err(_) => { 46 | eprintln!("ERROR: Compositor doesn't implement NextControlV1."); 47 | exit(1); 48 | } 49 | }; 50 | 51 | { 52 | let mut args = env::args(); 53 | args.next(); 54 | for flag in args { 55 | next_control.add_argument(flag.to_string()); 56 | } 57 | } 58 | 59 | let command_callback = next_control.run_command(); 60 | command_callback.quick_assign({ 61 | move |_, event, _| match event { 62 | Event::Success { output } => { 63 | println!("{}", output); 64 | } 65 | Event::Failure { failure_message } => { 66 | eprintln!("ERROR: {}", failure_message); 67 | match failure_message.as_str() { 68 | "Unknown command\n" | "No command provided\n" => print_usage(), 69 | _ => {} 70 | } 71 | } 72 | } 73 | }); 74 | 75 | if event_queue 76 | .sync_roundtrip(&mut (), |_, _, _| unreachable!()) 77 | .is_err() 78 | { 79 | eprintln!("ERROR: wayland dispatch failed."); 80 | exit(1); 81 | }; 82 | Ok(()) 83 | } 84 | 85 | fn print_usage() { 86 | eprintln!("Usage: nextctl "); 87 | eprintln!(" -h, --help Print this help message and exit."); 88 | eprintln!(); 89 | eprintln!(" -v, --version Print the version number and exit."); 90 | eprintln!(); 91 | eprintln!("Complete documentation for recognized commands can be found in"); 92 | eprintln!("the nextctl(1) man page."); 93 | } 94 | -------------------------------------------------------------------------------- /nextctl-rs/src/wayland/mod.rs: -------------------------------------------------------------------------------- 1 | extern crate wayland_client; 2 | extern crate wayland_commons; 3 | 4 | pub use wayland::client as next_control_v1; 5 | 6 | pub mod wayland { 7 | #![allow(dead_code, non_camel_case_types, unused_unsafe, unused_variables)] 8 | #![allow(non_upper_case_globals, non_snake_case, unused_imports)] 9 | 10 | pub mod client { 11 | pub(crate) use wayland_client::{ 12 | sys, 13 | sys::common::{wl_argument, wl_array, wl_interface, wl_message}, 14 | }; 15 | pub(crate) use wayland_client::{AnonymousObject, Main, Proxy, ProxyMap}; 16 | pub(crate) use wayland_commons::map::{Object, ObjectMetadata}; 17 | pub(crate) use wayland_commons::smallvec; 18 | pub(crate) use wayland_commons::wire::{Argument, ArgumentType, Message, MessageDesc}; 19 | pub(crate) use wayland_commons::{Interface, MessageGroup}; 20 | include!(concat!( 21 | env!("CARGO_MANIFEST_DIR"), 22 | "/src/wayland/next_control_v1.rs" 23 | )); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /nextctl.zig: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | // 3 | // nextctl.zig 4 | // 5 | // Created by: Aakash Sen Sharma, October 2023 6 | // Copyright: (C) 2022, Aakash Sen Sharma & Contributors 7 | 8 | const Self = @This(); 9 | const std = @import("std"); 10 | 11 | pub const BuildType = enum { 12 | c, 13 | go, 14 | rust, 15 | }; 16 | 17 | step: std.build.Step, 18 | build_type: BuildType, 19 | version: []const u8, 20 | 21 | pub fn init(builder: *std.Build, build_type: BuildType, version: []const u8) !*Self { 22 | const self = try builder.allocator.create(Self); 23 | self.* = .{ 24 | .step = std.build.Step.init(.{ 25 | .id = .custom, 26 | .name = "Build nextctl", 27 | .makeFn = &make, 28 | .owner = builder, 29 | }), 30 | .build_type = build_type, 31 | .version = version, 32 | }; 33 | 34 | return self; 35 | } 36 | 37 | fn make(step: *std.build.Step, _: *std.Progress.Node) anyerror!void { 38 | const self = @fieldParentPtr(Self, "step", step); 39 | const builder = self.step.owner; 40 | 41 | switch (self.build_type) { 42 | .c => { 43 | try syncVersion(builder.allocator, "#define VERSION ", "nextctl/include/nextctl.h", self.version); 44 | _ = builder.exec(&.{ "make", "-C", "nextctl" }); 45 | }, 46 | .rust => { 47 | try syncVersion(builder.allocator, "version = ", "nextctl-rs/Cargo.toml", self.version); 48 | _ = builder.exec(&.{ "make", "-C", "nextctl-rs" }); 49 | }, 50 | .go => { 51 | try syncVersion(builder.allocator, "const VERSION = ", "nextctl-go/cmd/nextctl/nextctl.go", self.version); 52 | _ = builder.exec(&.{ "make", "-C", "nextctl-go" }); 53 | }, 54 | } 55 | } 56 | 57 | pub fn install(self: *Self) !void { 58 | const builder = self.step.owner; 59 | const install_nextctl = blk: { 60 | switch (self.build_type) { 61 | .c => { 62 | break :blk builder.addInstallFile(.{ .path = "./nextctl/zig-out/bin/nextctl" }, "bin/nextctl"); 63 | }, 64 | .rust => { 65 | break :blk builder.addInstallFile(.{ .path = "./nextctl-rs/target/release/nextctl" }, "bin/nextctl"); 66 | }, 67 | .go => { 68 | break :blk builder.addInstallFile(.{ .path = "./nextctl-go/nextctl" }, "bin/nextctl"); 69 | }, 70 | } 71 | }; 72 | 73 | install_nextctl.step.dependOn(&self.step); 74 | builder.getInstallStep().dependOn(&install_nextctl.step); 75 | } 76 | 77 | fn syncVersion(allocator: std.mem.Allocator, needle: []const u8, file_name: []const u8, new_version: []const u8) !void { 78 | const file = try std.fs.cwd().openFile(file_name, .{}); 79 | defer file.close(); 80 | 81 | const file_size = (try file.stat()).size; 82 | const file_buffer = try file.readToEndAlloc(allocator, file_size); 83 | 84 | const start_index = std.mem.indexOfPos(u8, file_buffer, 0, needle).? + needle.len; 85 | const end_index = std.mem.indexOfPos(u8, file_buffer, start_index + 1, "\"").? + 1; 86 | const old_version = file_buffer[start_index..end_index]; 87 | 88 | const old_version_str = try std.fmt.allocPrint(allocator, "{s}{s}\n", .{ needle, old_version }); 89 | const new_version_str = try std.fmt.allocPrint(allocator, "{s}\"{s}\"\n", .{ needle, new_version }); 90 | const replaced_str = try std.mem.replaceOwned(u8, allocator, file_buffer, old_version_str, new_version_str); 91 | 92 | try std.fs.cwd().writeFile(file_name, replaced_str); 93 | } 94 | -------------------------------------------------------------------------------- /nextctl/Makefile: -------------------------------------------------------------------------------- 1 | BINARY:=nextctl 2 | BUILD_FLAGS:= 3 | 4 | all: build 5 | 6 | build: 7 | zig build $(BUILD_FLAGS) 8 | 9 | check: 10 | $(MAKE) clean -s 11 | zig fmt --check *.zig 12 | clang-format -style=file --dry-run --Werror src/*.c 13 | clang-format -style=file --dry-run --Werror include/*.h 14 | 15 | clean: 16 | $(RM) -r zig-cache zig-out 17 | $(RM) ./include/next-control-v1.h 18 | $(RM) ./src/next-control-v1.c 19 | 20 | .PHONY: all build clean check 21 | -------------------------------------------------------------------------------- /nextctl/build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(builder: *std.Build) !void { 4 | const protocols_path = "protocols"; 5 | const include_path = "include"; 6 | const src_path = "src"; 7 | 8 | const target = builder.standardTargetOptions(.{}); 9 | const optimize = builder.standardOptimizeOption(.{}); 10 | 11 | try generateProtocolFiles(builder, protocols_path, include_path, src_path); 12 | 13 | var src_files = try getFiles(builder, src_path, .file); 14 | defer deinitFilesList(builder, &src_files); 15 | 16 | const c_flags = &.{ 17 | "-Wall", 18 | "-Wsign-conversion", 19 | "-Wunused-result", 20 | "-Wconversion", 21 | "-Wextra", 22 | "-Wfloat-conversion", 23 | "-Wformat", 24 | "-Wformat-security", 25 | "-Wno-keyword-macro", 26 | "-Wno-missing-field-initializers", 27 | "-Wno-narrowing", 28 | "-Wno-unused-parameter", 29 | "-Wno-unused-value", 30 | "-Wpedantic", 31 | "-std=c18", 32 | "-O3", 33 | }; 34 | 35 | const exe = builder.addExecutable(.{ 36 | .name = "nextctl", 37 | .target = target, 38 | .optimize = optimize, 39 | }); 40 | exe.addCSourceFiles(src_files.items, c_flags); 41 | exe.addIncludePath(.{ .path = include_path }); 42 | 43 | exe.linkLibC(); 44 | exe.linkSystemLibrary("wayland-client"); 45 | 46 | builder.installArtifact(exe); 47 | } 48 | 49 | fn getFiles(builder: *std.Build, dir_path: []const u8, file_kind: std.fs.File.Kind) !std.ArrayList([]const u8) { 50 | var files = std.ArrayList([]const u8).init(builder.allocator); 51 | var dir = try std.fs.cwd().openIterableDir(dir_path, .{ 52 | .access_sub_paths = true, 53 | }); 54 | 55 | var iterator = dir.iterate(); 56 | while (try iterator.next()) |file| { 57 | if (file.kind == file_kind) { 58 | try files.append(try std.fmt.allocPrint(builder.allocator, "{s}/{s}", .{ dir_path, file.name })); 59 | } 60 | } 61 | 62 | return files; 63 | } 64 | 65 | fn deinitFilesList(builder: *std.Build, files: *std.ArrayList([]const u8)) void { 66 | for (files.items) |file| { 67 | builder.allocator.free(file); 68 | } 69 | files.deinit(); 70 | } 71 | 72 | fn generateProtocolFiles(builder: *std.Build, protocols_path: []const u8, include_path: []const u8, src_path: []const u8) !void { 73 | var xml_files = try getFiles(builder, protocols_path, .sym_link); 74 | defer deinitFilesList(builder, &xml_files); 75 | 76 | for (xml_files.items) |xml_file| { 77 | // .h generation 78 | { 79 | const header_file_name = try std.mem.replaceOwned(u8, builder.allocator, xml_file, ".xml", ".h"); 80 | defer builder.allocator.free(header_file_name); 81 | 82 | const header_file = try std.mem.replaceOwned(u8, builder.allocator, header_file_name, protocols_path, include_path); 83 | defer builder.allocator.free(header_file); 84 | 85 | _ = builder.exec(&.{ "wayland-scanner", "client-header", xml_file, header_file }); 86 | } 87 | 88 | // .c generation 89 | { 90 | const src_file_name = try std.mem.replaceOwned(u8, builder.allocator, xml_file, ".xml", ".c"); 91 | defer builder.allocator.free(src_file_name); 92 | 93 | const src_file = try std.mem.replaceOwned(u8, builder.allocator, src_file_name, protocols_path, src_path); 94 | defer builder.allocator.free(src_file); 95 | 96 | _ = builder.exec(&.{ "wayland-scanner", "private-code", xml_file, src_file }); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /nextctl/compile_flags.txt: -------------------------------------------------------------------------------- 1 | -I./include 2 | -------------------------------------------------------------------------------- /nextctl/include/nextctl.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | * 3 | * nextctl/nextctl.h 4 | * 5 | * Created by: Aakash Sen Sharma, May 2022 6 | * Copyright: (C) 2022, Aakash Sen Sharma & Contributors 7 | */ 8 | 9 | #pragma once 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #define VERSION "0.1.0-dev" 16 | #include "next-control-v1.h" 17 | 18 | const char usage[] = "Usage: nextctl \n" 19 | " -h, --help Print this help message and exit.\n\n" 20 | " -v, --version Print the version number and exit.\n\n" 21 | "Complete documentation for recognized commands can be found in\n" 22 | "the nextctl(1) man page.\n"; 23 | 24 | static void noop() { 25 | } 26 | static void registry_handle_global(void *, struct wl_registry *, uint32_t, const char *, 27 | uint32_t); 28 | static void next_handle_success(void *, struct next_command_callback_v1 *, const char *); 29 | static void next_handle_failure(void *, struct next_command_callback_v1 *, const char *); 30 | 31 | struct nextctl_state { 32 | struct wl_display *wl_display; 33 | struct wl_registry *wl_registry; 34 | struct next_control_v1 *next_control; 35 | struct next_command_callback_v1 *next_command_callback; 36 | }; 37 | 38 | static const struct wl_registry_listener registry_listener = { 39 | .global = registry_handle_global, 40 | .global_remove = noop, 41 | }; 42 | 43 | static const struct next_command_callback_v1_listener callback_listener = { 44 | .success = next_handle_success, 45 | .failure = next_handle_failure, 46 | }; 47 | -------------------------------------------------------------------------------- /nextctl/protocols/next-control-v1.xml: -------------------------------------------------------------------------------- 1 | ../../protocols/next-control-v1.xml -------------------------------------------------------------------------------- /nextctl/src/nextctl.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | * 3 | * nextctl/nextctl.c 4 | * 5 | * Created by: Aakash Sen Sharma, May 2022 6 | * Copyright: (C) 2022, Aakash Sen Sharma & Contributors 7 | */ 8 | 9 | #include "nextctl.h" 10 | 11 | int main(int argc, char *argv[]) { 12 | for (int i = 0; i < argc; i++) { 13 | if (strcmp("-h", argv[i]) == 0 || strcmp("--help", argv[i]) == 0) { 14 | (void)fputs(usage, stderr); 15 | return EXIT_SUCCESS; 16 | } else if (strcmp("-v", argv[i]) == 0 || strcmp("--version", argv[i]) == 0) { 17 | (void)printf("Nextctl version: %s\n", VERSION); 18 | return EXIT_SUCCESS; 19 | } 20 | } 21 | 22 | struct nextctl_state state = { 0 }; 23 | state.wl_display = wl_display_connect(NULL); 24 | 25 | if (state.wl_display == NULL) { 26 | (void)fputs("ERROR: Cannot connect to wayland display.\n", stderr); 27 | return EXIT_FAILURE; 28 | } 29 | 30 | state.wl_registry = wl_display_get_registry(state.wl_display); 31 | (void)wl_registry_add_listener(state.wl_registry, ®istry_listener, &state); 32 | 33 | if (wl_display_dispatch(state.wl_display) < 0) { 34 | (void)fputs("ERROR: wayland dispatch failed.\n", stderr); 35 | return EXIT_FAILURE; 36 | } 37 | 38 | if (state.next_control == NULL) { 39 | (void)fputs("ERROR: Compositor doesn't implement NextControlV1.\n", stderr); 40 | return EXIT_FAILURE; 41 | } 42 | 43 | for (int i = 1; i < argc; i++) { 44 | (void)next_control_v1_add_argument(state.next_control, argv[i]); 45 | } 46 | 47 | state.next_command_callback = next_control_v1_run_command(state.next_control); 48 | (void)next_command_callback_v1_add_listener(state.next_command_callback, 49 | &callback_listener, NULL); 50 | 51 | if (wl_display_dispatch(state.wl_display) < 0) { 52 | (void)fputs("ERROR: wayland dispatch failed.\n", stderr); 53 | return EXIT_FAILURE; 54 | } 55 | 56 | return EXIT_SUCCESS; 57 | } 58 | 59 | static void registry_handle_global(void *data, struct wl_registry *registry, 60 | uint32_t name, const char *interface, 61 | uint32_t version) { 62 | struct nextctl_state *state = data; 63 | 64 | if (strcmp(interface, next_control_v1_interface.name) == 0) { 65 | state->next_control = 66 | wl_registry_bind(registry, name, &next_control_v1_interface, 1); 67 | } 68 | } 69 | 70 | static void next_handle_success(void *data, struct next_command_callback_v1 *callback, 71 | const char *output) { 72 | (void)fputs(output, stdout); 73 | } 74 | 75 | static void next_handle_failure(void *data, struct next_command_callback_v1 *callback, 76 | const char *failure_message) { 77 | (void)fprintf(stderr, "ERROR: %s", failure_message); 78 | if (strcmp("Unknown command\n\0", failure_message) == 0 || 79 | strcmp("No command provided\n\0", failure_message) == 0) { 80 | (void)fputs(usage, stderr); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /protocols/next-control-v1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright 2022 Aakash Sen Sharma 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | 18 | 19 | 20 | 21 | This interface allows clients to run compositor commands and receive a 22 | success/failure response with output or a failure message respectively. 23 | 24 | 25 | 26 | 27 | This request indicates that the client will not use the 28 | next_control object any more. Objects that have been created 29 | through this instance are not affected. 30 | 31 | 32 | 33 | 34 | 35 | Arguments are stored by the server in the order they were sent until 36 | the run_command request is made. 37 | 38 | 39 | 40 | 41 | 42 | 43 | Execute the command built up using the add_argument request for the 44 | default seat. 45 | 46 | 48 | 49 | 50 | 51 | 52 | 53 | This object is created by the run_command request. Exactly one of the 54 | success or failure events will be sent. This object will be destroyed 55 | by the compositor after one of the events is sent. 56 | 57 | 58 | 59 | 60 | Sent when the command has been successfully received and executed by 61 | the compositor. Some commands may produce output, in which case the 62 | output argument will be a non-empty string. 63 | 64 | 65 | 66 | 67 | 68 | 69 | Sent when the command could not be carried out. This could be due to 70 | sending a non-existent command, no command, not enough arguments, too 71 | many arguments, invalid arguments, etc. 72 | 73 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | -------------------------------------------------------------------------------- /scdoc.zig: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD 2-Clause "Simplified" License 2 | // 3 | // scdoc.zig 4 | // 5 | // Created by: Aakash Sen Sharma, September 2022 6 | // Copyright: (C) 2022, Aakash Sen Sharma & Contributors 7 | 8 | const std = @import("std"); 9 | 10 | pub fn build(builder: *std.build.Builder, docs_dir: []const u8) !void { 11 | var dir = try std.fs.cwd().openIterableDir(docs_dir, .{ 12 | .access_sub_paths = true, 13 | }); 14 | defer dir.close(); 15 | 16 | //TODO: https://github.com/ziglang/zig/blob/master/lib/std/compress/gzip.zig Gzip the man-pages properly 17 | 18 | var iterator = dir.iterate(); 19 | while (try iterator.next()) |entry| { 20 | if (entry.kind == .file) { 21 | if (std.mem.lastIndexOfScalar(u8, entry.name, '.')) |idx| { 22 | if (std.mem.eql(u8, entry.name[idx..], ".scd")) { 23 | const p = try std.fmt.allocPrint(builder.allocator, "{s}{s}", .{ docs_dir, entry.name }); 24 | const path = try std.fmt.allocPrint(builder.allocator, "{s}.gz", .{p[0..(p.len - 4)]}); 25 | 26 | const path_no_ext = path[0..(path.len - 3)]; 27 | const section = path_no_ext[(path_no_ext.len - 1)..]; 28 | 29 | const output = try std.fmt.allocPrint( 30 | builder.allocator, 31 | "share/man/man{s}/{s}", 32 | .{ section, std.fs.path.basename(path) }, 33 | ); 34 | 35 | const cmd = try std.fmt.allocPrint( 36 | builder.allocator, 37 | "scdoc < {s} > {s}", 38 | .{ p, path }, 39 | ); 40 | 41 | _ = builder.exec(&.{ "sh", "-c", cmd }); 42 | builder.installFile(path, output); 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /scenefx.nix: -------------------------------------------------------------------------------- 1 | { lib, stdenv, fetchFromGitHub, meson, ninja, cmake, pkg-config, wlroots_0_16 2 | , wayland, libdrm, libxkbcommon, udev, pixman, wayland-protocols, libGL, mesa, 3 | }: 4 | stdenv.mkDerivation rec { 5 | pname = "scenefx"; 6 | version = "unstable-2023-08-06"; 7 | 8 | src = fetchFromGitHub { 9 | owner = "wlrfx"; 10 | repo = "scenefx"; 11 | rev = "b929a2bbadf467864796ad4ec90882ce86cfebff"; 12 | hash = "sha256-c/zRWz6njC3RsHzIcWpd5m7CXGprrIhKENpaQVH7Owk="; 13 | }; 14 | 15 | nativeBuildInputs = [ wlroots_0_16 meson ninja cmake pkg-config ]; 16 | 17 | buildInputs = 18 | [ wayland libdrm libxkbcommon udev pixman wayland-protocols libGL mesa ]; 19 | 20 | meta = with lib; { 21 | description = 22 | "A drop-in replacement for the wlroots scene API that allows wayland compositors to render surfaces with eye-candy effects"; 23 | homepage = "https://github.com/wlrfx/scenefx/"; 24 | license = licenses.mit; 25 | maintainers = with maintainers; [ arjan-s ]; 26 | mainProgram = "scenefx"; 27 | platforms = platforms.all; 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /sniff.json: -------------------------------------------------------------------------------- 1 | { 2 | "zig": [ 3 | "make" 4 | ], 5 | "c": { 6 | "commands": [ 7 | "make" 8 | ], 9 | "relative_dir": "./nextctl" 10 | }, 11 | "rs": { 12 | "commands": [ 13 | "cargo clippy", 14 | "cargo build --release" 15 | ], 16 | "relative_dir": "./nextctl-rs" 17 | }, 18 | "sniff_ignore_dir": [ 19 | "zig-out", 20 | "zig-cache", 21 | ".git", 22 | "target" 23 | ], 24 | "sniff_ignore_file": [], 25 | "sniff_cooldown": 650 26 | } 27 | --------------------------------------------------------------------------------