├── CHANGELOG.md ├── LICENCE ├── README.md ├── hellfire ├── hellfire-context-manager.lua ├── hellfire-key-standard.lua ├── hellfire-keycaster.lua ├── hellfire-keylogger-listener.lua ├── hellfire-modes.lua └── hellfire.lua ├── hellfred-bootstrap.lua ├── hellfuzz ├── hellfuzz-chooser.lua ├── hellfuzz-helpers.lua └── hellfuzz.lua ├── hellprompt ├── hellprompt-keycaster.lua └── hellprompt.lua ├── quick-start.lua ├── resources ├── Hellfire.png ├── Hellfuzz.png ├── Hellprompt.png ├── hackernoon-article.png ├── hellfred-banner-id534.png ├── hellfred-logo-orange-512x512.png ├── hellfred-logo-orange-96x96.png └── hellfuzz-example-apps-struct.png └── utils ├── keylogger.lua └── shift-map.lua /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Hellfred Changelog 2 | All notable changes to this project will be documented in this file. 3 | Date formats follow the [ISO standard](https://www.iso.org/iso-8601-date-and-time-format.html) of YYYY-MM-DD 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | 9 | ## [1.0.0]() (2022-08-15) 10 | ## Removed 11 | - **Breaking:** remove `enter()` and `exit()` methods from the Hellfire and Hellprompt public APIs 12 | 13 | ## [0.1.0]() (2022-08-04) 14 | 15 | ## Added 16 | - Quickstart configuration 17 | - **Hellfire**: Add Hellfire app 18 | - **Hellfuzz**: Add Hellfuzz app 19 | - **Hellprompt**: Add Hellprompt app -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Brad Blundell 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Hellfred. `Hellishly productive.` 4 | > Built on Hammerspoon for macOS automation. 5 | 6 |
7 | 8 | ![hellfred banner](resources/hellfred-banner-id534.png) 9 | 10 | ### Contents 11 | - [What the hell is Hellfred?](#what-the-hell-is-hellfred) 12 | - [The Apps](#the-apps) 13 | - [Installation: Firestarter](#installation-firestarter) 14 | - [Bootstrap: Light it up](#bootstrap-light-it-up) 15 | - [Tutorial: A basic setup](#tutorial-a-basic-setup) 16 | 17 | ## What the hell is Hellfred? 18 | Hellfred is a collection of 3 mini-apps built on top of Hammerspoon so you can automate tasks, boost productivity and eliminate time-suck by programming shortcuts into your daily workflows. 19 | 20 | > It's a way to map repetitive time consuming tasks to key sequences, commands or searchable texts. 21 | 22 | Each app is a different **style of interface** that map `triggers` to the `behaviours or actions` they should perform. 23 | 24 | ![hellfuzz](resources/Hellfuzz.png) 25 | *Hellfuzz. A fuzzy search, choice-to-behaviour mapper.* 26 | 27 | ![hellfire](resources/Hellfire.png) 28 | *Hellfire. A keys-to-behaviour mapper.* 29 | 30 | ![hellfire](resources/Hellprompt.png) 31 | *Hellprompt. A command-to-behaviour mapper.* 32 | 33 | ### Triggers and callbacks 34 | | App | Trigger(s) | Callback (Behaviour) | 35 | | ------------- | -------- | ------------------- | 36 | | Hellfire | | User defined function | 37 | | Hellfuzz | A searchable list of choices.
Selecting a choice fires the trigger. | User defined function
Choice metadata is passed to it| 38 | | Hellprompt | A text command | User defined function
The command is passed to it | 39 | 40 | 41 | 42 | ### Subscribers 43 | The `trigger` and `callback` are user-defined by wrapping them inside simple configuration objects. These objects, act as `subscribers` and are registered with the respective app and notified whenever something important happens inside the app. 44 | 45 | 46 | 47 | ## The Apps 48 | ### Hellfire 49 | >A quick-fire, mode-based, hotkey-to-action mapping utility. 50 | >- Supports single key triggers as well as key chord sequences as triggers. 51 | >- Exposes virtually every key on the keyboard including modifier keys to use as triggers. 52 | >- Modes allow the same trigger to have different behaviours under different contexts. 53 | 54 | English, please? 55 | > When I type a character or a sequence of characters, then execute a specific function, but only if I am in a particular mode! 56 | 57 | Ok. An example maybe? 58 | >- When I type c then run function `launchGoogleChromeApp` (but only if I am in Default Mode) 59 | >- When I type w+m then run function `changeToWindowManagerMode` (again, whilst in Default Mode) 60 | >- When I type c then run function `centerWindowOnScreen` (whilst in WindowManager mode) 61 | 62 | ### Hellfuzz 63 | >A fuzzy-search chooser utility with choice-to-action mapping. 64 | >- Supports multi-level (nested) choice sets. 65 | 66 | English, please? 67 | > When I search through a list of choices and select one, then execute a specific function.
68 | If my choice has subchoices (think:`parent->children`), then show me those so I can search through them. 69 | 70 | Ok. An example maybe?
71 | >Suppose you have this structure: 72 | hellfuzz-example-apps-struct.png 73 | ![hellfuzz example nested structure](resources/hellfuzz-example-apps-struct.png) 74 | 75 | >- When I type '**goog**', then highlight the choice '**Open Google**'. Selecting this option will execute the function `openGoogleInBrowser`. 76 | >- Alternatively, if I type '**app**', then highlight the choice '**Launch Apps**'. Selecting this option will replace the current choices with **Terminal, Notes, and Calendar** (the subset of choices for **Launch Apps**) 77 | >- When I fuzzy search through those and select one, Hellfuzz will execute `launchOrOpenApp` with the selected app. 78 | 79 | 80 | ### Hellprompt 81 | >A commandline-like utility with basic string match support. 82 | 83 | English, please? 84 | >When I type out a command and submit it, then inspect my command for any matching string patterns and execute functions related to that command. 85 | 86 | Ok. An example maybe? 87 | >- When I type the command '**open notes**' and then hit enter, then execute any function with a `filter` (e.g. command must start with the word '**open**') and behaviour (e.g. open app associated with '**notes**') suitable to open the Notes app. 88 | >- When I type '**browse github**' and then hit enter, then execute any function with a filter (e.g. command starts with the word '**browse**') and behaviour (open url associated with '**github**') suitable to open the link. 89 | 90 | 91 | 92 | ## Installation: Firestarter 93 | 1. Download and install [Hammerspoon](https://github.com/Hammerspoon/hammerspoon/releases) 94 | 2. Install Hellfred: Clone the repository to your `~/.hammerspoon` directory: 95 | ```bash 96 | git clone https://github.com/braddevelop/hellfred.git ~/.hammerspoon 97 | ``` 98 | 99 | 100 | ## Bootstrap: Light it up 101 | 102 | There is a **bootstrap file** for Hellfred with a pre-configured setup. Let's reference it in Hammerspoon's `init.lua` file 103 | 104 | ```lua 105 | -- ~/.hammerspoon/init.lua 106 | 107 | require('hellfred.hellfred-bootstrap') 108 | ``` 109 | 110 | Save the file and reload the configuration (or save yourself some time and use [fancy reload](http://www.hammerspoon.org/go/#fancyreload)) 111 | 112 | ### What's in the box? Try out the pre-configuration 113 | Out-the-box the three Hellfred apps are ready to use and are pre-configured with a quick-start example. Let's test it out to make sure everything is wiring and firing. 114 | 115 | #### Try Hellfire 116 | - Open **Hellfire** with the hotkey shift++h 117 | - Type the character c 118 | - The repo for Hellfred will open in a browser. 119 | 120 | #### Try Hellfuzz 121 | - Open **Hellfuzz** with the hotkey shift++h 122 | - Type in '**wiki**' 123 | - This highlights the option '**Open Hellfred wiki**' 124 | - Press enter and the wiki for Hellfred will open in a browser 125 | 126 | #### Try Hellprompt 127 | - Open **Hellprompt** with the hotkey shift+^+h 128 | - Type '**open code**' 129 | - Hit enter and the repo for Hellfred will open in a browser 130 | 131 | What **Hellfire**, **Hellfuzz** and **Hellprompt** achieve is map a **trigger** or **input** to an **action** or **behaviour**, `if-this-then-that`, and whilst we have just demonstrated using each app to achieve the same outcome, you will find each app more suited to certain use cases than others. 132 | 133 | ## Tutorial: A basic setup 134 | > For a walkthrough of the basics, see [this article](https://hackernoon.com/hellfred-or-how-i-learned-to-automate-macos-and-become-hellishly-productive) on Hackernoon. 135 | 136 | [![top-story-on-hackernoon](resources/hackernoon-article.png)](https://hackernoon.com/hellfred-or-how-i-learned-to-automate-macos-and-become-hellishly-productive) 137 | 138 | 139 | The final code can be found on the `basics` branch: 140 | 141 | ```bash 142 | git checkout basics 143 | ``` 144 | 145 | 146 | ## Extensions: Add fuel to the fire. 147 | Look out for upcoming Hellfred experiments and extensions on the repo by checking out the `extend` branch. 148 | 149 | ```bash 150 | git checkout extend 151 | ``` 152 | ## Info 153 | 154 | | Compatibility | Version | 155 | | ------------- | -------- | 156 | | Hammerspoon | Tested on Version 0.9.97 (6267) | -------------------------------------------------------------------------------- /hellfire/hellfire-context-manager.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | Hellfire context manager 4 | 5 | Handles events from a keylogger listener, pre-processes the 6 | events and then enriches with context before passing it onto subscribers 7 | 8 | --]] 9 | 10 | -------------------------------------------------------------------------- 11 | -- Public namespace 12 | -------------------------------------------------------------------------- 13 | local module = {} 14 | 15 | -------------------------------------------------------------------------- 16 | -- Private namespace 17 | -------------------------------------------------------------------------- 18 | local _internal = { 19 | subscribers = {}, 20 | keyloggerListener = require('hellfred.hellfire.hellfire-keylogger-listener'), 21 | mode = { name = 'None' } 22 | } 23 | 24 | function _internal.onKeyLogEvent(sequence) 25 | local context = { 26 | mode = _internal.mode, 27 | trigger = sequence, 28 | triggerAsString = hs.fnutils.reduce(sequence, function(n, m) return n .. m end), 29 | app = hs.window.frontmostWindow():application(), 30 | window = hs.window.frontmostWindow() 31 | } 32 | 33 | for _, subscriber in pairs(_internal.subscribers) do 34 | subscriber(context) 35 | end 36 | end 37 | 38 | function _internal.init() 39 | _internal.keyloggerListener.addSubscriber(_internal.onKeyLogEvent) 40 | _internal.keyloggerListener.setMode(_internal.mode) 41 | return module 42 | end 43 | 44 | -------------------------------------------------------------------------- 45 | -- Public API 46 | -------------------------------------------------------------------------- 47 | function module.start() 48 | _internal.keyloggerListener.start() 49 | end 50 | 51 | function module.stop() 52 | _internal.keyloggerListener.stop() 53 | end 54 | 55 | --- 56 | --- Add a subscriber to internal subscriber list 57 | --- 58 | function module.addSubscriber(subscriber) 59 | table.insert(_internal.subscribers, subscriber) 60 | end 61 | 62 | --- 63 | --- Set the operating mode 64 | --- @param mode table 65 | --- 66 | function module.setMode(mode) 67 | _internal.mode = mode or { name = 'None' } 68 | _internal.keyloggerListener.setMode(_internal.mode) 69 | end 70 | 71 | return _internal.init() -- returns module 72 | -------------------------------------------------------------------------------- /hellfire/hellfire-key-standard.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | Hellfire key standard 4 | 5 | Converts different representations of key and key chord 6 | sequences to a standardised representation 7 | 8 | --]] 9 | 10 | -------------------------------------------------------------------------- 11 | -- Public namespace 12 | -------------------------------------------------------------------------- 13 | local module = {} 14 | 15 | -------------------------------------------------------------------------- 16 | -- Private namespace 17 | -------------------------------------------------------------------------- 18 | local _internal = { 19 | modifierList = { 'cmd', 'shift', 'ctrl', 'alt', 'capslock'}, -- supported modifier strings. Ignore `fn` for Hellfire (#documentation) 20 | modMap = { shift = '⇧', ctrl = '⌃', alt = '⌥', cmd = '⌘', capslock = '⊼', fn = 'ƒ' }, 21 | shiftMap = require('hellfred.utils.shift-map') 22 | } 23 | 24 | --- 25 | --- Convert a modifier string to a symbol 26 | --- 27 | function _internal.convertModifierToSymbol(modifier) 28 | return _internal.modMap[modifier] 29 | end 30 | 31 | --- 32 | --- Takes a human readable version of a keychord and standardises it 33 | --- e.g. shift+h 34 | --- 35 | function _internal.stringToStandardKeyChord(keyChord) 36 | -- Case: if user defines a keychord of 'shift++' for example. 37 | -- Remove '+' globally 38 | keyChord = keyChord:lower():gsub('%+%+', "+="):gsub('%+', "") 39 | 40 | local standardised = '' 41 | 42 | -- Search for modifiers in the string. 43 | -- If found, note the modifier in the standard to be returned 44 | -- and then strip the modifier out. 45 | for _, m in pairs(_internal.modifierList) do 46 | if string.find(keyChord, m) then 47 | standardised = standardised .. m 48 | keyChord = keyChord:gsub(m, "") 49 | end 50 | end 51 | 52 | -- canonicalise characters e.g. "!" => "1" 53 | for k, v in pairs(_internal.shiftMap) do 54 | if keyChord == v then 55 | keyChord = k 56 | break 57 | end 58 | end 59 | 60 | standardised = standardised .. keyChord 61 | return standardised 62 | end 63 | 64 | --- 65 | --- Takes a hs.eventtap.event and standardises it 66 | --- 67 | function _internal.eventTapEventToStandardKeyChord(event) 68 | local standardised = '' 69 | 70 | -- search for supported modifiers in the eventTap event 71 | local eventmodifiers = hs.eventtap.checkKeyboardModifiers() 72 | for _, m in pairs(_internal.modifierList) do 73 | if eventmodifiers[m] then standardised = standardised .. m end 74 | end 75 | 76 | local character = hs.keycodes.map[event:getKeyCode()] 77 | standardised = standardised .. character 78 | 79 | return string.lower(standardised) 80 | end 81 | 82 | function _internal.keyChordSequenceToStandard(sequence) 83 | local standardised = '' 84 | for _, v in pairs(sequence) do 85 | standardised = standardised .. _internal.stringToStandardKeyChord(v) 86 | end 87 | return standardised 88 | end 89 | 90 | -------------------------------------------------------------------------------- 91 | -- Public API 92 | -------------------------------------------------------------------------------- 93 | 94 | --- 95 | --- Standardise a key or key chord 96 | --- 97 | --- Accepts a keychord, whether generated from an hs.eventtap.event 98 | --- or supplied as a string and outputs a standardised version 99 | --- Value can be either: 100 | --- userdata from a keylogger eventtap event 101 | --- string like "cmd+shift+v" 102 | --- 103 | --- Outputs string => "cmdshiftv" 104 | --- 105 | --- @param keyChord any 106 | --- @return string 107 | --- 108 | function module.standardiseKeyChord(keyChord) 109 | local standardised = '' 110 | if type(keyChord) == 'userdata' then -- eventtap event 111 | standardised = _internal.eventTapEventToStandardKeyChord(keyChord) 112 | elseif type(keyChord) == 'string' then -- string e.g.'cmd+g' 113 | standardised = _internal.stringToStandardKeyChord(keyChord) 114 | end 115 | 116 | return string.lower(standardised) 117 | end 118 | 119 | --- 120 | --- Converts a standardised keychord to a prettified output 121 | --- 122 | --- E.g. 123 | --- "cmdshiftx" to ⌘⇧X 124 | --- "cmdx" to ⌘x 125 | --- "cmdshift1" to ⌘⇧! 126 | --- 127 | --- @param standardised any 128 | --- @return string 129 | --- 130 | function module.standardToHumanPrettified(standardised) 131 | local human = '' 132 | local isShifted = string.find(standardised, 'shift') or string.find(standardised, 'capslock') 133 | 134 | for _, m in pairs(_internal.modifierList) do 135 | if string.find(standardised, m) then 136 | if m ~= 'shift' then 137 | human = human .. _internal.convertModifierToSymbol(m) 138 | end 139 | standardised = standardised:gsub(m, "") 140 | end 141 | end 142 | 143 | -- if is a shiftable sequence then check the shiftmap 144 | if isShifted then 145 | for k, v in pairs(_internal.shiftMap) do 146 | if standardised == k then 147 | standardised = v 148 | break 149 | end 150 | -- if an alpha char then uppercase 151 | standardised = string.upper(standardised) 152 | end 153 | end 154 | 155 | human = human .. standardised 156 | return human 157 | end 158 | 159 | --- 160 | --- Compare two key/keychord sequences for equality 161 | --- 162 | --- @param keyChordSequenceA any 163 | --- @param keyChordSequenceB any 164 | --- @return boolean `true` if they are equivalent, `false` if otherwise 165 | --- 166 | function module.compare(keyChordSequenceA, keyChordSequenceB) 167 | return _internal.keyChordSequenceToStandard(keyChordSequenceA) == _internal.keyChordSequenceToStandard(keyChordSequenceB) 168 | end 169 | 170 | return module -------------------------------------------------------------------------------- /hellfire/hellfire-keycaster.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | Hellfire Keycaster 4 | 5 | A keystroke visualiser. 6 | Inspired by https://github.com/keycastr/keycastr 7 | 8 | --]] 9 | 10 | -------------------------------------------------------------------------------- 11 | -- Public namespace 12 | -------------------------------------------------------------------------------- 13 | local module = {} 14 | 15 | -------------------------------------------------------------------------------- 16 | -- Private namespace 17 | -------------------------------------------------------------------------------- 18 | local _internal = { 19 | defaultChooserImage = hs.image.imageFromPath("hellfred/resources/hellfred-logo-orange-96x96.png"), 20 | canvas = nil, 21 | casterText = nil, 22 | modeTextElement = nil, 23 | frameWidth = 1200.0, 24 | frameHeight = 500.0, -- 200:bottom; 500:middle; 900:top 25 | barWidth = 1200, 26 | barHeight = 55, 27 | casterTextSize = 20, 28 | modeTextSize = 20, 29 | style = { 30 | fonts = { 31 | primary = "Menlo" 32 | }, 33 | colours = { 34 | black = { hex = "#000000" }, 35 | white = { hex = "#FFFFFF" }, 36 | primary = { hex = "#E43C26" }, -- intense orange 37 | secondary = { hex = "#F1B51C" }, -- yellow 38 | } 39 | }, 40 | } 41 | 42 | --- 43 | --- Initialise elements of the visualiser interface 44 | --- 45 | --- @return table The key caster object 46 | --- 47 | function _internal.init() 48 | local mainScreen = hs.screen.mainScreen() 49 | local mainRes = mainScreen:fullFrame() 50 | 51 | if not _internal.canvas then 52 | _internal.canvas = hs.canvas.new({ x = 0, y = 0, w = 0, h = 0 }) 53 | end 54 | 55 | -- Background 56 | _internal.canvas[1] = { 57 | action = "fill", 58 | fillColor = _internal.style.colours.black, 59 | frame = { h = _internal.barHeight, w = _internal.barWidth, x = 0.0, y = 0.0 }, 60 | type = "rectangle" 61 | } 62 | _internal.canvas[1].fillColor.alpha = 0.9 63 | 64 | -- Caster text 65 | _internal.canvas[2] = { 66 | type = "text", 67 | text = "", 68 | textFont = _internal.style.fonts.primary, 69 | textSize = _internal.casterTextSize, 70 | textColor = _internal.style.colours.secondary, 71 | textAlignment = "center", 72 | frame = { x = 30.0, y = 16.0, h = _internal.barHeight, w = _internal.barWidth } 73 | } 74 | _internal.casterText = _internal.canvas[2] 75 | 76 | -- Mode title 77 | _internal.canvas[3] = { 78 | type = "text", 79 | text = "MODE:", 80 | textFont = _internal.style.fonts.primary, 81 | textSize = 10, 82 | textColor = { hex = "#CCCCCC" }, 83 | frame = { x = 80.0, y = 20.0, h = 100, w = 100 } 84 | } 85 | 86 | -- Mode text 87 | _internal.canvas[4] = { 88 | type = "text", 89 | text = "No mode set", 90 | textFont = _internal.style.fonts.primary, 91 | textSize = 14, 92 | textColor = _internal.style.colours.white, 93 | frame = { x = 120.0, y = 18.0, h = 100, w = 500 } 94 | } 95 | _internal.modeTextElement = _internal.canvas[4] 96 | 97 | -- Hellfred logo 98 | _internal.canvas[5] = { 99 | type = "image", 100 | image = _internal.defaultChooserImage, 101 | frame = { x = 12.0, y = 0.0, h = 48, w = 48 } 102 | } 103 | 104 | _internal.canvas:frame({ 105 | x = (mainRes.w - _internal.frameWidth) / 2, 106 | y = (mainRes.h - _internal.frameHeight), 107 | w = _internal.frameWidth, 108 | h = _internal.frameHeight 109 | }) 110 | 111 | return module 112 | end 113 | 114 | -------------------------------------------------------------------------------- 115 | -- Public API 116 | -------------------------------------------------------------------------------- 117 | 118 | --- 119 | --- Show the key caster 120 | --- 121 | function module.show() 122 | _internal.canvas:show() 123 | end 124 | 125 | --- 126 | --- Hide the key caster 127 | --- 128 | function module.hide() 129 | _internal.canvas:hide() 130 | end 131 | 132 | --- 133 | --- Set the mode 134 | --- @param mode string 135 | --- 136 | function module.setMode(mode) 137 | _internal.modeTextElement.text = mode.name 138 | 139 | if mode.style and mode.style.background then 140 | _internal.canvas[1].fillColor = mode.style.background.color -- main background 141 | end 142 | 143 | if mode.style and mode.style.casterText then 144 | _internal.canvas[2].textColor = mode.style.casterText.color -- caster text 145 | end 146 | 147 | if mode.style and mode.style.modeDisplay then 148 | _internal.canvas[3].textColor = mode.style.modeDisplay.color -- MODE title 149 | _internal.canvas[4].textColor = mode.style.modeDisplay.color -- MODE text 150 | end 151 | end 152 | 153 | --- 154 | --- Set the key caster text 155 | --- @param text string 156 | --- 157 | function module.setText(text) 158 | _internal.casterText.text = text 159 | end 160 | 161 | --- 162 | --- Reset the key caster object 163 | --- 164 | function module.reset() 165 | _internal.casterText.text = '' 166 | end 167 | 168 | return _internal.init() -- returns module 169 | -------------------------------------------------------------------------------- /hellfire/hellfire-keylogger-listener.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | Hellfire key logger listener 4 | 5 | Composes a keylogger and manages the timing and broadcast 6 | of outputs to subscribers 7 | 8 | --]] 9 | 10 | -------------------------------------------------------------------------- 11 | -- Dependencies 12 | -------------------------------------------------------------------------- 13 | local keyStandard = require('hellfred.hellfire.hellfire-key-standard') 14 | 15 | -------------------------------------------------------------------------- 16 | -- Public namespace 17 | -------------------------------------------------------------------------- 18 | local module = {} 19 | 20 | -------------------------------------------------------------------------- 21 | -- Private namespace 22 | -------------------------------------------------------------------------- 23 | local _internal = { 24 | keylogger = require('hellfred.utils.keylogger'):new(), 25 | subscribers = {}, 26 | digestTimer = nil, 27 | digestTimeInSeconds = 0.5, 28 | keyBuffer = {}, 29 | keyCaster = require('hellfred.hellfire.hellfire-keycaster'), 30 | } 31 | 32 | function _internal.onKeyLogEvent(eventTapEvent) 33 | if _internal.digestTimer then 34 | _internal.digestTimer:setNextTrigger(_internal.digestTimeInSeconds) 35 | else 36 | _internal.digestTimer = hs.timer.doAfter(_internal.digestTimeInSeconds, _internal.onDigestTimerTrigger) 37 | end 38 | 39 | local standardised = keyStandard.standardiseKeyChord(eventTapEvent) 40 | 41 | table.insert(_internal.keyBuffer, standardised) 42 | 43 | local humanReadable = {} 44 | hs.fnutils.each(_internal.keyBuffer, function(kEvent) 45 | table.insert(humanReadable, keyStandard.standardToHumanPrettified(kEvent)) 46 | end) 47 | 48 | _internal.keyCaster.setText(table.concat(humanReadable, " › ")) 49 | end 50 | 51 | function _internal.clearKeyBuffer() 52 | _internal.keyBuffer = {} 53 | _internal.keyCaster.reset() 54 | end 55 | 56 | function _internal.onDigestTimerTrigger() 57 | for _, subscriber in pairs(_internal.subscribers) do 58 | subscriber(_internal.keyBuffer) 59 | end 60 | _internal.clearKeyBuffer() 61 | end 62 | 63 | 64 | function _internal.init() 65 | _internal.keylogger.addSubscriber(_internal.onKeyLogEvent) 66 | return module 67 | end 68 | 69 | -------------------------------------------------------------------------- 70 | -- Public API 71 | -------------------------------------------------------------------------- 72 | function module.start() 73 | _internal.clearKeyBuffer() 74 | _internal.keylogger:start() 75 | _internal.keyCaster.show() 76 | end 77 | 78 | function module.stop() 79 | _internal.keylogger:stop() 80 | if _internal.digestTimer then 81 | _internal.digestTimer:stop() 82 | end 83 | _internal.keyCaster.hide() 84 | _internal.keyCaster.reset() 85 | end 86 | 87 | function module.setMode(mode) 88 | _internal.keyCaster.setMode(mode) 89 | end 90 | 91 | function module.addSubscriber(subscriber) 92 | table.insert(_internal.subscribers, subscriber) 93 | end 94 | 95 | return _internal.init() -- returns module -------------------------------------------------------------------------------- /hellfire/hellfire-modes.lua: -------------------------------------------------------------------------------- 1 | local modes = { 2 | ANY = { 3 | name = '__ANY__', 4 | style = { 5 | background = { 6 | color = { hex = "#808080" } 7 | }, 8 | modeDisplay = { 9 | color = { hex = "#666666" } 10 | }, 11 | casterText = { 12 | color = { hex = "#666666" } 13 | } 14 | } 15 | }, 16 | DEFAULT = { 17 | name = 'Default', 18 | style = { 19 | background = { 20 | color = { hex = "#000000" } 21 | }, 22 | modeDisplay = { 23 | color = { hex = "#FFFFFF" } 24 | }, 25 | casterText = { 26 | color = { hex = "#F1B51C" } 27 | } 28 | } 29 | } 30 | } 31 | 32 | return modes 33 | -------------------------------------------------------------------------------- /hellfire/hellfire.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | Hellfire 4 | 5 | A key-to-action mapping utility with subscriber notification. 6 | Supports single key triggers as well as key chord sequences as triggers. 7 | 8 | Composes a key caster to display the user key strokes and 9 | a key logger that listens for event tap events. 10 | 11 | --]] 12 | 13 | 14 | -------------------------------------------------------------------------- 15 | -- Dependencies 16 | -------------------------------------------------------------------------- 17 | local keyStandard = require('hellfred.hellfire.hellfire-key-standard') 18 | local hellfireModes = require('hellfred.hellfire.hellfire-modes') 19 | 20 | -------------------------------------------------------------------------- 21 | -- Public namespace 22 | -------------------------------------------------------------------------- 23 | local module = {} 24 | 25 | -------------------------------------------------------------------------- 26 | -- Private namespace 27 | -------------------------------------------------------------------------- 28 | local _internal = { 29 | contextManager = require('hellfred.hellfire.hellfire-context-manager'), 30 | subscribers = {} 31 | } 32 | 33 | --- 34 | --- Enter Hellfire 35 | --- 36 | function _internal.enter() 37 | _internal.contextManager.start() 38 | end 39 | 40 | --- 41 | --- Exit Hellfire 42 | --- 43 | function _internal.exit() 44 | _internal.contextManager.stop() 45 | if _internal.modal then 46 | _internal.modal:exit() 47 | end 48 | end 49 | 50 | --- 51 | --- Notify subscribers 52 | --- @param context table 53 | --- 54 | function _internal.notify(context) 55 | for _, subscriber in pairs(_internal.subscribers) do 56 | subscriber(context) 57 | end 58 | end 59 | 60 | -------------------------------------------------------------------------- 61 | -- Public API 62 | -------------------------------------------------------------------------- 63 | 64 | --- 65 | --- Initialise Hellfire 66 | --- @param keySpec table A hotkey specification that triggers Hellfire 67 | --- 68 | function module.init(keySpec) 69 | if keySpec then 70 | _internal.modal = hs.hotkey.modal.new(keySpec[1], keySpec[2]) 71 | function _internal.modal:entered() 72 | _internal.enter() 73 | end 74 | _internal.modal:bind('', 'escape', function() _internal.exit() end) 75 | end 76 | _internal.contextManager.addSubscriber(_internal.notify) 77 | module.setMode(hellfireModes.DEFAULT) 78 | end 79 | 80 | --- 81 | --- Add a subscriber to internal subscriber list 82 | --- 83 | --- @param subscriber any A KeysHandler object 84 | --- 85 | function module.addSubscriber(subscriber) 86 | -- Always set a mode to a default if not configured 87 | subscriber.fireIfModeIs = subscriber.fireIfModeIs or hellfireModes.ANY 88 | 89 | local trigger = subscriber.trigger 90 | local callback = subscriber.callback 91 | local fireIfModeIs = subscriber.fireIfModeIs.name 92 | 93 | local fn = function(context) 94 | 95 | -- If the subscriber is not configured to run in Hellfire's ANY mode 96 | -- then short circuit if it should not execute its callback in the current mode 97 | if fireIfModeIs ~= hellfireModes.ANY.name then 98 | if context.mode.name ~= fireIfModeIs then 99 | return 0 100 | end 101 | end 102 | 103 | -- Only compare if a trigger was explicitly supplied 104 | local comparisonResult = nil 105 | if trigger then 106 | local a = context.trigger -- standard key chord sequence 107 | local b = trigger -- user configured 108 | comparisonResult = keyStandard.compare(a, b) 109 | end 110 | 111 | -- Invoke callback if there is a comparison match or if no trigger was supplied 112 | if trigger == nil or comparisonResult == true then 113 | context['hellfire'] = module 114 | 115 | local cbResponse = callback(context) or {exitAfterCallback = true} 116 | 117 | if cbResponse['exitAfterCallback'] ~= false then 118 | _internal.exit() 119 | end 120 | end 121 | end 122 | 123 | table.insert(_internal.subscribers, fn) 124 | end 125 | 126 | --- 127 | --- Add multiple subscribers to internal subscriber list 128 | --- 129 | --- @param subscribers table An array of KeysHandler objects 130 | --- 131 | function module.addSubscribers(subscribers) 132 | hs.fnutils.each(subscribers, function(subscriber) 133 | module.addSubscriber(subscriber) 134 | end) 135 | end 136 | 137 | --- 138 | --- Set the operating mode 139 | --- @param mode table A configuration for a Hellfire mode 140 | --- 141 | function module.setMode(mode) 142 | _internal.contextManager.setMode(mode) 143 | end 144 | 145 | return module -------------------------------------------------------------------------------- /hellfred-bootstrap.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | Hellfred Bootstrap 4 | 5 | A bootstrap file for Hellfred with a pre-configured setup. 6 | 7 | --]] 8 | 9 | -------------------------------------------------------------------------- 10 | -- Public namespace 11 | -------------------------------------------------------------------------- 12 | local hellfred = {} 13 | hellfred.__index = hellfred 14 | 15 | -- Metadata 16 | hellfred.name = "Hellfred" 17 | hellfred.version = "1.0.0" 18 | hellfred.author = "Brad Blundell" 19 | hellfred.homepage = "https://github.com/braddevelop/hellfred" 20 | hellfred.license = "MIT - https://opensource.org/licenses/MIT" 21 | 22 | --[[ ______________________________________________________________________ 23 | 24 | Hellfire 25 | __________________________________________________________________________]] 26 | local hellfire = require('hellfred.hellfire.hellfire') 27 | 28 | -- Initialise Hellfire, passing a hotkey 29 | hellfire.init({{'shift','cmd'},'h'}) 30 | 31 | -- Load subscribers from packs 32 | hellfire.addSubscribers(require('hellfred.quick-start').hellfirePack) 33 | 34 | --[[ ______________________________________________________________________ 35 | 36 | Hellprompt 37 | __________________________________________________________________________]] 38 | local hellprompt = require('hellfred.hellprompt.hellprompt') 39 | 40 | -- Initialise Hellprompt, passing a hotkey 41 | hellprompt.init({{'shift','ctrl'},'h'}) 42 | 43 | -- Load subscribers from packs 44 | hellprompt.addSubscribers(require('hellfred.quick-start').hellpromptPack) 45 | 46 | --[[ ______________________________________________________________________ 47 | 48 | Hellfuzz 49 | __________________________________________________________________________]] 50 | local hellfuzz = require('hellfred.hellfuzz.hellfuzz') 51 | 52 | -- Initialise Hellfuzz, passing a hotkey 53 | hellfuzz.init({{'shift','alt'},'h'}) 54 | 55 | -- Load subscribers from packs 56 | hellfuzz.addSubscribers(require('hellfred.quick-start').hellfuzzPack) 57 | 58 | -------------------------------------------------------------------------- 59 | -- Public API 60 | -------------------------------------------------------------------------- 61 | return hellfred 62 | -------------------------------------------------------------------------------- /hellfuzz/hellfuzz-chooser.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | Hellfuzz chooser 4 | 5 | Attribution: Fuzzy Window Switcher https://gist.github.com/RainmanNoodles/70aaff04b20763041d7acb771b0ff2b2 6 | 7 | --]] 8 | 9 | -------------------------------------------------------------------------- 10 | -- Public namespace 11 | -------------------------------------------------------------------------- 12 | local module={} 13 | 14 | -------------------------------------------------------------------------- 15 | -- Private namespace 16 | -------------------------------------------------------------------------- 17 | local _internal = { 18 | choices = nil, 19 | lastApp = nil, 20 | chooser = nil, 21 | onChooserEventHandler = nil, 22 | userMeta = nil, 23 | queryInterceptorFunc = nil, 24 | bindings = {}, 25 | } 26 | 27 | function _internal.enter() 28 | local escapeHotkey = hs.hotkey.bindSpec({ {}, 'escape' }, 29 | function() 30 | _internal.chooser:cancel() 31 | _internal.exit() 32 | end 33 | ) 34 | table.insert(_internal.bindings, escapeHotkey) 35 | 36 | _internal.userMeta = nil 37 | _internal.lastApp = hs.window.focusedWindow() 38 | 39 | _internal.chooser:show() 40 | _internal.chooser:query('') -- workaround 41 | _internal.onQueryChanged('') -- workaround 42 | end 43 | 44 | 45 | --- 46 | --- Unbind all hotkeys 47 | --- 48 | function _internal.unbindAll() 49 | hs.fnutils.each(_internal.bindings, function(bindingObj) 50 | bindingObj:delete() 51 | end) 52 | 53 | -- clear bindings 54 | local count = #_internal.bindings 55 | for i = 0, count do _internal.bindings[i] = nil end 56 | end 57 | 58 | 59 | function _internal.exit() 60 | _internal.unbindAll() 61 | end 62 | 63 | 64 | function _internal.onSelectCallback(choice, userParams) 65 | _internal.onChooserEventHandler(choice, userParams) 66 | end 67 | 68 | --- 69 | --- Fuzzy score algorithm 70 | --- 71 | function _internal.fuzzyScore(s, m) 72 | local s_index = 1 73 | local m_index = 1 74 | local match_start = nil 75 | while true do 76 | if s_index > s:len() or m_index > m:len() then 77 | return -1 78 | end 79 | local s_char = s:sub(s_index, s_index) 80 | local m_char = m:sub(m_index, m_index) 81 | if s_char == m_char then 82 | if match_start == nil then 83 | match_start = s_index 84 | end 85 | s_index = s_index + 1 86 | m_index = m_index + 1 87 | if m_index > m:len() then 88 | local match_end = s_index 89 | local s_match_length = match_end - match_start 90 | local score = m:len() / s_match_length 91 | return score 92 | end 93 | else 94 | s_index = s_index + 1 95 | end 96 | end 97 | end 98 | 99 | 100 | function _internal.onSelectCallbackDecorator(choice) 101 | if choice == nil then 102 | if _internal.lastApp then 103 | -- Workaround so last focused window stays focused after dismissing 104 | _internal.lastApp:focus() 105 | _internal.lastApp = nil 106 | end 107 | else 108 | if _internal.onSelectCallback then 109 | _internal.onSelectCallback(choice, _internal.userMeta) 110 | end 111 | end 112 | _internal.exit() 113 | end 114 | 115 | 116 | function _internal.onInvalidCallback(choice) 117 | _internal.onChooserEventHandler(choice, _internal.userMeta) 118 | end 119 | 120 | 121 | function _internal.onQueryChanged(query) 122 | if _internal.queryInterceptorFunc then 123 | query, _internal.userMeta = _internal.queryInterceptorFunc(query) 124 | end 125 | 126 | -- If the user has backspaced (deleted) to an empty string 127 | -- then reset back to all choices 128 | if query:len() == 0 then 129 | _internal.chooser:choices(_internal.choices) 130 | return 131 | end 132 | 133 | local pickedChoices = {} 134 | for i, j in pairs(_internal.choices) do 135 | 136 | -- Prepare the search space for this choice 137 | local fullText = j["text"] 138 | if j["subText"] ~= nil then 139 | fullText = fullText .. " " .. j["subText"] 140 | end 141 | fullText = fullText:lower() 142 | 143 | -- Score the match of query to choice 144 | local score = _internal.fuzzyScore(fullText, query:lower()) 145 | 146 | -- If the choice has any interesting score then assign `score` to `choice.fzf_score` 147 | if score > 0 then 148 | j["fzf_score"] = score 149 | table.insert(pickedChoices, j) 150 | end 151 | end 152 | 153 | -- Sort the choices by score 154 | local sort_func = function(a, b) return a["fzf_score"] > b["fzf_score"] end 155 | table.sort(pickedChoices, sort_func) 156 | 157 | -- Update chooser choices with scored items 158 | _internal.chooser:choices(pickedChoices) 159 | end 160 | 161 | -------------------------------------------------------------------------- 162 | -- Public API 163 | -------------------------------------------------------------------------- 164 | 165 | --- 166 | --- Initialise Hellfuzz chooser 167 | --- @param onEventHandler any 168 | --- 169 | function module.init(onEventHandler) 170 | _internal.onChooserEventHandler = onEventHandler 171 | _internal.chooser = hs.chooser.new(_internal.onSelectCallbackDecorator) 172 | _internal.chooser:bgDark(true) 173 | _internal.chooser:queryChangedCallback(_internal.onQueryChanged) -- Enable true fuzzy find 174 | _internal.chooser:invalidCallback(_internal.onInvalidCallback) 175 | end 176 | --- 177 | --- Enter the chooser 178 | --- 179 | --- @param choices table A list of choices 180 | --- 181 | function module.enter(choices) 182 | _internal.choices = choices 183 | _internal.enter() 184 | end 185 | 186 | --- 187 | --- Supply another [sub set] of choices for the chooser 188 | --- 189 | --- @param choices table A list of choices 190 | --- 191 | function module.next(choices) 192 | _internal.userMeta = nil 193 | _internal.choices = choices 194 | _internal.chooser:choices(_internal.choices) 195 | _internal.chooser:query('') -- workaround 196 | end 197 | 198 | --- 199 | --- Set a query interceptor function 200 | --- 201 | --- A function that intercepts the user 202 | --- input before it is processed by the fuzzy search. 203 | --- 204 | --- It should accept one string argument (the user query) 205 | --- and return a string as a first return value, to be used 206 | --- by the chooser, followed by an optional second meta value, 207 | --- that will be passed to the subscriber callback. 208 | --- 209 | --- Format example: 210 | --- inspectorFunc = function (query) 211 | --- ...some logic here... 212 | --- return query, query.upper() 213 | --- end 214 | --- 215 | --- @param fn any 216 | --- 217 | function module.setQueryInterceptor(fn) 218 | _internal.queryInterceptorFunc = fn 219 | end 220 | 221 | return module -------------------------------------------------------------------------------- /hellfuzz/hellfuzz-helpers.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | Helper methods for Hellfuzz 4 | 5 | --]] 6 | 7 | -------------------------------------------------------------------------- 8 | -- Private namespace 9 | -------------------------------------------------------------------------- 10 | local _internal = { 11 | defaultChooserImage = hs.image.imageFromPath("hellfred/resources/hellfred-logo-orange-96x96.png") 12 | } 13 | 14 | -------------------------------------------------------------------------- 15 | -- Public namespace 16 | -------------------------------------------------------------------------- 17 | local module = {} 18 | 19 | -------------------------------------------------------------------------- 20 | -- Public API 21 | -------------------------------------------------------------------------- 22 | --- 23 | --- A convenience method for generating Hellfuzz ChoiceHandler objects 24 | --- 25 | --- @param text any A string or hs.styledtext object that will be shown as the main text of the choice 26 | --- @param subText any A string or hs.styledtext object that will be shown underneath the main text of the choice 27 | --- @param callback any A function that is called if this choice is selected 28 | --- @param nextChoicesFn any A function that returns an array of ChoiceHandler objects 29 | --- @param showInFirstChoiceSet any Show this choice when Hellfuzz opens 30 | --- @return table A ChoiceHandler object 31 | --- 32 | function module.choiceHandlerFactory(text, subText, callback, nextChoicesFn, showInFirstChoiceSet) 33 | 34 | local choice = { 35 | text = text, 36 | image = _internal.defaultChooserImage 37 | } 38 | 39 | if subText then 40 | choice.subText = subText 41 | end 42 | 43 | return { 44 | choice = choice, 45 | callback = callback, 46 | nextChoicesFn = nextChoicesFn, 47 | showInFirstChoiceSet = showInFirstChoiceSet or false 48 | } 49 | end 50 | 51 | --- 52 | --- A convenience method for generating Hellfuzz ChoiceHandler objects (custom) 53 | --- This method allows for more custom configuration over 54 | --- the `choice` argument than `choiceHandlerFactory` does 55 | --- 56 | --- @param choice any A choice as specified in https://www.hammerspoon.org/docs/hs.chooser.html#choices 57 | --- @param callback any A function that is called if this choice is selected 58 | --- @param nextChoicesFn any A function that returns an array of ChoiceHandler objects 59 | --- @param showInFirstChoiceSet any Show this choice when Hellfuzz opens 60 | --- @return table A ChoiceHandler object 61 | ---- 62 | function module.choiceHandlerCustomFactory(choice, callback, nextChoicesFn, showInFirstChoiceSet) 63 | choice.image = choice.image or _internal.defaultChooserImage 64 | 65 | return { 66 | choice = choice, 67 | callback = callback, 68 | nextChoicesFn = nextChoicesFn, 69 | showInFirstChoiceSet = showInFirstChoiceSet or false 70 | } 71 | end 72 | 73 | return module 74 | -------------------------------------------------------------------------------- /hellfuzz/hellfuzz.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | Hellfuzz 4 | 5 | A fuzzy-search chooser utility with subscriber notification. 6 | Composes a chooser that supports multi-level choice sets. 7 | 8 | --]] 9 | 10 | -------------------------------------------------------------------------- 11 | -- Public namespace 12 | -------------------------------------------------------------------------- 13 | local module = {} 14 | 15 | -------------------------------------------------------------------------- 16 | -- Private namespace 17 | -------------------------------------------------------------------------- 18 | local _internal = { 19 | chooser = require('hellfred.hellfuzz.hellfuzz-chooser'), 20 | choices = {}, 21 | firstChoiceSet = {}, 22 | nextChoicesProviders = {}, 23 | callbacks = {}, 24 | } 25 | 26 | --- 27 | --- Handles chooser selection events 28 | --- 29 | --- @param selectedChoice any The `ChoiceHandler.choice` returned from the chooser 30 | --- @param meta any Custom meta generated by the query interceptor function 31 | --- 32 | function _internal.onChooserEvent(selectedChoice, meta) 33 | if selectedChoice then 34 | -- If the selected choice has next choices then add them as the next choice set; 35 | -- else call the choice handler's callback function 36 | local id = selectedChoice.text 37 | if _internal.nextChoicesProviders[id] then 38 | local nextChoices = _internal.nextChoicesProviders[id]() 39 | 40 | if nextChoices then 41 | -- Register next choices 42 | nextChoices = module.addSubscribers(nextChoices) 43 | 44 | local next = {} 45 | hs.fnutils.each(nextChoices, function(choiceHandler) 46 | table.insert(next, choiceHandler.choice) 47 | end) 48 | _internal.chooser.next(next) 49 | end 50 | elseif _internal.callbacks[id] then 51 | _internal.callbacks[id](selectedChoice, meta) 52 | end 53 | end 54 | end 55 | 56 | -------------------------------------------------------------------------- 57 | -- Public API 58 | -------------------------------------------------------------------------- 59 | 60 | --- 61 | --- Add a subscriber to internal subscriber list 62 | --- 63 | --- @param subscriber any A ChoiceHandler object 64 | --- @return any 65 | --- 66 | function module.addSubscriber(subscriber) 67 | local key = subscriber.choice.text 68 | 69 | if subscriber.showInFirstChoiceSet then 70 | _internal.firstChoiceSet[key] = subscriber 71 | end 72 | 73 | if subscriber.callback then 74 | _internal.callbacks[key] = subscriber.callback 75 | end 76 | 77 | if subscriber.nextChoicesFn then 78 | _internal.nextChoicesProviders[key] = subscriber.nextChoicesFn 79 | subscriber.choice['valid'] = false 80 | end 81 | 82 | return subscriber 83 | end 84 | 85 | --- 86 | --- Add multiple subscribers to internal subscriber list 87 | --- 88 | --- @param subscribers any A list of ChoiceHandler objects 89 | --- @return any 90 | --- 91 | function module.addSubscribers(subscribers) 92 | hs.fnutils.each(subscribers, function(subscriber) 93 | subscriber = module.addSubscriber(subscriber) 94 | end) 95 | 96 | return subscribers 97 | end 98 | 99 | --- 100 | --- Set a query interceptor function 101 | --- 102 | --- @param fn any 103 | --- 104 | function module.setQueryInterceptor(fn) 105 | _internal.chooser.setQueryInterceptor(fn) 106 | end 107 | 108 | --- 109 | --- Initialise Hellfuzz 110 | --- 111 | --- @param keySpec table 112 | --- 113 | function module.init(keySpec) 114 | -- assign the function to call every time a selection event occurs 115 | _internal.chooser.init(_internal.onChooserEvent) 116 | 117 | -- when the hotkey fires, group all ChoiceHandlers that must show first, 118 | -- into a table and enter the chooser. 119 | hs.hotkey.bindSpec(keySpec, function() 120 | local firstSetOfChoices = {} 121 | for k, choiceHandler in pairs(_internal.firstChoiceSet) do 122 | table.insert(firstSetOfChoices, choiceHandler.choice) 123 | end 124 | _internal.chooser.enter(firstSetOfChoices) 125 | end) 126 | end 127 | 128 | return module -------------------------------------------------------------------------------- /hellprompt/hellprompt-keycaster.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | Hellprompt Keycaster 4 | 5 | A keystroke visualiser. 6 | Inspired by https://github.com/keycastr/keycastr 7 | 8 | --]] 9 | 10 | -------------------------------------------------------------------------------- 11 | -- Public namespace 12 | -------------------------------------------------------------------------------- 13 | local module = {} 14 | 15 | -------------------------------------------------------------------------------- 16 | -- Private namespace 17 | -------------------------------------------------------------------------------- 18 | local _internal = { 19 | defaultChooserImage = hs.image.imageFromPath("hellfred/resources/hellfred-logo-orange-96x96.png"), 20 | canvas = nil, 21 | casterText = nil, 22 | frameWidth = 1200.0, 23 | frameHeight = 500.0, -- 200:bottom; 500:middle; 900:top 24 | barWidth = 1200, 25 | barHeight = 55, 26 | casterTextSize = 20, 27 | style = { 28 | fonts = { 29 | primary = "Menlo" 30 | }, 31 | colours = { 32 | black = { hex = "#000000" }, 33 | white = { hex = "#FFFFFF" }, 34 | primary = { hex = "#E43C26" }, -- intense orange 35 | secondary = { hex = "#F1B51C" }, -- yellow 36 | } 37 | }, 38 | } 39 | 40 | --- 41 | --- Initialise elements of the visualiser interface 42 | --- 43 | --- @return table The key caster object 44 | --- 45 | function _internal.init() 46 | local mainScreen = hs.screen.primaryScreen() 47 | local mainRes = mainScreen:fullFrame() 48 | 49 | if not _internal.canvas then 50 | _internal.canvas = hs.canvas.new({ x = 0, y = 0, w = 0, h = 0 }) 51 | end 52 | 53 | -- Background 54 | _internal.canvas[1] = { 55 | action = "fill", 56 | fillColor = _internal.style.colours.black, 57 | frame = { x = 0.0, y = 0.0, h = _internal.barHeight, w = _internal.barWidth }, 58 | type = "rectangle", 59 | } 60 | 61 | -- Caster text 62 | _internal.canvas[2] = { 63 | type = "text", 64 | text = "", 65 | textFont = _internal.style.fonts.primary, 66 | textSize = _internal.casterTextSize, 67 | textColor = _internal.style.colours.secondary, 68 | frame = { 69 | x = 70.0, 70 | y = 14.0, 71 | h = _internal.barHeight, 72 | w = _internal.barWidth, 73 | }, 74 | } 75 | _internal.casterText = _internal.canvas[2] 76 | 77 | -- Hellfred logo 78 | _internal.canvas[3] = { 79 | type = "image", 80 | image = _internal.defaultChooserImage, 81 | frame = { x = 12.0, y = 0.0, h = 48, w = 48 } 82 | } 83 | 84 | _internal.canvas:frame({ 85 | x = (mainRes.w - _internal.frameWidth) / 2, 86 | y = (mainRes.h - _internal.frameHeight), 87 | w = _internal.frameWidth, 88 | h = _internal.frameHeight, 89 | }) 90 | 91 | return module 92 | end 93 | 94 | -------------------------------------------------------------------------------- 95 | -- Public API 96 | -------------------------------------------------------------------------------- 97 | 98 | --- 99 | --- Show the key caster 100 | --- 101 | function module.show() 102 | _internal.canvas:show() 103 | end 104 | 105 | --- 106 | --- Hide the key caster 107 | --- 108 | function module.hide() 109 | _internal.canvas:hide() 110 | end 111 | 112 | --- 113 | --- Set the key caster text 114 | --- @param text string 115 | --- 116 | function module.setText(text) 117 | _internal.casterText.text = text 118 | end 119 | 120 | --- 121 | --- Reset the key caster object 122 | --- 123 | function module.reset() 124 | _internal.casterText.text = '' 125 | end 126 | 127 | return _internal.init() -- returns module 128 | -------------------------------------------------------------------------------- /hellprompt/hellprompt.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | Hellprompt 4 | 5 | A commandline-like utility with subscriber notification. 6 | 7 | Composes a key caster to display user key strokes and 8 | a key logger that it listens to for event tap events. 9 | 10 | --]] 11 | 12 | -------------------------------------------------------------------------- 13 | -- Public namespace 14 | -------------------------------------------------------------------------- 15 | local module = {} 16 | 17 | -------------------------------------------------------------------------- 18 | -- Private namespace 19 | -------------------------------------------------------------------------- 20 | local _internal = { 21 | modal = nil, 22 | keylogger = require('hellfred.utils.keylogger'):new(), 23 | subscribers = {}, 24 | commandBuffer = '', 25 | keyCaster = require('hellfred.hellprompt.hellprompt-keycaster'), 26 | cursor = '▏', 27 | promptPrefix = ' ›› ', 28 | shiftMap = require('hellfred.utils.shift-map') 29 | } 30 | 31 | --- 32 | --- Enter Hellprompt 33 | --- 34 | function _internal.enter() 35 | _internal.clearCommandBuffer() 36 | _internal.keyCaster.setText(_internal.promptPrefix.._internal.cursor) 37 | _internal.keylogger:start() 38 | _internal.keyCaster.show() 39 | end 40 | 41 | --- 42 | --- Exit Hellprompt 43 | --- 44 | function _internal.exit() 45 | _internal.keylogger:stop() 46 | _internal.keyCaster.hide() 47 | _internal.keyCaster.reset() 48 | _internal.modal:exit() 49 | end 50 | 51 | --- 52 | --- Notify subscribers 53 | --- 54 | --- @param message string 55 | --- 56 | function _internal.notify(message) 57 | hs.fnutils.each(_internal.subscribers, function(subscriber) 58 | -- Check the filter rules, if any, specified by the subscriber object 59 | -- If any filters do not match then do not notify the subscriber 60 | local doNotify = true 61 | if subscriber.filters then 62 | hs.fnutils.each(subscriber.filters, function(filter) 63 | if string.match(message, filter) == nil then 64 | doNotify = false 65 | end 66 | end) 67 | end 68 | 69 | if doNotify then 70 | subscriber.callback(message) 71 | end 72 | end) 73 | end 74 | 75 | --- 76 | --- Handle event tap events emitted from a keylogger 77 | --- 78 | --- @param eventTapEvent hs.eventtap.event 79 | --- 80 | function _internal.onKeyLoggerEvent(eventTapEvent) 81 | 82 | local key = hs.keycodes.map[eventTapEvent:getKeyCode()] 83 | 84 | -- Check if we should submit the command and exit Hellprompt 85 | if key == 'return' or key == 'padenter' then 86 | _internal.exit() 87 | _internal.notify(_internal.commandBuffer) 88 | return 89 | end 90 | 91 | -- Ignore these key inputs 92 | local ignoreableKeys = { 93 | ["f1"] = true, ["f2"] = true, ["f3"] = true, ["f4"] = true, 94 | ["f5"] = true, ["f6"] = true, ["f7"] = true, ["f8"] = true, ["f9"] = true, 95 | ["f10"] = true, ["f11"] = true, ["f12"] = true, ["f13"] = true, 96 | ["f14"] = true, ["f15"] = true, ["f16"] = true, ["f17"] = true, 97 | ["f18"] = true, ["f19"] = true, ["f20"] = true, ["tab"] = true, 98 | ["escape"] = true, ["help"] = true, ["home"] = true, ["pageup"] = true, 99 | ["forwarddelete"] = true, ["end"] = true, ["pagedown"] = true, 100 | ["left"] = true, ["right"] = true, ["down"] = true, ["up"] = true, 101 | } 102 | for k, v in pairs(ignoreableKeys) do 103 | if key == k then return end 104 | end 105 | 106 | -- Convert some keys to canonical character 107 | local convertableKeys = { 108 | ["pad"] = '"', ["pad*"] = '*', ["pad+"] = '+', ["pad/"] = '/', ["pad-"] = '-', 109 | ["pad="] = '=', ["pad0"] = '0', ["pad1"] = '1', ["pad2"] = '2', ["pad3"] = '3', 110 | ["pad4"] = '4', ["pad5"] = '5', ["pad6"] = '6', ["pad7"] = '7', ["pad8"] = '8', 111 | ["pad9"] = '9', ["space"] = ' ', 112 | } 113 | for k, v in pairs(convertableKeys) do 114 | if key == k then 115 | key = v 116 | end 117 | end 118 | 119 | -- Convert to 'shift up' characters if necessary 120 | local flags = eventTapEvent:getFlags() 121 | if flags.shift then 122 | key = key:upper() 123 | if _internal.shiftMap[key] then 124 | key = _internal.shiftMap[key] 125 | end 126 | end 127 | 128 | -- Handle character and word deletion 129 | if key:lower() == 'delete' or key:lower() == 'padclear' then 130 | if _internal.commandBuffer ~= '' then 131 | if flags.alt or flags.cmd or flags.ctrl then 132 | -- delete previous word 133 | local words = hs.fnutils.split(_internal.commandBuffer,' ') 134 | table.remove(words, #words) 135 | _internal.commandBuffer = table.concat(words, ' ') 136 | else 137 | -- delete last character 138 | _internal.commandBuffer = string.sub(_internal.commandBuffer, 1,-2) 139 | end 140 | end 141 | else 142 | -- Append the character 143 | _internal.commandBuffer = _internal.commandBuffer..key 144 | end 145 | 146 | _internal.keyCaster.setText(_internal.promptPrefix.._internal.commandBuffer.._internal.cursor) 147 | end 148 | 149 | --- 150 | --- Clear the command buffer 151 | --- 152 | function _internal.clearCommandBuffer() 153 | _internal.commandBuffer = '' 154 | _internal.keyCaster.reset() 155 | end 156 | 157 | -------------------------------------------------------------------------- 158 | -- Public API 159 | -------------------------------------------------------------------------- 160 | 161 | --- 162 | --- Initialise Hellprompt 163 | --- @param keySpec table 164 | --- 165 | function module.init(keySpec) 166 | _internal.modal = hs.hotkey.modal.new(keySpec[1], keySpec[2]) 167 | 168 | function _internal.modal:entered() 169 | _internal.enter() 170 | end 171 | _internal.modal:bind('', 'escape', function() _internal.exit() end) 172 | _internal.keylogger.addSubscriber(_internal.onKeyLoggerEvent) 173 | end 174 | 175 | --- 176 | --- Add a subscriber to internal subscriber list 177 | --- 178 | --- @param subscriber any A CommandHandler object 179 | --- 180 | function module.addSubscriber(subscriber) 181 | table.insert(_internal.subscribers, subscriber) 182 | end 183 | 184 | --- 185 | --- Add multiple subscribers to internal subscriber list 186 | --- 187 | --- @param subscribers any An array of CommandHandler objects 188 | --- 189 | function module.addSubscribers(subscribers) 190 | hs.fnutils.each(subscribers, function(sub) 191 | module.addSubscriber(sub) 192 | end) 193 | end 194 | 195 | return module -------------------------------------------------------------------------------- /quick-start.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | Hellfred Quick Start 4 | 5 | A sample configuration for Hellfred 6 | 7 | --]] 8 | 9 | local hfh = require('hellfred.hellfuzz.hellfuzz-helpers') 10 | 11 | -- A mapping of keys to resources 12 | local _resourceMap = { 13 | wiki = 'https://github.com/braddevelop/hellfred/wiki', 14 | discuss = 'https://github.com/braddevelop/hellfred/discussions', 15 | code = 'https://github.com/braddevelop/hellfred', 16 | } 17 | 18 | -- Factory function that creates subscribers 19 | -- for Hellfire 20 | local _factory = function(trigger, url, mode) 21 | return { 22 | trigger = trigger, 23 | fireIfModeIs = mode or nil, 24 | callback = function() hs.urlevent.openURL(url) end 25 | } 26 | end 27 | 28 | -- A configuration object with 29 | -- setups for Hellfire, Hellfuzz and Hellprompt 30 | return { 31 | --[[ 32 | Using Hellfire: 33 | - the`w` key opens the Hellfred Wiki online. 34 | - the`c` key opens the Hellfred Repo online. 35 | - the`d` key opens the Hellfred Discussion page online. 36 | ]] 37 | hellfirePack = { 38 | _factory({'w'}, _resourceMap.wiki), 39 | _factory({'c'}, _resourceMap.code), 40 | _factory({'d'}, _resourceMap.discuss), 41 | }, 42 | --[[ 43 | Using Hellfuzz: 44 | - display choices for opening Hellfred Wiki, Code or Discussion page 45 | ]] 46 | hellfuzzPack = { 47 | hfh.choiceHandlerFactory("Open Hellfred wiki", "", function(choice) 48 | hs.urlevent.openURL(_resourceMap.wiki) 49 | end, nil, true), 50 | hfh.choiceHandlerFactory("Open Hellfred repo", "", function(choice) 51 | hs.urlevent.openURL(_resourceMap.code) 52 | end, nil, true), 53 | hfh.choiceHandlerFactory("Discuss Hellfred", "", function(choice) 54 | hs.urlevent.openURL(_resourceMap.discuss) 55 | end, nil, true) 56 | }, 57 | --[[ 58 | Using Hellprompt: 59 | - type `open wiki` to open the Hellfred Wiki online. 60 | - type `open code` to open the Hellfred Repo online. 61 | - type `open discuss` to open the Hellfred Discussion page online. 62 | ]] 63 | hellpromptPack = { 64 | { 65 | filters = {'^open'}, 66 | callback = function(command) 67 | local words = hs.fnutils.split(command, ' ') 68 | 69 | if #words == 2 then 70 | local resource = words[2] 71 | if _resourceMap[resource] then 72 | hs.urlevent.openURL(_resourceMap[resource]) 73 | end 74 | end 75 | end 76 | } 77 | }, 78 | } -------------------------------------------------------------------------------- /resources/Hellfire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/braddevelop/hellfred/f03bda7463f1e4ddd7c34aad407035a589e1349e/resources/Hellfire.png -------------------------------------------------------------------------------- /resources/Hellfuzz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/braddevelop/hellfred/f03bda7463f1e4ddd7c34aad407035a589e1349e/resources/Hellfuzz.png -------------------------------------------------------------------------------- /resources/Hellprompt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/braddevelop/hellfred/f03bda7463f1e4ddd7c34aad407035a589e1349e/resources/Hellprompt.png -------------------------------------------------------------------------------- /resources/hackernoon-article.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/braddevelop/hellfred/f03bda7463f1e4ddd7c34aad407035a589e1349e/resources/hackernoon-article.png -------------------------------------------------------------------------------- /resources/hellfred-banner-id534.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/braddevelop/hellfred/f03bda7463f1e4ddd7c34aad407035a589e1349e/resources/hellfred-banner-id534.png -------------------------------------------------------------------------------- /resources/hellfred-logo-orange-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/braddevelop/hellfred/f03bda7463f1e4ddd7c34aad407035a589e1349e/resources/hellfred-logo-orange-512x512.png -------------------------------------------------------------------------------- /resources/hellfred-logo-orange-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/braddevelop/hellfred/f03bda7463f1e4ddd7c34aad407035a589e1349e/resources/hellfred-logo-orange-96x96.png -------------------------------------------------------------------------------- /resources/hellfuzz-example-apps-struct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/braddevelop/hellfred/f03bda7463f1e4ddd7c34aad407035a589e1349e/resources/hellfuzz-example-apps-struct.png -------------------------------------------------------------------------------- /utils/keylogger.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | Hellfred Key logger 4 | 5 | A key logger with subscriber notification. 6 | 7 | --]] 8 | 9 | -------------------------------------------------------------------------------- 10 | -- Public namespace 11 | -------------------------------------------------------------------------------- 12 | local module = {} 13 | 14 | -------------------------------------------------------------------------------- 15 | -- Public API 16 | -------------------------------------------------------------------------------- 17 | 18 | --- 19 | --- Instantiate a new key logger instance 20 | --- @return table A key logger object 21 | --- 22 | function module.new() 23 | 24 | ------------------------------- 25 | -- Instance public namespace 26 | ------------------------------- 27 | local obj = {} 28 | 29 | ------------------------------- 30 | -- Instance private namespace 31 | ------------------------------- 32 | local _internal = { 33 | modal = nil, 34 | subscribers = {}, 35 | keyListener = nil, 36 | } 37 | 38 | --- 39 | --- Initialise this key logger 40 | --- @return table The key logger object 41 | --- 42 | function _internal.init() 43 | _internal.modal = hs.hotkey.modal.new() 44 | _internal.modal.entered = function() 45 | _internal.keyListener:start() 46 | end 47 | 48 | _internal.modal.exited = function() 49 | _internal.keyListener:stop() 50 | end 51 | 52 | _internal.keyListener = hs.eventtap.new( 53 | { hs.eventtap.event.types.keyDown }, 54 | _internal.onKeyEvent 55 | ) 56 | end 57 | 58 | --- 59 | --- Handle event tap events emitted 60 | --- 61 | --- @param eventTapEvent hs.eventtap.event 62 | --- 63 | function _internal.onKeyEvent(eventTapEvent) 64 | 65 | local keyCode = eventTapEvent:getKeyCode() 66 | 67 | if keyCode ~= 53 then -- ignore escape: must propogate 68 | for _, callback in pairs(_internal.subscribers) do 69 | callback(eventTapEvent) 70 | end 71 | return true -- prevent event propagation 72 | end 73 | return false -- allow event propagation 74 | end 75 | 76 | ------------------------------- 77 | -- Instance public API 78 | ------------------------------- 79 | 80 | --- 81 | --- Start logging keys 82 | --- 83 | function obj.start() 84 | _internal.modal:enter() 85 | end 86 | 87 | --- 88 | --- Stop logging keys 89 | --- 90 | function obj.stop() 91 | _internal.modal:exit() 92 | end 93 | 94 | --- 95 | --- Add a subscriber to internal subscriber list 96 | --- 97 | function obj.addSubscriber(subscriber) 98 | table.insert(_internal.subscribers, subscriber) 99 | end 100 | 101 | --- 102 | --- Add multiple subscribers to internal subscriber list 103 | --- 104 | function obj.addSubscribers(subscribers) 105 | hs.fnutils.each(subscribers, function(sub) 106 | obj.addSubscriber(sub) 107 | end) 108 | end 109 | 110 | ------------------------------- 111 | -- Program 112 | ------------------------------- 113 | _internal.init() 114 | 115 | return obj 116 | end 117 | 118 | return module -------------------------------------------------------------------------------- /utils/shift-map.lua: -------------------------------------------------------------------------------- 1 | return { 2 | ["'"] = '"', 3 | [','] = '<', 4 | ['-'] = '_', 5 | ['.'] = '>', 6 | ['/'] = '?', 7 | ['0'] = ')', 8 | ['1'] = '!', 9 | ['2'] = '@', 10 | ['3'] = '#', 11 | ['4'] = '$', 12 | ['5'] = '%', 13 | ['6'] = '^', 14 | ['7'] = '&', 15 | ['8'] = '*', 16 | ['9'] = '(', 17 | [';'] = ':', 18 | ['='] = '+', 19 | ['['] = '{', 20 | ['\\'] = '|', 21 | [']'] = '}', 22 | ['`'] = '~' 23 | } --------------------------------------------------------------------------------