From 1e2b806d2cda24b30d9dd639dead0a450c64376a Mon Sep 17 00:00:00 2001 From: Joris <6518350+Joristdh@users.noreply.github.com> Date: Tue, 16 Sep 2025 11:51:20 +0200 Subject: [PATCH 1/5] feat: dynamic topCorners for notched displays - Updated version to 1.1 - Add `excludeMenuBar` variable to optionally add an offset to the top corners - Automatically apply this variable when switching spaces, depending on whether it's fullscreen or not - Add `hs.canvas.windowBehaviors.stationary` so the corners are static in mission control --- Source/RoundedCorners.spoon/init.lua | 61 ++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 13 deletions(-) diff --git a/Source/RoundedCorners.spoon/init.lua b/Source/RoundedCorners.spoon/init.lua index 4a363edd..2aaf146d 100644 --- a/Source/RoundedCorners.spoon/init.lua +++ b/Source/RoundedCorners.spoon/init.lua @@ -8,13 +8,15 @@ obj.__index = obj -- Metadata obj.name = "RoundedCorners" -obj.version = "1.0" +obj.version = "1.1" obj.author = "Chris Jones " obj.homepage = "https://github.com/Hammerspoon/Spoons" obj.license = "MIT - https://opensource.org/licenses/MIT" obj.corners = {} +obj.topCorners = {} obj.screenWatcher = nil +obj.spacesWatcher = nil --- RoundedCorners.allScreens --- Variable @@ -23,14 +25,19 @@ obj.allScreens = true --- RoundedCorners.radius --- Variable ---- Controls the radius of the rounded corners, in points. Defaults to 6 -obj.radius = 6 +--- Controls the radius of the rounded corners, in points. Defaults to 12 +obj.radius = 12 --- RoundedCorners.level --- Variable --- Controls which level of the screens the corners are drawn at. See `hs.canvas.windowLevels` for more information. Defaults to `screenSaver + 1` obj.level = hs.canvas.windowLevels["screenSaver"] + 1 +--- RoundedCorners.excludeMenuBar +--- Variable +--- Controls whether the rounded corners are drawn below the menu bar. Defaults to false +obj.excludeMenuBar = false + -- Internal function used to find our location, so we know where to load files from local function script_path() local str = debug.getinfo(2, "S").source:sub(2) @@ -40,6 +47,10 @@ obj.spoonPath = script_path() function obj:init() self.screenWatcher = hs.screen.watcher.new(function() self:screensChanged() end) + self.spacesWatcher = hs.spaces.watcher.new(function () + self.excludeMenuBar = hs.window.frontmostWindow():isFullScreen() + self:spacesChanged() + end) end --- RoundedCorners:start() @@ -56,6 +67,7 @@ end --- * This will draw the rounded screen corners and start watching for changes in screen sizes/layouts, reacting accordingly function obj:start() self.screenWatcher:start() + self.spacesWatcher:start() self:render() return self end @@ -74,12 +86,26 @@ end --- * This will remove all rounded screen corners and stop watching for changes in screen sizes/layouts function obj:stop() self.screenWatcher:stop() + self.spacesWatcher:stop() self:deleteAllCorners() return self end +-- Delete only the dynamic top corners +function obj:deleteTopCorners() + hs.fnutils.each(self.topCorners, function(corner) corner:delete() end) + self.topCorners = {} +end + +-- React to the spaces having changed +function obj:spacesChanged() + self:deleteTopCorners() + self:render(true) +end + -- Delete all the corners function obj:deleteAllCorners() + self:deleteTopCorners() hs.fnutils.each(self.corners, function(corner) corner:delete() end) self.corners = {} end @@ -99,11 +125,21 @@ function obj:getScreens() end end +-- Draw a single corner +function obj:draw(data, radius, offset, behavior) + return hs.canvas.new({x=data.frame.x,y=data.frame.y+offset,w=radius,h=radius}):appendElements( + { action="build", type="rectangle", }, + { action="clip", type="circle", center=data.center, radius=radius, reversePath=true, }, + { action="fill", type="rectangle", frame={x=0, y=0, w=radius, h=radius, }, fillColor={ alpha=1, }}, + { type="resetClip", } + ):behavior({hs.canvas.windowBehaviors.stationary, behavior}):level(self.level):show() +end + -- Draw the corners -function obj:render() - local screens = self:getScreens() +function obj:render(topOnly) + local offset = self.excludeMenuBar and 37 or 0 local radius = self.radius - hs.fnutils.each(screens, function(screen) + hs.fnutils.each(self:getScreens(), function(screen) local screenFrame = screen:fullFrame() local cornerData = { { frame={x=screenFrame.x, y=screenFrame.y}, center={x=radius,y=radius} }, @@ -111,13 +147,12 @@ function obj:render() { frame={x=screenFrame.x, y=screenFrame.y + screenFrame.h - radius}, center={x=radius,y=0} }, { frame={x=screenFrame.x + screenFrame.w - radius, y=screenFrame.y + screenFrame.h - radius}, center={x=0,y=0} }, } - for _,data in pairs(cornerData) do - self.corners[#self.corners+1] = hs.canvas.new({x=data.frame.x,y=data.frame.y,w=radius,h=radius}):appendElements( - { action="build", type="rectangle", }, - { action="clip", type="circle", center=data.center, radius=radius, reversePath=true, }, - { action="fill", type="rectangle", frame={x=0, y=0, w=radius, h=radius, }, fillColor={ alpha=1, }}, - { type="resetClip", } - ):behavior(hs.canvas.windowBehaviors.canJoinAllSpaces):level(self.level):show() + for i, data in pairs(cornerData) do + if (screen:name() == "Built-in Retina Display" and i < 3) then + self.topCorners[#self.topCorners+1] = obj:draw(data, radius, offset) + elseif (not topOnly) then + self.corners[#self.corners+1] = obj:draw(data, radius, 0, hs.canvas.windowBehaviors.canJoinAllSpaces) + end end end) end From a60c4433362d1fcc2b00b7b4430a49b882ea2596 Mon Sep 17 00:00:00 2001 From: Joris <6518350+Joristdh@users.noreply.github.com> Date: Mon, 16 Feb 2026 10:23:12 +0100 Subject: [PATCH 2/5] Better handle moving between screens --- Source/RoundedCorners.spoon/init.lua | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/Source/RoundedCorners.spoon/init.lua b/Source/RoundedCorners.spoon/init.lua index 2aaf146d..5655a2a1 100644 --- a/Source/RoundedCorners.spoon/init.lua +++ b/Source/RoundedCorners.spoon/init.lua @@ -33,11 +33,6 @@ obj.radius = 12 --- Controls which level of the screens the corners are drawn at. See `hs.canvas.windowLevels` for more information. Defaults to `screenSaver + 1` obj.level = hs.canvas.windowLevels["screenSaver"] + 1 ---- RoundedCorners.excludeMenuBar ---- Variable ---- Controls whether the rounded corners are drawn below the menu bar. Defaults to false -obj.excludeMenuBar = false - -- Internal function used to find our location, so we know where to load files from local function script_path() local str = debug.getinfo(2, "S").source:sub(2) @@ -47,10 +42,7 @@ obj.spoonPath = script_path() function obj:init() self.screenWatcher = hs.screen.watcher.new(function() self:screensChanged() end) - self.spacesWatcher = hs.spaces.watcher.new(function () - self.excludeMenuBar = hs.window.frontmostWindow():isFullScreen() - self:spacesChanged() - end) + self.spacesWatcher = hs.spaces.watcher.new(function() self:spacesChanged() end) end --- RoundedCorners:start() @@ -137,9 +129,10 @@ end -- Draw the corners function obj:render(topOnly) - local offset = self.excludeMenuBar and 37 or 0 local radius = self.radius hs.fnutils.each(self:getScreens(), function(screen) + local windows = hs.window.filter.new():setScreens(screen:id()):getWindows() + local offset = screen:id() == 1 and #windows > 0 and windows[1]:isFullscreen() and 33 or 0 local screenFrame = screen:fullFrame() local cornerData = { { frame={x=screenFrame.x, y=screenFrame.y}, center={x=radius,y=radius} }, @@ -148,7 +141,7 @@ function obj:render(topOnly) { frame={x=screenFrame.x + screenFrame.w - radius, y=screenFrame.y + screenFrame.h - radius}, center={x=0,y=0} }, } for i, data in pairs(cornerData) do - if (screen:name() == "Built-in Retina Display" and i < 3) then + if (offset > 0 and i < 3) then self.topCorners[#self.topCorners+1] = obj:draw(data, radius, offset) elseif (not topOnly) then self.corners[#self.corners+1] = obj:draw(data, radius, 0, hs.canvas.windowBehaviors.canJoinAllSpaces) From 6038f2edf1ef2ee890300d79b1b6fa12d5903cdb Mon Sep 17 00:00:00 2001 From: Joris <6518350+Joristdh@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:20:48 +0100 Subject: [PATCH 3/5] Allow configuring offset --- Source/RoundedCorners.spoon/init.lua | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Source/RoundedCorners.spoon/init.lua b/Source/RoundedCorners.spoon/init.lua index 5655a2a1..c4669596 100644 --- a/Source/RoundedCorners.spoon/init.lua +++ b/Source/RoundedCorners.spoon/init.lua @@ -28,6 +28,12 @@ obj.allScreens = true --- Controls the radius of the rounded corners, in points. Defaults to 12 obj.radius = 12 + +--- RoundedCorners.offsetMenuBar +--- Variable +--- Controls the offset of the top corners when in fullscreen, in points. Defaults to 33, can be disabled by setting to 0 +obj.offsetMenuBar = 33 + --- RoundedCorners.level --- Variable --- Controls which level of the screens the corners are drawn at. See `hs.canvas.windowLevels` for more information. Defaults to `screenSaver + 1` @@ -132,7 +138,7 @@ function obj:render(topOnly) local radius = self.radius hs.fnutils.each(self:getScreens(), function(screen) local windows = hs.window.filter.new():setScreens(screen:id()):getWindows() - local offset = screen:id() == 1 and #windows > 0 and windows[1]:isFullscreen() and 33 or 0 + local offset = screen:id() == 1 and #windows > 0 and windows[1]:isFullscreen() and self.offsetMenuBar or 0 local screenFrame = screen:fullFrame() local cornerData = { { frame={x=screenFrame.x, y=screenFrame.y}, center={x=radius,y=radius} }, From d6d24ba95a15ceb16d69949e1ce187a0beec748f Mon Sep 17 00:00:00 2001 From: Joris <6518350+Joristdh@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:29:28 +0100 Subject: [PATCH 4/5] Disable the offset by default --- Source/RoundedCorners.spoon/init.lua | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Source/RoundedCorners.spoon/init.lua b/Source/RoundedCorners.spoon/init.lua index c4669596..30c24786 100644 --- a/Source/RoundedCorners.spoon/init.lua +++ b/Source/RoundedCorners.spoon/init.lua @@ -29,10 +29,11 @@ obj.allScreens = true obj.radius = 12 ---- RoundedCorners.offsetMenuBar +--- RoundedCorners.fullscreenOffset --- Variable ---- Controls the offset of the top corners when in fullscreen, in points. Defaults to 33, can be disabled by setting to 0 -obj.offsetMenuBar = 33 +--- Controls the offset of the top corners when in fullscreen, in points. Defaults to 0 +--- Place just under the MenuBar in Tahoe: 33 +obj.fullscreenOffset = 0 --- RoundedCorners.level --- Variable @@ -138,7 +139,7 @@ function obj:render(topOnly) local radius = self.radius hs.fnutils.each(self:getScreens(), function(screen) local windows = hs.window.filter.new():setScreens(screen:id()):getWindows() - local offset = screen:id() == 1 and #windows > 0 and windows[1]:isFullscreen() and self.offsetMenuBar or 0 + local offset = screen:id() == 1 and #windows > 0 and windows[1]:isFullscreen() and self.fullscreenOffset or 0 local screenFrame = screen:fullFrame() local cornerData = { { frame={x=screenFrame.x, y=screenFrame.y}, center={x=radius,y=radius} }, From 486b8476617ec3892846d8adcd9ae186b5dc6618 Mon Sep 17 00:00:00 2001 From: Joris <6518350+Joristdh@users.noreply.github.com> Date: Wed, 25 Mar 2026 11:41:08 +0100 Subject: [PATCH 5/5] Don't apply `fullscreenOffset` in mission control --- Source/RoundedCorners.spoon/init.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/RoundedCorners.spoon/init.lua b/Source/RoundedCorners.spoon/init.lua index 30c24786..e76beeca 100644 --- a/Source/RoundedCorners.spoon/init.lua +++ b/Source/RoundedCorners.spoon/init.lua @@ -137,9 +137,10 @@ end -- Draw the corners function obj:render(topOnly) local radius = self.radius + local mission = hs.spaces.focusedSpace() == 1 hs.fnutils.each(self:getScreens(), function(screen) local windows = hs.window.filter.new():setScreens(screen:id()):getWindows() - local offset = screen:id() == 1 and #windows > 0 and windows[1]:isFullscreen() and self.fullscreenOffset or 0 + local offset = not mission and screen:id() == 1 and #windows > 0 and windows[1]:isFullscreen() and self.fullscreenOffset or 0 local screenFrame = screen:fullFrame() local cornerData = { { frame={x=screenFrame.x, y=screenFrame.y}, center={x=radius,y=radius} },