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 16 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
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
8 changes: 5 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
221 changes: 221 additions & 0 deletions lib/new-statemachine.fnl
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
(require-macros :lib.macros)
(local atom (require :lib.atom))
(local {: butlast
: call-when
: concat
: conj
: last
: merge
: slice} (require :lib.functional))

(local log (hs.logger.new "\tstatemachine.fnl\t" "debug"))

;; Finite state machine
;; Template schema
;; {
;; ; The state is converted to an atom in the contructor
;; :state {:current-state :state1
;; :context {}}
;; ; States table: A map of state names to a map of actions to functions
;; ; These functions must return a map containing the new state keyword, the
;; ; effect, and a new context
;; :states {:state1 {}
;; :state2 {}
;; :state3 {:leave state3-leave
;; :exit state3-exit}}}

; TODO: Handle a signal with no handler for the provided action. E.g. if a state
; has a keyword instead of a function should we just create a new state from the
; old one, setting the new current-state to the key? This would allow simple
; transitions that don't change context, but still allow subscribers a chance to
; run (though the 'effect' will be nil)

(fn update-state
[fsm state]
(atom.swap! fsm.state (fn [_ state] state) state))

(fn get-transition-function
[fsm current-state action]
(. fsm.states current-state action))

(fn get-state
[fsm]
(atom.deref fsm.state))

(fn signal
Grazfather marked this conversation as resolved.
Show resolved Hide resolved
[fsm action extra]
"Based on the action and the fsm's current-state, set the new state and call
all subscribers with the previous state, new state, action, and extra"
(let [state (get-state fsm)
{: current-state : context} state]
(if-let [tx-fn (get-transition-function fsm current-state action)]
(let [
; TODO: Should we pass the whole state (current state and context) or just context?
transition (tx-fn state action extra)
new-state transition.state
effect transition.effect]
; If next-state is nil, error: Means the action is not expected in this state

(update-state fsm new-state)
; Call all subscribers
(each [_ sub (pairs (atom.deref fsm.subscribers))]
(sub {:prev-state state :next-state new-state : action : effect : extra}))
true)
(do
(if fsm.log
(fsm.log.wf "Action :%s does not have a transition function in state :%s"
action current-state))
false))))

(fn subscribe
[fsm sub]
"Adds a subscriber to the provided fsm. Returns a function to unsubscribe"
; Super naive: Returns a function that just removes the entry at the inserted
; key, but doesn't allow the same function to subscribe more than once since
; its keyed by the string of the function itself.
(let [sub-key (tostring sub)]
Grazfather marked this conversation as resolved.
Show resolved Hide resolved
(atom.swap! fsm.subscribers (fn [subs sub]
(merge {sub-key sub} subs)) sub)
; Return the unsub func
(fn []
(atom.swap! fsm.subscribers (fn [subs key] (tset subs key nil) subs) sub-key))))

