├── .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 |
--------------------------------------------------------------------------------