diff --git a/api/lua/example_config.lua b/api/lua/example_config.lua index 1be76a1..1dfe7b7 100644 --- a/api/lua/example_config.lua +++ b/api/lua/example_config.lua @@ -66,14 +66,28 @@ require("pinnacle").setup(function(pinnacle) -- Just testing stuff input.keybind({ mod_key }, keys.h, function() - local win = window.get_focused() - if win ~= nil then - print("loc: " .. (win:loc() and win:loc().x or "nil") .. ", " .. (win:loc() and win:loc().y or "nil")) - print("size: " .. (win:size() and win:size().w or "nil") .. ", " .. (win:size() and win:size().h or "nil")) - print("class: " .. (win:class() or "nil")) - print("title: " .. (win:title() or "nil")) - print("float: " .. tostring(win:floating())) - end + -- local win = window.get_focused() + -- if win ~= nil then + -- print("loc: " .. (win:loc() and win:loc().x or "nil") .. ", " .. (win:loc() and win:loc().y or "nil")) + -- print("size: " .. (win:size() and win:size().w or "nil") .. ", " .. (win:size() and win:size().h or "nil")) + -- print("class: " .. (win:class() or "nil")) + -- print("title: " .. (win:title() or "nil")) + -- print("float: " .. tostring(win:floating())) + -- end + + local op = output.get_focused() --[[@as Output]] + print("res: " .. (op:res() and (op:res().w .. ", " .. op:res().h) or "nil")) + print("loc: " .. (op:loc() and (op:loc().x .. ", " .. op:loc().y) or "nil")) + print("rr: " .. (op:refresh_rate() or "nil")) + print("make: " .. (op:make() or "nil")) + print("model: " .. (op:model() or "nil")) + print("focused: " .. (tostring(op:focused()))) + + -- local tags = tag.get_on_output(output.get_focused()) + -- for _, tg in pairs(tags) do + -- print(tg:name()) + -- print(tg:output() and tg:output().name or "nil output") + -- end end) -- Tags --------------------------------------------------------------------------- @@ -103,6 +117,9 @@ require("pinnacle").setup(function(pinnacle) for _, tg in pairs(tags) do if tg:active() then local name = tg:name() + if name == nil then + return + end tg:set_layout(layouts[indices[name] or 1]) if indices[name] == nil then indices[name] = 2 @@ -122,6 +139,9 @@ require("pinnacle").setup(function(pinnacle) for _, tg in pairs(tags) do if tg:active() then local name = tg:name() + if name == nil then + return + end tg:set_layout(layouts[indices[name] or #layouts]) if indices[name] == nil then indices[name] = #layouts - 1 diff --git a/api/lua/msg.lua b/api/lua/msg.lua index 88741f8..f1d9737 100644 --- a/api/lua/msg.lua +++ b/api/lua/msg.lua @@ -41,14 +41,15 @@ ---@field GetWindowClass { window_id: WindowId } ---@field GetWindowTitle { window_id: WindowId } --Outputs ----@field GetOutputByName { output_name: OutputName } ----@field GetOutputsByModel { model: string } ----@field GetOutputsByRes { res: integer[] } +---@field GetOutputProps { output_name: string } +--Tags ---@field GetTagsByOutput { output_name: string } +---@field GetTagsByName { tag_name: string } +---@field GetTagOutput { tag_id: TagId } ---@field GetTagActive { tag_id: TagId } ---@field GetTagName { tag_id: TagId } ----@alias Request _Request | "GetWindowByFocus" | "GetAllWindows" | "GetOutputByFocus" +---@alias Request _Request | "GetWindowByFocus" | "GetAllWindows" | "GetOutputs" ---@class IncomingMsg ---@field CallCallback { callback_id: integer, args: Args } @@ -66,15 +67,16 @@ --Windows ---@field Window { window_id: WindowId|nil } ---@field Windows { window_ids: WindowId[] } ----@field WindowSize { size: (integer[])? } ----@field WindowLocation { loc: (integer[])? } +---@field WindowSize { size: integer[]? } +---@field WindowLocation { loc: integer[]? } ---@field WindowClass { class: string? } ---@field WindowTitle { title: string? } ---@field WindowFloating { floating: boolean? } --Outputs ---@field Output { output_name: OutputName? } ---@field Outputs { output_names: OutputName[] } +---@field OutputProps { make: string?, model: string?, loc: integer[]?, res: integer[]?, refresh_rate: integer?, physical_size: integer[]?, focused: boolean? } --Tags ---@field Tags { tag_ids: TagId[] } ----@field TagActive { active: boolean } ----@field TagName { name: string } +---@field TagActive { active: boolean? } +---@field TagName { name: string? } diff --git a/api/lua/output.lua b/api/lua/output.lua index 4223fa3..19c8159 100644 --- a/api/lua/output.lua +++ b/api/lua/output.lua @@ -26,10 +26,121 @@ function op:add_tags_table(names) require("tag").add_table(self, names) end ----Add methods to this output. +---Get this output's make. +---@return string|nil +function op:make() + SendRequest({ + GetOutputProps = { + output_name = self.name, + }, + }) + + local response = ReadMsg() + local props = response.RequestResponse.response.OutputProps + return props.make +end + +---Get this output's model. +---@return string|nil +function op:model() + SendRequest({ + GetOutputProps = { + output_name = self.name, + }, + }) + + local response = ReadMsg() + local props = response.RequestResponse.response.OutputProps + return props.model +end + +---Get this output's location in the global space. +---@return { x: integer, y: integer }|nil +function op:loc() + SendRequest({ + GetOutputProps = { + output_name = self.name, + }, + }) + + local response = ReadMsg() + local props = response.RequestResponse.response.OutputProps + if props.loc == nil then + return nil + else + return { x = props.loc[1], y = props.loc[2] } + end +end + +---Get this output's resolution in pixels. +---@return { w: integer, h: integer }|nil +function op:res() + SendRequest({ + GetOutputProps = { + output_name = self.name, + }, + }) + + local response = ReadMsg() + local props = response.RequestResponse.response.OutputProps + if props.res == nil then + return nil + else + return { w = props.res[1], h = props.res[2] } + end +end + +---Get this output's refresh rate in millihertz. +---For example, 60Hz will be returned as 60000. +---@return integer|nil +function op:refresh_rate() + SendRequest({ + GetOutputProps = { + output_name = self.name, + }, + }) + + local response = ReadMsg() + local props = response.RequestResponse.response.OutputProps + return props.refresh_rate +end + +---Get this output's physical size in millimeters. +---@return { w: integer, h: integer }|nil +function op:physical_size() + SendRequest({ + GetOutputProps = { + output_name = self.name, + }, + }) + + local response = ReadMsg() + local props = response.RequestResponse.response.OutputProps + if props.physical_size == nil then + return nil + else + return { w = props.physical_size[1], h = props.physical_size[2] } + end +end + +---Get whether or not this output is focused. This is currently defined as having the cursor on it. +---@return boolean|nil +function op:focused() + SendRequest({ + GetOutputProps = { + output_name = self.name, + }, + }) + + local response = ReadMsg() + local props = response.RequestResponse.response.OutputProps + return props.focused +end + +---This is an internal global function used to create an output object from an output name. ---@param props Output ---@return Output -local function new_output(props) +function NewOutput(props) -- Copy functions over for k, v in pairs(op) do props[k] = v @@ -40,6 +151,7 @@ end ------------------------------------------------------ +---@class OutputGlobal local output = {} ---Get an output by its name. @@ -56,21 +168,17 @@ local output = {} ---@param name string The name of the output. ---@return Output|nil output The output, or nil if none have the provided name. function output.get_by_name(name) - SendRequest({ - GetOutputByName = { - output_name = name, - }, - }) - + SendRequest("GetOutputs") local response = ReadMsg() + local output_names = response.RequestResponse.response.Outputs.output_names - local output_name = response.RequestResponse.response.Output.output_name - - if output_name ~= nil then - return new_output({ name = output_name }) - else - return nil + for _, output_name in pairs(output_names) do + if output_name == name then + return NewOutput({ name = output_name }) + end end + + return nil end ---Note: This may or may not be what is reported by other monitor listing utilities. Pinnacle currently fails to pick up one of my monitors' models when it is correctly picked up by tools like wlr-randr. I'll fix this in the future. @@ -80,20 +188,17 @@ end ---@param model string The model of the output(s). ---@return Output[] outputs All outputs with this model. function output.get_by_model(model) - SendRequest({ - GetOutputsByModel = { - model = model, - }, - }) - + SendRequest("GetOutputs") local response = ReadMsg() - local output_names = response.RequestResponse.response.Outputs.output_names - ---@type Output + ---@type Output[] local outputs = {} - for _, v in pairs(output_names) do - table.insert(outputs, new_output({ name = v })) + for _, output_name in pairs(output_names) do + local o = NewOutput({ name = output_name }) + if o:model() == model then + table.insert(outputs, o) + end end return outputs @@ -105,11 +210,7 @@ end ---@param height integer The height of the outputs, in pixels. ---@return Output[] outputs All outputs with this resolution. function output.get_by_res(width, height) - SendRequest({ - GetOutputsByRes = { - res = { width, height }, - }, - }) + SendRequest("GetOutputs") local response = ReadMsg() @@ -118,7 +219,10 @@ function output.get_by_res(width, height) ---@type Output local outputs = {} for _, output_name in pairs(output_names) do - table.insert(outputs, new_output({ name = output_name })) + local o = NewOutput({ name = output_name }) + if o:res() and o:res().w == width and o:res().h == height then + table.insert(outputs, o) + end end return outputs @@ -145,17 +249,18 @@ end ---``` ---@return Output|nil output The output, or nil if none are focused. function output.get_focused() - SendRequest("GetOutputByFocus") - + SendRequest("GetOutputs") local response = ReadMsg() + local output_names = response.RequestResponse.response.Outputs.output_names - local output_name = response.RequestResponse.response.Output.output_name - - if output_name ~= nil then - return new_output({ name = output_name }) - else - return nil + for _, output_name in pairs(output_names) do + local o = NewOutput({ name = output_name }) + if o:focused() then + return o + end end + + return nil end ---Connect a function to be run on all current and future outputs. @@ -170,7 +275,7 @@ 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 })) + func(NewOutput({ name = args.output_name })) end) SendMsg({ ConnectForAllOutputs = { diff --git a/api/lua/pinnacle.lua b/api/lua/pinnacle.lua index eb2de17..1a04125 100644 --- a/api/lua/pinnacle.lua +++ b/api/lua/pinnacle.lua @@ -82,6 +82,7 @@ function pinnacle.setup(config_func) ---@type fun(args: table?)[] CallbackTable = {} + ---This is an internal global function used to send serialized messages to the Pinnacle server. ---@param data Msg function SendMsg(data) local encoded = msgpack.encode(data) @@ -92,6 +93,7 @@ function pinnacle.setup(config_func) socket.send(socket_fd, encoded) end + ---This is an internal global function used to send requests to the Pinnacle server for information. ---@param data Request function SendRequest(data) SendMsg({ @@ -99,6 +101,8 @@ function pinnacle.setup(config_func) }) end + ---This is an internal global function used to read messages sent from the server. + ---These are used to call user-defined functions and provide requested information. function ReadMsg() local msg_len_bytes, err_msg, err_num = read_exact(socket_fd, 4) assert(msg_len_bytes) diff --git a/api/lua/tag.lua b/api/lua/tag.lua index 00f3240..2fd3e96 100644 --- a/api/lua/tag.lua +++ b/api/lua/tag.lua @@ -31,7 +31,7 @@ local function new_tag(props) end ---Get this tag's active status. ----@return boolean active True if the tag is active, otherwise false. +---@return boolean|nil active `true` if the tag is active, `false` if not, and `nil` if the tag doesn't exist. function tg:active() SendRequest({ GetTagActive = { @@ -44,6 +44,8 @@ function tg:active() return active end +---Get this tag's name. +---@return string|nil name The name of this tag, or nil if it doesn't exist. function tg:name() SendRequest({ GetTagName = { @@ -56,10 +58,32 @@ function tg:name() return name end +---Get this tag's output. +---@return Output|nil output The output this tag is on, or nil if the tag doesn't exist. +function tg:output() + SendRequest({ + GetTagOutput = { + tag_id = self.id, + }, + }) + + local response = ReadMsg() + local output_name = response.RequestResponse.response.Output.output_name + + if output_name == nil then + return nil + else + return NewOutput({ name = output_name }) + end +end + ---Set this tag's layout. ---@param layout Layout function tg:set_layout(layout) -- TODO: output param - tag.set_layout(self:name(), layout) + local name = self:name() + if name ~= nil then + tag.set_layout(name, layout) + end end ----------------------------------------------------------- @@ -68,12 +92,14 @@ end --- ---If you need to add the names as a table, use `tag.add_table` instead. --- +---You can also do `op:add_tags(...)`. +--- ---### Example --- ---```lua ----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 +---local op = output.get_by_name("DP-1") +---if op ~= nil then +--- tag.add(op, "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. @@ -237,4 +263,28 @@ function tag.get_on_output(output) return tags end +---Get all tags with this name across all outputs. +---@param name string The name of the tags you want. +---@return Tag[] +function tag.get_by_name(name) + SendRequest({ + GetTagsByName = { + tag_name = name, + }, + }) + + local response = ReadMsg() + + local tag_ids = response.RequestResponse.response.Tags.tag_ids + + ---@type Tag[] + local tags = {} + + for _, tag_id in pairs(tag_ids) do + table.insert(tags, new_tag({ id = tag_id })) + end + + return tags +end + return tag diff --git a/src/api/msg.rs b/src/api/msg.rs index d83f2c2..d8bbefa 100644 --- a/src/api/msg.rs +++ b/src/api/msg.rs @@ -110,12 +110,12 @@ pub enum Request { GetWindowClass { window_id: WindowId }, GetWindowTitle { window_id: WindowId }, // Outputs - GetOutputByName { output_name: String }, - GetOutputsByModel { model: String }, - GetOutputsByRes { res: (u32, u32) }, - GetOutputByFocus, + GetOutputs, + GetOutputProps { output_name: String }, // Tags GetTagsByOutput { output_name: String }, + GetTagsByName { tag_name: String }, + GetTagOutput { tag_id: TagId }, GetTagActive { tag_id: TagId }, GetTagName { tag_id: TagId }, } @@ -196,16 +196,56 @@ pub enum Args { #[derive(Debug, serde::Serialize, serde::Deserialize)] pub enum RequestResponse { - Window { window_id: Option }, - Windows { window_ids: Vec }, - WindowSize { size: Option<(i32, i32)> }, - WindowLocation { loc: Option<(i32, i32)> }, - WindowClass { class: Option }, - WindowTitle { title: Option }, - WindowFloating { floating: Option }, - Output { output_name: Option }, - Outputs { output_names: Vec }, - Tags { tag_ids: Vec }, - TagActive { active: bool }, - TagName { name: String }, + Window { + window_id: Option, + }, + Windows { + window_ids: Vec, + }, + WindowSize { + size: Option<(i32, i32)>, + }, + WindowLocation { + loc: Option<(i32, i32)>, + }, + WindowClass { + class: Option, + }, + WindowTitle { + title: Option, + }, + WindowFloating { + floating: Option, + }, + Output { + output_name: Option, + }, + Outputs { + output_names: Vec, + }, + OutputProps { + /// The make of the output. + make: Option, + /// The model of the output. + model: Option, + /// The location of the output in the space. + loc: Option<(i32, i32)>, + /// The resolution of the output. + res: Option<(i32, i32)>, + /// The refresh rate of the output. + refresh_rate: Option, + /// The size of the output, in millimeters. + physical_size: Option<(i32, i32)>, + /// Whether the output is focused or not. + focused: Option, + }, + Tags { + tag_ids: Vec, + }, + TagActive { + active: Option, + }, + TagName { + name: Option, + }, } diff --git a/src/state.rs b/src/state.rs index ffd36ba..d748204 100644 --- a/src/state.rs +++ b/src/state.rs @@ -471,74 +471,63 @@ impl State { ) .expect("failed to send to client"); } - Request::GetOutputByName { output_name } => { - // TODO: name better - let name = self + Request::GetOutputs => { + let output_names = self .space .outputs() - .find(|output| output.name() == output_name) - .map(|output| output.name()); - crate::api::send_to_client( - &mut stream, - &OutgoingMsg::RequestResponse { - response: RequestResponse::Output { output_name: name }, - }, - ) - .expect("failed to send to client"); - } - Request::GetOutputsByModel { model } => { - let names = self - .space - .outputs() - .filter(|output| output.physical_properties().model == model) .map(|output| output.name()) .collect::>(); crate::api::send_to_client( &mut stream, &OutgoingMsg::RequestResponse { - response: RequestResponse::Outputs { - output_names: names, - }, + response: RequestResponse::Outputs { output_names }, }, ) .expect("failed to send to client"); } - Request::GetOutputsByRes { res } => { - let names = self + Request::GetOutputProps { output_name } => { + let output = 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::>(); - crate::api::send_to_client( - &mut stream, - &OutgoingMsg::RequestResponse { - response: RequestResponse::Outputs { - output_names: names, - }, - }, - ) - .expect("failed to send to client"); - } - Request::GetOutputByFocus => { - let name = self + .find(|output| output.name() == output_name); + let res = output.as_ref().and_then(|output| { + output.current_mode().map(|mode| (mode.size.w, mode.size.h)) + }); + let refresh_rate = output + .as_ref() + .and_then(|output| output.current_mode().map(|mode| mode.refresh)); + let model = output + .as_ref() + .map(|output| output.physical_properties().model); + let physical_size = output.as_ref().map(|output| { + ( + output.physical_properties().size.w, + output.physical_properties().size.h, + ) + }); + let make = output + .as_ref() + .map(|output| output.physical_properties().make); + let loc = output + .as_ref() + .map(|output| (output.current_location().x, output.current_location().y)); + let focused = self .focus_state .focused_output .as_ref() - .map(|output| output.name()); + .and_then(|foc_op| output.map(|op| op == foc_op)); crate::api::send_to_client( &mut stream, &OutgoingMsg::RequestResponse { - response: RequestResponse::Output { output_name: name }, + response: RequestResponse::OutputProps { + make, + model, + loc, + res, + refresh_rate, + physical_size, + focused, + }, }, ) .expect("failed to send to client"); @@ -558,39 +547,54 @@ impl State { .expect("failed to send to client"); } } - Request::GetTagActive { tag_id } => { - let tag = self + Request::GetTagsByName { tag_name } => { + let tag_ids = self .space .outputs() .flat_map(|op| op.with_state(|state| state.tags.clone())) - .find(|tag| tag.id() == tag_id); - if let Some(tag) = tag { - crate::api::send_to_client( - &mut stream, - &OutgoingMsg::RequestResponse { - response: RequestResponse::TagActive { - active: tag.active(), - }, - }, - ) - .expect("failed to send to client"); - } + .filter(|tag| tag.name() == tag_name) + .map(|tag| tag.id()) + .collect::>(); + crate::api::send_to_client( + &mut stream, + &OutgoingMsg::RequestResponse { + response: RequestResponse::Tags { tag_ids }, + }, + ) + .expect("failed to send to client"); + } + Request::GetTagOutput { tag_id } => { + let output_name = tag_id + .tag(self) + .and_then(|tag| tag.output(self)) + .map(|output| output.name()); + crate::api::send_to_client( + &mut stream, + &OutgoingMsg::RequestResponse { + response: RequestResponse::Output { output_name }, + }, + ) + .expect("failed to send to client"); + } + Request::GetTagActive { tag_id } => { + let active = tag_id.tag(self).map(|tag| tag.active()); + crate::api::send_to_client( + &mut stream, + &OutgoingMsg::RequestResponse { + response: RequestResponse::TagActive { active }, + }, + ) + .expect("failed to send to client"); } Request::GetTagName { tag_id } => { - let tag = self - .space - .outputs() - .flat_map(|op| op.with_state(|state| state.tags.clone())) - .find(|tag| tag.id() == tag_id); - if let Some(tag) = tag { - crate::api::send_to_client( - &mut stream, - &OutgoingMsg::RequestResponse { - response: RequestResponse::TagName { name: tag.name() }, - }, - ) - .expect("failed to send to client"); - } + let name = tag_id.tag(self).map(|tag| tag.name()); + crate::api::send_to_client( + &mut stream, + &OutgoingMsg::RequestResponse { + response: RequestResponse::TagName { name }, + }, + ) + .expect("failed to send to client"); } } } @@ -752,7 +756,7 @@ impl State { state .tags .iter() - .any(|tag| self.output_for_tag(tag).is_some_and(|op| &op == output)) + .any(|tag| tag.output(self).is_some_and(|op| &op == output)) }) }) .cloned() diff --git a/src/tag.rs b/src/tag.rs index 9f866db..ea128a9 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -28,6 +28,14 @@ impl TagId { fn next() -> Self { Self(TAG_ID_COUNTER.fetch_add(1, Ordering::Relaxed)) } + + pub fn tag(&self, state: &State) -> Option { + state + .space + .outputs() + .flat_map(|op| op.with_state(|state| state.tags.clone())) + .find(|tag| &tag.id() == self) + } } #[derive(Debug)] @@ -88,13 +96,11 @@ impl Tag { layout: Layout::MasterStack, // TODO: get from config }))) } -} - -impl State { - pub fn output_for_tag(&self, tag: &Tag) -> Option { - self.space + pub fn output(&self, state: &State) -> Option { + state + .space .outputs() - .find(|output| output.with_state(|state| state.tags.iter().any(|tg| tg == tag))) + .find(|output| output.with_state(|state| state.tags.iter().any(|tg| tg == self))) .cloned() } }