diff options
| -rw-r--r-- | webgui/data/index.html | 6 | ||||
| -rw-r--r-- | webgui/data/style.css | 24 | ||||
| -rw-r--r-- | webgui/src/Main.hs | 121 |
3 files changed, 110 insertions, 41 deletions
diff --git a/webgui/data/index.html b/webgui/data/index.html index b677387..b0a2de2 100644 --- a/webgui/data/index.html +++ b/webgui/data/index.html | |||
| @@ -59,7 +59,8 @@ | |||
| 59 | <tr> | 59 | <tr> |
| 60 | <td colspan="2" style="border-style:none;"></td> | 60 | <td colspan="2" style="border-style:none;"></td> |
| 61 | <td> | 61 | <td> |
| 62 | <input id="allowDeletion" type="checkbox" /><label for="allowDeletion">Allow Deletion</label> | 62 | <!-- <input id="allowDeletion" type="checkbox" /><label for="allowDeletion">Allow Deletion</label> --> |
| 63 | <button id="enactDeletion">Delete marked</button> | ||
| 63 | </td> | 64 | </td> |
| 64 | </tr> | 65 | </tr> |
| 65 | </tfoot> | 66 | </tfoot> |
| @@ -88,7 +89,8 @@ | |||
| 88 | <tr> | 89 | <tr> |
| 89 | <td colspan="4" style="border-style:none;"></td> | 90 | <td colspan="4" style="border-style:none;"></td> |
| 90 | <td> | 91 | <td> |
| 91 | <input id="allowAbortion" type="checkbox" /><label for="allowAbortion">Allow Abortion</label> | 92 | <!-- <input id="allowAbortion" type="checkbox" /><label for="allowAbortion">Allow Abortion</label> --> |
| 93 | <button id="enactAbortion">Abort marked</button> | ||
| 92 | </td> | 94 | </td> |
| 93 | </tr> | 95 | </tr> |
| 94 | </tfoot> | 96 | </tfoot> |
diff --git a/webgui/data/style.css b/webgui/data/style.css index d496e2a..11c532c 100644 --- a/webgui/data/style.css +++ b/webgui/data/style.css | |||
| @@ -90,12 +90,24 @@ tfoot tr:first-child td { | |||
| 90 | color:#c00000; | 90 | color:#c00000; |
| 91 | } | 91 | } |
| 92 | 92 | ||
| 93 | #draftListBody button { | 93 | tbody tr:nth-child(odd) td { |
| 94 | width:6em; | 94 | background-color:#f8f8f8; |
| 95 | } | ||
| 96 | |||
| 97 | tbody tr:nth-child(even) td { | ||
| 98 | background-color:#ffffff; | ||
| 99 | } | ||
| 100 | |||
| 101 | tbody tr.printer td { | ||
| 102 | background-color:initial; | ||
| 95 | } | 103 | } |
| 96 | 104 | ||
| 97 | tr.focused td { | 105 | tbody tr.focused td { |
| 98 | background-color:#f0f0f0; | 106 | background-color:#dfdfdf; |
| 107 | } | ||
| 108 | |||
| 109 | #draftListBody button { | ||
| 110 | width:6em; | ||
| 99 | } | 111 | } |
| 100 | 112 | ||
| 101 | .close { | 113 | .close { |
| @@ -108,6 +120,10 @@ tr.focused td { | |||
| 108 | text-decoration:none; | 120 | text-decoration:none; |
| 109 | } | 121 | } |
| 110 | 122 | ||
| 123 | span.mark { | ||
| 124 | margin-right:1em; | ||
| 125 | } | ||
| 126 | |||
| 111 | /*----- Tabs -----*/ | 127 | /*----- Tabs -----*/ |
| 112 | .tabs { | 128 | .tabs { |
| 113 | display:block; | 129 | display:block; |
diff --git a/webgui/src/Main.hs b/webgui/src/Main.hs index 018e59b..a295fd9 100644 --- a/webgui/src/Main.hs +++ b/webgui/src/Main.hs | |||
| @@ -21,6 +21,7 @@ import System.Environment | |||
| 21 | import Data.ByteString (ByteString) | 21 | import Data.ByteString (ByteString) |
| 22 | import qualified Data.ByteString as BS | 22 | import qualified Data.ByteString as BS |
| 23 | import qualified Data.ByteString.Char8 as CBS | 23 | import qualified Data.ByteString.Char8 as CBS |
| 24 | import qualified Data.ByteString.Lazy.Char8 as CLBS | ||
| 24 | 25 | ||
| 25 | import Data.Text (Text) | 26 | import Data.Text (Text) |
| 26 | import qualified Data.Text as T | 27 | import qualified Data.Text as T |
| @@ -102,6 +103,13 @@ config = do | |||
| 102 | hostEnv = "ADDR" | 103 | hostEnv = "ADDR" |
| 103 | portEnv = "PORT" | 104 | portEnv = "PORT" |
| 104 | 105 | ||
| 106 | fatal :: String -> UI a | ||
| 107 | fatal str = do | ||
| 108 | window <- askWindow | ||
| 109 | (getBody window #) . set children =<< sequence [UI.p # set TP.text str # set UI.class_ "fatal"] | ||
| 110 | liftIO (throwIO $ ErrorCall str) | ||
| 111 | return undefined | ||
| 112 | |||
| 105 | setup :: Config -> Window -> Event (Either WebSocketException URI) -> UI () | 113 | setup :: Config -> Window -> Event (Either WebSocketException URI) -> UI () |
| 106 | setup Config{..} window (split -> (socketErr, dataUpdate)) = void $ do | 114 | setup Config{..} window (split -> (socketErr, dataUpdate)) = void $ do |
| 107 | onEvent socketErr handleSocketErr | 115 | onEvent socketErr handleSocketErr |
| @@ -130,11 +138,6 @@ setup Config{..} window (split -> (socketErr, dataUpdate)) = void $ do | |||
| 130 | errors #+ [UI.li # set TP.text str] | 138 | errors #+ [UI.li # set TP.text str] |
| 131 | errorsTab # set style [("display", "inline-block")] | 139 | errorsTab # set style [("display", "inline-block")] |
| 132 | runFunction $ switchTab "errors" | 140 | runFunction $ switchTab "errors" |
| 133 | fatal :: String -> UI a | ||
| 134 | fatal str = do | ||
| 135 | (getBody window #) . set children =<< sequence [UI.p # set TP.text str # set UI.class_ "fatal"] | ||
| 136 | liftIO (throwIO $ ErrorCall str) | ||
| 137 | return undefined | ||
| 138 | 141 | ||
| 139 | maybeM = maybe $ return () | 142 | maybeM = maybe $ return () |
| 140 | 143 | ||
| @@ -147,7 +150,9 @@ setup Config{..} window (split -> (socketErr, dataUpdate)) = void $ do | |||
| 147 | status <- stepper init statusEvent | 150 | status <- stepper init statusEvent |
| 148 | return (status, triggerStatusChange) | 151 | return (status, triggerStatusChange) |
| 149 | 152 | ||
| 150 | Client{..} = mkClient (Nat $ either (fatal . ("Error while communicating to Thermoprint.Server: " ++) . show) return <=< liftIO . runEitherT) server | 153 | Client{..} = mkClient (hoistNat $ Nat liftIO) server |
| 154 | withFatal :: EitherT ServantError UI a -> UI a | ||
| 155 | withFatal a = either (fatal . ("Error while communicating to Thermoprint.Server: " ++) . show) return =<< runEitherT a | ||
| 151 | 156 | ||
| 152 | handleEditor selectedPrinter (_, modifyFocusedJobs) = do | 157 | handleEditor selectedPrinter (_, modifyFocusedJobs) = do |
| 153 | title <- fatal' "Could not find editor title field" =<< getElementById window "editorTitle" | 158 | title <- fatal' "Could not find editor title field" =<< getElementById window "editorTitle" |
| @@ -189,8 +194,8 @@ setup Config{..} window (split -> (socketErr, dataUpdate)) = void $ do | |||
| 189 | return False | 194 | return False |
| 190 | Right p -> do | 195 | Right p -> do |
| 191 | draftId <- case associatedDraft of | 196 | draftId <- case associatedDraft of |
| 192 | Nothing -> draftCreate (T.pack <$> eTitle) p | 197 | Nothing -> withFatal $ draftCreate (T.pack <$> eTitle) p |
| 193 | Just i -> i <$ when (different s) (draftReplace i (T.pack <$> eTitle) p) | 198 | Just i -> i <$ when (different s) (withFatal $ draftReplace i (T.pack <$> eTitle) p) |
| 194 | time <- liftIO getCurrentTime | 199 | time <- liftIO getCurrentTime |
| 195 | modifyStatus (\x -> x { associatedDraft = Just draftId, lastSaved = Just (time, s) }) | 200 | modifyStatus (\x -> x { associatedDraft = Just draftId, lastSaved = Just (time, s) }) |
| 196 | return True | 201 | return True |
| @@ -217,9 +222,9 @@ setup Config{..} window (split -> (socketErr, dataUpdate)) = void $ do | |||
| 217 | Right po -> case associatedDraft of | 222 | Right po -> case associatedDraft of |
| 218 | Just dId -> do | 223 | Just dId -> do |
| 219 | saved <- saveAction False | 224 | saved <- saveAction False |
| 220 | when saved $ reFocusJob =<< draftPrint dId =<< currentValue selectedPrinter | 225 | when saved $ reFocusJob =<< withFatal . draftPrint dId =<< currentValue selectedPrinter |
| 221 | Nothing -> do | 226 | Nothing -> do |
| 222 | reFocusJob =<< flip jobCreate po =<< currentValue selectedPrinter | 227 | reFocusJob =<< withFatal . flip jobCreate po =<< currentValue selectedPrinter |
| 223 | Left err -> emitError $ "Could not print draft due to error parsing bbcode: " ++ show err | 228 | Left err -> emitError $ "Could not print draft due to error parsing bbcode: " ++ show err |
| 224 | 229 | ||
| 225 | onEvent (whenE saveDraft $ tick autoSaveTimer) (const . void $ saveAction True) | 230 | onEvent (whenE saveDraft $ tick autoSaveTimer) (const . void $ saveAction True) |
| @@ -243,28 +248,52 @@ setup Config{..} window (split -> (socketErr, dataUpdate)) = void $ do | |||
| 243 | discardable EditorState{..} = not (maybe True null eTitle && null eText) | 248 | discardable EditorState{..} = not (maybe True null eTitle && null eText) |
| 244 | 249 | ||
| 245 | handleDraftTable changeEditorState = do | 250 | handleDraftTable changeEditorState = do |
| 246 | allowDeletion <- fatal' "Could not find deletion switch" =<< getElementById window "allowDeletion" | 251 | -- allowDeletion <- fatal' "Could not find deletion switch" =<< getElementById window "allowDeletion" |
| 247 | deletion' <- allowDeletion # get UI.checked | 252 | -- deletion' <- allowDeletion # get UI.checked |
| 253 | |||
| 254 | -- deletion <- stepper deletion' $ UI.checkedChange allowDeletion | ||
| 255 | (marking, (liftIO .) -> updateMarking) <- stepper' $ Set.empty | ||
| 248 | 256 | ||
| 249 | deletion <- stepper deletion' $ UI.checkedChange allowDeletion | 257 | |
| 258 | enactDeletion <- fatal' "Could not find deletion button" =<< getElementById window "enactDeletion" | ||
| 259 | on UI.click enactDeletion . const $ currentValue marking >>= mapM_ (runEitherT . draftDelete) >> updateMarking Set.empty | ||
| 260 | -- deletion' <- allowDeletion # get UI.checked | ||
| 250 | let | 261 | let |
| 262 | updateMarking' = callFunction (mangle <$> ffi getChecked) >>= updateMarking | ||
| 263 | where mangle = Set.fromList . map DraftId . fromMaybe [] . parse | ||
| 264 | getChecked = "$.makeArray($('input[name=draftMark]:checked').map(function() {return $(this).val()}))" | ||
| 265 | parse str | ||
| 266 | | [(i, rs)] <- [ (i, rs) | (i, ',' : rs) <- reads str ] = (:) <$> Just i <*> parse rs | ||
| 267 | | r@([_]) <- [ i | (i, "") <- reads str ] = Just r | ||
| 268 | | otherwise = Nothing | ||
| 269 | |||
| 251 | toTable :: Map DraftId (Maybe DraftTitle) -> UI [Element] | 270 | toTable :: Map DraftId (Maybe DraftTitle) -> UI [Element] |
| 252 | toTable = mapM toLine . Map.toList | 271 | toTable = mapM toLine . Map.toList |
| 253 | 272 | ||
| 254 | toLine (id@(DraftId (show -> tId)), fromMaybe "" . fmap T.unpack -> title) = do | 273 | toLine (id@(DraftId (show -> tId)), fromMaybe "" . fmap T.unpack -> title) = do |
| 255 | id' <- UI.td # set TP.text tId | 274 | id' <- UI.td # set TP.text tId |
| 256 | title' <- UI.td # set TP.text title | 275 | title' <- UI.td # set TP.text title |
| 257 | delete <- UI.button | 276 | mark <- UI.input |
| 258 | # set TP.text "Delete" | 277 | # set UI.type_ "checkbox" |
| 259 | # sink UI.enabled deletion | 278 | # set UI.name "draftMark" |
| 260 | on UI.click delete . const $ draftDelete id >> changeEditorState (\s@(EditorState{..}) -> if associatedDraft == Just id then def else s) | 279 | # set UI.id_ ("draftMark" ++ tId) |
| 280 | # set UI.value tId | ||
| 281 | # sink UI.checked (Set.member id <$> marking) | ||
| 282 | on UI.checkedChange mark . const $ updateMarking' | ||
| 283 | mark' <- UI.span #+ [ return mark | ||
| 284 | , UI.label # set UI.for ("draftMark" ++ tId) # set UI.text "Mark" | ||
| 285 | ] # set UI.class_ "mark" | ||
| 286 | -- delete <- UI.button | ||
| 287 | -- # set TP.text "Delete" | ||
| 288 | -- # sink UI.enabled deletion | ||
| 289 | -- on UI.click delete . const $ draftDelete id >> changeEditorState (\s@(EditorState{..}) -> if associatedDraft == Just id then def else s) | ||
| 261 | load <- UI.button | 290 | load <- UI.button |
| 262 | # set TP.text "Load" | 291 | # set TP.text "Load" |
| 263 | on UI.click load . const $ loadDraft id | 292 | on UI.click load . const $ loadDraft id |
| 264 | actions <- UI.td # set children [load, delete] | 293 | actions <- UI.td # set children [mark', load] |
| 265 | UI.tr # set children [id', title', actions] | 294 | UI.tr # set children [id', title', actions] |
| 266 | loadDraft id = do | 295 | loadDraft id = do |
| 267 | (title, po) <- draft id | 296 | (title, po) <- withFatal $ draft id |
| 268 | let | 297 | let |
| 269 | t = cobbcode po | 298 | t = cobbcode po |
| 270 | case t of | 299 | case t of |
| @@ -281,7 +310,7 @@ setup Config{..} window (split -> (socketErr, dataUpdate)) = void $ do | |||
| 281 | changeEditorState (const newState) | 310 | changeEditorState (const newState) |
| 282 | runFunction $ switchTab "editor" | 311 | runFunction $ switchTab "editor" |
| 283 | table <- fatal' "Could not find draft table" =<< getElementById window "draftListBody" | 312 | table <- fatal' "Could not find draft table" =<< getElementById window "draftListBody" |
| 284 | initialContent <- toTable =<< drafts | 313 | initialContent <- toTable =<< withFatal drafts |
| 285 | return table # set children initialContent | 314 | return table # set children initialContent |
| 286 | 315 | ||
| 287 | update <- do | 316 | update <- do |
| @@ -292,7 +321,7 @@ setup Config{..} window (split -> (socketErr, dataUpdate)) = void $ do | |||
| 292 | -- return $ unionWith const (() <$ filterE concernsDrafts dataUpdate) (tick recheckTimer) | 321 | -- return $ unionWith const (() <$ filterE concernsDrafts dataUpdate) (tick recheckTimer) |
| 293 | return $ filterE concernsDrafts dataUpdate | 322 | return $ filterE concernsDrafts dataUpdate |
| 294 | 323 | ||
| 295 | onEvent update . const $ drafts >>= toTable >>= (\c -> return table # set children c) | 324 | onEvent update . const $ withFatal drafts >>= toTable >>= (\c -> return table # set children c) |
| 296 | 325 | ||
| 297 | concernsDrafts :: URI -> Bool | 326 | concernsDrafts :: URI -> Bool |
| 298 | concernsDrafts (uriPath -> p) | 327 | concernsDrafts (uriPath -> p) |
| @@ -307,10 +336,14 @@ setup Config{..} window (split -> (socketErr, dataUpdate)) = void $ do | |||
| 307 | | otherwise = False | 336 | | otherwise = False |
| 308 | 337 | ||
| 309 | handleJobTable (focusedJobs, _) = do | 338 | handleJobTable (focusedJobs, _) = do |
| 310 | allowAbortion <- do | 339 | -- allowAbortion <- do |
| 311 | allowAbortion <- fatal' "Could not find abortion switch" =<< getElementById window "allowAbortion" | 340 | -- allowAbortion <- fatal' "Could not find abortion switch" =<< getElementById window "allowAbortion" |
| 312 | flip stepper (UI.checkedChange allowAbortion) =<< (allowAbortion # get UI.checked) | 341 | -- flip stepper (UI.checkedChange allowAbortion) =<< (allowAbortion # get UI.checked) |
| 313 | 342 | (marking, (liftIO .) -> updateMarking) <- stepper' $ Set.empty | |
| 343 | |||
| 344 | enactAbortion <- fatal' "Could not find deletion button" =<< getElementById window "enactAbortion" | ||
| 345 | on UI.click enactAbortion . const $ currentValue marking >>= mapM_ (runEitherT . jobDelete) >> updateMarking Set.empty | ||
| 346 | |||
| 314 | (selectedPrinter, updatePrinter) <- do | 347 | (selectedPrinter, updatePrinter) <- do |
| 315 | autoselectPrinter <- fatal' "Could not find printer autoselect switch" =<< getElementById window "autoselectPrinter" | 348 | autoselectPrinter <- fatal' "Could not find printer autoselect switch" =<< getElementById window "autoselectPrinter" |
| 316 | (selectedPrinter, printerSelect) <- stepper' Nothing | 349 | (selectedPrinter, printerSelect) <- stepper' Nothing |
| @@ -323,14 +356,22 @@ setup Config{..} window (split -> (socketErr, dataUpdate)) = void $ do | |||
| 323 | return (selectedPrinter, updatePrinterSelect) | 356 | return (selectedPrinter, updatePrinterSelect) |
| 324 | 357 | ||
| 325 | let | 358 | let |
| 359 | updateMarking' = callFunction (mangle <$> ffi getChecked) >>= updateMarking | ||
| 360 | where mangle = Set.fromList . map JobId . fromMaybe [] . parse | ||
| 361 | getChecked = "$.makeArray($('input[name=jobMark]:checked').map(function() {return $(this).val()}))" | ||
| 362 | parse str | ||
| 363 | | [(i, rs)] <- [ (i, rs) | (i, ',' : rs) <- reads str ] = (:) <$> Just i <*> parse rs | ||
| 364 | | r@([_]) <- [ i | (i, "") <- reads str ] = Just r | ||
| 365 | | otherwise = Nothing | ||
| 366 | |||
| 326 | -- getServerState :: UI [(PrinterId, PrinterStatus, [(JobId, UTCTime, JobStatus)])] | 367 | -- getServerState :: UI [(PrinterId, PrinterStatus, [(JobId, UTCTime, JobStatus)])] |
| 327 | getServerState = map mangleTuple . Map.toList <$> (Map.traverseWithKey (\pId status -> (,) status <$> getJobState pId) =<< printers) | 368 | getServerState = map mangleTuple . Map.toList <$> (Map.traverseWithKey (\pId status -> (,) status <$> getJobState pId) =<< withFatal printers) |
| 328 | -- getJobState :: PrinterId -> UI [(JobId, UTCTime, JobStatus)] | 369 | -- getJobState :: PrinterId -> UI [(JobId, UTCTime, JobStatus)] |
| 329 | getJobState pId = toList <$> jobs (Just pId) Nothing Nothing | 370 | getJobState pId = toList <$> withFatal (jobs (Just pId) Nothing Nothing) |
| 330 | mangleTuple (a, (b, c)) = (a, b, c) | 371 | mangleTuple (a, (b, c)) = (a, b, c) |
| 331 | 372 | ||
| 332 | -- jobSort :: (JobId, UTCTime, JobStatus) -> (JobId, UTCTime, JobStatus) -> Ordering | 373 | -- jobSort :: (JobId, UTCTime, JobStatus) -> (JobId, UTCTime, JobStatus) -> Ordering |
| 333 | jobSort (id, time, status) (id', time', status') = queueSort status status' <> compare time' time <> compare id' id | 374 | jobSort (id, time, status) (id', time', status') = queueSort status status' <> compare time' time <> compare id id' |
| 334 | where | 375 | where |
| 335 | compare' :: Ord a => a -> a -> Ordering | 376 | compare' :: Ord a => a -> a -> Ordering |
| 336 | compare' | 377 | compare' |
| @@ -356,15 +397,25 @@ setup Config{..} window (split -> (socketErr, dataUpdate)) = void $ do | |||
| 356 | jId' <- UI.td # set UI.text (show jId) | 397 | jId' <- UI.td # set UI.text (show jId) |
| 357 | jStatus' <- UI.td # set UI.text (show status) | 398 | jStatus' <- UI.td # set UI.text (show status) |
| 358 | time' <- UI.td # set UI.text (formatTime defaultTimeLocale "%F %X" time) | 399 | time' <- UI.td # set UI.text (formatTime defaultTimeLocale "%F %X" time) |
| 359 | abortButton <- UI.button # sink UI.enabled allowAbortion # set UI.text "Abort" | 400 | mark <- UI.input |
| 360 | on UI.click abortButton . const $ jobDelete rJId | 401 | # set UI.type_ "checkbox" |
| 361 | let abortButton' = case status of | 402 | # set UI.name "jobMark" |
| 362 | Queued _ -> [abortButton] | 403 | # set UI.id_ ("jobMark" ++ show jId) |
| 404 | # set UI.value (show jId) | ||
| 405 | # sink UI.checked (Set.member rJId <$> marking) | ||
| 406 | on UI.checkedChange mark . const $ updateMarking' | ||
| 407 | mark' <- UI.span #+ [ return mark | ||
| 408 | , UI.label # set UI.for ("jobMark" ++ show jId) # set UI.text "Mark" | ||
| 409 | ] # set UI.class_ "mark" | ||
| 410 | -- abortButton <- UI.button # sink UI.enabled allowAbortion # set UI.text "Abort" | ||
| 411 | -- on UI.click abortButton . const $ jobDelete rJId | ||
| 412 | let mark'' = case status of | ||
| 413 | (Queued _) -> [mark'] | ||
| 363 | _ -> [] | 414 | _ -> [] |
| 364 | viewJob = do | 415 | viewJob = do |
| 365 | tabLinkList <- fatal' "Could not find tab link list" =<< getElementById window "tabLinks" | 416 | tabLinkList <- fatal' "Could not find tab link list" =<< getElementById window "tabLinks" |
| 366 | tabContainer <- fatal' "Could not find tab container" =<< getElementById window "tabContent" | 417 | tabContainer <- fatal' "Could not find tab container" =<< getElementById window "tabContent" |
| 367 | content <- job rJId | 418 | content <- withFatal $ job rJId |
| 368 | let | 419 | let |
| 369 | text = cobbcode content | 420 | text = cobbcode content |
| 370 | case text of | 421 | case text of |
| @@ -381,9 +432,9 @@ setup Config{..} window (split -> (socketErr, dataUpdate)) = void $ do | |||
| 381 | runFunction . switchTab $ "viewJob" ++ show jId | 432 | runFunction . switchTab $ "viewJob" ++ show jId |
| 382 | viewButton <- UI.button # set UI.text "View" | 433 | viewButton <- UI.button # set UI.text "View" |
| 383 | on UI.click viewButton . const $ viewJob | 434 | on UI.click viewButton . const $ viewJob |
| 384 | actions <- UI.td # set children (viewButton : abortButton') | 435 | actions <- UI.td # set children (mark'' ++ [viewButton]) |
| 385 | UI.tr # set UI.id_ ("job" ++ show jId) # set children [jPId, jId', time', jStatus', actions] # sink UI.class_ (bool "" "focused" . Set.member rJId <$> focusedJobs) | 436 | UI.tr # set UI.id_ ("job" ++ show jId) # set children [jPId, jId', time', jStatus', actions] # sink UI.class_ (bool "" "focused" . Set.member rJId <$> focusedJobs) |
| 386 | (:) <$> UI.tr # set children [pId', pFiller, pStatus', pSelect'] <*> mapM toLine jobs | 437 | (:) <$> UI.tr # set children [pId', pFiller, pStatus', pSelect'] # set UI.class_ "printer" <*> mapM toLine jobs |
| 387 | 438 | ||
| 388 | update <- do | 439 | update <- do |
| 389 | -- recheckTimer <- timer | 440 | -- recheckTimer <- timer |
