diff --git a/README.md b/README.md index 2a61367..67583c2 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,24 @@ A very, VERY WIP Smithay-based wayland compositor +## Features +- [x] Winit backend +- [x] Udev backend + - This is currently just a copy of Anvil's udev backend. +- [x] Basic tags + - Tags are currently very jank on the udev backend with multiple monitors. If you're checking udev out, I suggest unplugging all but one monitor or just using the winit backend until I flesh out the tag system. +- [ ] Widget system +- [ ] Layout system +- [ ] Server-side decorations +- [ ] The other stuff Awesome has +- [x] Is very cool :thumbsup: + ## Info ### Why Pinnacle? Well, I currently use [Awesome](https://github.com/awesomeWM/awesome). And I really like it! Unfortunately, Awesome doesn't exist for Wayland ([anymore](http://way-cooler.org/blog/2020/01/09/way-cooler-post-mortem.html)). There doesn't seem to be any Wayland compositor out there that has all of the following: - - Tags for window management - - Configurable in Lua (or any other programming language for that matter) - - Has a bunch of batteries included (widget system, systray, etc) +- Tags for window management +- Configurable in Lua (or any other programming language for that matter) +- Has a bunch of batteries included (widget system, systray, etc) So, this is my attempt at making an Awesome-esque Wayland compositor. @@ -52,13 +64,13 @@ cargo run [--release] -- -- `backend` can be one of two values: - - `winit`: run Pinnacle as a window in your graphical environment - - `udev`: run Pinnacle in a tty. NOTE: I tried running udev in Awesome and some things broke so uh, don't do that +- `winit`: run Pinnacle as a window in your graphical environment +- `udev`: run Pinnacle in a tty. NOTE: I tried running udev in Awesome and some things broke so uh, don't do that ## Configuration Please note: this is VERY WIP and has basically no options yet. -Pinnacle supports configuration through Lua (and hopefully more languages if I architect it correctly :crab:). +Pinnacle supports configuration through Lua (and hopefully more languages if it's not too unwieldy :crab:). Run Pinnacle with the `PINNACLE_CONFIG` environment variable set to the path of your config file. If not specified, Pinnacle will look for the following: ``` @@ -93,7 +105,7 @@ Doc website soon:tm: ## Controls The following controls are currently hardcoded: - - `Ctrl + Left Mouse`: Move a window - - `Ctrl + Right Mouse`: Resize a window +- `Ctrl + Left Mouse`: Move a window +- `Ctrl + Right Mouse`: Resize a window You can find the rest of the controls in the [`example_config`](api/lua/example_config.lua). diff --git a/api/lua/example_config.lua b/api/lua/example_config.lua index 2791a78..21a3844 100644 --- a/api/lua/example_config.lua +++ b/api/lua/example_config.lua @@ -12,33 +12,108 @@ pcall(require, "luarocks.loader") -- Neovim users be like: require("pinnacle").setup(function(pinnacle) local input = pinnacle.input -- Key and mouse binds - local client = pinnacle.client -- Window management + local window = pinnacle.window -- Window management local process = pinnacle.process -- Process spawning + local tag = pinnacle.tag -- Tag management -- Every key supported by xkbcommon. -- Support for just putting in a string of a key is intended. local keys = input.keys + ---@type Modifier + local mod_key = "Ctrl" -- This is set to `Ctrl` instead of `Super` to not conflict with your WM/DE keybinds + -- ^ Add type annotations for that sweet, sweet autocomplete + + local terminal = "alacritty" + -- Keybinds ---------------------------------------------------------------------- - input.keybind({ "Ctrl", "Alt" }, keys.q, pinnacle.quit) + input.keybind({ mod_key, "Alt" }, keys.q, pinnacle.quit) - input.keybind({ "Ctrl", "Alt" }, keys.c, client.close_window) + input.keybind({ mod_key, "Alt" }, keys.c, window.close_window) - input.keybind({ "Ctrl", "Alt" }, keys.space, client.toggle_floating) + input.keybind({ mod_key, "Alt" }, keys.space, window.toggle_floating) - input.keybind({ "Ctrl" }, keys.Return, function() - process.spawn("alacritty", function(stdout, stderr, exit_code, exit_msg) - -- do something with the output here; remember to check for nil! + input.keybind({ mod_key }, keys.Return, function() + process.spawn(terminal, function(stdout, stderr, exit_code, exit_msg) + -- do something with the output here end) end) - input.keybind({ "Ctrl" }, keys.KEY_1, function() + input.keybind({ mod_key }, keys.l, function() process.spawn("kitty") end) - input.keybind({ "Ctrl" }, keys.KEY_2, function() + input.keybind({ mod_key }, keys.k, function() process.spawn("foot") end) - input.keybind({ "Ctrl" }, keys.KEY_3, function() + input.keybind({ mod_key }, keys.j, function() process.spawn("nautilus") end) + + -- Tags --------------------------------------------------------------------------- + tag.add("1", "2", "3", "4", "5") + tag.toggle("1") + + input.keybind({ mod_key }, keys.KEY_1, function() + tag.switch_to("1") + end) + input.keybind({ mod_key }, keys.KEY_2, function() + tag.switch_to("2") + end) + input.keybind({ mod_key }, keys.KEY_3, function() + tag.switch_to("3") + end) + input.keybind({ mod_key }, keys.KEY_4, function() + tag.switch_to("4") + end) + input.keybind({ mod_key }, keys.KEY_5, function() + tag.switch_to("5") + end) + + input.keybind({ mod_key, "Shift" }, keys.KEY_1, function() + tag.toggle("1") + end) + input.keybind({ mod_key, "Shift" }, keys.KEY_2, function() + tag.toggle("2") + end) + input.keybind({ mod_key, "Shift" }, keys.KEY_3, function() + tag.toggle("3") + end) + input.keybind({ mod_key, "Shift" }, keys.KEY_4, function() + tag.toggle("4") + end) + input.keybind({ mod_key, "Shift" }, keys.KEY_5, function() + tag.toggle("5") + end) + + input.keybind({ mod_key, "Alt" }, keys.KEY_1, function() + window.get_focused():move_to_tag("1") + end) + input.keybind({ mod_key, "Alt" }, keys.KEY_2, function() + window.get_focused():move_to_tag("2") + end) + input.keybind({ mod_key, "Alt" }, keys.KEY_3, function() + window.get_focused():move_to_tag("3") + end) + input.keybind({ mod_key, "Alt" }, keys.KEY_4, function() + window.get_focused():move_to_tag("4") + end) + input.keybind({ mod_key, "Alt" }, keys.KEY_5, function() + window.get_focused():move_to_tag("5") + end) + + input.keybind({ mod_key, "Shift", "Alt" }, keys.KEY_1, function() + window.get_focused():toggle_tag("1") + end) + input.keybind({ mod_key, "Shift", "Alt" }, keys.KEY_2, function() + window.get_focused():toggle_tag("2") + end) + input.keybind({ mod_key, "Shift", "Alt" }, keys.KEY_3, function() + window.get_focused():toggle_tag("3") + end) + input.keybind({ mod_key, "Shift", "Alt" }, keys.KEY_4, function() + window.get_focused():toggle_tag("4") + end) + input.keybind({ mod_key, "Shift", "Alt" }, keys.KEY_5, function() + window.get_focused():toggle_tag("5") + end) end) diff --git a/api/lua/input.lua b/api/lua/input.lua index 4cda139..a362ee0 100644 --- a/api/lua/input.lua +++ b/api/lua/input.lua @@ -8,9 +8,9 @@ local input = { keys = require("keys"), } ----Set a keybind. If called on an already existing keybind, it gets replaced. ----@param key Keys The key for the keybind. NOTE: uppercase and lowercase characters are considered different. ----@param modifiers Modifiers[] Which modifiers need to be pressed for the keybind to trigger. +---Set a keybind. If called with an already existing keybind, it gets replaced. +---@param key Keys The key for the keybind. +---@param modifiers (Modifier)[] Which modifiers need to be pressed for the keybind to trigger. ---@param action fun() What to run. function input.keybind(modifiers, key, action) table.insert(CallbackTable, action) diff --git a/api/lua/keys.lua b/api/lua/keys.lua index 2c0a8b5..88286e0 100644 --- a/api/lua/keys.lua +++ b/api/lua/keys.lua @@ -4,7 +4,7 @@ -- -- SPDX-License-Identifier: MPL-2.0 ----@alias Modifiers "Alt" | "Ctrl" | "Shift" | "Super" +---@alias Modifier "Alt" | "Ctrl" | "Shift" | "Super" ---@enum Keys local M = { diff --git a/api/lua/msg.lua b/api/lua/msg.lua index e96e827..c56ea11 100644 --- a/api/lua/msg.lua +++ b/api/lua/msg.lua @@ -9,11 +9,20 @@ ---@class _Msg ---@field SetKeybind { key: Keys, modifiers: Modifiers[], callback_id: integer } ---@field SetMousebind { button: integer } +--Windows ---@field CloseWindow { client_id: integer? } ---@field ToggleFloating { client_id: integer? } ---@field SetWindowSize { window_id: integer, size: { w: integer, h: integer } } +---@field MoveWindowToTag { window_id: integer, tag_id: string } +---@field ToggleTagOnWindow { window_id: integer, tag_id: string } +-- ---@field Spawn { command: string[], callback_id: integer? } ---@field Request Request +--Tags +---@field ToggleTag { tag_id: string } +---@field SwitchToTag { tag_id: string } +---@field AddTags { tags: string[] } +---@field RemoveTags { tags: string[] } ---@alias Msg _Msg | "Quit" diff --git a/api/lua/pinnacle.lua b/api/lua/pinnacle.lua index e6dfb2e..d2fbf0f 100644 --- a/api/lua/pinnacle.lua +++ b/api/lua/pinnacle.lua @@ -51,9 +51,11 @@ local pinnacle = { ---Key and mouse binds input = require("input"), ---Window management - client = require("client"), + window = require("window"), ---Process spawning process = require("process"), + ---Tag management + tag = require("tag"), } ---Quit Pinnacle. diff --git a/api/lua/tag.lua b/api/lua/tag.lua new file mode 100644 index 0000000..decce6f --- /dev/null +++ b/api/lua/tag.lua @@ -0,0 +1,60 @@ +-- This Source Code Form is subject to the terms of the Mozilla Public +-- License, v. 2.0. If a copy of the MPL was not distributed with this +-- file, You can obtain one at https://mozilla.org/MPL/2.0/. +-- +-- SPDX-License-Identifier: MPL-2.0 + +local tag = {} + +---Add tags. +--- +---If you need to add the strings in a table, use `tag.add_table` instead. +--- +---# Example +--- +---```lua +---tag.add("1", "2", "3", "4", "5") -- Add tags with names 1-5 +---``` +---@param ... string The names of the new tags you want to add. +function tag.add(...) + local tags = table.pack(...) + tags["n"] = nil + + SendMsg({ + AddTags = { + tags = tags, + }, + }) +end + +---Like `tag.add`, but with a table of strings instead. +---@param tags string[] The names of the new tags you want to add, as a table. +function tag.add_table(tags) + SendMsg({ + AddTags = { + tags = tags, + }, + }) +end + +---Toggle a tag's display. +---@param name string The name of the tag. +function tag.toggle(name) + SendMsg({ + ToggleTag = { + tag_id = name, + }, + }) +end + +---Switch to a tag, deactivating any other active tags. +---@param name string The name of the tag. +function tag.switch_to(name) + SendMsg({ + SwitchToTag = { + tag_id = name, + }, + }) +end + +return tag diff --git a/api/lua/client.lua b/api/lua/window.lua similarity index 55% rename from api/lua/client.lua rename to api/lua/window.lua index 146331b..810e2c7 100644 --- a/api/lua/client.lua +++ b/api/lua/window.lua @@ -11,13 +11,13 @@ ---@field private size { w: integer, h: integer } The size of the window ---@field private location { x: integer, y: integer } The location of the window ---@field private floating boolean Whether the window is floating or not (tiled) -local window = {} +local win = {} ---@param props { id: integer, app_id: string?, title: string?, size: { w: integer, h: integer }, location: { x: integer, y: integer }, floating: boolean } ---@return Window local function new_window(props) -- Copy functions over - for k, v in pairs(window) do + for k, v in pairs(win) do props[k] = v end @@ -26,7 +26,7 @@ end ---Set a window's size. ---@param size { w: integer?, h: integer? } -function window:set_size(size) +function win:set_size(size) self.size = { w = size.w or self.size.w, h = size.h or self.size.h, @@ -39,19 +39,41 @@ function window:set_size(size) }) end +---Move a window to a tag, removing all other ones. +---@param name string The name of the tag. +function win:move_to_tag(name) + SendMsg({ + MoveWindowToTag = { + window_id = self.id, + tag_id = name, + }, + }) +end + +---Toggle the specified tag for this window. +---@param name string The name of the tag. +function win:toggle_tag(name) + SendMsg({ + ToggleTagOnWindow = { + window_id = self.id, + tag_id = name, + }, + }) +end + ---Get a window's size. ---@return { w: integer, h: integer } -function window:get_size() +function win:get_size() return self.size end ------------------------------------------------------------------- -local client = {} +local window = {} ---Close a window. ---@param client_id integer? The id of the window you want closed, or nil to close the currently focused window, if any. -function client.close_window(client_id) +function window.close_window(client_id) SendMsg({ CloseWindow = { client_id = client_id, @@ -61,7 +83,7 @@ end ---Toggle a window's floating status. ---@param client_id integer? The id of the window you want to toggle, or nil to toggle the currently focused window, if any. -function client.toggle_floating(client_id) +function window.toggle_floating(client_id) SendMsg({ ToggleFloating = { client_id = client_id, @@ -69,39 +91,25 @@ function client.toggle_floating(client_id) }) end ----Get a window. ----@param identifier { app_id: string } | { title: string } | "focus" A table with either the key app_id or title, depending if you want to get the window via its app_id or title, OR the string "focus" to get the currently focused window. ----@return Window -function client.get_window(identifier) +---Get a window by its app id (aka its X11 class). +---@param app_id string The window's app id. For example, Alacritty's app id is "Alacritty". +---@return Window window -- TODO: nil +function window.get_by_app_id(app_id) local req_id = Requests:next() - if type(identifier) == "string" then - SendRequest({ - GetWindowByFocus = { - id = req_id, - }, - }) - elseif identifier.app_id then - SendRequest({ - GetWindowByAppId = { - id = req_id, - app_id = identifier.app_id, - }, - }) - else - SendRequest({ - GetWindowByTitle = { - id = req_id, - title = identifier.title, - }, - }) - end + + SendRequest({ + GetWindowByAppId = { + id = req_id, + app_id = app_id, + }, + }) local response = ReadMsg() local props = response.RequestResponse.response.Window.window ---@type Window - local win = { + local wind = { id = props.id, app_id = props.app_id or "", title = props.title or "", @@ -116,12 +124,82 @@ function client.get_window(identifier) floating = props.floating, } - return new_window(win) + return new_window(wind) +end + +---Get a window by its title. +---@param title string The window's title. +---@return Window +function window.get_by_title(title) + local req_id = Requests:next() + + SendRequest({ + GetWindowByTitle = { + id = req_id, + title = title, + }, + }) + + local response = ReadMsg() + + local props = response.RequestResponse.response.Window.window + + ---@type Window + local wind = { + id = props.id, + app_id = props.app_id or "", + title = props.title or "", + size = { + w = props.size[1], + h = props.size[2], + }, + location = { + x = props.location[1], + y = props.location[2], + }, + floating = props.floating, + } + + return new_window(wind) +end + +---Get the currently focused window. +---@return Window +function window.get_focused() + local req_id = Requests:next() + + SendRequest({ + GetWindowByFocus = { + id = req_id, + }, + }) + + local response = ReadMsg() + + local props = response.RequestResponse.response.Window.window + + ---@type Window + local wind = { + id = props.id, + app_id = props.app_id or "", + title = props.title or "", + size = { + w = props.size[1], + h = props.size[2], + }, + location = { + x = props.location[1], + y = props.location[2], + }, + floating = props.floating, + } + + return new_window(wind) end ---Get all windows. ---@return Window[] -function client.get_windows() +function window.get_windows() SendRequest({ GetAllWindows = { id = Requests:next(), @@ -152,6 +230,4 @@ function client.get_windows() return windows end --- local win = client.get_window("focus") - -return client +return window diff --git a/src/api/msg.rs b/src/api/msg.rs index 8eef078..97cb8bf 100644 --- a/src/api/msg.rs +++ b/src/api/msg.rs @@ -7,7 +7,10 @@ // The MessagePack format for these is a one-element map where the element's key is the enum name and its // value is a map of the enum's values -use crate::window::{tag::Tag, window_state::WindowId, WindowProperties}; +use crate::{ + tag::TagId, + window::{window_state::WindowId, WindowProperties}, +}; #[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Copy)] pub struct CallbackId(pub u32); @@ -33,16 +36,32 @@ pub enum Msg { #[serde(default)] client_id: Option, }, - MoveToTag { - tag: Tag, - }, - ToggleTag { - tag: Tag, - }, SetWindowSize { window_id: WindowId, size: (i32, i32), }, + MoveWindowToTag { + window_id: WindowId, + tag_id: TagId, + }, + ToggleTagOnWindow { + window_id: WindowId, + tag_id: TagId, + }, + + // Tag management + ToggleTag { + tag_id: TagId, + }, + SwitchToTag { + tag_id: TagId, + }, + AddTags { + tags: Vec, + }, + RemoveTags { + tags: Vec, + }, // Process management /// Spawn a program with an optional callback. diff --git a/src/backend/udev.rs b/src/backend/udev.rs index 44f9cb2..75faa4e 100644 --- a/src/backend/udev.rs +++ b/src/backend/udev.rs @@ -225,7 +225,13 @@ pub fn run_udev() -> Result<(), Box> { pointer_images: Vec::new(), pointer_element: PointerElement::default(), }; - let mut state = State::init( + + // + // + // + // + + let mut state = State::::init( data, &mut display, event_loop.get_signal(), @@ -236,6 +242,39 @@ pub fn run_udev() -> Result<(), Box> { * Initialize the udev backend */ let udev_backend = UdevBackend::new(state.seat.name())?; + + for (device_id, path) in udev_backend.device_list() { + if let Err(err) = DrmNode::from_dev_id(device_id) + .map_err(DeviceAddError::DrmNode) + .and_then(|node| state.device_added(node, path)) + { + tracing::error!("Skipping device {device_id}: {err}"); + } + } + event_loop + .handle() + .insert_source(udev_backend, move |event, _, data| match event { + UdevEvent::Added { device_id, path } => { + if let Err(err) = DrmNode::from_dev_id(device_id) + .map_err(DeviceAddError::DrmNode) + .and_then(|node| data.state.device_added(node, &path)) + { + tracing::error!("Skipping device {device_id}: {err}"); + } + } + UdevEvent::Changed { device_id } => { + if let Ok(node) = DrmNode::from_dev_id(device_id) { + data.state.device_changed(node) + } + } + UdevEvent::Removed { device_id } => { + if let Ok(node) = DrmNode::from_dev_id(device_id) { + data.state.device_removed(node) + } + } + }) + .unwrap(); + /* * Initialize libinput backend */ @@ -299,14 +338,6 @@ pub fn run_udev() -> Result<(), Box> { }) .unwrap(); - for (device_id, path) in udev_backend.device_list() { - if let Err(err) = DrmNode::from_dev_id(device_id) - .map_err(DeviceAddError::DrmNode) - .and_then(|node| state.device_added(node, path)) - { - tracing::error!("Skipping device {device_id}: {err}"); - } - } state.shm_state.update_formats( state .backend_data @@ -422,30 +453,6 @@ pub fn run_udev() -> Result<(), Box> { }); }); - event_loop - .handle() - .insert_source(udev_backend, move |event, _, data| match event { - UdevEvent::Added { device_id, path } => { - if let Err(err) = DrmNode::from_dev_id(device_id) - .map_err(DeviceAddError::DrmNode) - .and_then(|node| data.state.device_added(node, &path)) - { - tracing::error!("Skipping device {device_id}: {err}"); - } - } - UdevEvent::Changed { device_id } => { - if let Ok(node) = DrmNode::from_dev_id(device_id) { - data.state.device_changed(node) - } - } - UdevEvent::Removed { device_id } => { - if let Ok(node) = DrmNode::from_dev_id(device_id) { - data.state.device_removed(node) - } - } - }) - .unwrap(); - event_loop.run( Some(Duration::from_millis(6)), &mut CalloopData { state, display }, @@ -829,6 +836,8 @@ impl State { ); let global = output.create_global::>(&self.backend_data.display_handle); + self.focus_state.focused_output = Some(output.clone()); + let x = self.space.outputs().fold(0, |acc, o| { acc + self.space.output_geometry(o).unwrap().size.w }); diff --git a/src/backend/winit.rs b/src/backend/winit.rs index d740e25..89c7ed9 100644 --- a/src/backend/winit.rs +++ b/src/backend/winit.rs @@ -177,7 +177,7 @@ pub fn run_winit() -> Result<(), Box> { } }; - let mut state = State::init( + let mut state = State::::init( WinitData { backend: winit_backend, damage_tracker: OutputDamageTracker::from_output(&output), @@ -387,7 +387,9 @@ pub fn run_winit() -> Result<(), Box> { event_loop.run( Some(Duration::from_millis(6)), &mut CalloopData { display, state }, - |_data| {}, + |_data| { + // println!("{}", _data.state.space.elements().count()); + }, )?; Ok(()) diff --git a/src/focus.rs b/src/focus.rs index e63f2fa..a536af9 100644 --- a/src/focus.rs +++ b/src/focus.rs @@ -17,6 +17,7 @@ impl FocusState { Default::default() } + /// Get the currently focused window. If there is none, the previous focus is returned. pub fn current_focus(&mut self) -> Option { while let Some(window) = self.focus_stack.last() { if window.alive() { @@ -27,6 +28,7 @@ impl FocusState { None } + /// Set the currently focused window. pub fn set_focus(&mut self, window: Window) { self.focus_stack.retain(|win| win != &window); self.focus_stack.push(window); diff --git a/src/handlers.rs b/src/handlers.rs index 900a7e2..d4f71c0 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -48,6 +48,7 @@ use smithay::{ use crate::{ backend::Backend, layout::Layout, + output::OutputState, state::{ClientState, State}, window::window_state::{WindowResizeState, WindowState}, }; @@ -116,11 +117,9 @@ impl CompositorHandler for State { if let Some(window) = self.window_for_surface(surface) { WindowState::with_state(&window, |state| { if let WindowResizeState::WaitingForCommit(new_pos) = state.resize_state { - // tracing::info!("Committing, new location"); state.resize_state = WindowResizeState::Idle; self.space.map_element(window.clone(), new_pos, false); } - // state.resize_state }); } } @@ -224,6 +223,24 @@ impl XdgShellHandler for State { fn new_toplevel(&mut self, surface: ToplevelSurface) { let window = Window::new(surface); + WindowState::with_state(&window, |state| { + state.tags = if let Some(focused_output) = &self.focus_state.focused_output { + OutputState::with(focused_output, |state| { + state + .focused_tags + .iter() + .filter_map(|(id, active)| active.then_some(id.clone())) + .collect() + }) + } else if let Some(first_tag) = self.tag_state.tags.first() { + vec![first_tag.id.clone()] + } else { + vec![] + }; + tracing::debug!("new window, tags are {:?}", state.tags); + }); + + self.windows.push(window.clone()); self.space.map_element(window.clone(), (0, 0), true); self.loop_handle.insert_idle(move |data| { data.state @@ -236,6 +253,7 @@ impl XdgShellHandler for State { SERIAL_COUNTER.next_serial(), ); }); + let windows: Vec = self.space.elements().cloned().collect(); self.loop_handle.insert_idle(|data| { @@ -245,6 +263,8 @@ impl XdgShellHandler for State { } fn toplevel_destroyed(&mut self, surface: ToplevelSurface) { + tracing::debug!("toplevel destroyed"); + self.windows.retain(|window| window.toplevel() != &surface); let mut windows: Vec = self.space.elements().cloned().collect(); windows.retain(|window| window.toplevel() != &surface); Layout::master_stack(self, windows, crate::layout::Direction::Left); @@ -345,15 +365,15 @@ impl XdgShellHandler for State { } fn ack_configure(&mut self, surface: WlSurface, configure: Configure) { - // TODO: add serial to WaitingForAck + tracing::debug!("start of ack_configure"); if let Some(window) = self.window_for_surface(&surface) { + tracing::debug!("found window for surface"); WindowState::with_state(&window, |state| { if let WindowResizeState::WaitingForAck(serial, new_loc) = state.resize_state { match &configure { Configure::Toplevel(configure) => { - // tracing::info!("acking before serial check"); if configure.serial >= serial { - // tracing::info!("acking, serial >="); + tracing::debug!("acked configure, new loc is {:?}", new_loc); state.resize_state = WindowResizeState::WaitingForCommit(new_loc); } } @@ -361,9 +381,31 @@ impl XdgShellHandler for State { } } }); + + // HACK: If a window is currently going through something that generates a bunch of + // | commits, like an animation, unmapping it while it's doing that has a chance + // | to cause any send_configures to not trigger a commit. I'm not sure if this is because of + // | the way I've implemented things or if it's something else. Because of me + // | mapping the element in commit, this means that the window won't reappear on a tag + // | change. The code below is a workaround until I can figure it out. + if !self.space.elements().any(|win| win == &window) { + tracing::debug!("remapping window"); + WindowState::with_state(&window, |state| { + if let WindowResizeState::WaitingForCommit(new_loc) = state.resize_state { + self.space.map_element(window.clone(), new_loc, false); + state.resize_state = WindowResizeState::Idle; + } + }); + } } } + // fn minimize_request(&mut self, surface: ToplevelSurface) { + // if let Some(window) = self.window_for_surface(surface.wl_surface()) { + // self.space.unmap_elem(&window); + // } + // } + // TODO: impl the rest of the fns in XdgShellHandler } delegate_xdg_shell!(@ State); diff --git a/src/input.rs b/src/input.rs index ae1f52d..82b7e6e 100644 --- a/src/input.rs +++ b/src/input.rs @@ -234,12 +234,23 @@ impl State { if modifiers.logo { modifier_mask.push(Modifiers::Super); } + let raw_sym = if keysym.raw_syms().len() == 1 { + keysym.raw_syms()[0] + } else { + keysyms::KEY_NoSymbol + }; if let Some(callback_id) = state .input_state .keybinds - .get(&(modifier_mask.into(), keysym.modified_sym())) + .get(&(modifier_mask.into(), raw_sym)) { return FilterResult::Intercept(*callback_id); + } else if modifiers.ctrl + && modifiers.shift + && modifiers.alt + && keysym.modified_sym() == keysyms::KEY_Escape + { + return FilterResult::Intercept(CallbackId(999999)); } } @@ -262,6 +273,9 @@ impl State { self.move_mode = move_mode; if let Some(callback_id) = action { + if callback_id.0 == 999999 { + self.loop_signal.stop(); + } if let Some(stream) = self.api_state.stream.as_ref() { if let Err(err) = crate::api::send_to_client( &mut stream.lock().expect("Could not lock stream mutex"), diff --git a/src/layout/automatic.rs b/src/layout/automatic.rs index d71c862..57786c2 100644 --- a/src/layout/automatic.rs +++ b/src/layout/automatic.rs @@ -44,6 +44,7 @@ impl Layout { }; let output_size = state.space.output_geometry(output).unwrap().size; if window_count == 1 { + tracing::debug!("Laying out only window"); let window = windows[0].clone(); window.toplevel().with_pending_state(|tl_state| { @@ -60,8 +61,10 @@ impl Layout { .unwrap() .initial_configure_sent }); + tracing::debug!("initial configure sent is {initial_configure_sent}"); if initial_configure_sent { WindowState::with_state(&window, |state| { + tracing::debug!("sending configure"); state.resize_state = WindowResizeState::WaitingForAck( window.toplevel().send_configure(), output.current_location(), @@ -72,6 +75,7 @@ impl Layout { return; } + tracing::debug!("layed out first window"); let mut windows = windows.iter(); let first_window = windows.next().unwrap(); @@ -105,15 +109,6 @@ impl Layout { let x = output.current_location().x + output_size.w / 2; for (i, win) in windows.enumerate() { - // let (min_size, _max_size) = match win.wl_surface() { - // Some(wl_surface) => compositor::with_states(&wl_surface, |states| { - // let data = states.cached_state.current::(); - // (data.min_size, data.max_size) - // }), - // None => ((0, 0).into(), (0, 0).into()), - // }; - // let min_height = - // i32::max(i32::max(0, win.geometry().loc.y.abs()) + 1, min_size.h); win.toplevel().with_pending_state(|state| { let mut new_size = output_size; new_size.w /= 2; diff --git a/src/main.rs b/src/main.rs index 2cc04a5..d1634bc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,6 +27,7 @@ mod output; mod pointer; mod render; mod state; +mod tag; mod window; mod xdg; diff --git a/src/output.rs b/src/output.rs index 6466c13..d440083 100644 --- a/src/output.rs +++ b/src/output.rs @@ -4,15 +4,15 @@ // // SPDX-License-Identifier: MPL-2.0 -use std::cell::RefCell; +use std::{cell::RefCell, collections::HashMap}; use smithay::output::Output; -use crate::window::tag::Tag; +use crate::tag::TagId; #[derive(Default)] pub struct OutputState { - focused_tags: Vec, + pub focused_tags: HashMap, } impl OutputState { @@ -22,7 +22,7 @@ impl OutputState { { output .user_data() - .insert_if_missing(|| RefCell::::default); + .insert_if_missing(RefCell::::default); let state = output .user_data() diff --git a/src/state.rs b/src/state.rs index b8f9742..a65498b 100644 --- a/src/state.rs +++ b/src/state.rs @@ -8,6 +8,7 @@ use std::{ error::Error, ffi::OsString, os::{fd::AsRawFd, unix::net::UnixStream}, + path::Path, process::Stdio, sync::{Arc, Mutex}, }; @@ -17,7 +18,11 @@ use crate::{ msg::{Args, CallbackId, Msg, OutgoingMsg, Request, RequestResponse}, PinnacleSocketSource, }, + backend::{udev::UdevData, winit::WinitData}, focus::FocusState, + layout::Layout, + output::OutputState, + tag::{Tag, TagState}, window::{window_state::WindowState, WindowProperties}, }; use calloop::futures::Scheduler; @@ -84,297 +89,239 @@ pub struct State { pub input_state: InputState, pub api_state: ApiState, pub focus_state: FocusState, + pub tag_state: TagState, pub popup_manager: PopupManager, pub cursor_status: CursorImageStatus, pub pointer_location: Point, + pub windows: Vec, pub async_scheduler: Scheduler<()>, } impl State { - /// Create the main [`State`]. - /// - /// This will set the WAYLAND_DISPLAY environment variable, insert Wayland necessary sources - /// into the event loop, and run an implementation of the config API (currently Lua). - pub fn init( - backend_data: B, - display: &mut Display, - loop_signal: LoopSignal, - loop_handle: LoopHandle<'static, CalloopData>, - ) -> Result> { - let socket = ListeningSocketSource::new_auto()?; - let socket_name = socket.socket_name().to_os_string(); + pub fn handle_msg(&mut self, msg: Msg) { + match msg { + Msg::SetKeybind { + key, + modifiers, + callback_id, + } => { + tracing::info!("set keybind: {:?}, {}", modifiers, key); + self.input_state + .keybinds + .insert((modifiers.into(), key), callback_id); + } + Msg::SetMousebind { button } => todo!(), + Msg::CloseWindow { client_id } => { + // TODO: client_id + tracing::info!("CloseWindow {:?}", client_id); + if let Some(window) = self.focus_state.current_focus() { + window.toplevel().send_close(); + } + } + Msg::ToggleFloating { client_id } => { + // TODO: add client_ids + if let Some(window) = self.focus_state.current_focus() { + crate::window::toggle_floating(self, &window); + } + } - std::env::set_var("WAYLAND_DISPLAY", socket_name.clone()); + Msg::Spawn { + command, + callback_id, + } => { + self.handle_spawn(command, callback_id); + } - // Opening a new process will use up a few file descriptors, around 10 for Alacritty, for - // example. Because of this, opening up only around 100 processes would exhaust the file - // descriptor limit on my system (Arch btw) and cause a "Too many open files" crash. - // - // To fix this, I just set the limit to be higher. As Pinnacle is the whole graphical - // environment, I *think* this is ok. - if let Err(err) = smithay::reexports::nix::sys::resource::setrlimit( - smithay::reexports::nix::sys::resource::Resource::RLIMIT_NOFILE, - 65536, - 65536 * 2, - ) { - tracing::error!("Could not raise fd limit: errno {err}"); - } + Msg::SetWindowSize { window_id, size } => { + let Some(window) = self.space.elements().find(|&win| { + WindowState::with_state(win, |state| state.id == window_id) + }) else { return; }; - loop_handle.insert_source(socket, |stream, _metadata, data| { - data.display - .handle() - .insert_client(stream, Arc::new(ClientState::default())) - .expect("Could not insert client into loop handle"); - })?; + // TODO: tiled vs floating + window.toplevel().with_pending_state(|state| { + state.size = Some(size.into()); + }); + window.toplevel().send_pending_configure(); + } + Msg::MoveWindowToTag { window_id, tag_id } => { + if let Some(window) = self + .windows + .iter() + .find(|&win| WindowState::with_state(win, |state| state.id == window_id)) + { + WindowState::with_state(window, |state| { + state.tags = vec![tag_id.clone()]; + }); + } - loop_handle.insert_source( - Generic::new( - display.backend().poll_fd().as_raw_fd(), - Interest::READ, - Mode::Level, - ), - |_readiness, _metadata, data| { - data.display.dispatch_clients(&mut data.state)?; - Ok(PostAction::Continue) - }, - )?; - - let (tx_channel, rx_channel) = calloop::channel::channel::(); - loop_handle.insert_source(rx_channel, |msg, _, data| match msg { - Event::Msg(msg) => { - // TODO: move this into its own function - // TODO: no like seriously this is getting a bit unwieldy - // TODO: no like rustfmt literally refuses to format the code below - match msg { - Msg::SetKeybind { - key, - modifiers, - callback_id, - } => { - tracing::info!("set keybind: {:?}, {}", modifiers, key); - data.state - .input_state - .keybinds - .insert((modifiers.into(), key), callback_id); - } - Msg::SetMousebind { button } => todo!(), - Msg::CloseWindow { client_id } => { - // TODO: client_id - tracing::info!("CloseWindow {:?}", client_id); - if let Some(window) = data.state.focus_state.current_focus() { - window.toplevel().send_close(); + self.re_layout(); + } + Msg::ToggleTagOnWindow { window_id, tag_id } => { + if let Some(window) = self + .windows + .iter() + .find(|&win| WindowState::with_state(win, |state| state.id == window_id)) + { + WindowState::with_state(window, |state| { + if state.tags.contains(&tag_id) { + state.tags.retain(|id| id != &tag_id); + } else { + state.tags.push(tag_id.clone()); } - } - Msg::ToggleFloating { client_id } => { - // TODO: add client_ids - if let Some(window) = data.state.focus_state.current_focus() { - crate::window::toggle_floating(&mut data.state, &window); + }); + + self.re_layout(); + } + } + Msg::ToggleTag { tag_id } => { + OutputState::with( + self.focus_state.focused_output.as_ref().unwrap(), // TODO: handle error + |state| match state.focused_tags.get_mut(&tag_id) { + Some(id) => { + *id = !*id; + tracing::debug!( + "toggled tag {tag_id:?} {}", + if *id { "on" } else { "off" } + ); } + None => { + state.focused_tags.insert(tag_id.clone(), true); + tracing::debug!("toggled tag {tag_id:?} on"); + } + }, + ); + + self.re_layout(); + } + Msg::SwitchToTag { tag_id } => { + OutputState::with(self.focus_state.focused_output.as_ref().unwrap(), |state| { + for (_, active) in state.focused_tags.iter_mut() { + *active = false; } - - Msg::Spawn { - command, - callback_id, - } => { - data.state.handle_spawn(command, callback_id); + if let Some(active) = state.focused_tags.get_mut(&tag_id) { + *active = true; + } else { + state.focused_tags.insert(tag_id.clone(), true); } - Msg::MoveToTag { tag } => todo!(), - Msg::ToggleTag { tag } => todo!(), + tracing::debug!("focused tags: {:?}", state.focused_tags); + }); - Msg::SetWindowSize { window_id, size } => { - let Some(window) = data.state.space.elements().find(|&win| { - WindowState::with_state(win, |state| state.id == window_id) - }) else { return; }; + self.re_layout(); + } + Msg::AddTags { tags } => { + self.tag_state.tags.extend(tags.into_iter().map(|tag| Tag { + id: tag, + windows: vec![], + })); + } + Msg::RemoveTags { tags } => { + self.tag_state.tags.retain(|tag| !tags.contains(&tag.id)); + } - // TODO: tiled vs floating - window.toplevel().with_pending_state(|state| { - state.size = Some(size.into()); + Msg::Quit => { + self.loop_signal.stop(); + } + + Msg::Request(request) => match request { + Request::GetWindowByAppId { id, app_id } => todo!(), + Request::GetWindowByTitle { id, title } => todo!(), + Request::GetWindowByFocus { id } => { + let Some(current_focus) = self.focus_state.current_focus() else { return; }; + let (app_id, title) = + compositor::with_states(current_focus.toplevel().wl_surface(), |states| { + let lock = states + .data_map + .get::() + .expect("XdgToplevelSurfaceData doesn't exist") + .lock() + .expect("Couldn't lock XdgToplevelSurfaceData"); + (lock.app_id.clone(), lock.title.clone()) }); - window.toplevel().send_pending_configure(); - } - - Msg::Quit => { - data.state.loop_signal.stop(); - } - - Msg::Request(request) => match request { - Request::GetWindowByAppId { id, app_id } => todo!(), - Request::GetWindowByTitle { id, title } => todo!(), - Request::GetWindowByFocus { id } => { - let Some(current_focus) = data.state.focus_state.current_focus() else { return; }; - let (app_id, title) = compositor::with_states( - current_focus.toplevel().wl_surface(), - |states| { - let lock = states. - data_map + let (window_id, floating) = WindowState::with_state(¤t_focus, |state| { + (state.id, state.floating.is_floating()) + }); + // TODO: unwrap + let location = self.space.element_location(¤t_focus).unwrap(); + let props = WindowProperties { + id: window_id, + app_id, + title, + size: current_focus.geometry().size.into(), + location: location.into(), + floating, + }; + let stream = self + .api_state + .stream + .as_ref() + .expect("Stream doesn't exist"); + let mut stream = stream.lock().expect("Couldn't lock stream"); + crate::api::send_to_client( + &mut stream, + &OutgoingMsg::RequestResponse { + request_id: id, + response: RequestResponse::Window { window: props }, + }, + ) + .expect("Send to client failed"); + } + Request::GetAllWindows { id } => { + let window_props = self + .space + .elements() + .map(|win| { + let (app_id, title) = + compositor::with_states(win.toplevel().wl_surface(), |states| { + let lock = states + .data_map .get::() .expect("XdgToplevelSurfaceData doesn't exist") .lock() .expect("Couldn't lock XdgToplevelSurfaceData"); (lock.app_id.clone(), lock.title.clone()) - } - ); - let (window_id, floating) = WindowState::with_state(¤t_focus, |state| { + }); + let (window_id, floating) = WindowState::with_state(win, |state| { (state.id, state.floating.is_floating()) }); // TODO: unwrap - let location = data.state.space.element_location(¤t_focus).unwrap(); - let props = WindowProperties { + let location = self + .space + .element_location(win) + .expect("Window location doesn't exist"); + WindowProperties { id: window_id, app_id, title, - size: current_focus.geometry().size.into(), + size: win.geometry().size.into(), location: location.into(), floating, - }; - let stream = data.state.api_state.stream.as_ref().expect("Stream doesn't exist"); - let mut stream = stream.lock().expect("Couldn't lock stream"); - crate::api::send_to_client( - &mut stream, - &OutgoingMsg::RequestResponse { - request_id: id, - response: RequestResponse::Window { window: props } - } - ) - .expect("Send to client failed"); + } + }) + .collect::>(); + + // FIXME: figure out what to do if error + let stream = self + .api_state + .stream + .as_ref() + .expect("Stream doesn't exist"); + let mut stream = stream.lock().expect("Couldn't lock stream"); + crate::api::send_to_client( + &mut stream, + &OutgoingMsg::RequestResponse { + request_id: id, + response: RequestResponse::GetAllWindows { + windows: window_props, + }, }, - Request::GetAllWindows { id } => { - let window_props = data.state.space.elements().map(|win| { - - let (app_id, title) = compositor::with_states( - win.toplevel().wl_surface(), - |states| { - let lock = states. - data_map - .get::() - .expect("XdgToplevelSurfaceData doesn't exist") - .lock() - .expect("Couldn't lock XdgToplevelSurfaceData"); - (lock.app_id.clone(), lock.title.clone()) - } - ); - let (window_id, floating) = WindowState::with_state(win, |state| { - (state.id, state.floating.is_floating()) - }); - // TODO: unwrap - let location = data.state.space.element_location(win).expect("Window location doesn't exist"); - WindowProperties { - id: window_id, - app_id, - title, - size: win.geometry().size.into(), - location: location.into(), - floating, - } - }).collect::>(); - - // FIXME: figure out what to do if error - let stream = data.state.api_state.stream.as_ref().expect("Stream doesn't exist"); - let mut stream = stream.lock().expect("Couldn't lock stream"); - crate::api::send_to_client( - &mut stream, - &OutgoingMsg::RequestResponse { - request_id: id, - response: RequestResponse::GetAllWindows { windows: window_props }, - } - ) - .expect("Couldn't send to client"); - } - }, - }; - } - Event::Closed => todo!(), - })?; - - // We want to replace the client if a new one pops up - // TODO: there should only ever be one client working at a time, and creating a new client - // | when one is already running should be impossible. - // INFO: this source try_clone()s the stream - loop_handle.insert_source(PinnacleSocketSource::new(tx_channel)?, |stream, _, data| { - if let Some(old_stream) = data - .state - .api_state - .stream - .replace(Arc::new(Mutex::new(stream))) - { - old_stream - .lock() - .expect("Couldn't lock old stream") - .shutdown(std::net::Shutdown::Both) - .expect("Couldn't shutdown old stream"); - } - })?; - - let (executor, sched) = calloop::futures::executor::<()>().expect("Couldn't create executor"); - loop_handle.insert_source(executor, |_, _, _| {})?; - - // TODO: move all this into the lua api - let config_path = std::env::var("PINNACLE_CONFIG").unwrap_or_else(|_| { - let mut default_path = - std::env::var("XDG_CONFIG_HOME").unwrap_or("~/.config".to_string()); - default_path.push_str("/pinnacle/init.lua"); - default_path - }); - - let lua_path = std::env::var("LUA_PATH").expect("Lua is not installed!"); - let mut local_lua_path = std::env::current_dir() - .expect("Couldn't get current dir") - .to_string_lossy() - .to_string(); - local_lua_path.push_str("/api/lua"); // TODO: get from crate root and do dynamically - let new_lua_path = - format!("{local_lua_path}/?.lua;{local_lua_path}/?/init.lua;{local_lua_path}/lib/?.lua;{local_lua_path}/lib/?/init.lua;{lua_path}"); - - let lua_cpath = std::env::var("LUA_CPATH").expect("Lua is not installed!"); - let new_lua_cpath = format!("{local_lua_path}/lib/?.so;{lua_cpath}"); - - std::process::Command::new("lua5.4") - .arg(config_path) - .env("LUA_PATH", new_lua_path) - .env("LUA_CPATH", new_lua_cpath) - .spawn() - .expect("Could not start config process"); - - let display_handle = display.handle(); - let mut seat_state = SeatState::new(); - let mut seat = seat_state.new_wl_seat(&display_handle, backend_data.seat_name()); - seat.add_pointer(); - seat.add_keyboard(XkbConfig::default(), 200, 25)?; - - Ok(Self { - backend_data, - loop_signal, - loop_handle, - clock: Clock::::new()?, - compositor_state: CompositorState::new::(&display_handle), - data_device_state: DataDeviceState::new::(&display_handle), - seat_state, - pointer_location: (0.0, 0.0).into(), - shm_state: ShmState::new::(&display_handle, vec![]), - space: Space::::default(), - cursor_status: CursorImageStatus::Default, - output_manager_state: OutputManagerState::new_with_xdg_output::(&display_handle), - xdg_shell_state: XdgShellState::new::(&display_handle), - viewporter_state: ViewporterState::new::(&display_handle), - fractional_scale_manager_state: FractionalScaleManagerState::new::( - &display_handle, - ), - input_state: InputState::new(), - api_state: ApiState::new(), - focus_state: FocusState::new(), - - seat, - - move_mode: false, - socket_name: socket_name.to_string_lossy().to_string(), - - popup_manager: PopupManager::default(), - - async_scheduler: sched, - }) + ) + .expect("Couldn't send to client"); + } + }, + } } pub fn handle_spawn(&self, command: Vec, callback_id: Option) { @@ -412,11 +359,15 @@ impl State { return; }; - // TODO: find a way to make this hellish code look better, deal with unwraps if let Some(callback_id) = callback_id { let stdout = child.stdout.take(); let stderr = child.stderr.take(); - let stream_out = self.api_state.stream.as_ref().expect("Stream doesn't exist").clone(); + let stream_out = self + .api_state + .stream + .as_ref() + .expect("Stream doesn't exist") + .clone(); let stream_err = stream_out.clone(); let stream_exit = stream_out.clone(); @@ -447,7 +398,7 @@ impl State { Err(err) => { tracing::warn!("child read err: {err}"); break; - }, + } } } }; @@ -483,7 +434,7 @@ impl State { Err(err) => { tracing::warn!("child read err: {err}"); break; - }, + } } } }; @@ -520,6 +471,350 @@ impl State { } } } + + pub fn re_layout(&mut self) { + let windows = + OutputState::with(self.focus_state.focused_output.as_ref().unwrap(), |state| { + for window in self.space.elements().cloned().collect::>() { + let should_render = WindowState::with_state(&window, |win_state| { + for tag_id in win_state.tags.iter() { + if *state.focused_tags.get(tag_id).unwrap_or(&false) { + return true; + } + } + false + }); + if !should_render { + self.space.unmap_elem(&window); + } + } + + self.windows + .iter() + .filter(|&win| { + WindowState::with_state(win, |win_state| { + for tag_id in win_state.tags.iter() { + if *state.focused_tags.get(tag_id).unwrap_or(&false) { + return true; + } + } + false + }) + }) + .cloned() + .collect::>() + }); + + tracing::debug!("Laying out {} windows", windows.len()); + + Layout::master_stack(self, windows, crate::layout::Direction::Left); + } +} + +impl State { + /// Create the main [`State`]. + /// + /// This will set the WAYLAND_DISPLAY environment variable, insert Wayland necessary sources + /// into the event loop, and run an implementation of the config API (currently Lua). + pub fn init( + backend_data: WinitData, + display: &mut Display, + loop_signal: LoopSignal, + loop_handle: LoopHandle<'static, CalloopData>, + ) -> Result> { + let socket = ListeningSocketSource::new_auto()?; + let socket_name = socket.socket_name().to_os_string(); + + std::env::set_var("WAYLAND_DISPLAY", socket_name.clone()); + + // Opening a new process will use up a few file descriptors, around 10 for Alacritty, for + // example. Because of this, opening up only around 100 processes would exhaust the file + // descriptor limit on my system (Arch btw) and cause a "Too many open files" crash. + // + // To fix this, I just set the limit to be higher. As Pinnacle is the whole graphical + // environment, I *think* this is ok. + if let Err(err) = smithay::reexports::nix::sys::resource::setrlimit( + smithay::reexports::nix::sys::resource::Resource::RLIMIT_NOFILE, + 65536, + 65536 * 2, + ) { + tracing::error!("Could not raise fd limit: errno {err}"); + } + + loop_handle.insert_source(socket, |stream, _metadata, data| { + data.display + .handle() + .insert_client(stream, Arc::new(ClientState::default())) + .expect("Could not insert client into loop handle"); + })?; + + loop_handle.insert_source( + Generic::new( + display.backend().poll_fd().as_raw_fd(), + Interest::READ, + Mode::Level, + ), + |_readiness, _metadata, data| { + data.display.dispatch_clients(&mut data.state)?; + Ok(PostAction::Continue) + }, + )?; + + let (tx_channel, rx_channel) = calloop::channel::channel::(); + loop_handle.insert_source(rx_channel, |msg, _, data| match msg { + Event::Msg(msg) => data.state.handle_msg(msg), + Event::Closed => todo!(), + })?; + + // We want to replace the client if a new one pops up + // TODO: there should only ever be one client working at a time, and creating a new client + // | when one is already running should be impossible. + // INFO: this source try_clone()s the stream + loop_handle.insert_source(PinnacleSocketSource::new(tx_channel)?, |stream, _, data| { + if let Some(old_stream) = data + .state + .api_state + .stream + .replace(Arc::new(Mutex::new(stream))) + { + old_stream + .lock() + .expect("Couldn't lock old stream") + .shutdown(std::net::Shutdown::Both) + .expect("Couldn't shutdown old stream"); + } + })?; + + let (executor, sched) = + calloop::futures::executor::<()>().expect("Couldn't create executor"); + loop_handle.insert_source(executor, |_, _, _| {})?; + + // TODO: move all this into the lua api + let config_path = std::env::var("PINNACLE_CONFIG").unwrap_or_else(|_| { + let mut default_path = + std::env::var("XDG_CONFIG_HOME").unwrap_or("~/.config".to_string()); + default_path.push_str("/pinnacle/init.lua"); + default_path + }); + + if Path::new(&config_path).exists() { + let lua_path = std::env::var("LUA_PATH").expect("Lua is not installed!"); + let mut local_lua_path = std::env::current_dir() + .expect("Couldn't get current dir") + .to_string_lossy() + .to_string(); + local_lua_path.push_str("/api/lua"); // TODO: get from crate root and do dynamically + let new_lua_path = + format!("{local_lua_path}/?.lua;{local_lua_path}/?/init.lua;{local_lua_path}/lib/?.lua;{local_lua_path}/lib/?/init.lua;{lua_path}"); + + let lua_cpath = std::env::var("LUA_CPATH").expect("Lua is not installed!"); + let new_lua_cpath = format!("{local_lua_path}/lib/?.so;{lua_cpath}"); + + std::process::Command::new("lua5.4") + .arg(config_path) + .env("LUA_PATH", new_lua_path) + .env("LUA_CPATH", new_lua_cpath) + .spawn() + .expect("Could not start config process"); + } else { + tracing::error!("Could not find {}", config_path); + } + + let display_handle = display.handle(); + let mut seat_state = SeatState::new(); + let mut seat = seat_state.new_wl_seat(&display_handle, backend_data.seat_name()); + seat.add_pointer(); + seat.add_keyboard(XkbConfig::default(), 200, 25)?; + + Ok(Self { + backend_data, + loop_signal, + loop_handle, + clock: Clock::::new()?, + compositor_state: CompositorState::new::(&display_handle), + data_device_state: DataDeviceState::new::(&display_handle), + seat_state, + pointer_location: (0.0, 0.0).into(), + shm_state: ShmState::new::(&display_handle, vec![]), + space: Space::::default(), + cursor_status: CursorImageStatus::Default, + output_manager_state: OutputManagerState::new_with_xdg_output::(&display_handle), + xdg_shell_state: XdgShellState::new::(&display_handle), + viewporter_state: ViewporterState::new::(&display_handle), + fractional_scale_manager_state: FractionalScaleManagerState::new::( + &display_handle, + ), + input_state: InputState::new(), + api_state: ApiState::new(), + focus_state: FocusState::new(), + tag_state: TagState::new(), + + seat, + + move_mode: false, + socket_name: socket_name.to_string_lossy().to_string(), + + popup_manager: PopupManager::default(), + + async_scheduler: sched, + + windows: vec![], + }) + } +} + +impl State { + pub fn init( + backend_data: UdevData, + display: &mut Display, + loop_signal: LoopSignal, + loop_handle: LoopHandle<'static, CalloopData>, + ) -> Result> { + let socket = ListeningSocketSource::new_auto()?; + let socket_name = socket.socket_name().to_os_string(); + + std::env::set_var("WAYLAND_DISPLAY", socket_name.clone()); + + // Opening a new process will use up a few file descriptors, around 10 for Alacritty, for + // example. Because of this, opening up only around 100 processes would exhaust the file + // descriptor limit on my system (Arch btw) and cause a "Too many open files" crash. + // + // To fix this, I just set the limit to be higher. As Pinnacle is the whole graphical + // environment, I *think* this is ok. + if let Err(err) = smithay::reexports::nix::sys::resource::setrlimit( + smithay::reexports::nix::sys::resource::Resource::RLIMIT_NOFILE, + 65536, + 65536 * 2, + ) { + tracing::error!("Could not raise fd limit: errno {err}"); + } + + loop_handle.insert_source(socket, |stream, _metadata, data| { + data.display + .handle() + .insert_client(stream, Arc::new(ClientState::default())) + .expect("Could not insert client into loop handle"); + })?; + + loop_handle.insert_source( + Generic::new( + display.backend().poll_fd().as_raw_fd(), + Interest::READ, + Mode::Level, + ), + |_readiness, _metadata, data| { + data.display.dispatch_clients(&mut data.state)?; + Ok(PostAction::Continue) + }, + )?; + + let (tx_channel, rx_channel) = calloop::channel::channel::(); + + // We want to replace the client if a new one pops up + // TODO: there should only ever be one client working at a time, and creating a new client + // | when one is already running should be impossible. + // INFO: this source try_clone()s the stream + loop_handle.insert_source(PinnacleSocketSource::new(tx_channel)?, |stream, _, data| { + if let Some(old_stream) = data + .state + .api_state + .stream + .replace(Arc::new(Mutex::new(stream))) + { + old_stream + .lock() + .expect("Couldn't lock old stream") + .shutdown(std::net::Shutdown::Both) + .expect("Couldn't shutdown old stream"); + } + })?; + + let (executor, sched) = + calloop::futures::executor::<()>().expect("Couldn't create executor"); + loop_handle.insert_source(executor, |_, _, _| {})?; + + // TODO: move all this into the lua api + let config_path = std::env::var("PINNACLE_CONFIG").unwrap_or_else(|_| { + let mut default_path = + std::env::var("XDG_CONFIG_HOME").unwrap_or("~/.config".to_string()); + default_path.push_str("/pinnacle/init.lua"); + default_path + }); + + if Path::new(&config_path).exists() { + let lua_path = std::env::var("LUA_PATH").expect("Lua is not installed!"); + let mut local_lua_path = std::env::current_dir() + .expect("Couldn't get current dir") + .to_string_lossy() + .to_string(); + local_lua_path.push_str("/api/lua"); // TODO: get from crate root and do dynamically + let new_lua_path = + format!("{local_lua_path}/?.lua;{local_lua_path}/?/init.lua;{local_lua_path}/lib/?.lua;{local_lua_path}/lib/?/init.lua;{lua_path}"); + + let lua_cpath = std::env::var("LUA_CPATH").expect("Lua is not installed!"); + let new_lua_cpath = format!("{local_lua_path}/lib/?.so;{lua_cpath}"); + + std::process::Command::new("lua5.4") + .arg(config_path) + .env("LUA_PATH", new_lua_path) + .env("LUA_CPATH", new_lua_cpath) + .spawn() + .expect("Could not start config process"); + } else { + tracing::error!("Could not find {}", config_path); + } + + let display_handle = display.handle(); + let mut seat_state = SeatState::new(); + let mut seat = seat_state.new_wl_seat(&display_handle, backend_data.seat_name()); + seat.add_pointer(); + seat.add_keyboard(XkbConfig::default(), 200, 25)?; + + loop_handle.insert_idle(|data| { + data.state + .loop_handle + .insert_source(rx_channel, |msg, _, data| match msg { + Event::Msg(msg) => data.state.handle_msg(msg), + Event::Closed => todo!(), + }) + .unwrap(); // TODO: unwrap + }); + + Ok(Self { + backend_data, + loop_signal, + loop_handle, + clock: Clock::::new()?, + compositor_state: CompositorState::new::(&display_handle), + data_device_state: DataDeviceState::new::(&display_handle), + seat_state, + pointer_location: (0.0, 0.0).into(), + shm_state: ShmState::new::(&display_handle, vec![]), + space: Space::::default(), + cursor_status: CursorImageStatus::Default, + output_manager_state: OutputManagerState::new_with_xdg_output::(&display_handle), + xdg_shell_state: XdgShellState::new::(&display_handle), + viewporter_state: ViewporterState::new::(&display_handle), + fractional_scale_manager_state: FractionalScaleManagerState::new::( + &display_handle, + ), + input_state: InputState::new(), + api_state: ApiState::new(), + focus_state: FocusState::new(), + tag_state: TagState::new(), + + seat, + + move_mode: false, + socket_name: socket_name.to_string_lossy().to_string(), + + popup_manager: PopupManager::default(), + + async_scheduler: sched, + + windows: vec![], + }) + } } pub struct CalloopData { @@ -535,8 +830,6 @@ impl ClientData for ClientState { fn initialized(&self, _client_id: ClientId) {} fn disconnected(&self, _client_id: ClientId, _reason: DisconnectReason) {} - - // fn debug(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {} } #[derive(Debug, Copy, Clone)] diff --git a/src/tag.rs b/src/tag.rs new file mode 100644 index 0000000..d3ce220 --- /dev/null +++ b/src/tag.rs @@ -0,0 +1,28 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// +// SPDX-License-Identifier: MPL-2.0 + +use smithay::desktop::Window; + +#[derive(Debug, Hash, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)] +pub struct TagId(String); + +#[derive(Debug)] +pub struct Tag { + pub id: TagId, + pub windows: Vec, + // TODO: layout +} + +#[derive(Debug, Default)] +pub struct TagState { + pub tags: Vec, +} + +impl TagState { + pub fn new() -> Self { + Default::default() + } +} diff --git a/src/window.rs b/src/window.rs index 71d54e7..9c80bb7 100644 --- a/src/window.rs +++ b/src/window.rs @@ -18,7 +18,6 @@ use crate::{ use self::window_state::{Float, WindowId, WindowState}; -pub mod tag; pub mod window_state; // TODO: maybe get rid of this and move the fn into resize_surface state because it's the only user @@ -51,6 +50,12 @@ impl State { .elements() .find(|window| window.wl_surface().map(|s| s == *surface).unwrap_or(false)) .cloned() + .or_else(|| { + self.windows + .iter() + .find(|&win| win.toplevel().wl_surface() == surface) + .cloned() + }) } /// Swap the positions and sizes of two windows. diff --git a/src/window/tag.rs b/src/window/tag.rs deleted file mode 100644 index f808aaa..0000000 --- a/src/window/tag.rs +++ /dev/null @@ -1,28 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. -// -// SPDX-License-Identifier: MPL-2.0 - -use smithay::desktop::Window; - -use crate::{backend::Backend, state::State}; - -use super::window_state::WindowState; - -#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct Tag(String); - -impl Tag { - /// Returns all windows that have this tag. - pub fn windows(&self, state: &State) -> Vec { - state - .space - .elements() - .filter(|&window| { - WindowState::with_state(window, |win_state| win_state.tags.contains(self)) - }) - .cloned() - .collect() - } -} diff --git a/src/window/window_state.rs b/src/window/window_state.rs index 5e521a0..c8f1731 100644 --- a/src/window/window_state.rs +++ b/src/window/window_state.rs @@ -14,11 +14,12 @@ use smithay::{ utils::{Logical, Point, Serial, Size}, }; -use super::tag::Tag; +use crate::tag::{Tag, TagId, TagState}; #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct WindowId(u32); +// TODO: this probably doesn't need to be atomic static WINDOW_ID_COUNTER: AtomicU32 = AtomicU32::new(0); impl WindowId { @@ -35,7 +36,16 @@ pub struct WindowState { /// The window's resize state. See [WindowResizeState] for more. pub resize_state: WindowResizeState, /// What tags the window is currently on. - pub tags: Vec, + pub tags: Vec, +} + +/// Returns a vec of references to all the tags the window is on. +pub fn tags<'a>(tag_state: &'a TagState, window: &Window) -> Vec<&'a Tag> { + tag_state + .tags + .iter() + .filter(|&tag| WindowState::with_state(window, |state| state.tags.contains(&tag.id))) + .collect() } /// The state of a window's resize operation. @@ -116,12 +126,11 @@ impl WindowState { .user_data() .insert_if_missing(RefCell::::default); - let mut state = window + let state = window .user_data() .get::>() - .expect("This should never happen") - .borrow_mut(); - func(&mut state) + .expect("This should never happen"); + func(&mut state.borrow_mut()) } }