Enlico
Enlico

Reputation: 28500

Can I have a monitor in XMobar that keeps state form one invocation to the next?

tl;dr

I guess my question could be distilled to a Y/N question: "Can the IO monad keep state only via I/O actions?" In other words, is my understanding correct that if I have to write an action

run :: IO String

that is executed repeatedly by some other IO monad instance, there's no way I can write it so that it keeps memory, from a call to the next, of a state, other than by serializing the state somewhere (e.g. to file or in the caller if it offers an API to do so)?

But if the answer is "Yes, you can't do that", then question is the one in the title: how can I write a monitor for XMobar which keeps a local state?

The full story

I'm experimenting with xmobar, and in particular with its plugins and monitors.

All monitors are run via Run $ SomeMonitor where SomeMonitor must be of type Runnable, i.e. it can be any type implementing the (Exec r, Read r, Show r) interface, of which Exec is the interesting one, because it's where you put the business logic of the plugin, as it has type IO String, so it can create the String that is displayed by XMobar using IO.

Here was my first, very easy, experiment,

data ArchUpdates = ArchUpdates deriving (Read, Show)

instance Exec ArchUpdates where
  rate _ = 36000
  run _ = fmap (makeMessage . length . lines) $ getCommandOutput "checkupdates"
  where
    makeMessage :: Int -> String
    makeMessage = show -- simplified version

where I determine how many updates are possible via pacman on my ArchLinux system.

Now, clearly the value shown by XMobar changes over time, but not because it has an explicit dependency on time; it's because the state of the system changes, and the change is retrieved via an I/O action, expressed in the code above by getCommandOutput "checkupdates".

But what if I wanted a plugin with an explicit dependency on time? I mean, a plugin that updates every second and shows, in turn, one of several strings? Say it shows first "A long time ago", then "in a galaxy far,", and finally "far away...", and then loops again.

My requirement makes me think of State, but the Exec constraint is forcing me in the IO monad, so I don't think that the StateT transformer is the way to go, because I can't use another monad wrapping IO in run. I'd rather need IO to wrap some state... but I can't change IO!

Therefore the only way I see to keep state is for serializing it somewhere, and then re-reading it:

instance Exec MyPlugin where
  rate _ = 1000
  run _ = do
    old <- getCurrentStateOfSelf
    return $ makeNewState old

where makeNewState is String -> String¹, but getCurrentStateOfSelf should be provided by XMobar's API, otherwise what'd be left? Writing the state to a file?

  run _ = do
    old <- readStateFromFile
    let new = makeNewState old
    writeStateToFile new
    return new

This would be horrible.


I tried asking ChatGPT, and it gave me this:

import Control.Monad.State

func :: StateT Int IO String
func = do
  currentState <- get
  liftIO $ putStrLn $ "Current state: " ++ show currentState
  modify (+1)
  return "Hello, World!"

main :: IO ()
main = do
  (result, newState) <- runStateT func 0
  putStrLn $ "Result: " ++ result
  putStrLn $ "Final state: " ++ show newState

but I don't think that's a solution, because main is still in charge of making multiple calls to runStateT piping the state from one call to another; it's not like multiple calls to main are accessing successive states of func, which is what I'd want.

I guess probably the answer is that I'm hitting a wall: XMobar montitors have been designed in the IO monad whereas I'd need the State monad (then clearly XMobar would still use the IO monad to show stuff on screen, because after all it is a program, so it comes from compiling a main function, which is IO ()).


(¹) In the case of the 3 distinct strings above, it could be a closure with a [String] local state that takes a String, locates it in the cycle state, and gives back the item after that.

Upvotes: 4

Views: 226

Answers (2)

duplode
duplode

Reputation: 34398

The general approach in amalloy's answer of initialising an IORef in the setup code and then making it available to the loop action is sound. However, as we have found out, by doing the initialisation in the main of xmobar.hs and trying to pass it through the plugin type we end up swimming against the current of the arrangement xmobar expects for its plugins. We can instead adapt the idea while implementing Exec in terms of start (:: Exec e => e -> (String -> IO ()) -> IO ()) rather than run (:: Exec e => e -> IO String), at the modest cost of having to lay down the updating loop ourselves. (The snippet below is partly based on the Date example from xmobar's docs.)

import Xmobar
import Data.IORef
import Control.Monad (forever)

data MyPlugin = MyPlugin
  deriving (Read, Show)

instance Exec MyPlugin where
  start MyPlugin callback = do
    ref <- newIORef 0
    forever $ do
      output <- atomicModifyIORef' ref next
      callback output
      tenthSeconds 10  -- Wait one second.
    where
    next x = (x + 1, show x)

-- Then add MyPlugin to your config's commands and template as usual.

Incidentally, since start allows us to write the update loop, using StateT for the updates now becomes viable as well:

import Xmobar
import Control.Monad.State.Strict
import Control.Monad (forever)

-- etc.

instance Exec MyPlugin where
  start MyPlugin callback = evalStateT loop 0
    where
    next = gets show <* modify' (+ 1)
    loop = forever $ do
      output <- next
      liftIO $ do
        callback output
        tenthSeconds 10

Upvotes: 2

amalloy
amalloy

Reputation: 92117

It sorta depends. What is the API by which you give these IO actions to xmobar? If you can perform IO of your own to create an IO action, you can construct an IORef that lives outside of the IO action itself, for it to refer to and store data in. But if you can't interact with how xmobar starts, and all it asks you for is some IO a actions, you can't really smuggle an outside IORef in there (unless you cheat with unsafePerformIO).

But let's imagine that you are running xmobar yourself, and it returns some kind of IO action:

runXmobar :: [IO String] -> IO ()
runXmobar = undefined -- implemented by xmobar

Your main can look something like this:

main = do
  ref <- newIORef 0
  runXmobar [uptick ref]

uptick :: IORef Int -> IO String
uptick r = atomicModifyIORef' r next
  where next x = (x + 1, show x)

Here we've achieved what you asked for in your TL;DR question: an IO String action (uptick ref) that saves state locally to produce a different result each time it's called.


How to fit that into an Exec instance? It's not hard. duplode outlines it in a comment on this answer: store the IORef as a field in your object so that the run action can refer to it.

data MyPlugin = MyPlugin (IORef Int)

instance Exec MyPlugin where
  -- Read, Show, etc...
  run (MyPlugin r) = atomicModifyIORef' r next
    where next x = (x + 1, show x)

Construct this MyPlugin object in the main in your xmobar.hs, and include it in your config. You won't be able to define config as a top-level value anymore, because it depends on a value constructed in main by IO, but you can either define it in main, or make it a function taking a MyPlugin object as an argument.

main = do
  myplugin <- MyPlugin <$> newIORef 0
  let config = defaultConfig {
    -- ...
    commands = [
               -- ...
               , Run myplugin
               ]
  }
  xmobar =<< configFromArgs config

Upvotes: 3

Related Questions