Add output to API

This commit is contained in:
Ottatop 2023-07-11 11:59:38 -05:00 committed by Ottatop
parent 930925a8f1
commit d91c06dbe9
13 changed files with 415 additions and 102 deletions

View file

@ -15,6 +15,7 @@ require("pinnacle").setup(function(pinnacle)
local window = pinnacle.window -- Window management
local process = pinnacle.process -- Process spawning
local tag = pinnacle.tag -- Tag management
local output = pinnacle.output -- Output management
-- Every key supported by xkbcommon.
-- Support for just putting in a string of a key is intended.
@ -27,6 +28,7 @@ require("pinnacle").setup(function(pinnacle)
local terminal = "alacritty"
-- Keybinds ----------------------------------------------------------------------
input.keybind({ mod_key, "Alt" }, keys.q, pinnacle.quit)
input.keybind({ mod_key, "Alt" }, keys.c, window.close_window)
@ -49,8 +51,18 @@ require("pinnacle").setup(function(pinnacle)
process.spawn("nautilus")
end)
input.keybind({ mod_key }, keys.g, function()
local op = output.get_by_res(2560, 1440)
for _, v in pairs(op) do
print(v.name)
end
end)
-- Tags ---------------------------------------------------------------------------
tag.add("1", "2", "3", "4", "5")
output.connect_for_all(function(op)
tag.add(op, "1", "2", "3", "4", "5")
end)
tag.toggle("1")
input.keybind({ mod_key }, keys.KEY_1, function()

View file

@ -9,9 +9,19 @@ local input = {
}
---Set a keybind. If called with an already existing keybind, it gets replaced.
---
---# Example
---
---```lua
----- The following sets Super + Return to open Alacritty
---
---input.keybind({ "Super" }, input.keys.Return, function()
--- process.spawn("Alacritty")
---end)
---```
---@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.
---@param action fun() What to do.
function input.keybind(modifiers, key, action)
table.insert(CallbackTable, action)
SendMsg({

View file

@ -7,7 +7,7 @@
---@alias Modifier "Alt" | "Ctrl" | "Shift" | "Super"
---@enum Keys
local M = {
local keys = {
NoSymbol = 0x00000000,
VoidSymbol = 0x00ffffff,
@ -4321,4 +4321,4 @@ local M = {
block = 0x100000fc,
}
return M
return keys

View file

@ -7,7 +7,7 @@
---@meta _
---@class _Msg
---@field SetKeybind { key: Keys, modifiers: Modifiers[], callback_id: integer }
---@field SetKeybind { key: Keys, modifiers: Modifier[], callback_id: integer }
---@field SetMousebind { button: integer }
--Windows
---@field CloseWindow { client_id: integer? }
@ -21,25 +21,38 @@
--Tags
---@field ToggleTag { tag_id: string }
---@field SwitchToTag { tag_id: string }
---@field AddTags { tags: string[] }
---@field RemoveTags { tags: string[] }
---@field AddTags { output_name: string, tags: string[] }
---@field RemoveTags { output_name: string, tags: string[] }
--Outputs
---@field ConnectForAllOutputs { callback_id: integer }
---@alias Msg _Msg | "Quit"
---@class Request
---@field GetWindowByFocus { id: integer }
---@field GetAllWindows { id: integer }
--------------------------------------------------------------------------------------------
---@class _Request
--Windows
---@field GetWindowByAppId { app_id: string }
---@field GetWindowByTitle { title: string }
--Outputs
---@field GetOutputByName { name: string }
---@field GetOutputsByModel { model: string }
---@field GetOutputsByRes { res: integer[] }
---@alias Request _Request | "GetWindowByFocus" | "GetAllWindows"
---@class IncomingMsg
---@field CallCallback { callback_id: integer, args: Args }
---@field RequestResponse { request_id: integer, response: RequestResponse }
---@field RequestResponse { response: RequestResponse }
---@class Args
---@field Spawn { stdout: string?, stderr: string?, exit_code: integer?, exit_msg: string? }
---@field ConnectForAllOutputs { output_name: string }
---@class RequestResponse
---@field Window { window: WindowProperties }
---@field GetAllWindows { windows: WindowProperties[] }
---@field Outputs { names: string[] }
---@class WindowProperties
---@field id integer

133
api/lua/output.lua Normal file
View file

@ -0,0 +1,133 @@
-- 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
---@class Output A display.
---@field name string The name of this output (or rather, of its connector).
local op = {}
---Add methods to this output.
---@param props Output
---@return Output
local function new_output(props)
-- Copy functions over
for k, v in pairs(op) do
props[k] = v
end
return props
end
------------------------------------------------------
local output = {}
---Get an output by its name.
---
---"Name" in this sense does not mean its model or manufacturer;
---rather, "name" is the name of the connector the output is connected to.
---This should be something like "HDMI-A-0", "eDP-1", or similar.
---
---# Examples
---```lua
---local monitor = output.get_by_name("DP-1")
---print(monitor.name) -- should print `DP-1`
---```
---@param name string The name of the output.
---@return Output|nil
function output.get_by_name(name)
SendMsg({
Request = {
GetOutputByName = {
name = name,
},
},
})
local response = ReadMsg()
local names = response.RequestResponse.response.Outputs.names
if names[1] ~= nil then
return new_output({ name = names[1] })
else
return nil
end
end
---NOTE: This may or may not be what is reported by other monitor listing utilities. One of my monitors fails to report itself in Smithay when it is correctly picked up by tools like wlr-randr. I'll fix this in the future.
---
---Get outputs by their model.
---This is something like "DELL E2416H" or whatever gibberish monitor manufacturers call their displays.
---@param model string The model of the output(s).
---@return Output[] outputs All outputs with this model. If there are none, the returned table will be empty.
function output.get_by_model(model)
SendMsg({
Request = {
GetOutputsByModel = {
model = model,
},
},
})
local response = ReadMsg()
local names = response.RequestResponse.response.Outputs.names
---@type Output
local outputs = {}
for _, v in pairs(names) do
table.insert(outputs, new_output({ name = v }))
end
return outputs
end
---Get outputs by their resolution.
---
---@param width integer The width of the outputs, in pixels.
---@param height integer The height of the outputs, in pixels.
---@return Output[] outputs All outputs with this resolution. If there are none, the returned table will be empty.
function output.get_by_res(width, height)
SendMsg({
Request = {
GetOutputsByRes = {
res = { width, height },
},
},
})
local response = ReadMsg()
local names = response.RequestResponse.response.Outputs.names
---@type Output
local outputs = {}
for _, v in pairs(names) do
table.insert(outputs, new_output({ name = v }))
end
return outputs
end
---Connect a function to be run on all current and future outputs.
---
---When called, `connect_for_all` will immediately run `func` with all currently connected outputs.
---If a new output is connected, `func` will also be called with it.
---@param func fun(output: Output) The function that will be run.
function output.connect_for_all(func)
---@param args Args
table.insert(CallbackTable, function(args)
local args = args.ConnectForAllOutputs
func(new_output({ name = args.output_name }))
end)
SendMsg({
ConnectForAllOutputs = {
callback_id = #CallbackTable,
},
})
end
return output

View file

@ -56,6 +56,8 @@ local pinnacle = {
process = require("process"),
---Tag management
tag = require("tag"),
---Output management
output = require("output"),
}
---Quit Pinnacle.
@ -114,15 +116,6 @@ function pinnacle.setup(config_func)
return tb
end
Requests = {
id = 1,
}
function Requests:next()
local id = self.id
self.id = self.id + 1
return id
end
config_func(pinnacle)
while true do

View file

@ -8,36 +8,62 @@ local tag = {}
---Add tags.
---
---If you need to add the strings in a table, use `tag.add_table` instead.
---If you need to add the names as a table, use `tag.add_table` instead.
---
---# Example
---
---```lua
---tag.add("1", "2", "3", "4", "5") -- Add tags with names 1-5
---local output = output.get_by_name("DP-1")
---if output ~= nil then
--- tag.add(output, "1", "2", "3", "4", "5") -- Add tags with names 1-5
---end
---```
---@param output Output The output you want these tags to be added to.
---@param ... string The names of the new tags you want to add.
function tag.add(...)
function tag.add(output, ...)
local tags = table.pack(...)
tags["n"] = nil
SendMsg({
AddTags = {
output_name = output.name,
tags = tags,
},
})
end
---Like `tag.add`, but with a table of strings instead.
---
---# Example
---
---```lua
---local tags = { "Terminal", "Browser", "Mail", "Gaming", "Potato" }
---local output = output.get_by_name("DP-1")
---if output ~= nil then
--- tag.add(output, tags) -- Add tags with the names above
---end
---```
---@param output Output The output you want these tags to be added to.
---@param tags string[] The names of the new tags you want to add, as a table.
function tag.add_table(tags)
function tag.add_table(output, tags)
SendMsg({
AddTags = {
output_name = output.name,
tags = tags,
},
})
end
---Toggle a tag's display.
---Toggle a tag on the currently focused output.
---
---# Example
---
---```lua
----- Assuming all tags are toggled off...
---tag.toggle("1")
---tag.toggle("2")
----- will cause windows on both tags 1 and 2 to be displayed at the same time.
---```
---@param name string The name of the tag.
function tag.toggle(name)
SendMsg({
@ -47,7 +73,15 @@ function tag.toggle(name)
})
end
---Switch to a tag, deactivating any other active tags.
---Switch to a tag on the currently focused output, deactivating any other active tags on that output.
---
---This is used to replicate what a traditional workspace is on some other Wayland compositors.
---
---# Example
---
---```lua
---tag.switch_to("3") -- Switches to and displays *only* windows on tag 3
---```
---@param name string The name of the tag.
function tag.switch_to(name)
SendMsg({

View file

@ -13,7 +13,7 @@
---@field private floating boolean Whether the window is floating or not (tiled)
local win = {}
---@param props { id: integer, app_id: string?, title: string?, size: { w: integer, h: integer }, location: { x: integer, y: integer }, floating: boolean }
---@param props Window
---@return Window
local function new_window(props)
-- Copy functions over
@ -91,15 +91,14 @@ function window.toggle_floating(client_id)
})
end
---TODO: This function is not implemented yet.
---
---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()
SendRequest({
GetWindowByAppId = {
id = req_id,
app_id = app_id,
},
})
@ -127,15 +126,14 @@ function window.get_by_app_id(app_id)
return new_window(wind)
end
---TODO: This function is not implemented yet.
---
---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,
},
})
@ -166,13 +164,7 @@ end
---Get the currently focused window.
---@return Window
function window.get_focused()
local req_id = Requests:next()
SendRequest({
GetWindowByFocus = {
id = req_id,
},
})
SendRequest("GetWindowByFocus")
local response = ReadMsg()
@ -199,12 +191,8 @@ end
---Get all windows.
---@return Window[]
function window.get_windows()
SendRequest({
GetAllWindows = {
id = Requests:next(),
},
})
function window.get_all()
SendRequest("GetAllWindows")
-- INFO: these read synchronously so this should always work IF the server works correctly

View file

@ -17,7 +17,7 @@ pub enum Msg {
// Input
SetKeybind {
key: u32,
modifiers: Vec<Modifiers>,
modifiers: Vec<Modifier>,
callback_id: CallbackId,
},
SetMousebind {
@ -47,20 +47,30 @@ pub enum Msg {
},
// Tag management
// FIXME: tag_id should not be a string
ToggleTag {
tag_id: String,
},
// FIXME: tag_id should not be a string
SwitchToTag {
tag_id: String,
},
AddTags {
/// The name of the output you want these tags on.
output_name: String,
tags: Vec<String>,
},
RemoveTags {
// TODO:
/// The name of the output you want these tags removed from.
output_name: String,
tags: Vec<String>,
},
// Output management
ConnectForAllOutputs {
callback_id: CallbackId,
},
// Process management
/// Spawn a program with an optional callback.
Spawn {
@ -82,14 +92,17 @@ pub struct RequestId(pub u32);
#[derive(Debug, serde::Serialize, serde::Deserialize)]
/// Messages that require a server response, usually to provide some data.
pub enum Request {
GetWindowByAppId { id: RequestId, app_id: String },
GetWindowByTitle { id: RequestId, title: String },
GetWindowByFocus { id: RequestId },
GetAllWindows { id: RequestId },
GetWindowByAppId { app_id: String },
GetWindowByTitle { title: String },
GetWindowByFocus,
GetAllWindows,
GetOutputByName { name: String },
GetOutputsByModel { model: String },
GetOutputsByRes { res: (u32, u32) },
}
#[derive(Debug, PartialEq, Eq, Copy, Clone, serde::Serialize, serde::Deserialize)]
pub enum Modifiers {
pub enum Modifier {
Shift = 0b0000_0001,
Ctrl = 0b0000_0010,
Alt = 0b0000_0100,
@ -100,7 +113,7 @@ pub enum Modifiers {
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
pub struct ModifierMask(u8);
impl<T: IntoIterator<Item = Modifiers>> From<T> for ModifierMask {
impl<T: IntoIterator<Item = Modifier>> From<T> for ModifierMask {
fn from(value: T) -> Self {
let value = value.into_iter();
let mut mask: u8 = 0b0000_0000;
@ -112,19 +125,19 @@ impl<T: IntoIterator<Item = Modifiers>> From<T> for ModifierMask {
}
impl ModifierMask {
pub fn values(self) -> Vec<Modifiers> {
let mut res = Vec::<Modifiers>::new();
if self.0 & Modifiers::Shift as u8 == Modifiers::Shift as u8 {
res.push(Modifiers::Shift);
pub fn values(self) -> Vec<Modifier> {
let mut res = Vec::<Modifier>::new();
if self.0 & Modifier::Shift as u8 == Modifier::Shift as u8 {
res.push(Modifier::Shift);
}
if self.0 & Modifiers::Ctrl as u8 == Modifiers::Ctrl as u8 {
res.push(Modifiers::Ctrl);
if self.0 & Modifier::Ctrl as u8 == Modifier::Ctrl as u8 {
res.push(Modifier::Ctrl);
}
if self.0 & Modifiers::Alt as u8 == Modifiers::Alt as u8 {
res.push(Modifiers::Alt);
if self.0 & Modifier::Alt as u8 == Modifier::Alt as u8 {
res.push(Modifier::Alt);
}
if self.0 & Modifiers::Super as u8 == Modifiers::Super as u8 {
res.push(Modifiers::Super);
if self.0 & Modifier::Super as u8 == Modifier::Super as u8 {
res.push(Modifier::Super);
}
res
}
@ -139,7 +152,6 @@ pub enum OutgoingMsg {
args: Option<Args>,
},
RequestResponse {
request_id: RequestId,
response: RequestResponse,
},
}
@ -157,10 +169,14 @@ pub enum Args {
#[serde(default)]
exit_msg: Option<String>,
},
ConnectForAllOutputs {
output_name: String,
},
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub enum RequestResponse {
Window { window: WindowProperties },
GetAllWindows { windows: Vec<WindowProperties> },
Outputs { names: Vec<String> },
}

View file

@ -97,6 +97,7 @@ use smithay_drm_extras::{
};
use crate::{
api::msg::{Args, OutgoingMsg},
render::{pointer::PointerElement, CustomRenderElements, OutputRenderElements},
state::{take_presentation_feedback, CalloopData, State, SurfaceDmabufFeedback},
};
@ -226,11 +227,6 @@ pub fn run_udev() -> Result<(), Box<dyn Error>> {
pointer_element: PointerElement::default(),
};
//
//
//
//
let mut state = State::<UdevData>::init(
data,
&mut display,
@ -852,6 +848,32 @@ impl State<UdevData> {
device_id: node,
});
// Run any connected callbacks
{
let clone = output.clone();
self.loop_handle.insert_idle(move |data| {
let stream = data
.state
.api_state
.stream
.as_ref()
.expect("Stream doesn't exist");
let mut stream = stream.lock().expect("Couldn't lock stream");
for callback_id in data.state.output_callback_ids.iter() {
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::CallCallback {
callback_id: *callback_id,
args: Some(Args::ConnectForAllOutputs {
output_name: clone.name(),
}),
},
)
.expect("Send to client failed");
}
});
}
let allocator = GbmAllocator::new(
device.gbm.clone(),
GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT,

View file

@ -6,7 +6,7 @@
use std::collections::HashMap;
use crate::api::msg::{CallbackId, ModifierMask, Modifiers, OutgoingMsg};
use crate::api::msg::{CallbackId, Modifier, ModifierMask, OutgoingMsg};
use smithay::{
backend::input::{
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, InputBackend, InputEvent,
@ -221,18 +221,18 @@ impl<B: Backend> State<B> {
time,
|state, modifiers, keysym| {
if press_state == KeyState::Pressed {
let mut modifier_mask = Vec::<Modifiers>::new();
let mut modifier_mask = Vec::<Modifier>::new();
if modifiers.alt {
modifier_mask.push(Modifiers::Alt);
modifier_mask.push(Modifier::Alt);
}
if modifiers.shift {
modifier_mask.push(Modifiers::Shift);
modifier_mask.push(Modifier::Shift);
}
if modifiers.ctrl {
modifier_mask.push(Modifiers::Ctrl);
modifier_mask.push(Modifier::Ctrl);
}
if modifiers.logo {
modifier_mask.push(Modifiers::Super);
modifier_mask.push(Modifier::Super);
}
let raw_sym = if keysym.raw_syms().len() == 1 {
keysym.raw_syms()[0]

View file

@ -98,6 +98,10 @@ pub struct State<B: Backend> {
pub windows: Vec<Window>,
pub async_scheduler: Scheduler<()>,
// TODO: move into own struct
// | basically just clean this mess up
pub output_callback_ids: Vec<CallbackId>,
}
impl<B: Backend> State<B> {
@ -240,38 +244,59 @@ impl<B: Backend> State<B> {
self.re_layout();
}
// TODO: add output
Msg::AddTags { tags } => {
Msg::AddTags { output_name, tags } => {
if let Some(output) = self
.focus_state
.focused_output
.as_ref()
.or_else(|| self.space.outputs().next())
.space
.outputs()
.find(|output| output.name() == output_name)
{
output.with_state(|state| {
state.tags.extend(
tags.clone()
.into_iter()
.map(|name| Tag::new(name, output.clone())),
);
state.tags.extend(tags.iter().cloned().map(Tag::new));
});
}
}
Msg::RemoveTags { tags } => {
for output in self.space.outputs() {
Msg::RemoveTags { output_name, tags } => {
if let Some(output) = self
.space
.outputs()
.find(|output| output.name() == output_name)
{
output.with_state(|state| {
state.tags.retain(|tag| !tags.contains(&tag.name));
});
}
}
Msg::ConnectForAllOutputs { callback_id } => {
let stream = self
.api_state
.stream
.as_ref()
.expect("Stream doesn't exist");
let mut stream = stream.lock().expect("Couldn't lock stream");
for output in self.space.outputs() {
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::CallCallback {
callback_id,
args: Some(Args::ConnectForAllOutputs {
output_name: output.name(),
}),
},
)
.expect("Send to client failed");
}
self.output_callback_ids.push(callback_id);
}
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 } => {
Request::GetWindowByAppId { app_id } => todo!(),
Request::GetWindowByTitle { title } => todo!(),
Request::GetWindowByFocus => {
let Some(current_focus) = self.focus_state.current_focus() else { return; };
let (app_id, title) =
compositor::with_states(current_focus.toplevel().wl_surface(), |states| {
@ -304,13 +329,12 @@ impl<B: Backend> State<B> {
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 } => {
Request::GetAllWindows => {
let window_props = self
.space
.elements()
@ -353,7 +377,6 @@ impl<B: Backend> State<B> {
crate::api::send_to_client(
&mut stream,
&OutgoingMsg::RequestResponse {
request_id: id,
response: RequestResponse::GetAllWindows {
windows: window_props,
},
@ -361,6 +384,78 @@ impl<B: Backend> State<B> {
)
.expect("Couldn't send to client");
}
Request::GetOutputByName { name } => {
let names = self
.space
.outputs()
.filter(|output| output.name() == name)
.map(|output| output.name())
.collect::<Vec<_>>();
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 {
response: RequestResponse::Outputs { names },
},
)
.unwrap();
}
Request::GetOutputsByModel { model } => {
let names = self
.space
.outputs()
.filter(|output| output.physical_properties().model == model)
.map(|output| output.name())
.collect::<Vec<_>>();
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 {
response: RequestResponse::Outputs { names },
},
)
.unwrap();
}
Request::GetOutputsByRes { res } => {
let names = self
.space
.outputs()
.filter_map(|output| {
if let Some(mode) = output.current_mode() {
if mode.size == (res.0 as i32, res.1 as i32).into() {
Some(output.name())
} else {
None
}
} else {
None
}
})
.collect::<Vec<_>>();
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 {
response: RequestResponse::Outputs { names },
},
)
.unwrap();
}
},
}
}
@ -718,6 +813,7 @@ impl<B: Backend> State<B> {
async_scheduler: sched,
windows: vec![],
output_callback_ids: vec![],
})
}
}
@ -779,6 +875,7 @@ pub fn take_presentation_feedback(
/// State containing the config API's stream.
#[derive(Default)]
pub struct ApiState {
// TODO: this may not need to be in an arc mutex because of the move to async
pub stream: Option<Arc<Mutex<UnixStream>>>,
}

View file

@ -9,8 +9,6 @@ use std::{
sync::atomic::{AtomicU32, Ordering},
};
use smithay::output::Output;
static TAG_ID_COUNTER: AtomicU32 = AtomicU32::new(0);
#[derive(Debug, Hash, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)]
@ -28,19 +26,16 @@ pub struct Tag {
pub id: TagId,
/// The name of this tag.
pub name: String,
/// The output that this tag should be on.
pub output: Output,
/// Whether this tag is active or not.
pub active: bool,
// TODO: layout
}
impl Tag {
pub fn new(name: String, output: Output) -> Self {
pub fn new(name: String) -> Self {
Self {
id: TagId::next(),
name,
output,
active: false,
}
}