From da886cb1c4f41a24a8b8017101b28b1c6f558a73 Mon Sep 17 00:00:00 2001 From: Nooo37 <70270606+Nooo37@users.noreply.github.com> Date: Thu, 8 Apr 2021 20:17:10 +0200 Subject: [PATCH] Scratchpad feature (#35) --- README.md | 4 +- docs/_sidebar.md | 1 + docs/module/scratch.md | 34 +++++++++++++ helpers/client.lua | 38 ++++++++++++++- module/init.lua | 3 +- module/scratchpad.lua | 95 ++++++++++++++++++++++++++++++++++++ module/window_swallowing.lua | 9 +--- 7 files changed, 174 insertions(+), 10 deletions(-) create mode 100644 docs/module/scratch.md create mode 100644 module/scratchpad.lua diff --git a/README.md b/README.md index 746e803..6ebcf84 100644 --- a/README.md +++ b/README.md @@ -19,14 +19,16 @@ All documentation, instructions, and previews are [here](https://nooo37.github.i - Tiled Wallpaper - Wallpaper Easy Setup - Window Swallowing + - Scratchpad - Signals - Playerctl - Widgets - Tag Preview ## TODO +- [ ] Add a built-in option to animate scratchpads with [awestore](https://github.com/K4rakara/awestore) - [ ] Add external sources management for the wallpaper module (URLs, RSS feeds, NASA picture of the day, ...) -- [ ] Scratchpad module +- [x] Scratchpad module - [x] Some more documentation on the tabbed module - [x] Add a cool alternative tabbar style - [x] Add another cool tabbar style (we need more styles) diff --git a/docs/_sidebar.md b/docs/_sidebar.md index beed3a6..7bfcbdb 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -8,6 +8,7 @@ - [Tiled Wallpaper](module/twall.md) - [Wallpaper Easy Setup](module/wall.md) - [Window Swallowing](module/swal.md) + - [Scratchpad](module/scratch.md) - Signals - [Playerctl](signals/pctl.md) diff --git a/docs/module/scratch.md b/docs/module/scratch.md new file mode 100644 index 0000000..b49fdaf --- /dev/null +++ b/docs/module/scratch.md @@ -0,0 +1,34 @@ +## 🍃 Scratchpad + +An easy way to create multiple scratchpads. + +### A... what? + +You can think about a scratchpad as a window whose visibility can be toggled, but still runs in the background without being visible (or minimized) most of the time. Many people use it to have one terminal in which to perform minor tasks, but it is the most useful for windows which only need a couple seconds in between your actual activity, such as music players or chat applications. + +### Usage + +To initalize a scratchpad you can do something like the following: + +```lua +local bling = require("bling") + +local term_scratch = bling.module.scratchpad:new { + command = "wezterm start --class spad", -- How to spawn the scratchpad + rule = { instance = "spad" }, -- The rule that the scratchpad will be searched by + sticky = true, -- Whether the scratchpad should be sticky + autoclose = true, -- Whether it should hide itself when losing focus + floating = true, -- Whether it should be floating + geometry = {x=360, y=90, height=900, width=1200}, -- The geometry in a floating state + reapply = false, -- Whether all those properties should be reapplied on every new opening of the scratchpad + dont_focus_before_close = false, -- When set to true, the scratchpad will be closed by the toggle function regardless of whether its focused or not. When set to false, the toggle function will first bring the scratchpad into focus and only close it on a second call +} +``` + +Once initalized, you can use the object (which in this case is named `term_scratch`) like this: + +```lua +term_scratch:toggle() -- toggles the scratchpads visibility +term_scratch:turn_on() -- turns the scratchpads visibility on +term_scratch:turn_off() -- turns the scratchpads visibility off +``` diff --git a/helpers/client.lua b/helpers/client.lua index f3dc708..9bd5ae0 100644 --- a/helpers/client.lua +++ b/helpers/client.lua @@ -1,11 +1,12 @@ local awful = require("awful") +local gears = require("gears") local _client = {} --- Turn off passed client -- Remove current tag from window's tags -- --- @param c a client +-- @param c A client function _client.turn_off(c) local current_tag = awful.tag.selected(c.screen) local ctags = {} @@ -45,5 +46,40 @@ function _client.sync(to_c, from_c) -- TODO: Should also copy over the position in a tiling layout end +--- Checks whether the passed client is a childprocess of a given process ID +-- +-- @param c A client +-- @param pid The process ID +-- @return True if the passed client is a childprocess of the given PID otherwise false +function _client.is_child_of(c, pid) + -- io.popen is normally discouraged. Should probably be changed + if not c or not c.valid then return false end + if tostring(c.pid) == tostring(pid) then return true end + local pid_cmd = [[pstree -T -p -a -s ]] .. tostring(c.pid) .. + [[ | sed '2q;d' | grep -o '[0-9]*$' | tr -d '\n']] + local handle = io.popen(pid_cmd) + local parent_pid = handle:read("*a") + handle:close() + return tostring(parent_pid) == tostring(pid) or + tostring(parent_pid) == tostring(c.pid) +end + +--- Finds all clients that satisfy the passed rule +-- +-- @param rule The rule to be searched for +function _client.find(rule) + local function matcher(c) return awful.rules.match(c, rule) end + local clients = client.get() + local findex = gears.table.hasitem(clients, client.focus) or 1 + local start = gears.math.cycle(#clients, findex + 1) + + local matches = {} + for c in awful.client.iterate(matcher, start) do + matches[#matches + 1] = c + end + + return matches +end + return _client diff --git a/module/init.lua b/module/init.lua index 953e24a..2954a0f 100644 --- a/module/init.lua +++ b/module/init.lua @@ -3,5 +3,6 @@ return { tiled_wallpaper = require(... .. ".tiled_wallpaper"), wallpaper = require(... .. ".wallpaper"), flash_focus = require(... .. ".flash_focus"), - tabbed = require(... .. ".tabbed") + tabbed = require(... .. ".tabbed"), + scratchpad = require(... .. ".scratchpad") } diff --git a/module/scratchpad.lua b/module/scratchpad.lua new file mode 100644 index 0000000..ce3c9b6 --- /dev/null +++ b/module/scratchpad.lua @@ -0,0 +1,95 @@ +local awful = require("awful") + +local helpers = require(tostring(...):match(".*bling") .. ".helpers") + + +local Scratchpad = {} + +--- Creates a new scratchpad object based on the argument +-- +-- @param info A table of possible arguments +-- @return The new scratchpad object +function Scratchpad:new(info) + info = info or {} + setmetatable(info, self) + self.__index = self + return info +end + +--- Find all clients that satisfy the the rule +-- +-- @return A list of all clients that satisfy the rule +function Scratchpad:find() + return helpers.client.find(self.rule) +end + +--- Applies the objects scratchpad properties to a given client +-- +-- @param c A client to which to apply the properties +function Scratchpad:apply(c) + if not c or not c.valid then return end + c.floating = self.floating + c.sticky = self.sticky + c:geometry(self.geometry) + if self.autoclose then + c:connect_signal("unfocus", function(c) + c.sticky = false -- client won't turn off if sticky + helpers.client.turn_off(c) + end) + end +end + +--- Turns the scratchpad on +function Scratchpad:turn_on() + local matches = self:find() + if matches[1] then + -- if a client was found, turn it on + c = matches[1] + if self.reapply then self:apply(c) end + -- c.sticky was set to false in turn_off so it has to be reapplied anyway + c.sticky = self.sticky + helpers.client.turn_on(c) + return + else + -- if no client was found, spawn one, find the corresponding window, + -- apply the properties only once (until the next closing) + local pid = awful.spawn.with_shell(self.command) + local function inital_apply(c) + if helpers.client.is_child_of(c, pid) then self:apply(c) end + client.disconnect_signal("manage", inital_apply) + end + client.connect_signal("manage", inital_apply) + return + end +end + +--- Turns the scratchpad off +function Scratchpad:turn_off() + local matches = self:find() + local c = matches[1] + if c then + c.sticky = false + helpers.client.turn_off(c) + end +end + +--- Turns the scratchpad off if it is focused otherwise it raises the scratchpad +function Scratchpad:toggle() + local is_turn_off = false + if self.dont_focus_before_close then + local matches = self:find() + if matches[1] and matches[1].first_tag then + is_turn_off = matches[1].first_tag.selected + end + else + is_turn_off = client.focus and awful.rules.match(client.focus, self.rule) + end + + if is_turn_off then + self:turn_off() + else + self:turn_on() + end +end + +return Scratchpad diff --git a/module/window_swallowing.lua b/module/window_swallowing.lua index aa2a122..0080c80 100644 --- a/module/window_swallowing.lua +++ b/module/window_swallowing.lua @@ -31,14 +31,9 @@ end local function manage_clientspawn(c) -- get the last focused window to check if it is a parent window local parent_client=awful.client.focus.history.get(c.screen, 1) - if not parent_client then return end + if not parent_client or not parent_client.valid then return end - -- io.popen is normally discouraged. Should probably be changed - local handle = io.popen([[pstree -T -p -a -s ]] .. tostring(c.pid) .. [[ | sed '2q;d' | grep -o '[0-9]*$' | tr -d '\n']]) - local parent_pid = handle:read("*a") - handle:close() - - if (tostring(parent_pid) == tostring(parent_client.pid)) and check_if_swallow(c) then + if helpers.client.is_child_of(c, parent_client.pid) and check_if_swallow(c) then c:connect_signal("unmanage", function() helpers.client.turn_on(parent_client)