How do I show an alert on all fullscreen spaces?
NightMachinery opened this issue · comments
I have this code for showing alerts. I want the alerts to be on all my fullscreen spaces (I have many fullscreen apps). I have a single monitor. It currently only shows the alert on the current space.
local hyperStyle = {
-- [[https://github.com/Hammerspoon/hammerspoon/blob/master/extensions/alert/alert.lua#L17][hammerspoon/extensions/alert/alert.lua at master · Hammerspoon/hammerspoon]]
-- strokeWidth = 2,
-- strokeColor = { white = 1, alpha = 1 },
-- fillColor = { white = 0, alpha = 0.75 },
-- textColor = { white = 1, alpha = 1 },
-- textFont = ".AppleSystemUIFont",
-- textSize = 27,
-- radius = 27,
atScreenEdge = 1,
fadeInDuration = 0.001,
fadeOutDuration = 0.001,
-- padding = nil,
fillColor = { white = 1, alpha = 2 / 3 },
radius = 24,
strokeColor = { red = 19 / 255, green = 182 / 255, blue = 133 / 255, alpha = 1},
strokeWidth = 16,
textColor = { white = 0.125 },
textSize = 48,
}
function hyper_modality:entered()
hyper_modality.entered_p = true
-- I have not yet added the redis updaters for purple_modality.
redisActivateMode("hyper_modality")
if hs.eventtap.isSecureInputEnabled() then
hs.alert("⚠️ Secure Input is on. Our Hyper Mode commands might not work.")
-- [[https://github.com/Hammerspoon/hammerspoon/issues/3555][Hammerspoon hangs spradically when entering hyper mode and displaying a modal window · Issue #3555 · Hammerspoon/hammerspoon]]
end
hyperAlerts = {}
for i, screen in pairs(hs.screen.allScreens()) do
msg = "🌟"
-- msg = "Hyper Mode 🌟"
-- msg = "Hyper Mode ✈"
alert = hs.alert(msg, hyperStyle, screen, "")
hyperAlerts[i] = alert
end
end
function hyper_modality:exited()
hyper_modality.entered_p = false
hyper_modality.exit_on_release_p = false
for i, alert in pairs(hyperAlerts) do
hs.alert.closeSpecific(alert, 0.25)
end
redisDeactivateMode("hyper_modality")
end
TL;DR: There's a hacky way of doing it, but it doesn't work quite right and I haven't figured out how to fix it yet, so you may be better off not using hs.alert
for this.
Hammerspoon doesn't seem to have any official way to do that, but if you're okay with directly messing with hs.alert
's implementation details behind its back, it is possible to set the 'behavior' of the underlying hs.drawing
objects to make them appear in all spaces. (The various behavior flags are supposed to be documented here - or here for hs.canvas
- but the specific values aren't currently showing up in the HTML documentation right now. Your best bet is to go to the source code for hs.canvas
and read the comments there.)
Attempt #1: Just setting the drawings' behavior
function setAlertBehavior(alertUUID, behavior)
local method = type(behavior) == 'number' and 'setBehavior' or 'setBehaviorByLabels'
for _, alert in ipairs(hs.alert._visibleAlerts) do
if alert.UUID == alertUUID then
for _, drawing in ipairs(alert.drawings) do
drawing[method](drawing, behavior)
end
break
end
end
end
-- Example usage:
local alert = hs.alert('This is a test', 20)
setAlertBehavior(alert, {'canJoinAllSpaces', 'transient', 'fullScreenAuxiliary'}) -- alert will appear in all spaces
-- OR
setAlertBehavior(alert, {'canJoinAllSpaces', 'stationary', 'fullScreenAuxiliary'}) -- alert will appear in all spaces AND Mission Control
You'll need to uncheck 'Show dock icon' in Hammerspoon's preferences for it to be allowed to draw over fullscreen windows.
Unfortunately, even with fullScreenAuxiliary
, it still doesn't seem to work quite right. Because hs.alert
is based on the old, deprecated hs.drawing
module, the background/frame and the text are two separate windows, and they seem to reverse their order for some reason when switching between fullscreen windows and normal Spaces, resulting in the text being behind the (translucent) background. The order stays the same when switching from a normal Space to another normal Space, or from a fullscreen window to another fullscreen window - it's only when I switch from one type to the other that the alert's parts get reversed (or de-reversed).
I tried using an hs.spaces.watcher
to bringToFront
the drawings in the correct order, but for some reason they still end up reversed.
Attempt #2: Set behavior of drawings, then re-stack when switching Spaces
local allSpacesAlerts = {}
local function fixAllSpacesAlerts()
for _, alert in pairs(allSpacesAlerts) do
for _, drawing in ipairs(alert.drawings) do
drawing:bringToFront()
end
end
end
local allSpacesAlertsSpacesWatcher = hs.spaces.watcher.new(fixAllSpacesAlerts)
function showAlertInAllSpaces(alertUUID, showInMissionControl)
local behaviors = {'canJoinAllSpaces', showInMissionControl and 'stationary' or 'transient', 'fullScreenAuxiliary'}
for _, alert in ipairs(hs.alert._visibleAlerts) do
if alert.UUID == alertUUID then
for _, drawing in ipairs(alert.drawings) do
drawing:setBehaviorByLabels(behaviors)
end
allSpacesAlerts[alertUUID] = alert
allSpacesAlertsSpacesWatcher:start()
break
end
end
end
-- patch hs.alert to clean up the allSpacesAlerts table when alerts go away
do
local getupvalue = debug.getupvalue
local closeSpecific = hs.alert.closeSpecific
local n = 0
local name, value
repeat
n = n + 1
name, value = getupvalue(closeSpecific, n)
until name == 'purgeAlert' or not name
if name then
local origPurgeAlert = value
local function newPurgeAlert(alertUUID, ...)
allSpacesAlerts[alertUUID] = nil
if not next(allSpacesAlerts) then
allSpacesAlertsSpacesWatcher:stop()
end
return origPurgeAlert(alertUUID, ...)
end
debug.setupvalue(closeSpecific, n, newPurgeAlert)
else
error "Couldn't patch hs.alert's purgeAlert function!"
end
end
-- Example usage:
local alertUUID = hs.alert('This is a test', 10)
showAlertInAllSpaces(alertUUID, true)
I'm not really sure what to try at this point, other than abandoning hs.alert
for this purpose and displaying the Hyper mode indicator some other way, e.g. using an hs.canvas
.
Attempt #3: Using hs.canvas
instead of hs.alert
local hyperStyle = {
fillColor = { white = 1, alpha = 2 / 3 },
radius = 24,
strokeColor = { red = 19 / 255, green = 182 / 255, blue = 133 / 255, alpha = 1},
strokeWidth = 8,
textColor = { white = 0.125 },
textSize = 48,
text = "🌟",
}
local hyperModeIndicator = hs.canvas.new{x=0, y=0, w=0, h=0}:insertElement{
id = 'background',
type = 'rectangle',
action = 'strokeAndFill',
fillColor = hyperStyle.fillColor,
roundedRectRadii = { xRadius = hyperStyle.radius, yRadius = hyperStyle.radius },
strokeColor = hyperStyle.strokeColor,
strokeWidth = hyperStyle.strokeWidth,
padding = hyperStyle.strokeWidth/2,
}:insertElement{
id = 'textBox',
type = 'text',
text = hyperStyle.text,
textAlignment = 'center',
textColor = hyperStyle.textColor,
textSize = hyperStyle.textSize,
}
local hyperTextBoxSize = hyperModeIndicator:minimumTextSize(2, hyperStyle.text)
local screenFrame = hs.screen.primaryScreen():fullFrame()
local hyperFrame = {}
hyperFrame.w = hyperTextBoxSize.w + hyperStyle.strokeWidth*2 + hyperStyle.textSize -- default alert padding is 1/2 of font size, but we need it on both sides
hyperFrame.h = hyperTextBoxSize.h + hyperStyle.strokeWidth*2 + hyperStyle.textSize -- ditto
hyperFrame.x = (screenFrame.w - hyperFrame.w)/2
hyperFrame.y = screenFrame.y -- top edge
-- Hammerspoon can automatically center the text horizontally, but not vertically, so:
hyperModeIndicator.textBox.frame = {
x = (hyperFrame.w - hyperTextBoxSize.w)/2,
y = (hyperFrame.h - hyperTextBoxSize.h)/2,
w = hyperTextBoxSize.w,
h = hyperTextBoxSize.h,
}
hyperModeIndicator:frame(hyperFrame)
hyperModeIndicator:behavior{'canJoinAllSpaces', 'transient', 'fullScreenAuxiliary'} -- canvas will appear in all spaces
-- OR
hyperModeIndicator:behavior{'canJoinAllSpaces', 'stationary', 'fullScreenAuxiliary'} -- canvas will appear in all spaces AND Mission Control
function hyper_modality:entered()
hyper_modality.entered_p = true
-- I have not yet added the redis updaters for purple_modality.
redisActivateMode("hyper_modality")
if hs.eventtap.isSecureInputEnabled() then
hs.alert("⚠️ Secure Input is on. Our Hyper Mode commands might not work.")
-- [[https://github.com/Hammerspoon/hammerspoon/issues/3555][Hammerspoon hangs spradically when entering hyper mode and displaying a modal window · Issue #3555 · Hammerspoon/hammerspoon]]
end
hyperModeIndicator:show()
end
function hyper_modality:exited()
hyper_modality.entered_p = false
hyper_modality.exit_on_release_p = false
hyperModeIndicator:hide()
redisDeactivateMode("hyper_modality")
end
There may be some subtle differences in how that indicator looks, and I had to cut strokeWidth
in half to make it look the same, but it should be pretty close to what you had before. Note that since it's not an hs.alert
, any actual hs.alert
s with atScreenEdge = 1
won't know about it, and will draw over the top of it.
@Rhys-T Thanks for the detailed writeup. I tried using the new canvas
based approach, and it works, but it still only shows up on the current space?
Whoops! Forgot to actually set the behavior on the canvas. Add one of these lines right after hyperModeIndicator:frame(hyperFrame)
:
hyperModeIndicator:behavior{'canJoinAllSpaces', 'transient', 'fullScreenAuxiliary'} -- canvas will appear in all spaces
-- OR
hyperModeIndicator:behavior{'canJoinAllSpaces', 'stationary', 'fullScreenAuxiliary'} -- canvas will appear in all spaces AND Mission Control
I'll edit that back into my original comment. Sorry for the confusion.