diff --git a/hammerspoon/Makefile b/hammerspoon/Makefile index 5ee5419..cb6192d 100644 --- a/hammerspoon/Makefile +++ b/hammerspoon/Makefile @@ -70,3 +70,4 @@ endef $(eval $(call dep-file,inspect.lua,"https://github.com/kikito/inspect.lua/raw/v3.1.0/inspect.lua")) $(eval $(call dep-spoon,RoundedCorners,"https://github.com/Hammerspoon/Spoons/raw/master/Spoons/RoundedCorners.spoon.zip")) $(eval $(call dep-spoon,HeadphoneAutoPause,"https://github.com/Hammerspoon/Spoons/raw/master/Spoons/HeadphoneAutoPause.spoon.zip")) +$(eval $(call dep-spoon,PushToTalk,"https://github.com/Hammerspoon/Spoons/raw/master/Spoons/PushToTalk.spoon.zip")) diff --git a/hammerspoon/Spoons/PushToTalk.spoon/docs.json b/hammerspoon/Spoons/PushToTalk.spoon/docs.json new file mode 100644 index 0000000..9501442 --- /dev/null +++ b/hammerspoon/Spoons/PushToTalk.spoon/docs.json @@ -0,0 +1,100 @@ +[ + { + "Command": [], + "Constant": [], + "Constructor": [], + "Deprecated": [], + "Field": [], + "Function": [], + "Method": [ + { + "def": "PushToTalk:init()", + "desc": "Starts menu and key watcher", + "doc": "Starts menu and key watcher", + "name": "init", + "signature": "PushToTalk:init()", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "PushToTalk:stop()", + "desc": "Stops PushToTalk", + "doc": "Stops PushToTalk", + "name": "stop", + "signature": "PushToTalk:stop()", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "PushToTalk:toggleStates()", + "desc": "Cycle states in order", + "doc": "Cycle states in order\n\nParameters:\n * states - A array of states to toggle. For example: `{'push-to-talk', 'release-to-talk'}`", + "name": "toggleStates", + "parameters": [ + " * states - A array of states to toggle. For example: `{'push-to-talk', 'release-to-talk'}`" + ], + "signature": "PushToTalk:toggleStates()", + "stripped_doc": "", + "type": "Method" + } + ], + "Variable": [ + { + "def": "PushToTalk.app_switcher", + "desc": "Takes mapping from application name to mic state.", + "doc": "Takes mapping from application name to mic state.\nFor example this `{ ['zoom.us'] = 'push-to-talk' }` will switch mic to `push-to-talk` state when Zoom app starts.", + "name": "app_switcher", + "signature": "PushToTalk.app_switcher", + "stripped_doc": "For example this `{ ['zoom.us'] = 'push-to-talk' }` will switch mic to `push-to-talk` state when Zoom app starts.", + "type": "Variable" + } + ], + "desc": "Implements push-to-talk and push-to-mute functionality with `fn` key.", + "doc": "Implements push-to-talk and push-to-mute functionality with `fn` key.\nI implemented this after reading Gitlab remote handbook https://about.gitlab.com/handbook/communication/ about Shush utility.\n\nMy workflow:\n\nWhen Zoom starts, PushToTalk automatically changes mic state from `default`\nto `push-to-talk`, so I need to press `fn` key to unmute myself and speak.\nIf I need to actively chat in group meeting or it's one-on-one meeting,\nI'm switching to `push-to-mute` state, so mic will be unmute by default and `fn` key mutes it.\n\nPushToTalk has menubar with colorful icons so you can easily see current mic state.\n\nSample config: `spoon.SpoonInstall:andUse(\"PushToTalk\", {start = true, config = { app_switcher = { ['zoom.us'] = 'push-to-talk' }}})`\nand separate keybinding to toggle states with lambda function `function() spoon.PushToTalk.toggleStates({'push-to-talk', 'release-to-talk'}) end`\n\nCheck out my config: https://github.com/skrypka/hammerspoon_config/blob/master/init.lua", + "items": [ + { + "def": "PushToTalk.app_switcher", + "desc": "Takes mapping from application name to mic state.", + "doc": "Takes mapping from application name to mic state.\nFor example this `{ ['zoom.us'] = 'push-to-talk' }` will switch mic to `push-to-talk` state when Zoom app starts.", + "name": "app_switcher", + "signature": "PushToTalk.app_switcher", + "stripped_doc": "For example this `{ ['zoom.us'] = 'push-to-talk' }` will switch mic to `push-to-talk` state when Zoom app starts.", + "type": "Variable" + }, + { + "def": "PushToTalk:init()", + "desc": "Starts menu and key watcher", + "doc": "Starts menu and key watcher", + "name": "init", + "signature": "PushToTalk:init()", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "PushToTalk:stop()", + "desc": "Stops PushToTalk", + "doc": "Stops PushToTalk", + "name": "stop", + "signature": "PushToTalk:stop()", + "stripped_doc": "", + "type": "Method" + }, + { + "def": "PushToTalk:toggleStates()", + "desc": "Cycle states in order", + "doc": "Cycle states in order\n\nParameters:\n * states - A array of states to toggle. For example: `{'push-to-talk', 'release-to-talk'}`", + "name": "toggleStates", + "parameters": [ + " * states - A array of states to toggle. For example: `{'push-to-talk', 'release-to-talk'}`" + ], + "signature": "PushToTalk:toggleStates()", + "stripped_doc": "", + "type": "Method" + } + ], + "name": "PushToTalk", + "stripped_doc": "I implemented this after reading Gitlab remote handbook https://about.gitlab.com/handbook/communication/ about Shush utility.\n\nMy workflow:\n\nWhen Zoom starts, PushToTalk automatically changes mic state from `default`\nto `push-to-talk`, so I need to press `fn` key to unmute myself and speak.\nIf I need to actively chat in group meeting or it's one-on-one meeting,\nI'm switching to `push-to-mute` state, so mic will be unmute by default and `fn` key mutes it.\n\nPushToTalk has menubar with colorful icons so you can easily see current mic state.\n\nSample config: `spoon.SpoonInstall:andUse(\"PushToTalk\", {start = true, config = { app_switcher = { ['zoom.us'] = 'push-to-talk' }}})`\nand separate keybinding to toggle states with lambda function `function() spoon.PushToTalk.toggleStates({'push-to-talk', 'release-to-talk'}) end`\n\nCheck out my config: https://github.com/skrypka/hammerspoon_config/blob/master/init.lua", + "submodules": [], + "type": "Module" + } +] \ No newline at end of file diff --git a/hammerspoon/Spoons/PushToTalk.spoon/init.lua b/hammerspoon/Spoons/PushToTalk.spoon/init.lua new file mode 100644 index 0000000..240c175 --- /dev/null +++ b/hammerspoon/Spoons/PushToTalk.spoon/init.lua @@ -0,0 +1,147 @@ +--- === PushToTalk === +--- +--- Implements push-to-talk and push-to-mute functionality with `fn` key. +--- I implemented this after reading Gitlab remote handbook https://about.gitlab.com/handbook/communication/ about Shush utility. +--- +--- My workflow: +--- +--- When Zoom starts, PushToTalk automatically changes mic state from `default` +--- to `push-to-talk`, so I need to press `fn` key to unmute myself and speak. +--- If I need to actively chat in group meeting or it's one-on-one meeting, +--- I'm switching to `push-to-mute` state, so mic will be unmute by default and `fn` key mutes it. +--- +--- PushToTalk has menubar with colorful icons so you can easily see current mic state. +--- +--- Sample config: `spoon.SpoonInstall:andUse("PushToTalk", {start = true, config = { app_switcher = { ['zoom.us'] = 'push-to-talk' }}})` +--- and separate keybinding to toggle states with lambda function `function() spoon.PushToTalk.toggleStates({'push-to-talk', 'release-to-talk'}) end` +--- +--- Check out my config: https://github.com/skrypka/hammerspoon_config/blob/master/init.lua + +local obj = {} +obj.__index = obj + +-- Metadata +obj.name = "PushToTalk" +obj.version = "0.1" +obj.author = "Roman Khomenko " +obj.homepage = "https://github.com/Hammerspoon/Spoons" +obj.license = "MIT - https://opensource.org/licenses/MIT" + +obj.defaultState = 'unmute' + +obj.state = obj.defaultState +obj.pushed = false +--- PushToTalk.app_switcher +--- Variable +--- Takes mapping from application name to mic state. +--- For example this `{ ['zoom.us'] = 'push-to-talk' }` will switch mic to `push-to-talk` state when Zoom app starts. +obj.app_switcher = {} + +local function showState() + local device = hs.audiodevice.defaultInputDevice() + local muted = false + if obj.state == 'unmute' then + obj.menubar:setIcon(hs.spoons.resourcePath("speak.pdf")) + elseif obj.state == 'mute' then + obj.menubar:setIcon(hs.spoons.resourcePath("muted.pdf")) + muted = true + elseif obj.state == 'push-to-talk' then + if obj.pushed then + obj.menubar:setIcon(hs.spoons.resourcePath("record.pdf"), false) + else + obj.menubar:setIcon(hs.spoons.resourcePath("unrecord.pdf")) + muted = true + end + elseif obj.state == 'release-to-talk' then + if obj.pushed then + obj.menubar:setIcon(hs.spoons.resourcePath("unrecord.pdf")) + muted = true + else + obj.menubar:setIcon(hs.spoons.resourcePath("record.pdf"), false) + end + end + + device:setMuted(muted) +end + +function obj.setState(s) + obj.state = s + showState() +end + +obj.menutable = { + { title = "UnMuted", fn = function() obj.setState('unmute') end }, + { title = "Muted", fn = function() obj.setState('mute') end }, + { title = "Push-to-talk (fn)", fn = function() obj.setState('push-to-talk') end }, + { title = "Release-to-talk (fn)", fn = function() obj.setState('release-to-talk') end }, +} + +local function appWatcher(appName, eventType, appObject) + local new_app_state = obj.app_switcher[appName]; + if (new_app_state) then + if (eventType == hs.application.watcher.launching) then + obj.setState(new_app_state) + elseif (eventType == hs.application.watcher.terminated) then + obj.setState(obj.defaultState) + end + end +end + +local function eventTapWatcher(event) + device = hs.audiodevice.defaultInputDevice() + if event:getFlags()['fn'] then + obj.pushed = true + else + obj.pushed = false + end + showState() +end + +--- PushToTalk:init() +--- Method +--- Initial setup. It's empty currently +function obj:init() +end + +--- PushToTalk:init() +--- Method +--- Starts menu and key watcher +function obj:start() + self:stop() + obj.appWatcher = hs.application.watcher.new(appWatcher) + obj.appWatcher:start() + + obj.eventTapWatcher = hs.eventtap.new({hs.eventtap.event.types.flagsChanged}, eventTapWatcher) + obj.eventTapWatcher:start() + + obj.menubar = hs.menubar.new() + obj.menubar:setMenu(obj.menutable) + obj.setState(obj.state) +end + +--- PushToTalk:stop() +--- Method +--- Stops PushToTalk +function obj:stop() + if obj.appWatcher then obj.appWatcher:stop() end + if obj.eventTapWatcher then obj.eventTapWatcher:stop() end + if obj.menubar then obj.menubar:delete() end +end + +--- PushToTalk:toggleStates() +--- Method +--- Cycle states in order +--- +--- Parameters: +--- * states - A array of states to toggle. For example: `{'push-to-talk', 'release-to-talk'}` +function obj:toggleStates(states) + new_state = states[1] + for i, v in pairs(states) do + if v == obj.state then + new_state = states[(i % #states) + 1] + end + end + obj.setState(new_state) +end + +return obj diff --git a/hammerspoon/Spoons/PushToTalk.spoon/muted.pdf b/hammerspoon/Spoons/PushToTalk.spoon/muted.pdf new file mode 100644 index 0000000..a600956 Binary files /dev/null and b/hammerspoon/Spoons/PushToTalk.spoon/muted.pdf differ diff --git a/hammerspoon/Spoons/PushToTalk.spoon/record.pdf b/hammerspoon/Spoons/PushToTalk.spoon/record.pdf new file mode 100644 index 0000000..26b4929 Binary files /dev/null and b/hammerspoon/Spoons/PushToTalk.spoon/record.pdf differ diff --git a/hammerspoon/Spoons/PushToTalk.spoon/speak.pdf b/hammerspoon/Spoons/PushToTalk.spoon/speak.pdf new file mode 100644 index 0000000..e18d159 --- /dev/null +++ b/hammerspoon/Spoons/PushToTalk.spoon/speak.pdf @@ -0,0 +1,68 @@ +%PDF-1.4 +% +3 0 obj +<< /Length 4 0 R + /Filter /FlateDecode +>> +stream +x]In0 EreVS>(}'ǎ4j*\v0Tz,+t شz((yZ@mftexE'Ck9o:aӶ6 8s;ҩ HkדOuwC+FT{\CIeV6xzsm،@> + >> +>> +endobj +5 0 obj +<< /Type /Page + /Parent 1 0 R + /MediaBox [ 0 0 15.75 15.75 ] + /Contents 3 0 R + /Group << + /Type /Group + /S /Transparency + /I true + /CS /DeviceRGB + >> + /Resources 2 0 R +>> +endobj +1 0 obj +<< /Type /Pages + /Kids [ 5 0 R ] + /Count 1 +>> +endobj +6 0 obj +<< /Creator (cairo 1.14.10 (http://cairographics.org)) + /Producer (cairo 1.14.10 (http://cairographics.org)) +>> +endobj +7 0 obj +<< /Type /Catalog + /Pages 1 0 R +>> +endobj +xref +0 8 +0000000000 65535 f +0000000714 00000 n +0000000424 00000 n +0000000015 00000 n +0000000402 00000 n +0000000496 00000 n +0000000779 00000 n +0000000908 00000 n +trailer +<< /Size 8 + /Root 7 0 R + /Info 6 0 R +>> +startxref +960 +%%EOF diff --git a/hammerspoon/Spoons/PushToTalk.spoon/unrecord.pdf b/hammerspoon/Spoons/PushToTalk.spoon/unrecord.pdf new file mode 100644 index 0000000..7a4abf3 --- /dev/null +++ b/hammerspoon/Spoons/PushToTalk.spoon/unrecord.pdf @@ -0,0 +1,69 @@ +%PDF-1.4 +% +3 0 obj +<< /Length 4 0 R + /Filter /FlateDecode +>> +stream +x]= +A ]Lv{AX-BFP- m Cۛ&a!]i 7h,4qg3:pp+BP$ jNRߘ:E֬k&ckm-?Ϙh/C3b +endstream +endobj +4 0 obj + 149 +endobj +2 0 obj +<< + /ExtGState << + /a0 << /CA 1 /ca 1 >> + >> +>> +endobj +5 0 obj +<< /Type /Page + /Parent 1 0 R + /MediaBox [ 0 0 15.75 15.75 ] + /Contents 3 0 R + /Group << + /Type /Group + /S /Transparency + /I true + /CS /DeviceRGB + >> + /Resources 2 0 R +>> +endobj +1 0 obj +<< /Type /Pages + /Kids [ 5 0 R ] + /Count 1 +>> +endobj +6 0 obj +<< /Creator (cairo 1.14.10 (http://cairographics.org)) + /Producer (cairo 1.14.10 (http://cairographics.org)) +>> +endobj +7 0 obj +<< /Type /Catalog + /Pages 1 0 R +>> +endobj +xref +0 8 +0000000000 65535 f +0000000553 00000 n +0000000263 00000 n +0000000015 00000 n +0000000241 00000 n +0000000335 00000 n +0000000618 00000 n +0000000747 00000 n +trailer +<< /Size 8 + /Root 7 0 R + /Info 6 0 R +>> +startxref +799 +%%EOF diff --git a/hammerspoon/init.lua b/hammerspoon/init.lua index a7e7d4d..00c680b 100644 --- a/hammerspoon/init.lua +++ b/hammerspoon/init.lua @@ -28,6 +28,11 @@ hs.loadSpoon('HeadphoneAutoPause') spoon.HeadphoneAutoPause.autoResume = false spoon.HeadphoneAutoPause:start() +-- Enable push-to-talk microphone functionality when specific apps are running. +hs.loadSpoon('PushToTalk') +spoon.PushToTalk.app_switcher = { ['TeamSpeak 3'] = 'push-to-talk' } +spoon.PushToTalk:start() + -------------------------------------------------------------------------------- -- Host specific configuration --------------------------------------------------------------------------------