Skip to content
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 48 commits into from
Oct 15, 2021
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
31a538c
Initial work on new fsm
Grazfather Sep 18, 2021
c674d13
pop/push -> butlast/conf
Grazfather Sep 19, 2021
04e620f
fsm2: Simplify schema and add subscribe functionality
Grazfather Sep 19, 2021
278d760
Add and tweak Jay's effect-handler
Grazfather Sep 19, 2021
8d78658
Keep handlers from touching atoms
Grazfather Sep 19, 2021
71975ba
Cleanup a bit
Grazfather Sep 25, 2021
942d773
create-machine takes initial state in states arg
Grazfather Sep 25, 2021
714a51f
Combine context and state into single atom
Grazfather Sep 25, 2021
9a4da43
cleanup
Grazfather Sep 25, 2021
67299d6
log error if current state has no handler for action
Grazfather Sep 25, 2021
2b96c82
Add methods to fsm
Grazfather Sep 25, 2021
820d818
Let caller provider a logger tag
Grazfather Sep 25, 2021
25cf8bb
Make signal return boolean success
Grazfather Sep 26, 2021
907fc0c
Fix unsub function
Grazfather Sep 26, 2021
278c626
Export effect-handler
Grazfather Sep 26, 2021
284eb43
Add tests
Grazfather Sep 26, 2021
775bb8d
Cleanup tests slightly
Grazfather Sep 26, 2021
ff9b5ec
Add new-modal using new-statemachine
Grazfather Sep 26, 2021
78e79fb
Remove example and add big doc string
Grazfather Sep 26, 2021
1cc0852
Rename timeout transition function
Grazfather Sep 26, 2021
b167f03
Remove more duplicated code for review
Grazfather Sep 26, 2021
8bca53c
Duplicate funcs from statemachine into new
Grazfather Oct 1, 2021
fe2840a
Fix timeout current-state
Grazfather Oct 6, 2021
ac4199e
Remove some todos
Grazfather Oct 6, 2021
a1c0180
remove todo
Grazfather Oct 6, 2021
f3be7ec
Cleanup docstrings
Grazfather Oct 6, 2021
e397806
wip new apps
Grazfather Oct 6, 2021
6eed996
apps: Fix getting current app to display app-menu
Grazfather Oct 8, 2021
31bf99e
apps: Update app even when new app isn't in config
Grazfather Oct 8, 2021
3ec63bb
Log debug not warn
Grazfather Oct 8, 2021
a385ae7
Get app switching in modal working
Grazfather Oct 8, 2021
8b6c388
Cleanup some logging
Grazfather Oct 8, 2021
35873d6
Remove old modal, apps, mostly remove old statemachine
Grazfather Oct 10, 2021
f2e18f8
Fix monkey patch
Grazfather Oct 10, 2021
b4a427e
Remove extra logging from apps and modal
Grazfather Oct 10, 2021
758ac82
wip: Port vim to new statemachine
Grazfather Oct 10, 2021
f7354fb
minor cleanup
Grazfather Oct 10, 2021
7628b47
cleanup in modal
Grazfather Oct 10, 2021
32c6b15
apps: Update app even when new app isn't in config
Grazfather Oct 8, 2021
8b86f26
apps: close & leave effects have to operate on prev app
Grazfather Oct 10, 2021
0883a7f
fix bug in watch-screen
Grazfather Oct 10, 2021
89018c8
statemachine: Rename 'signal' to 'send'
Grazfather Oct 12, 2021
65c0f4e
Remove all debug prints
Grazfather Oct 12, 2021
fe69427
Remove comment
Grazfather Oct 12, 2021
2b8e11b
Better heading
Grazfather Oct 15, 2021
4f2470c
Better function names
Grazfather Oct 15, 2021
9784eed
s/signal/send in statemachine test
Grazfather Oct 15, 2021
53a4cbc
statemachine test: assert in sub func
Grazfather Oct 15, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core.fnl
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ Returns nil. This function causes side-effects.
:apps
:lib.bind
:lib.modal
:lib.new-modal
:lib.apps])

(defadvice get-config-impl
Expand Down
14 changes: 13 additions & 1 deletion lib/functional.fnl
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,16 @@
(let [filtered (filter f tbl)]
(<= 1 (length filtered))))

(fn conj
[tbl e]
"Return a new list with the element e added at the end"
(concat tbl [e]))

(fn butlast
[tbl]
"Return a new list with all but the last item"
(slice 1 -1 tbl))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Others
Expand All @@ -226,9 +236,11 @@
;; Exports
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

