├── .editorconfig ├── .flake8 ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── config.yml └── workflows │ └── ci.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── libnext ├── backend.py ├── control.py ├── inputs.py ├── layout_manager.py ├── libinput_ffi_build.py ├── outputs.py ├── util.py └── window.py ├── next ├── next.1.scd ├── protocols ├── next-control-v1.xml └── river-layout-v3.xml ├── requirements-optional.txt ├── requirements.txt ├── setup.py └── tox.ini /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = lf 4 | indent_size = 4 5 | indent_style = space 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E501, I001, I005 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: "Report an issue" 2 | description: "NextWM unconfirmed bugs." 3 | labels: ["unconfirmed"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Please include: 9 | - Your NextWM version (`next --version`). 10 | - Relevant **logs** if any. 11 | - type: textarea 12 | attributes: 13 | label: "The issue:" 14 | value: I wanted to do X, but Y happened, and I expected Z. I think this is a bug. 15 | validations: 16 | render: markdown 17 | - type: checkboxes 18 | attributes: 19 | label: "Required:" 20 | options: 21 | - label: I have searched past issues to see if this bug has already been reported. 22 | required: true 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Question 4 | about: Ask a question 5 | url: https://matrix.to/#/#herbwm:matrix.org 6 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-22.04 12 | name: "python 3.10" 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up python 3.10 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: 3.10.4 19 | - name: Install dependencies 20 | run: | 21 | sudo apt update 22 | sudo apt install make libinput-dev --no-install-recommends -y 23 | pip -q install "tox<4" tox-gh-actions 24 | - name: Lint 25 | run: | 26 | make lint 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *_libinput.* 2 | *flycheck* 3 | .dmypy.json 4 | .eggs/ 5 | .env 6 | .mypy_cache/ 7 | .mypy_cache/ 8 | .tox/ 9 | .venv 10 | .vim/ 11 | __pycache__/ 12 | dmypy.json 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to NextWM 2 | 3 | 1. Fork the repo and create your branch from `master`. 4 | 1. Make sure to add type definitions for all your declarations. 5 | 6 | ## Any contributions you make will be under the BSD-2-Clause Software License 7 | 8 | In short, when you submit code changes, your submissions are understood to be under the same BSD-2-Clause License that covers the project. 9 | Feel free to contact the maintainers if that's a concern. 10 | 11 | ## Use a Consistent Coding Style 12 | 13 | - 4 spaces for indentation. 14 | - Consistent variable and function names. 15 | 16 | ## Proper Commit Messages 17 | 18 | Make sure to write proper commit messages, sign your commits with gpg, and use `-s` flag while running git commit. 19 | 20 | Example: 21 | 22 | ``` 23 | [update] libnext.backend.py, xwayland init 24 | 25 | * Basic Xwayland setup. 26 | 27 | * Xwayland surface handler listeners registered. 28 | 29 | Signed-off-by: Shinyzenith 30 | ``` 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Shinyzenith & Contributors 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BINARY=next 2 | 3 | all: dev 4 | 5 | dev: 6 | @./$(BINARY) -d 7 | 8 | clean: 9 | @rm -rf ./libnext/_libinput.* 10 | @rm -rf **/**/__pycache__ 11 | @rm -rf **/__pycache__ 12 | @rm -rf .tox 13 | @rm -rf .eggs 14 | @rm -rf .mypy_cache 15 | 16 | setup: 17 | @sudo python3 -m pip install -U -r ./requirements.txt 18 | @sudo python3 -m pip install -U -r ./requirements-optional.txt 19 | @python3 ./libnext/libinput_ffi_build.py 20 | 21 | lint: 22 | @TOXENV=codestyle,flake,black,mypy,py310 tox 23 | 24 | .PHONY: run clean lint 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NextWM 2 | 3 | WIP Wayland compositing window manager. 4 | -------------------------------------------------------------------------------- /libnext/backend.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 Shinyzenith 2 | # 3 | # Redistribution and use in source and binary forms, with or without 4 | # modification, are permitted provided that the following conditions are met: 5 | # 6 | # 1. Redistributions of source code must retain the above copyright notice, 7 | # this list of conditions and the following disclaimer. 8 | # 9 | # 2. Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # 13 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 14 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 15 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 16 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 17 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 18 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 19 | # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 20 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 21 | # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 22 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 23 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | import logging 26 | import os 27 | import signal 28 | from typing import Any 29 | 30 | from pywayland.protocol.wayland import WlSeat 31 | from pywayland.server import Display, Listener 32 | from pywayland.server.eventloop import EventSource 33 | from wlroots import helper as wlroots_helper 34 | from wlroots import xwayland 35 | from wlroots.wlr_types import ( 36 | Cursor, 37 | DataControlManagerV1, 38 | DataDeviceManager, 39 | ExportDmabufManagerV1, 40 | ForeignToplevelManagerV1, 41 | GammaControlManagerV1, 42 | Output, 43 | OutputLayout, 44 | PrimarySelectionV1DeviceManager, 45 | Scene, 46 | ScreencopyManagerV1, 47 | Surface, 48 | XCursorManager, 49 | XdgOutputManagerV1, 50 | seat, 51 | xdg_decoration_v1, 52 | ) 53 | from wlroots.wlr_types.cursor import WarpMode 54 | from wlroots.wlr_types.idle import Idle 55 | from wlroots.wlr_types.idle_inhibit_v1 import IdleInhibitorManagerV1 56 | from wlroots.wlr_types.input_device import InputDevice, InputDeviceType 57 | from wlroots.wlr_types.layer_shell_v1 import LayerShellV1, LayerSurfaceV1 58 | from wlroots.wlr_types.output_management_v1 import OutputManagerV1 59 | from wlroots.wlr_types.output_power_management_v1 import OutputPowerManagerV1 60 | from wlroots.wlr_types.pointer import ( 61 | PointerEventAxis, 62 | PointerEventButton, 63 | PointerEventMotion, 64 | PointerEventMotionAbsolute, 65 | ) 66 | from wlroots.wlr_types.xdg_shell import XdgShell, XdgSurface, XdgSurfaceRole 67 | 68 | from libnext.inputs import NextKeyboard 69 | from libnext.layout_manager import LayoutManager 70 | from libnext.outputs import NextOutput 71 | from libnext.util import Listeners 72 | from libnext.window import WindowType, XdgWindow 73 | 74 | log = logging.getLogger("Next: Backend") 75 | 76 | 77 | class NextCore(Listeners): 78 | def __init__(self) -> None: 79 | """ 80 | Setup nextwm 81 | """ 82 | if os.getenv("XDG_RUNTIME_DIR") is None or os.getenv("XDG_RUNTIME_DIR") == "": 83 | log.error("XDG_RUNTIME_DIR is not set in the environment") 84 | return 85 | 86 | self.display: Display = Display() 87 | self.event_loop = self.display.get_event_loop() 88 | 89 | self.event_loop_callbacks: list[EventSource] = [] 90 | for handled_signal in [ 91 | signal.SIGINT, 92 | signal.SIGTERM, 93 | signal.SIGABRT, 94 | signal.SIGKILL, 95 | signal.SIGQUIT, 96 | ]: 97 | self.event_loop_callbacks.append( 98 | self.event_loop.add_signal( 99 | handled_signal, self.signal_callback, self.display 100 | ) 101 | ) 102 | 103 | ( 104 | self.compositor, 105 | self.allocator, 106 | self.renderer, 107 | self.backend, 108 | ) = wlroots_helper.build_compositor(self.display) 109 | 110 | self.renderer.init_display(self.display) 111 | self.socket = self.display.add_socket() 112 | 113 | os.environ["WAYLAND_DISPLAY"] = self.socket.decode() 114 | log.info(f"WAYLAND_DISPLAY {self.socket.decode()}") 115 | 116 | self.add_listener(self.backend.new_input_event, self._on_new_input) 117 | self.add_listener(self.backend.new_output_event, self._on_new_output) 118 | 119 | # These windows have not been mapped yet. 120 | # They'll get managed when mapped. 121 | self.pending_windows: set[WindowType] = set() 122 | self.mapped_windows: list[WindowType] = [] 123 | 124 | # List of outputs managed by the compositor. 125 | self.outputs: list[NextOutput] = [] 126 | 127 | # Input configuration. 128 | self.keyboards: list[NextKeyboard] = [] 129 | 130 | DataDeviceManager(self.display) 131 | DataControlManagerV1(self.display) 132 | self.seat: seat.Seat = seat.Seat(self.display, "NextWM-Seat0") 133 | self.add_listener( 134 | self.seat.request_set_selection_event, self._on_request_set_selection 135 | ) 136 | self.add_listener( 137 | self.seat.request_set_primary_selection_event, 138 | self._on_request_set_primary_selection, 139 | ) 140 | self.add_listener( 141 | self.seat.request_set_cursor_event, self._on_request_set_cursor 142 | ) 143 | # TODO: Bind more seat listeners 144 | 145 | # Output configuration. 146 | self.output_layout: OutputLayout = OutputLayout() 147 | self.scene: Scene = Scene(self.output_layout) 148 | self.output_manager: OutputManagerV1 = OutputManagerV1(self.display) 149 | self.layout_manager = LayoutManager(self.display) 150 | 151 | # Cursor configuration 152 | self.cursor: Cursor = Cursor(self.output_layout) 153 | 154 | self.cursor_manager: XCursorManager = XCursorManager(24) 155 | self.add_listener(self.cursor.axis_event, self._on_cursor_axis) 156 | self.add_listener(self.cursor.button_event, self._on_cursor_button) 157 | self.add_listener(self.cursor.frame_event, self._on_cursor_frame) 158 | # TODO: On motion check the view under the cursor and focus. 159 | # Or focus on click? NOTE: Use wlroots scene_graph node_at func for this. 160 | self.add_listener(self.cursor.motion_event, self._on_cursor_motion) 161 | self.add_listener( 162 | self.cursor.motion_absolute_event, self._on_cursor_motion_absolute 163 | ) 164 | 165 | # Setup Xdg shell 166 | self.xdg_shell: XdgShell = XdgShell(self.display) 167 | self.add_listener(self.xdg_shell.new_surface_event, self._on_new_xdg_surface) 168 | self.layer_shell: LayerShellV1 = LayerShellV1(self.display) 169 | self.add_listener( 170 | self.layer_shell.new_surface_event, self._on_new_layer_surface 171 | ) 172 | 173 | # Some protocol initialization. 174 | ExportDmabufManagerV1(self.display) 175 | GammaControlManagerV1(self.display) 176 | PrimarySelectionV1DeviceManager(self.display) 177 | ScreencopyManagerV1(self.display) 178 | XdgOutputManagerV1(self.display, self.output_layout) 179 | # idle_inhibitor_manager = IdleInhibitorManagerV1(self.display) 180 | # output_power_manager = OutputPowerManagerV1(self.display) 181 | _ = IdleInhibitorManagerV1(self.display) 182 | _ = OutputPowerManagerV1(self.display) 183 | 184 | self.xdg_decoration_manager_v1 = ( 185 | xdg_decoration_v1.XdgDecorationManagerV1.create(self.display) 186 | ) 187 | self.add_listener( 188 | self.xdg_decoration_manager_v1.new_toplevel_decoration_event, 189 | self._on_new_toplevel_decoration, 190 | ) 191 | 192 | self.idle = Idle(self.display) 193 | self.foreign_toplevel_managerv1 = ForeignToplevelManagerV1.create(self.display) 194 | 195 | # XWayland initialization. 196 | # True -> lazy evaluation. 197 | # A.K.A XWayland is started when a client needs it. 198 | self.xwayland = xwayland.XWayland(self.display, self.compositor, True) 199 | if not self.xwayland: 200 | log.error("Failed to setup XWayland. Continuing without.") 201 | else: 202 | self.xwayland.set_seat(self.seat) 203 | os.environ["DISPLAY"] = self.xwayland.display_name or "" 204 | log.info(f"XWAYLAND DISPLAY {self.xwayland.display_name}") 205 | 206 | self.backend.start() 207 | 208 | # Getting output_layout dimensions and setting the cursor to spawn in the middle of it. 209 | layout_box = self.output_layout.get_box(None) 210 | self.cursor.warp(WarpMode.Layout, layout_box.width / 2, layout_box.height / 2) 211 | 212 | self.display.run() 213 | 214 | # Cleanup 215 | self.destroy() 216 | 217 | def signal_callback(self, sig_num: int, display: Display): 218 | log.info("Terminating event loop") 219 | display.terminate() 220 | 221 | # Resource cleanup. 222 | def destroy(self) -> None: 223 | self.destroy_listeners() 224 | [ 225 | event_loop_callback.remove() 226 | for event_loop_callback in self.event_loop_callbacks 227 | ] 228 | 229 | [keyboard.destroy_listeners() for keyboard in self.keyboards] 230 | 231 | [output.destroy_listeners() for output in self.outputs] 232 | 233 | if self.xwayland: 234 | self.xwayland.destroy() 235 | self.layout_manager.destroy() 236 | self.cursor.destroy() 237 | self.cursor_manager.destroy() 238 | self.output_layout.destroy() 239 | self.seat.destroy() 240 | self.backend.destroy() 241 | self.display.destroy() 242 | log.debug("Server destroyed") 243 | 244 | def focus_window(self, window: WindowType, surface: Surface | None = None) -> None: 245 | if self.seat.destroyed: 246 | return 247 | if surface is None and window is not None: 248 | surface = window.surface.surface 249 | 250 | previous_surface = self.seat.keyboard_state.focused_surface 251 | if previous_surface == surface: 252 | log.error("Focus requested on currently focused surface. Focus unchanged.") 253 | return 254 | 255 | if previous_surface is not None: 256 | if previous_surface.is_xdg_surface: 257 | previous_xdg_surface = XdgSurface.from_surface(previous_surface) 258 | if not window or window.surface != previous_xdg_surface: 259 | previous_xdg_surface.set_activated(False) 260 | if previous_xdg_surface.data: # We store the ftm handle in data. 261 | previous_xdg_surface.data.set_activated(False) 262 | 263 | if previous_surface.is_xwayland_surface: 264 | previous_xwayland_surface = xwayland.Surface.from_wlr_surface( 265 | previous_surface 266 | ) 267 | if not window or window.surface != previous_xwayland_surface: 268 | previous_xwayland_surface.activate(False) 269 | if ( 270 | previous_xwayland_surface.data 271 | ): # We store the ftm handle in data. 272 | previous_xwayland_surface.data.set_activated(False) 273 | 274 | if not window: 275 | self.seat.keyboard_clear_focus() 276 | return 277 | 278 | log.debug("Focusing on surface") 279 | window.scene_node.raise_to_top() 280 | window.surface.set_activated(True) 281 | if window.surface.data: 282 | window.surface.set_activated(True) # Setting ftm_handle to activated_true 283 | 284 | self.seat.keyboard_notify_enter(window.surface.surface, self.seat.keyboard) 285 | 286 | def hide_cursor(self) -> None: 287 | log.debug("Hiding cursor") 288 | # TODO: Finish this. 289 | # XXX:lib.wlr_cursor_set_image(self.cursor._ptr, None, 0, 0, 0, 0, 0, 0) 290 | # XXX:self.cursor.set_cursor_image(None, 0, 0, 0, 0, 0, 0) 291 | log.debug("Clearing pointer focus") 292 | self.seat.pointer_notify_clear_focus() 293 | 294 | # Properties 295 | @property 296 | def wayland_socket_name(self) -> str: 297 | """ 298 | The socket name. 299 | You can access it at /run/user/$(InvokingUserId)/$(socket_name) 300 | """ 301 | return self.socket.decode() 302 | 303 | @property 304 | def xwayland_socket_name(self) -> str: 305 | """ 306 | XWayland socket name. 307 | """ 308 | return self.xwayland.display_name or "" 309 | 310 | # Listeners 311 | def _on_new_input(self, _listener: Listener, device: InputDevice) -> None: 312 | log.debug("Signal: wlr_backend_new_input_event") 313 | match device.device_type: 314 | case InputDeviceType.KEYBOARD: 315 | self.keyboards.append(NextKeyboard(self, device)) 316 | self.seat.set_keyboard(device) 317 | case InputDeviceType.POINTER: 318 | self.cursor.attach_input_device(device) 319 | 320 | # Fetching capabilities 321 | capabilities = WlSeat.capability.pointer 322 | if self.keyboards: 323 | capabilities |= WlSeat.capability.keyboard 324 | 325 | self.seat.set_capabilities(capabilities) 326 | # TODO: Set libinput settings as needed after setting capabilities 327 | 328 | log.debug( 329 | "Device: %s of type %s detected.", 330 | device.name, 331 | device.device_type.name.lower(), 332 | ) 333 | 334 | def _on_new_output(self, _listener: Listener, wlr_output: Output) -> None: 335 | log.debug("Signal: wlr_backend_new_output_event") 336 | 337 | wlr_output.init_render(self.allocator, self.renderer) 338 | 339 | if wlr_output.modes != []: 340 | mode = wlr_output.preferred_mode() 341 | if mode is None: 342 | log.error("New output advertised with no output mode") 343 | else: 344 | wlr_output.set_mode(mode) 345 | wlr_output.enable() 346 | wlr_output.commit() 347 | 348 | NextOutput(self, wlr_output) 349 | 350 | def _on_request_set_selection( 351 | self, _listener: Listener, event: seat.RequestSetSelectionEvent 352 | ) -> None: 353 | log.debug("Signal: wlr_seat_request_set_selection_event") 354 | self.seat.set_selection(event._ptr.source, event.serial) 355 | 356 | def _on_request_set_primary_selection( 357 | self, _listener: Listener, event: seat.RequestSetPrimarySelectionEvent 358 | ) -> None: 359 | log.debug("Signal: wlr_seat_on_request_set_primary_selection_event") 360 | self.seat.set_primary_selection(event._ptr.source, event.serial) 361 | 362 | def _on_request_set_cursor( 363 | self, _listener: Listener, event: seat.PointerRequestSetCursorEvent 364 | ) -> None: 365 | log.debug("Signal: wlr_seat_on_request_set_cursor") 366 | self.cursor.set_surface(event.surface, event.hotspot) 367 | 368 | def _on_cursor_frame(self, _listener: Listener, data: Any) -> None: 369 | log.debug("Signal: wlr_cursor_frame_event") 370 | self.seat.pointer_notify_frame() 371 | 372 | def _on_cursor_motion( 373 | self, _listener: Listener, event_motion: PointerEventMotion 374 | ) -> None: 375 | # TODO: This should get abstracted into it's own function to check if 376 | # image shoud be ptr or resize type. 377 | # TODO: Finish this. 378 | log.debug("Signal: wlr_cursor_motion_event") 379 | self.cursor.move( 380 | event_motion.delta_x, event_motion.delta_y, input_device=event_motion.device 381 | ) 382 | self.cursor_manager.set_cursor_image("left_ptr", self.cursor) 383 | 384 | def _on_cursor_motion_absolute( 385 | self, _listener: Listener, event_motion: PointerEventMotionAbsolute 386 | ) -> None: 387 | log.debug("Signal: wlr_cursor_motion_absolute_event") 388 | self.cursor.warp( 389 | WarpMode.LayoutClosest, 390 | event_motion.x, 391 | event_motion.y, 392 | input_device=event_motion.device, 393 | ) 394 | # TODO: Finish this. 395 | 396 | def _on_cursor_axis(self, _listener: Listener, event: PointerEventAxis) -> None: 397 | self.seat.pointer_notify_axis( 398 | event.time_msec, 399 | event.orientation, 400 | event.delta, 401 | event.delta_discrete, 402 | event.source, 403 | ) 404 | 405 | def _on_cursor_button(self, _listener: Listener, event: PointerEventButton) -> None: 406 | log.debug("Signal: wlr_cursor_button_event") 407 | # TODO: If config wants focus_by_hover then do so, else focus_by_click. 408 | 409 | # NOTE: Maybe support compositor bindings involving buttons? 410 | self.seat.pointer_notify_button( 411 | event.time_msec, event.button, event.button_state 412 | ) 413 | self.idle.notify_activity(self.seat) 414 | log.debug("Cursor button emitted to focused client") 415 | 416 | def _on_new_xdg_surface(self, _listener: Listener, surface: XdgSurface) -> None: 417 | log.debug("Signal: xdg_shell_new_xdg_surface_event") 418 | if surface.role == XdgSurfaceRole.TOPLEVEL: 419 | self.pending_windows.add(XdgWindow(self, surface)) 420 | 421 | def _on_new_layer_surface( 422 | self, _listener: Listener, surface: LayerSurfaceV1 423 | ) -> None: 424 | log.debug("Signal: layer_shell_new_layer_surface_event") 425 | 426 | def _on_new_toplevel_decoration( 427 | self, _listener: Listener, decoration: xdg_decoration_v1.XdgToplevelDecorationV1 428 | ) -> None: 429 | log.debug("Signal: xdg_decoration_v1_new_toplevel_decoration_event") 430 | # TODO: https://github.com/Shinyzenith/NextWM/issues/10 431 | decoration.set_mode(xdg_decoration_v1.XdgToplevelDecorationV1Mode.SERVER_SIDE) 432 | -------------------------------------------------------------------------------- /libnext/control.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 Shinyzenith 2 | # 3 | # Redistribution and use in source and binary forms, with or without 4 | # modification, are permitted provided that the following conditions are met: 5 | # 6 | # 1. Redistributions of source code must retain the above copyright notice, 7 | # this list of conditions and the following disclaimer. 8 | # 9 | # 2. Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # 13 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 14 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 15 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 16 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 17 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 18 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 19 | # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 20 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 21 | # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 22 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 23 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | import logging 26 | 27 | # TODO: Figure out why this breaks mypy. 28 | from pywayland.protocol.next_control_v1 import NextControlV1 # ignore: type 29 | from pywayland.protocol_core.globals import Global 30 | from pywayland.server import Display 31 | 32 | log = logging.getLogger("Next: Control") 33 | 34 | 35 | class Control(Global): 36 | def __init__(self, display: Display) -> None: 37 | self.interface = NextControlV1 38 | super().__init__(display, 1) 39 | log.info("Created next_control_v1 global") 40 | 41 | def destroy(self) -> None: 42 | super().destroy() 43 | -------------------------------------------------------------------------------- /libnext/inputs.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 Shinyzenith 2 | # 3 | # Redistribution and use in source and binary forms, with or without 4 | # modification, are permitted provided that the following conditions are met: 5 | # 6 | # 1. Redistributions of source code must retain the above copyright notice, 7 | # this list of conditions and the following disclaimer. 8 | # 9 | # 2. Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # 13 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 14 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 15 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 16 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 17 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 18 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 19 | # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 20 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 21 | # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 22 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 23 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | import logging 26 | import subprocess 27 | from typing import Any 28 | 29 | from pywayland.protocol.wayland import WlKeyboard 30 | from pywayland.server import Listener 31 | from wlroots import ffi, lib 32 | from wlroots.wlr_types import InputDevice 33 | from wlroots.wlr_types.keyboard import KeyboardKeyEvent, KeyboardModifier 34 | from xkbcommon import xkb 35 | 36 | from libnext.util import Listeners 37 | 38 | log = logging.getLogger("Next: Inputs") 39 | xkb_keysym = ffi.new("const xkb_keysym_t **") 40 | 41 | 42 | class NextKeyboard(Listeners): 43 | def __init__(self, core, device: InputDevice): 44 | self.device = device 45 | self.keyboard = device.keyboard 46 | self.core = core 47 | 48 | # NOTE: https://github.com/Shinyzenith/NextWM/issues/5 49 | self.keyboard.set_repeat_info(100, 300) 50 | self.xkb_context = xkb.Context() 51 | 52 | # TODO: Populate this keymap call later. 53 | self.keymap = self.xkb_context.keymap_new_from_names() 54 | self.keyboard.set_keymap(self.keymap) 55 | 56 | self.add_listener(self.keyboard.destroy_event, self._on_destroy) 57 | self.add_listener(self.keyboard.key_event, self._on_key) 58 | self.add_listener(self.keyboard.modifiers_event, self._on_modifiers) 59 | 60 | def destroy(self) -> None: 61 | self.destroy_listeners() 62 | self.core.keyboards.remove(self) 63 | if self.core.keyboards and self.core.seat.keyboard.destroyed: 64 | self.core.seat.set_keyboard(self.core.keyboards[-1].device) 65 | 66 | # Listeners 67 | def _on_destroy(self, _listener: Listener, _data: Any) -> None: 68 | log.debug("Signal: wlr_keyboard_destroy_event") 69 | self.destroy() 70 | 71 | def _on_key(self, _listener: Listener, key_event: KeyboardKeyEvent) -> None: 72 | log.debug("Signal: wlr_keyboard_key_event") 73 | # TODO: Add option to hide cursor when typing. 74 | # self.core.cursor.hide() -> From river. 75 | 76 | # Translate libinput keycode -> xkbcommon 77 | keycode = key_event.keycode + 8 78 | 79 | layout_index = lib.xkb_state_key_get_layout( 80 | self.keyboard._ptr.xkb_state, keycode 81 | ) 82 | nsyms = lib.xkb_keymap_key_get_syms_by_level( 83 | self.keyboard._ptr.keymap, keycode, layout_index, 0, xkb_keysym 84 | ) 85 | keysyms = [xkb_keysym[0][i] for i in range(nsyms)] 86 | for keysym in keysyms: 87 | # TODO: Support change_vt() 88 | if ( 89 | self.keyboard.modifier == KeyboardModifier.ALT 90 | and key_event.state == WlKeyboard.key_state.pressed # noqa: W503 91 | ): 92 | if keysym == xkb.keysym_from_name("Escape"): 93 | # We don't care for sig_num anyways. 94 | self.core.signal_callback(0, self.core.display) 95 | return 96 | 97 | if keysym == xkb.keysym_from_name("l"): 98 | subprocess.Popen(["alacritty"]) 99 | return 100 | 101 | if keysym == xkb.keysym_from_name("j"): 102 | if len(self.core.mapped_windows) >= 2: 103 | window = self.core.mapped_windows.pop() 104 | self.core.mapped_windows.insert(0, window) 105 | self.core.focus_window(self.core.mapped_windows[-1]) 106 | return 107 | 108 | if keysym == xkb.keysym_from_name("k"): 109 | if len(self.core.mapped_windows) >= 2: 110 | window = self.core.mapped_windows.pop(0) 111 | self.core.mapped_windows.append(window) 112 | self.core.focus_window(self.core.mapped_windows[-1]) 113 | return 114 | 115 | if keysym == xkb.keysym_from_name("q"): 116 | if self.core.mapped_windows: 117 | self.core.mapped_windows[-1].kill() 118 | return 119 | 120 | if keysym == xkb.keysym_from_name("1"): 121 | self.core.backend.get_session().change_vt(1) 122 | return 123 | 124 | log.debug("Key emitted to focused client") 125 | self.core.seat.set_keyboard(self.device) 126 | self.core.seat.keyboard_notify_key(key_event) 127 | 128 | def _on_modifiers(self, _listener: Listener, _data: Any): 129 | log.debug("Signal: wlr_keyboard_modifiers_event") 130 | self.core.seat.set_keyboard(self.device) 131 | self.core.seat.keyboard_notify_modifiers(self.keyboard.modifiers) 132 | -------------------------------------------------------------------------------- /libnext/layout_manager.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 Shinyzenith 2 | # 3 | # Redistribution and use in source and binary forms, with or without 4 | # modification, are permitted provided that the following conditions are met: 5 | # 6 | # 1. Redistributions of source code must retain the above copyright notice, 7 | # this list of conditions and the following disclaimer. 8 | # 9 | # 2. Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # 13 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 14 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 15 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 16 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 17 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 18 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 19 | # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 20 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 21 | # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 22 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 23 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | import logging 26 | 27 | # TODO: Figure out why this breaks mypy. 28 | from pywayland.protocol.river_layout_v3 import RiverLayoutManagerV3 # ignore: type 29 | from pywayland.protocol_core.globals import Global 30 | from pywayland.server import Display 31 | 32 | log = logging.getLogger("Next: LayoutManager") 33 | 34 | 35 | class LayoutManager(Global): 36 | def __init__(self, display: Display) -> None: 37 | self.interface = RiverLayoutManagerV3 38 | super().__init__(display, 1) 39 | log.debug("Created RiverLayoutManagerV3 global") 40 | 41 | def destroy(self) -> None: 42 | super().destroy() 43 | -------------------------------------------------------------------------------- /libnext/libinput_ffi_build.py: -------------------------------------------------------------------------------- 1 | import wlroots.ffi_build as wlr 2 | from cffi import FFI 3 | 4 | CDEF = """ 5 | enum libinput_config_status { 6 | LIBINPUT_CONFIG_STATUS_SUCCESS = 0, 7 | LIBINPUT_CONFIG_STATUS_UNSUPPORTED, 8 | LIBINPUT_CONFIG_STATUS_INVALID, 9 | }; 10 | 11 | int 12 | libinput_device_config_tap_get_finger_count(struct libinput_device *device); 13 | 14 | enum libinput_config_tap_state { 15 | LIBINPUT_CONFIG_TAP_DISABLED, 16 | LIBINPUT_CONFIG_TAP_ENABLED, 17 | }; 18 | 19 | enum libinput_config_status 20 | libinput_device_config_tap_set_enabled(struct libinput_device *device, 21 | enum libinput_config_tap_state enable); 22 | 23 | enum libinput_config_tap_button_map { 24 | LIBINPUT_CONFIG_TAP_MAP_LRM, 25 | LIBINPUT_CONFIG_TAP_MAP_LMR, 26 | }; 27 | 28 | enum libinput_config_status 29 | libinput_device_config_tap_set_button_map(struct libinput_device *device, 30 | enum libinput_config_tap_button_map map); 31 | 32 | enum libinput_config_drag_state { 33 | LIBINPUT_CONFIG_DRAG_DISABLED, 34 | LIBINPUT_CONFIG_DRAG_ENABLED, 35 | }; 36 | 37 | enum libinput_config_status 38 | libinput_device_config_tap_set_drag_enabled(struct libinput_device *device, 39 | enum libinput_config_drag_state enable); 40 | 41 | enum libinput_config_drag_lock_state { 42 | LIBINPUT_CONFIG_DRAG_LOCK_DISABLED, 43 | LIBINPUT_CONFIG_DRAG_LOCK_ENABLED, 44 | }; 45 | 46 | enum libinput_config_status 47 | libinput_device_config_tap_set_drag_lock_enabled(struct libinput_device *device, 48 | enum libinput_config_drag_lock_state enable); 49 | 50 | int 51 | libinput_device_config_accel_is_available(struct libinput_device *device); 52 | 53 | enum libinput_config_status 54 | libinput_device_config_accel_set_speed(struct libinput_device *device, 55 | double speed); 56 | 57 | enum libinput_config_accel_profile { 58 | LIBINPUT_CONFIG_ACCEL_PROFILE_NONE = 0, 59 | LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT = (1 << 0), 60 | LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE = (1 << 1), 61 | }; 62 | 63 | enum libinput_config_status 64 | libinput_device_config_accel_set_profile(struct libinput_device *device, 65 | enum libinput_config_accel_profile profile); 66 | 67 | int 68 | libinput_device_config_scroll_has_natural_scroll(struct libinput_device *device); 69 | 70 | enum libinput_config_status 71 | libinput_device_config_scroll_set_natural_scroll_enabled(struct libinput_device *device, 72 | int enable); 73 | 74 | int 75 | libinput_device_config_left_handed_is_available(struct libinput_device *device); 76 | 77 | enum libinput_config_status 78 | libinput_device_config_left_handed_set(struct libinput_device *device, 79 | int left_handed); 80 | 81 | enum libinput_config_click_method { 82 | LIBINPUT_CONFIG_CLICK_METHOD_NONE = 0, 83 | LIBINPUT_CONFIG_CLICK_METHOD_BUTTON_AREAS = (1 << 0), 84 | LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER = (1 << 1), 85 | }; 86 | 87 | enum libinput_config_status 88 | libinput_device_config_click_set_method(struct libinput_device *device, 89 | enum libinput_config_click_method method); 90 | 91 | enum libinput_config_middle_emulation_state { 92 | LIBINPUT_CONFIG_MIDDLE_EMULATION_DISABLED, 93 | LIBINPUT_CONFIG_MIDDLE_EMULATION_ENABLED, 94 | }; 95 | 96 | enum libinput_config_status 97 | libinput_device_config_middle_emulation_set_enabled( 98 | struct libinput_device *device, 99 | enum libinput_config_middle_emulation_state enable); 100 | 101 | enum libinput_config_scroll_method { 102 | LIBINPUT_CONFIG_SCROLL_NO_SCROLL = 0, 103 | LIBINPUT_CONFIG_SCROLL_2FG = (1 << 0), 104 | LIBINPUT_CONFIG_SCROLL_EDGE = (1 << 1), 105 | LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN = (1 << 2), 106 | }; 107 | 108 | enum libinput_config_status 109 | libinput_device_config_scroll_set_method(struct libinput_device *device, 110 | enum libinput_config_scroll_method method); 111 | 112 | enum libinput_config_status 113 | libinput_device_config_scroll_set_button(struct libinput_device *device, 114 | uint32_t button); 115 | 116 | enum libinput_config_dwt_state { 117 | LIBINPUT_CONFIG_DWT_DISABLED, 118 | LIBINPUT_CONFIG_DWT_ENABLED, 119 | }; 120 | 121 | int 122 | libinput_device_config_dwt_is_available(struct libinput_device *device); 123 | 124 | enum libinput_config_status 125 | libinput_device_config_dwt_set_enabled(struct libinput_device *device, 126 | enum libinput_config_dwt_state enable); 127 | """ 128 | 129 | libinput_ffi = FFI() 130 | libinput_ffi.set_source( 131 | "libnext._libinput", 132 | "#include \n" + wlr.SOURCE, 133 | libraries=["wlroots", "input"], 134 | define_macros=[("WLR_USE_UNSTABLE", None)], 135 | include_dirs=["/usr/include/pixman-1", wlr.include_dir], 136 | ) 137 | 138 | libinput_ffi.include(wlr.ffi_builder) 139 | libinput_ffi.cdef(CDEF) 140 | 141 | if __name__ == "__main__": 142 | libinput_ffi.compile() 143 | -------------------------------------------------------------------------------- /libnext/outputs.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 Shinyzenith 2 | # 3 | # Redistribution and use in source and binary forms, with or without 4 | # modification, are permitted provided that the following conditions are met: 5 | # 6 | # 1. Redistributions of source code must retain the above copyright notice, 7 | # this list of conditions and the following disclaimer. 8 | # 9 | # 2. Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # 13 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 14 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 15 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 16 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 17 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 18 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 19 | # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 20 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 21 | # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 22 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 23 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | import logging 26 | from typing import Any 27 | 28 | from pywayland.server import Listener 29 | from wlroots.util.clock import Timespec 30 | from wlroots.wlr_types import OutputDamage 31 | 32 | from libnext.util import Listeners 33 | 34 | log = logging.getLogger("Next: Outputs") 35 | 36 | 37 | class NextOutput(Listeners): 38 | def __init__(self, core, wlr_output): 39 | self.core = core 40 | self.wlr_output = wlr_output 41 | self.damage: OutputDamage = OutputDamage(wlr_output) 42 | self.core.output_layout.add_auto(self.wlr_output) 43 | self.x, self.y = self.core.output_layout.output_coords(wlr_output) 44 | self.width: int 45 | self.height: int 46 | 47 | self.core.outputs.append(self) 48 | 49 | self.add_listener(self.wlr_output.destroy_event, self._on_destroy) 50 | self.add_listener(self.damage.frame_event, self._on_frame) 51 | 52 | def get_geometry(self) -> tuple[int, int, int, int]: 53 | width, height = self.wlr_output.effective_resolution() 54 | return int(self.x), int(self.y), width, height 55 | 56 | def destroy(self) -> None: 57 | self.core.outputs.remove(self) 58 | self.destroy_listeners() 59 | 60 | def _on_destroy(self, _listener: Listener, _data: Any) -> None: 61 | log.debug("Signal: wlr_output_destroy_event") 62 | self.destroy() 63 | 64 | def _on_frame(self, _listener: Listener, _data: Any) -> None: 65 | log.debug("Signal: wlr_output_frame_event") 66 | scene_output = self.core.scene.get_scene_output(self.wlr_output) 67 | try: 68 | scene_output.commit() 69 | except Exception as e: 70 | log.error("Failed to commit to scene: ", e) 71 | 72 | # This function is a no-op when hardware cursors are in use. 73 | self.wlr_output.render_software_cursors() 74 | 75 | now = Timespec.get_monotonic_time() 76 | scene_output.send_frame_done(now) 77 | -------------------------------------------------------------------------------- /libnext/util.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 Shinyzenith 2 | # 3 | # Redistribution and use in source and binary forms, with or without 4 | # modification, are permitted provided that the following conditions are met: 5 | # 6 | # 1. Redistributions of source code must retain the above copyright notice, 7 | # this list of conditions and the following disclaimer. 8 | # 9 | # 2. Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # 13 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 14 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 15 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 16 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 17 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 18 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 19 | # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 20 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 21 | # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 22 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 23 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | from typing import Callable, Union 26 | 27 | from pywayland.server import Listener, Signal 28 | 29 | ColorType = Union[str, tuple[int, int, int], tuple[int, int, int, float]] 30 | 31 | 32 | class Listeners: 33 | def add_listener(self, event: Signal, callback: Callable) -> None: 34 | """ 35 | Add a listener to any event. 36 | """ 37 | if not hasattr(self, "listeners"): 38 | self.listeners = [] 39 | 40 | listener = Listener(callback) 41 | event.add(listener) 42 | self.listeners.append(listener) 43 | 44 | def destroy_listeners(self) -> None: 45 | """ 46 | Destroy all assigned listeners. 47 | """ 48 | for listener in reversed(self.listeners): 49 | listener.remove() 50 | 51 | 52 | def rgb(x: ColorType) -> tuple[float, float, float, float]: 53 | """ 54 | Parse 55 | """ 56 | if isinstance(x, (tuple, list)): 57 | if len(x) == 4: 58 | alpha = x[-1] 59 | else: 60 | alpha = 1.0 61 | return (x[0] / 255.0, x[1] / 255.0, x[2] / 255.0, alpha) 62 | elif isinstance(x, str): 63 | if x.startswith("#"): 64 | x = x[1:] 65 | if "." in x: 66 | x, alpha_str = x.split(".") 67 | alpha = float("0." + alpha_str) 68 | else: 69 | alpha = 1.0 70 | if len(x) not in (3, 6, 8): 71 | raise ValueError("RGB specifier must be 3, 6 or 8 characters long.") 72 | if len(x) == 3: 73 | vals = tuple(int(i, 16) * 17 for i in x) 74 | else: 75 | vals = tuple(int(i, 16) for i in (x[0:2], x[2:4], x[4:6])) 76 | if len(x) == 8: 77 | alpha = int(x[6:8], 16) / 255.0 78 | vals += (alpha,) # type: ignore 79 | return rgb(vals) # type: ignore 80 | raise ValueError("Invalid RGB specifier.") 81 | 82 | 83 | def hex(x: ColorType) -> str: 84 | r, g, b, _ = rgb(x) 85 | return "#%02x%02x%02x" % (int(r * 255), int(g * 255), int(b * 255)) 86 | -------------------------------------------------------------------------------- /libnext/window.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022 Shinyzenith 2 | # 3 | # Redistribution and use in source and binary forms, with or without 4 | # modification, are permitted provided that the following conditions are met: 5 | # 6 | # 1. Redistributions of source code must retain the above copyright notice, 7 | # this list of conditions and the following disclaimer. 8 | # 9 | # 2. Redistributions in binary form must reproduce the above copyright 10 | # notice, this list of conditions and the following disclaimer in the 11 | # documentation and/or other materials provided with the distribution. 12 | # 13 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 14 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 15 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 16 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 17 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 18 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 19 | # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 20 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 21 | # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 22 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 23 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | import functools 26 | import logging 27 | from typing import Any, Generic, TypeVar, Union 28 | 29 | import pywayland 30 | from pywayland.server import Listener 31 | from wlroots import PtrHasData, ffi 32 | from wlroots.util.edges import Edges 33 | from wlroots.wlr_types import SceneNode, foreign_toplevel_management_v1 34 | from wlroots.wlr_types.surface import SubSurface 35 | from wlroots.wlr_types.xdg_shell import ( 36 | XdgPopup, 37 | XdgSurface, 38 | XdgTopLevelSetFullscreenEvent, 39 | ) 40 | 41 | from libnext import util 42 | from libnext.util import Listeners 43 | 44 | EDGES_TILED = Edges.TOP | Edges.BOTTOM | Edges.LEFT | Edges.RIGHT 45 | EDGES_FLOAT = Edges.NONE 46 | 47 | Surface = TypeVar("Surface", bound=PtrHasData) 48 | log = logging.getLogger("Next: Window") 49 | 50 | 51 | @functools.lru_cache() 52 | def rgb(color: util.ColorType) -> ffi.CData: 53 | if isinstance(color, ffi.CData): 54 | return color 55 | return ffi.new("float[4]", util.rgb(color)) 56 | 57 | 58 | class Window(Generic[Surface], Listeners): 59 | """ 60 | Generic class for windows. 61 | """ 62 | 63 | def __init__(self, core, surface: Surface): 64 | self.core = core 65 | self.surface = surface 66 | self.mapped: bool = False 67 | self.scene_node: SceneNode 68 | 69 | self.x = 0 70 | self.y = 0 71 | self.width: int = 0 72 | self.float_width: int = 0 73 | self.height: int = 0 74 | self.float_height: int = 0 75 | self.opacity: float = 1.0 76 | 77 | self.borderwidth: int = 0 78 | self.bordercolor: list[ffi.CData] = [rgb((0, 0, 0, 1))] 79 | 80 | self.name: str = "" 81 | self.wm_class: str | None = None 82 | 83 | surface.data = ( 84 | self.ftm_handle 85 | ) = self.core.foreign_toplevel_managerv1.create_handle() 86 | 87 | def destroy(self) -> None: 88 | self.destroy_listeners() 89 | self.ftm_handle.destroy() 90 | 91 | def set_border(self, color: util.ColorType | None, width: int) -> None: 92 | # NOTE: Does this need anything else? Check qtile. 93 | if color: 94 | if isinstance(color, list): 95 | self.bordercolor = [rgb(c) for c in color] 96 | else: 97 | self.bordercolor = [rgb(color)] 98 | self.borderwidth = width 99 | 100 | def _on_destroy(self, _listener: Listener, _data: Any) -> None: 101 | """ 102 | Window destroy callback. 103 | """ 104 | log.debug("Signal: window_destroy_event") 105 | if self.mapped: 106 | log.warn("Window destroy signal sent before unmap event.") 107 | self.mapped = False 108 | self.core.mapped_windows.remove(self) 109 | # Focus on the next window. 110 | if len(self.core.mapped_windows) >= 1: 111 | self.core.focus_window(self.core.mapped_windows[-1]) 112 | 113 | if self in self.core.pending_windows: 114 | self.core.pending_windows.remove(self) 115 | 116 | self.destroy() 117 | 118 | 119 | WindowType = Union[Window] 120 | 121 | 122 | class XdgWindow(Window[XdgSurface]): 123 | """ 124 | Wayland client connecting over xdg_shell 125 | """ 126 | 127 | def __init__(self, core, surface: XdgSurface): 128 | super().__init__(core, surface) 129 | 130 | self.wm_class = surface.toplevel.app_id 131 | self.popups: list[XdgPopupWindow] = [] 132 | self.subsurfaces: list[SubSurface] = [] 133 | self.scene_node = SceneNode.xdg_surface_create(self.core.scene.node, surface) 134 | 135 | self.fullscreen: bool = False 136 | # NOTE: Do we really need this? 137 | self.maximized: bool = False 138 | 139 | # TODO: Finish this. 140 | self.add_listener(self.surface.destroy_event, self._on_destroy) 141 | self.add_listener(self.surface.map_event, self._on_map) 142 | self.add_listener(self.surface.new_popup_event, self._on_new_popup) 143 | self.add_listener(self.surface.unmap_event, self._on_unmap) 144 | 145 | def _on_map(self, _listener: Listener, _data: Any) -> None: 146 | log.debug("Signal: wlr_xdg_surface_map_event") 147 | if self in self.core.pending_windows: 148 | log.debug("Managing a new top-level window") 149 | self.core.pending_windows.remove(self) 150 | self.mapped = True 151 | 152 | geometry = self.surface.get_geometry() 153 | self.width = self.float_width = geometry.width 154 | self.height = self.float_height = geometry.height 155 | 156 | self.surface.set_tiled(EDGES_TILED) 157 | 158 | if self.surface.toplevel.title: 159 | self.name = self.surface.toplevel.title 160 | self.ftm_handle.set_title(self.name) 161 | 162 | if self.wm_class: 163 | self.ftm_handle.set_app_id(self.wm_class or "") 164 | 165 | # TODO: Toplevel listeners go here. 166 | self.add_listener( 167 | self.surface.toplevel.request_fullscreen_event, 168 | self._on_request_fullscreen, 169 | ) 170 | self.add_listener(self.surface.toplevel.set_title_event, self._on_set_title) 171 | self.add_listener( 172 | self.surface.toplevel.set_app_id_event, self._on_set_app_id 173 | ) 174 | # foreign_toplevel_management_v1 callbacks. 175 | self.add_listener( 176 | self.ftm_handle.request_maximize_event, 177 | self._on_foreign_request_maximize, 178 | ) 179 | self.add_listener( 180 | self.ftm_handle.request_fullscreen_event, 181 | self._on_foreign_request_fullscreen, 182 | ) 183 | 184 | self.core.mapped_windows.append(self) 185 | self.core.focus_window(self) 186 | # TODO: Remove this before first release candidate. 187 | # This is only here for testing. 188 | self.place(0, 0, 1920, 1080, 0, None, True, None, False) 189 | 190 | def get_pid(self) -> int: 191 | pid = pywayland.ffi.new("pid_t *") 192 | pywayland.lib.wl_client_get_credentials( 193 | self.surface._ptr.client.client, pid, ffi.NULL, ffi.NULL 194 | ) 195 | return pid[0] 196 | 197 | def kill(self) -> None: 198 | self.surface.send_close() 199 | 200 | def place( 201 | self, 202 | x: int, 203 | y: int, 204 | width: int, 205 | height: int, 206 | borderwidth: int, 207 | bordercolor: util.ColorType | None, 208 | above: bool = False, 209 | margin: int | list[int] | None = None, 210 | respect_hints: bool = False, 211 | ) -> None: 212 | if margin is not None: 213 | if isinstance(margin, int): 214 | margin = [margin] * 4 215 | x += margin[3] 216 | y += margin[0] 217 | width -= margin[1] + margin[3] 218 | height -= margin[0] + margin[2] 219 | # TODO: This is incomplete. Finish this. 220 | 221 | self.x = x 222 | self.y = y 223 | self.width = int(width) 224 | self.height = int(height) 225 | self.surface.set_size(self.width, self.height) 226 | self.scene_node.set_position(self.x, self.y) 227 | self.set_border(bordercolor, borderwidth) 228 | 229 | if above: 230 | self.core.focus_window(self) 231 | 232 | def _on_foreign_request_maximize( 233 | self, 234 | _listener: Listener, 235 | event: foreign_toplevel_management_v1.ForeignToplevelHandleV1MaximizedEvent, 236 | ) -> None: 237 | log.debug("Signal: wlr_foreign_toplevel_management_request_maximize") 238 | self.maximized = event.maximized 239 | 240 | def _on_foreign_request_fullscreen( 241 | self, 242 | _listener: Listener, 243 | event: foreign_toplevel_management_v1.ForeignToplevelHandleV1FullscreenEvent, 244 | ) -> None: 245 | log.debug("Signal: wlr_foreign_toplevel_management_request_fullscreen") 246 | self.borderwidth = 0 247 | self.fullscreen = event.fullscreen 248 | 249 | def _on_request_fullscreen( 250 | self, _listener: Listener, event: XdgTopLevelSetFullscreenEvent 251 | ) -> None: 252 | log.debug("Signal: wlr_xdg_surface_toplevel_request_fullscreen") 253 | self.borderwidth = 0 254 | self.fullscreen = event.fullscreen 255 | 256 | def _on_set_title(self, _listener: Listener, _data: Any) -> None: 257 | log.debug("Signal: wlr_xdg_surface_toplevel_set_title") 258 | title = self.surface.toplevel.title 259 | 260 | if title and title != self.name: 261 | self.name = title 262 | self.ftm_handle.set_title(self.name) 263 | 264 | def _on_set_app_id(self, _listener: Listener, _data: Any) -> None: 265 | log.debug("Signal: wlr_xdg_surface_toplevel_set_app_id") 266 | self.wm_class = self.surface.toplevel.app_id 267 | 268 | if ( 269 | self.surface.toplevel.app_id 270 | and self.surface.toplevel.app_id != self.wm_class # noqa: W503 271 | ): 272 | self.ftm_handle.set_app_id(self.wm_class or "") 273 | 274 | def _on_new_popup(self, _listener: Listener, xdg_popup: XdgPopup) -> None: 275 | log.debug("Signal: wlr_xdg_surface_new_popup_event") 276 | self.popups.append(XdgPopupWindow(self, xdg_popup)) 277 | 278 | def _on_unmap(self, _listener: Listener, _data: Any) -> None: 279 | log.debug("Signal: wlr_xdg_surface_unmap_event") 280 | self.mapped = False 281 | self.core.mapped_windows.remove(self) 282 | 283 | # Focus on the next window. 284 | if len(self.core.mapped_windows) >= 1: 285 | self.core.focus_window(self.core.mapped_windows[-1]) 286 | 287 | 288 | class XdgPopupWindow(Listeners): 289 | # parent: Any because it can be a nested popup too aka XdgPopupWindow. 290 | def __init__(self, parent: XdgWindow | Any, xdg_popup: XdgPopup): 291 | self.scene_node = SceneNode.xdg_surface_create( 292 | parent.scene_node, parent.surface 293 | ) 294 | # TODO: Finish this. 295 | -------------------------------------------------------------------------------- /next: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (c) 2022 Shinyzenith 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 7 | # 1. Redistributions of source code must retain the above copyright notice, 8 | # this list of conditions and the following disclaimer. 9 | # 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 15 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 16 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 17 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 18 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 22 | # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 23 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 24 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | import argparse 27 | import logging 28 | import os 29 | import subprocess 30 | 31 | import wlroots 32 | 33 | try: 34 | from libnext.backend import NextCore 35 | except ModuleNotFoundError: 36 | subprocess.run( 37 | [ 38 | "python3", 39 | "-m", 40 | "pywayland.scanner", 41 | "-i", 42 | "./protocols/river-layout-v3.xml", 43 | "/usr/share/wayland/wayland.xml", 44 | ] 45 | ) 46 | from libnext.backend import NextCore 47 | 48 | 49 | def main(): 50 | # Default log level. 51 | log_level = logging.ERROR 52 | wlroots.util.log.log_init(log_level) 53 | 54 | # Setting up the parser. 55 | parser = argparse.ArgumentParser( 56 | description="NextWM - Wayland compositing window manager." 57 | ) 58 | parser.add_argument("-d", "--debug", help="enable debug mode", action="store_true") 59 | args = parser.parse_args() 60 | 61 | if args.debug: 62 | log_level = logging.DEBUG 63 | wlroots.util.log.log_init(log_level) 64 | 65 | log = logging.getLogger("NextWM") 66 | logging.basicConfig( 67 | level=log_level, 68 | format="(%(asctime)s) %(levelname)s %(message)s", 69 | datefmt="%d/%m/%y - %H:%M:%S %Z", 70 | ) 71 | 72 | try: 73 | import coloredlogs # type: ignore 74 | 75 | if args.debug: 76 | coloredlogs.install(logger=log, level="DEBUG") 77 | else: 78 | coloredlogs.install(logger=log) 79 | finally: 80 | log.info(f"Starting NextWM with PID: {os.getpid()}") 81 | NextCore() 82 | 83 | 84 | if __name__ == "__main__": 85 | main() 86 | -------------------------------------------------------------------------------- /next.1.scd: -------------------------------------------------------------------------------- 1 | next(1) "github.com/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 python. 14 | 15 | # OPTIONS 16 | 17 | *-h* 18 | Print the help message and exit. 19 | 20 | *-d* 21 | Enable debug mode. 22 | 23 | # AUTHORS 24 | 25 | Maintained by Shinyzenith . 26 | For more information about development, see . 27 | -------------------------------------------------------------------------------- /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 | given seat. 45 | 46 | 47 | 49 | 50 | 51 | 52 | 53 | 54 | This object is created by the run_command request. Exactly one of the 55 | success or failure events will be sent. This object will be destroyed 56 | by the compositor after one of the events is sent. 57 | 58 | 59 | 60 | 61 | Sent when the command has been successfully received and executed by 62 | the compositor. Some commands may produce output, in which case the 63 | output argument will be a non-empty string. 64 | 65 | 66 | 67 | 68 | 69 | 70 | Sent when the command could not be carried out. This could be due to 71 | sending a non-existent command, no command, not enough arguments, too 72 | many arguments, invalid arguments, etc. 73 | 74 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /protocols/river-layout-v3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright 2020-2021 The River Developers 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 | This protocol specifies a way for clients to propose arbitrary positions 21 | and dimensions for a set of views on a specific output of a compositor 22 | through the river_layout_v3 object. 23 | 24 | Layouts are a strictly linear list of views, the position and dimensions 25 | of which are supplied by the client. Any complex underlying data structure 26 | a client may use when generating the layout is lost in transmission. This 27 | is an intentional limitation. 28 | 29 | Additionally, this protocol allows the compositor to deliver arbitrary 30 | user-provided commands associated with a layout to clients. A client 31 | may use these commands to implement runtime configuration/control, or 32 | may ignore them entirely. How the user provides these commands to the 33 | compositor is not specified by this protocol and left to compositor policy. 34 | 35 | Warning! The protocol described in this file is currently in the 36 | testing phase. Backward compatible changes may be added together with 37 | the corresponding interface version bump. Backward incompatible changes 38 | can only be done by creating a new major version of the extension. 39 | 40 | 41 | 42 | 43 | A global factory for river_layout_v3 objects. 44 | 45 | 46 | 47 | 48 | This request indicates that the client will not use the 49 | river_layout_manager object any more. Objects that have been created 50 | through this instance are not affected. 51 | 52 | 53 | 54 | 55 | 56 | This creates a new river_layout_v3 object for the given wl_output. 57 | 58 | All layout related communication is done through this interface. 59 | 60 | The namespace is used by the compositor to decide which river_layout_v3 61 | object will receive layout demands for the output. 62 | 63 | The namespace is required to be be unique per-output. Furthermore, 64 | two separate clients may not share a namespace on separate outputs. If 65 | these conditions are not upheld, the the namespace_in_use event will 66 | be sent directly after creation of the river_layout_v3 object. 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | This interface allows clients to receive layout demands from the 77 | compositor for a specific output and subsequently propose positions and 78 | dimensions of individual views. 79 | 80 | 81 | 82 | 84 | 86 | 87 | 88 | 89 | 90 | This request indicates that the client will not use the river_layout_v3 91 | object any more. 92 | 93 | 94 | 95 | 96 | 97 | After this event is sent, all requests aside from the destroy event 98 | will be ignored by the server. If the client wishes to try again with 99 | a different namespace they must create a new river_layout_v3 object. 100 | 101 | 102 | 103 | 104 | 105 | The compositor sends this event to inform the client that it requires a 106 | layout for a set of views. 107 | 108 | The usable width and height indicate the space in which the client 109 | can safely position views without interfering with desktop widgets 110 | such as panels. 111 | 112 | The serial of this event is used to identify subsequent requests as 113 | belonging to this layout demand. Beware that the client might need 114 | to handle multiple layout demands at the same time. 115 | 116 | The server will ignore responses to all but the most recent layout 117 | demand. Thus, clients are only required to respond to the most recent 118 | layout_demand received. If a newer layout_demand is received before 119 | the client has finished responding to an old demand, the client should 120 | abort work on the old demand as any further work would be wasted. 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | This request proposes a size and position for a view in the layout demand 132 | with matching serial. 133 | 134 | A client must send this request for every view that is part of the 135 | layout demand. The number of views in the layout is given by the 136 | view_count argument of the layout_demand event. Pushing too many or 137 | too few view dimensions is a protocol error. 138 | 139 | The x and y coordinates are relative to the usable area of the output, 140 | with (0,0) as the top left corner. 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | This request indicates that the client is done pushing dimensions 152 | and the compositor may apply the layout. This completes the layout 153 | demand with matching serial, any other requests sent with the serial 154 | are a protocol error. 155 | 156 | The layout_name argument is a user-facing name or short description 157 | of the layout that is being committed. The compositor may for example 158 | display this on a status bar, though what exactly is done with it is 159 | left to the compositor's discretion. 160 | 161 | The compositor is free to use this proposed layout however it chooses, 162 | including ignoring it. 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | This event informs the client of a command sent to it by the user. 171 | 172 | The semantic meaning of the command is left for the client to 173 | decide. It is also free to ignore it entirely if it so chooses. 174 | 175 | A layout_demand will be sent after this event if the compositor is 176 | currently using this layout object to arrange the output. 177 | 178 | 179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /requirements-optional.txt: -------------------------------------------------------------------------------- 1 | coloredlogs 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cffi 2 | pywayland 3 | pywlroots 4 | xkbcommon 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (c) 2022 Shinyzenith 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 7 | # 1. Redistributions of source code must retain the above copyright notice, 8 | # this list of conditions and the following disclaimer. 9 | # 10 | # 2. Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 15 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 16 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 17 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 18 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 22 | # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 23 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 24 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | 27 | import subprocess 28 | import sys 29 | 30 | from setuptools import setup 31 | from setuptools.command.install import install 32 | 33 | 34 | class CheckProtocolExistence(install): 35 | def protocol_check(self) -> bool: 36 | try: 37 | from pywayland.protocol.next_control_v1 import NextControlV1 38 | from pywayland.protocol.river_layout_v3 import RiverLayoutManagerV3 39 | return True 40 | except ModuleNotFoundError: 41 | return False 42 | 43 | def finalize_options(self): 44 | if not self.protocol_check(): 45 | subprocess.run( 46 | [ 47 | "python3", 48 | "-m", 49 | "pywayland.scanner", 50 | "-i", 51 | "./protocols/river-layout-v3.xml", 52 | "./protocols/next-control-v1.xml", 53 | "/usr/share/wayland/wayland.xml", 54 | ] 55 | ) 56 | install.finalize_options(self) 57 | 58 | 59 | def get_cffi_modules(): 60 | cffi_modules = [] 61 | try: 62 | from cffi.error import PkgConfigError 63 | from cffi.pkgconfig import call 64 | except ImportError: 65 | # technically all ffi defined above wont be built 66 | print('CFFI package is missing') 67 | try: 68 | import wlroots.ffi_build 69 | cffi_modules.append( 70 | 'libnext/libinput_ffi_build.py:libinput_ffi' 71 | ) 72 | except ImportError: 73 | print( 74 | "Failed to find pywlroots. " 75 | "Wayland backend libinput configuration will be unavailable." 76 | ) 77 | pass 78 | return cffi_modules 79 | 80 | 81 | setup( 82 | cmdclass={'install': CheckProtocolExistence}, 83 | cffi_modules=get_cffi_modules(), 84 | include_package_data=True, 85 | ) 86 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skip_missing_interpreters = True 3 | skipsdist=True 4 | minversion = 3.4.0 5 | envlist = 6 | codestyle, 7 | flake, 8 | black, 9 | py310, 10 | 11 | [testenv:black] 12 | deps= 13 | black 14 | commands = 15 | black libnext next 16 | 17 | [testenv:flake] 18 | deps = 19 | flake8 20 | flake8-black>=0.2.4 21 | flake8-isort 22 | flake8-tidy-imports 23 | flake8-logging-format 24 | pep8-naming 25 | commands = 26 | flake8 {toxinidir}/libnext {toxinidir}/next --exclude=libnext/libinput_ffi_build.py 27 | 28 | [testenv:mypy] 29 | setenv = 30 | MYPYPATH = ./stubs 31 | deps = 32 | mypy 33 | types-dataclasses 34 | types-python-dateutil 35 | types-pytz 36 | types-pkg_resources 37 | commands = 38 | pip3 install pywlroots 39 | python3 ./libnext/libinput_ffi_build.py 40 | mypy next 41 | mypy -p libnext 42 | 43 | [testenv:codestyle] 44 | deps = 45 | pycodestyle >= 2.7 46 | skip_install = true 47 | commands = 48 | pycodestyle --max-line-length=150 --exclude="_*.py" {toxinidir}/libnext 49 | --------------------------------------------------------------------------------