(fn effect-handler
[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.

These functions must return their own cleanup function or nil.
"
;; Create a one-time atom used to store the cleanup function
(let [cleanup-ref (atom.new nil)]
;; Return a subscriber function
(fn [{: prev-state : next-state : action : effect : extra}]
;; Whenever a transition occurs, call the cleanup function, if set
(call-when (atom.deref cleanup-ref))
;; Get a new cleanup function or nil and update cleanup-ref atom
(atom.reset! cleanup-ref
; TODO: Should we provide everything e.g. prev-state, action, effect?
(call-when (. effect-map effect) next-state extra)))))

(fn create-machine
Grazfather marked this conversation as resolved.
Show resolved Hide resolved
[template]
(let [fsm {:state (atom.new {:current-state template.state.current-state :context template.state.context})
:states template.states
; TODO: Use something less naive for subscribers
jaidetree marked this conversation as resolved.
Show resolved Hide resolved
:subscribers (atom.new {})
:log (if template.log (hs.logger.new template.log "info"))}]
; Add methods
(tset fsm :get-state (partial get-state fsm))
(tset fsm :signal (partial signal fsm))
(tset fsm :subscribe (partial subscribe fsm))
fsm))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Example
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(var modal-fsm nil)

;; Transition functions
(fn enter-menu
[state action extra]
{:state {:current-state :menu
:context (merge state.context {:menu-stack (conj state.context.menu-stack extra)
:current-menu :main})}
:effect :modal-opened})

(fn up-menu
[state action extra]
"Go up a menu in the stack."
; Pop the menu off the stack & calculate new state transition
(let [stack (butlast state.context.menu-stack)
depth (length stack)
target-state (if (= 0 depth) :idle :menu)
target-effect (if (= :idle target-state) :modal-closed :modal-opened)
new-menu (last stack)]
{:state {:current-state target-state
:context (merge state.context {:menu-stack stack
:current-menu new-menu})}
:effect target-effect}) )

(fn leave-menu
[state action extra]
{:state {:current-state :idle
:context (merge state.context {:menu-stack state.context.menu-stack
:menu :main-menu})}
:effect :modal-closed})

;; State machine
(local modal-states
{:state {:current-state :idle
:context {
; This would be structured based on config in the modal module
:menu-hierarchy {:a {}
:b {}
:c {}}
:current-menu :nil
:menu-stack []}}
:states {:idle {:leave :idle
:open enter-menu}
:menu {:leave leave-menu
:back up-menu
:select enter-menu}}
:log "modal FSM"})


;; Effect handlers
(fn modal-opened-menu-handler
[state extra]
(log.wf "Modal opened menu handler called")
(alert (string.format "MENU %s" extra))
;; Return a cleanup func
(fn [] (log.wf "Modal opened menu handler CLEANUP called")))

(fn modal-closed-menu-handler
[state extra]
(log.wf "Modal closed menu handler called")
(alert (string.format "MENU %s" extra))
;; Return a cleanup func
(fn [] (log.wf "Modal closed menu handler CLEANUP called")))

(fn modal-opened-key-handler
[state extra]
(log.wf "Modal opened key handler called")
; TODO: Make this consider keys relative to its position in the hierarchy
(if (. state :context :menu-hierarchy extra)
(log.wf "Key in hierarchy")
(log.wf "Key NOT in hierarchy"))
;; Return a cleanup func
(fn [] (log.wf "Modal opened key handler CLEANUP called")))

; Create FSM
(set modal-fsm (create-machine modal-states))

; Add subscribers
(local unsub-menu-sub
(subscribe modal-fsm (effect-handler {:modal-opened modal-opened-menu-handler
:modal-closed modal-closed-menu-handler})))
(local unsub-key-sub
(subscribe modal-fsm (effect-handler {:modal-opened modal-opened-key-handler})))

; Debuging bindings. Call it in config.fnl so the bindings aren't not trampled ;; DELETEME
(fn bind [] ;; DELETEME
(hs.hotkey.bind [:alt :cmd :ctrl] :v ;; DELETEME
(fn [] ;; DELETEME
(log.wf "XXX Current stack: %s" ;; DELETEME
(hs.inspect (. (atom.deref modal-fsm.state) :context :menu-stack))))) ;; DELETEME
(hs.hotkey.bind [:cmd] :o (fn [] (signal modal-fsm :open :main))) ;; DELETEME
(hs.hotkey.bind [:cmd] :u (fn [] (signal modal-fsm :back nil))) ;; DELETEME
(hs.hotkey.bind [:cmd] :l (fn [] (signal modal-fsm :leave nil))) ;; DELETEME
(hs.hotkey.bind [:cmd] :a (fn [] (signal modal-fsm :select :a))) ;; DELETEME
(hs.hotkey.bind [:cmd] :r (fn [] (signal modal-fsm :select :b))) ;; DELETEME
(hs.hotkey.bind [:cmd] :s (fn [] (signal modal-fsm :select :c)))) ;; DELETEME

{: effect-handler
: signal
: bind ;; DELETEME
: subscribe
:new create-machine}
4 changes: 4 additions & 0 deletions lib/testing/assert.fnl
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
[actual expected message]
(assert (= actual expected) (.. message " instead got " (hs.inspect actual))))

(fn exports.not-eq?
[first second message]
(assert (not= first second) (.. message " instead both were " (hs.inspect first))))

(fn exports.ok?
[actual message]
(assert (= (not (not actual)) true) (.. message " instead got " (hs.inspect actual))))
Expand Down
Loading