Newer
Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE OverloadedStrings #-}
module QBar.Core where
import QBar.Pango
import Control.Exception (catch, finally, IOException)
import Control.Monad (forever)
import Control.Concurrent (threadDelay)
import Control.Concurrent.Async
import Control.Concurrent.Event as Event
import Control.Concurrent.MVar
import Data.Aeson.TH
import qualified Data.ByteString.Lazy.Char8 as C8
import qualified Data.HashMap.Lazy as HM
import Data.Int (Int64)
import Data.Maybe (fromMaybe, catMaybes)
import qualified Data.Text.Lazy as T
import qualified Data.Text.Lazy.Encoding as E
import qualified Data.Text.Lazy.IO as TIO
import Numeric (showHex)
import Pipes
import Pipes.Concurrent
import qualified Pipes.Prelude as PP
import System.Exit
import System.IO
import System.Process.Typed (shell, withProcessWait, setStdin, setStdout, getStdout, closed, createPipe, readProcessStdout)
import Data.Colour.RGBSpace
data Block = Block {
values :: HM.HashMap T.Text T.Text,
clickAction :: Maybe (IO ())
}
instance Show Block where
show Block{values} = show values
data Click = Click {
name :: T.Text
} deriving Show
$(deriveJSON defaultOptions ''Click)
type BlockProducer = Producer Block IO ()
data BarUpdateChannel = BarUpdateChannel (IO ())
type BarUpdateEvent = Event.Event
defaultColor :: T.Text
defaultColor = "#969896"
activeColor :: T.Text
activeColor = "#ffffff"
updatingColor :: T.Text
--updatingColor = "#444444"
updatingColor = "#96989677"
createBlock :: T.Text -> Block
createBlock text = setColor defaultColor $ Block {
values = HM.singleton "full_text" text,
clickAction = Nothing
}
createErrorBlock :: T.Text -> Block
createErrorBlock = setColor "ff0000" . createBlock
setValue :: T.Text -> T.Text -> Block -> Block
setValue key val block = block {
values = HM.insert key val (values block)
}
getValue :: T.Text -> Block -> Maybe T.Text
getValue key block = HM.lookup key (values block)
adjustValue :: (T.Text -> T.Text) -> T.Text -> Block -> Block
adjustValue f k block = block {
values = HM.adjust f k (values block)
}
emptyBlock :: Block
emptyBlock = createBlock ""
shortText :: T.Text -> Block -> Block
shortText = setValue "short_text"
fullText :: T.Text -> Block -> Block
fullText = setValue "full_text"
getFullText :: Block -> T.Text
getFullText = fromMaybe "" . getValue "full_text"
setColor :: T.Text -> Block -> Block
setColor = setValue "color"
setBlockName :: T.Text -> Block -> Block
setBlockName = setValue "name"
getBlockName :: Block -> Maybe T.Text
getBlockName = getValue "name"
pangoMarkup :: Block -> Block
pangoMarkup = setValue "markup" "pango"
adjustText :: (T.Text -> T.Text) -> Block -> Block
adjustText f = adjustValue f "full_text" . adjustValue f "short_text"
coloredText :: T.Text -> T.Text -> T.Text
coloredText color text = "<span color='" <> color <> "'>" <> text <> "</span>"
addIcon :: T.Text -> Block -> Block
addIcon icon block = prefixIcon "full_text" $ prefixIcon "short_text" block
where
prefixIcon = adjustValue ((icon <> " ") <>)
removePango :: Block -> Block
removePango block
| getValue "markup" block == Just "pango" = adjustText removePangoFromText $ block {
values = HM.delete "markup" (values block)
}
| otherwise = block
where
removePangoFromText :: T.Text -> T.Text
removePangoFromText text =
case parsePango text of
Left _ -> text
Right parsed -> removeFormatting parsed
modify :: (Block -> Block) -> Pipe Block Block IO ()
modify = PP.map
autoPadding :: Pipe Block Block IO ()
autoPadding = autoPadding' 0 0
where
autoPadding' :: Int64 -> Int64 -> Pipe Block Block IO ()
autoPadding' fullLength shortLength = do
block <- await
let values' = (values block)
let fullLength' = T.length $ HM.lookupDefault "" "full_text" values'
let shortLength' = T.length $ HM.lookupDefault "" "short_text" values'
let values'' = HM.adjust (<> (T.take (fullLength - fullLength') $ T.repeat ' ')) "full_text" values'
let values''' = HM.adjust (<> (T.take (shortLength - shortLength') $ T.repeat ' ')) "short_text" values''
yield block { values = values''' }
autoPadding' (max fullLength fullLength') (max shortLength shortLength')
-- | Create a shared interval. Takes a BarUpdateChannel to signal bar updates and an interval (in seconds).Data.Maybe
-- Returns an IO action that can be used to attach blocks to the shared interval and an async that contains a reference to the scheduler thread.
sharedInterval :: BarUpdateChannel -> Int -> IO (IO Block -> BlockProducer, Async ())
sharedInterval barUpdateChannel seconds = do
clientsMVar <- newMVar ([] :: [(IO Block, Output Block)])
task <- async $ forever $ do
threadDelay $ seconds * 1000000
-- Updates all client blocks
-- If send returns 'False' the clients mailbox has been closed, so it is removed
modifyMVar_ clientsMVar (fmap catMaybes . mapConcurrently runAndFilterClient)
-- Then update the bar
updateBar barUpdateChannel
return (addClient clientsMVar, task)
where
runAndFilterClient :: (IO Block, Output Block) -> IO (Maybe (IO Block, Output Block))
runAndFilterClient client = do
result <- runClient client
return $ if result then Just client else Nothing
runClient :: (IO Block, Output Block) -> IO Bool
runClient (blockAction, output) = do
result <- blockAction
atomically $ send output result {
clickAction = Just (updateClickHandler result)
}
where
updateClickHandler :: Block -> IO ()
updateClickHandler block = do
-- Give user feedback that the block is updating
let outdatedBlock = setColor updatingColor $ removePango block
void $ atomically $ send output $ outdatedBlock
-- Notify bar about changed block state to display the feedback
updateBar barUpdateChannel
-- Run a normal block update to update the block to the new value
void $ runClient (blockAction, output)
-- Notify bar about changed block state, this is usually done by the shared interval handler
updateBar barUpdateChannel
addClient :: MVar [(IO Block, Output Block)] -> IO Block -> BlockProducer
addClient clientsMVar blockAction = do
-- Spawn the mailbox that preserves the latest block
(output, input) <- lift $ spawn $ latest emptyBlock
-- Generate initial block and send it to the mailbox
lift $ void $ runClient (blockAction, output)
-- Register the client for regular updates
lift $ modifyMVar_ clientsMVar $ \ clients -> return ((blockAction, output):clients)
-- Return a block producer from the mailbox
fromInput input
blockScript :: FilePath -> IO Block
blockScript path = do
-- The exit code is used for i3blocks signaling but ignored here (=not implemented)
-- I am trying to replace i3blocks scripts with native haskell blocks, so I do not need it
(exitCode, output) <- readProcessStdout $ shell path
case exitCode of
ExitSuccess -> return $ case map E.decodeUtf8 (C8.lines output) of
(text:short:color:_) -> setColor color $ shortText short $ createScriptBlock text
(text:short:_) -> shortText short $ createScriptBlock text
(text:_) -> createScriptBlock text
[] -> createScriptBlock "-"
(ExitFailure nr) -> return $ createErrorBlock $ "[" <> (T.pack $ show nr) <> "]"
where
createScriptBlock :: T.Text -> Block
createScriptBlock text = pangoMarkup $ setBlockName (T.pack path) $ createBlock text
startPersistentBlockScript :: BarUpdateChannel -> FilePath -> Producer Block IO ()
startPersistentBlockScript barUpdateChannel path = do
(output, input, seal) <- lift $ spawn' $ latest $ emptyBlock
initialDataEvent <- lift $ Event.new
task <- lift $ async $ do
let processConfig = setStdin closed $ setStdout createPipe $ shell path
finally (
catch (
withProcessWait processConfig $ \ process -> do
let handle = getStdout process
runEffect $ (fromHandle handle) >-> signalFirstBlock initialDataEvent >-> toOutput output
)
( \ e ->
-- output error
runEffect $ (yield $ createErrorBlock $ "[" <> (T.pack $ show (e :: IOException)) <> "]") >-> signalFirstBlock initialDataEvent >-> toOutput output
)
)
(atomically seal)
lift $ link task
lift $ Event.wait initialDataEvent
fromInput input
where
signalFirstBlock :: Event.Event -> Pipe Block Block IO ()
signalFirstBlock event = do
-- Await first block
await >>= yield
lift $ Event.set event
-- Replace with cat
cat
fromHandle :: Handle -> Producer Block IO ()
fromHandle handle = forever $ do
line <- lift $ TIO.hGetLine handle
yield $ pangoMarkup $ createBlock line
lift $ updateBar barUpdateChannel
pangoColor :: RGB Double -> T.Text
pangoColor (RGB r g b) =
let r' = hexColorComponent r
g' = hexColorComponent g
b' = hexColorComponent b
in "#" <> r' <> g' <> b'
where
hexColorComponent :: Double -> T.Text
hexColorComponent val = paddedHexComponent $ T.pack $ showHex (max 0 $ min 255 $ (truncate (val * 255) :: Int)) ""
paddedHexComponent hex =
let len = 2 - T.length hex
padding = if len == 1 then "0" else ""
in padding <> hex
updateBar :: BarUpdateChannel -> IO ()
updateBar (BarUpdateChannel updateAction) = updateAction