{: call-when
{: butlast
: call-when
: compose
: concat
: conj
: contains?
: count
: eq?
Expand Down
16 changes: 13 additions & 3 deletions lib/modal.fnl
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ switching menus in one place which is then powered by config.fnl.
(local atom (require :lib.atom))
(local statemachine (require :lib.statemachine))
(local apps (require :lib.apps))
(local {: call-when
(local {: butlast
: call-when
: concat
: conj
: find
: filter
: has-some?
Expand Down Expand Up @@ -272,7 +274,7 @@ switching menus in one place which is then powered by config.fnl.
:stop-timeout :nil
:unbind-keys (bind-menu-keys menu.items)
:history (if history
(concat [] history [menu])
(conj history menu)
[menu])})


Expand Down Expand Up @@ -438,7 +440,7 @@ switching menus in one place which is then powered by config.fnl.
(show-modal-menu (merge state
{:menu prev-menu
:prev-menu menu}))
{:history (slice 1 -1 history)})
{:history (butlast history)})
(idle->active state))))


Expand Down Expand Up @@ -536,4 +538,12 @@ switching menus in one place which is then powered by config.fnl.


{:init init
: modal-alert ;; DELETEME: Temporary for PR
: timeout ;; DELETEME: Temporary for PR
: by-key ;; DELETEME: Temporary for PR
: activate-modal ;; DELETEME: Temporary for PR
: deactivate-modal ;; DELETEME: Temporary for PR
: previous-modal ;; DELETEME: Temporary for PR
: proxy-app-action ;; DELETEME: Temporary for PR
: bind-menu-keys
:activate-modal activate-modal}
297 changes: 297 additions & 0 deletions lib/new-modal.fnl
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
"
Displays the menu modals, sub-menus, and application-specific modals if set
in config.fnl.

We define a state machine, which uses our local states to determine states, and
transitions. Then we can signal events that may transition between specific
states defined in the table.

Allows us to create the machinery for displaying, entering, exiting, and
switching menus in one place which is then powered by config.fnl.
"
(local atom (require :lib.atom))
(local statemachine (require :lib.new-statemachine))
(local om (require :lib.modal)) ;; DELETEME: For PR
(local apps (require :lib.apps))
(local {: butlast
: call-when
: concat
: conj
: find
: filter
: has-some?
: identity
: join
: map
: merge
: noop}
(require :lib.functional))
(local {:align-columns align-columns}
(require :lib.text))
(local {:action->fn action->fn
:bind-keys bind-keys}
(require :lib.bind))
(local lifecycle (require :lib.lifecycle))

(local log (hs.logger.new "\tmodal.fnl\t" "debug"))
(var fsm nil)


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Display Modals
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(fn show-modal-menu
[context]
"
Main API to display a modal and run side-effects
- Display the modal alert
Takes current modal state from our modal statemachine
Returns the function to cleanup everything it sets up
"
(lifecycle.enter-menu context.menu)
(om.modal-alert context.menu)
(let [unbind-keys (om.bind-menu-keys context.menu.items)
stop-timeout context.stop-timeout]
(fn []
(hs.alert.closeAll 0)
(unbind-keys)
(call-when stop-timeout)
(lifecycle.exit-menu context.menu)
)))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; State Transition Functions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


(fn idle->active
[state action extra]
"
Transition our modal statemachine from the idle state to active where a menu
modal is displayed to the user.
Takes the current modal state table plus the key of the menu if submenu
Kicks off an effect to display the modal or local app menu
Returns updated modal state machine state table.
"
(let [config state.context.config
app-menu (apps.get-app)
menu (if (and app-menu (has-some? app-menu.items))
app-menu
config)]
(log.wf "TRANSITION: idle->active app-menu %s menu %s config %s" app-menu menu config) ;; DELETEME
{:state {:current-state :active
:context (merge state.context {:menu menu
:history (if state.history
(conj history menu)
[menu])})}
:effect :show-modal-menu}))


(fn active->idle
[state action extra]
"
Transition our modal state machine from the active, open state to idle.
Takes the current modal state table.
Kicks off an effect to close the modal, stop the timeout, and unbind keys
Returns updated modal state machine state table.
"
(log.wf "TRANSITION: active->idle") ;; DELETEME
{:state {:current-state :idle
:context (merge state.context {:menu :nil
:history []})}
:effect :close-modal-menu})


(fn active->enter-app
[state action extra]
"
Transition our modal state machine the main menu to an app menu
Takes the current modal state table and the app menu table.
Displays updated modal menu if the current menu is different than the previous
menu otherwise results in no operation
Returns new modal state
"
(log.wf "TRANSITION: active->enter-app") ;; DELETEME
(let [{:config config
:menu prev-menu} state.context
app-menu (apps.get-app)
menu (if (and app-menu (has-some? app-menu.items))
app-menu
config)]
(if (= menu.key prev-menu.key)
; nil transition object means keep all state
nil
{:state {:current-state :submenu
:context (merge state.context {:menu menu})}
:effect :open-submenu})))


