├── LICENSE ├── README.md ├── _Spoons ├── CPUMEMBAT.spoon │ └── init.lua ├── MountedVolumes.spoon │ ├── README.md │ ├── docs.json │ ├── init.lua │ └── list.png └── SlidingPanels.spoon │ ├── README.md │ ├── init.lua │ ├── slidingPanelObject.lua │ └── widgets │ └── FromSpoon.lua ├── _gists ├── LSUIElementCheck.lua ├── altPress.lua ├── doubleTap.lua ├── graphpaper.lua ├── moonscript_traceback.lua └── pi.lua ├── _init.lua ├── _init.test.lua ├── _localAssets ├── .gitkeep ├── index.html ├── psychotic.png ├── sample.cgi └── sample.lp ├── _scratch ├── CMIicon.lua ├── _eventtap_notes.txt ├── alerts.lua ├── altnames.lua ├── ansi.lua ├── avplayerExample.lua ├── axh.lua ├── boxes.lua ├── buttonExamples.lua ├── chooseyMcChooseyier.lua ├── colorTests.lua ├── dash.lua ├── digest.lua ├── dragging.lua ├── dterm.lua ├── dumpStdout.lua ├── eventCapture.lua ├── eventtest.lua ├── examineHotKeys.lua ├── examiner.lua ├── fnkey.lua ├── focusedWindowChange.lua ├── globalVsLocalLookups.lua ├── graphPaper.lua ├── graphpaper2.lua ├── groupDrawings.lua ├── historyChooser.lua ├── idleWatcher.lua ├── imagetest.lua ├── imageviewExample.lua ├── menuiconplaysheet.lua ├── modalSuppression.lua ├── modtest.lua ├── nastest.lua ├── nc.lua ├── newfilemenu.lua ├── newimage.lua ├── no_timemachine_icon.lua ├── nsvalueConversions.lua ├── objecttable.lua ├── otherMouseWindowMove.lua ├── patternMatch.lua ├── progressItemExample.lua ├── rand.lua ├── rdoc.lua ├── sampleimages.lua ├── serviceTest.lua ├── slidingPanel.canvas.lua ├── slidingPanel.old.lua ├── spacescreendiff.lua ├── t.sh ├── tablegrowth.lua ├── test.lua ├── text.lua ├── textStuff.lua ├── textfieldExamples.lua ├── tm.lua ├── tmtranscript.txt ├── touchbarWindowSlide.lua ├── touchbartest1.lua ├── touchbartest2.lua ├── touchbartest3.lua ├── trackpadDraw.lua ├── usage.lua ├── viKeys.lua ├── webviewOtherURLS.lua └── zerobrane.lua ├── functionAsBeziers.lua ├── geekery.lua ├── geeklets ├── addToClockGeeklet.lua ├── geeklets.txt ├── wifi.lua └── wifi.sh ├── hammer-build ├── init.lua ├── nv.lua ├── resources └── timebg.png ├── robots ├── robotest.lua └── rsClamping.lua ├── test.lua └── utils ├── _actions ├── TMwidget.lua ├── annoyances.lua ├── battery_usbdrives.lua ├── consoleHistory.lua ├── crash_test_dummy.lua ├── dots.lua ├── geeklets.lua ├── inspectors.lua ├── localAssets.lua ├── location.lua ├── loopcatcher.lua ├── luaFileChangeWatcher.lua ├── nc.lua ├── popConsole.lua ├── preParsers.lua ├── remoteCheck.lua ├── screen_bluetooth_toggle.lua └── siteWatcher.lua ├── _keys ├── cheatsheet.lua ├── chooseApp.lua ├── documentsWebServer.lua ├── fonts.lua ├── gridded.lua ├── information.lua ├── redshift.lua ├── singletons.lua ├── switcher.lua ├── touchbar.lua ├── viKeys.lua ├── wifiSNR.lua └── windowMemory.lua ├── _menus ├── XProtectStatus │ ├── XProtectPluginChecker.png │ ├── init.lua │ └── myXProtectStatus.png ├── amphetamine.lua ├── applicationMenu.lua ├── battery.lua ├── dateMenu.lua ├── developerMenu.lua ├── documentsMenu.lua ├── hammerspoonMenu.lua └── newClipper.lua ├── _panels └── infoPanel.lua ├── consoleToolbar.lua ├── consolidateMenus.lua ├── dev_info.lua ├── docmaker.lua ├── fontTables.lua ├── gc.lua ├── kodiRemote.lua ├── prompter.lua ├── require.lua ├── speech.lua ├── spinner.lua ├── typee.lua ├── watchables.lua └── wifimeter.lua /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Aaron Magill 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ***This Repository is no longer updated or maintained*** 2 | ======================================================== 3 | 4 | I found it was getting unwieldy and wanted to slim things up and simplify, so my current active configuration can be found at https://github.com/asmagill/hammerspoon-config-take2. 5 | 6 | I'm keeping this around for reference, and occasionally I do migrate some things from here that I find that I do actually miss, but in general, this repository should be considered outdated and any changes or modifications will be found at the new location. 7 | 8 | *...And now back to your regularly scheduled README.md...* 9 | 10 | Personal Configuration for Hammerspoon 11 | ====================================== 12 | 13 | My configuration files for Hammerspoon 14 | 15 | Mostly random junk I find useful, a few tips and tricks that might be interesting, and a playground for the hamster running on the squeaky wheel of my brain. 16 | 17 | Modules which aren't currently in the Hammerspoon core, and are also not in one of the sub-directories here, can most likely be found in one of the following places: 18 | 19 | 1. Luarocks (not so much anymore, but a few, maybe, especially if they're from Mjolnir...) 20 | 2. https://github.com/asmagill/hammerspoon_asm 21 | 3. https://github.com/asmagill/hammerspoon_asm.undocumented 22 | 23 | 24 | 25 | ### License 26 | 27 | > Released under MIT license. 28 | > 29 | > Copyright (c) 2014 Aaron Magill 30 | > 31 | > Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 32 | > 33 | > The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 34 | > 35 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 36 | > 37 | -------------------------------------------------------------------------------- /_Spoons/MountedVolumes.spoon/list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmagill/hammerspoon-config/432c65705203d7743d3298441bd4319137b466fd/_Spoons/MountedVolumes.spoon/list.png -------------------------------------------------------------------------------- /_Spoons/SlidingPanels.spoon/README.md: -------------------------------------------------------------------------------- 1 | SlidingPanels 2 | ============= 3 | 4 | Create sliding panels which can emerge from the sides of your monitor to display canvas and guitk element objects 5 | 6 | Also requires [hs._asm.guitk](https://github.com/asmagill/hammerspoon_asm/tree/master/guitk) version 0.1.5alpha or newer to be installed. GUITK is a candidate for future inclusion in the Hammerspoon core modules, so hopefully this requirement is temporary 7 | 8 | TODO: 9 | * Document, including docs.json file and slidingPanelObject.lua version 10 | * Add methods to add/remove canvas and guitk element objects, including slidingPanelObject.lua version 11 | 12 | Download: `svn export https://github.com/asmagill/hammerspoon-config/trunk/_Spoons/SlidingPanels.spoon` 13 | 14 | ### Status 15 | 16 | This is so much a work in progress that I hesitate to even recommend that you look at it. An example of how to use it can be found at https://github.com/asmagill/hammerspoon-config/tree/master/utils/_panels/infoPanel.lua. 17 | 18 | You will also need the `MountedVolumes` spoon for the example (or remove the third `addWidget` line) which is currently available at https://github.com/asmagill/Spoons/raw/MountedVolumes/Spoons/MountedVolumes.spoon.zip (a pull request to the Hammerspoon master spoon repository has been submitted, but hasn't been merged yet) 19 | 20 | To trigger the panel, hold down the `fn` key on your keyboard and move the mouse pointer to the bottom of the screen and wait a second or two. If you are not on a laptop, you can remove (or change) the requirement to use the `fn` key by removing the `modifiers` line. 21 | 22 | To release the panel, move the mouse up and then back to the bottom of the screen (`fn` is not required to release the panel). 23 | 24 | As you can see here, documentation is lacking at present, and I'm not even positive the above syntax won't be changed in the future. 25 | 26 | ### Usage 27 | ~~~lua 28 | SlidingPanels = hs.loadSpoon("SlidingPanels") 29 | ~~~ 30 | 31 | ### Contents 32 | 33 | 34 | ##### Module Variables 35 | * SlidingPanels.logger 36 | 37 | - - - 38 | 39 | ### Module Variables 40 | 41 | 42 | ~~~lua 43 | SlidingPanels.logger 44 | ~~~ 45 | Logger object used within the Spoon. Can be accessed to set the default log level for the messages coming from the Spoon. 46 | 47 | - - - 48 | 49 | ### License 50 | 51 | > The MIT License (MIT) 52 | > 53 | > Copyright (c) 2017 Aaron Magill 54 | > 55 | > Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 56 | > 57 | > The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 58 | > 59 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 60 | > 61 | 62 | 63 | -------------------------------------------------------------------------------- /_gists/LSUIElementCheck.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- Code snippit to check/add LSUIElement to Hammerspoon's Info.plist file 3 | -- if running 10.8, since this entry is required for HS to be able to toggle 4 | -- the dock icon. Surprisingly it isn't in later versions... 5 | -- 6 | -- Save this file as LSUIElementCheck.lua in ~/.hammerspoon/ and add: 7 | -- 8 | -- dofile("LSUIElementCheck.lua") 9 | -- 10 | -- to the top of your init.lua also located in ~/.hammerspoon/ 11 | -- 12 | 13 | local host = require("hs.host") 14 | local fnutils = require("hs.fnutils") 15 | 16 | local osVersion = host.operatingSystemVersion() 17 | 18 | if osVersion.major == 10 and osVersion.minor < 9 then 19 | local f = io.open(hs.docstrings_json_file:gsub("Resources/docs.json$","Info.plist"), 'r') 20 | local c = f:read("*a") 21 | f:close() 22 | 23 | local NeedsFix = true 24 | 25 | local LSUIElementFound = false 26 | local LSUIElementLine = 0 27 | local CloseDictLine = 0 28 | local Contents = fnutils.split(c, "[\r\n]") 29 | 30 | for i,v in ipairs(Contents) do 31 | if v:match("^$") then 32 | CloseDictLine = i 33 | elseif v:match("\tLSUIElement$") then 34 | LSUIElementFound = true 35 | LSUIElementLine = i 36 | end 37 | end 38 | 39 | if LSUIElementFound then 40 | if Contents[LSUIElementLine + 1]:match("\t") then 41 | print("-- Update to LSUIElement already in place.") 42 | NeedsFix = false 43 | else 44 | Contents[LSUIElementLine + 1] = "\t" 45 | end 46 | else 47 | table.insert(Contents, CloseDictLine, "\t") 48 | table.insert(Contents, CloseDictLine, "\tLSUIElement") 49 | end 50 | 51 | if NeedsFix then 52 | f = io.open(hs.docstrings_json_file:gsub("Resources/docs.json$","Info.plist"), 'w') 53 | f:write(table.concat(Contents, "\n")) 54 | f:close() 55 | print("-- LSUIElement Updated. You will need to restart Hammerspoon.") 56 | -- 57 | -- Uncomment the following if you want Hammerspoon to restart automatically 58 | -- 59 | 60 | -- os.execute([[ (while ps -p ]]..hs.processInfo.processID..[[ > /dev/null ; do sleep 1 ; done ; open -a "]]..hs.processInfo.bundlePath..[[" ) & ]]) 61 | -- hs._exit(true, true) 62 | 63 | end 64 | else 65 | print("-- Update to LSUIElement not required for "..host.operatingSystemVersionString()) 66 | end -------------------------------------------------------------------------------- /_gists/altPress.lua: -------------------------------------------------------------------------------- 1 | local alert = require("hs.alert") 2 | local timer = require("hs.timer") 3 | local eventtap = require("hs.eventtap") 4 | 5 | local events = eventtap.event.types 6 | 7 | local module = {} 8 | 9 | -- You either override these here or after including this file from another, e.g. 10 | -- 11 | -- altHold = require("altHold") 12 | -- altHold.timeFrame = 2 13 | -- altHold.action = function() 14 | -- do something special 15 | -- end 16 | 17 | -- how long must the alt key be held? 18 | module.timeFrame = 2 19 | 20 | -- what to do when the alt key has been held that long 21 | module.action = function() 22 | alert("You held the Alt/Option key!") 23 | end 24 | 25 | 26 | -- Synopsis: 27 | 28 | -- what we're looking for is the alt key down event and no other 29 | -- key or flag change event before the specified time has passed 30 | 31 | -- verify that *only* the ctrl key flag is being pressed 32 | local onlyAlt = function(ev) 33 | local result = ev:getFlags().alt 34 | for k,v in pairs(ev:getFlags()) do 35 | if k ~= "alt" and v then 36 | result = false 37 | break 38 | end 39 | end 40 | return result 41 | end 42 | 43 | module.eventwatcher = eventtap.new({events.flagsChanged, events.keyDown}, function(ev) 44 | -- if we're called and a time is running, something changed -- unset the timer 45 | if module.countDownTimer then 46 | module.countDownTimer:stop() 47 | module.countDownTimer = nil 48 | end 49 | 50 | if ev:getType() == events.flagsChanged then 51 | if onlyAlt(ev) then 52 | module.countDownTimer = timer.doAfter(module.timeFrame, function() 53 | module.countDownTimer = nil 54 | if module.action then module.action() end 55 | end) 56 | end 57 | end 58 | 59 | return false ; 60 | end):start() 61 | -------------------------------------------------------------------------------- /_gists/doubleTap.lua: -------------------------------------------------------------------------------- 1 | local alert = require("hs.alert") 2 | local timer = require("hs.timer") 3 | local eventtap = require("hs.eventtap") 4 | 5 | local events = eventtap.event.types 6 | 7 | local module = {} 8 | 9 | -- You either override these here or after including this file from another, e.g. 10 | -- 11 | -- ctrlDoublePress = require("this-file") 12 | -- ctrlDoublePress.timeFrame = 2 13 | -- ctrlDoublePress.action = function() 14 | -- do something special 15 | -- end 16 | 17 | -- how quickly must the two single ctrl taps occur? 18 | module.timeFrame = 1 19 | 20 | -- what to do when the double tap of ctrl occurs 21 | module.action = function() 22 | alert("You double tapped ctrl!") 23 | end 24 | 25 | 26 | -- Synopsis: 27 | 28 | -- what we're looking for is 4 events within a set time period and no intervening other key events: 29 | -- flagsChanged with only ctrl = true 30 | -- flagsChanged with all = false 31 | -- flagsChanged with only ctrl = true 32 | -- flagsChanged with all = false 33 | 34 | 35 | local timeFirstControl, firstDown, secondDown = 0, false, false 36 | 37 | -- verify that no keyboard flags are being pressed 38 | local noFlags = function(ev) 39 | local result = true 40 | for k,v in pairs(ev:getFlags()) do 41 | if v then 42 | result = false 43 | break 44 | end 45 | end 46 | return result 47 | end 48 | 49 | -- verify that *only* the ctrl key flag is being pressed 50 | local onlyCtrl = function(ev) 51 | local result = ev:getFlags().ctrl 52 | for k,v in pairs(ev:getFlags()) do 53 | if k ~= "ctrl" and v then 54 | result = false 55 | break 56 | end 57 | end 58 | return result 59 | end 60 | 61 | -- the actual workhorse 62 | 63 | module.eventWatcher = eventtap.new({events.flagsChanged, events.keyDown}, function(ev) 64 | -- if it's been too long; previous state doesn't matter 65 | if (timer.secondsSinceEpoch() - timeFirstControl) > module.timeFrame then 66 | timeFirstControl, firstDown, secondDown = 0, false, false 67 | end 68 | 69 | if ev:getType() == events.flagsChanged then 70 | if noFlags(ev) and firstDown and secondDown then -- ctrl up and we've seen two, so do action 71 | if module.action then module.action() end 72 | timeFirstControl, firstDown, secondDown = 0, false, false 73 | elseif onlyCtrl(ev) and not firstDown then -- ctrl down and it's a first 74 | firstDown = true 75 | timeFirstControl = timer.secondsSinceEpoch() 76 | elseif onlyCtrl(ev) and firstDown then -- ctrl down and it's the second 77 | secondDown = true 78 | elseif not noFlags(ev) then -- otherwise reset and start over 79 | timeFirstControl, firstDown, secondDown = 0, false, false 80 | end 81 | else -- it was a key press, so not a lone ctrl char -- we don't care about it 82 | timeFirstControl, firstDown, secondDown = 0, false, false 83 | end 84 | return false 85 | end):start() 86 | 87 | return module -------------------------------------------------------------------------------- /_gists/moonscript_traceback.lua: -------------------------------------------------------------------------------- 1 | local module = { 2 | --[=[ 3 | _NAME = 'moonscript.traceback.lua', 4 | _VERSION = 'the 1st digit of Pi/0', 5 | _URL = 'https://github.com/asmagill/hammerspoon-config', 6 | _LICENSE = [[ See README.md ]] 7 | _DESCRIPTION = [[ 8 | 9 | debug.traceback supplement for moonscript files 10 | 11 | ]], 12 | --]=] 13 | } 14 | 15 | local _verbose = true -- default is to be verbose in operations 16 | 17 | -- private variables and methods ----------------------------------------- 18 | 19 | local moonscript = require("moonscript.base") 20 | local util = require("moonscript.util") 21 | local errors = require("moonscript.errors") 22 | 23 | local print_err = function(...) 24 | local msg = table.concat((function(...) 25 | local _accum_0 = { } 26 | local _len_0 = 1 27 | local _list_0 = { 28 | ... 29 | } 30 | for _index_0 = 1, #_list_0 do 31 | local v = _list_0[_index_0] 32 | _accum_0[_len_0] = tostring(v) 33 | _len_0 = _len_0 + 1 34 | end 35 | return _accum_0 36 | end)(...), "\t") 37 | -- return io.stderr:write(msg .. "\n") 38 | return msg 39 | end 40 | 41 | local moonscript_traceback = function(_err) 42 | local err = _err 43 | local trace = debug._preMoonscript_traceback("", 2) 44 | 45 | if err then 46 | local truncated = errors.truncate_traceback(util.trim(trace)) 47 | local rewritten = errors.rewrite_traceback(truncated, err) 48 | if rewritten then 49 | return print_err(rewritten) 50 | else 51 | return print_err(table.concat({ 52 | err, 53 | util.trim(trace) 54 | }, "\n")) 55 | end 56 | end 57 | end 58 | 59 | -- Public interface ------------------------------------------------------ 60 | 61 | module.add = function(verbose) 62 | verbose = verbose or _verbose 63 | if not debug._preMoonscript_traceback then 64 | debug._preMoonscript_traceback = debug.traceback 65 | debug.traceback = moonscript_traceback 66 | if verbose then print("++ Moonscript traceback inserted.") end 67 | elseif debug._preMoonscript_traceback == debug.traceback then 68 | if verbose then print("++ Moonscript traceback already in place. Doing nothing.") end 69 | else 70 | print("++ Backup debug.traceback detected, but debug.traceback isn't ours. Cowardly doing nothing.") 71 | end 72 | end 73 | 74 | module.remove = function(verbose) 75 | verbose = verbose or _verbose 76 | if debug._preMoonscript_traceback then 77 | if debug.traceback == moonscript_traceback then 78 | debug.traceback = debug._preMoonscript_traceback 79 | debug._preMoonscript_traceback = nil 80 | if verbose then print("++ Moonscript traceback removed.") end 81 | else 82 | print("++ Backup debug.traceback detected, but debug.traceback isn't ours. Cowardly doing nothing.") 83 | end 84 | else 85 | if verbose then print("++ Moonscript traceback not installed. Doing nothing.") end 86 | end 87 | end 88 | 89 | -- Return Module Object -------------------------------------------------- 90 | 91 | return module 92 | 93 | -------------------------------------------------------------------------------- /_init.lua: -------------------------------------------------------------------------------- 1 | print("-- "..os.date()) 2 | 3 | hs.logger = require("hs.logger") 4 | -- require("hs.crash").crashLogToNSLog = true 5 | hs.logger.historySize(200) 6 | 7 | inspect = require("hs.inspect") 8 | -- _xtras = require("hs._asm.extras") 9 | 10 | inspect1 = function(what) return inspect(what, {depth=1}) end 11 | inspect2 = function(what) return inspect(what, {depth=2}) end 12 | inspectnm = function(what) return inspect(what ,{process=function(item,path) if path[#path] == inspect.METATABLE then return nil else return item end end}) end 13 | inspectnm1 = function(what) return inspect(what ,{process=function(item,path) if path[#path] == inspect.METATABLE then return nil else return item end end, depth=1}) end 14 | 15 | isinf = function(x) return x == math.huge end 16 | isnan = function(x) return x ~= x end 17 | 18 | tobits = function(num, bits) 19 | bits = bits or (math.floor(math.log(num,2) / 8) + 1) * 8 20 | if bits == -(1/0) then bits = 8 end 21 | local value = "" 22 | for i = (bits - 1), 0, -1 do 23 | value = value..tostring((num >> i) & 0x1) 24 | end 25 | return value 26 | end 27 | -------------------------------------------------------------------------------- /_init.test.lua: -------------------------------------------------------------------------------- 1 | print("-- test "..os.date()) 2 | -------------------------------------------------------------------------------- /_localAssets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmagill/hammerspoon-config/432c65705203d7743d3298441bd4319137b466fd/_localAssets/.gitkeep -------------------------------------------------------------------------------- /_localAssets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hammerspoon Local Assets 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 |
Local Assets

10 | This is a repository of assets used by other aspects of my Hammerspoon setup which are requested by URL for various reasons. Because I often work where network connectivity is questionable, it seemed prudent to setup a small web server to serve them up, and what better than to use Hammerspoon itself. 11 |

-- The Management
15 | 16 | -------------------------------------------------------------------------------- /_localAssets/psychotic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmagill/hammerspoon-config/432c65705203d7743d3298441bd4319137b466fd/_localAssets/psychotic.png -------------------------------------------------------------------------------- /_localAssets/sample.cgi: -------------------------------------------------------------------------------- 1 | #! /usr/bin/perl -wT 2 | 3 | BEGIN { unshift (@INC, "/opt/amagill/perl/lib/perl5", "/opt/amagill/perl/lib/perl5/darwin-thread-multi-2level") ; } 4 | 5 | use strict ; 6 | use IPC::System::Simple qw(runx) ; 7 | 8 | $ENV{'PERL5LIB'} = "/opt/amagill/perl/lib/perl5:/opt/amagill/perl/lib/perl5/darwin-thread-multi-2level" ; 9 | $ENV{'PATH'} = "/opt/amagill/perl/bin:/usr/bin:/bin" ; 10 | 11 | my ($buffer, @pairs, $pair, $name, $value, $k) ; 12 | our (%FORM) ; 13 | 14 | # Read in text 15 | if ( !defined $ENV{'REQUEST_METHOD'} ) { 16 | $ENV{'REQUEST_METHOD'} = "GET" ; 17 | 18 | print "Type in the GET data for a web page (i.e. everything after the ? character.)\n" ; 19 | 20 | $ENV{'QUERY_STRING'} = <> ; 21 | } 22 | 23 | $ENV{'REQUEST_METHOD'} =~ tr/a-z/A-Z/ ; 24 | 25 | if ($ENV{'REQUEST_METHOD'} eq "POST") { 26 | read(STDIN, $buffer, $ENV{'CONTENT_LENGTH'}) ; 27 | } else { 28 | $buffer = $ENV{'QUERY_STRING'} ; 29 | } 30 | 31 | # Split information into name/value pairs 32 | 33 | @pairs = split(/&/, $buffer) ; 34 | foreach $pair (@pairs) { 35 | ($name, $value) = split(/=/, $pair) ; 36 | $value =~ tr/+/ / ; 37 | $value =~ s/%(..)/pack("C", hex($1))/eg ; 38 | $FORM{$name} = $value ; 39 | } 40 | 41 | print "Content-type:text/html\r\n\r\n" ; 42 | print "\n" ; 43 | print "\n" ; 44 | print "\n" ; 45 | print "Basic CGI-DUMP\n" ; 46 | print "Basic CGI-DUMP\n
\n" ; 47 | 48 | print "\n" ; 49 | print "\n" ; 50 | print "\n" ; 51 | foreach $k (sort (keys %ENV)) { print "\n" ; } 52 | # print "
Environment
KeyValue
$k$ENV{$k}
\n" ; 53 | 54 | # print "\n" ; 55 | print "\n" ; 56 | print "\n" ; 57 | foreach $k (sort (keys %FORM)) { print "\n" ; } 58 | print "
Form
KeyValue
$k$FORM{$k}
\n" ; 59 | 60 | print "\n" ; 61 | print "
\n
" ; eval { runx("date") ; }; 62 | print "

ERROR!


