├── GlobalMute.spoon ├── docs.json └── init.lua ├── LICENSE └── README.md /GlobalMute.spoon/docs.json: -------------------------------------------------------------------------------- 1 | -- Loading extension: doc 2 | [ 3 | { 4 | "Constant" : [ 5 | 6 | ], 7 | "submodules" : [ 8 | 9 | ], 10 | "Function" : [ 11 | 12 | ], 13 | "Variable" : [ 14 | { 15 | "parameters" : [ 16 | 17 | ], 18 | "stripped_doc" : [ 19 | "Accessible variable to adjust the logging level" 20 | ], 21 | "desc" : "Accessible variable to adjust the logging level", 22 | "doc" : "Accessible variable to adjust the logging level", 23 | "notes" : [ 24 | 25 | ], 26 | "signature" : "GlobalMute.logger", 27 | "type" : "Variable", 28 | "returns" : [ 29 | 30 | ], 31 | "def" : "GlobalMute.logger", 32 | "name" : "logger" 33 | } 34 | ], 35 | "stripped_doc" : [ 36 | 37 | ], 38 | "Deprecated" : [ 39 | 40 | ], 41 | "type" : "Module", 42 | "desc" : "With this Spoon you will be able ot globally mute and unmute your Microphone devices.", 43 | "Constructor" : [ 44 | 45 | ], 46 | "items" : [ 47 | { 48 | "parameters" : [ 49 | 50 | ], 51 | "stripped_doc" : [ 52 | "Accessible variable to adjust the logging level" 53 | ], 54 | "desc" : "Accessible variable to adjust the logging level", 55 | "doc" : "Accessible variable to adjust the logging level", 56 | "notes" : [ 57 | 58 | ], 59 | "signature" : "GlobalMute.logger", 60 | "type" : "Variable", 61 | "returns" : [ 62 | 63 | ], 64 | "def" : "GlobalMute.logger", 65 | "name" : "logger" 66 | }, 67 | { 68 | "parameters" : [ 69 | " * applist - A table containing hotkey details for defined applications:", 70 | "", 71 | "A configuration example:", 72 | "``` lua", 73 | "local hyper = {\"ctrl\", \"alt\", \"cmd\"}", 74 | "hs.loadSpoon(\"GlobalMute\")", 75 | "spoon.GlobalMute:configure({", 76 | " unmute_background = 'file:\/\/\/Library\/Desktop%20Pictures\/Solid%20Colors\/Red%20Orange.png',", 77 | " mute_background = 'file:\/\/\/Library\/Desktop%20Pictures\/Solid%20Colors\/Turquoise%20Green.png',", 78 | " enforce_desired_state = true,", 79 | " unmute_title = \"---- THEY CAN HEAR YOU ----\",", 80 | " mute_title = \"MUTE\",", 81 | "})", 82 | "spoon.GlobalMute:bindHotkeys({", 83 | " unmute = {hyper, \"u\"},", 84 | " mute = {hyper, \"m\"},", 85 | " toggle = {hyper, \"space\"}", 86 | "})", 87 | "spoon.GlobalMute._logger.level = 3", 88 | "```" 89 | ], 90 | "stripped_doc" : [ 91 | "Binds hotkeys for GlobalMute", 92 | "" 93 | ], 94 | "desc" : "Binds hotkeys for GlobalMute", 95 | "doc" : "Binds hotkeys for GlobalMute\n\nParameters:\n * applist - A table containing hotkey details for defined applications:\n\nA configuration example:\n``` lua\nlocal hyper = {\"ctrl\", \"alt\", \"cmd\"}\nhs.loadSpoon(\"GlobalMute\")\nspoon.GlobalMute:configure({\n unmute_background = 'file:\/\/\/Library\/Desktop%20Pictures\/Solid%20Colors\/Red%20Orange.png',\n mute_background = 'file:\/\/\/Library\/Desktop%20Pictures\/Solid%20Colors\/Turquoise%20Green.png',\n enforce_desired_state = true,\n unmute_title = \"---- THEY CAN HEAR YOU ----\",\n mute_title = \"MUTE\",\n})\nspoon.GlobalMute:bindHotkeys({\n unmute = {hyper, \"u\"},\n mute = {hyper, \"m\"},\n toggle = {hyper, \"space\"}\n})\nspoon.GlobalMute._logger.level = 3\n```", 96 | "notes" : [ 97 | 98 | ], 99 | "signature" : "GlobalMute:bindHotkeys()", 100 | "type" : "Method", 101 | "returns" : [ 102 | 103 | ], 104 | "def" : "GlobalMute:bindHotkeys()", 105 | "name" : "bindHotkeys" 106 | }, 107 | { 108 | "parameters" : [ 109 | 110 | ], 111 | "stripped_doc" : [ 112 | "Set spoon level configuration" 113 | ], 114 | "desc" : "Set spoon level configuration", 115 | "doc" : "Set spoon level configuration", 116 | "notes" : [ 117 | 118 | ], 119 | "signature" : "GlobalMute:configure(conf)", 120 | "type" : "Method", 121 | "returns" : [ 122 | 123 | ], 124 | "def" : "GlobalMute:configure(conf)", 125 | "name" : "configure" 126 | }, 127 | { 128 | "parameters" : [ 129 | 130 | ], 131 | "stripped_doc" : [ 132 | "Currently does nothing (implemented so that treating this Spoon like others won't cause errors)." 133 | ], 134 | "desc" : "Currently does nothing (implemented so that treating this Spoon like others won't cause errors).", 135 | "doc" : "Currently does nothing (implemented so that treating this Spoon like others won't cause errors).", 136 | "notes" : [ 137 | 138 | ], 139 | "signature" : "GlobalMute:init()", 140 | "type" : "Method", 141 | "returns" : [ 142 | 143 | ], 144 | "def" : "GlobalMute:init()", 145 | "name" : "init" 146 | }, 147 | { 148 | "parameters" : [ 149 | 150 | ], 151 | "stripped_doc" : [ 152 | "Callback function when audio input events happen" 153 | ], 154 | "desc" : "Callback function when audio input events happen", 155 | "doc" : "Callback function when audio input events happen", 156 | "notes" : [ 157 | 158 | ], 159 | "signature" : "GlobalMute:microphone_changes(device_uid, event_name, event_scope, event_element)", 160 | "type" : "Method", 161 | "returns" : [ 162 | 163 | ], 164 | "def" : "GlobalMute:microphone_changes(device_uid, event_name, event_scope, event_element)", 165 | "name" : "microphone_changes" 166 | }, 167 | { 168 | "parameters" : [ 169 | 170 | ], 171 | "stripped_doc" : [ 172 | "Mute all system sound input, force to mute" 173 | ], 174 | "desc" : "Mute all system sound input, force to mute", 175 | "doc" : "Mute all system sound input, force to mute", 176 | "notes" : [ 177 | 178 | ], 179 | "signature" : "GlobalMute:mute(force)", 180 | "type" : "Method", 181 | "returns" : [ 182 | 183 | ], 184 | "def" : "GlobalMute:mute(force)", 185 | "name" : "mute" 186 | }, 187 | { 188 | "parameters" : [ 189 | 190 | ], 191 | "stripped_doc" : [ 192 | "Mute all system sound input, force to mute" 193 | ], 194 | "desc" : "Mute all system sound input, force to mute", 195 | "doc" : "Mute all system sound input, force to mute", 196 | "notes" : [ 197 | 198 | ], 199 | "signature" : "GlobalMute:toggle()", 200 | "type" : "Method", 201 | "returns" : [ 202 | 203 | ], 204 | "def" : "GlobalMute:toggle()", 205 | "name" : "toggle" 206 | }, 207 | { 208 | "parameters" : [ 209 | 210 | ], 211 | "stripped_doc" : [ 212 | "UnMute all system sound input" 213 | ], 214 | "desc" : "UnMute all system sound input", 215 | "doc" : "UnMute all system sound input", 216 | "notes" : [ 217 | 218 | ], 219 | "signature" : "GlobalMute:unmute()", 220 | "type" : "Method", 221 | "returns" : [ 222 | 223 | ], 224 | "def" : "GlobalMute:unmute()", 225 | "name" : "unmute" 226 | } 227 | ], 228 | "Field" : [ 229 | 230 | ], 231 | "Method" : [ 232 | { 233 | "parameters" : [ 234 | 235 | ], 236 | "stripped_doc" : [ 237 | "Mute all system sound input, force to mute" 238 | ], 239 | "desc" : "Mute all system sound input, force to mute", 240 | "doc" : "Mute all system sound input, force to mute", 241 | "notes" : [ 242 | 243 | ], 244 | "signature" : "GlobalMute:toggle()", 245 | "type" : "Method", 246 | "returns" : [ 247 | 248 | ], 249 | "def" : "GlobalMute:toggle()", 250 | "name" : "toggle" 251 | }, 252 | { 253 | "parameters" : [ 254 | 255 | ], 256 | "stripped_doc" : [ 257 | "Mute all system sound input, force to mute" 258 | ], 259 | "desc" : "Mute all system sound input, force to mute", 260 | "doc" : "Mute all system sound input, force to mute", 261 | "notes" : [ 262 | 263 | ], 264 | "signature" : "GlobalMute:mute(force)", 265 | "type" : "Method", 266 | "returns" : [ 267 | 268 | ], 269 | "def" : "GlobalMute:mute(force)", 270 | "name" : "mute" 271 | }, 272 | { 273 | "parameters" : [ 274 | 275 | ], 276 | "stripped_doc" : [ 277 | "UnMute all system sound input" 278 | ], 279 | "desc" : "UnMute all system sound input", 280 | "doc" : "UnMute all system sound input", 281 | "notes" : [ 282 | 283 | ], 284 | "signature" : "GlobalMute:unmute()", 285 | "type" : "Method", 286 | "returns" : [ 287 | 288 | ], 289 | "def" : "GlobalMute:unmute()", 290 | "name" : "unmute" 291 | }, 292 | { 293 | "parameters" : [ 294 | 295 | ], 296 | "stripped_doc" : [ 297 | "Callback function when audio input events happen" 298 | ], 299 | "desc" : "Callback function when audio input events happen", 300 | "doc" : "Callback function when audio input events happen", 301 | "notes" : [ 302 | 303 | ], 304 | "signature" : "GlobalMute:microphone_changes(device_uid, event_name, event_scope, event_element)", 305 | "type" : "Method", 306 | "returns" : [ 307 | 308 | ], 309 | "def" : "GlobalMute:microphone_changes(device_uid, event_name, event_scope, event_element)", 310 | "name" : "microphone_changes" 311 | }, 312 | { 313 | "parameters" : [ 314 | " * applist - A table containing hotkey details for defined applications:", 315 | "", 316 | "A configuration example:", 317 | "``` lua", 318 | "local hyper = {\"ctrl\", \"alt\", \"cmd\"}", 319 | "hs.loadSpoon(\"GlobalMute\")", 320 | "spoon.GlobalMute:configure({", 321 | " unmute_background = 'file:\/\/\/Library\/Desktop%20Pictures\/Solid%20Colors\/Red%20Orange.png',", 322 | " mute_background = 'file:\/\/\/Library\/Desktop%20Pictures\/Solid%20Colors\/Turquoise%20Green.png',", 323 | " enforce_desired_state = true,", 324 | " unmute_title = \"---- THEY CAN HEAR YOU ----\",", 325 | " mute_title = \"MUTE\",", 326 | "})", 327 | "spoon.GlobalMute:bindHotkeys({", 328 | " unmute = {hyper, \"u\"},", 329 | " mute = {hyper, \"m\"},", 330 | " toggle = {hyper, \"space\"}", 331 | "})", 332 | "spoon.GlobalMute._logger.level = 3", 333 | "```" 334 | ], 335 | "stripped_doc" : [ 336 | "Binds hotkeys for GlobalMute", 337 | "" 338 | ], 339 | "desc" : "Binds hotkeys for GlobalMute", 340 | "doc" : "Binds hotkeys for GlobalMute\n\nParameters:\n * applist - A table containing hotkey details for defined applications:\n\nA configuration example:\n``` lua\nlocal hyper = {\"ctrl\", \"alt\", \"cmd\"}\nhs.loadSpoon(\"GlobalMute\")\nspoon.GlobalMute:configure({\n unmute_background = 'file:\/\/\/Library\/Desktop%20Pictures\/Solid%20Colors\/Red%20Orange.png',\n mute_background = 'file:\/\/\/Library\/Desktop%20Pictures\/Solid%20Colors\/Turquoise%20Green.png',\n enforce_desired_state = true,\n unmute_title = \"---- THEY CAN HEAR YOU ----\",\n mute_title = \"MUTE\",\n})\nspoon.GlobalMute:bindHotkeys({\n unmute = {hyper, \"u\"},\n mute = {hyper, \"m\"},\n toggle = {hyper, \"space\"}\n})\nspoon.GlobalMute._logger.level = 3\n```", 341 | "notes" : [ 342 | 343 | ], 344 | "signature" : "GlobalMute:bindHotkeys()", 345 | "type" : "Method", 346 | "returns" : [ 347 | 348 | ], 349 | "def" : "GlobalMute:bindHotkeys()", 350 | "name" : "bindHotkeys" 351 | }, 352 | { 353 | "parameters" : [ 354 | 355 | ], 356 | "stripped_doc" : [ 357 | "Set spoon level configuration" 358 | ], 359 | "desc" : "Set spoon level configuration", 360 | "doc" : "Set spoon level configuration", 361 | "notes" : [ 362 | 363 | ], 364 | "signature" : "GlobalMute:configure(conf)", 365 | "type" : "Method", 366 | "returns" : [ 367 | 368 | ], 369 | "def" : "GlobalMute:configure(conf)", 370 | "name" : "configure" 371 | }, 372 | { 373 | "parameters" : [ 374 | 375 | ], 376 | "stripped_doc" : [ 377 | "Currently does nothing (implemented so that treating this Spoon like others won't cause errors)." 378 | ], 379 | "desc" : "Currently does nothing (implemented so that treating this Spoon like others won't cause errors).", 380 | "doc" : "Currently does nothing (implemented so that treating this Spoon like others won't cause errors).", 381 | "notes" : [ 382 | 383 | ], 384 | "signature" : "GlobalMute:init()", 385 | "type" : "Method", 386 | "returns" : [ 387 | 388 | ], 389 | "def" : "GlobalMute:init()", 390 | "name" : "init" 391 | } 392 | ], 393 | "Command" : [ 394 | 395 | ], 396 | "doc" : "With this Spoon you will be able ot globally mute and unmute your Microphone devices.\n[https:\/\/github.com\/cmaahs\/global-mute-spoon](https:\/\/github.com\/cmaahs\/global-mute-spoon)\n\nThis spoon was inspired by one of my previous co-workers: https:\/\/github.com\/jesselang\/dotfiles", 397 | "name" : "GlobalMute" 398 | } 399 | ] 400 | -------------------------------------------------------------------------------- /GlobalMute.spoon/init.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (c) 2019 Christopher Maahs 2 | -- Permission is hereby granted, free of charge, to any person obtaining a copy of this 3 | -- software and associated documentation files (the "Software"), to deal in the Software 4 | -- without restriction, including without limitation the rights to use, copy, modify, merge, 5 | -- publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 6 | -- to whom the Software is furnished to do so, subject to the following conditions: 7 | -- 8 | -- The above copyright notice and this permission notice shall be included in all copies 9 | -- or substantial portions of the Software. 10 | -- 11 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | -- INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 13 | -- PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 14 | -- FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 15 | -- OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | -- DEALINGS IN THE SOFTWARE. 17 | 18 | --- === GlobalMute === 19 | --- 20 | --- With this Spoon you will be able ot globally mute and unmute your Microphone devices. 21 | --- [https://github.com/cmaahs/global-mute-spoon](https://github.com/cmaahs/global-mute-spoon) 22 | --- 23 | --- This spoon was inspired by one of my previous co-workers: https://github.com/jesselang/dotfiles 24 | -- ## TODO 25 | 26 | local obj={} 27 | obj.__index = obj 28 | 29 | -- Metadata 30 | obj.name = "GlobalMute" 31 | obj.version = "0.1" 32 | obj.author = "Christopher Maahs " 33 | obj.homepage = "https://github.com/cmaahs/global-mute-spoon" 34 | obj.license = "MIT - https://opensource.org/licenses/MIT" 35 | 36 | --- GlobalMute.logger 37 | --- Variable 38 | --- Accessible variable to adjust the logging level 39 | local logger = hs.logger.new(obj.name) 40 | obj._logger = logger 41 | logger.i("Loading ".. obj.name) 42 | 43 | 44 | -- ## Public variables 45 | 46 | -- Comment: Lots of work here to save users a little work. Previous versions required users to call 47 | -- GlobalMute:start() every time they changed GRID. The metatable work here watches for those changes and does the work :start() would have done. 48 | package.path = package.path..";Spoons/".. ... ..".spoon/?.lua" 49 | 50 | -- ## Internal 51 | 52 | function setbackground(bgfile, screens_to_change) 53 | if bgfile ~= nil then 54 | local screens = hs.screen.allScreens() 55 | for _, newScreen in ipairs(screens) do 56 | logger.d("Screens to change: ".. screens_to_change) 57 | logger.d("Changing Background: ".. newScreen:name()) 58 | if string.find(screens_to_change, newScreen:name()) then 59 | logger.d("Changing to: ".. bgfile ) 60 | newScreen:desktopImageURL(bgfile) 61 | end 62 | end 63 | end 64 | end 65 | 66 | function setmenutitle(menu, text) 67 | menu:setTitle(text) 68 | end 69 | 70 | function table_count(T) 71 | local count = 0 72 | for _ in pairs(T) do count = count + 1 end 73 | return count 74 | end 75 | 76 | -- ### Utilities 77 | 78 | -- ## Public 79 | 80 | --- GlobalMute:toggle() 81 | --- Method 82 | --- Mute all system sound input, force to mute 83 | function obj:toggle() 84 | is_synced = true 85 | for _, device in pairs(hs.audiodevice.allInputDevices()) do 86 | is_muted = device:inputMuted() 87 | logger.d("Toggle Operation, Device Mute Status Was: ".. tostring(is_muted)) 88 | 89 | if is_muted ~= self.muted then 90 | is_synced = false 91 | end 92 | end 93 | 94 | if self.muted then 95 | -- muted, now unmute 96 | self:unmute() 97 | else -- self.muted = false 98 | -- unmuted, please mute 99 | self:mute() 100 | end 101 | 102 | end 103 | 104 | --- GlobalMute:mute(force) 105 | --- Method 106 | --- Mute all system sound input, force to mute 107 | function obj:mute(force) 108 | is_changed = force or false 109 | for _, device in pairs(hs.audiodevice.allInputDevices()) do 110 | is_muted = device:inputMuted() 111 | logger.d("Mute Operation, Device Mute Status Was: ".. tostring(is_muted) .." name is: ".. device:name()) 112 | 113 | if not is_muted then 114 | out_device = hs.audiodevice.findOutputByName(device:name()) 115 | default_out = hs.audiodevice.defaultOutputDevice() 116 | device:setInputMuted(true) 117 | if out_device ~= nil then 118 | if out_device:name() == default_out:name() then 119 | logger.d("....Ensuring output is not muted".. tostring(out_volume)) 120 | out_device:setOutputMuted(false) 121 | end 122 | end 123 | is_changed = true 124 | end 125 | end 126 | 127 | if is_changed then 128 | setbackground(self.mute_bg, self.change_screens) 129 | if self.mute_title ~= nil then 130 | local titletext = hs.styledtext.new(self.mute_title,{color=hs.drawing.color.hammerspoon.osx_green}) 131 | self.mb:setTitle(titletext) 132 | else 133 | self.mb:setTitle(nil) 134 | end 135 | self.mb:setIcon(self.icon_muted) 136 | self.muted = true 137 | -- if self.sococo then 138 | -- local titletext = hs.styledtext.new('',{color=hs.drawing.color.hammerspoon.osx_red}) 139 | -- self.mbs:setTitle(titletext) 140 | -- end 141 | end 142 | 143 | return is_changed 144 | end 145 | 146 | --- GlobalMute:unmute() 147 | --- Method 148 | --- UnMute all system sound input 149 | function obj:unmute(force) 150 | is_changed = force or false 151 | for _, device in pairs(hs.audiodevice.allInputDevices()) do 152 | is_muted = device:inputMuted() 153 | logger.d("UnMute Operation, Device Mute Status Was: ".. tostring(is_muted)) 154 | 155 | if is_muted then 156 | device:setInputMuted(false) 157 | is_changed = true 158 | end 159 | end 160 | 161 | if is_changed then 162 | -- Stupid Webex detects the system level mute, and mutes the app 163 | -- however, it doesn't UNMUTE when the system level unmutes 164 | -- Just force it, I guess 165 | baseproc = {hs.application.find("Cisco Webex Meetings")} 166 | for _, app in pairs(baseproc) do 167 | logger.d("Cisco Unmute Me".. app:name()) 168 | app:selectMenuItem({"Participant", "Unmute Me"}) 169 | end 170 | setbackground(self.unmute_bg, self.change_screens) 171 | local screens = hs.screen.allScreens() 172 | for _, newScreen in ipairs(screens) do 173 | hs.alert('UNMUTED', self.yellow, newScreen) 174 | end 175 | if self.unmute_title ~= nil then 176 | local titletext = hs.styledtext.new(self.unmute_title,{color=hs.drawing.color.hammerspoon.osx_red}) 177 | self.mb:setTitle(titletext) 178 | else 179 | self.mb:setTitle(nil) 180 | end 181 | self.mb:setIcon(self.icon_unmuted) 182 | self.muted = false 183 | -- if self.sococo then 184 | -- local titletext = hs.styledtext.new('SOCOCO LISTENING ---->',{color=hs.drawing.color.hammerspoon.osx_red}) 185 | -- self.mbs:setTitle(titletext) 186 | -- end 187 | local zoom = hs.application.get'zoom.us' 188 | if zoom then 189 | local zoomwin = zoom:allWindows() 190 | local wincount = table_count(zoomwin) 191 | -- hs.alert('Zoom Windows '.. tostring(wincount)) 192 | if wincount > 1 then 193 | local sococo = hs.application.get'Sococo' 194 | if sococo then 195 | local sococowin = sococo:allWindows() 196 | local socococount = table_count(sococowin) 197 | -- hs.alert('Sococo Wins: '.. tostring(socococount)) 198 | if socococount > 0 then 199 | if self.stop_sococo then 200 | hs.alert('Closing Sococo', self.red) 201 | sococo:kill() 202 | else 203 | hs.alert('Sococo RUNNING', self.red) 204 | hs.alert('Sococo RUNNING', self.red) 205 | hs.alert('Sococo RUNNING', self.red) 206 | end 207 | end 208 | end 209 | end 210 | end 211 | end 212 | 213 | return is_changed 214 | end 215 | 216 | --- GlobalMute:microphone_changes(device_uid, event_name, event_scope, event_element) 217 | --- Method 218 | --- Callback function when audio input events happen 219 | function obj:microphone_changes(device_uid, event_name, event_scope, event_element) 220 | logger.d("microphone_changes args: ".. device_uid ..", ".. event_name ..",".. event_scope ..",".. event_element) 221 | mic = hs.audiodevice.findDeviceByUID(device_uid) 222 | if event_name == 'mute' then 223 | 224 | is_synced = true 225 | is_muted = mic:inputMuted() 226 | 227 | if is_muted ~= self.muted then 228 | is_synced = false 229 | end 230 | 231 | if self.muted then 232 | if not is_synced then 233 | -- want muted, is now unmuted 234 | local screens = hs.screen.allScreens() 235 | for _, newScreen in ipairs(screens) do 236 | hs.alert('UNMUTED Externally', self.red, newScreen) 237 | end 238 | 239 | if self.enforce_state then 240 | self:mute(true) 241 | else 242 | self:unmute(true) 243 | end 244 | end 245 | else -- self.muted = false 246 | if not is_synced then 247 | -- want unmuted, now muted 248 | local screens = hs.screen.allScreens() 249 | for _, newScreen in ipairs(screens) do 250 | hs.alert('MUTED Externally', self.yellow, newScreen) 251 | end 252 | if self.enforce_state then 253 | self:unmute(true) 254 | else 255 | self:mute(true) 256 | end 257 | end 258 | end 259 | end 260 | end 261 | 262 | -- function obj:hush() 263 | -- local stack = hs.application.find('Sococo') 264 | -- if stack then 265 | -- local allWin = stack:allWindows() 266 | -- for i, win in pairs(allWin) do 267 | -- if not stack:isHidden() and win and win:subrole() == 'AXStandardWindow' then 268 | -- win:focus() 269 | -- end 270 | -- end 271 | -- end 272 | -- hs.eventtap.event.newKeyEvent("alt", "t", true):post() 273 | -- hs.eventtap.event.newKeyEvent("alt", "t", false):post() 274 | -- hs.eventtap.event.newKeyEvent("alt", "l", true):post() 275 | -- hs.eventtap.event.newKeyEvent("alt", "l", false):post() 276 | -- if self.sococo then 277 | -- if self.muted ~= true then 278 | -- local titletext = hs.styledtext.new('SOCOCO LISTENING ---->',{color=hs.drawing.color.hammerspoon.osx_red}) 279 | -- self.mbs:setTitle(titletext) 280 | -- end 281 | -- else 282 | -- local titletext = hs.styledtext.new('',{color=hs.drawing.color.hammerspoon.osx_green}) 283 | -- self.mbs:setTitle(titletext) 284 | -- end 285 | -- self.sococo = not self.sococo 286 | -- end 287 | 288 | 289 | -- ## Spoon mechanics (`bind`, `init`) 290 | 291 | obj.hotkeys = {} 292 | obj.unmute_bg = nil 293 | obj.unmute_title = nil 294 | obj.mute_bg = nil 295 | obj.mute_title = nil 296 | obj.muted = nil 297 | obj.change_screens = "" 298 | obj.stop_sococo = false 299 | obj.enforce_state = false 300 | obj.red = hs.fnutils.copy(hs.alert.defaultStyle) 301 | obj.red.fillColor = { 302 | alpha = 0.7, 303 | red = 1 304 | } 305 | obj.red.strokeColor = { 306 | alpha = 1, 307 | red = 1 308 | } 309 | obj.yellow = hs.fnutils.copy(hs.alert.defaultStyle) 310 | obj.yellow.fillColor = { 311 | alpha = 1, 312 | red = 1, 313 | green = 1, 314 | } 315 | obj.yellow.strokeColor = { 316 | alpha = 1, 317 | red = 0, 318 | green = 0, 319 | blue = 0 320 | } 321 | obj.yellow.textColor = { 322 | alpha = 1, 323 | red = 0, 324 | green = 0, 325 | blue = 0 326 | } 327 | obj.mb = nil 328 | -- obj.mbs = nil 329 | 330 | local screens = hs.screen.allScreens() 331 | for _, newScreen in ipairs(screens) do 332 | obj.change_screens = obj.change_screens ..",".. newScreen:name() 333 | end 334 | 335 | obj.icon_muted = hs.image.imageFromASCII(table.concat({ 336 | '................', 337 | '..S..F....F.....', 338 | '.R..............', 339 | '................', 340 | '................', 341 | '................', 342 | '.....A....B.....', 343 | '................', 344 | '.....H....H.....', 345 | '.....F....F.....', 346 | '..1..D....C..8..', 347 | '..2..........7..', 348 | '.......HH.......', 349 | '...3........6...', 350 | '......J..K....P.', 351 | '.......45....Q..', 352 | '................', 353 | '.......ML.......'}, '\n'), 354 | {{ 355 | strokeColor = {alpha = 1}, 356 | fillColor = {alpha = 0}, 357 | strokeWidth = 2, 358 | shouldClose = false, 359 | -- antialias = false 360 | }, 361 | { 362 | shouldClose = true 363 | }} 364 | ) 365 | obj.icon_unmuted = hs.image.imageFromASCII(table.concat({ 366 | '................', 367 | '.....F....F.....', 368 | '................', 369 | '................', 370 | '................', 371 | '................', 372 | '.....A....B.....', 373 | '................', 374 | '.....H....H.....', 375 | '.....F....F.....', 376 | '..1..D....C..8..', 377 | '..2..........7..', 378 | '.......HH.......', 379 | '...3........6...', 380 | '......J..K......', 381 | '.......45.......', 382 | '................', 383 | '.......ML.......'}, '\n'), 384 | {{ 385 | strokeColor = {alpha = 1}, 386 | fillColor = {alpha = 0}, 387 | strokeWidth = 2, 388 | shouldClose = false, 389 | -- antialias = false 390 | }, 391 | { 392 | shouldClose = true 393 | }} 394 | ) 395 | 396 | 397 | --- GlobalMute:bindHotkeys() 398 | --- Method 399 | --- Binds hotkeys for GlobalMute 400 | --- 401 | --- Parameters: 402 | --- * applist - A table containing hotkey details for defined applications: 403 | --- 404 | --- A configuration example: 405 | --- ``` lua 406 | --- local hyper = {"ctrl", "alt", "cmd"} 407 | --- hs.loadSpoon("GlobalMute") 408 | --- spoon.GlobalMute:configure({ 409 | --- unmute_background = 'file:///Library/Desktop%20Pictures/Solid%20Colors/Red%20Orange.png', 410 | --- mute_background = 'file:///Library/Desktop%20Pictures/Solid%20Colors/Turquoise%20Green.png', 411 | --- enforce_desired_state = true, 412 | --- unmute_title = "---- THEY CAN HEAR YOU ----", 413 | --- mute_title = "MUTE", 414 | ---}) 415 | --- spoon.GlobalMute:bindHotkeys({ 416 | --- unmute = {hyper, "u"}, 417 | --- mute = {hyper, "m"}, 418 | --- toggle = {hyper, "space"} 419 | --- }) 420 | --- spoon.GlobalMute._logger.level = 3 421 | --- ``` 422 | --- 423 | function obj:bindHotkeys(mapping) 424 | logger.i("Bind Hotkeys for GlobalMute") 425 | 426 | -- -- 'hush' hotkey 427 | -- if mapping.hush then 428 | -- -- if self.mbs == nil then 429 | -- -- self.mbs = hs.menubar.new() 430 | -- -- local titletext = hs.styledtext.new('SOCOCO LISTENING',{color=hs.drawing.color.hammerspoon.osx_red}) 431 | -- -- self.mbs:setTitle(titletext) 432 | -- -- end 433 | -- self.hotkeys[#self.hotkeys + 1] = hs.hotkey.bind( 434 | -- mapping.hush[1], 435 | -- mapping.hush[2], 436 | -- function() self:hush() end) 437 | -- end 438 | 439 | -- `unmute` hotkey 440 | if mapping.unmute then 441 | self.hotkeys[#self.hotkeys + 1] = hs.hotkey.bind( 442 | mapping.unmute[1], 443 | mapping.unmute[2], 444 | function() self:unmute() end) 445 | end 446 | 447 | if mapping.mute then 448 | self.hotkeys[#self.hotkeys + 1] = hs.hotkey.bind( 449 | mapping.mute[1], 450 | mapping.mute[2], 451 | function() self:unmute() end, 452 | function() self:mute() end) 453 | end 454 | 455 | if mapping.toggle then 456 | self.hotkeys[#self.hotkeys + 1] = hs.hotkey.bind( 457 | mapping.toggle[1], 458 | mapping.toggle[2], 459 | function() self:toggle() end) 460 | end 461 | end 462 | 463 | --- GlobalMute:configure(conf) 464 | --- Method 465 | --- Set spoon level configuration 466 | function obj:configure(conf) 467 | logger.i("Set configuration for GlobalMute") 468 | for key,confitem in pairs(conf) do 469 | if key == 'stop_sococo_for_zoom' then 470 | self.stop_sococo = confitem 471 | end 472 | if key == 'unmute_background' then 473 | self.unmute_bg = confitem 474 | end 475 | if key == 'mute_background' then 476 | self.mute_bg = confitem 477 | end 478 | if key == 'enforce_desired_state' then 479 | self.enforce_state = confitem 480 | end 481 | if key == 'mute_title' then 482 | self.mute_title = confitem 483 | end 484 | if key == 'unmute_title' then 485 | self.unmute_title = confitem 486 | end 487 | if key == 'change_screens' then 488 | self.change_screens = confitem 489 | end 490 | 491 | end 492 | -- if self.mute_bg == nil then 493 | -- self.mute_bg = 'file:///Library/Desktop%20Pictures/Solid%20Colors/Turquoise%20Green.png' 494 | -- end 495 | -- if self.unmute_bg == nil then 496 | -- self.unmute_bg = 'file:///Library/Desktop%20Pictures/Solid%20Colors/Red%20Orange.png' 497 | -- end 498 | self:mute(true) 499 | self.muted = true 500 | -- self.sococo = true 501 | end 502 | 503 | --- GlobalMute:init() 504 | --- Method 505 | --- Currently does nothing (implemented so that treating this Spoon like others won't cause errors). 506 | function obj:init() 507 | -- void (but it could be used to initialize the module) 508 | for _, device in pairs(hs.audiodevice.allInputDevices()) do 509 | device:watcherCallback(hs.fnutils.partial(self.microphone_changes, self)):watcherStart() 510 | logger.w("Setting up watcher for audio device ".. device:name()) 511 | end 512 | if self.mb == nil then 513 | self.mb = hs.menubar.new() 514 | end 515 | -- if self.mbs == nil then 516 | -- self.mbs = hs.menubar.new() 517 | -- end 518 | end 519 | 520 | return obj -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Christopher Maahs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Global Mute 2 | 3 | With this script you can manage the mute/unmute of your computer's microphone at the system level. 4 | Thus avoiding having to un-bury whatever window holds the button you need to click to mute or unmute 5 | yourself. 6 | 7 | This Spoon has three possible bindings: mute, unmute, and toggle. 8 | 9 | If you don't specify a key binding for toggle, then you MUST specify a mute AND unmute key binding. 10 | You can specify only the key binding for toggle and not define the other two. The mute key binding 11 | can be HELD down and used similarly to a walkie-talkie button. 12 | 13 | One of my previous co-workers initially provided me with this particular feature in Hammerspoon. His 14 | original work can be found here: [Jesse Lang](https://github.com/jesselang/dotfiles) 15 | The initial work contained an alert that would flash on a timer, keeping you aware that the MIC was 16 | hot. As I added monitors however, that alert would only flash on the monitor containing the front 17 | most application. As I made modifications, I also figured I would convert it to a Spoon format, 18 | using the [Miro Windows Manager](https://github.com/miromannino/miro-windows-manager) code as my 19 | baseline. 20 | 21 | ## Special Note for ZOOM users 22 | 23 | ZOOM meetings through me for a bit of loop when I discovered that my MIC was being opened seemingly randomly. It turns out that 24 | ZOOM will now unmute your system mic devices at startup, independently of it's own MUTE operation. Even opening the Settings/Audio 25 | panel will unmute the system mic devices. And of course this is BAD. Bad, bad programmers at Zoom! There doesn't seem to be a 26 | way to stop this nefarious behavior through settings. 27 | 28 | Presumably other collaboration/conference tools may also practice this behavior, so a callback was added to handle this change. 29 | 30 | ## Extra Special Note for SOCOCO users 31 | 32 | From the *this feature is for me* department. Managing SOCOCO *and* any other Conferencing software becomes a huge "sound and mic" 33 | management nightmare. Feeback loops, people talking on their personal web conference and the sound piping right into Sococo. This 34 | becomes especially true when you start using a Global Mic Mute tool like this one. Initially my thought was to send ALT-T and ALT-L 35 | to toggle both the sound and mic within Sococo, and while that can be done, one minor drawback is that the Sococo app becomes the 36 | focus window, and one major drawback of not being able to query what state Sococo sound and mic are in. So I opted for the only 37 | solution that shouldn't really fail. If you enable this feature through the *configure* call, when you toggle your mic ON, the spoon 38 | will check to see if you have Sococo open *and* check to see if you have more than the Zoom control app open. If you don't run the 39 | Zoom control app, you may have to adjust the code to trigger on > 0 Zoom windows, rather than > 1. In any case, if it detects you 40 | are running Sococo *and* more than 1 Zoom window, it simply sends a `kill` to the Sococo app. Problem:Solved(). Hopefully others 41 | can use this as an example for killing Sococo when they run other Conferencing apps. 42 | 43 | ## The glories of Mojave 44 | 45 | It turns out that some of my fellow MacOS users haven't upgraded to Mojave yet, and thus the whole changing the background colors doesn't 46 | actually show through their menubar and dock. How quickly one forgets that those didn't used to be transparent... 47 | 48 | So, in with the OLD, and some extra options to add some text to make it more visible that you are unmuted... Which of course is the whole point. 49 | 50 | I've brought back the Microphone icon from the original Push2Talk.lua script, and added an option to add a Title/Text component to it. 51 | 52 | ## Installation 53 | 54 | This will create a ~/tmp temp file in your home directory and clone the repository into it, then move the Spoon to the ~/.hammerspoon/Spoons install directory. Then add the base loading lines into your ~/.hammerspoon/init.lua file. Once complete you can clean up the ~/tmp/global-mute-spoon directory as you see fit. 55 | 56 | - NOTE: The upgrade to Catalina from Mojave wiped out my `/Library/Desktop Pictures/Solid Colors` directory. I simply restored them from time machine. For those of you who are installing global-mute-spoon for the first time on Catalina, you'll have to come up with a replacement for the background color change. 57 | - UPDATE: Each Catalina patch is wiping out the `Solid Colors` directory files. The directory exists, the jpg files just disappear. At this point I'm guessing that they were not licensed by Apple, and they are taking steps to rectify. Anyway you look at it, it seems odd. I may generate my own "solid color" jpg files and just add them to the repository. 58 | 59 | ```bash 60 | mkdir ~/tmp 61 | 62 | cd ~/tmp && git clone https://github.com/cmaahs/global-mute-spoon.git 63 | cd ~/tmp/global-mute-spoon 64 | mv GlobalMute.spoon ~/.hammerspoon/Spoons 65 | 66 | if grep -Fxq 'local hyper = {"ctrl", "alt", "cmd"}' ~/.hammerspoon/init.lua 67 | then 68 | echo "line already exists." 69 | else 70 | echo 'local hyper = {"ctrl", "alt", "cmd"}' >> ~/.hammerspoon/init.lua 71 | fi 72 | if grep -Fxq 'local lesshyper = {"ctrl", "alt"}' ~/.hammerspoon/init.lua 73 | then 74 | echo "line already exists." 75 | else 76 | echo 'local lesshyper = {"ctrl", "alt"}' >> ~/.hammerspoon/init.lua 77 | fi 78 | 79 | if grep -Fxq 'hs.loadSpoon("GlobalMute")' ~/.hammerspoon/init.lua 80 | then 81 | echo "line already exists." 82 | else 83 | echo 'hs.loadSpoon("GlobalMute")' >> ~/.hammerspoon/init.lua 84 | fi 85 | 86 | if grep -Fxq 'spoon.GlobalMute:bindHotkeys({ unmute = {lesshyper, "u"}, mute = {lesshyper, "m"}, toggle = {hyper, "space"} })' ~/.hammerspoon/init.lua 87 | then 88 | echo "line already exists." 89 | else 90 | echo 'spoon.GlobalMute:bindHotkeys({ unmute = {lesshyper, "u"}, mute = {lesshyper, "m"}, toggle = {hyper, "space"} })' >> ~/.hammerspoon/init.lua 91 | fi 92 | 93 | if grep -Fxq 'spoon.GlobalMute:configure({ unmute_background = "file:///Library/Desktop%20Pictures/Solid%20Colors/Red%20Orange.png", mute_background = "file:///Library/Desktop%20Pictures/Solid%20Colors/Turquoise%20Green.png", enforce_desired_state = true, stop_sococo_for_zoom = true,})' ~/.hammerspoon/init.lua 94 | then 95 | echo "line already exists." 96 | else 97 | echo 'spoon.GlobalMute:configure({ unmute_background = "file:///Library/Desktop%20Pictures/Solid%20Colors/Red%20Orange.png", mute_background = "file:///Library/Desktop%20Pictures/Solid%20Colors/Turquoise%20Green.png", enforce_desired_state = true, stop_sococo_for_zoom = true,})' >> ~/.hammerspoon/init.lua 98 | fi 99 | ``` 100 | 101 | ## Configuration 102 | 103 | The configuration file looks like this: 104 | 105 | ```lua 106 | local hyper = {"ctrl", "alt", "cmd"} 107 | local lesshyper = {"ctrl", "alt"} 108 | hs.loadSpoon("GlobalMute") 109 | spoon.GlobalMute:bindHotkeys({ 110 | unmute = {lesshyper, "u"}, 111 | mute = {lesshyper, "m"}, 112 | toggle = {hyper, "space"} 113 | }) 114 | spoon.GlobalMute:configure({ 115 | unmute_background = 'file:///Library/Desktop%20Pictures/Solid%20Colors/Red%20Orange.png', 116 | mute_background = 'file:///Library/Desktop%20Pictures/Solid%20Colors/Turquoise%20Green.png', 117 | enforce_desired_state = true, 118 | stop_sococo_for_zoom = true, 119 | unmute_title = "<---- THEY CAN HEAR YOU -----", 120 | mute_title = "<-- MUTE", 121 | -- change_screens = "SCREENNAME1, SCREENNAME2" -- This will only change the background of the specific screens. string.find() 122 | }) 123 | spoon.GlobalMute._logger.level = 3 124 | ``` 125 | 126 | The `enforced_desired_state` is the flag that handles when external forces make changes to the mute at a system level. If set to 127 | `false` your background will change and the new mute state will be accepted. If set to `true` the GlobalMute spoon will make a 128 | course correction and reset the system mute level on the device to what your current setting was. 129 | 130 | The mute_title and unmute_title text will appear to the right of the microphone icon in the menubar. Hopefully making it easier to 131 | pickup on when you are actually unmuted. If you don't want the mute_title text, simply don't provide that option to the configure 132 | routine. 133 | 134 | ## TODO / Thoughts 135 | 136 | The downside is that one cannot just change the background color of the menu bar on the mac. Though in Dark Mode it is generally transparent and thus setting a Red/Orange background image allows it to bleed through. Set the "mute_background" to your normal image that you use and when you are muted (the normal mode) your background will be normal. This will be a problem for those who have adopted any **active** type backgrounds. Possibly there is a way to handle this, certainly some AppleScript must be able to configure that feature. It isn't high on my list of things, so if you have a burning desire to set an active background, feel free to submit a PR. 137 | --------------------------------------------------------------------------------