(fn active->leave-app
[state action extra]
"
Transition to the regular menu when user removes focus (blurs) another app.
If the leave event was fired for the app we are already in, do nothing.
Takes the current modal state table.
Returns new updated modal state if we are leaving the current app.
"
(log.wf "TRANSITION: active->leave-app") ;; DELETEME
(let [{:config config
:menu prev-menu} state.context]
(if (= prev-menu.key config.key)
nil
(idle->active state action extra))))


(fn active->submenu
[state action menu-key]
"
Enter a submenu like entering into the Window menu from the default main menu.
Takes the current menu state table and the submenu key as 'extra'.
Returns updated menu state
"
(let [{:config config
:menu prev-menu} state.context
menu (if menu-key
(find (om.by-key menu-key) prev-menu.items)
config)]
(log.wf "TRANSITION: active->submenu with menu-key %s menu %s" menu-key menu) ;; DELETEME
{:state {:current-state :submenu
:context (merge state.context {:menu menu})}
:effect :open-submenu}))

(fn add-timeout-transition
[state action extra]
"
Transition from active to idle, but this transition only fires when the
timeout occurs. The timeout is only started after firing a repeatable action.
For instance if you enter window > jump east you may want to jump again
without having to bring up the modal and enter the window submenu. We wait for
more modal keypresses until the timeout triggers which will deactivate the
modal.
Takes the current modal state table.
Returns a the old state with a :stop-timeout added
"
(log.wf "TRANSITION: add-timeout-transition") ;; DELETEME
{:state {:current-state state.context.current-state
:context
(merge state.context {:stop-timeout (om.timeout om.deactivate-modal)})}
:effect :open-submenu})

(fn submenu->previous
[state action extra]
"
Transition to the previous submenu. Like if you went into the window menu
and wanted to go back to the main menu.
Takes the modal state table.
Returns a partial modal state table update.
Dynamically calls another transition depending on history.
"
(let [{:config config
:history hist
:menu menu} state.context
prev-menu (. hist (- (length hist) 1))]
(log.wf "TRANSITION: submenu->previous") ;; DELETEME
(if prev-menu
{:state {:current-state :submenu
:context (merge state.context {:menu prev-menu
:history (butlast hist)})}
:effect :open-submenu}
(idle->active state))))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Finite State Machine States
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


;; State machine states table. Maps states to actions to transition functions.
;; These transition functions return transition objects that contain the new
;; state key and context.
(local states
{:idle {:activate idle->active
:enter-app noop
:leave-app noop}
:active {:deactivate active->idle
:activate active->submenu
:start-timeout add-timeout-transition
:enter-app active->enter-app
:leave-app active->leave-app}
:submenu {:deactivate active->idle
:activate active->submenu
:previous submenu->previous
:start-timeout add-timeout-transition
:enter-app noop
:leave-app noop}})


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Watchers, Dispatchers, & Logging
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


(fn start-logger
[fsm]
"
Start logging the status of the modal state machine.
Takes our finite state machine.
Returns nil
Creates a watcher of our state atom to log state changes reactively.
"
(atom.add-watch
fsm.state :log-state
(fn log-state
[state]
(log.df "state is now: %s" state.current-state) ;; DELETEME
(when state.context.history
(log.df (hs.inspect (map #(. $1 :title) state.context.history)))))))

; TODO: Bind show-modal-menu direct
; TODO: Do we only need one effect?
(local modal-effect
(statemachine.effect-handler
{:show-modal-menu (fn [state extra]
(log.wf "Effect: show modal") ;; DELETEME
(show-modal-menu state.context))
:open-submenu (fn [state extra]
(log.wf "Effect: Open submenu with extra %s" extra) ;; DELETEME
(show-modal-menu state.context))}))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Initialization
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(fn init
[config]
"
Initialize the modal state machine responsible for displaying modal alerts
to the user to trigger actions defined by their config.fnl.
Takes the config.fnl table.
Causes side effects to start the state machine, show the modal, and logging.
Returns a function to unsubscribe from the app state machine.
"
(let [initial-context {:config config
:history []
:menu :nil}
template {:state {:current-state :idle
:context initial-context}
:states states
:log "modal"}
unsubscribe (apps.subscribe om.proxy-app-action)]
(set fsm (statemachine.new template))
(tset fsm :dispatch fsm.signal) ; DELETEME: TEMP: Monkey patch dispatch to show dispatchers haven't changed
(fsm.subscribe modal-effect)
(start-logger fsm)
(fn cleanup []
(unsubscribe))))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Exports
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


{:init init
:activate-modal om.activate-modal}
Loading