├── 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 | 
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 | 
25 | *Hellfuzz. A fuzzy search, choice-to-behaviour mapper.*
26 |
27 | 
28 | *Hellfire. A keys-to-behaviour mapper.*
29 |
30 | 
31 | *Hellprompt. A command-to-behaviour mapper.*
32 |
33 | ### Triggers and callbacks
34 | | App | Trigger(s) | Callback (Behaviour) |
35 | | ------------- | -------- | ------------------- |
36 | | Hellfire |
- Single key. x
- Single key with modifiers. ⌘ ⌥x
- Key sequences. ⌘k , x
| 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 | 
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 | [](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 | }
--------------------------------------------------------------------------------