Skip to content
jutaro edited this page Sep 29, 2011 · 6 revisions

Services (billeksah-services)

A plugin is a standard cabal library, with one speciality. It exports a function with Type StateM (PluginInterface event)

data PluginInterface event =
    PluginInterface {
        piInit1   ∷  BaseEvent → EventChannel event → StateM (),
        piInit2   ∷  BaseEvent → EventChannel event → StateM (),
        piEvent   ∷  EventChannel event,
        piName    ∷  String,
        piVersion ∷  Version}

A plugin has a name and a version. The convention is that name and version equals the cabal package name and version.

Every plugin exposes an event, which gives it the possibility to inform interested plugins about changes. See the section on Events. Events are used as well as callback mechanism, as they can return values. This is a somehow adventurous design decision.

Every plugin exposes two initialization functions, which are called at start-up. In the first round, you can do any local initialization. In the second round you can register events.

The StateM monad type is explained now.

You need to import Base to use billeksah-services in your code.

State

The basic services of the plugin mechanism contains a state service. State is registered with a kind of a name called Selector. A selector has to be an instance of Eq,Ord,Show and Typeable and an instance of class Selector. Selectors are used as well in other places, like events and menu sensitivity. Selectors are special, as they carry with them the type of the value via a type family:

class (Eq α, Ord α, Show α, Typeable α) ⇒ Selector α where
    type ValueType α ∷  *

In the following example a selector which refers to a file path is defined:

-- | The file path for preferences
data PrefsPathSel = PrefsPathSel
    deriving (Eq,Ord,Show,Typeable)h

instance Selector PrefsPathSel where
    type ValueType PrefsPathSel = FilePath

Of course you can one data statement for selectors with the same value type. (data X = Sel1 | Sel2; ...) Every state has to be registered with an initial value before you can get and set the state.

-- | Registers a key and sets the value. Returns Nothing if everything works, 
-- else just an error string
registerState ∷  Selector α ⇒ α → ValueType α → StateM (Maybe String)

-- | Set a value for a key
setState ∷   Selector α ⇒ α  → ValueType α → StateM ()

-- | Get a value for a key
getState ∷  Selector α ⇒ α  → StateM (ValueType α)

We offer some convenience methods to work with the StateM monad:

-- | Is this state registered?
hasState ∷  Selector α ⇒ α → StateM Bool

-- | Do the function with the state and set the result as new value
withState ∷  Selector α ⇒ α → (ValueType α → ValueType α) → StateM ()

-- | Lift an IO action to an IDE action
reifyState ∷  (StateRef → IO a) → StateM a

-- | Do an IDE action in the IO monad
reflectState ∷  StateM a → StateRef → IO a

runInIO ∷  forall α β. (β → StateM α) → StateM (β → IO α)

catchState ∷  Exception e ⇒ StateM a → (e → IO a) → StateM a

forkState ∷  StateAction  → StateAction

The state monad is a ReaderT of an IORef in the IO monad.

type StateM = ReaderT StateRef IO

Events

Events are used to inform interested plugins about changes. They are as well used as a callback mechanism, as they can pass and return a value.

The billeksah-services package defines a BaseEvent channel which can trigger two events:

  1. StartUp, which tells the application after the plugin initialization to start. The Leksah plugin then registers a handler for this event, which is called startupLeksah.

  2. BaseLog MessageLevel String which is used to report messages to the user.

-- from module Base/PluginTypes
type BaseEvent = EventChannel BaseEventValue

data BaseEventValue = StartUp | BaseLog String

-- from module Base/Plugin in runAll
    ... -- do the initialisation
    (evtTrigger baseEvent) StartUp -- Here the application shall take control

-- from module Leksah
leksahInit2 ∷  BaseEvent → EventChannel LeksahEvent → StateM ()
leksahInit2 baseEvent myEvent = do
    registerEvent' baseEvent (λ e → case e of
                                        StartUp → startupLeksah
                                        _ → return ())
    ...

Since an event does return the event value to the place were it is triggered, events can be used as a general callback mechanism. For example the billeksah-pane package builds the menu, the toolbar, the keyboard accelerators from so called action descriptions. When it needs all the ActionDescrs from all pluginsit triggers the RegisterActions event, which has a list of action descriptions as argument.

-- from module Graphics.FrameTypes
data FrameEvent =
      RegisterActions [ActionDescr]
    | ...

-- in any module which adds to the menu, toolbar, or accelerators:
init2 ∷  BaseEvent → EventChannel PluginPaneEvent → StateM ()
init2 baseEvent myEvent = do
    registerFrameEvent (λ e → case e of
                                RegisterActions actions →
                                    return $ RegisterActions $ actions ⊕ myActions
                                otherwise → return e)...

-- in the Graphics.Pane module the event is triggered to receive all actions
startupFrame ... = do
    ...
    RegisterActions allActions ←  triggerFrameEvent (RegisterActions frameActions)
    ...

I give you this examples here to show you the general use of events, and nor to explain how to init an application or build a menu, which shall be described in another place. But now I describe the interface of events.

To make an event channel, you have to construct a selector, as you did for state, but it is even more complicated, as you have to add an instance of EventSelector. From this you can then construct the event:

data DummySel = DummySel
    deriving(Typeable, Show, Ord, Eq)

instance Selector DummySel where
    type ValueType DummySel = EventChannel DummyEvent

... do
 event ← makeEvent DummySel

The event system works together with the state service to make the events persistent, so you can get the event channel everywhere in the state monad. This gives this interface:

-- | Get the event from a plugin name (The type has to fit, otherwise
getEvent ∷  Selector α ⇒ α → StateM (ValueType α)

-- | Registers an event handler for this event.
-- The HandlerID is for unregistering the event
registerEvent ∷  EventChannel α → Handler α → StateM (HandlerID)

type Handler event = event → StateM event

-- | Triggers the event with the provided value
triggerEvent ∷   (Selector α, ValueType α ~ EventChannel event) ⇒ α  →  event  →  StateM event

As a convenience method we as well provide a variation of registerEvent, where the handler doesn't return a value:

registerEvent' ∷  EventChannel α → (α → StateM ()) → StateM (HandlerID)

Some functions to work with events are provided:

-- | Merge two event streams of the same type
unionEvent ∷  Typeable event ⇒ EventChannel event  → EventChannel event  → StateM (EventChannel event )

-- | Allow all events that fulfill the predicate, discard the rest. Think of it as
filterEvent ∷  Typeable e ⇒  (e → Bool) → EventChannel e → StateM (EventChannel e)

propagateEvent ∷  Typeable e ⇒  EventChannel e → [EventChannel e] → StateM ()

retriggerEvent ∷  Typeable e ⇒  EventChannel e → (e → Maybe e) → StateM ()

Messages

You can output messages with the message function. It defines the message levels:

  • Debug
  • Info
  • Warning
  • Error

The BaseEvent BaseLog MessageLevel String is triggered, so your message may appear not only on the console, which is how this package handles the event.

Clone this wiki locally