-
Notifications
You must be signed in to change notification settings - Fork 70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rework statemachine #139
Merged
Merged
Rework statemachine #139
Changes from 44 commits
Commits
Show all changes
48 commits
Select commit
Hold shift + click to select a range
31a538c
Initial work on new fsm
Grazfather c674d13
pop/push -> butlast/conf
Grazfather 04e620f
fsm2: Simplify schema and add subscribe functionality
Grazfather 278d760
Add and tweak Jay's effect-handler
Grazfather 8d78658
Keep handlers from touching atoms
Grazfather 71975ba
Cleanup a bit
Grazfather 942d773
create-machine takes initial state in states arg
Grazfather 714a51f
Combine context and state into single atom
Grazfather 9a4da43
cleanup
Grazfather 67299d6
log error if current state has no handler for action
Grazfather 2b96c82
Add methods to fsm
Grazfather 820d818
Let caller provider a logger tag
Grazfather 25cf8bb
Make signal return boolean success
Grazfather 907fc0c
Fix unsub function
Grazfather 278c626
Export effect-handler
Grazfather 284eb43
Add tests
Grazfather 775bb8d
Cleanup tests slightly
Grazfather ff9b5ec
Add new-modal using new-statemachine
Grazfather 78e79fb
Remove example and add big doc string
Grazfather 1cc0852
Rename timeout transition function
Grazfather b167f03
Remove more duplicated code for review
Grazfather 8bca53c
Duplicate funcs from statemachine into new
Grazfather fe2840a
Fix timeout current-state
Grazfather ac4199e
Remove some todos
Grazfather a1c0180
remove todo
Grazfather f3be7ec
Cleanup docstrings
Grazfather e397806
wip new apps
Grazfather 6eed996
apps: Fix getting current app to display app-menu
Grazfather 31bf99e
apps: Update app even when new app isn't in config
Grazfather 3ec63bb
Log debug not warn
Grazfather a385ae7
Get app switching in modal working
Grazfather 8b6c388
Cleanup some logging
Grazfather 35873d6
Remove old modal, apps, mostly remove old statemachine
Grazfather f2e18f8
Fix monkey patch
Grazfather b4a427e
Remove extra logging from apps and modal
Grazfather 758ac82
wip: Port vim to new statemachine
Grazfather f7354fb
minor cleanup
Grazfather 7628b47
cleanup in modal
Grazfather 32c6b15
apps: Update app even when new app isn't in config
Grazfather 8b86f26
apps: close & leave effects have to operate on prev app
Grazfather 0883a7f
fix bug in watch-screen
Grazfather 89018c8
statemachine: Rename 'signal' to 'send'
Grazfather 65c0f4e
Remove all debug prints
Grazfather fe69427
Remove comment
Grazfather 2b8e11b
Better heading
Grazfather 4f2470c
Better function names
Grazfather 9784eed
s/signal/send in statemachine test
Grazfather 53a4cbc
statemachine test: assert in sub func
Grazfather File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,7 @@ This module works mechanically similar to lib/modal.fnl. | |
(local os (require :os)) | ||
(local {: call-when | ||
: find | ||
: merge | ||
: noop | ||
: tap} | ||
(require :lib.functional)) | ||
|
@@ -57,9 +58,8 @@ This module works mechanically similar to lib/modal.fnl. | |
" | ||
(atom.swap! actions (fn [] [action data]))) | ||
|
||
|
||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||
;; Event Dispatchers | ||
;; Action senders | ||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||
|
||
(fn enter | ||
|
@@ -72,7 +72,7 @@ This module works mechanically similar to lib/modal.fnl. | |
Transitions to the entered finite-state-machine state. | ||
Returns nil. | ||
" | ||
(fsm.dispatch :enter-app app-name)) | ||
(fsm.send :enter-app app-name)) | ||
|
||
(fn leave | ||
[app-name] | ||
|
@@ -82,7 +82,7 @@ This module works mechanically similar to lib/modal.fnl. | |
Transition the state machine to idle from active app state. | ||
Returns nil. | ||
" | ||
(fsm.dispatch :leave-app app-name)) | ||
(fsm.send :leave-app app-name)) | ||
|
||
(fn launch | ||
[app-name] | ||
|
@@ -92,7 +92,7 @@ This module works mechanically similar to lib/modal.fnl. | |
Calls the launch lifecycle method defined for an app in config.fnl | ||
Returns nil. | ||
" | ||
(fsm.dispatch :launch-app app-name)) | ||
(fsm.send :launch-app app-name)) | ||
|
||
(fn close | ||
[app-name] | ||
|
@@ -102,7 +102,8 @@ This module works mechanically similar to lib/modal.fnl. | |
Calls the exit lifecycle method defined for an app in config.fnl | ||
Returns nil. | ||
" | ||
(fsm.dispatch :close-app app-name)) | ||
(fsm.send :close-app app-name)) | ||
|
||
|
||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||
;; Set Key Bindings | ||
|
@@ -140,113 +141,71 @@ This module works mechanically similar to lib/modal.fnl. | |
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||
|
||
(fn ->enter | ||
[state app-name] | ||
[state action app-name] | ||
" | ||
Transition the app state machine from the general, shared key bindings to an | ||
app we have local keybindings for. | ||
Runs the following side-effects | ||
- Unbinds the previous app local keys if there were any set | ||
- Calls the :deactivate method of previous app config.fnl table lifecycle | ||
precautionary in case it was set by a previous app in use | ||
- Calls the :activate method of the current app config.fnl table if config | ||
exists for current app | ||
Takes the current app state machine state table | ||
Returns the next app state machine state table | ||
" | ||
(let [{:apps apps | ||
:app prev-app | ||
:unbind-keys unbind-keys} state | ||
next-app (find (by-key app-name) apps)] | ||
(when next-app | ||
(call-when unbind-keys) | ||
(lifecycle.deactivate-app prev-app) | ||
(lifecycle.activate-app next-app) | ||
{:status :in-app | ||
:app next-app | ||
:unbind-keys (bind-app-keys next-app.keys) | ||
:action :enter-app}))) | ||
|
||
(fn in-app->enter | ||
[state app-name] | ||
" | ||
Transition the app state machine from an app the user was using with local keybindings | ||
to another app that may or may not have local keybindings. | ||
Runs the following side-effects | ||
- Unbinds the previous app local keys | ||
- Calls the :deactivate method of previous app config.fnl table lifecycle | ||
- Calls the :activate method of the current app config.fnl table for the new app | ||
that we are activating | ||
Kicks off an effect to bind app-specific keys. | ||
Takes the current app state machine state table | ||
Returns the next app state machine state table | ||
Returns update modal state machine state table. | ||
" | ||
(let [{:apps apps | ||
:app prev-app | ||
:unbind-keys unbind-keys} state | ||
(let [{: apps | ||
: app} state.context | ||
next-app (find (by-key app-name) apps)] | ||
(when next-app | ||
(call-when unbind-keys) | ||
(lifecycle.deactivate-app prev-app) | ||
(lifecycle.activate-app next-app) | ||
{:status :in-app | ||
:app next-app | ||
:unbind-keys (bind-app-keys next-app.keys) | ||
:action :enter-app}))) | ||
{:state {:current-state :in-app | ||
:context {:apps apps | ||
:app next-app | ||
:prev-app app}} | ||
:effect :enter-app-effect})) | ||
|
||
(fn in-app->leave | ||
[state app-name] | ||
" | ||
Transition the app state machine from an app the user was using with local keybindings | ||
to another app that may or may not have local keybindings. | ||
Runs the following side-effects | ||
- Unbinds the previous app local keys | ||
- Calls the :deactivate method of previous app config.fnl table lifecycle | ||
- Calls the :activate method of the current app config.fnl table for the new app | ||
that we are activating | ||
Takes the current app state machine state table | ||
Returns the next app state machine state table | ||
" | ||
(let [{:apps apps | ||
:app current-app | ||
:unbind-keys unbind-keys} state] | ||
(if (= current-app.key app-name) | ||
(do | ||
(call-when unbind-keys) | ||
(lifecycle.deactivate-app current-app) | ||
{:status :general-app | ||
:app :nil | ||
:unbind-keys :nil | ||
:action :leave-app}) | ||
nil))) | ||
|
||
(fn ->launch | ||
[state app-name] | ||
" | ||
Using the state machine we also react to launching apps by calling the :launch lifecycle method | ||
on apps defined in a user's config.fnl. This way they can run hammerspoon functions when an app | ||
is opened like say resizing emacs on launch. | ||
Takes the current app state machine state table | ||
Calls the lifecycle method on the given app config defined in config.fnl | ||
Returns nil which tells the statemachine that no state updates have ocurred. | ||
" | ||
(let [{:apps apps} state | ||
app-menu (find (by-key app-name) apps)] | ||
(lifecycle.launch-app app-menu) | ||
nil)) | ||
(fn in-app->leave | ||
[state action app-name] | ||
" | ||
Transition the app state machine from an app the user was using with local | ||
keybindings to another app that may or may not have local keybindings. | ||
Because a 'enter (new) app' action is fired before a 'leave (old) app', we | ||
know that this will be called AFTER the enter transition has updated the | ||
state, so we should not update the state. | ||
Takes the current app state machine state table, | ||
Kicks off an effect to run leave-app effects and unbind the old app's keys | ||
Returns the old state. | ||
" | ||
{:state state | ||
:effect :leave-app-effect}) | ||
|
||
(fn launch-app | ||
[state action app-name] | ||
" | ||
Using the state machine we also react to launching apps by calling the :launch | ||
lifecycle method on apps defined in a user's config.fnl. This way they can run | ||
hammerspoon functions when an app is opened like say resizing emacs on launch. | ||
Takes the current app state machine state table. | ||
Kicks off an effect to bind app-specific keys & fire launch app lifecycle | ||
Returns a new state. | ||
" | ||
(let [{: apps | ||
: app} state | ||
next-app (find (by-key app-name) apps)] | ||
{:state {:current-state :in-app | ||
:context {:apps apps | ||
:app next-app | ||
:prev-app app}} | ||
:effect :launch-app-effect})) | ||
|
||
(fn ->close | ||
[state app-name] | ||
[state action app-name] | ||
" | ||
Using the state machine we also react to launching apps by calling the :close lifecycle method | ||
on apps defined in a user's config.fnl. This way they can run hammerspoon functions when an app | ||
is closed. For instance re-enabling vim mode when an app is closed that was incompatible | ||
Using the state machine we also react to launching apps by calling the :close | ||
lifecycle method on apps defined in a user's config.fnl. This way they can run | ||
hammerspoon functions when an app is closed. For instance re-enabling vim mode | ||
when an app is closed that was incompatible | ||
Takes the current app state machine state table | ||
Calls the lifecycle method on the given app config defined in config.fnl | ||
Returns nil which tells the statemachine that no state updates have ocurred. | ||
Kicks off an effect to bind app-specific keys | ||
Returns the old state | ||
" | ||
(let [{:apps apps} state | ||
app-menu (find (by-key app-name) apps)] | ||
(lifecycle.close-app app-menu) | ||
nil)) | ||
{:state state | ||
:effect :close-app-effect}) | ||
|
||
|
||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||
|
@@ -261,22 +220,17 @@ Defines the two states our app state machine can be in: | |
modal menu items, or lifecycle methods to trigger other hammerspoon functions | ||
Maps each state to a table of actions mapped to handlers responsible for | ||
returning the next state the statemachine is in. | ||
|
||
TODO: Currently each handler function is responsible for performing transition | ||
side effects like cleaning up previous key bindings and lifecycle methods | ||
as well as returning the next statemachine state. | ||
In the near future we can likely separate those responsibilities out more | ||
akin to something like ClojureScript's re-frame or JS's redux. | ||
" | ||
|
||
(local states | ||
{:general-app {:enter-app ->enter | ||
:leave-app noop | ||
:launch-app ->launch | ||
:close-app ->close} | ||
:in-app {:enter-app in-app->enter | ||
:leave-app in-app->leave | ||
:launch-app ->launch | ||
:close-app ->close}}) | ||
{:general-app {:enter-app ->enter | ||
:leave-app noop | ||
:launch-app launch-app | ||
:close-app ->close} | ||
:in-app {:enter-app ->enter | ||
:leave-app in-app->leave | ||
:launch-app launch-app | ||
:close-app ->close}}) | ||
|
||
|
||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||
|
@@ -339,23 +293,19 @@ Assign some simple keywords for each hs.application.watcher event type. | |
fsm.state :log-state | ||
(fn log-state | ||
[state] | ||
(log.df "app is now: %s" (and state.app state.app.key))))) | ||
(log.df "app is now: %s" (and state.context.app state.context.app.key))))) | ||
|
||
(fn proxy-actions | ||
[fsm] | ||
(fn action-watcher | ||
[{: prev-state : next-state : action : effect : extra}] | ||
" | ||
Internal API function to emit app-specific state machine events and transitions to | ||
other state machines. Like telling our modal state machine the user has | ||
entered into emacs so display the emacs-specific menu modal. | ||
Takes the apps finite state machine instance. | ||
Performs a side-effect to watch the finite-state-machine and log each action | ||
to a list of actions other FSMs can subscribe to like a stream. | ||
Subscribes to the apps state machine. | ||
Takes a transition record from the FSM. | ||
Returns nil. | ||
" | ||
(atom.add-watch fsm.state :actions | ||
(fn action-watcher | ||
[state] | ||
(emit state.action state.app)))) | ||
(emit action next-state.context.app)) | ||
|
||
|
||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||
|
@@ -373,7 +323,7 @@ Assign some simple keywords for each hs.application.watcher event type. | |
" | ||
(when fsm | ||
(let [state (atom.deref fsm.state)] | ||
state.app))) | ||
state.context.app))) | ||
|
||
(fn subscribe | ||
[f] | ||
|
@@ -390,6 +340,70 @@ Assign some simple keywords for each hs.application.watcher event type. | |
[] | ||
(atom.remove-watch actions key)))) | ||
|
||
(fn enter-app-effect | ||
[context] | ||
" | ||
Bind keys and lifecycle for the new current app. | ||
Return a cleanup function to cleanup these bindings. | ||
" | ||
(when context.app | ||
(lifecycle.activate-app context.app) | ||
(let [unbind-keys (bind-app-keys context.app.keys)] | ||
(fn [] | ||
(unbind-keys))))) | ||
|
||
(fn launch-app-effect | ||
[context] | ||
" | ||
Bind keys and lifecycle for the next current app. | ||
Return a cleanup function to cleanup these bindings. | ||
" | ||
(when context.app | ||
(lifecycle.launch-app context.app) | ||
(let [unbind-keys (bind-app-keys context.app.keys)] | ||
(fn [] | ||
(unbind-keys))))) | ||
|
||
(fn my-effect-handler | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor change but could use a better name. Some options that come to mind:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep! I forgot about this one |
||
[effect-map] | ||
" | ||
Takes a map of effect->function and returns a function that handles these | ||
effects by calling the mapped-to function, and then calls that function's | ||
return value (a cleanup function) and calls it on the next transition. | ||
|
||
Unlike the fsm's effect-handler, these are app-aware and only call the cleanup | ||
function for that particular app. | ||
|
||
These functions must return their own cleanup function or nil. | ||
" | ||
;; Create a one-time atom used to store the cleanup function map | ||
(let [cleanup-ref (atom.new {})] | ||
;; Return a subscriber function | ||
(fn [{: prev-state : next-state : action : effect : extra}] | ||
;; Call the cleanup function for this app if it's set | ||
(call-when (. (atom.deref cleanup-ref) extra)) | ||
(let [cleanup-map (atom.deref cleanup-ref) | ||
effect-func (. effect-map effect)] | ||
;; Update the cleanup entry for this app with a new func or nil | ||
(atom.reset! cleanup-ref | ||
(merge cleanup-map | ||
{extra (call-when effect-func next-state extra)})))))) | ||
|
||
(local apps-effect | ||
(my-effect-handler | ||
{:enter-app-effect (fn [state extra] | ||
(enter-app-effect state.context)) | ||
:leave-app-effect (fn [state extra] | ||
(when state.context.prev-app | ||
(lifecycle.deactivate-app state.context.prev-app)) | ||
nil) | ||
:launch-app-effect (fn [state extra] | ||
(launch-app-effect state.context)) | ||
:close-app-effect (fn [state extra] | ||
(when state.context.prev-app | ||
(lifecycle.close-app state.context.prev-app)) | ||
nil)})) | ||
|
||
|
||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||
;; Initialization | ||
|
@@ -404,15 +418,17 @@ Assign some simple keywords for each hs.application.watcher event type. | |
Returns a function to cleanup the hs.application.watcher. | ||
" | ||
(let [active-app (active-app-name) | ||
initial-state {:apps config.apps | ||
:app nil | ||
:status :general-app | ||
:unbind-keys nil | ||
:action nil} | ||
initial-context {:apps config.apps | ||
:app nil} | ||
template {:state {:current-state :general-app | ||
:context initial-context} | ||
:states states | ||
:log "apps"} | ||
app-watcher (hs.application.watcher.new watch-apps)] | ||
(set fsm (statemachine.new states initial-state :status)) | ||
(set fsm (statemachine.new template)) | ||
(fsm.subscribe apps-effect) | ||
(start-logger fsm) | ||
(proxy-actions fsm) | ||
(fsm.subscribe action-watcher) | ||
(enter active-app) | ||
(: app-watcher :start) | ||
(fn cleanup [] | ||
|
@@ -424,6 +440,6 @@ Assign some simple keywords for each hs.application.watcher event type. | |
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||
|
||
|
||
{:init init | ||
:get-app get-app | ||
:subscribe subscribe} | ||
{: init | ||
: get-app | ||
: subscribe} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we discussed naming functions after verbs. Though this is pretty concise now maybe it would benefit from being an inline function?