$@\n" if ($@) ; 63 | print "
\n" ; 64 | print "\n" ; 65 | 66 | -------------------------------------------------------------------------------- /_localAssets/sample.lp: -------------------------------------------------------------------------------- 1 | 2 | Lua Template Test 3 | 4 | 16 | 17 | <%= "<","!--" %>
Requires hs._asm.minweb:luaTemplateExtension("lp") to be enabled for your web server
<%= "--",">" %> 18 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /_scratch/CMIicon.lua: -------------------------------------------------------------------------------- 1 | 2 | local openI = [[ 3 | 1 # # # # # # # # 2 4 | # . . . . . . . . # 5 | # . 6 . . . . 9 . # 6 | # . . # . . # . . # 7 | # . . . # # . . . # 8 | # . . . # # . . . # 9 | # . . # . . # . . # 10 | # . A . . . . 7 . # 11 | # . . . . . . . . # 12 | # e # # # # # # f # 13 | # . . . . . . . . # 14 | # . . . . . . a . # 15 | # . . . . # # . . # 16 | # . . # # . . . . # 17 | # . b . . . . . . # 18 | # . . # # . . . . # 19 | # . . . . # # . . # 20 | # . . . . . . c . # 21 | # . . . . . . . . # 22 | 4 # # # # # # # # 3 23 | ]] 24 | 25 | local openC = { 26 | { fillColor = { alpha = 0 } }, 27 | { strokeColor = { red = .5 } }, 28 | { strokeColor = { red = .5 } }, 29 | { 30 | strokeColor = { green = .75 }, 31 | fillColor = { green = .5 }, -- alpha = 0 }, 32 | shouldClose = false 33 | }, { 34 | fillColor = {}, 35 | strokeColor = {}, 36 | antialias = true, 37 | shouldClose = true 38 | } 39 | } 40 | 41 | local closeI = [[ 42 | 1 # # # # # # # # 2 43 | # . . . . . . . . # 44 | # . 6 . . . . 9 . # 45 | # . . # . . # . . # 46 | # . . . # # . . . # 47 | # . . . # # . . . # 48 | # . . # . . # . . # 49 | # . A . . . . 7 . # 50 | # . . . . . . . . # 51 | # e # # # # # # f # 52 | # . . . . . . . . # 53 | # . a . . . . . . # 54 | # . . # # . . . . # 55 | # . . . . # # . . # 56 | # . . . . . . b . # 57 | # . . . . # # . . # 58 | # . . # # . . . . # 59 | # . c . . . . . . # 60 | # . . . . . . . . # 61 | 4 # # # # # # # # 3 62 | ]] 63 | 64 | local closeC = { 65 | { fillColor = { alpha = 0 } }, 66 | { strokeColor = { red = .5 } }, 67 | { strokeColor = { red = .5 } }, 68 | { 69 | strokeColor = { red = .75, green = .75 }, 70 | fillColor = { red = .5, green = .5 }, -- alpha = 0 }, 71 | shouldClose = false 72 | }, { 73 | fillColor = {}, 74 | strokeColor = {}, 75 | antialias = true, 76 | shouldClose = true 77 | } 78 | } 79 | 80 | local a = {} 81 | 82 | a[1] = hs.drawing.image({x=100,y=100,h=200,w=100}, 83 | hs.image.imageFromASCII(openI, openC)):show() 84 | 85 | a[2] = hs.drawing.image({x=300,y=100,h=200,w=100}, 86 | hs.image.imageFromASCII(closeI, closeC)):show() 87 | 88 | -- cleanup so I don't have to reload everything 89 | 90 | local esc = function() 91 | -- do whatever to cleanup 92 | hs.fnutils.map(a, function(_) _:delete() end) 93 | end 94 | 95 | xyzzy = hs.hotkey.bind({},"escape", 96 | function() esc() end, 97 | function() xyzzy:disable() end 98 | ) 99 | -------------------------------------------------------------------------------- /_scratch/alerts.lua: -------------------------------------------------------------------------------- 1 | local module = {} 2 | 3 | local drawing = require("hs.drawing") 4 | local timer = require("hs.timer") 5 | local screen = require("hs.screen") 6 | local uuid = require"hs.host".uuid 7 | 8 | module._visibleAlerts = {} 9 | 10 | local purgeAlert = function(UUID, duration) 11 | duration = math.max(duration, 0.0) or 0.15 12 | local indexToRemove 13 | for i,v in ipairs(module._visibleAlerts) do 14 | if v.UUID == UUID then 15 | if v.timer then v.timer:stop() end 16 | for i2,v2 in ipairs(v.drawings) do 17 | v2:hide(duration) 18 | if duration > 0.0 then 19 | timer.doAfter(duration, function() v2:delete() end) 20 | end 21 | v.drawings[i2] = nil 22 | end 23 | indexToRemove = i 24 | break 25 | end 26 | end 27 | if indexToRemove then 28 | table.remove(module._visibleAlerts, indexToRemove) 29 | end 30 | end 31 | 32 | local showAlert = function(message, duration) 33 | local screenFrame = screen.mainScreen():fullFrame() 34 | 35 | local absoluteTop = screenFrame.h * (1 - 1 / 1.55) + 55 -- mimic module behavior for inverted rect 36 | if #module._visibleAlerts > 0 then 37 | absoluteTop = module._visibleAlerts[#module._visibleAlerts].frame.y + module._visibleAlerts[#module._visibleAlerts].frame.h + 3 38 | end 39 | 40 | if absoluteTop > screenFrame.h then 41 | absoluteTop = screen.mainScreen():frame().y 42 | end 43 | 44 | local alertEntry = { 45 | drawings = {}, 46 | } 47 | local UUID = uuid() 48 | alertEntry.UUID = UUID 49 | 50 | local textFrame = drawing.getTextDrawingSize(message) 51 | textFrame.w = textFrame.w + 4 -- known fudge factor, see hs.drawing.getTextDrawingSize docs 52 | local drawingFrame = { 53 | x = screenFrame.x + (screenFrame.w - (textFrame.w + 26)) / 2, 54 | y = absoluteTop, 55 | h = textFrame.h + 24, 56 | w = textFrame.w + 26, 57 | } 58 | textFrame.x = drawingFrame.x + 13 59 | textFrame.y = drawingFrame.y + 12 60 | 61 | table.insert(alertEntry.drawings, drawing.rectangle(drawingFrame) 62 | :setStroke(true) 63 | :setStrokeWidth(2) 64 | :setStrokeColor{white = 1, alpha = 1} 65 | :setFill(true) 66 | -- :setFillColor{white = 0, alpha = 0.75} -- use something different so its obvious if our replacement isn't being used by something 67 | :setFillColor{blue = 0.25, green = 0.25, alpha = 0.75} 68 | :setRoundedRectRadii(27, 27) 69 | :show(0.15) 70 | ) 71 | table.insert(alertEntry.drawings, drawing.text(textFrame, message) 72 | :orderAbove(alertEntry.drawings[1]) 73 | :show(0.15) 74 | ) 75 | alertEntry.frame = drawingFrame 76 | 77 | table.insert(module._visibleAlerts, alertEntry) 78 | if type(duration) == "number" then 79 | alertEntry.timer = timer.doAfter(duration, function() 80 | purgeAlert(UUID, 0.15) 81 | end) 82 | end 83 | return UUID 84 | end 85 | 86 | module.show = function(message, duration) 87 | message = tostring(message) 88 | duration = duration or 2.0 89 | return showAlert(message, duration) 90 | end 91 | 92 | module.closeAll = function(duration) 93 | duration = duration and math.max(duration, 0.0) or 0.15 94 | while (#module._visibleAlerts > 0) do 95 | purgeAlert(module._visibleAlerts[#module._visibleAlerts].UUID, duration) 96 | end 97 | end 98 | 99 | module.closeSpecific = function(UUID, duration) 100 | duration = duration and math.max(duration, 0.0) or 0.15 101 | purgeAlert(UUID, duration) 102 | end 103 | 104 | return setmetatable(module, { __call = function(_, ...) return module.show(...) end }) 105 | -------------------------------------------------------------------------------- /_scratch/altnames.lua: -------------------------------------------------------------------------------- 1 | -- Keeps an up-to-date list of alternate names for applications 2 | 3 | local spotlight = require("hs.spotlight") 4 | local module = {} 5 | 6 | local nameMap = {} 7 | 8 | local modifyNameMap = function(info, add) 9 | for _, item in ipairs(info) do 10 | local applicationName = item.kMDItemFSName 11 | for __, alt in ipairs(item.kMDItemAlternateNames or {}) do 12 | nameMap[alt:match("^(.*)%.app$") or alt] = add and applicationName or nil 13 | end 14 | end 15 | end 16 | 17 | local updateNameMap = function(obj, msg, info) 18 | if info then 19 | -- all three can occur in either message, so check them all! 20 | if info.kMDQueryUpdateAddedItems then modifyNameMap(info.kMDQueryUpdateAddedItems, true) end 21 | if info.kMDQueryUpdateChangedItems then modifyNameMap(info.kMDQueryUpdateChangedItems, true) end 22 | if info.kMDQueryUpdateRemovedItems then modifyNameMap(info.kMDQueryUpdateRemovedItems, false) end 23 | else 24 | -- shouldn't happen for didUpdate or inProgress 25 | print("~~~ userInfo from SpotLight was empty for " .. msg) 26 | end 27 | end 28 | 29 | module.watcher = spotlight.new():queryString([[ kMDItemContentType = "com.apple.application-bundle" ]]) 30 | :callbackMessages("didUpdate", "inProgress") 31 | :setCallback(updateNameMap) 32 | :start() 33 | module.nameMap = nameMap 34 | 35 | module.realNameFor = function(value, exact) 36 | if type(value) ~= "string" then 37 | error('hint must be a string', 2) 38 | end 39 | if not exact then 40 | local results = {} 41 | for k, v in pairs(nameMap) do 42 | if k:lower():find(value:lower()) then 43 | -- I can foresee someday wanting to know how often a match was found, so make it a 44 | -- number rather than a boolean so I can cut & paste this 45 | results[v] = (results[v] or 0) + 1 46 | end 47 | end 48 | local returnedResults = {} 49 | for k,v in pairs(results) do 50 | table.insert(returnedResults, k:match("^(.*)%.app$") or k) 51 | end 52 | return table.unpack(returnedResults) 53 | else 54 | local realName = nameMap[value] 55 | -- hs.application functions/methods do not like the .app at the end of application 56 | -- bundles, so remove it. 57 | return realName and realName:match("^(.*)%.app$") or realName 58 | end 59 | end 60 | 61 | return module 62 | -------------------------------------------------------------------------------- /_scratch/avplayerExample.lua: -------------------------------------------------------------------------------- 1 | local guitk = require("hs._asm.guitk") 2 | 3 | -- Note: this gets real annoying real fast if you have audio turned up... it's just a proof of concept example 4 | 5 | local module = {} 6 | 7 | local gui = guitk.new{x = 100, y = 100, h = 300, w = 300 } 8 | local player = guitk.element.avplayer.new() 9 | 10 | -- in this example we attach the element directly to the window, eschewing a content manager. Since the AV view is 11 | -- all we want in this window, it can be its "own" content manager. If you want to be able to add additional buttons 12 | -- or images or what-not in the window, use `hs._asm.guitk.manager` as the content manager and add the player 13 | -- to it instead (see the button example). 14 | 15 | gui:contentManager(player) 16 | player:controlsStyle("inline") 17 | :load("http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8") 18 | :play() 19 | :show() -- actually a guitk method, but unrecognized methods pass through up the responder chain 20 | 21 | -- returning only the player; usually we can ignore the window once it's created because 22 | -- (a) usually we're primarily interested in the window's content and not the window itself 23 | -- (b) the window will not auto-collect; it requires an explicit delete to completely remove it 24 | -- (c) methods not recognized by the element/manager will pass up the responder chain so methods like 25 | -- frame, size, show, hide, delete, etc. will reach the window object anyways 26 | module.player = player 27 | return module 28 | -------------------------------------------------------------------------------- /_scratch/axh.lua: -------------------------------------------------------------------------------- 1 | local module = {} 2 | local ax = require("hs._asm.axuielement") 3 | local fnutils = require("hs.fnutils") 4 | local inspect = require("hs.inspect") 5 | 6 | local hierarchy 7 | hierarchy = function(obj, indent, seen) 8 | indent = indent or 0 9 | seen = seen or {} 10 | if getmetatable(obj) == hs.getObjectMetatable("hs._asm.axuielement") then 11 | if fnutils.find(seen, function(_) return _ == obj end) then return end -- probably not necessary, but be safe 12 | table.insert(seen, obj) 13 | 14 | print(string.format("%s%s", string.rep(" ", indent), obj:role())) 15 | for _, attrName in ipairs(obj:attributeNames()) do 16 | if attrName == ax.attributes.general.parent then 17 | print(string.format("%s%s->%s: ", string.rep(" ", indent), string.rep(" ", #obj:role()), attrName)) 18 | elseif attrName == ax.attributes.general.topLevelUIElement then 19 | print(string.format("%s%s->%s: ", string.rep(" ", indent), string.rep(" ", #obj:role()), attrName)) 20 | else 21 | local attrValue = obj:attributeValue(attrName) 22 | if getmetatable(attrValue) == hs.getObjectMetatable("hs._asm.axuielement") then 23 | if fnutils.find(seen, function(_) return _ == obj end) then 24 | print(string.format("%s%s->%s: ", string.rep(" ", indent), string.rep(" ", #obj:role()), attrName)) 25 | else 26 | print(string.format("%s%s->%s:", string.rep(" ", indent), string.rep(" ", #obj:role()), attrName)) 27 | hierarchy(attrValue, indent + #obj:role(), seen) 28 | end 29 | elseif type(attrValue) == "table" then 30 | if #attrValue == 0 then 31 | print(string.format("%s%s->%s = %s", string.rep(" ", indent), string.rep(" ", #obj:role()), attrName, inspect(attrValue):gsub("[\r\n]"," "):gsub("%s+", " "))) 32 | else 33 | print(string.format("%s%s->%s {", string.rep(" ", indent), string.rep(" ", #obj:role()), attrName)) 34 | hierarchy(attrValue, indent + #obj:role() + #attrName + 6, seen) 35 | print(string.format("%s%s}", string.rep(" ", indent), string.rep(" ", #obj:role()))) ; 36 | end 37 | else 38 | print(string.format("%s%s->%s = %s", string.rep(" ", indent), string.rep(" ", #obj:role()), attrName, inspect(attrValue))) 39 | end 40 | end 41 | end 42 | elseif type(obj) == "table" then 43 | for i, v in ipairs(obj) do hierarchy(v, indent, seen) end 44 | end 45 | end 46 | 47 | module.hierarchy = hierarchy 48 | return module 49 | -------------------------------------------------------------------------------- /_scratch/boxes.lua: -------------------------------------------------------------------------------- 1 | function leftClick(point) 2 | hs.eventtap.event.newMouseEvent(hs.eventtap.event.types["leftMouseDown"], point):post() 3 | hs.eventtap.event.newMouseEvent(hs.eventtap.event.types["leftMouseUp"], point):post() 4 | end 5 | 6 | function drawBoxes() 7 | print("Drawing red and blue boxes.") 8 | redFrame = hs.geometry.rect({x=100, y=100, h=100, w=200}) 9 | redBox = hs.drawing.rectangle(redFrame) 10 | :setFillColor({red=1.0}) 11 | :setFill(true) 12 | :setClickCallback(function() 13 | print("Red clicked!") 14 | end) 15 | :show() 16 | blueFrame = hs.geometry.rect({x=350, y=100, h=100, w=200}) 17 | blueBox = hs.drawing.rectangle(blueFrame) 18 | :setFillColor({red=0, blue=1.0}) 19 | :setFill(true) 20 | :setClickCallback(function() 21 | print("Blue clicked!") 22 | end) 23 | :show() 24 | end 25 | 26 | function deleteBoxes() 27 | print("Deleting boxes.") 28 | redBox:delete() 29 | blueBox:delete() 30 | end 31 | 32 | function clickBox(box) 33 | local center = hs.geometry.rect(box:frame()).center 34 | hs.eventtap.leftClick(center) 35 | -- leftClick(center) 36 | end 37 | 38 | function swapLocations(box1, box2) 39 | local frame = box1:frame() 40 | box1:setFrame(box2:frame()) 41 | box2:setFrame(frame) 42 | end 43 | 44 | function redBlueClick() 45 | print("redBlueClick About to click red then blue box.") 46 | clickBox(redBox) 47 | clickBox(blueBox) 48 | print("redBlueClick Done.") 49 | end 50 | 51 | function redBlueClickSleep() 52 | print("redBlueClickSleep About to click red then blue box.") 53 | clickBox(redBox) 54 | clickBox(blueBox) 55 | hs.timer.usleep(1) 56 | print("redBlueClickSleep Done.") 57 | end 58 | 59 | function redBlueClickTimer() 60 | print("redBlueClickTimer About to click red then blue box.") 61 | clickBox(redBox) 62 | clickBox(blueBox) 63 | hs.timer.doAfter(0.000001, function() 64 | print("redBlueClickTimer Done.") 65 | end) 66 | end 67 | 68 | function redBlueClickSwapTimer() 69 | print("redBlueClickSwapTimer About to click red then blue box then swap them before the timer.") 70 | clickBox(redBox) 71 | clickBox(blueBox) 72 | swapLocations(redBox, blueBox) 73 | hs.timer.doAfter(0.000001, function() 74 | print("redBlueClickSwapTimer Done.") 75 | end) 76 | end 77 | 78 | drawBoxes() 79 | 80 | hs.timer.doAfter(0.000001, function() 81 | redBlueClick() 82 | 83 | redBlueClickSleep() 84 | 85 | redBlueClickTimer() 86 | 87 | redBlueClickSwapTimer() 88 | 89 | hs.timer.doAfter(1, deleteBoxes) 90 | end) 91 | -------------------------------------------------------------------------------- /_scratch/buttonExamples.lua: -------------------------------------------------------------------------------- 1 | local guitk = require("hs._asm.guitk") 2 | local image = require("hs.image") 3 | local inspect = require("hs.inspect") 4 | 5 | local finspect = function(...) return (inspect({...}):gsub("%s+", " ")) end 6 | 7 | local module = {} 8 | 9 | local display = guitk.new{ x = 100, y = 100, h = 100, w = 100 }:show():passthroughCallback(function(...) print(finspect(...)) end) 10 | local manager = guitk.manager.new() 11 | display:contentManager(manager) 12 | 13 | -- TODO: 14 | -- need to add code to display bezel types as well and code to display those in their best form 15 | -- e.g. the disclosure bezels only make sense with the onOff or pushOnPushOff types 16 | local types = { 17 | "momentaryLight", 18 | "toggle", 19 | "switch", 20 | "radio", 21 | "momentaryChange", 22 | "multiLevelAccelerator", 23 | "onOff", 24 | "pushOnPushOff", 25 | "accelerator", 26 | "momentaryPushIn" 27 | } 28 | 29 | for i, v in ipairs(types) do 30 | manager[#manager + 1] = guitk.element.button.buttonType(v):title(v):alternateTitle("not " .. v):tooltip("button type " .. v) 31 | end 32 | 33 | local lastFrame = manager[#manager].frameDetails._effective 34 | 35 | -- 10.12 constructors; approximations are used if 10.11 or 10.10 detected; included here so I can determine what to mimic 36 | manager[#manager + 1] = { 37 | _element = guitk.element.button.buttonWithImage(image.imageFromName(image.systemImageNames.ApplicationIcon)), 38 | frameDetails = { y = lastFrame.y + 2 * lastFrame.h } 39 | } 40 | manager[#manager + 1] = guitk.element.button.buttonWithTitle("buttonWithTitle") 41 | manager[#manager + 1] = guitk.element.button.buttonWithTitleAndImage("buttonWithTitleAndImage", image.imageFromName(image.systemImageNames.ApplicationIcon)) 42 | manager[#manager + 1] = guitk.element.button.checkbox("checkbox") 43 | manager[#manager + 1] = guitk.element.button.radioButton("radioButton") 44 | 45 | -- radio buttons within the same manager only allow one at a time to be selected (they automatically unselect the others) 46 | -- to have multiple sets of radio buttons they need to be in different managers (views) 47 | local radio = guitk.manager.new():tooltip("grouped radiobuttons") 48 | radio:insert(guitk.element.button.radioButton("A"):tooltip("A")) 49 | radio:insert(guitk.element.button.radioButton("B"):tooltip("not A")) 50 | radio:insert(guitk.element.button.radioButton("C"):tooltip("also not A")) 51 | -- then add the new manager to the main one just like any other element 52 | manager:insert(radio, { x = 200, y = 200 }) 53 | manager:sizeToFit(20, 10) 54 | 55 | -- returning only the manager; usually we can ignore the window once it's created because 56 | -- (a) usually we're primarily interested in the window's content and not the window itself 57 | -- (b) the window will not auto-collect; it requires an explicit delete to completely remove it 58 | -- (c) methods not recognized by the element/manager will pass up the responder chain so methods like 59 | -- frame, size, show, hide, delete, etc. will reach the window object anyways 60 | module.manager = manager 61 | 62 | return module 63 | -------------------------------------------------------------------------------- /_scratch/chooseyMcChooseyier.lua: -------------------------------------------------------------------------------- 1 | local count = 20 2 | 3 | local buildNewTable = function() 4 | local queries = {} 5 | for i = 1, count, 1 do 6 | queries[i] = { 7 | text = hs.host.uuid(), 8 | subText = hs.host.globallyUniqueString(), 9 | } 10 | end 11 | return queries 12 | end 13 | 14 | chooser = hs.chooser.new(function(result) 15 | print((hs.inspect(result):gsub("%s+", " "))) 16 | end):choices(buildNewTable()) 17 | 18 | changer = hs.timer.doEvery(5, function() 19 | chooser:choices(buildNewTable()) 20 | end) 21 | 22 | key = hs.hotkey.bind({"cmd", "ctrl", "alt"}, "8", function() 23 | chooser:show() 24 | end) 25 | -------------------------------------------------------------------------------- /_scratch/colorTests.lua: -------------------------------------------------------------------------------- 1 | -- testing functions for use in arduino for hsb2rgb and rgb2hsb conversion against mac's built in conversion 2 | -- adapted from https://github.com/ratkins/RGBConverter/blob/master/RGBConverter.cpp 3 | -- converted to ranges used by Philips Hue bridge 4 | -- converted to lua for these tests 5 | 6 | local rgb2hsb = function(red, green, blue) 7 | local rd = red / 255 8 | local gd = green / 255 9 | local bd = blue / 255 10 | local mx = math.max(rd, gd, bd) 11 | local mn = math.min(rd, gd, bd) 12 | local h, s 13 | local v = mx 14 | 15 | local d = mx - mn 16 | s = (mx == 0) and 0 or (d / mx) 17 | 18 | if (mx == mn) then 19 | h = 0 -- achromatic 20 | else 21 | if (mx == rd) then 22 | h = (gd - bd) / d + ((gd < bd) and 6 or 0) 23 | elseif (mx == gd) then 24 | h = (bd - rd) / d + 2 25 | elseif (mx == bd) then 26 | h = (rd - gd) / d + 4 27 | end 28 | h = h / 6 29 | end 30 | 31 | return math.floor(h * 65535), math.floor(s * 255), math.floor(v * 255) 32 | end 33 | 34 | local hsb2rgb = function(hue, sat, bri) 35 | hue, sat, bri = hue / 65535, sat / 255, bri / 255 36 | local r, g, b 37 | 38 | local i = math.floor(hue * 6) 39 | local f = hue * 6 - i 40 | local p = bri * (1 - sat) 41 | local q = bri * (1 - f * sat) 42 | local t = bri * (1 - (1 - f) * sat) 43 | 44 | if (i % 6) == 0 then 45 | r, g, b = bri, t, p 46 | elseif (i % 6) == 1 then 47 | r, g, b = q, bri, p 48 | elseif (i % 6) == 2 then 49 | r, g, b = p, bri, t 50 | elseif (i % 6) == 3 then 51 | r, g, b = p, q, bri 52 | elseif (i % 6) == 4 then 53 | r, g, b = t, p, bri 54 | elseif (i % 6) == 5 then 55 | r, g, b = bri, p, q 56 | end 57 | 58 | return math.floor(r * 255), math.floor(g * 255), math.floor(b * 255) 59 | end 60 | 61 | -- now mac conversion functions 62 | 63 | local mac_rgb2hsb = function(red, green, blue) 64 | local color = require("hs.drawing").color 65 | local temp = color.asHSB{ red = red / 255, green = green / 255, blue = blue / 255 } 66 | return math.floor(temp.hue * 65535), math.floor(temp.saturation * 255), math.floor(temp.brightness * 255) 67 | end 68 | 69 | local mac_hsb2rgb = function(hue, sat, bri) 70 | local color = require("hs.drawing").color 71 | local temp = color.asRGB{ hue = hue / 65535, saturation = sat / 255, brightness = bri / 255 } 72 | return math.floor(temp.red * 255), math.floor(temp.green * 255), math.floor(temp.blue * 255) 73 | end 74 | 75 | 76 | 77 | require("hs.console").clearConsole() 78 | 79 | -- we'll accept conversions that are within a margin of error 80 | local closeEnough = function(a, b) return math.abs(a - b) <= 1 end 81 | 82 | local count, good = 0, 0 83 | for r = 0, 1, .01 do for g = 0, 1, .01 do for b = 0, 1, .01 do -- for final test, do 1,000,000 comparisons 84 | count = count + 1 85 | local r1, g1, b1 = math.floor(r * 255), math.floor(g * 255), math.floor(b * 255) 86 | local h2, s2, b2 = rgb2hsb(r1, g1, b1) 87 | local h3, s3, b3 = mac_rgb2hsb(r1, g1, b1) 88 | -- hue of 65535 == hue of 0, so catch both 89 | local st = (closeEnough(h2,h3) or ((h2 == 0) and (h3 == 65535)) or ((h2 == 65535) and (h3 == 0))) and (s2 == s3) and (b2 == b3) 90 | if st then 91 | good = good + 1 92 | else 93 | hs.printf("%3d, %3d, %3d rgb2hsb == %5d, %3d, %3d mac_rgb2hsb == %5d, %3d, %3d %s", r1, g1, b1, h2, s2, b2, h3, s3, b3, st) 94 | end 95 | end end end 96 | 97 | hs.printf("rgb2hsb total %d, passed %d\n", count, good) 98 | 99 | local count, good = 0, 0 100 | for h = 0, 1, .01 do for s = 0, 1, .01 do for b = 0, 1, .01 do 101 | count = count + 1 102 | local h1, s1, b1 = math.floor(h * 65535), math.floor(s * 255), math.floor(b * 255) 103 | local r2, g2, b2 = hsb2rgb(h1, s1, b1) 104 | local r3, g3, b3 = mac_hsb2rgb(h1, s1, b1) 105 | local st = (r2 == r3) and (g2 == g3) and (b2 == b3) 106 | if st then 107 | good = good + 1 108 | else 109 | hs.printf("%5d, %3d, %3d hsb2rgb == %3d, %3d, %3d mac_hsb2rgb == %3d, %3d, %3d %s", h1, s1, b1, r2, g2, b2, r3, g3, b3, st) 110 | end 111 | end end end 112 | 113 | hs.printf("hsb2rgb total %d, passed %d\n", count, good) 114 | -------------------------------------------------------------------------------- /_scratch/dash.lua: -------------------------------------------------------------------------------- 1 | local _rootPath = "/opt/amagill/src/hammerspoon/hammerspoon/build/Hammerspoon.docset" 2 | local _baseURL = "file://".._rootPath.."/Contents/Resources/Documents" 3 | 4 | local w = require("hs.webview") 5 | 6 | local getSearchData = function(_root) 7 | package.loadlib("/usr/local/opt/sqlite/lib/libsqlite3.dylib","*") 8 | local l = require("lsqlite3") 9 | 10 | local _path = _root.."/Contents/Resources/docSet.dsidx" 11 | 12 | db = l.open(_path) 13 | 14 | local searchData = {} 15 | for row in db:nrows("SELECT * FROM searchindex") do 16 | table.insert(searchData, { 17 | id = row.id, 18 | name = row.name, 19 | kind = row.type, 20 | path = row.path, 21 | }) 22 | end 23 | 24 | db:close() 25 | 26 | return searchData 27 | end 28 | 29 | local dataSet = getSearchData(_rootPath) 30 | 31 | local webView 32 | 33 | local doSearch = function(message) 34 | -- print(inspect(message)) 35 | message = tostring(message.body) 36 | local results = {} 37 | for i, v in ipairs(dataSet) do 38 | if v.name:match(message) then 39 | table.insert(results, v) 40 | end 41 | end 42 | 43 | local htmlResults = [[ 44 | 45 | Dash Search Results 46 | 47 |

Search for ']]..message..[[':

48 |
49 | ]] 50 | 51 | if (#results == 0) then 52 | htmlResults = htmlResults..[[ 53 | No results found 54 | ]] 55 | else 56 | htmlResults = htmlResults.."" 57 | for i,v in ipairs(results) do 58 | htmlResults = htmlResults..[[ 59 | 60 | ]] 61 | end 62 | htmlResults = htmlResults.."
]]..v.kind..[[]]..v.name..[[
" 63 | end 64 | 65 | htmlResults = htmlResults..[[ 66 |
67 |
Search performed at: ]]..os.date()..[[
68 | 69 | 70 | ]] 71 | 72 | webView:html(htmlResults, _baseURL) 73 | end 74 | 75 | local ucc = w.usercontent.new("dashamajig"):injectScript({ source = [[ 76 | function KeyPressHappened(e) 77 | { 78 | if (!e) e=window.event; 79 | var code; 80 | if ((e.charCode) && (e.keyCode==0)) 81 | code = e.charCode ; 82 | else 83 | code = e.keyCode; 84 | // console.log(code) ; 85 | if ((code == 102) && e.metaKey) { 86 | var textMesg = window.prompt("Enter a search term:","") ; 87 | if (textMesg != null) { 88 | try { 89 | webkit.messageHandlers.dashamajig.postMessage(textMesg); 90 | } catch(err) { 91 | console.log('The controller does not exist yet'); 92 | } 93 | } 94 | return false ; 95 | } else { 96 | return true ; 97 | } 98 | } 99 | 100 | document.onkeypress = KeyPressHappened; 101 | ]], mainFrame = true, injectionTime = "documentStart"}):setCallback(doSearch) 102 | 103 | webView = w.new({ x = 50, y = 50,h = 500, w = 900 }, { developerExtrasEnabled = true }, ucc) 104 | :windowStyle(1+2+4+8) 105 | :allowTextEntry(true) 106 | :url(_baseURL.."/index.html") 107 | :allowGestures(true) 108 | :show() 109 | 110 | -- For debugging purposes... may remove 111 | module.webView = webView 112 | module.doSearch = doSearch 113 | module.ucc = ucc 114 | module.dataSet = dataSet 115 | module._rootPath = _rootPath 116 | return module 117 | -------------------------------------------------------------------------------- /_scratch/dterm.lua: -------------------------------------------------------------------------------- 1 | local w = require("hs.webview") 2 | 3 | local htmlBegin = [[ 4 | 5 | 6 |
7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 |
13 | Hit enter to submit or click here. 14 |
17 |
18 | 28 | ]] 29 | 30 | local htmlEnd = [[ 31 | 32 | 33 | ]] 34 | 35 | local ucc = w.usercontent.new("passItAlong"):injectScript({ source = [[ 36 | function KeyPressHappened(e) 37 | { 38 | if (!e) e=window.event; 39 | var code; 40 | if ((e.charCode) && (e.keyCode==0)) { 41 | code = e.charCode ; 42 | } else { 43 | code = e.keyCode; 44 | } 45 | // console.log(code) ; 46 | if (code == 13) { 47 | submitCmd() ; 48 | return false ; // we handled it 49 | } else { 50 | return true ; // we didn't handle it 51 | } 52 | } 53 | document.onkeypress = KeyPressHappened; 54 | ]], mainFrame = true, injectionTime = "documentStart"}):setCallback(function(input) 55 | -- print(inspect(input)) 56 | local output, status, tp, rc = hs.execute(input.body) 57 | myView:html(htmlBegin..[[ 58 |
59 | 60 | 61 | 62 | 63 | 64 | 65 |
Status:]]..tostring(status)..[[Type:]]..tostring(tp)..[[RC:]]..tostring(rc)..[[
66 |
67 |
]]..output..[[
68 |
69 |
Executed at: ]]..os.date()..[[
70 | ]]..htmlEnd) 71 | end) 72 | 73 | myView = w.new({x = 50, y = 50, w = 500, h = 500}, { developerExtrasEnabled = true }, ucc) 74 | :windowStyle(1+2+4+8) 75 | :allowTextEntry(true) 76 | :html(htmlBegin..htmlEnd) 77 | :allowGestures(true) 78 | :show() 79 | 80 | myView:asHSDrawing():setAlpha(.75) 81 | -------------------------------------------------------------------------------- /_scratch/dumpStdout.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- Sample use of capturing Hammerspoon's stdout 3 | -- 4 | -- (1) Save this code as "dumpStdout.lua" in ~/.hammerspoon/ 5 | -- (2) In the Hammerspoon console, type the following: dump = require("dumpStdout") 6 | -- (3) In a terminal window, tyoe "tail -f ~/.hammerspoon/dump.txt" 7 | -- 8 | -- Now, anything which is sent to Hammerspoon's stdout will be replicated with a timestamp 9 | -- in the text file. Currently this means anything which is printed to the Hammerspoon 10 | -- console with the `print` command... this includes log messages handled with `hs.logger` 11 | -- and at least some error messages, but I don't think all... a deeper investigation of 12 | -- the Hammerspoon source is required to determine why the difference when I get the time. 13 | -- 14 | -- You may need to wait a few seconds after printing something in the console (you can 15 | -- speed this up a little with `io.output():flush()`) -- it's not quite immediate. Not 16 | -- sure why yet. 17 | -- 18 | -- Note that some third party code doesn't seem to generate output via the print command 19 | -- (the LuaRocks code itself is a good example). Instead, they use something along the 20 | -- lines of `io.output():write(...)` or something similar... this watcher will catch that 21 | -- while the Hammerspoon console won't. 22 | -- 23 | -- Note that because Hammerspoon *does* invoke the builtin `print` command as part of its 24 | -- routines to replicate output to the console, I cannot stress enough that you should 25 | -- **NEVER** use the `print` command in your callback... this will cause a death spiral 26 | -- and you'll have to type `killall Hammerspoon` into a terminal window. 27 | 28 | local module = {} 29 | local consolepipe = require("hs._asm.consolepipe") 30 | local timer = require("hs.timer") 31 | 32 | local err 33 | 34 | local timestamp = function(date) 35 | date = date or timer.secondsSinceEpoch() 36 | return os.date("%F %T" .. string.format("%-5s", ((tostring(date):match("(%.%d+)$")) or "")), math.floor(date)) 37 | end 38 | 39 | module.file, err = io.open("dump.txt", "w+") 40 | if not module.file or err then error(err) end 41 | 42 | module.replicator = consolepipe.new("stdout"):setCallback(function(stuff) 43 | if io.type(module.file) == "file" then 44 | local file, err = module.file:write(timestamp() .. ": " .. stuff) 45 | if not file or err then 46 | module.file:close() 47 | module.replicator:stop() 48 | error(err) -- do not throw until replicator is stopped 49 | end 50 | else 51 | module.replicator:stop() 52 | error("file handle not valid") -- do not throw until replicator is stopped 53 | end 54 | end):start() 55 | 56 | module.stop = function() 57 | module.replicator:stop() 58 | if io.type(module.file) == "file" then module.file:close() end 59 | end 60 | 61 | return module 62 | -------------------------------------------------------------------------------- /_scratch/examineHotKeys.lua: -------------------------------------------------------------------------------- 1 | 2 | local flagMasks = { 3 | activeFlag = 1 << 0, 4 | btnState = 1 << 7, 5 | cmdKey = 1 << 8, 6 | shiftKey = 1 << 9, 7 | alphaLock = 1 << 10, 8 | optionKey = 1 << 11, 9 | controlKey = 1 << 12, 10 | rightShiftKey = 1 << 13, 11 | rightOptionKey = 1 << 14, 12 | rightControlKey = 1 << 15, 13 | 14 | -- I think -- we can't set it, but I see this on keys that I also see this set for when using hs.eventtap 15 | functionKey = 1 << 17, 16 | } 17 | 18 | for i, v in ipairs(_xtras.hotkeys()) do 19 | local hotkey = rawget(hs.keycodes.map, v.kHISymbolicHotKeyCode) or ("{" .. tostring(v.kHISymbolicHotKeyCode) .. "}") 20 | local mods, mask = {}, v.kHISymbolicHotKeyModifiers 21 | for k2, v2 in pairs(flagMasks) do 22 | if (mask & v2) == v2 then 23 | mask = mask - v2 24 | table.insert(mods, k2) 25 | end 26 | end 27 | if mask ~= 0 then table.insert(mods, mask) end 28 | print(string.format("%s %-12s %s", (v.kHISymbolicHotKeyEnabled and "+" or "-"), hotkey, finspect(mods))) 29 | end 30 | -------------------------------------------------------------------------------- /_scratch/examiner.lua: -------------------------------------------------------------------------------- 1 | local objc = require("hs._asm.objc") 2 | local inspect = require("hs.inspect") 3 | local fnutils = require("hs.fnutils") 4 | local module = {} 5 | 6 | 7 | 8 | 9 | module.examineClass = function(className) 10 | local class, tmpString 11 | 12 | if type(className) == "string" then 13 | class = objc.class(className) 14 | else 15 | class = className 16 | end 17 | 18 | local propList = class:propertyList() 19 | local methList = class:methodList() 20 | local ivarList = class:ivarList() 21 | local protList = class:adoptedProtocols() 22 | 23 | print("Class: "..class:name().." isMetaClass: ", (class:isMetaClass() and "Yes" or "No")) 24 | print("Meta Class:"..class:metaClass():name()) 25 | if class:superclass() then 26 | print("Superclass: "..class:superclass():name()) 27 | print("") 28 | end 29 | 30 | print("Adopted Protocols") 31 | tmpString = "" 32 | for k,v in fnutils.sortByKeys(protList) do tmpString = "\t"..k end 33 | if tmpString ~= "" then print(tmpString) end 34 | print("") 35 | 36 | print("Instance Variables") 37 | for k,v in fnutils.sortByKeys(ivarList) do 38 | print(string.format("\t%s = %s (@ %d)", k, v:typeEncoding(), v:offset())) 39 | end 40 | print("") 41 | 42 | print("Properties") 43 | for k,v in fnutils.sortByKeys(propList) do 44 | print(string.format("\t%s: %s", k, v:attributes())) 45 | end 46 | print("") 47 | 48 | print("Methods") 49 | for k,v in fnutils.sortByKeys(methList) do 50 | print(string.format("\t%s = %s(%s) (%d arguments)", v:returnType(), k, v:typeEncoding(), v:numberOfArguments())) 51 | end 52 | end 53 | 54 | 55 | return module -------------------------------------------------------------------------------- /_scratch/fnkey.lua: -------------------------------------------------------------------------------- 1 | -- see github issue #689 2 | 3 | xyzzy = hs.hotkey.bind({}, "j", 4 | function() 5 | if hs.eventtap.checkKeyboardModifiers().fn then 6 | hs.alert.show("FN is DOWN!!!") 7 | else 8 | xyzzy:disable() 9 | hs.eventtap.keyStroke({}, "j") 10 | xyzzy:enable() 11 | end 12 | end 13 | ) 14 | -------------------------------------------------------------------------------- /_scratch/focusedWindowChange.lua: -------------------------------------------------------------------------------- 1 | -- Still seems to be unable to detect new windows when launching an application that launches 2 | -- in two stages (e.g. java applications, in my case Smart Git) but at least it no longer stops 3 | -- working when this occurs occurs. 4 | -- 5 | -- If you leave such an application and return to it, then everything works as expected. Best 6 | -- guess is that the application element changes, but not its pid or title so application.watcher 7 | -- doesn't see it; otoh uielement's watcher didn't either, but I'm not as confident about it. 8 | -- Will revisit if/when I add watchers to `hs._asm.axuielement` which provides more direct 9 | -- access to uielements without as much of a wrapper trying to hide the internals. 10 | 11 | local module = {} 12 | local window = require "hs.window" 13 | local application = require "hs.application" 14 | local uielement = require "hs.uielement" 15 | 16 | -- may be nil if application has no window atm 17 | module.actionFunction = function(win) 18 | if win then 19 | print(string.format("%s -- focused window change: %s (%s)", os.date("%F %T"), win:title(), win:application():name())) 20 | else 21 | print(string.format("%s -- focused window change: %s (%s)", os.date("%F %T"), "** no window **", application.frontmostApplication():name())) 22 | end 23 | end 24 | 25 | local watcherFunction -- forward declaration since this is needed to create the watcher 26 | 27 | local newWatcher = function(andNotify) 28 | local win = window.focusedWindow() 29 | if andNotify then module.actionFunction(win) end 30 | local app = win and win:application() or application.frontmostApplication() 31 | -- local watcher = app:newWatcher(watcherFunction):start({uielement.watcher.applicationDeactivated, uielement.watcher.focusedWindowChanged, uielement.watcher.elementDestroyed}) 32 | local watcher = app:newWatcher(watcherFunction):start({uielement.watcher.focusedWindowChanged}) 33 | 34 | -- this is annoying... quitting an application doesn't destroy it's uielement, so we can't 35 | -- add just add uielement.watcher.elementDestroyed to the hs.uielement watcher... we have to 36 | -- store the application's pid so hs.application.watcher can detect the termination and force 37 | -- an update for us 38 | module.activePID = app:pid() 39 | 40 | return watcher 41 | end 42 | 43 | watcherFunction = function(el, ev, ...) 44 | if ev == uielement.watcher.applicationDeactivated then 45 | module.watcher:stop() 46 | module.watcher = newWatcher(true) 47 | elseif (ev == uielement.watcher.focusedWindowChanged) then 48 | -- when the last window for an app closes, but the app remains empty, el will be 49 | -- a raw `hs.uielement` object, which we don't want to deal with. 50 | if getmetatable(el).__name ~= "hs.window" then el = nil end 51 | module.actionFunction(el) 52 | else 53 | print("~~ unexpected event: " .. ev .. " on " .. tostring(el)) 54 | end 55 | end 56 | 57 | module.watcher = newWatcher() 58 | 59 | module.terminationWatcher = application.watcher.new(function(n, e, o) 60 | if (e == application.watcher.terminated and o:pid() == module.activePID) or 61 | (e == application.watcher.activated) 62 | then 63 | -- force an applicationDeactivated event so that it rebuilds the watcher. 64 | watcherFunction(nil, uielement.watcher.applicationDeactivated) 65 | end 66 | end):start() 67 | 68 | return module 69 | 70 | -------------------------------------------------------------------------------- /_scratch/globalVsLocalLookups.lua: -------------------------------------------------------------------------------- 1 | -- Hammerspoon/Lua global versus local lookups for a built-in function 2 | -- 3 | -- based on http://www.ludicroussoftware.com/blog/2011/11/01/local-v--table-functions/ 4 | -- and http://stackoverflow.com/a/20480641 5 | 6 | -- In Hammerspoon, MacbookAir6,1: 7 | -- > dofile("/Users/amagill/test.lua") 8 | -- Sample size of 100 9 | -- global lookup average: 0.76152027 stdev: 0.02302169938916 10 | -- local lookup average: 0.51476572 stdev: 0.016631186701877 11 | -- 12 | -- Lua 5.3.2 Copyright (C) 1994-2015 Lua.org, PUC-Rio 13 | -- > dofile("test.lua") 14 | -- Sample size of 100 15 | -- global lookup average: 0.35828007 stdev: 0.013754442376776 16 | -- local lookup average: 0.25715287 stdev: 0.012281427961801 17 | 18 | -- While it is interesting to me that Hammerspoon took almost double the time to run 19 | -- compared to a raw Lua instance, (may have to look into that at some point), 20 | -- a savings of approx a third of a second over 5,000,000 lookups does not justify 21 | -- the loss of readability in my opinion. 22 | 23 | local samples = 100 24 | 25 | local runTest = function() 26 | local startTime = os.clock() 27 | for i = 1, 5000000 do 28 | local j = math.floor(4.35) 29 | end 30 | local gTime = os.clock() - startTime 31 | 32 | local floor = math.floor 33 | local startTime = os.clock() 34 | for i = 1, 5000000 do 35 | local j = floor(4.35) 36 | end 37 | local lTime = os.clock() - startTime 38 | return gTime, lTime 39 | end 40 | 41 | local gTotal, lTotal = 0.0, 0.0 42 | local gSumSq, lSumSq = 0.0, 0.0 43 | 44 | for i = 1, samples, 1 do 45 | local gTime, lTime = runTest() 46 | gTotal = gTotal + gTime 47 | lTotal = lTotal + lTime 48 | gSumSq = gSumSq + gTime^2 49 | lSumSq = lSumSq + lTime^2 50 | end 51 | 52 | local gAvg, lAvg = gTotal / samples, lTotal / samples 53 | local gStdev = math.sqrt((samples * gSumSq - gTotal^2) / (samples * (samples - 1))) 54 | local lStdev = math.sqrt((samples * lSumSq - lTotal^2) / (samples * (samples - 1))) 55 | 56 | print("Sample size of ", samples) 57 | print(" global lookup average: ", gAvg, "stdev:", gStdev) 58 | print(" local lookup average: ", lAvg, "stdev:", lStdev) 59 | 60 | -------------------------------------------------------------------------------- /_scratch/graphPaper.lua: -------------------------------------------------------------------------------- 1 | d = hs.drawing.rectangle(hs.screen.mainScreen():fullFrame()) 2 | :setFill(true) 3 | :setStroke(false) 4 | :setFillColor({ image = hs.image.imageFromASCII([[ 5 | .........1.........3.........5.........7.........N 6 | .................................................. 7 | .................................................. 8 | .................................................. 9 | .................................................. 10 | .................................................. 11 | .................................................. 12 | .................................................. 13 | .................................................. 14 | A................................................A 15 | .................................................. 16 | .................................................. 17 | .................................................. 18 | .................................................. 19 | .................................................. 20 | .................................................. 21 | .................................................. 22 | .................................................. 23 | .................................................. 24 | C................................................C 25 | .................................................. 26 | .................................................. 27 | .................................................. 28 | .................................................. 29 | .................................................. 30 | .................................................. 31 | .................................................. 32 | .................................................. 33 | .................................................. 34 | E................................................E 35 | .................................................. 36 | .................................................. 37 | .................................................. 38 | .................................................. 39 | .................................................. 40 | .................................................. 41 | .................................................. 42 | .................................................. 43 | .................................................. 44 | G................................................G 45 | .................................................. 46 | .................................................. 47 | .................................................. 48 | .................................................. 49 | .................................................. 50 | .................................................. 51 | .................................................. 52 | .................................................. 53 | .................................................. 54 | L........1.........3.........5.........7.........M 55 | ]], 56 | { 57 | [9] = { strokeColor = { blue = 1, alpha = 1 } }, 58 | [10] = { strokeColor = { white = .25, alpha = 1 }, fillColor = { alpha = 0 }, shouldClose = false }, 59 | })}):show() 60 | -------------------------------------------------------------------------------- /_scratch/graphpaper2.lua: -------------------------------------------------------------------------------- 1 | -- for large scale, non stretched look, requires image pattern as color support, expected in release 0.9.47 2 | 3 | return require"hs.image".imageFromASCII([[ 4 | .........1.........3.........5.........7.........N 5 | .................................................. 6 | .................................................. 7 | .................................................. 8 | .................................................. 9 | .................................................. 10 | .................................................. 11 | .................................................. 12 | .................................................. 13 | A................................................A 14 | .................................................. 15 | .................................................. 16 | .................................................. 17 | .................................................. 18 | .................................................. 19 | .................................................. 20 | .................................................. 21 | .................................................. 22 | .................................................. 23 | C................................................C 24 | .................................................. 25 | .................................................. 26 | .................................................. 27 | .................................................. 28 | .................................................. 29 | .................................................. 30 | .................................................. 31 | .................................................. 32 | .................................................. 33 | E................................................E 34 | .................................................. 35 | .................................................. 36 | .................................................. 37 | .................................................. 38 | .................................................. 39 | .................................................. 40 | .................................................. 41 | .................................................. 42 | .................................................. 43 | G................................................G 44 | .................................................. 45 | .................................................. 46 | .................................................. 47 | .................................................. 48 | .................................................. 49 | .................................................. 50 | .................................................. 51 | .................................................. 52 | .................................................. 53 | L........1.........3.........5.........7.........M 54 | ]], 55 | { 56 | [9] = { strokeColor = { blue = 1, alpha = 1 } }, 57 | [10] = { strokeColor = { white = .25, alpha = 1 }, fillColor = { alpha = 0 }, shouldClose = false }, 58 | }) -------------------------------------------------------------------------------- /_scratch/historyChooser.lua: -------------------------------------------------------------------------------- 1 | local chooser = require("hs.chooser") 2 | local console = require("hs.console") 3 | local inspect = require("hs.inspect") 4 | local hotkey = require("hs.hotkey") 5 | 6 | local mods = require("hs._asm.extras").mods 7 | 8 | local module = {} 9 | 10 | local gotAnAnswer = function(what) 11 | if what then 12 | print(inspect(what)) 13 | end 14 | end 15 | 16 | local generateChoices = function() 17 | local results = {} 18 | for i,v in ipairs(console.getHistory()) do 19 | table.insert(results, { 20 | text = v, 21 | subText="", 22 | index = i 23 | }) 24 | end 25 | return results 26 | end 27 | 28 | module.chooser = chooser.new(gotAnAnswer):choices(generateChoices) 29 | 30 | module.hotkey = hotkey.bind(mods.CAsc, "return", function() 31 | module.chooser:refreshChoicesCallback():show() 32 | end) 33 | 34 | return module 35 | -------------------------------------------------------------------------------- /_scratch/imagetest.lua: -------------------------------------------------------------------------------- 1 | local path = "https://avatars1.githubusercontent.com/u/%d?v=3&s=96" 2 | local start = math.random(8139480 * 2) 3 | local max = 5 4 | 5 | local drawing = require("hs.drawing") 6 | local image = require("hs.image") 7 | 8 | drawMeth1 = {} 9 | 10 | table.insert(drawMeth1, os.time()) 11 | for i = 0, max, 1 do 12 | table.insert(drawMeth1, drawing.image({x = 100 + 100 * i, y = 200, h = 100, w = 100}, image.imageFromURL(string.format(path, start + i))):show()) 13 | table.insert(drawMeth1, os.time()) 14 | end 15 | 16 | for i = 1, #drawMeth1, 2 do print(os.date("%c", drawMeth1[i])) end 17 | 18 | print() 19 | 20 | -- start = math.random(8139480 * 2) 21 | -- 22 | -- drawMeth2 = {} 23 | -- 24 | -- table.insert(drawMeth2, os.time()) 25 | -- for i = 0, max, 1 do 26 | -- table.insert(drawMeth2, drawing.image({x = 100 + 100 * i, y = 400, h = 100, w = 100}, image.imageFromURL2(string.format(path, start + i))):show()) 27 | -- table.insert(drawMeth2, os.time()) 28 | -- end 29 | -- 30 | -- for i = 1, #drawMeth2, 2 do print(os.date("%c", drawMeth2[i])) end 31 | 32 | dd = function() 33 | if drawMeth1 then 34 | for i = 2, #drawMeth1, 2 do drawMeth1[i]:delete() end 35 | drawMeth1 = nil 36 | end 37 | if drawMeth2 then 38 | for i = 2, #drawMeth2, 2 do drawMeth2[i]:delete() end 39 | drawMeth2 = nil 40 | end 41 | dd = nil 42 | end -------------------------------------------------------------------------------- /_scratch/imageviewExample.lua: -------------------------------------------------------------------------------- 1 | local guitk = require("hs._asm.guitk") 2 | local image = require("hs.image") 3 | local stext = require("hs.styledtext") 4 | local canvas = require("hs.canvas") 5 | 6 | local module = {} 7 | 8 | local gui = guitk.new{ x = 100, y = 100, h = 500, w = 500 }:show() 9 | local mgr = guitk.manager.new() 10 | gui:contentManager(mgr) 11 | 12 | mgr[#mgr + 1] = { 13 | _element = guitk.element.textfield.newLabel(stext.new( 14 | "Drag an image file into the box or\npaste one from the clipboard", 15 | { paragraphStyle = { alignment = "center" } } 16 | )), 17 | frameDetails = { 18 | cX = "50%", 19 | y = 5, 20 | } 21 | } 22 | 23 | local placeholder = canvas.new{ x = 0, y = 0, h = 500, w = 500 }:appendElements{ 24 | { 25 | type = "image", 26 | image = image.imageFromName(image.systemImageNames.ExitFullScreenTemplate) 27 | }, { 28 | type = "image", 29 | image = image.imageFromName(image.systemImageNames.ExitFullScreenTemplate), 30 | transformation = canvas.matrix.translate(250,250):rotate(90):translate(-250,-250), 31 | } 32 | }:imageFromCanvas() 33 | 34 | local imageElement = guitk.element.image.new():image(placeholder) 35 | :allowsCutCopyPaste(true) 36 | :editable(true) 37 | :imageAlignment("center") 38 | :imageFrameStyle("bezel") 39 | :imageScaling("proportionallyUpOrDown") 40 | :callback(function(o) 41 | if module.canvas then module.canvas:delete() end 42 | module.canvas = canvas.new{ x = 700, y = 100, h = 100, w = 100 }:show() 43 | module.canvas[1] = { 44 | type = "image", 45 | image = o:image() 46 | } 47 | end) 48 | 49 | mgr:insert(imageElement, { w = 450, h = 450 }) 50 | imageElement:moveBelow(mgr(1), 5, "centered") 51 | 52 | module.manager = mgr 53 | 54 | return module 55 | -------------------------------------------------------------------------------- /_scratch/modtest.lua: -------------------------------------------------------------------------------- 1 | -- mymod.lua 2 | local mod={} -- the module 3 | 4 | local vartypes={ -- types for variables 5 | anumber='number', 6 | astring='string', 7 | afunctionvalue='function', 8 | } 9 | local argtypes={ -- types for function/method arguments 10 | afunction={'number','string'} 11 | } 12 | for fn,args in pairs(argtypes) do 13 | args.wrapper=function(...) 14 | for i=1,math.max(select('#',...),#args) do -- check arg types 15 | local tp = type(select(i,...) or nil) 16 | if tp~=args[i] then error(string.format('%s: wrong type for argument #%d: %s expected, got %s',fn,i,args[i],tp),2) end 17 | end 18 | return mod[fn](...) -- call the function 19 | end 20 | end 21 | 22 | mod.astring='this is a string' 23 | mod.afunctionvalue=function()print('callback')end 24 | 25 | function mod.afunction(num,str) 26 | print(string.rep(str,num)) 27 | end 28 | 29 | local function get(_,k) 30 | local fn=argtypes[k] 31 | if fn then return fn.wrapper --it's a function, return a wrapper 32 | else return mod[k] end -- it's a value 33 | end 34 | local function set(_,k,v) 35 | if argtypes[k] then error('cannot override function '..k,2) end -- don't allow overriding functions/methods 36 | if type(v)~=vartypes[k] then error(string.format('wrong type for variable %s: %s expected, got %s',k,vartypes[k],type(v)),2) end -- can't set wrong type 37 | mod[k]=v 38 | end 39 | 40 | local wrap=setmetatable({},{__index=get,__newindex=set}) -- this is the table that gets exported 41 | 42 | return wrap 43 | 44 | -- init.lua 45 | --mymod = wrap -- it'd be require'mymod' 46 | -- 47 | --local function test(code) 48 | -- local fn=load(code) 49 | -- local ok,error=pcall(fn) 50 | -- if ok then print(code..' -- pass') 51 | -- else print(code..' -- '..error) end 52 | --end 53 | -- 54 | --test'print(mymod.astring)' 55 | --test'mymod.astring=42' 56 | --test'mymod.astring="hello"' 57 | --test'mymod.anumber="hi"' 58 | --test'mymod.afunction(1,2)' 59 | --test'mymod.afunction("a")' 60 | --test'mymod.afunction(42)' 61 | --test'mymod.afunction(3,"repeat")' 62 | -------------------------------------------------------------------------------- /_scratch/nastest.lua: -------------------------------------------------------------------------------- 1 | local module = {} 2 | 3 | local escAppleScriptStr = function(text) 4 | local s = string.gsub(text, "\\", "\\\\") 5 | local s = string.gsub(s, '"', "\\\"") 6 | local s = string.gsub(s, "'", "\\'") 7 | return s 8 | end 9 | 10 | module.NASTimer = {} 11 | module.NASDrives = { 12 | ['Home'] = { 13 | "afp://ASM APE._afpovertcp._tcp.local/Cortex" 14 | }, 15 | ['Work'] = { 16 | "smb://latitude/wtps", 17 | "smb://latitude/amagill" 18 | } 19 | } 20 | 21 | module.mountNAS = function(id) 22 | if module.NASTimer[id] then 23 | local running = false 24 | for i = 1, #module.NASTimer[id] do 25 | if module.NASTimer[id][i] then 26 | -- utils.log(id .. " NAS are already being mounted") 27 | return 28 | end 29 | end 30 | end 31 | module.NASTimer[id] = {} 32 | for i = 1, #module.NASDrives[id] do 33 | module.NASTimer[id][i] = hs.timer.doEvery(10, function() 34 | local fullpath = module.NASDrives[id][i] 35 | -- utils.log("Attempting to mount " .. fullpath) 36 | -- if not config.cachedNetwork or config.cachedNetwork ~= id then 37 | -- module.NASTimer[id][i]:stop() 38 | -- module.NASTimer[id][i] = nil 39 | -- return 40 | -- end 41 | local shortvol = fullpath:match('^([^/]+://[^/]+/[^/]+)') 42 | print(shortvol) 43 | local _, res = hs.applescript.applescript([[ 44 | tell application "Finder" 45 | try 46 | mount volume "]] .. escAppleScriptStr(shortvol) .. [[" 47 | on error 48 | return 0 49 | end try 50 | return 1 51 | end tell 52 | ]]) 53 | print(_, res) 54 | if res == 1 then 55 | -- os.execute("open " .. fullpath) 56 | module.NASTimer[id][i]:stop() 57 | module.NASTimer[id][i] = nil 58 | return 59 | end 60 | _, res = hs.applescript.applescript([[ 61 | tell application "Finder" 62 | try 63 | if (disk "]] .. escAppleScriptStr(shortvol:match('([^/]+)$')) .. [[" exists) then 64 | return 1 65 | end 66 | end try 67 | return 0 68 | end tell 69 | ]]) 70 | print(_, res) 71 | if res == 1 then 72 | -- os.execute("open " .. fullpath) 73 | module.NASTimer[id][i]:stop() 74 | module.NASTimer[id][i] = nil 75 | return 76 | end 77 | end) 78 | end 79 | end 80 | 81 | return module -------------------------------------------------------------------------------- /_scratch/nc.lua: -------------------------------------------------------------------------------- 1 | nc = require("hs._asm.notificationcenter") 2 | 3 | output = function(name, obj, info) 4 | print("name:"..name.."\n obj:"..inspect(obj):gsub("%s+"," ").."\ninfo:"..inspect(info):gsub("%s+"," ")) 5 | end 6 | 7 | appActivateWatcher = nc.workspaceObserver(output, 8 | nc.notificationNames.NSWorkspaceDidActivateApplication):start() 9 | 10 | -- returns nil for info 11 | spacesWatcher = nc.workspaceObserver(output, 12 | nc.notificationNames.NSWorkspaceActiveSpaceDidChange):start() 13 | 14 | -- returns NSRunningApplication in info dict 15 | appActivateWatcher = nc.workspaceObserver(output, 16 | nc.notificationNames.NSWorkspaceDidActivateApplication):start() 17 | 18 | -- returns nil for info 19 | screenWatcher = nc.internalObserver(output, 20 | nc.notificationNames.NSApplicationDidChangeScreenParameters):start() 21 | 22 | -- returns nil for info 23 | keyboardWatcher = nc.internalObserver(output, 24 | nc.notificationNames.NSTextInputContextKeyboardSelectionDidChange):start() 25 | 26 | -- requires a retained reference to the Wifi Interface to work... can't do this yet. 27 | -- wifiWatcher = nc.internalObserver(output, 28 | -- nc.deprecatedNotificationNames.CWSSIDDidChange):start() 29 | 30 | dwn = nc.workspaceObserver(output, 31 | nc.notificationNames.NSWorkspaceDidWake):start() 32 | wsn = nc.workspaceObserver(output, 33 | nc.notificationNames.NSWorkspaceWillSleep):start() 34 | wpon = nc.workspaceObserver(output, 35 | nc.notificationNames.NSWorkspaceWillPowerOff):start() 36 | sdsn = nc.workspaceObserver(output, 37 | nc.notificationNames.NSWorkspaceScreensDidSleep):start() 38 | sdwn = nc.workspaceObserver(output, 39 | nc.notificationNames.NSWorkspaceScreensDidWake):start() 40 | 41 | 42 | -- Need parser for: 43 | -- ? NSWorkspace as obj 44 | -- NSRunningApplication as info --> hs.application? 45 | -- 46 | 47 | -- 48 | -- w = wrong notification type 49 | -- - = requires retention of object we can't support yet 50 | -- 51 | -- * application/watcher.m 52 | -- * screen/watcher.m 53 | -- - wifi/watcher.m 54 | -- w battery/watcher.m 55 | -- * spaces/watcher.m 56 | -- * caffeinate/watcher.m 57 | -- w usb/watcher.m 58 | -- uielement.m -------------------------------------------------------------------------------- /_scratch/newimage.lua: -------------------------------------------------------------------------------- 1 | 2 | local i = [[ASCII: 3 | · · · · · · · · · · · · · · · 4 | · · · · 1 · · · · · · 1 · · · 5 | · · · · · · · · · · · · · · · 6 | · · · · · · · · · · · · · · · 7 | · · · · · · · · · · · · · · · 8 | · · 3 · 1 · · · · · · 1 · 4 · 9 | · · · · · · · · · · · · · · · 10 | · · · · · · A · · A · · · · · 11 | · · · · 1 · · · · · · 1 · · · 12 | · · · · · · · C D · · · · · · 13 | · · · · · · A · · A · · · · · 14 | · · · · · · · · · · · · · · · 15 | · · · · · · · B E · · · · · · 16 | · · · · · · · · · · · · · · · 17 | · · 6 · · · · · · · · · · 5 · 18 | ]] 19 | 20 | local black = { red = 0, blue = 0, green = 0, alpha = 1 } 21 | local clear = { red = 0, blue = 0, green = 0, alpha = 0 } 22 | local white = { red = 1, blue = 1, green = 1, alpha = 1 } 23 | local gray = { red = .2, blue = .2, green = .2, alpha = 1 } 24 | 25 | local c = { 26 | { 27 | strokeColor = black, 28 | fillColor = clear, 29 | }, 30 | { 31 | strokeColor = black, 32 | fillColor = gray, 33 | }, 34 | { 35 | strokeColor = white, 36 | fillColor = white, 37 | antialias = true, 38 | shouldClose = true 39 | } 40 | } 41 | 42 | local d = hs.drawing.image({x = 100, y = 100, h = 500, w = 500}, 43 | hs.image.imageFromASCII(i)):show() 44 | 45 | local e = hs.drawing.image({x = 600, y = 100, h = 500, w = 500}, 46 | hs.image.imageFromASCII(i, c)):show() 47 | 48 | local esc = function() d:delete() ; e:delete() end 49 | 50 | xyzzy = hs.hotkey.bind({},"escape", 51 | function() esc() end, 52 | function() xyzzy:disable() end 53 | ) 54 | -------------------------------------------------------------------------------- /_scratch/no_timemachine_icon.lua: -------------------------------------------------------------------------------- 1 | a = hs.canvas.new{ x = 100, y = 100, h = 500, w = 500 }:show() 2 | a[1] = { 3 | type = "image", 4 | image = hs.image.imageFromName("NSFolder") 5 | } 6 | a[2] = { 7 | type = "image", 8 | image = hs.image.imageFromPath("/System/Library/PreferencePanes/TimeMachine.prefPane/Contents/Resources/TimeMachine_128x128.png"), 9 | frame = { x = 100, y = 115, h = 300, w = 300 }, 10 | } 11 | a[3] = { 12 | action = "stroke", 13 | closed = false, 14 | coordinates = {{ x = 125, y = 140 }, { x = 375, y = 390 }}, 15 | strokeColor = { red = 1 }, 16 | strokeWidth = 20, 17 | type = "segments" 18 | } 19 | a[4] = { 20 | action = "stroke", 21 | closed = false, 22 | coordinates = {{ x = 375, y = 140 }, { x = 125, y = 390 }}, 23 | strokeColor = { red = 1 }, 24 | strokeWidth = 20, 25 | type = "segments" 26 | } 27 | hs.pasteboard.writeObjects(a:imageFromCanvas()) 28 | -------------------------------------------------------------------------------- /_scratch/objecttable.lua: -------------------------------------------------------------------------------- 1 | local module = {} 2 | 3 | 4 | local metatable = { 5 | __self = setmetatable({}, { __mode = "k" }), 6 | __trace = setmetatable({}, { __mode = "k" }), 7 | } 8 | 9 | metatable.__index = function(self, key) 10 | if metatable.__trace[self] then print("~~ index for ", tostring(key)) end 11 | return metatable.__self[self][key] 12 | end 13 | 14 | metatable.__newindex = function(self, key, value) 15 | if metatable.__trace[self] then print("~~ newindex for ", tostring(key), tostring(value)) end 16 | metatable.__self[self][key] = value 17 | end 18 | 19 | metatable.__len = function(self) 20 | if metatable.__trace[self] then print("~~ len") end 21 | return #metatable.__self[self] 22 | end 23 | 24 | metatable.__pairs = function(self) 25 | if metatable.__trace[self] then print("~~ pairs") end 26 | return function(_, k) 27 | if metatable.__trace[self] then print("~~ pairs.iterator for ", tostring(k)) end 28 | return next(_, k) 29 | end, metatable.__self[self], nil 30 | end 31 | 32 | module.new = function() 33 | local newTable = {} 34 | metatable.__self[newTable] = {} 35 | metatable.__trace[newTable] = false 36 | return setmetatable(newTable, metatable) 37 | end 38 | 39 | module.trace = function(obj, value) 40 | if type(value) == "boolean" then 41 | metatable.__trace[obj] = value 42 | end 43 | return metatable.__trace[obj] 44 | end 45 | 46 | return module -------------------------------------------------------------------------------- /_scratch/otherMouseWindowMove.lua: -------------------------------------------------------------------------------- 1 | -- alternative to https://github.com/Hammerspoon/hammerspoon/issues/1519 2 | 3 | local eventtap = require("hs.eventtap") 4 | local window = require("hs.window") 5 | local geometry = require("hs.geometry") 6 | local mouse = require("hs.mouse") 7 | local fnutils = require("hs.fnutils") 8 | local screen = require("hs.screen") 9 | local alert = require("hs.alert") 10 | 11 | local eventTypes = eventtap.event.types 12 | local eventProps = eventtap.event.properties 13 | 14 | local function get_window_under_mouse() 15 | local my_pos = geometry.new(mouse.getAbsolutePosition()) 16 | local my_screen = mouse.getCurrentScreen() 17 | local myWindow = nil 18 | 19 | -- some windows don't appear in `hs.window.orderedWindows` because of their style or application type 20 | -- this allows us to use the Cmd key as a way to say use the topmost window rather then try to figure 21 | -- out which window is beneath the current mouse position 22 | if eventtap.checkKeyboardModifiers().cmd then 23 | myWindow = window.frontmostWindow() 24 | else 25 | myWindow = fnutils.find(window.orderedWindows(), function(w) 26 | return my_screen == w:screen() and my_pos:inside(w:frame()) 27 | end) 28 | end 29 | return myWindow 30 | end 31 | 32 | -- establish these as local, since we need to set them in the callback but have them persist between multiple callbacks 33 | local targetWindow, targetTopLeft = nil, nil 34 | 35 | eventtapOtherMouseDragged = eventtap.new( { 36 | eventTypes.otherMouseDown, eventTypes.otherMouseUp, eventTypes.otherMouseDragged 37 | }, function(event) 38 | -- we only want to override the third mouse button; if they have more, let those procede normally 39 | if event:getProperty(eventProps.mouseEventButtonNumber) == 2 then 40 | local receivedEvent = event:getType() 41 | if receivedEvent == eventTypes.otherMouseDown then 42 | targetWindow = get_window_under_mouse() 43 | if targetWindow then 44 | alert("Target Window: " .. (targetWindow:title() or "")) 45 | targetTopLeft = targetWindow:topLeft() 46 | else 47 | alert("no window at current mouse location") 48 | end 49 | elseif receivedEvent == eventTypes.otherMouseUp then 50 | targetWindow = nil 51 | elseif receivedEvent == eventTypes.otherMouseDragged then 52 | if targetWindow then 53 | local dx = event:getProperty(eventProps.mouseEventDeltaX) 54 | local dy = event:getProperty(eventProps.mouseEventDeltaY) 55 | targetTopLeft = { x = targetTopLeft.x + dx, y = targetTopLeft.y + dy } 56 | targetWindow:setTopLeft(targetTopLeft) 57 | end 58 | else 59 | -- this should never happen 60 | alert("unexpected event: " .. (eventTypes[receivedEvent] or ("eventID " .. tostring(receivedEvent)))) 61 | return false 62 | end 63 | return true 64 | else 65 | return false 66 | end 67 | end):start() 68 | -------------------------------------------------------------------------------- /_scratch/patternMatch.lua: -------------------------------------------------------------------------------- 1 | -- from https://gist.github.com/heptal/7e578c3129012f0e7e91965bb1f2010e 2 | 3 | local name = "id"..hs.host.uuid():gsub("-",""); 4 | 5 | local html = [[ 6 |
 7 | .   all characters
 8 | %a  letters
 9 | %b  balanced delimiters
10 | %c  control characters
11 | %d  digits
12 | %l  lower case letters
13 | %p  punctuation characters
14 | %s  space characters
15 | %u  upper case letters
16 | %w  alphanumeric characters
17 | %x  hexadecimal digits
18 | %z  the character with representation 0
19 | 
20 | 21 |
22 |
23 | Result: 24 |
25 | ]] 26 | 27 | local js = [[ 28 | function sendData() { 29 | webkit.messageHandlers.]]..name..[[.postMessage({ 30 | pattern: document.getElementById("pattern").value, 31 | text: document.getElementById("text").value 32 | }) 33 | } 34 | ]] 35 | 36 | local css = [[ 37 | input { width: 250px; } 38 | body {font-family: monospace;} 39 | span {background:rgba(0,0,200,0.2) } 40 | #response {font-size:11px} 41 | ]] 42 | 43 | local uc = hs.webview.usercontent.new(name):setCallback(function(input) 44 | local pattern, text = input.body.pattern, input.body.text; 45 | local result = text:gsub(pattern, function(s) return ''..s..'' end) 46 | result = result:gsub("\n", "
"):gsub(" ", " ") 47 | webview:evaluateJavaScript("document.getElementById('response').innerHTML = "..string.format("%q",result)) 48 | end) 49 | 50 | local frame = hs.geometry.rect(hs.screen.mainScreen():frame().topleft, "600x700") 51 | 52 | webview = hs.webview.new(frame, {developerExtrasEnabled=true}, uc):windowStyle(1|2|4|8):deleteOnClose(true):allowTextEntry(true) 53 | webview:html(string.format('%s', html, js, css)):show() -------------------------------------------------------------------------------- /_scratch/progressItemExample.lua: -------------------------------------------------------------------------------- 1 | local guitk = require("hs._asm.guitk") 2 | local timer = require("hs.timer") 3 | 4 | local module = {} 5 | 6 | local gui = guitk.new{ x = 100, y = 100, h = 100, w = 204 }:show() 7 | local mgr = guitk.manager.new() 8 | gui:contentManager(mgr) 9 | 10 | mgr[#mgr + 1] = { 11 | id = "backgroundSpinner", 12 | _element = guitk.element.progress.new():circular(true):start(), 13 | } 14 | mgr[#mgr + 1] = { 15 | id = "foregroundSpinner", 16 | _element = guitk.element.progress.new():circular(true):threaded(false):start(), 17 | } 18 | 19 | mgr[#mgr + 1] = { 20 | id = "backgroundBar", 21 | _element = guitk.element.progress.new():start(), 22 | frameDetails = { x = 10, y = 10, w = 184 }, 23 | } 24 | mgr[#mgr + 1] = { 25 | id = "foregroundBar", 26 | _element = guitk.element.progress.new():threaded(false):start(), 27 | frameDetails = { w = 184 }, 28 | } 29 | 30 | mgr[#mgr + 1] = { 31 | id = "hoursBar", 32 | _element = guitk.element.progress.new(), 33 | min = 0, 34 | max = 23, 35 | indeterminate = false, 36 | indicatorSize = "small", 37 | color = { red = 1 }, 38 | tooltip = "hours", 39 | frameDetails = { w = 120 }, 40 | } 41 | mgr[#mgr + 1] = { 42 | id = "minutesBar", 43 | _element = guitk.element.progress.new(), 44 | min = 0, 45 | max = 60, 46 | indeterminate = false, 47 | indicatorSize = "small", 48 | color = { green = 1 }, 49 | tooltip = "minutes", 50 | frameDetails = { w = 120 }, 51 | } 52 | mgr[#mgr + 1] = { 53 | id = "secondsBar", 54 | _element = guitk.element.progress.new(), 55 | min = 0, 56 | max = 60, 57 | indeterminate = false, 58 | indicatorSize = "small", 59 | color = { blue = 1 }, 60 | tooltip = "seconds", 61 | frameDetails = { w = 120 }, 62 | } 63 | 64 | mgr[#mgr + 1] = { 65 | id = "hoursSpinner", 66 | _element = guitk.element.progress.new(), 67 | circular = true, 68 | min = 0, 69 | max = 23, 70 | indeterminate = false, 71 | indicatorSize = "small", 72 | color = { red = 1, green = 1 }, 73 | tooltip = "hours", 74 | } 75 | mgr[#mgr + 1] = { 76 | id = "minutesSpinner", 77 | _element = guitk.element.progress.new(), 78 | circular = true, 79 | min = 0, 80 | max = 60, 81 | indeterminate = false, 82 | indicatorSize = "small", 83 | color = { green = 1, blue = 1 }, 84 | tooltip = "minutes", 85 | } 86 | mgr[#mgr + 1] = { 87 | id = "secondsSpinner", 88 | _element = guitk.element.progress.new(), 89 | circular = true, 90 | min = 0, 91 | max = 60, 92 | indeterminate = false, 93 | indicatorSize = "small", 94 | color = { blue = 1, red = 1 }, 95 | tooltip = "seconds", 96 | } 97 | 98 | mgr("backgroundBar"):frameDetails{ x = 10, y = 10, w = 184 } 99 | 100 | mgr("backgroundSpinner"):moveBelow(mgr("backgroundBar")) 101 | mgr("foregroundSpinner"):moveBelow(mgr("backgroundBar"), "flushRight") 102 | 103 | mgr("hoursBar"):moveBelow(mgr("backgroundBar"), -2, "centered") 104 | mgr("minutesBar"):moveBelow(mgr("hoursBar")) 105 | mgr("secondsBar"):moveBelow(mgr("minutesBar")) 106 | 107 | mgr("foregroundBar"):moveBelow(mgr("backgroundSpinner")) 108 | 109 | mgr("hoursSpinner"):moveBelow(mgr("foregroundBar")) 110 | mgr("minutesSpinner"):moveBelow(mgr("foregroundBar"), "centered") 111 | mgr("secondsSpinner"):moveBelow(mgr("foregroundBar"), "flushRight") 112 | 113 | local updateTimeBars = function() 114 | local t = os.date("*t") 115 | mgr("hoursBar"):value(t.hour) 116 | mgr("minutesBar"):value(t.min) 117 | mgr("secondsBar"):value(t.sec) 118 | mgr("hoursSpinner"):value(t.hour) 119 | mgr("minutesSpinner"):value(t.min) 120 | mgr("secondsSpinner"):value(t.sec) 121 | end 122 | 123 | module.timer = timer.doEvery(1, updateTimeBars):start() 124 | updateTimeBars() 125 | 126 | module.mgr = mgr 127 | 128 | return module 129 | 130 | -------------------------------------------------------------------------------- /_scratch/rand.lua: -------------------------------------------------------------------------------- 1 | local f = io.open("/usr/share/dict/words", "r") 2 | local a = f:read("a") 3 | f:close() 4 | local module = {} 5 | 6 | local fnutils = require("hs.fnutils") 7 | module.words = fnutils.split(a, "[\r\n]") 8 | module.random = function(count) 9 | count = tonumber(count) or 1 10 | local someWords = {} 11 | for i = 1, count, 1 do 12 | table.insert(someWords, module.words[math.random(1,#module.words)]) 13 | end 14 | return table.concat(someWords, " ") 15 | end 16 | 17 | return module -------------------------------------------------------------------------------- /_scratch/rdoc.lua: -------------------------------------------------------------------------------- 1 | -- Doc tools 2 | 3 | local module = {} 4 | local fnutils = require("hs.fnutils") 5 | 6 | module.rdoc = function(thing) 7 | if type(thing) ~= "table" then 8 | print("rdoc(docObject, filename) -- Output recursive documentation dump.") 9 | else 10 | print(thing) 11 | print("--------------------------------------------------------------------------------") 12 | print() 13 | for name, item in fnutils.sortByKeys(thing) 14 | do 15 | if type(item) == "table" then module.rdoc(item) end 16 | end 17 | end 18 | end 19 | 20 | module.fdoc = function(thing, file) 21 | if type(thing) ~= "table" or type(file) ~= "string" then 22 | print("fdoc(docObject, filename) -- Output recursive documentation dump to file.") 23 | else 24 | local f = io.open(file, "w+") 25 | local op = print 26 | local np = function(a) 27 | if type(a) == "nil" then 28 | f:write("\n\n") 29 | else 30 | f:write(tostring(a)) 31 | end 32 | end 33 | 34 | print = np 35 | module.rdoc(thing) 36 | f:close() 37 | print = op 38 | print("File '"..file.."' has been written.") 39 | end 40 | end 41 | 42 | return module -------------------------------------------------------------------------------- /_scratch/sampleimages.lua: -------------------------------------------------------------------------------- 1 | 2 | -- the primary "known good" list is handled a little differently than the rest, so... 3 | local primaryTable = {} 4 | for k, v in hs.fnutils.sortByKeys(hs.image.systemImageNames) do table.insert(primaryTable, v) end 5 | 6 | local sources = { 7 | { "hs.image.systemImageNames", primaryTable }, 8 | } 9 | 10 | for k,v in hs.fnutils.sortByKeys(hs.image.additionalImageNames) do 11 | table.insert(sources, { "hs.image.additionalImageNames." .. k, v }) 12 | end 13 | 14 | local position = 1 15 | local currentOffset = 0 16 | local maxCols = 14 17 | local maxRows = 5 18 | local maxPerPage = maxCols * maxRows 19 | 20 | local drawBlock 21 | drawBlock = function() 22 | local imageSource = sources[position][2] 23 | 24 | local a = { 25 | hs.drawing.rectangle{x=10, y=40, w=1420, h=740} 26 | :setRoundedRectRadii(20,20):setStroke(true):setStrokeWidth(10) 27 | :setFill(true):setFillColor{red=1, blue=1, green = 1, alpha = 1}:show() 28 | } 29 | 30 | local pos = 0 31 | local c = 0 32 | 33 | for i,v in ipairs(imageSource) do 34 | pos = pos + 1 35 | if pos >= currentOffset then 36 | c = c + 1 37 | table.insert(a, hs.drawing.text({ 38 | x=20, y=45, h=30, w=1380}, sources[position][1]) 39 | :setTextSize(20):setTextColor{red = 0, blue = 0, green = 0, alpha=1} 40 | :setTextFont("Menlo"):show()) 41 | local picture = hs.image.imageFromName(v) 42 | if picture then 43 | table.insert(a, hs.drawing.image({ 44 | x=20 + ((c - 1) % 14) * 100, 45 | y=75 + math.floor((c-1)/14) * 140, 46 | h=100, w=100}, picture):show()) 47 | end 48 | table.insert(a, hs.drawing.text({ 49 | x=20 + ((c - 1) % 14) * 100, 50 | y=175 + math.floor((c-1)/14) * 140, 51 | h=50, w=100}, v) 52 | :setTextSize(10):setTextColor{red = 0, blue = 0, green = 0, alpha=1} 53 | :setTextFont("Menlo"):show()) 54 | end 55 | if c == maxPerPage then break end 56 | end 57 | 58 | esc = function() hs.fnutils.map(a, function(a) a:delete() end) end 59 | 60 | if position < #sources or pos < #imageSource then 61 | nextPage = hs.hotkey.bind({},"right", 62 | function() esc() end, 63 | function() 64 | if xyzzy then xyzzy:disable() ; xyzzy = nil end 65 | if nextPage then nextPage:disable() ; nextPage = nil end 66 | if prevPage then prevPage:disable() ; prevPage = nil end 67 | if pos < #imageSource then 68 | currentOffset = currentOffset + maxPerPage 69 | else 70 | currentOffset = 0 71 | position = position + 1 72 | end 73 | drawBlock() 74 | end 75 | ) 76 | end 77 | 78 | if position > 1 then 79 | prevPage = hs.hotkey.bind({}, "left", 80 | function() esc() end, 81 | function() 82 | if xyzzy then xyzzy:disable() ; xyzzy = nil end 83 | if nextPage then nextPage:disable() ; nextPage = nil end 84 | if prevPage then prevPage:disable() ; prevPage = nil end 85 | if currentOffset == 0 then 86 | position = position - 1 87 | else 88 | currentOffset = currentOffset - maxPerPage 89 | end 90 | drawBlock() 91 | end 92 | ) 93 | end 94 | 95 | xyzzy = hs.hotkey.bind({},"escape", 96 | function() esc() end, 97 | function() 98 | if xyzzy then xyzzy:disable() ; xyzzy = nil end 99 | if nextPage then nextPage:disable() ; nextPage = nil end 100 | if prevPage then prevPage:disable() ; prevPage = nil end 101 | end 102 | ) 103 | end 104 | 105 | drawBlock() 106 | -------------------------------------------------------------------------------- /_scratch/serviceTest.lua: -------------------------------------------------------------------------------- 1 | local s = require("hs._asm.service") 2 | local p = require("hs.pasteboard") 3 | 4 | local runstring = function(s) 5 | local fn, err = load("return " .. s) 6 | if not fn then fn, err = load(s) end 7 | if not fn then return tostring(err) end 8 | 9 | local str = "" 10 | local results = pack(xpcall(fn,debug.traceback)) 11 | for i = 2,results.n do 12 | if i > 2 then str = str .. "\t" end 13 | str = str .. tostring(results[i]) 14 | end 15 | return str 16 | end 17 | 18 | serviceRunString = s.new("HSRunStringService"):setCallback(function(pboardName) 19 | local goodType = false 20 | for i,v in ipairs(p.contentTypes(pboardName)) do 21 | if v == "public.utf8-plain-text" then 22 | goodType = true 23 | break 24 | end 25 | end 26 | if not goodType then 27 | return "pasteboard does not contain text" 28 | end 29 | print(pboardName, c) 30 | local c = hs.pasteboard.getContents(pboardName) 31 | local r = runstring(c) 32 | if r == nil then 33 | return "runstring returned nil" 34 | end 35 | if r == "testError" then 36 | return "testError Hooray!" 37 | end 38 | p.clearContents(pboardName) 39 | p.setContents(r, pboardName) 40 | return true 41 | end) 42 | 43 | 44 | -- 1+10 45 | -------------------------------------------------------------------------------- /_scratch/slidingPanel.canvas.lua: -------------------------------------------------------------------------------- 1 | local timer = require("hs.timer") 2 | local canvas = require("hs.canvas") 3 | local screen = require("hs.screen") 4 | 5 | local MAXSTEPS = 10 -- how many steps should the panel take to go from full close to full open or vice-versa 6 | 7 | local module = {} 8 | local _canvas, _sensor 9 | local _frame 10 | local resetOnCurrentScreen = function() 11 | _frame = screen.mainScreen():fullFrame() 12 | _canvas:frame{ x = _frame.x, y = _frame.y + _frame.h, h = _frame.h / 2, w = _frame.w } 13 | _sensor:frame{ x = _frame.x, y = _frame.y + _frame.h - 1, h = 1, w = _frame.w } 14 | _sensor:orderAbove(_canvas) 15 | end 16 | 17 | local _targetCount, _count, _dir 18 | local startPanelTimer = function() 19 | return timer.doEvery(0.5 / MAXSTEPS, function() 20 | if _count == 0 and _targetCount == MAXSTEPS then _canvas:show() end 21 | _count = _count + _dir 22 | _canvas:topLeft{ x = _frame.x, y = _frame.y + _frame.h * ( 1 - _count / (2 * MAXSTEPS) ) } 23 | if _count == 0 and _targetCount == 0 then _canvas:hide() end 24 | if _count == _targetCount then 25 | module._panelMoveTimer:stop() 26 | module._panelMoveTimer = nil 27 | end 28 | end) 29 | end 30 | _canvas = canvas.new{}:level("status") 31 | _sensor = canvas.new{}:level("status"):behavior("canJoinAllSpaces"):orderAbove(_canvas) 32 | :canvasMouseEvents(false, false, true, false) 33 | :mouseCallback(function(c, m, i, x, y) 34 | if m == "mouseEnter" then 35 | _targetCount, _dir = MAXSTEPS, 1 36 | if not module._panelMoveTimer then 37 | _count = 0 38 | module._panelMoveTimer = startPanelTimer() 39 | end 40 | elseif m == "mouseExit" then 41 | _targetCount, _dir = 0, -1 42 | if not module._panelMoveTimer then 43 | _count = MAXSTEPS 44 | module._panelMoveTimer = startPanelTimer() 45 | end 46 | end 47 | end):show() 48 | resetOnCurrentScreen() 49 | 50 | _canvas[#_canvas + 1] = { 51 | type = "rectangle", 52 | id = "backPanel", 53 | strokeWidth = 10, 54 | fillColor = { alpha = .5 }, 55 | strokeColor = { alpha = .7 }, 56 | roundedRectRadii = {xRadius = 10, yRadius = 10}, 57 | clipToPath = true, 58 | } 59 | 60 | module._canvas = _canvas 61 | module._sensor = _sensor 62 | 63 | return module 64 | -------------------------------------------------------------------------------- /_scratch/spacescreendiff.lua: -------------------------------------------------------------------------------- 1 | local nc = require("hs._asm.notificationcenter") 2 | 3 | local module = {} 4 | module.watcher = nc.workspaceObserver(function(n, o, i) 5 | if n == "NSWorkspaceActiveSpaceDidChangeNotification" or 6 | n == "NSWorkspaceActiveDisplayDidChangeNotification" then 7 | print(os.date().."\t".."name:"..n.."\tobj:"..inspect(o):gsub("%s+"," ").."\tinfo:"..inspect(i):gsub("%s+"," ")) 8 | end 9 | end):start() 10 | 11 | return module 12 | -------------------------------------------------------------------------------- /_scratch/t.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | sleep 15 3 | echo "begin" 4 | a=$(cat -) 5 | echo $a 6 | echo "end" -------------------------------------------------------------------------------- /_scratch/tablegrowth.lua: -------------------------------------------------------------------------------- 1 | -- simplified table expansion like the hs.doc `help` function, 2 | -- but without requiring the domain to be pre-filled 3 | 4 | local meta 5 | meta = { 6 | __index = function(self, key) 7 | local label = (self.__node == "") and key or (self.__node .. "." .. key) 8 | return setmetatable({ __action = self.__action, __node = label }, meta) 9 | end, 10 | 11 | __call = function(self, ...) 12 | if type(self.__action) == "function" then 13 | return self.__action(self.__node, ...) 14 | else 15 | return self.__node 16 | end 17 | end, 18 | 19 | __tostring = function(self) return self.__node end 20 | } 21 | 22 | return setmetatable({ __node = "" }, meta) 23 | -------------------------------------------------------------------------------- /_scratch/test.lua: -------------------------------------------------------------------------------- 1 | test = hs.hotkey.modal.new({"cmd","alt","ctrl"},"y") 2 | function test:entered() hs.alert.show("entered modal mode") end 3 | test:bind({"cmd"},"c", function() print("cmd-c down") end, nil, function() print("cmd-c repeat") end) 4 | test:bind({},"escape", function() test:exit() end) 5 | function test:exited() hs.alert.show("exiting modal mode") end -------------------------------------------------------------------------------- /_scratch/text.lua: -------------------------------------------------------------------------------- 1 | if a then a:delete() end 2 | result = nil 3 | cf = hs.fnutils.sortByKeys(hs.drawing.color.colorsFor("x11")) 4 | cName = nil 5 | 6 | for i,v in hs.fnutils.sortByKeys(hs.utf8.registeredKeys) do 7 | if not result then 8 | result = hs.styledtext.new("{", { 9 | font = { name = "Monaco", size = 12 }, 10 | color = { list = "Apple", name = "White" }, 11 | backgroundColor = { alpha = .75 } 12 | }) 13 | else 14 | result = result.."{" 15 | end 16 | s = #result + 1 17 | result = result..v 18 | e = #result 19 | result = result.."} "..i.."\n" 20 | if not cName then cName, _ = cf(nil) end 21 | result = result:setStyle({ color = { list = "x11", name = cName } }, s, e) 22 | cName, _ = cf(cName) 23 | end 24 | 25 | a = hs.drawing.text({x = 100, y = 50, w = 200, h = 800}, result):wantsLayer(true):show() 26 | -------------------------------------------------------------------------------- /_scratch/textStuff.lua: -------------------------------------------------------------------------------- 1 | local styleOne = { 2 | font = "Helvetica", 3 | size = 24, 4 | color = {}, 5 | } 6 | 7 | local textOne = "This is a test string!" 8 | 9 | local sizeOne = hs.drawing.getTextDrawingSize(textOne, styleOne) ; 10 | 11 | print(textOne, "h = "..sizeOne.h..", w = "..sizeOne.w) ; 12 | 13 | local d1 = hs.drawing.text({}, textOne):setSize(sizeOne): 14 | setTopLeft{x = 40, y = 40}:setTextStyle(styleOne):show() 15 | 16 | local d2 = hs.drawing.text({}, textOne):setSize(sizeOne): 17 | setTopLeft{x = 40, y = 80}:setTextStyle(styleOne): 18 | setTextStyle{lineBreak="clip"}:show() 19 | 20 | local d3 = hs.drawing.text({}, textOne):setSize(sizeOne): 21 | setTopLeft{x = 40, y = 120}:setTextStyle(styleOne): 22 | setTextStyle{alignment="justified"}:show() 23 | 24 | local d4 = hs.drawing.text({}, textOne):setSize(sizeOne): 25 | setTopLeft{x = 40, y = 160}:setTextStyle(styleOne): 26 | setTextStyle{lineBreak="truncateMiddle"}:show() 27 | 28 | sizeOne.w = sizeOne.w + 4 29 | 30 | local d11 = hs.drawing.text({}, textOne):setSize(sizeOne): 31 | setTopLeft{x = 260, y = 40}:setTextStyle(styleOne):show() 32 | 33 | local d12 = hs.drawing.text({}, textOne):setSize(sizeOne): 34 | setTopLeft{x = 260, y = 80}:setTextStyle(styleOne): 35 | setTextStyle{lineBreak="clip"}:show() 36 | 37 | local d13 = hs.drawing.text({}, textOne):setSize(sizeOne): 38 | setTopLeft{x = 260, y = 120}:setTextStyle(styleOne): 39 | setTextStyle{alignment="justified"}:show() 40 | 41 | local d14 = hs.drawing.text({}, textOne):setSize(sizeOne): 42 | setTopLeft{x = 260, y = 160}:setTextStyle(styleOne): 43 | setTextStyle(nil):show() 44 | 45 | sizeOne.w = sizeOne.w * 2 46 | 47 | local d21 = hs.drawing.text({}, textOne):setSize(sizeOne): 48 | setTopLeft{x = 40, y = 200}:setTextStyle(styleOne): 49 | setTextStyle{alignment="left", color={red=1}}:show() 50 | 51 | local d22 = hs.drawing.text({}, textOne):setSize(sizeOne): 52 | setTopLeft{x = 40, y = 240}:setTextStyle(styleOne): 53 | setTextStyle{alignment="center", color={blue=1}}:show() 54 | 55 | local d23 = hs.drawing.text({}, textOne):setSize(sizeOne): 56 | setTopLeft{x = 40, y = 280}:setTextStyle(styleOne): 57 | setTextStyle{alignment="right", color={green=1}}:show() 58 | 59 | sizeOne.w = sizeOne.w / 3 60 | 61 | local d31 = hs.drawing.text({}, textOne):setSize(sizeOne): 62 | setTopLeft{x = 40, y = 320}:setTextStyle(styleOne): 63 | setTextStyle{lineBreak="truncateHead", color={red=1, blue=1}}:show() 64 | 65 | local d32 = hs.drawing.text({}, textOne):setSize(sizeOne): 66 | setTopLeft{x = 190, y = 320}:setTextStyle(styleOne): 67 | setTextStyle{lineBreak="truncateMiddle", color={blue=1, green=1}}:show() 68 | 69 | local d33 = hs.drawing.text({}, textOne):setSize(sizeOne): 70 | setTopLeft{x = 340, y = 320}:setTextStyle(styleOne): 71 | setTextStyle{lineBreak="truncateTail", color={red=1, green=1}}:show() 72 | 73 | local styleTwo= { 74 | font = "Times", 75 | size = 36, 76 | color = {}, 77 | } 78 | 79 | local textTwo = "This is a multi-line test.\rThe second line is longer on purpose." 80 | 81 | local sizeTwo = hs.drawing.getTextDrawingSize(textTwo, styleTwo) ; 82 | 83 | print(textTwo, "h = "..sizeTwo.h..", w = "..sizeTwo.w) ; 84 | 85 | sizeTwo.w = sizeTwo.w + 4 86 | 87 | local d51 = hs.drawing.text({}, textTwo):setSize(sizeTwo): 88 | setTopLeft{x = 40, y = 360}:setTextStyle(styleTwo):show() 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /_scratch/textfieldExamples.lua: -------------------------------------------------------------------------------- 1 | local guitk = require("hs._asm.guitk") 2 | local styledtext = require("hs.styledtext") 3 | local eventtap = require("hs.eventtap") 4 | 5 | local module = {} 6 | 7 | local previousValue 8 | local editFn = function(textObj, msg, ...) 9 | local args = table.pack(...) 10 | local defaultReturn = args[#args] 11 | 12 | if msg == "keyPress" then 13 | local key = args[1] 14 | local mods = eventtap.checkKeyboardModifiers() 15 | 16 | if key == "return" then 17 | if not next(mods) then -- is the table empty? 18 | eventtap.keyStroke({}, "tab") 19 | return true 20 | elseif mods.shift and not mods.cmd and not mods.alt and not mods.ctrl and not mods.capslock and not mods.fn then 21 | eventtap.keyStroke({ "shift" }, "tab") 22 | return true 23 | end 24 | elseif key == "escape" and not next(mods) then 25 | if previousValue then 26 | textObj:value(previousValue) 27 | textObj:selectAll() 28 | return true -- technically not necessary for escape, but if that ever changes, this tells the invoking function that we took care of the escape key and it shouldn't 29 | end 30 | end 31 | elseif msg == "shouldBeginEditing" and defaultReturn then 32 | previousValue = textObj:value() 33 | elseif msg == "shouldEndEditing" and defaultReturn then 34 | previousValue = nil 35 | end 36 | -- return the default return value since we didn't return sooner 37 | return defaultReturn 38 | end 39 | 40 | local gui = guitk.new{x = 100, y = 100, h = 300, w = 300 }:show() 41 | local manager = guitk.manager.new() 42 | gui:contentManager(manager) 43 | 44 | manager:insert(guitk.element.textfield.newLabel("I am a label, not selectable"):tooltip("newLabel")) 45 | manager:insert(guitk.element.textfield.newLabel(styledtext.new({ 46 | "I am a StyledText selectable label", 47 | { starts = 8, ends = 13, attributes = { color = { red = 1 }, font = { name = "Helvetica-Bold", size = 12 } } }, 48 | { starts = 14, ends = 17, attributes = { color = { blue = 1 }, font = { name = "Helvetica-Oblique", size = 12 } } }, 49 | { starts = 19, ends = 28, attributes = { strikethroughStyle = styledtext.lineAppliesTo.word | styledtext.lineStyles.single } }, 50 | })):tooltip("newLabel with styledtext")) 51 | manager:insert(guitk.element.textfield.newTextField("I am a text field"):tooltip("newTextField"):editingCallback(editFn), { w = 200 }) 52 | manager:insert(guitk.element.textfield.newWrappingLabel("I am a wrapping label\nthe only difference so far is that I'm selectable"):tooltip("newWrappingLabel -- still trying to figure out what that means")) 53 | 54 | -- testing tab/shift-tab works; note if you're testing this before I create formal releases, this required a change to 55 | -- the root module (hs._asm.guitk) as well, so you'll need to recompile that too. 56 | manager:insert(guitk.element.textfield.newTextField("Another one!"):tooltip("added for tabbing"):editingCallback(editFn), { w = 200 }) 57 | manager:insert(guitk.element.textfield.newTextField("Another two!"):tooltip("and shift-tabbing"):editingCallback(editFn), { w = 200 }) 58 | 59 | module.manager = manager 60 | 61 | return module 62 | -------------------------------------------------------------------------------- /_scratch/touchbarWindowSlide.lua: -------------------------------------------------------------------------------- 1 | local touchbar = require("hs._asm.undocumented.touchbar") 2 | local canvas = require("hs.canvas") 3 | local window = require("hs.window") 4 | local screen = require("hs.screen") 5 | 6 | local module = {} 7 | 8 | local _c = canvas.new{x = 0, y = 0, h = 30, w = 150} 9 | _c[#_c + 1] = { 10 | type = "rectangle", 11 | action = "strokeAndFill", 12 | strokeColor = { white = 1 }, 13 | fillColor = { white = .25 }, 14 | roundedRectRadii = { xRadius = 5, yRadius = 5 }, 15 | } 16 | _c[#_c + 1] = { 17 | id = "zigzag", 18 | type = "segments", 19 | action = "stroke", 20 | strokeColor = { blue = 1, green = 1 }, 21 | coordinates = { 22 | { x = 0, y = 15 }, 23 | { x = 65, y = 15 }, 24 | { x = 70, y = 5 }, 25 | { x = 80, y = 25 }, 26 | { x = 85, y = 15 }, 27 | { x = 150, y = 15}, 28 | } 29 | } 30 | 31 | local _i = touchbar.item.newCanvas(_c, "zigzagCanvas"):canvasClickColor{ alpha = 0.0 } 32 | 33 | _c:canvasMouseEvents(true, true, false, true):mouseCallback(function(o,m,i,x,y) 34 | local max = _i:canvasWidth() 35 | local win = window.frontmostWindow() 36 | if not win then return end 37 | 38 | local screenFrame = screen.mainScreen():frame() 39 | local winFrame = win:frame() 40 | 41 | local newCenterPos = screenFrame.x + (x / max) * screenFrame.w 42 | local newWinX = newCenterPos - winFrame.w / 2 43 | 44 | if m == "mouseDown" or m == "mouseMove" then 45 | win:setTopLeft{ x = newWinX, y = winFrame.y } 46 | _c.zigzag.coordinates[2].x = x - 10 47 | _c.zigzag.coordinates[3].x = x - 5 48 | _c.zigzag.coordinates[4].x = x + 5 49 | _c.zigzag.coordinates[5].x = x + 10 50 | elseif m == "mouseUp" then 51 | _c.zigzag.coordinates[2].x = 65 52 | _c.zigzag.coordinates[3].x = 70 53 | _c.zigzag.coordinates[4].x = 80 54 | _c.zigzag.coordinates[5].x = 85 55 | -- elseif m == "mouseEnter" then 56 | -- elseif m == "mouseExit" then 57 | end 58 | end) 59 | 60 | local _b = touchbar.bar.new():templateItems{ _i }:defaultIdentifiers{ _i:identifier() }:presentModalBar(true) 61 | 62 | module.canvas = _c 63 | module.item = _i 64 | module.bar = _b 65 | 66 | return module 67 | -------------------------------------------------------------------------------- /_scratch/touchbartest1.lua: -------------------------------------------------------------------------------- 1 | tb = require("hs._asm.undocumented.touchbar") 2 | 3 | bar = tb.bar.new() 4 | 5 | items, allowed, required, default = {}, {}, {}, {} 6 | for i = 1, 10, 1 do 7 | local label = "item" .. tostring(i) 8 | table.insert(items, tb.item.newButton(label, hs.image.imageFromName(hs.image.systemImageNames.Bonjour), label):callback(function(item) 9 | print("Button " .. tostring(i) .. " was pressed") 10 | if i == 1 then bar:minimizeModalBar() end 11 | if i == 2 then bar:dismissModalBar() end 12 | end)) 13 | if i < 3 then table.insert(required, label) end 14 | if i < 5 then table.insert(default, label) end 15 | table.insert(allowed, label) 16 | end 17 | 18 | for k, v in pairs(tb.bar.builtInIdentifiers) do table.insert(allowed, v) end 19 | 20 | bar:templateItems(items) 21 | :customizableIdentifiers(allowed) 22 | :requiredIdentifiers(required) 23 | :defaultIdentifiers(default) 24 | :customizationLabel("sample") 25 | :escapeKeyReplacement("item3") 26 | -- :principleItem("item3") 27 | 28 | closeBox = true 29 | 30 | sampleCallback = function(self) 31 | self:presentModalBar(bar, closeBox) 32 | end 33 | -------------------------------------------------------------------------------- /_scratch/touchbartest2.lua: -------------------------------------------------------------------------------- 1 | tb = require("hs._asm.undocumented.touchbar") 2 | 3 | c = hs.canvas.new{x = 0, y = 0, h = 90, w = 90}:canvasMouseEvents(true,true,true,true) 4 | :mouseCallback(function(o,m,i,x,y) print(timestamp(),m,i,x,y) end) 5 | c[#c + 1] = { 6 | type = "circle", 7 | radius = tostring(1/3), 8 | center = { x =tostring(1/2), y = tostring(2/3 - math.sin(math.rad(60)) * 1/3) }, 9 | fillColor = { red = 1, alpha = .5 }, 10 | } 11 | c[#c + 1] = { 12 | type = "circle", 13 | radius = tostring(1/3), 14 | center = { x = tostring(1/3), y = tostring(2/3) }, 15 | fillColor = { green = 1, alpha = .5 }, 16 | } 17 | c[#c + 1] = { 18 | type = "circle", 19 | radius = tostring(1/3), 20 | center = { x = tostring(2/3), y = tostring(2/3) }, 21 | fillColor = { blue = 1, alpha = .5 }, 22 | } 23 | 24 | idle = hs.canvas.new{h = 100, w = 100} 25 | idle[#idle + 1] = { 26 | type = "rectangle", 27 | action = "fill", 28 | fillColor = { green = 1 }, 29 | frame = { x = "0%", y = "0%", h = "100%", w = "33%" }, 30 | id = "idle", 31 | } 32 | idle[#idle + 1] = { 33 | type = "rectangle", 34 | action = "fill", 35 | fillColor = { red = 1 }, 36 | frame = { x = "33%", y = "0%", h = "100%", w = "33%" }, 37 | id = "user", 38 | } 39 | idle[#idle + 1] = { 40 | type = "rectangle", 41 | action = "fill", 42 | fillColor = { green = 1, red = 1 }, 43 | frame = { x = "66%", y = "0%", h = "100%", w = "33%" }, 44 | id = "system", 45 | } 46 | idle[#idle + 1] = { 47 | type = "rectangle", 48 | action = "stroke", 49 | strokeColor = { white = 1 }, 50 | } 51 | 52 | local state = true 53 | idleCallback = function(o) 54 | state = not state 55 | if state then 56 | idle:transformation(nil) 57 | else 58 | local w = o:canvasWidth() / 2 59 | local h = 15 -- we know that the touchbar gives us a hight of 30 to work with, since we need half for rotation, use 15 here 60 | idle:transformation(hs.canvas.matrix.translate(w, h):rotate(90):translate(-w, -h)) 61 | end 62 | end 63 | 64 | idleTimer = hs.timer.doEvery(1, function() 65 | local cpu = hs.host.cpuUsage().overall 66 | idle["idle"].frame.y, idle["idle"].frame.h = tostring(100 - cpu.idle) .. "%", tostring(cpu.idle) .. "%" 67 | idle["user"].frame.y, idle["user"].frame.h = tostring(100 - cpu.user) .. "%", tostring(cpu.user) .. "%" 68 | idle["system"].frame.y, idle["system"].frame.h = tostring(100 - cpu.system) .. "%", tostring(cpu.system) .. "%" 69 | end) 70 | 71 | callbackFN = function(o, ...) print(timestamp(), o:identifier(), finspect(table.pack(...))) end 72 | sliderFN = function(o, v) 73 | callbackFN(o, v) 74 | if v == "minimum" then o:sliderValue(-100) ; v = o:sliderValue() end 75 | if v == "maximum" then o:sliderValue(100) ; v = o:sliderValue() end 76 | end 77 | visibilityCallbackFN = function(o, state) print("touchbar goes " .. (state and "on" or "off")) end 78 | b = tb.bar.new():visibilityCallback(visibilityCallbackFN) 79 | i = { 80 | -- minimizing (i.e. hitting the built in close button) a touchbar doesn't trigger visible change... maybe once they're attached to webviews, etc. 81 | tb.item.newButton(hs.image.imageFromName("NSStopProgressFreestandingTemplate"), "stop"):callback(function(o, ...) b:dismissModalBar() end), 82 | tb.item.newButton("text", "textButtonItem"):callback(callbackFN), 83 | tb.item.newCanvas(c, "canvasItem"), --:callback(callbackFN), 84 | tb.item.newGroup("groupItem"):groupItems{ 85 | tb.item.newButton(hs.image.imageFromName("NSStatusAvailable"), "available"):callback(callbackFN), 86 | tb.item.newButton(hs.image.imageFromName("NSStatusPartiallyAvailable"), "partiallyAvailable"):callback(callbackFN), 87 | tb.item.newButton(hs.image.imageFromName("NSStatusUnavailable"), "unavailable"):callback(callbackFN), 88 | }, 89 | tb.item.newCanvas(idle, "idle"):callback(idleCallback):canvasClickColor{ alpha = 0 }, 90 | tb.item.newSlider("sliderItem"):callback(sliderFN) -- :sliderMin(-100):sliderMax(100):sliderValue(0) 91 | :sliderMinImage(hs.image.imageFromName("NSExitFullScreenTemplate")) 92 | :sliderMaxImage(hs.image.imageFromName("NSEnterFullScreenTemplate")), 93 | } 94 | b:templateItems(i):defaultIdentifiers(hs.fnutils.imap(i, function(o) return o:identifier() end)) 95 | 96 | b:presentModalBar(false) 97 | 98 | -- for poking around at the objective-c objects more directly, not needed for the module or demonstrations 99 | if package.searchpath("hs._asm.objc", package.path) then 100 | o = require("hs._asm.objc") 101 | j = hs.fnutils.imap(i, function(x) return o.object.fromLuaObject(x) end) 102 | end 103 | -------------------------------------------------------------------------------- /_scratch/touchbartest3.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- An example of creating an image from the touch bar even if the virtual touchbar is not currently visible/running 3 | -- 4 | -- This should work on Mac's that actually have a touchbar as well, but since I can't test it myself, I'll have to 5 | -- wait until publicly announcing the latest round of updates to confirm. 6 | -- 7 | 8 | local tb = require("hs._asm.undocumented.touchbar") 9 | 10 | local mb = tb.new():streaming(true) 11 | 12 | -- we need to wait until streaming can begin and a full context can be generated, maybe 13 | -- a second or two... 14 | local zz 15 | hs.timer.waitUntil(function() 16 | zz = mb:image() 17 | return zz 18 | end, function() 19 | local sz = zz:size() 20 | local fr = { x = 100, y = 100, h = sz.h + 20, w = sz.w + 20 } 21 | 22 | nd = hs.canvas.new(fr):show() 23 | nd[#nd + 1] = { 24 | type = "rectangle" 25 | } 26 | nd[#nd + 1] = { 27 | type = "image", 28 | image = zz, 29 | frame = { x = 10, y = 10, h = sz.h, w = sz.w } 30 | } 31 | 32 | ndt = hs.timer.doEvery(1, function() 33 | nd[2].image = mb:image() 34 | end) 35 | end) 36 | -------------------------------------------------------------------------------- /_scratch/trackpadDraw.lua: -------------------------------------------------------------------------------- 1 | local touchdevice = require("hs._asm.undocumented.touchdevice") 2 | local canvas = require("hs.canvas") 3 | local eventtap = require("hs.eventtap") 4 | local mouse = require("hs.mouse") 5 | local events = eventtap.event.types 6 | 7 | local module = {} 8 | 9 | -- eventtap can prevent mouseDown/Up but it can't prevent mouseMove 10 | 11 | -------------------------------------------------------------------------------- /_scratch/usage.lua: -------------------------------------------------------------------------------- 1 | local battery = require("hs.battery") 2 | local menu = require("hs.menubar") 3 | local styledtext = require("hs.styledtext") 4 | 5 | local module = {} 6 | 7 | local adjustTitle = function() 8 | if module.menu then 9 | local text = tostring(battery.amperage()) .. "\n" 10 | if battery.isCharging() then 11 | text = text .. tostring(battery.timeToFullCharge()) 12 | else 13 | text = text .. tostring(battery.timeRemaining()) 14 | end 15 | module.menu:setTitle(styledtext.new(text, { 16 | font = { 17 | name = "Menlo", 18 | size = 9 19 | }, 20 | paragraphStyle = { 21 | alignment = "center", 22 | }, 23 | })) 24 | else 25 | if module.watcher then 26 | module.watcher:stop() 27 | module.watcher = nil 28 | end 29 | end 30 | end 31 | 32 | module.menu = menu.newWithPriority(999) 33 | module.watcher = battery.watcher.new(adjustTitle):start() 34 | adjustTitle() 35 | return module 36 | -------------------------------------------------------------------------------- /_scratch/viKeys.lua: -------------------------------------------------------------------------------- 1 | local module = {} 2 | 3 | module.debugging = false -- whether to print status updates 4 | 5 | local eventtap = require "hs.eventtap" 6 | local event = eventtap.event 7 | local inspect = require "hs.inspect" 8 | 9 | local keyHandler = function(e) 10 | local watchFor = { h = "left", j = "down", k = "up", l = "right" } 11 | local actualKey = e:getCharacters(true) 12 | local replacement = watchFor[actualKey:lower()] 13 | if replacement then 14 | local isDown = e:getType() == event.types.keyDown 15 | local flags = {} 16 | for k, v in pairs(e:getFlags()) do 17 | if v and k ~= "ctrl" then -- ctrl will be down because that's our "wrapper", so ignore it 18 | table.insert(flags, k) 19 | end 20 | end 21 | if module.debugging then print("viKeys: " .. replacement, inspect(flags), isDown) end 22 | local replacementEvent = event.newKeyEvent(flags, replacement, isDown) 23 | if isDown then 24 | -- allow for auto-repeat 25 | replacementEvent:setProperty(event.properties.keyboardEventAutorepeat, e:getProperty(event.properties.keyboardEventAutorepeat)) 26 | end 27 | return true, { replacementEvent } 28 | else 29 | return false -- do nothing to the event, just pass it along 30 | end 31 | end 32 | 33 | local modifierHandler = function(e) 34 | local flags = e:getFlags() 35 | local onlyControlPressed = false 36 | for k, v in pairs(flags) do 37 | onlyControlPressed = v and k == "ctrl" 38 | if not onlyControlPressed then break end 39 | end 40 | -- you must tap and hold ctrl by itself to turn this on 41 | if onlyControlPressed and not module.keyListener then 42 | if module.debugging then print("viKeys: keyhandler on") end 43 | module.keyListener = eventtap.new({ event.types.keyDown, event.types.keyUp }, keyHandler):start() 44 | -- however, adding additional modifiers afterwards is ok... its only when ctrl isn't down that we switch back off 45 | elseif not flags.ctrl and module.keyListener then 46 | if module.debugging then print("viKeys: keyhandler off") end 47 | module.keyListener:stop() 48 | module.keyListener = nil 49 | end 50 | return false 51 | end 52 | 53 | module.modifierListener = eventtap.new({ event.types.flagsChanged }, modifierHandler) 54 | 55 | module.start = function() 56 | module.modifierListener:start() 57 | end 58 | 59 | module.stop = function() 60 | if module.keyListener then 61 | module.keyListener:stop() 62 | module.keyListener = nil 63 | end 64 | module.modifierListener:stop() 65 | end 66 | 67 | module.start() -- autostart 68 | 69 | return module 70 | -------------------------------------------------------------------------------- /_scratch/webviewOtherURLS.lua: -------------------------------------------------------------------------------- 1 | local linkToHtml = [[ 2 | 3 | 4 | 5 | 6 | Hammerspoon link example 7 | 8 | 10 | 12 | 13 | 14 | 15 |
16 |
17 |

18 | Meeting 19 |

20 |

21 | Lunch 22 |

23 |

24 | Tea 25 |

26 |

27 | Out 28 |

29 |
30 |
31 | 40 | 41 | 42 | ]] 43 | 44 | hs.hotkey.bind({"cmd", "alt", "ctrl", "shift"}, "W", function() 45 | showButtonsWebView() 46 | end) 47 | 48 | function showButtonsWebView() 49 | local rect = hs.geometry.rect({1, 1, 800, 200}) 50 | local web = hs.webview.new(rect, {developerExtrasEnabled = true}) 51 | web:windowStyle({'closable', 'titled', 'resizable'}) 52 | -- web:url(linkToHtml) 53 | web:html(linkToHtml) 54 | web:allowTextEntry(true) 55 | web:closeOnEscape(true) 56 | web:show() 57 | end 58 | 59 | function breakReportCallback(eventName, params) 60 | hs.alert.show(params.breakType) 61 | end 62 | hs.urlevent.bind('break-report', breakReportCallback) 63 | -------------------------------------------------------------------------------- /_scratch/zerobrane.lua: -------------------------------------------------------------------------------- 1 | local application = require "hs.application" 2 | application.launchOrFocusByBundleID("com.ZeroBrane.ZeroBraneStudio") 3 | 4 | local ZBS = "/Applications/_ASM_/Developer/ZeroBraneStudio.app/Contents/ZeroBraneStudio" 5 | package.path = package.path .. ";" .. ZBS .. "/lualibs/?/?.lua;" .. ZBS .. "/lualibs/?.lua" 6 | package.cpath = package.cpath .. ";" .. ZBS .. "/bin/?.dylib;" .. ZBS .. "/bin/clibs53/?.dylib" 7 | hs.mobdebug = require "mobdebug" 8 | 9 | local timer = require "hs.timer" 10 | 11 | local waitCount = 0 12 | timer.waitUntil(function() 13 | local proceed = false 14 | for i, v in ipairs(application.runningApplications()) do 15 | proceed = v:bundleID() == "com.ZeroBrane.ZeroBraneStudio" 16 | if proceed then break end 17 | end 18 | waitCount = waitCount + 1 19 | if waitCount > 10 then 20 | print("~~ application launch timeout") 21 | proceed = true 22 | end 23 | return proceed 24 | end, function(t) 25 | hs.mobdebug.start() 26 | end) 27 | 28 | return hs.mobdebug 29 | -------------------------------------------------------------------------------- /functionAsBeziers.lua: -------------------------------------------------------------------------------- 1 | local module = {} 2 | local canvas = require("hs._asm.canvas") 3 | local matrix = canvas.matrix 4 | 5 | local calculateControlPoints = function(xy0, xy1, xy2, t) 6 | -- see http://scaledinnovation.com/analytics/splines/aboutSplines.html 7 | -- 8 | -- x0,y0,x1,y1 are the coordinates of the end (knot) pts of this segment 9 | -- x2,y2 is the next knot -- not connected here but needed to calculate p2 10 | -- p1 is the control point calculated here, from x1 back toward x0. 11 | -- p2 is the next control point, calculated here and returned to become the 12 | -- next segment's p1. 13 | -- t is the 'tension' which controls how far the control points spread. 14 | 15 | -- Scaling factors: distances from this knot to the previous and following knots. 16 | local d01 = math.sqrt((xy1.x - xy0.x) ^ 2 + (xy1.y - xy0.y) ^ 2) 17 | local d12 = math.sqrt((xy2.x - xy1.x) ^ 2 + (xy2.y - xy1.y) ^ 2) 18 | 19 | local fa = t * d01 / (d01 + d12) 20 | local fb = t - fa 21 | 22 | local p1 = { 23 | x = xy1.x + fa * (xy0.x - xy2.x), 24 | y = xy1.y + fa * (xy0.y - xy2.y) 25 | } 26 | 27 | local p2 = { 28 | x = xy1.x - fb * (xy0.x - xy2.x), 29 | y = xy1.y - fb * (xy0.y - xy2.y) 30 | } 31 | 32 | return p1, p2 33 | end 34 | 35 | local scaleCoordinates = function(coords, frame) 36 | coords = coords or {} 37 | 38 | local maxX, maxY = math.mininteger, math.mininteger 39 | local minX, minY = math.maxinteger, math.maxinteger 40 | for c, v in ipairs(coords) do 41 | maxX, maxY = math.max(v.x or 0.0, maxX), math.max(v.y or 0.0, maxY) 42 | minX, minY = math.min(v.x or 0.0, minX), math.min(v.y or 0.0, minY) 43 | end 44 | 45 | local xSpan, ySpan = maxX - minX, maxY - minY 46 | local results = {} 47 | for c, v in ipairs(coords) do 48 | local newPoint = {} 49 | for l, n in pairs(v) do 50 | if l:match("x$") then 51 | newPoint[l] = n * frame.w / xSpan 52 | elseif l:match("y$") then 53 | newPoint[l] = n * frame.h / ySpan 54 | end 55 | end 56 | table.insert(results, newPoint) 57 | end 58 | return results 59 | end 60 | 61 | module.coordinateSet = function(fn, start, stop, inc) 62 | fn = fn or math.sin 63 | start = start or 0.0 64 | stop = stop or 2 * math.pi 65 | inc = inc or .1 66 | 67 | local coords = {} 68 | for i = start, stop, inc do table.insert(coords, { x = i, y = fn(i) }) end 69 | if (stop - start) % inc ~= 0.0 then 70 | table.insert(coords, { x = stop, y = fn(stop) }) 71 | end 72 | return coords 73 | end 74 | 75 | module.addControlPointsToCoordinates = function(coords, t) 76 | t = t or .5 77 | local cp = {} 78 | for i = 1, #coords - 2, 1 do 79 | local p1, p2 = calculateControlPoints(coords[i], coords[i + 1], coords[i + 2], t) 80 | table.insert(cp, p1) 81 | table.insert(cp, p2) 82 | end 83 | local results = { coords[1] } 84 | for i = 2, #coords - 1, 1 do 85 | table.insert(results, { 86 | x = coords[i].x, 87 | y = coords[i].y, 88 | c1x = cp[(i - 2) * 2 + 1].x, 89 | c1y = cp[(i - 2) * 2 + 1].y, 90 | c2x = cp[(i - 2) * 2 + 2].x, 91 | c2y = cp[(i - 2) * 2 + 2].y, 92 | }) 93 | end 94 | table.insert(results, { 95 | x = coords[#coords].x, 96 | y = coords[#coords].y, 97 | c1x = cp[#cp].x, 98 | c1y = cp[#cp].y, 99 | c2x = cp[#cp].x, 100 | c2y = cp[#cp].y, 101 | }) 102 | return results 103 | end 104 | 105 | module.coordinatesForCanvasElementForFofX = function(frame, fn, start, stop, inc, tension) 106 | local coords = module.coordinateSet(fn, start, stop, inc) 107 | coords = module.addControlPointsToCoordinates(coords, tension) 108 | return scaleCoordinates(coords, frame) 109 | end 110 | 111 | module.canvasElementForFofX = function(frame, fn, start, stop, inc, tension) 112 | local coordinates = module.coordinatesForCanvasElementForFofX(frame, fn, start, stop, inc, tension) 113 | return canvas.new(frame):insertElement{ 114 | type = "segments", 115 | action = "stroke", 116 | transformation = matrix.translate(-1 * coordinates[1].x, frame.h / 2):scale(1, -1), 117 | coordinates = coordinates, 118 | } 119 | end 120 | 121 | return module -------------------------------------------------------------------------------- /geekery.lua: -------------------------------------------------------------------------------- 1 | local geekery = _asm._actions.geeklets 2 | local screen = require("hs.screen") 3 | local stext = require("hs.styledtext") 4 | local fnutils = require("hs.fnutils") 5 | local drawing = require("hs.drawing") 6 | 7 | local monitorTopY = screen.mainScreen():frame().y 8 | local monitorBotY = monitorTopY + screen.mainScreen():frame().h 9 | 10 | --geekery.registerShellGeeklet("cpu", 15, "geeklets/system.sh", 11 | -- geekery.registerLuaGeeklet("cpu", 5, "geeklets/system.lua", 12 | -- { x = 22, y = monitorTopY + 22, h = 60, w = 350}, { color = { alpha = 1 }, skip = true }, 13 | -- { drawing.rectangle{ x = 12, y = monitorTopY + 12, h = 80, w = 370 } 14 | -- :setFillColor{ alpha=.7, white = .5 } 15 | -- :setStrokeColor{ alpha=.5 } 16 | -- :setFill(true) 17 | -- :setRoundedRectRadii(5,5) 18 | -- }):start() 19 | 20 | geekery.registerShellGeeklet("wifi", 60, "geeklets/wifi.sh", 21 | { x = 22, y = monitorTopY + 102, h = 60, w = 350}, { 22 | color = { alpha = 1 }, 23 | paragraphStyle = { lineBreak = "clip" } 24 | }, { drawing.rectangle{ x = 12, y = monitorTopY + 92, h = 80, w = 370 } 25 | :setFillColor{ alpha=.7, white = .5 } 26 | :setStrokeColor{ alpha=.5 } 27 | :setFill(true) 28 | :setRoundedRectRadii(5,5) 29 | }):start() 30 | 31 | geekery.registerLuaGeeklet("hwm_check", 60, "geeklets/hwm_check.lua", 32 | { x = 22, y = monitorTopY + 182, h = 20, w = 350}, { color = { alpha = 1 } }, 33 | { drawing.rectangle{ x = 12, y = monitorTopY + 172, h = 40, w = 370 } 34 | :setFillColor{ alpha=.7, white = .5 } 35 | :setStrokeColor{ alpha=.5 } 36 | :setFill(true) 37 | :setRoundedRectRadii(5,5) 38 | }):start() 39 | 40 | local geekletRemoteCheck = function() 41 | local result = stext.new("") 42 | for i,v in fnutils.sortByKeys(_asm._actions.remoteCheck.output) do 43 | result = result..v.."\n" 44 | end 45 | return result 46 | end 47 | 48 | geekery.registerLuaGeeklet("remoteCheck", 300, geekletRemoteCheck, 49 | { x = 400, y = monitorTopY + 22, h = 56 * 3, w = 400 }, { skip = true }, 50 | { drawing.rectangle{x = 390, y = monitorTopY + 12, h = 200, w = 420 } 51 | :setFillColor{ alpha=.7, white = .5 } 52 | :setStrokeColor{ alpha=.5 } 53 | :setFill(true) 54 | :setRoundedRectRadii(5,5) 55 | }):start() 56 | 57 | -- geekery.registerLuaGeeklet("mailCheck", 30, _asm._actions.mailCheck.outputLine, 58 | -- { x = 232, y = monitorBotY - 48, h = 36, w = 300 }, { skip = true }, 59 | -- { drawing.rectangle{x = 222, y = monitorBotY - 56, h = 56, w = 320 } 60 | -- :setFillColor{ alpha=.7, white = .5 } 61 | -- :setStrokeColor{ alpha=.5 } 62 | -- :setFill(true) 63 | -- :setRoundedRectRadii(5,5) 64 | -- }):start() 65 | 66 | -- local geekletClock = function() 67 | -- local self = geekery.geeklets.clock 68 | -- local screenFrame = screen.mainScreen():fullFrame() 69 | -- local clockTime = os.date("%I:%M:%S %p") 70 | -- local clockPos = drawing.getTextDrawingSize(clockTime, self.textStyle) 71 | -- clockPos.w = clockPos.w + 4 72 | -- clockPos.x = screenFrame.x + screenFrame.w - (clockPos.w + 4) 73 | -- clockPos.y = screenFrame.y + screenFrame.h - (clockPos.h + 4) 74 | -- local clockBlockPos = { 75 | -- x = clockPos.x - 3, 76 | -- y = clockPos.y, 77 | -- h = clockPos.h + 3, 78 | -- w = clockPos.w + 6, 79 | -- } 80 | -- self.drawings[2]:setFrame(clockBlockPos) 81 | -- self.drawings[1]:setFrame(clockPos) 82 | -- return clockTime 83 | -- end 84 | -- 85 | -- geekery.registerLuaGeeklet("clock", 1, geekletClock, { }, { 86 | -- font = { name = "Menlo-Italic", size = 12, }, 87 | -- color = { red=.75, blue=.75, green=.75, alpha=.75}, 88 | -- paragraphStyle = { alignment = "center", lineBreak = "clip" } 89 | -- }, { 90 | -- drawing.rectangle{}:setStroke(true) 91 | -- :setStrokeColor({ red=.75, blue=.75, green=.75, alpha=.75}) 92 | -- :setFill(true) 93 | -- :setFillColor({alpha=.75}) 94 | -- :setRoundedRectRadii(5,5) 95 | -- }):hover(true):start() 96 | -- geekery.geeklets.clock.hoverlock = true 97 | 98 | geekery.startUpdates() 99 | -------------------------------------------------------------------------------- /geeklets/wifi.lua: -------------------------------------------------------------------------------- 1 | local configuration = require("hs.network.configuration") 2 | local reachability = require("hs.network.reachability") 3 | 4 | local watchable = require"hs.watchable" 5 | 6 | local http = require("hs.http") 7 | 8 | local output = { 9 | external = "unknown", 10 | primary = { 11 | interface = "unknown", 12 | address = "0.0.0.0", 13 | name = nil, 14 | }, 15 | secondary = { 16 | interface = "unknown", 17 | address = "0.0.0.0", 18 | name = nil, 19 | }, 20 | } 21 | 22 | local module = {} 23 | 24 | local cstore = configuration.open() 25 | 26 | local updateExternalAddress = function(w, p, i, oldValue, value) 27 | if value then 28 | module._getExternalAddress = http.asyncGet("http://eth0.me", nil, function(s, b, h) 29 | output.external = b 30 | end) 31 | else 32 | output.external = "NO INTERNET CONNECTION" 33 | end 34 | end 35 | 36 | module.watchInternetStatus = watchable.watch("generalStatus.internet", updateExternalAddress) 37 | 38 | module.cstore = cstore 39 | module.output = output 40 | 41 | updateExternalAddress(nil, nil, nil, nil, module.watchInternetStatus:value()) 42 | 43 | return module 44 | -------------------------------------------------------------------------------- /geeklets/wifi.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | ##################### 4 | # WIFI Network Code # 5 | ##################### 6 | 7 | # Script to get the current ip addresses in use 8 | # N.B. this was written for my Macbook Pro and iMac. 9 | # Check the en0, en1, etc assignments for a Mac Pro 10 | # which has 2 ethernet ports, not 1, also 11 | # Macbook Air has no ethernet port 12 | 13 | #SMC 1.5 and EFI 2.3 Updates (Nov. 2011) have killed this method for obtaining SSID info 14 | #wifi_network=`system_profiler -detailLevel basic SPAirPortDataType | head -25 | tail -1 | awk '{print $1}' | sed "s/://"` 15 | 16 | wifi_network=`/System/Library/PrivateFrameworks/Apple80211.framework/Versions/A/Resources/airport -I | awk -F: '/ SSID: / {print $2}' | sed -e 's/SSID: //' | sed -e 's/ //'` 17 | 18 | wifi=`ifconfig en0 | grep "broadcast" | awk '{print $2}'` 19 | ethe=`ifconfig en3 | grep "broadcast" | awk '{print $2}'` 20 | 21 | if [ "$wifi" != "" ] 22 | then 23 | echo "🔵 Wireless ip: $wifi on $wifi_network" 24 | else 25 | echo "🔴 Wireless ip: NO CONNECTION" 26 | fi 27 | 28 | if [ "$ethe" != "" ] 29 | then 30 | echo "🔵 Ethernet ip: $ethe" 31 | else 32 | echo "🔴 Ethernet ip: NO CONNECTION" 33 | fi 34 | 35 | #external=`curl --silent http://checkip.dyndns.org | awk '{print $6}' | cut -f 1 -d '<'` 36 | #external=`curl --silent ifcfg.me` 37 | external=`dig +short myip.opendns.com @resolver1.opendns.com` 38 | if [ "$external" != "" ] 39 | then 40 | echo "🌎 External ip: $external" 41 | else 42 | echo "🔴 NO INTERNET CONNECTION" 43 | fi 44 | -------------------------------------------------------------------------------- /hammer-build: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # This is the script I use to build interim release builds of Hammerspoon 4 | # 5 | # You'll probably want to adjust the paths somewhat as these are for my layout. 6 | # 7 | # invoke this script with no arguments to just build Hammerspoon 8 | # invoke this script with a single argument that starts with "doc" (e.g. documentation, docs, doc, etc.) to just build the latest versions of the documentation. 9 | # invoke it with any other single argument to build both Hammerspoon and the documentation. 10 | # 11 | # This script uses a self-signed certificate so that debug builds do not require you to have to set Accessibility each time you rebuild the app. You can find more information about this at: 12 | # https://github.com/Hammerspoon/hammerspoon/blob/a62c879bdbfc6646aebba1f943fe32fee817fbfd/CONTRIBUTING.md and http://bd808.com/blog/2013/10/21/creating-a-self-signed-code-certificate-for-xcode/ 13 | 14 | cd /opt/amagill/src/hammerspoon/hammerspoon 15 | 16 | GITBRANCH=$(git symbolic-ref --short -q HEAD)-$(git log -1 --format="%h") 17 | 18 | if [[ $1 != doc* ]]; then 19 | make clean 20 | make || exit 21 | 22 | echo $GITBRANCH > build/Hammerspoon.app/Contents/Resources/gitbranch 23 | 24 | xattr -cr "build/Hammerspoon.app" # codesign has become more anal in 10.12 25 | # signing with self-signed cert so I no longer have to reset accessibility all the time 26 | codesign --verbose --sign "Datalore Code Signing" "build/Hammerspoon.app/Contents/Frameworks/LuaSkin.framework/Versions/A" 27 | codesign --verbose --sign "Datalore Code Signing" "build/Hammerspoon.app" 28 | 29 | rm -fr `xcodebuild -workspace Hammerspoon.xcworkspace -scheme Hammerspoon -configuration DEBUG -showBuildSettings | sort | uniq | grep " BUILT_PRODUCTS_DIR =" | awk '{ print $3 }'`/Hammerspoon.app 30 | fi 31 | 32 | if [ $1 ]; then 33 | make docs 34 | # make build/html/LuaSkin 35 | # hack because headerdoc2html doesn't seem to recognise @code/@endcode and @remark anymore 36 | # classDir=build/html/LuaSkin 37 | # sed -e 's/@code/
@textblock/' \
38 | #       -e 's/@endcode/@\/textblock<\/pre>/' \
39 | #       -e 's/@remark/@attributeblock Notes/' LuaSkin/LuaSkin/Skin.h > Skin.h
40 | #   headerdoc2html -u -o ${classDir} Skin.h
41 | #   resolveLinks ${classDir}
42 | #   mv ${classDir}/Skin_h/* ${classDir}
43 | #   rmdir ${classDir}/Skin_h
44 | #   rm Skin.h
45 | 
46 |     rm -fr ~/Documents/Hammerspoon
47 |     mkdir ~/Documents/Hammerspoon
48 |     mv build/Hammerspoon.docset ~/Documents/Hammerspoon/
49 | #   mv build/html/LuaSkin ~/Documents/Hammerspoon/
50 |     /usr/local/bin/appledoc \
51 |         --project-name "LuaSkin" \
52 |         --project-company "Hammerspoon" \
53 |         --company-id "org.hammerspoon" \
54 |         --output "build/LSDocs" \
55 |         --clean-output \
56 |         --no-install-docset \
57 |         --no-publish-docset \
58 |         --logformat xcode \
59 |         --keep-undocumented-objects \
60 |         --keep-undocumented-members \
61 |         --keep-intermediate-files \
62 |         --ignore "*.m" \
63 |         LuaSkin/LuaSkin
64 |     mv build/LSDocs/html ~/Documents/Hammerspoon/LuaSkin
65 | fi
66 | 
67 | if [[ $1 != doc* ]]; then
68 |     osascript -e 'tell Application "Hammerspoon" to quit'
69 |     sleep 1
70 |     rm -fr /Applications/_ASM_/Developer/Hammerspoon.app
71 |     mv build/Hammerspoon.app /Applications/_ASM_/Developer/
72 | 
73 |     open -a /Applications/_ASM_/Developer/Hammerspoon.app
74 | fi
75 | 
76 | 


--------------------------------------------------------------------------------
/resources/timebg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/asmagill/hammerspoon-config/432c65705203d7743d3298441bd4319137b466fd/resources/timebg.png


--------------------------------------------------------------------------------
/test.lua:
--------------------------------------------------------------------------------
  1 | if not timelapsetopleft then timelapsetopleft = {980,400} end
  2 | 
  3 | timelapsed_canvas = hs.canvas.new({x=timelapsetopleft[1],y=timelapsetopleft[2],w=280,h=125}):show()
  4 | timelapsed_canvas:behavior(hs.canvas.windowBehaviors.canJoinAllSpaces)
  5 | -- timelapsed_canvas:level(hs.canvas.windowLevels.desktopIcon)
  6 | timelapsed_canvas:level(hs.canvas.windowLevels.screenSaver)
  7 | -- canvas background
  8 | timelapsed_canvas[1] = {
  9 |     action = "fill",
 10 |     type = "rectangle",
 11 |     fillColor = black,
 12 |     roundedRectRadii = {xRadius=5, yRadius=5},
 13 | }
 14 | timelapsed_canvas[1].fillColor.alpha = .2
 15 | -- title
 16 | timelapsed_canvas[2] = {
 17 |     type = "text",
 18 |     text = "Time Elapsed",
 19 |     textSize = 14,
 20 |     textColor = white,
 21 |     frame = {
 22 |         x = tostring(10/280),
 23 |         y = tostring(10/125),
 24 |         w = tostring(260/280),
 25 |         h = tostring(25/125),
 26 |     }
 27 | }
 28 | timelapsed_canvas[2].textColor.alpha = .3
 29 | -- time
 30 | timelapsed_canvas[3] = {
 31 |     type = "text",
 32 |     text = "",
 33 |     textColor = {hex="#A6AAC3"},
 34 |     textSize = 17,
 35 |     textAlignment = "center",
 36 |     frame = {
 37 |         x = tostring(0/280),
 38 |         y = tostring(35/125),
 39 |         w = tostring(280/280),
 40 |         h = tostring(25/125),
 41 |     }
 42 | }
 43 | -- indicator background
 44 | timelapsed_canvas[4] = {
 45 |     type = "image",
 46 |     image = hs.image.imageFromPath("./resources/timebg.png"),
 47 |     frame = {
 48 |         x = tostring(10/280),
 49 |         y = tostring(65/125),
 50 |         w = tostring(260/280),
 51 |         h = tostring(50/125),
 52 |     }
 53 | }
 54 | -- light indicator
 55 | timelapsed_canvas[5] = {
 56 |     action = "fill",
 57 |     type = "rectangle",
 58 |     fillColor = white,
 59 |     frame = {
 60 |         x = tostring(20/280),
 61 |         y = tostring(75/125),
 62 |         w = tostring(240/280),
 63 |         h = tostring(20/125),
 64 |     }
 65 | }
 66 | timelapsed_canvas[5].fillColor.alpha = .2
 67 | -- indicator mask
 68 | timelapsed_canvas[6] = {
 69 |     action = "fill",
 70 |     type = "rectangle",
 71 |     frame = {
 72 |         x = tostring(20/280),
 73 |         y = tostring(75/125),
 74 |         w = tostring(240/280),
 75 |         h = tostring(20/125),
 76 |     }
 77 | }
 78 | -- color indicator
 79 | timelapsed_canvas[7] = {
 80 |     action = "fill",
 81 |     type = "rectangle",
 82 |     frame = {
 83 |         x = tostring(20/280),
 84 |         y = tostring(75/125),
 85 |         w = tostring(240/280),
 86 |         h = tostring(20/125),
 87 |     },
 88 |     fillGradient="linear",
 89 |     fillGradientColors = {
 90 |         {hex = "#00A0F7"},
 91 |         {hex = "#92D2E5"},
 92 |         {hex = "#4BE581"},
 93 |         {hex = "#EAF25E"},
 94 |         {hex = "#F4CA55"},
 95 |         {hex = "#E04E4E"},
 96 |     },
 97 | }
 98 | timelapsed_canvas[7].compositeRule = "sourceAtop"
 99 | 
100 | function updateElapsedCanvas()
101 |    local nowtable = os.date("*t")
102 |    local nowyday = nowtable.yday
103 |    local nowhour = string.format("%2s", nowtable.hour)
104 |    local nowmin = string.format("%2s", nowtable.min)
105 |    local nowsec = string.format("%2s", nowtable.sec)
106 |    local timestr = nowyday.." days "..nowhour.." hours "..nowmin.." min "..nowsec.." sec"
107 |    local secs_since_epoch = os.time()
108 |    local nowyear = nowtable.year
109 |    local yearstartsecs_since_epoch = os.time({year=nowyear, month=1, day=1, hour=0})
110 |    local nowyear_elapsed_secs = secs_since_epoch - yearstartsecs_since_epoch
111 |    local yearendsecs_since_epoch = os.time({year=nowyear+1, month=1, day=1, hour=0})
112 |    local nowyear_total_secs = yearendsecs_since_epoch - yearstartsecs_since_epoch
113 |    local elapsed_percent = nowyear_elapsed_secs/nowyear_total_secs
114 |    if timelapsed_canvas:isShowing() then
115 |        timelapsed_canvas[3].text = timestr
116 |        timelapsed_canvas[6].frame.w = tostring(240/280*elapsed_percent)
117 |    end
118 | 
119 | end
120 | 
121 | if elapsedTimer == nil then
122 |     elapsedTimer = hs.timer.doEvery(1, function() updateElapsedCanvas() end)
123 | else
124 |     elapsedTimer:start()
125 | end
126 | 


--------------------------------------------------------------------------------
/utils/_actions/annoyances.lua:
--------------------------------------------------------------------------------
  1 | --
  2 | -- Annoyances in hammerspoon functions/modules/whatever
  3 | --
  4 | -- Some of them were design decisions by me, some design decisions by others... in any case, they
  5 | -- annoy me now, but not enough to make a huge deal about whether or not the actual module should
  6 | -- be changed/updated.
  7 | --
  8 | 
  9 | local module = {}
 10 | 
 11 | local settings = require("hs.settings")
 12 | 
 13 | -- private variables and methods -----------------------------------------
 14 | 
 15 | local _kMetaTable = {}
 16 | _kMetaTable._k = setmetatable({}, {__mode = "k"})
 17 | _kMetaTable._t = setmetatable({}, {__mode = "k"})
 18 | _kMetaTable.__index = function(obj, key)
 19 |         if _kMetaTable._k[obj] then
 20 |             if _kMetaTable._k[obj][key] then
 21 |                 return _kMetaTable._k[obj][key]
 22 |             else
 23 |                 for k,v in pairs(_kMetaTable._k[obj]) do
 24 |                     if v == key then return k end
 25 |                 end
 26 |             end
 27 |         end
 28 |         return nil
 29 |     end
 30 | _kMetaTable.__newindex = function(obj, key, value)
 31 |         error("attempt to modify a table of constants",2)
 32 |         return nil
 33 |     end
 34 | _kMetaTable.__pairs = function(obj) return pairs(_kMetaTable._k[obj]) end
 35 | _kMetaTable.__len = function(obj) return #_kMetaTable._k[obj] end
 36 | _kMetaTable.__tostring = function(obj)
 37 |         local result = ""
 38 |         if _kMetaTable._k[obj] then
 39 |             local width = 0
 40 |             for k,v in pairs(_kMetaTable._k[obj]) do width = width < #tostring(k) and #tostring(k) or width end
 41 |             for k,v in require("hs.fnutils").sortByKeys(_kMetaTable._k[obj]) do
 42 |                 if _kMetaTable._t[obj] == "table" then
 43 |                     result = result..string.format("%-"..tostring(width).."s %s\n", tostring(k),
 44 |                         ((type(v) == "table") and "{ table }" or tostring(v)))
 45 |                 else
 46 |                     result = result..((type(v) == "table") and "{ table }" or tostring(v)).."\n"
 47 |                 end
 48 |             end
 49 |         else
 50 |             result = "constants table missing"
 51 |         end
 52 |         return result
 53 |     end
 54 | _kMetaTable.__metatable = _kMetaTable -- go ahead and look, but don't unset this
 55 | 
 56 | local _makeConstantsTable
 57 | _makeConstantsTable = function(theTable)
 58 |     if type(theTable) ~= "table" then
 59 |         local dbg = debug.getinfo(2)
 60 |         local msg = dbg.short_src..":"..dbg.currentline..": attempting to make a '"..type(theTable).."' into a constant table"
 61 |         if module.log then module.log.ef(msg) else print(msg) end
 62 |         return theTable
 63 |     end
 64 |     for k,v in pairs(theTable) do
 65 |         if type(v) == "table" then
 66 |             local count = 0
 67 |             for a,b in pairs(v) do count = count + 1 end
 68 |             local results = _makeConstantsTable(v)
 69 |             if #v > 0 and #v == count then
 70 |                 _kMetaTable._t[results] = "array"
 71 |             else
 72 |                 _kMetaTable._t[results] = "table"
 73 |             end
 74 |             theTable[k] = results
 75 |         end
 76 |     end
 77 |     local results = setmetatable({}, _kMetaTable)
 78 |     _kMetaTable._k[results] = theTable
 79 |     local count = 0
 80 |     for a,b in pairs(theTable) do count = count + 1 end
 81 |     if #theTable > 0 and #theTable == count then
 82 |         _kMetaTable._t[results] = "array"
 83 |     else
 84 |         _kMetaTable._t[results] = "table"
 85 |     end
 86 |     return results
 87 | end
 88 | 
 89 | -- Public Changes ------------------------------------------------------
 90 | 
 91 | if settings.keys then print("++ settings.keys already exists, skipping") else
 92 |     settings.keys = function()
 93 |         local results = {}
 94 |         for i,v in ipairs(hs.settings.getKeys()) do table.insert(results, v) end
 95 |         table.sort(results)
 96 |         return _makeConstantsTable(results)
 97 |     end
 98 | end
 99 | 
100 | -- Return Module Object --------------------------------------------------
101 | 
102 | return module
103 | 


--------------------------------------------------------------------------------
/utils/_actions/battery_usbdrives.lua:
--------------------------------------------------------------------------------
 1 | -- Try to unmount USB drives if we switch to battery, cause we're probably
 2 | -- yanking the USB cable next...
 3 | 
 4 | local battery = require("hs.battery")
 5 | local alert   = require("hs.alert")
 6 | 
 7 | local PreviousPowerSource = battery.powerSource()
 8 | 
 9 | return battery.watcher.new(function()
10 |     local total, count = 0, 0
11 |     local CurrentPowerSource  = battery.powerSource()
12 |     if CurrentPowerSource ~= PreviousPowerSource then
13 |         if CurrentPowerSource ~= "AC Power" then
14 |             for volume in require("hs.fs").dir("/Volumes") do
15 |                 if not volume:match("^%.") and volume ~= "Yose" and volume ~= "DeepChaos" then
16 |                     local _,_,_,rc = hs.execute("diskutil umount '"..volume.."'")
17 |                     total = total + 1
18 |                     if tonumber(rc) == 0 then count = count + 1 end
19 |                 end
20 |             end
21 |             if total > 0 then
22 |                 alert.show("Auto dismount: "..tostring(count).." of "..tostring(total).." dismounted.")
23 |             end
24 |         end
25 |         PreviousPowerSource = CurrentPowerSource
26 |     end
27 | end) --:start()
28 | 


--------------------------------------------------------------------------------
/utils/_actions/consoleHistory.lua:
--------------------------------------------------------------------------------
 1 | local module   = {}
 2 | local console  = require("hs.console")
 3 | local settings = require("hs.settings")
 4 | local timer    = require("hs.timer")
 5 | 
 6 | local hashFN   = require("hs.hash").MD5 -- can use other hash fn if this proves insufficient
 7 | 
 8 | local saveLabel     = "_ASMConsoleHistory" -- label for saved history
 9 | local checkInterval = settings.get(saveLabel.."_interval") or 1 -- how often to check for changes
10 | local maxLength     = settings.get(saveLabel.."_max") or 100    -- maximum history to save
11 | 
12 | local uniqueHistory = function(raw)
13 |     local hashed, history = {}, {}
14 |     for i = #raw, 1, -1 do
15 |         local key = hashFN(raw[i])
16 |         if not hashed[key] then
17 |             table.insert(history, 1, raw[i])
18 |             hashed[key] = true
19 |         end
20 |     end
21 |     return history
22 | end
23 | 
24 | module.clearHistory = function() return console.setHistory({}) end
25 | 
26 | module.saveHistory = function()
27 |     local hist, save = console.getHistory(), {}
28 |     if #hist > maxLength then
29 |         table.move(hist, #hist - maxLength, #hist, 1, save)
30 |     else
31 |         save = hist
32 |     end
33 |     -- save only the unique lines
34 |     settings.set(saveLabel, uniqueHistory(save))
35 | end
36 | 
37 | module.retrieveHistory = function()
38 |     local history = settings.get(saveLabel)
39 |     if (history) then
40 |         console.setHistory(history)
41 |     end
42 | end
43 | 
44 | module.retrieveHistory()
45 | local currentHistoryCount = #console.getHistory()
46 | 
47 | module.autosaveHistory = timer.new(checkInterval, function()
48 |     local historyNow = console.getHistory()
49 |     if #historyNow ~= currentHistoryCount then
50 |         currentHistoryCount = #historyNow
51 |         module.saveHistory()
52 |     end
53 | end):start()
54 | 
55 | module.pruneHistory = function()
56 |     console.setHistory(uniqueHistory(console.getHistory()))
57 |     currentHistoryCount = #console.getHistory()
58 |     return currentHistoryCount
59 | end
60 | 
61 | module = setmetatable(module, { __gc =  function(_)
62 |                                     _.saveHistory()
63 |                                 end,
64 | })
65 | 
66 | module.history = function(toFind)
67 |     if type(toFind) == "number" then
68 |         local history = console.getHistory()
69 |         if toFind < 0 then toFind = #history - (toFind + 1) end
70 |         local command = history[toFind]
71 |         if command then
72 |             print(">> " .. command)
73 |             timer.doAfter(.1, function()
74 |                 local newHistory = console.getHistory()
75 |                 newHistory[#newHistory] = command
76 |                 console.setHistory(newHistory)
77 |             end)
78 | 
79 |             local fn, err = load("return " .. command)
80 |             if not fn then fn, err = load(command) end
81 |             if fn then return fn() else return err end
82 | 
83 | --             return load(command)()
84 |         else
85 |             error("nil item at specified history position", 2)
86 |         end
87 |     else
88 |         toFind = toFind or ""
89 |         for i,v in ipairs(console.getHistory()) do
90 |             if v:match(toFind) then print(i, v) end
91 |         end
92 |     end
93 | end
94 | 
95 | return module
96 | 


--------------------------------------------------------------------------------
/utils/_actions/crash_test_dummy.lua:
--------------------------------------------------------------------------------
 1 | local crash = require("hs.crash")
 2 | local settings = require("hs.settings")
 3 | 
 4 | crash.crashLogToNSLog = true ;
 5 | 
 6 | if settings.get("_asm.crashIfNotMain") then
 7 |     local function crashifnotmain(reason)
 8 |         if not crash.isMainThread() then
 9 |             print("crashifnotmain called with reason", reason) -- may want to remove this, very verbose otherwise
10 |             print("not in main thread, crashing")
11 |             crash.crash()
12 |         end
13 |     end
14 |     debug.sethook(crashifnotmain, 'c')
15 |     return true
16 | else
17 |     return false
18 | end
19 | 
20 | 


--------------------------------------------------------------------------------
/utils/_actions/dots.lua:
--------------------------------------------------------------------------------
  1 | -- Based on Szymon Kaliski's code found at https://github.com/szymonkaliski/Dotfiles/blob/ae42c100a56c26bc65f6e3ca2ad36e30b558ba10/Dotfiles/hammerspoon/utils/spaces/dots.lua
  2 | 
  3 | 
  4 | local spaces  = require("hs.spaces")
  5 | local screen  = require("hs.screen")
  6 | local _spaces = require("hs._asm.undocumented.spaces")
  7 | local fnutils = require("hs.fnutils")
  8 | local drawing = require("hs.drawing")
  9 | 
 10 | local cache = {
 11 |   watchers = {},
 12 |   dots     = {}
 13 | }
 14 | 
 15 | local module = {}
 16 | 
 17 | module.size          = 8
 18 | module.distance      = 16
 19 | module.cache         = cache
 20 | module.color         = { white = 0.7, alpha = 0.45 }
 21 | module.selectedColor = { white = 0.7, alpha = 0.95 }
 22 | module.activeColor   = { green = 0.5, alpha = 0.75 }
 23 | 
 24 | module.draw = function()
 25 |   local activeSpace = _spaces.activeSpace()
 26 | 
 27 |   for k, v in pairs(cache.dots) do
 28 |       cache.dots[k].stillHere = false
 29 |   end
 30 |   -- FIXME: what if I remove screen, the dots are still being drawn?
 31 |   fnutils.each(screen.allScreens(), function(screen)
 32 |     local screenFrame  = screen:fullFrame()
 33 |     local screenUUID   = screen:spacesUUID()
 34 |     local screenSpaces = _spaces.layout()[screenUUID]
 35 | 
 36 |     if screenSpaces then -- when screens don't have separate spaces, it won't appear in the layout
 37 |       if not cache.dots[screenUUID] then cache.dots[screenUUID] = {} end
 38 |       cache.dots[screenUUID].stillHere = true
 39 | 
 40 |       for i = 1, math.max(#screenSpaces, #cache.dots[screenUUID]) do
 41 |         local dot
 42 | 
 43 |         if not cache.dots[screenUUID][i] then
 44 |           dot = drawing.circle({ x = 0, y = 0, w = module.size, h = module.size })
 45 | 
 46 |           dot
 47 |             :setStroke(false)
 48 |   --           :setBehaviorByLabels({ 'canJoinAllSpaces', 'stationary' })
 49 |             :setBehaviorByLabels({ 'canJoinAllSpaces' })
 50 |   --           :setLevel(drawing.windowLevels.desktopIcon)
 51 |             :setLevel(drawing.windowLevels.popUpMenu)
 52 |         else
 53 |           dot = cache.dots[screenUUID][i]
 54 |         end
 55 | 
 56 |         local x     = screenFrame.x + screenFrame.w / 2 - (#screenSpaces / 2) * module.distance + i * module.distance - module.size * 3 / 2
 57 |         local y     = screenFrame.y + screenFrame.h - (module.distance/2)
 58 |   --       local y     = module.distance
 59 |   --       local y     = screenFrame.h - module.distance
 60 | 
 61 |         local dotColor = module.color
 62 |         if screenSpaces[i] == activeSpace then
 63 |             dotColor = module.activeColor
 64 |         else
 65 |             for i2, v2 in ipairs(_spaces.query(_spaces.masks.currentSpaces)) do
 66 |                 if screenSpaces[i] == v2 then
 67 |                     dotColor = module.selectedColor
 68 |                     break
 69 |                 end
 70 |             end
 71 |         end
 72 | 
 73 |         dot
 74 |           :setTopLeft({ x = x, y = y })
 75 |           :setFillColor(dotColor)
 76 | 
 77 |         if i <= #screenSpaces then
 78 |           dot:show()
 79 |         else
 80 |           dot:hide()
 81 |         end
 82 | 
 83 |         cache.dots[screenUUID][i] = dot
 84 |       end
 85 |     end
 86 |   end)
 87 |   for k, v in pairs(cache.dots) do
 88 |       if not cache.dots[k].stillHere then
 89 |           for i, v2 in ipairs(cache.dots[k]) do
 90 |               v2:delete()
 91 |           end
 92 |           cache.dots[k] = nil
 93 |       end
 94 |   end
 95 | end
 96 | 
 97 | module.start = function()
 98 |   -- we need to redraw dots on screen and space events
 99 |   cache.watchers.spaces = spaces.watcher.new(module.draw):start()
100 |   cache.watchers.screen = screen.watcher.newWithActiveScreen(module.draw):start()
101 |   module.draw()
102 | end
103 | 
104 | module.stop = function()
105 |   fnutils.each(cache.watchers, function(watcher) watcher:stop() end)
106 | 
107 |   cache.dots = {}
108 | end
109 | 
110 | module.start()
111 | 
112 | return module


--------------------------------------------------------------------------------
/utils/_actions/inspectors.lua:
--------------------------------------------------------------------------------
  1 | local module = {}
  2 | local timer = require("hs.timer")
  3 | 
  4 | inspect = require("hs.inspect")
  5 | 
  6 | timestamp = function(date)
  7 |     date = date or timer.secondsSinceEpoch()
  8 |     return os.date("%F %T" .. string.format("%-5s", ((tostring(date):match("(%.%d+)$")) or "")), math.floor(date))
  9 | end
 10 | 
 11 | local inspectWrapper = function(what, how, actual)
 12 |     how = how or {}
 13 |     for k, v in pairs(how) do actual[k] = v end
 14 |     return inspect(what, actual)
 15 | end
 16 | inspectm = function(what, how) return inspectWrapper(what, how, { metatables = 1 }) end
 17 | inspect1 = function(what, how) return inspectWrapper(what, how, { depth = 1 }) end
 18 | inspect2 = function(what, how) return inspectWrapper(what, how, { depth = 2 }) end
 19 | inspecta = function(what, how) return inspectWrapper(what, how, {
 20 |     process = function(i,p) if p[#p] ~= "n" then return i end end
 21 | }) end
 22 | 
 23 | -- finspect = function(...)
 24 | --     local args = table.pack(...)
 25 | --     if args.n == 1 and type(args[1]) == "table" then
 26 | --         args = args[1]
 27 | --     else
 28 | --         args.n = nil -- supress the count from table.pack
 29 | --     end
 30 | --     return (inspect(args):gsub("%s+", " "))
 31 | -- end
 32 | 
 33 | finspect = function(...)
 34 |     local args = table.pack(...)
 35 |     if args.n == 1 and type(args[1]) == "table" then
 36 |         args = args[1]
 37 |     else
 38 |         args.n = nil -- supress the count from table.pack
 39 |     end
 40 | 
 41 |     -- causes issues with recursive calls to __tostring in inspect
 42 |     local mt = getmetatable(args)
 43 |     if mt then setmetatable(args, nil) end
 44 |     local answer = inspect(args, { newline = " ", indent = "" })
 45 |     if mt then setmetatable(args, mt) end
 46 |     return answer
 47 | end
 48 | 
 49 | minspect = function(stuff)
 50 |     return (inspect(stuff, { process = function(item, path)
 51 |         if path[#path] == inspect.KEY then return item end
 52 |         if path[#path] == inspect.METATABLE then return nil end
 53 |         if #path > 0 and type(item) == "table" then
 54 |             return finspect(item)
 55 |         else
 56 |             return item
 57 |         end
 58 |     end
 59 |     }):gsub("[\"']{", "{"):gsub("}[\"']", "}"))
 60 | end
 61 | 
 62 | finspect2 = function(what, ...)
 63 |     local fn, opt = inspect, {}
 64 |     for i,v in ipairs(table.pack(...)) do
 65 |         -- "inspect" is a table with a __call metamethod, so...
 66 |         local vType = (getmetatable(v) or {}).__call and "function" or type(v)
 67 |         if vType == "function" then fn = v end
 68 |         if vType == "table"    then opt = v end
 69 |     end
 70 |     return (fn(what, opt):gsub("%s+", " "))
 71 | end
 72 | 
 73 | module.help = function(...)
 74 |     local output = [[
 75 | 
 76 | This module creates some shortcuts for inspecting Lua data:
 77 | 
 78 |     inspect   - equivalent to `hs.inspect`
 79 | 
 80 |     inspectm  - include options { metatables = 1} by default
 81 |     inspect1  - include options { depth = 1 } by default
 82 |     inspect2  - include options { depth = 2 } by default
 83 |     inspecta  - includes process function in options table to remove `n` key from tables;
 84 |                 this allows tables which contain non-numeric keys only because of
 85 |                 table.pack to be treated as the arrays they really are.
 86 | 
 87 |     finspect(...) - inspects the arguments, first combining them into a table if more
 88 |                     then one or if the one provided is not already a table. Flattens
 89 |                     the output by replacing run-on spaces, tabs, and newlines into a
 90 |                     single space.
 91 | 
 92 |     finspect2(what, [fn], [opt]) - inspects `what` with the function `fn` (default is
 93 |                                    "inspect") with the specfied options (default {}) and
 94 |                                    "flattens" the output by replacing run-on spaces, tabs,
 95 |                                    and newlines into a single space.
 96 | 
 97 |     timestamp([number]) - returns the current or specified time as a string in the format
 98 |                           of 'YYYY-MM-DD hh:mm:ss.nnnn'
 99 | 
100 |     Note that a second argument to any of the `inspect*` shortcuts is appended to the
101 |     default table described; i.e. if you specify the same key in your options table,
102 |     your value will override the default.
103 | 
104 | ]]
105 |     return output
106 | end
107 | 
108 | module = setmetatable(module, {
109 |     __tostring = function(self) return self.help() end,
110 |     __call     = function(self, ...) return self.help(...) end,
111 | })
112 | 
113 | return module
114 | 


--------------------------------------------------------------------------------
/utils/_actions/localAssets.lua:
--------------------------------------------------------------------------------
 1 | --
 2 | -- local assets web server -- I don't want to have to rely on internet access all of the
 3 | -- time, so a local web server takes care of those things that I might need...
 4 | 
 5 | local module = {}
 6 | 
 7 | local hsminweb = require("hs.httpserver.hsminweb")
 8 | local serverPort = 7734
 9 | local documentRoot = hs.configdir.."/_localAssets"
10 | 
11 | module.server = hsminweb.new(documentRoot):port(serverPort)
12 |                                           :allowDirectory(true)
13 |                                           :name("localAssets")
14 |                                           :bonjour(false)
15 |                                           :cgiEnabled(true)
16 |                                           :luaTemplateExtension("lp")
17 |                                           :directoryIndex{
18 |                                               "index.html", "index.lp", "index.cgi",
19 |                                           }:accessList{
20 |                                               {"X-Remote-Addr",  "::1",       false,   true},
21 |                                               {"X-Remote-Addr",  "127.0.0.1", false,   true},
22 |                                             -- technically optional, but I like being explicit
23 |                                               {"*",              "*",         false,   false},
24 |                                           }
25 |                                           :start()
26 | 
27 | module.server._logBadTranslations       = true
28 | module.server._logPageErrorTranslations = true
29 | module.server._allowRenderTranslations  = true
30 | 
31 | module.hsdocs = require"hs.doc.hsdocs".start()
32 | 
33 | return module
34 | 


--------------------------------------------------------------------------------
/utils/_actions/loopcatcher.lua:
--------------------------------------------------------------------------------
 1 | --
 2 | -- uses debug.sethook and a timer to break out of infinite loops in lua code within Hammerspoon
 3 | --
 4 | -- Haven't had any problems with it or false positives, but YMMV -- standard disclaimers, etc.
 5 | --
 6 | -- Updates 2015-12-21:
 7 | --      should play nicely with other hooks by storing info about it and chaining
 8 | --      you can force an "immediate" break by holding down CMD-CTRL-SHIFT-ALT-CAPSLOCK-FN all at once
 9 | --          you'll need to remove "and mods.fn" where noted below if your keyboard does not have this
10 | --          modifier (non-laptops, I suspect)
11 | --
12 | -- See https://gist.github.com/asmagill/cf1d6398aecc2cee37af for additional release notes
13 | --
14 | -- You can disable this (and all debug hooks) at any time by typing `debug.sethook(nil)` into the
15 | -- Hammerspoon Console.
16 | 
17 | local module = {}
18 | 
19 | local timer      = require("hs.timer")
20 | -- local caffeinate = require("hs.caffeinate")
21 | local eventtap   = require("hs.eventtap")
22 | 
23 | -- -- testing infinite loop detector with debug.sethook
24 | 
25 | local lastFn, lastMask, lastCount = debug.gethook()
26 | 
27 | local setHook = function(ourFn, ourMask, ourCount)
28 |     if ourFn then
29 |         lastFn, lastMask, lastCount = debug.gethook()
30 |         if lastCount > 0 and lastCount < ourCount then ourCount = lastCount end
31 |         for i = 1, #lastMask, 1 do
32 |             if not ourMask:match(lastMask:sub(i,i)) then
33 |                 ourMask = ourMask..lastMask:sub(i,i)
34 |             end
35 |         end
36 | --     print("*** setting Hook:", ourFn, ourMask.."("..#ourMask..")", ourCount)
37 |         debug.sethook(ourFn, ourMask, ourCount)
38 |     else
39 |         debug.sethook(lastFn, lastMask, lastCount)
40 |     end
41 | end
42 | 
43 | -- module._loopTimeStamp = os.time()
44 | -- module._loopTimer = timer.new(5, function() module._loopTimeStamp = os.time() end):start()
45 | module._loopChecker = function(t,l)
46 |     if lastFn then lastFn(t, l) end
47 | --     if (os.time() - module._loopTimeStamp) > 60 then
48 | --         module._loopTimeStamp = os.time()
49 | --         error("*** timeout -- infinite loop somewhere?\n\n"..debug.traceback(), 0)
50 | --     end
51 |     local mods = eventtap.checkKeyboardModifiers()
52 | -- remove "and mods.fn" if your keyboard does not have this key (non laptops most likely)
53 |     if mods.capslock and mods.fn and mods.cmd and mods.ctrl and mods.alt and mods.shift then
54 |         error("*** forced break\n\n"..debug.traceback(), 0)
55 |     end
56 | end
57 | 
58 | -- module._loopSleepWatcher = caffeinate.watcher.new(function(event)
59 | --     if event == caffeinate.watcher.systemDidWake then
60 | --         module._loopTimeStamp = os.time()
61 | --         setHook(module._loopChecker, "", 1000)
62 | --     elseif event == caffeinate.watcher.systemWillSleep then
63 | --         setHook(nil)
64 | --     end
65 | -- end):start()
66 | 
67 | setHook(module._loopChecker, "", 100)
68 | 
69 | return module
70 | 


--------------------------------------------------------------------------------
/utils/_actions/luaFileChangeWatcher.lua:
--------------------------------------------------------------------------------
 1 | local pathwatcher = require"hs.pathwatcher"
 2 | local inspect     = require"hs.inspect"
 3 | 
 4 | local module = {}
 5 | 
 6 | module.watcher = pathwatcher.new(hs.configdir, function(changedFiles, changedFlags)
 7 |     local luaFileFound = false
 8 |     for i = 1, #changedFiles, 1 do
 9 |         if changedFiles[i]:match("%.lua$") then
10 |             hs.printf("\t%s — %s", (changedFiles[i]:gsub("^" .. hs.configdir, "…")), (inspect(changedFlags[i]):gsub(" = true", ""):gsub("%s+", " ")))
11 |             luaFileFound = true
12 |         end
13 |     end
14 |     if luaFileFound then
15 |         print("\tyou might want to reload!")
16 |     end
17 | end):start ()
18 | 
19 | return module
20 | 


--------------------------------------------------------------------------------
/utils/_actions/nc.lua:
--------------------------------------------------------------------------------
 1 | local module = {}
 2 | require("utils._actions.inspectors")
 3 | 
 4 | local ignoreInWorkspaceObserver = {
 5 |     NSWorkspaceActiveSpaceDidChangeNotification = true,
 6 |     NSWorkspaceDidActivateApplicationNotification = true,
 7 |     NSWorkspaceDidDeactivateApplicationNotification = true,
 8 |     NSWorkspaceDidHideApplicationNotification = true,
 9 |     NSWorkspaceDidLaunchApplicationNotification = true,
10 |     NSWorkspaceDidMountNotification = true,
11 |     NSWorkspaceDidRenameVolumeNotification = true,
12 |     NSWorkspaceDidTerminateApplicationNotification = true,
13 |     NSWorkspaceDidUnhideApplicationNotification = true,
14 |     NSWorkspaceDidUnmountNotification = true,
15 |     NSWorkspaceDidWakeNotification = true,
16 |     NSWorkspaceScreensDidSleepNotification = true,
17 |     NSWorkspaceScreensDidWakeNotification = true,
18 |     NSWorkspaceSessionDidBecomeActiveNotification = true,
19 |     NSWorkspaceSessionDidResignActiveNotification = true,
20 |     NSWorkspaceWillLaunchApplicationNotification = true,
21 |     NSWorkspaceWillPowerOffNotification = true,
22 |     NSWorkspaceWillSleepNotification = true,
23 |     NSWorkspaceWillUnmountNotification = true,
24 |     NSWorkspaceActiveDisplayDidChangeNotification = true,
25 | }
26 | 
27 | local nc                       = require "hs._asm.notificationcenter"
28 | local distributednotifications = require "hs.distributednotifications"
29 | local fnutils                  = require "hs.fnutils"
30 | 
31 | module.workspaceObserver = nc.workspaceObserver(function(n,o,i)
32 |     if not ignoreInWorkspaceObserver[n] then
33 |         local f = io.open("__workspaceobserver.txt","a") ;
34 |         f:write(os.date().."\t".."name:"..inspect(n).."\tobj:"..inspect(o):gsub("%s+"," ").."\tinfo:"..inspect(i):gsub("%s+"," ").."\n")
35 |         f:close()
36 |     end
37 | end):start()
38 | 
39 | local do_logFile = "__distributedobserver_core.log"
40 | 
41 | local f, err = io.open(do_logFile, "r")
42 | if f then
43 |     local logData = f:read("a")
44 |     f:close()
45 |     local asArray = fnutils.split(logData, "[\r\n]")
46 |     if #asArray > 1000 then
47 |         local newArray = {}
48 |         table.move(asArray, #asArray - 1000, #asArray, 1, newArray)
49 |         f, err = io.open(do_logFile, "w")
50 |         if f then
51 |             f:write(table.concat(newArray, "\n"))
52 |             f:close()
53 |         else
54 |             print("unable to create truncated " .. do_logFile .. " (" .. err ..")")
55 |         end
56 |     end
57 | else
58 |     print("unable to read " .. do_logFile .. " to check length (" .. err ..")")
59 | end
60 | 
61 | module.distributedObserver_core = distributednotifications.new(function(n,o,i)
62 |     local f = io.open(do_logFile,"a") ;
63 |     f:write(timestamp().."\t".."name:"..inspect(n).."\tobj:"..inspect(o):gsub("%s+"," ").."\tinfo:"..inspect(i):gsub("%s+"," ").."\n")
64 |     f:close()
65 | end):start() -- gets big fast, so let me turn it on when I want to explore
66 | 
67 | return module
68 | 


--------------------------------------------------------------------------------
/utils/_actions/popConsole.lua:
--------------------------------------------------------------------------------
 1 | local noises      = require("hs.noises")
 2 | local watchable   = require("hs.watchable")
 3 | local window      = require("hs.window")
 4 | local application = require("hs.application")
 5 | local timer       = require("hs.timer")
 6 | 
 7 | local module = {}
 8 | module.watchables = watchable.new("popConsole", true)
 9 | module.watchables.enabled = true
10 | 
11 | module.popTimeout = 1
12 | module.debug   = false
13 | 
14 | local prevWindowHolder
15 | 
16 | local popTimer
17 | local popCount = 0
18 | 
19 | local consoleToggleThingy = function()
20 | -- this attempts to keep track of the previously focused window and return us to it
21 |     local conswin = window.get("Hammerspoon Console")
22 |     if conswin and application.get("Hammerspoon"):isFrontmost() then
23 |         conswin:close()
24 |         if prevWindowHolder and #prevWindowHolder:role() ~= 0 then
25 |             prevWindowHolder:becomeMain():focus()
26 |             prevWindowHolder = nil
27 |         end
28 |     else
29 |         prevWindowHolder = window.frontmostWindow()
30 |         hs.openConsole()
31 |     end
32 | end
33 | 
34 | local handlePops = function()
35 |     if module.debug then hs.printf("~~ heard %d pop(s) in %d second(s)", popCount, module.popTimeout) end
36 |     if popCount == 2 then
37 |         consoleToggleThingy()
38 |     end
39 |     popTimer = nil
40 |     popCount = 0
41 | end
42 | 
43 | local consolePopWatcher = function()
44 |     if not popTimer then
45 |         popTimer = timer.doAfter(module.popTimeout, handlePops)
46 |     end
47 |     popCount = popCount + 1
48 | end
49 | 
50 | module.callback = function(w)
51 |     if w == 1 then     -- start "sssss" sound
52 |     elseif w == 2 then -- end "sssss" sound
53 |     elseif w == 3 then -- mouth popping sound
54 |         consolePopWatcher()
55 |     end
56 | end
57 | 
58 | module._noiseWatcher = noises.new(module.callback):start()
59 | 
60 | module.toggleForWatchablesEnabled = watchable.watch("popConsole.enabled", function(w, p, i, oldValue, value)
61 |     if value then
62 |         module._noiseWatcher:start()
63 |     else
64 |         module._noiseWatcher:stop()
65 |     end
66 | end)
67 | 
68 | local caffeinate = require("hs.caffeinate")
69 | -- the listener can prevent or delay system sleep, so disable as appropriate
70 | module.watchCaffeinatedState = watchable.watch("generalStatus.caffeinatedState", function(w, p, i, old, new)
71 | --     print(string.format("~~~ %s popConsole caffeinatedWatcher called with %s (%d), was %s (%d), currently %s", timestamp(), caffeinate.watcher[new], new, caffeinate.watcher[old], old, module.watchables.enabled))
72 |     if new == 1 or new == 10 then -- systemWillSleep or screensDidLock
73 |         module.wasActive = module.watchables.enabled
74 |         module.watchables.enabled = false
75 |     elseif new == 0 or new == 11 then -- systemDidWake or screensDidUnlock
76 |         if type(module.wasActive) == "boolean" then
77 |             module.watchables.enabled = module.wasActive
78 |         else
79 |             module.watchables.enabled = true
80 |         end
81 |     end
82 | end)
83 | 
84 | return setmetatable(module, { __tostring = function(self)
85 |     return "Adjust with `self`.watchables.enabled or using hs.watchables with path 'popConsole.enabled'"
86 | end })
87 | 


--------------------------------------------------------------------------------
/utils/_actions/preParsers.lua:
--------------------------------------------------------------------------------
 1 | local module = {}
 2 | local ipc      = require("hs.ipc")
 3 | 
 4 | -- pre-parser core
 5 | local preParser = function(s)
 6 |     -- allow !# like bash to redo a command from the history
 7 |     local historyNumber = s:match("^!(-?%d+)$")
 8 |     if historyNumber then s = "history(" .. historyNumber .. ")" end
 9 | 
10 |     -- allow `history what`
11 |     if s:match("^history%s+[^\"]") then s = s:gsub("^history ", "history \"") .. "\"" end
12 | 
13 |     if s:match("^help") and not s:match("^help%.") then
14 |         -- allow help(what) without quotes
15 |         local helpParen = s:match("^help%s*%(([^\"]*)%)%s*$")
16 |         if helpParen then
17 |             if helpParen == "" then
18 |                 s = "help"
19 |             else
20 |                 s = "help." .. helpParen
21 |             end
22 |         end
23 | 
24 |         -- allow `help what`
25 |         if s:match("^help%s+[^\"]") then s = s:gsub("^help%s+", "help.") end
26 |     end
27 |     return s
28 | end
29 | 
30 | -- pre-parser for Console
31 | local previousParser = hs._consoleInputPreparser
32 | hs._consoleInputPreparser = function(s)
33 |     if previousParser then s = previousParser(s) end
34 |     -- invoke my common pre-parser
35 |     return preParser(s)
36 | end
37 | 
38 | -- pre-parser for ipc's command line tool
39 | -- local ipcRawhandler = ipc.handler
40 | -- ipc.handler = function(str)
41 | --     str = preParser(str)
42 | --     return ipcRawhandler(str)
43 | -- end
44 | 
45 | module.help = function(...)
46 |     local output = [[
47 | 
48 | This module preparses input from the Console or from the IPC `hs` command line tool.  The
49 | following conversions are applied:
50 | 
51 |     !#            - performs the command at the specified number in the console history;
52 |                     negative numbers are offset from the history end
53 |     history what  - automatically wraps `what` in quotes, if it isn't already
54 |     help(what)    - automatically wraps `what` in quotes, if it isn't already
55 |     help what     - replaces the first space with a period to take advantage of help's
56 |                     __tostring methods
57 | 
58 | ]]
59 |     return output
60 | end
61 | 
62 | module = setmetatable(module, {
63 |     __tostring = function(self) return self.help() end,
64 |     __call     = function(self, ...) return self.help(...) end,
65 | })
66 | 
67 | return module
68 | 


--------------------------------------------------------------------------------
/utils/_actions/remoteCheck.lua:
--------------------------------------------------------------------------------
 1 | local module = {}
 2 | 
 3 | local task        = require("hs.task")
 4 | local stext       = require("hs.styledtext")
 5 | local timer       = require("hs.timer")
 6 | local settings    = require("hs.settings")
 7 | local watchable   = require"hs.watchable"
 8 | 
 9 | local hosts = {
10 |     "tesla.private",
11 |     "marconi.private",
12 |     "hedylamarr.private",
13 | }
14 | 
15 | local style = {
16 |     font = { name = "Menlo", size = 10 },
17 |     color = { alpha = 1.0 },
18 |     paragraphStyle = { lineBreak = "clip" },
19 | }
20 | 
21 | local myTasks  = {}
22 | local myOutput = {}
23 | 
24 | module.updateTasks = function()
25 |     for i, v in ipairs(hosts) do
26 |         if myTasks[v] and myTasks[v]:isRunning() then
27 |             -- print("-- "..v.." still running")
28 |         else
29 |             if module.vpnStatus and module.vpnStatus:value() then
30 |                 myOutput[v] = stext.new(v.." is polling...\n", style):setStyle{
31 |                     color = { list = "Crayons", name = "Sea Foam" },
32 |                     font  = stext.convertFont(style.font, stext.fontTraits.italicFont),
33 |                 }
34 |                 myTasks[v] = task.new("/sbin/ping", function(c, o, e)
35 |                     local output  = o:gsub("^PING.+[\r\n][\r\n]", "")
36 |                     local soutput = stext.new(output, style)
37 |                     if c == 2 then
38 |                         soutput = soutput:setStyle{
39 |                             color = { red = 1.0 },
40 |                             paragraphStyle = { lineBreak = "wordWrap" },
41 |                         }
42 |                     else
43 |                         local _, e1 = output:find("^[^\r\n]+[\r\n]")
44 |                         local s2, e2, loss = output:find("(%d+%.%d+)%% packet loss")
45 |                         loss = tonumber(loss)
46 |                         local pStyle = (loss < 5.0)  and { color = { list = "Apple", name = "Green" } } or
47 |                                       ((loss < 10.0) and { color = { list = "Apple", name = "Yellow" } } or
48 |                                                          { color = { list = "Apple", name = "Red" } })
49 |                         soutput = soutput:setStyle({
50 |                             color = { list = "x11", name = "mediumvioletred" },
51 |                             font  = stext.convertFont(style.font, stext.fontTraits.italicFont),
52 |                         }, 5, e1 - 5):setStyle(pStyle, s2, e2)
53 |                     end
54 |                     myOutput[v] = soutput
55 |                     _asm._actions.geeklets.geeklets.remoteCheck.lastRun = os.time() - _asm._actions.geeklets.geeklets.remoteCheck.period
56 |                     myOutput[v] = myOutput[v]..stext.new("Last Check: "..os.date(), style):setStyle{
57 |                         color = { list = "x11", name = "royalblue" },
58 |                         font  = stext.convertFont(style.font, stext.fontTraits.italicFont),
59 |                         paragraphStyle = {
60 |                             alignment = "right"
61 |                         },
62 |                     }
63 |                 end, { "-c10", "-q", v }):start()
64 |             else
65 |                 myOutput[v] = stext.new(v..": VPN is Down\n", style):setStyle{
66 |                     color = { list = "Crayons", name = "Sea Foam" },
67 |                     font  = stext.convertFont(style.font, stext.fontTraits.italicFont),
68 |                 }
69 |                 _asm._actions.geeklets.geeklets.remoteCheck.lastRun = os.time() - _asm._actions.geeklets.geeklets.remoteCheck.period
70 |             end
71 |         end
72 |     end
73 | end
74 | 
75 | module.vpnStatus = watchable.watch("generalStatus.privateVPN", module.updateTasks)
76 | 
77 | module.output = myOutput
78 | module.tasks  = myTasks
79 | module.timer  = timer.new(300, module.updateTasks):start()
80 | 
81 | -- ensure this is triggered after we load, so _asm._actions exists
82 | timer.doAfter(1, module.updateTasks)
83 | 
84 | return module
85 | 


--------------------------------------------------------------------------------
/utils/_actions/screen_bluetooth_toggle.lua:
--------------------------------------------------------------------------------
 1 | -- For my setup, when the number of monitors == 1, it's likely that I'm either
 2 | -- using the battery and not my external mouse, or that I'm using the computer
 3 | -- just far enough from my desk (watching TV, most likely) that the mouse is
 4 | -- barely in range and drains it's battery trying to stay connected... so...
 5 | -- we turn it off when I drop to one monitor.  I can always toggle it back (see
 6 | -- _keys).
 7 | 
 8 | local screen = require("hs.screen")
 9 | local alert  = require("hs.alert")
10 | 
11 | local prevScreens = #screen.allScreens()
12 | 
13 | return screen.watcher.new(function()
14 |     local numScreens = #screen.allScreens()
15 |     if numScreens ~= prevScreens then
16 |         local btooth = require("hs._asm.undocumented.bluetooth")
17 |         if numScreens == 1 then
18 |             if btooth.available() and btooth.power() then
19 |                 alert.show("Turning bluetooth off to conserve mouse battery.",5)
20 |                 btooth.power(false)
21 |             end
22 |         else
23 |             if btooth.available() and not btooth.power() then
24 |                 alert.show("Turning bluetooth on.",5)
25 |                 btooth.power(true)
26 |             end
27 |         end
28 |         prevScreens = numScreens
29 |     end
30 | end) -- :start()
31 | 


--------------------------------------------------------------------------------
/utils/_keys/documentsWebServer.lua:
--------------------------------------------------------------------------------
 1 | --
 2 | -- Toggle private webserver for documentation... smaller footprint than Apache, but even so, not needed often
 3 | 
 4 | local module = {}
 5 | 
 6 | local hsminweb = require("hs.httpserver.hsminweb")
 7 | local hotkey   = require("hs.hotkey")
 8 | local alert    = require("hs.alert").show
 9 | 
10 | local serverPort = 8192
11 | local documentRoot = os.getenv("HOME") .. "/Sites"
12 | 
13 | module.server = hsminweb.new(documentRoot):port(serverPort)
14 |                                           :allowDirectory(true)
15 |                                           :name("Sites")
16 |                                           :bonjour(true)
17 |                                           :cgiEnabled(true)
18 |                                           :luaTemplateExtension("lp")
19 |                                           :directoryIndex{
20 |                                               "index.html", "index.lp", "index.cgi",
21 |                                           }:accessList{
22 |                                               {"X-Remote-Addr", "::1",            false, true},
23 |                                               {"X-Remote-Addr", "127.0.0.1",      false, true},
24 |                                               {"X-Remote-Addr", "^10%.0%.1%.",    true,  true},
25 |                                               {"X-Remote-Addr", "^10%.161%.82%.", true,  true},
26 |                                               {"*",             "*",              false, false},
27 |                                           }
28 | 
29 | module.server._logBadTranslations       = true
30 | module.server._logPageErrorTranslations = true
31 | module.server._allowRenderTranslations  = true
32 | module.debugging = function(value)
33 |     if type(value) == "boolean" then
34 |         module.server._logBadTranslations       = value
35 |         module.server._logPageErrorTranslations = value
36 |         module.server._allowRenderTranslations  = value
37 |     end
38 |     print("** translation debugging:", module.server._logBadTranslations and "enabled" or "disabled")
39 | end
40 | 
41 | module.hotkey = hotkey.bind({"cmd", "alt"}, "f10", function()
42 |     if module.server._server then
43 |         alert("Turning Documentation Server Off...")
44 |         module.server:stop()
45 |     else
46 |         alert("Turning Documentation Server On (Port 8192)...")
47 |         module.server:start()
48 |     end
49 | end)
50 | 
51 | return module
52 | 


--------------------------------------------------------------------------------
/utils/_keys/information.lua:
--------------------------------------------------------------------------------
  1 | local module = {
  2 | --[=[
  3 |     _NAME        = 'hydra.lua',
  4 |     _VERSION     = '0.1',
  5 |     _URL         = 'https://github.com/asmagill/hammerspoon-config',
  6 |     _DESCRIPTION = [[ personal keybindings for hammerspoon ]],
  7 |     _TODO        = [[]],
  8 |     _LICENSE     = [[ See README.md ]]
  9 | --]=]
 10 | }
 11 | 
 12 | -- private variables and methods -----------------------------------------
 13 | 
 14 | local mouse       = require("hs.mouse")
 15 | local pasteboard  = require("hs.pasteboard")
 16 | local devinfo     = require("utils.dev_info")
 17 | local mods        = require("hs._asm.extras").mods
 18 | local hotkey      = require("hs.hotkey")
 19 | local fnutils     = require("hs.fnutils")
 20 | local alert       = require("hs.alert")
 21 | local window      = require("hs.window")
 22 | local application = require("hs.application")
 23 | 
 24 | local point_in_rect = function(rect, point)
 25 |     return  point.x >= rect.x and
 26 |             point.y >= rect.y and
 27 |             point.x <= rect.x + rect.w and
 28 |             point.y <= rect.y + rect.h
 29 | end
 30 | 
 31 | local window_underneath_mouse = function()
 32 |     local pos = mouse.getAbsolutePosition()
 33 |     local win = fnutils.find(window.orderedWindows(), function(window)
 34 |         return point_in_rect(window:frame(), pos) and window:isStandard()
 35 |     end)
 36 |     return win or window.windowForID(0) or window.windowForID(nil)
 37 | end
 38 | 
 39 | local dev = hotkey.modal.new(mods.CAsC, "=")
 40 |      dev:bind(mods.Casc, "C",
 41 |         function()
 42 |             dev.clipboard = not dev.clipboard
 43 |             print("-- Clipping ----------------------------")
 44 |             print("Save to Clipboard = "..tostring(dev.clipboard))
 45 |             print("----------------------------------------")
 46 |         end
 47 |     )
 48 |     dev:bind(mods.casc, "W",
 49 |         function()
 50 |             local win = window_underneath_mouse()
 51 |             print("-- Window ------------------------------")
 52 |             print(devinfo.wininfo(win,dev.clipboard))
 53 |             print("----------------------------------------")
 54 |         end
 55 |     )
 56 |     dev:bind(mods.casc, "A",
 57 |         function()
 58 | --             local win = window_underneath_mouse()
 59 |             local app = application.frontmostApplication()
 60 |             print("-- Application -------------------------")
 61 | --             print(devinfo.appinfo(win:application(),dev.clipboard))
 62 |             print(devinfo.appinfo(app, dev.clipboard))
 63 |             print("----------------------------------------")
 64 |         end
 65 |     )
 66 |     dev:bind(mods.casc, "M",
 67 |         function()
 68 |             local win = window_underneath_mouse()
 69 |             print("-- Monitor -----------------------------")
 70 |             print(devinfo.screeninfo(win:screen(),dev.clipboard))
 71 |             print("----------------------------------------")
 72 |         end
 73 |     )
 74 | --    dev:bind(mods.casc, "S",
 75 | --        function()
 76 | --            print(devinfo.spaceinfo(dev.clipboard))
 77 | --        end
 78 | --    )
 79 |     dev:bind(mods.casc, "B",
 80 |         function()
 81 |             print("-- Battery -----------------------------")
 82 |             print(devinfo.batteryinfo(dev.clipboard))
 83 |             print("----------------------------------------")
 84 |         end
 85 |     )
 86 |     dev:bind(mods.casc, "I",
 87 |         function()
 88 |             local results = devinfo.audioinfo(false).."\r"
 89 |                             ..devinfo.mouseinfo(false).."\r"
 90 |                             ..devinfo.brightnessinfo(false)
 91 |             print("-- Info --------------------------------")
 92 |             print(results)
 93 |             print("----------------------------------------")
 94 |             if dev.clipboard then
 95 |                 pasteboard.setcontents(results)
 96 |             end
 97 |         end
 98 |     )
 99 |     dev:bind(mods.Casc, "V",
100 |         function()
101 |             print("-- Clipboard Contents ------------------")
102 |             print(pasteboard.getContents())
103 |             print("----------------------------------------")
104 |         end
105 |     )
106 | 
107 |     function dev:entered()
108 |         alert.show("Entering Information Mode")
109 |         dev.clipboard = false
110 |     end
111 |     function dev:exited()
112 |         alert.show("Leaving Information Mode")
113 |         dev.clipboard = false
114 |     end
115 | dev:bind(mods.casc, "ESCAPE", function() dev:exit() end)
116 | 
117 | -- Public interface ------------------------------------------------------
118 | -- Return Module Object --------------------------------------------------
119 | 
120 | return module
121 | 


--------------------------------------------------------------------------------
/utils/_keys/redshift.lua:
--------------------------------------------------------------------------------
 1 | --local redshift   = require("hs.redshift")
 2 | local hotkey     = require("hs.hotkey")
 3 | local mods       = require("hs._asm.extras").mods
 4 | local alert      = require("hs.alert")
 5 | --local settings   = require("hs.settings")
 6 | local screen     = require("hs.screen")
 7 | 
 8 | local module = {
 9 |     help = "⌘-F11 and ⌘⌥-F11"
10 | }
11 | 
12 | -- settings.clear("hs.redshift.inverted.override")
13 | -- settings.clear("hs.redshift.disabled.override")
14 | 
15 | --redshift.start(2800,'21:00','7:00','4h')
16 | 
17 | hotkey.bind(mods.Casc, "F11", function()
18 |     alert("Toggle Invert")
19 |     for i,v in ipairs(screen.allScreens()) do
20 |         local gamma = v:getGamma()
21 |         if gamma then
22 |             v:setGamma(gamma.blackpoint, gamma.whitepoint)
23 |         else
24 |             print("~~ could not get gamma for " .. tostring(v:name()))
25 |         end
26 |     end
27 | --    redshift.toggleInvert()
28 | end)
29 | 
30 | --hotkey.bind(mods.CAsc, "F11", function()
31 | --    alert("Toggle Redshift")
32 | --    redshift.toggle()
33 | --end)
34 | --
35 | --local watchable = require("hs.watchable")
36 | --module.watchCaffeinatedState = watchable.watch("generalStatus.caffeinatedState", function(w, p, i, old, new)
37 | --    if new == 0 or new == 9 then -- systemDidWake or screensaverDidStop
38 | --        redshift.start(2800,'21:00','7:00','4h')
39 | --    elseif new == 1 or new == 7 then -- systemWillSleep or screensaverDidStart
40 | --        redshift.stop()
41 | --    end
42 | --end)
43 | 
44 | return module
45 | 


--------------------------------------------------------------------------------
/utils/_keys/switcher.lua:
--------------------------------------------------------------------------------
 1 | local module = {}
 2 | 
 3 | local switcher = require"hs.window.switcher"
 4 | local filter   = require"hs.window.filter"
 5 | local hotkey   = require"hs.hotkey"
 6 | local mods     = require"hs._asm.extras".mods
 7 | 
 8 | module.switcher = switcher.new(filter.new():setDefaultFilter{}, {
 9 |     selectedThumbnailSize = 288,
10 |     thumbnailSize         = 96,
11 |     showTitles            = false,
12 | --    showSelectedThumbnail = false, -- wish it would just show the selected title, but this gets rid of both
13 |     textSize              = 7,
14 | 
15 |     textColor             = { 1.0, 1.0, 1.0, 0.75 },
16 |     backgroundColor       = { 0.3, 0.3, 0.3, 0.75 },
17 |     highlightColor        = { 0.8, 0.5, 0.0, 0.80 },
18 |     titleBackgroundColor  = { 0.0, 0.0, 0.0, 0.75 },
19 | })
20 | 
21 | -- bind to hotkeys; WARNING: at least one modifier key is required!
22 | hotkey.bind(mods.cAsc, 'tab', function() module.switcher:next() end)
23 | hotkey.bind(mods.cASc, 'tab', function() module.switcher:previous() end)
24 | 
25 | return module


--------------------------------------------------------------------------------
/utils/_keys/viKeys.lua:
--------------------------------------------------------------------------------
 1 | local module = {}
 2 | 
 3 | module.debugging = false -- whether to print status updates
 4 | 
 5 | local eventtap  = require "hs.eventtap"
 6 | local event     = eventtap.event
 7 | local inspect   = require "hs.inspect"
 8 | local watchable = require "hs.watchable"
 9 | 
10 | 
11 | local keyHandler = function(e)
12 |     local watchFor = { h = "left", j = "down", k = "up", l = "right" }
13 |     local actualKey = e:getCharacters(true)
14 |     local replacement = watchFor[actualKey:lower()]
15 |     if replacement then
16 |         local isDown = e:getType() == event.types.keyDown
17 |         local flags  = {}
18 |         for k, v in pairs(e:getFlags()) do
19 |             if v and k ~= "fn" then -- fn will be down because that's our "wrapper", so ignore it
20 |                 table.insert(flags, k)
21 |             end
22 |         end
23 |         if module.debugging then print("viKeys: " .. replacement, inspect(flags), isDown) end
24 |         local replacementEvent = event.newKeyEvent(flags, replacement, isDown)
25 |         if isDown then
26 |             -- allow for auto-repeat
27 |             replacementEvent:setProperty(event.properties.keyboardEventAutorepeat, e:getProperty(event.properties.keyboardEventAutorepeat))
28 |         end
29 |         return true, { replacementEvent }
30 |     else
31 |         return false -- do nothing to the event, just pass it along
32 |     end
33 | end
34 | 
35 | local modifierHandler = function(e)
36 |     local flags = e:getFlags()
37 |     local onlyFNPressed = false
38 |     for k, v in pairs(flags) do
39 |         onlyFNPressed = v and k == "fn"
40 |         if not onlyFNPressed then break end
41 |     end
42 |     -- you must tap and hold fn by itself to turn this on
43 |     if onlyFNPressed and not module.keyListener then
44 |         if module.debugging then print("viKeys: keyhandler on") end
45 |         module.keyListener = eventtap.new({ event.types.keyDown, event.types.keyUp }, keyHandler):start()
46 |     -- however, adding additional modifiers afterwards is ok... its only when fn isn't down that we switch back off
47 |     elseif not flags.fn and module.keyListener then
48 |         if module.debugging then print("viKeys: keyhandler off") end
49 |         module.keyListener:stop()
50 |         module.keyListener = nil
51 |     end
52 |     return false
53 | end
54 | 
55 | module.watchables = watchable.new("viKeys", true)
56 | 
57 | module.modifierListener = eventtap.new({ event.types.flagsChanged }, modifierHandler)
58 | 
59 | module.start = function()
60 |     module.watchables.enabled = true
61 |     module.modifierListener:start()
62 | end
63 | 
64 | module.stop = function()
65 |     if module.keyListener then
66 |         module.keyListener:stop()
67 |         module.keyListener = nil
68 |     end
69 |     module.modifierListener:stop()
70 |     module.watchables.enabled = false
71 | end
72 | 
73 | module.toggle = function()
74 |     if module.watchable.enabled then
75 |         module.stop()
76 |     else
77 |         module.start()
78 |     end
79 | end
80 | 
81 | module.watchExternalToggle = watchable.watch("viKeys.enabled", function(w, p, k, o, n)
82 |     if not o and n then
83 |         module.start()
84 |     elseif o and not n then
85 |         module.stop()
86 |     end
87 | end)
88 | 
89 | module.start() -- autostart
90 | 
91 | return module
92 | 


--------------------------------------------------------------------------------
/utils/_keys/windowMemory.lua:
--------------------------------------------------------------------------------
 1 | local mods           = require("hs._asm.extras").mods
 2 | local hotkey         = require("hs.hotkey")
 3 | local fnutils        = require("hs.fnutils")
 4 | local mouse          = require("hs.mouse")
 5 | local geometry       = require("hs.geometry")
 6 | local window         = require("hs.window")
 7 | local alert          = require("hs.alert")
 8 | 
 9 | local alertStyle = { fillColor = { blue = .6, green = .5 } }
10 | 
11 | local pickWindow = function()
12 |     return window.frontmostWindow()
13 | --     return fnutils.find(window.orderedWindows(), function(_)
14 | --         return geometry.isPointInRect(mouse.getAbsolutePosition(), _:frame()) and _:isStandard()
15 | --     end)
16 | end
17 | 
18 | local _firstTime = true
19 | 
20 | local module = {}
21 | module._savedWindows = {}
22 | 
23 | local saveWindow = function(key)
24 |     local win = pickWindow()
25 |     if win then
26 |         module._savedWindows[key] = win
27 |         local app = win:application():name() or ""
28 |         local win = win:title()              or ""
29 |         alert(app .. ":" .. win .. " saved to slot " .. tostring(key), alertStyle)
30 |     else
31 |         alert("Unable to get window object", alertStyle)
32 |     end
33 | end
34 | 
35 | local gotoWindow = function(key)
36 |     local win = module._savedWindows[key]
37 |     if win then
38 |         if win:role() ~= "" then
39 |             win:focus()
40 |         else
41 |             module._savedWindows[key] = nil
42 |             alert("Window in slot " .. tostring(key) .. " is no longer valid", alertStyle)
43 |         end
44 |     else
45 |         alert("No window saved in slot " .. tostring(key), alertStyle)
46 |     end
47 | end
48 | 
49 | module._keys = hotkey.modal.new()
50 | module._keys.entered = function(self)
51 |     if not _firstTime then
52 |         alert("Window slot saver enable", alertStyle)
53 |         _firstTime = false
54 |     end
55 | end
56 | for i = 0, 9, 1 do
57 |     module._keys:bind(mods.CASC, tostring(i), function() saveWindow(i) end)
58 |     module._keys:bind(mods.CAsC, tostring(i), function() gotoWindow(i) end)
59 | end
60 | module._keys.exited = function(self)
61 |     alert("Window slot saver disabled", alertStyle)
62 | end
63 | 
64 | module.start = function() module._keys:enter() end
65 | module.stop  = function() module._keys:exit()  end
66 | 
67 | module.start()
68 | 
69 | return module
70 | 


--------------------------------------------------------------------------------
/utils/_menus/XProtectStatus/XProtectPluginChecker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/asmagill/hammerspoon-config/432c65705203d7743d3298441bd4319137b466fd/utils/_menus/XProtectStatus/XProtectPluginChecker.png


--------------------------------------------------------------------------------
/utils/_menus/XProtectStatus/myXProtectStatus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/asmagill/hammerspoon-config/432c65705203d7743d3298441bd4319137b466fd/utils/_menus/XProtectStatus/myXProtectStatus.png


--------------------------------------------------------------------------------
/utils/_menus/amphetamine.lua:
--------------------------------------------------------------------------------
 1 | -- variation on https://gist.github.com/heptal/50998f66de5aba955c00
 2 | 
 3 | local ampOnIcon = [[ASCII:
 4 | .....1a..........AC..........E
 5 | ..............................
 6 | ......4.......................
 7 | 1..........aA..........CE.....
 8 | e.2......4.3...........h......
 9 | ..............................
10 | ..............................
11 | .......................h......
12 | e.2......6.3..........t..q....
13 | 5..........c..........s.......
14 | ......6..................q....
15 | ......................s..t....
16 | .....5c.......................
17 | ]]
18 | 
19 | local ampOffIcon = [[ASCII:
20 | .....1a.....x....AC.y.......zE
21 | ..............................
22 | ......4.......................
23 | 1..........aA..........CE.....
24 | e.2......4.3...........h......
25 | ..............................
26 | ..............................
27 | .......................h......
28 | e.2......6.3..........t..q....
29 | 5..........c..........s.......
30 | ......6..................q....
31 | ......................s..t....
32 | ...x.5c....y.......z..........
33 | ]]
34 | 
35 | -- caffeine replacement
36 | local menubar    = require"hs.menubar"
37 | local caffeinate = require"hs.caffeinate"
38 | 
39 | local module = {}
40 | 
41 | local function setIcon(state)
42 |   module.menu:setIcon(state and ampOnIcon or ampOffIcon)
43 | end
44 | 
45 | module.menu = menubar.new()
46 | module.menu:setClickCallback(function() setIcon(caffeinate.toggle("displayIdle")) end)
47 | setIcon(caffeinate.get("displayIdle"))
48 | 
49 | return module


--------------------------------------------------------------------------------
/utils/_menus/applicationMenu.lua:
--------------------------------------------------------------------------------
 1 | -- This example is for an Application launching menu  Most of the necessary
 2 | -- settings for this menu are already the defaults, but it serves as a simple
 3 | -- example none-the-less.
 4 | 
 5 | local FLM = require("hs._asm.filelistmenu")
 6 | 
 7 | -- create the application menu and give it a default label
 8 | local appMenu = FLM.new("Apps") ;
 9 | 
10 | -- The defaults for filelistmenu will create an Application based menu starting from
11 | -- the /Applications directory.  The commands listed here are included so you can more
12 | -- easily see what is actually being setup, but they aren't really necessary for
13 | -- this default behavior, so they are commented out.
14 | 
15 | -- Show an icon, if one is provided
16 | --appMenu:showForMenu("icon")
17 | 
18 | -- The match criteria here is a string which matches any name which ends in .app.  The
19 | -- parenthesis are included to indicate the portion of the name to use as the menu items
20 | -- label... without the parenthesis, the full name matched would be used.
21 | --appMenu:menuCriteria("^([^/]+)%.app$")
22 | 
23 | -- This function indicates what action should occur when a menu item is selected
24 | --appMenu:actionFunction(function(x) hs.application.launchOrFocus(x) end)
25 | 
26 | -- This function indicates what action should occur when a subfolder itself is selected
27 | --appMenu:folderFunction(function(x) os.execute([[open -a Finder "]]..x..[["]]) end)
28 | 
29 | -- Specify the root directory to start from.
30 | --appMenu:rootDirectory("/Applications")
31 | 
32 | -- The maximum folder depth that we will search for files or folders which match the
33 | -- criteria.  This prevents potential loops, which would ultimately crash HS.
34 | appMenu:subFolderDepth(12)
35 | 
36 | -- If false, then warning messages will not be printed to the HS console.
37 | appMenu:showWarnings(true)
38 | 
39 | -- Define an icon using ASCIIArt.  See hs.drawing and
40 | -- http://cocoamine.net/blog/2015/03/20/replacing-photoshop-with-nsstring/
41 | appMenu:menuIcon("ASCII:....................\n"..
42 |                        "............1..4....\n"..
43 |                        "....................\n"..
44 |                        "....................\n"..
45 |                        "....................\n"..
46 |                        "....................\n"..
47 |                        "....................\n"..
48 |                        "....................\n"..
49 |                        "....................\n"..
50 |                        "....................\n"..
51 |                        "....................\n"..
52 |                        "...B...............A\n"..
53 |                        "8...............9...\n"..
54 |                        "....................\n"..
55 |                        "....................\n"..
56 |                        "....................\n"..
57 |                        "...2...........5....\n"..
58 |                        "....................\n"..
59 |                        "...3..........6.....\n"..
60 |                        "....................")
61 | 
62 | -- Sort folder items before file items in the menu
63 | appMenu:subFolders("before")
64 | 
65 | -- activate the menu
66 | appMenu:activate()
67 | 
68 | -- This allows you to include this file like 'menu = require(...)' and capture the
69 | -- menu object in case you want to manipulate it elsewhere.
70 | 
71 | appMenu:itemImages(true)
72 | 
73 | return appMenu
74 | 


--------------------------------------------------------------------------------
/utils/_menus/documentsMenu.lua:
--------------------------------------------------------------------------------
 1 | -- In this example we create a menu of the users Documents folder.  We
 2 | -- want to match all files and folders (except for dot-files)
 3 | 
 4 | local FLM =  require("hs._asm.filelistmenu")
 5 | local hsfs = require("hs.fs")
 6 | local eventtap = require("hs.eventtap")
 7 | 
 8 | -- Here we define an action function which takes the modifiers pressed when the
 9 | -- menu is clicked on so we can choose what action to perform.  This action function
10 | -- is used for both Files and Folders
11 | local actionFunction = function(x)
12 |     local mods = eventtap.checkKeyboardModifiers()
13 |     if mods["cmd"] then
14 |         os.execute([[/usr/local/bin/edit "]]..x..[["]])
15 |     else
16 |         os.execute([[open -a Finder "]]..x..[["]])
17 |     end
18 | end
19 | 
20 | local docMenu = FLM.new("Docs") ;
21 | 
22 | -- Here we define the match criteria as a function.  The function receives 3 arguments
23 | -- and returns up to 2.  The arguments passed in are the file name (without path),
24 | -- the path (without the file at the end), and the purpose of this call, which will be
25 | -- "file"      -- indicates we're matching files (i.e. menu end nodes)
26 | -- "directory" -- indicates we're matching folders (i.e. potential submenus)
27 | -- "update"    -- indicates we're matching against the results of hs.pathwatcher for
28 | --                potential updates to the menu.
29 | 
30 | -- Note that unlike a string criteria, when a function is used, file matches are not
31 | -- automatically exempted from subfolder matches -- this allows more flexibility when
32 | -- it comes to OS X bundle types (like .app)
33 | 
34 | -- Returns 'boolean, label' where boolean will be true if we should consider this file
35 | -- a match or false if we should skip it.  Label is what will be put in the menu and is
36 | -- optional when the boolean value is false.
37 | docMenu:menuCriteria(function(file, path, purpose)
38 | 
39 |       -- ignore dot files
40 |       if string.match(file, "^%..*$") then return false end
41 | 
42 |       -- For file checks, we want to ignore directories
43 |       if purpose == "file" then
44 |           if hsfs.attributes(path.."/"..file, "mode") == "directory" then return false end
45 |           return true, file -- otherwise, return true and file as label
46 | 
47 |       -- We want all folders as well, when looking for them
48 |       elseif purpose == "directory" then
49 |           if hsfs.attributes(path.."/"..file, "mode") == "directory" then return true, file end
50 |           return false -- otherwise, return false
51 | 
52 |       -- And any update which makes it this far should also be accepted
53 |       elseif purpose == "update" then
54 |           return true, file
55 |       end
56 |     end
57 | )
58 | 
59 | docMenu:actionFunction(actionFunction)
60 | docMenu:folderFunction(actionFunction)
61 | docMenu:rootDirectory(os.getenv("HOME").."/Documents")
62 | 
63 | docMenu:showWarnings(false)
64 | 
65 | docMenu:menuIcon("ASCII:....................\n"..
66 |                        "....................\n"..
67 |                        "...c........c.......\n"..
68 |                        "...dt....tri12......\n"..
69 |                        "....v...............\n"..
70 |                        "...............3....\n"..
71 |                        "...........i...4b...\n"..
72 |                        "..........rg...g....\n"..
73 |                        "..........p...pm....\n"..
74 |                        "....................\n"..
75 |                        "....................\n"..
76 |                        "....................\n"..
77 |                        "....................\n"..
78 |                        "....................\n"..
79 |                        "....................\n"..
80 |                        "....v..........m....\n"..
81 |                        "...dk..........kb...\n"..
82 |                        "...a............a...\n"..
83 |                        "....................\n"..
84 |                        "....................")
85 | 
86 | docMenu:subFolders("mixed")
87 | 
88 | --docMenu:activate()
89 | 
90 | return docMenu


--------------------------------------------------------------------------------
/utils/_panels/infoPanel.lua:
--------------------------------------------------------------------------------
 1 | local slidingPanels = hs.loadSpoon("SlidingPanels")
 2 | 
 3 | slidingPanels:addPanel("infoPanel", {
 4 |     side              = "top",
 5 |     size              = 1/3,
 6 |     modifiers         = { "fn" },
 7 |     persistent        = true,
 8 |     animationDuration = 0.1,
 9 |     color             = { white = .35 },
10 |     fillAlpha         = .95,
11 | }):enable()
12 | 
13 | -- the "FromSpoon" widget takes the following arguments:
14 | --
15 | --  * spoonName    - string
16 | --  * frameDetails - table containing frameDetails describing where to place canvas within panel (see guitk.manager)
17 | --  * spoonConfig  - table
18 | --    * canvas     - string specifying the name of the canvas as stored within the spoon (usually "canvas" or similar)
19 | --    * start      - string naming method to invoke to start/show/build the canvas or function(spoon) ... end which does the starting
20 | --    * vars       - table of key-value pairs for spoon to be set before "start", if present, is invoked
21 | --    * background - a table specifying a canvas element, e.g. a rectangle or an image, to use as the background for the spoon canvas. This can be useful to provide contrast if the spoon's coloring is hard to see against the panel's background.
22 | 
23 | slidingPanels:panel("infoPanel"):addWidget("FromSpoon", "HCalendar",      { rX = "100%", bY = "100%" })
24 | 
25 | slidingPanels:panel("infoPanel"):addWidget("FromSpoon", "CircleClock",    { rX = "100%",  y = 0 }, {
26 |     background = {
27 |         type             = "rectangle",
28 |         action           = "fill",
29 |         roundedRectRadii = { xRadius = 20, yRadius = 20 },
30 |         fillColor        = { red = .4, blue = .32, green = .32, alpha = .7 },
31 |     },
32 | })
33 | 
34 | slidingPanels:panel("infoPanel"):addWidget("FromSpoon", "MountedVolumes", {  x = 0, bY = "100%" }, {
35 |     start = function(spoon)
36 |         spoon.textStyle.font.size = 9 -- easier then spelling out entire style in vars
37 |                                       -- need to think about separating out font from style in spoon
38 |         spoon:show()
39 |     end,
40 |     vars  = { cornerRadius = 20, },
41 | })
42 | 
43 | slidingPanels:panel("infoPanel"):addWidget("FromSpoon", "CPUMEMBAT", {  x = 0, y = 0 }, {
44 |     start = "show", -- could also be `function(spoon) spoon:show() end`
45 |     vars  = {
46 |         checkInterval = 10,
47 |         baseFont = { name = "Menlo", size = 10 },
48 |     },
49 | })
50 | 
51 | return slidingPanels:panel("infoPanel")
52 | 


--------------------------------------------------------------------------------
/utils/gc.lua:
--------------------------------------------------------------------------------
 1 | local module = {}
 2 | 
 3 | local seen_GCs = {}
 4 | 
 5 | local __gc_replacement = function(modName, originalFN)
 6 |     seen_GCs[modName] = originalFN
 7 |     return function(...)
 8 | --         print("~~ " .. os.date("%Y-%m-%d %H:%M:%S") .. " : invoking " .. tostring(modName) .. ".__gc")
 9 |         print("~~ " .. timestamp() .. " : invoking " .. tostring(modName) .. ".__gc")
10 |         originalFN(...)
11 |     end
12 | end
13 | 
14 | module.patch = function(k)
15 |     local mt = hs.getObjectMetatable(k)
16 |     if mt and mt.__name and not mt.__gc then
17 |     -- does nothing because the object had no __gc, but we need something for this to work
18 |         mt.__gc = function(self) end
19 |     end
20 |     if mt and type(mt.__gc) == "function" then
21 |         if type(seen_GCs[k]) == "function" then
22 |             print("~~ " .. tostring(k) .. " already patched")
23 |         else
24 |             seen_GCs[k] = mt.__gc
25 |             print("~~ patching " .. tostring(k))
26 |             mt.__gc = __gc_replacement(k, mt.__gc)
27 |             return true
28 |         end
29 |     else
30 |         print("~~ " .. tostring(k) .. " does not have a registered metatable with a __gc function")
31 |     end
32 |     return false
33 | end
34 | 
35 | -- note this can make garbage collection *VERY* slow if you haven't previously ignored a lot of things.
36 | -- better to call `patch` on the specific ones you want to watch
37 | module.patchAll = function()
38 |     for k, v in pairs(debug.getregistry()) do
39 |         if type(v) == "table" then
40 |             if type(v.__gc) == "function" and not seen_GCs[k] then
41 |                 module.patch(k)
42 |             elseif type(seen_GCs[k]) == "boolean" then
43 |                 print("~~ skipping " .. tostring(k))
44 |             end
45 |         end
46 |     end
47 | end
48 | 
49 | module.revert = function(k)
50 |     if type(seen_GCs[k]) == "function" then
51 |         local mt = hs.getObjectMetatable(k)
52 |         if mt then
53 |             mt.__gc = seen_GCs[k]
54 |             seen_GCs[k] = true
55 |             print("~~ reverting " .. tostring(k))
56 |             return true
57 |         else
58 |             print("~~ " .. tostring(k) .. " does not have a metatable")
59 |         end
60 |     else
61 |         print("~~ " .. tostring(k) .. " is not patched")
62 |     end
63 |     return false
64 | end
65 | 
66 | module.ignore = function(...)
67 |     local args = table.pack(...)
68 |     if type(args[1]) == "table" and args.n == 1 then
69 |         args = args[1]
70 |     end
71 |     for i, v in ipairs(args) do
72 |         if type(seen_GCs[v]) == "function" then
73 |             module.revert(v)
74 |         end
75 |         print("~~ ignoring " .. tostring(v))
76 |         seen_GCs[v] = true
77 |     end
78 | end
79 | 
80 | module.__originals = seen_GCs
81 | 
82 | return module
83 | 


--------------------------------------------------------------------------------
/utils/watchables.lua:
--------------------------------------------------------------------------------
 1 | local module = {}
 2 | 
 3 | -- common watchers... why replicate what so many of my "modlets" care about?
 4 | 
 5 | local reachability = require"hs.network.reachability"
 6 | local screen       = require"hs.screen"
 7 | local watchable    = require"hs.watchable"
 8 | local caffeinate   = require"hs.caffeinate"
 9 | local configuration = require"hs.network.configuration"
10 | 
11 | module.generalStatus = watchable.new("generalStatus")
12 | 
13 | module.generalStatus.internet = (reachability.internet():status() & reachability.flags.reachable) > 0
14 | module.internetWatcher = reachability.internet():setCallback(function(obj, status)
15 |     module.generalStatus.internet = (status & reachability.flags.reachable) > 0
16 | end):start()
17 | 
18 | module.generalStatus.activeScreenChanges = 0
19 | module.generalStatus.activeSpaceChanges = 0
20 | module.activeScreenSpaceWatcher = screen.watcher.newWithActiveScreen(function(screenOrSpace)
21 |     if screenOrSpace then
22 |         module.generalStatus.activeScreenChanges = module.generalStatus.activeScreenChanges + 1
23 |     else
24 |         module.generalStatus.activeSpaceChanges = module.generalStatus.activeSpaceChanges + 1
25 |     end
26 | end):start()
27 | 
28 | module.generalStatus.caffeinatedState = 0
29 | module.caffeinatedStateWatcher = caffeinate.watcher.new(function(event)
30 |     module.generalStatus.caffeinatedState = event
31 | end):start()
32 | 
33 | local vpnQueryKey = "State:/Network/Interface/utun[0-9]+/IPv4"
34 | local verifyOurVPNisUp = function()
35 |     local status = false
36 |     if module.vpnWatcher then
37 |         for k, v in pairs(module.vpnWatcher:contents(vpnQueryKey, true)) do
38 |             for i2, v2 in ipairs(v["Addresses"]) do
39 |                 if v2:match("^10%.161%.82%.") then
40 |                     status = true
41 |                     break
42 |                 end
43 |             end
44 |             if status then break end
45 |         end
46 |     end
47 |     module.generalStatus.privateVPN = status
48 | end
49 | module.vpnWatcher = configuration.open():setCallback(verifyOurVPNisUp)
50 |                                         :monitorKeys(vpnQueryKey, true)
51 |                                         :start()
52 | verifyOurVPNisUp()
53 | 
54 | return module
55 | 


--------------------------------------------------------------------------------