aboutsummaryrefslogtreecommitdiff
path: root/webgui
diff options
context:
space:
mode:
authorGregor Kleen <gkleen@yggdrasil.li>2016-02-24 04:18:03 +0000
committerGregor Kleen <gkleen@yggdrasil.li>2016-02-24 04:18:03 +0000
commitc09c2786d0654f144dab103292c47411ff1afa9a (patch)
tree9a49cf5c1a0502ef7382bb54312097c375666ced /webgui
parentc1451ffcbc021e4f25721541538173f24be8f3cc (diff)
downloadthermoprint-c09c2786d0654f144dab103292c47411ff1afa9a.tar
thermoprint-c09c2786d0654f144dab103292c47411ff1afa9a.tar.gz
thermoprint-c09c2786d0654f144dab103292c47411ff1afa9a.tar.bz2
thermoprint-c09c2786d0654f144dab103292c47411ff1afa9a.tar.xz
thermoprint-c09c2786d0654f144dab103292c47411ff1afa9a.zip
Partial GUI prototype
Diffstat (limited to 'webgui')
-rw-r--r--webgui/data/index.html77
-rw-r--r--webgui/data/style.css159
-rw-r--r--webgui/data/tabs.js13
-rw-r--r--webgui/src/Main.hs237
-rw-r--r--webgui/thermoprint-webgui.cabal9
-rw-r--r--webgui/thermoprint-webgui.nix13
6 files changed, 468 insertions, 40 deletions
diff --git a/webgui/data/index.html b/webgui/data/index.html
new file mode 100644
index 0000000..af780bd
--- /dev/null
+++ b/webgui/data/index.html
@@ -0,0 +1,77 @@
1<!doctype html>
2<html>
3 <head>
4 <meta charset="UTF-8" />
5 <title>Thermoprint</title>
6 <link rel="stylesheet" type="text/css" href="static/style.css" />
7 <script src="haskell.js"></script>
8 <script src="static/tabs.js"></script>
9 <script type="text/javascript" charset="utf-8">
10 Haskell.initFFI();
11 </script>
12 </head>
13 <body>
14 <p id="javascriptError" class="error fatal">
15 Need javascript to function.
16 </p>
17 <div class="tabs">
18 <ul class="tab-links">
19 <li class="active"><a href="#editor">Editor</a></li>
20 <li><a href="#queue">Queue</a></li>
21 <li id="errors-tab" style="display:none;"><a href="#errors">Errors</a></li>
22 </ul>
23
24 <div class="tab-content">
25 <div class="tab active" id="editor">
26 <div class="table" style="width:100%">
27 <div class="tr">
28 <h1 class="tc">Current Draft</h1>
29 <h1 class="tc">Saved Drafts</h1>
30 </div>
31 <div class="tr">
32 <div id="currentDraft" class="tc">
33 <!-- <label id="titleLabel" for="editorTitle">Title</label> -->
34 <input id="editorTitle" />
35 <textarea id="editorText"></textarea>
36 <span id="bbcodeStatus"></span>
37 <span id="editorStatus">Not connected to server</span>
38 <div class="table">
39 <div class="tr">
40 <div class="editorButtonContainer tc">
41 <button class="editorButton" id="saveButton">Save</button>
42 </div>
43 <div class="editorButtonContainer tc">
44 <button class="editorButton" id="printButton">Print</button>
45 </div>
46 <div class="editorButtonContainer tc">
47 <button class="editorButton" id="discardButton">Discard</button>
48 </div>
49 </div>
50 </div>
51 </div>
52 <div class="tc" style="text-align:center;">
53 <input id="allowDeletion" type="checkbox" /><label for="allowDeletion">Allow Deletion</label>
54 <table id="draftList">
55 <thead>
56 <tr>
57 <td>Id</td>
58 <td>Title</td>
59 <td>Actions</td>
60 </tr>
61 </thead>
62 <tbody id="draftListBody"></tbody>
63 </table>
64 </div>
65 </div>
66 </div>
67 </div>
68
69 <div class="tab" id="queue">
70 Blub.
71 </div>
72
73 <ul class="tab" id="errors">
74 </ul>
75 </div>
76 </body>
77</html>
diff --git a/webgui/data/style.css b/webgui/data/style.css
new file mode 100644
index 0000000..53ddb7a
--- /dev/null
+++ b/webgui/data/style.css
@@ -0,0 +1,159 @@
1.table { display: table; }
2.tr { display: table-row; }
3.tc { display: table-cell; }
4
5.table .table, table .table, .table table, table table { width:100%; height:100%; }
6
7.fatal {
8 z-index:-1;
9 position:fixed;
10 top:50%;
11 left:50%;
12 transform: translate(-50%, -50%);
13 background-color:#fdd;
14 border:2px solid #c00;
15 text-align:center;
16 vertical-align:center;
17 padding:1em;
18 font-weight:600;
19}
20
21h1 {
22 font-size:1.5em;
23 font-weight:normal;
24 text-align:center;
25}
26
27thead td {
28 font-size:1em;
29 font-weight:600;
30 text-align:center;
31}
32
33thead tr:last-child {
34 padding-bottom:0.125em;
35 border-bottom:1px solid #ddd;
36 margin-bottom:0.125em;
37}
38
39.editorButtonContainer {
40 text-align:center;
41}
42
43.editorButton {
44 display:inline-block;
45 width:8em;
46}
47
48#editorTitle {
49 width:100%;
50 box-sizing:border-box;
51 margin:0 0 0.25em 0;
52}
53
54#editorText {
55 width:100%;
56 box-sizing:border-box;
57 margin:0 0 0.25em 0;
58 resize:vertical;
59 min-height:25em;
60}
61
62#editorStatus {
63 display:block;
64 margin:0 0 0.25em 0;
65 font-size:0.75em;
66}
67
68#editorStatus:after {
69 display:block;
70 clear:both;
71 content:'';
72}
73
74#bbcodeStatus {
75 display:block;
76 margin:0 0 0.25em 0;
77 font-size:0.75em;
78 float:right;
79 color:#c00000;
80}
81
82/*----- Tabs -----*/
83.tabs {
84 display:block;
85 width:98%;
86 min-height:98%;
87 position:absolute;
88 left:1%;
89 top:1%;
90}
91
92/*----- Tab Links -----*/
93.tab-links {
94 display:block;
95 height:2.2em;
96 margin:0;
97 padding:0 0.5em;
98 z-index:0;
99}
100
101/* Clearfix */
102.tab-links:after {
103 display:block;
104 clear:both;
105 content:'';
106}
107
108.tab-links li {
109 margin:0px 0.5em;
110 float:left;
111 list-style:none;
112}
113
114.tab-links a {
115 padding:0.5em 1em;
116 display:inline-block;
117 border-radius:3px 3px 0px 0px;
118 background:#fff;
119 font-size:1em;
120 font-weight:600;
121 color:#4c4c4c;
122 transition:all linear 0.15s;
123}
124
125.tab-links a:hover {
126 text-decoration:none;
127}
128
129li.active a, li.active a:hover {
130 background:#f0f0f0;
131 color:#4c4c4c;
132 text-decoration:none;
133 }
134
135#errors-tab a {
136 color:#c00000;
137}
138
139/*----- Content of Tabs -----*/
140.tab-content {
141 padding:0.5em;
142 border:1px solid #ddd;
143 border-radius:3px;
144 box-shadow:-1px 1px 1px rgba(0,0,0,0.15);
145 background:#fff;
146 box-sizing:border-box;
147 width:100%;
148 z-index:1;
149 margin-top:-1px;
150 margin-bottom:1%;
151}
152
153.tab {
154 display:none;
155}
156
157.tab.active {
158 display:block;
159} \ No newline at end of file
diff --git a/webgui/data/tabs.js b/webgui/data/tabs.js
new file mode 100644
index 0000000..1d178d9
--- /dev/null
+++ b/webgui/data/tabs.js
@@ -0,0 +1,13 @@
1jQuery(document).ready(function() {
2 jQuery('.tabs .tab-links a').on('click', function(e) {
3 var currentAttrValue = jQuery(this).attr('href');
4
5 // Show/Hide Tabs
6 jQuery('.tabs ' + currentAttrValue).show().siblings().hide();
7
8 // Change/remove current tab to active
9 jQuery(this).parent('li').addClass('active').siblings().removeClass('active');
10
11 e.preventDefault();
12 });
13});
diff --git a/webgui/src/Main.hs b/webgui/src/Main.hs
index 92aba0e..44bcb88 100644
--- a/webgui/src/Main.hs
+++ b/webgui/src/Main.hs
@@ -4,10 +4,13 @@
4module Main (main) where 4module Main (main) where
5 5
6import qualified Graphics.UI.Threepenny as UI 6import qualified Graphics.UI.Threepenny as UI
7import qualified Graphics.UI.Threepenny.Core as TP (Config) 7import Graphics.UI.Threepenny.Timer
8import Graphics.UI.Threepenny.Core hiding (Config) 8import qualified Graphics.UI.Threepenny.Core as TP (Config, text)
9import Graphics.UI.Threepenny.Core hiding (Config, text)
9 10
10import Thermoprint.Client 11import Thermoprint.Client
12import Thermoprint.Printout.BBCode
13
11import Network.WebSockets 14import Network.WebSockets
12import Network.Socket (withSocketsDo) 15import Network.Socket (withSocketsDo)
13import Network.URI 16import Network.URI
@@ -22,6 +25,15 @@ import qualified Data.ByteString.Char8 as CBS
22import Data.Text (Text) 25import Data.Text (Text)
23import qualified Data.Text as T 26import qualified Data.Text as T
24 27
28import Data.Map (Map)
29import qualified Data.Map as Map
30import Data.Sequence (Seq)
31import qualified Data.Sequence as Seq
32
33import Data.Default.Class
34
35import Data.Time
36
25import Control.Concurrent 37import Control.Concurrent
26import Control.Exception 38import Control.Exception
27import Control.Monad.Catch 39import Control.Monad.Catch
@@ -30,7 +42,13 @@ import Control.Applicative
30import Control.Monad 42import Control.Monad
31import Data.Maybe 43import Data.Maybe
32import Data.Monoid 44import Data.Monoid
33import Text.Read 45import Text.Read hiding (get)
46import Data.Either
47import Data.List (isPrefixOf)
48
49import Paths_thermoprint_webgui
50
51import Debug.Trace
34 52
35data Config = Config 53data Config = Config
36 { tpConfig :: TP.Config 54 { tpConfig :: TP.Config
@@ -49,7 +67,13 @@ config :: IO (Opt.Parser Config)
49config = do 67config = do
50 p <- fromMaybe 8023 . (readMaybe =<<) <$> lookupEnv portEnv 68 p <- fromMaybe 8023 . (readMaybe =<<) <$> lookupEnv portEnv
51 a <- fromMaybe "localhost" <$> lookupEnv hostEnv 69 a <- fromMaybe "localhost" <$> lookupEnv hostEnv
52 return $ Config <$> ((\port addr -> defaultConfig { jsPort = port, jsAddr = addr }) <$> optional (Opt.option Opt.auto $ port p) <*> optional (fmap CBS.pack $ Opt.strOption $ addr a)) 70 static <- getDataDir
71 let
72 config = defaultConfig
73 { jsCustomHTML = Just "index.html"
74 , jsStatic = Just static
75 }
76 return $ Config <$> ((\port addr -> config { jsPort = port, jsAddr = addr }) <$> optional (Opt.option Opt.auto $ port p) <*> optional (fmap CBS.pack $ Opt.strOption $ addr a))
53 <*> (BaseUrl Http 77 <*> (BaseUrl Http
54 <$> Opt.strOption (Opt.long "target-addr" <> Opt.short 'A' <> Opt.metavar "HOST" <> Opt.help "Host to connect to" <> Opt.value "localhost" <> Opt.showDefault) 78 <$> Opt.strOption (Opt.long "target-addr" <> Opt.short 'A' <> Opt.metavar "HOST" <> Opt.help "Host to connect to" <> Opt.value "localhost" <> Opt.showDefault)
55 <*> Opt.option Opt.auto (Opt.long "target-port" <> Opt.short 'P' <> Opt.metavar "PORT" <> Opt.help "Port to connect to" <> Opt.value 3000 <> Opt.showDefault) 79 <*> Opt.option Opt.auto (Opt.long "target-port" <> Opt.short 'P' <> Opt.metavar "PORT" <> Opt.help "Port to connect to" <> Opt.value 3000 <> Opt.showDefault)
@@ -74,37 +98,182 @@ setup :: Config -> Window -> Event (Either WebSocketException URI) -> UI ()
74setup Config{..} window (split -> (socketErr, dataUpdate)) = void $ do 98setup Config{..} window (split -> (socketErr, dataUpdate)) = void $ do
75 onEvent socketErr handleSocketErr 99 onEvent socketErr handleSocketErr
76 100
77 return window # set UI.title "Thermoprint" 101 getElementById window "javascriptError" >>= maybeM delete
78 102
79 title <- UI.input 103 changeEditorStatus <- handleEditor
80 # set UI.id_ "title" 104
81 content <- UI.textarea 105 handleDraftTable changeEditorStatus
82 # set UI.id_ "content"
83 draftTable <- UI.table
84
85 saveBtn <- UI.button #+ [string "Save"]
86 discBtn <- UI.button #+ [string "Discard"]
87 prntBtn <- UI.button #+ [string "Print"]
88
89 getBody window #+ [ row [ column [ UI.h1 #+ [string "Current draft"]
90 , row [UI.label # set text "Title" # set UI.for "title", element title]
91 , element content
92 , row [ element saveBtn
93 , element prntBtn
94 , element discBtn
95 ]
96 ]
97 , column [ UI.h1 #+ [string "Saved drafts"]
98 , element draftTable
99 ]
100 ]
101 ]
102 106
103 where 107 where
104 handleSocketErr InvalidMessage = debug "Received unparseable message from websocket" 108 handleSocketErr InvalidMessage = emitError "Received unparseable message from server-side websocket"
105 handleSocketErr e@(Unhandled e') = void $ fatalError (show e') >> liftIO (throwIO e) 109 handleSocketErr ProcessDied = fatal "Thread handling server-side websocket died"
106 fatalError str = (getBody window #) . set children =<< sequence [UI.p # set text str # set UI.id_ "fatal-error"] 110 handleSocketErr (Unhandled e') = fatal $ "Unhandled error from server-side websocket: " ++ show e'
111 emitError str = void $ do
112 debug str
113 let
114 errors = maybe (fatal "No container for nonfatal errors found") return =<< getElementById window "errors"
115 errorsTab = maybe (fatal "Could not make nonfatal errors visible") return =<< getElementById window "errors-tab"
116 errors #+ [UI.li # set TP.text str]
117 errorsTab # set style [("display", "inline-block")]
118 fatal :: String -> UI a
119 fatal str = do
120 (getBody window #) . set children =<< sequence [UI.p # set TP.text str # set UI.class_ "fatal"]
121 liftIO (throwIO $ ErrorCall str)
122 return undefined
123
124 maybeM = maybe $ return ()
125
126 fatal' :: String -> Maybe a -> UI a
127 fatal' str = maybe (fatal str) return
128
129 stepper' :: a -> UI (Behavior a, a -> IO ())
130 stepper' init = do
131 (statusEvent, triggerStatusChange) <- liftIO newEvent
132 status <- stepper init statusEvent
133 return (status, triggerStatusChange)
134
135 Client{..} = mkClient (Nat $ either (fatal . ("Error while communicating to Thermoprint.Server: " ++) . show) return <=< liftIO . runEitherT) server
136
137 handleEditor = do
138 title <- fatal' "Could not find editor title field" =<< getElementById window "editorTitle"
139 text <- fatal' "Could not find editor text field" =<< getElementById window "editorText"
140 status <- fatal' "Could not find editor status field" =<< getElementById window "editorStatus"
141 bbcodeStatus <- fatal' "Could not find editor bbcode status field" =<< getElementById window "bbcodeStatus"
142
143 saveButton <- fatal' "Could not find editor save button" =<< getElementById window "saveButton"
144 printButton <- fatal' "Could not find editor print button" =<< getElementById window "printButton"
145 discardButton <- fatal' "Could not find editor discard button" =<< getElementById window "discardButton"
146
147 (editorStatus, fmap liftIO -> changeEditorStatus) <- stepper' def
148
149 let
150 modifyStatus f = changeEditorStatus . f =<< currentValue editorStatus
151
152 on UI.valueChange title $ \str -> modifyStatus (\x -> x { eTitle = if null str then Nothing else Just str })
153 on UI.valueChange text $ \str -> modifyStatus (\x -> x { eText = str })
154 on UI.valueChange text $ \str -> modifyStatus (\x -> x { ePrintout = bbcode $ T.pack str })
107 155
156 return title # sink UI.value (fromMaybe "" . eTitle <$> editorStatus)
157 return text # sink UI.text (eText <$> editorStatus)
158 return status # sink TP.text (toStatusString <$> editorStatus)
159 return bbcodeStatus # sink TP.text (toStatusString' . ePrintout <$> editorStatus)
160
161 autoSaveTimer <- timer # set interval 5000
162 start autoSaveTimer
163
164 let
165 saveAction automatic = do
166 s@(EditorState{..}) <- currentValue editorStatus
167 when (not $ maybe True null eTitle && null eText) $ case ePrintout of
168 Left err -> when (not automatic) . emitError $ "Could not save draft due to error parsing bbcode: " ++ show err
169 Right p -> do
170 draftId <- case associatedDraft of
171 Nothing -> draftCreate (T.pack <$> eTitle) p
172 Just i -> i <$ when (different s) (draftReplace i (T.pack <$> eTitle) p)
173 time <- liftIO getCurrentTime
174 modifyStatus (\x -> x { associatedDraft = Just draftId, lastSaved = Just (time, s) })
175 where
176 different s
177 | Just (_, s') <- lastSaved s = not $ and [ eTitle s == eTitle s'
178 , eText s == eText s'
179 ]
180 | otherwise = True
181 discardAction = do
182 maybe (return ()) draftDelete . associatedDraft =<< currentValue editorStatus
183 modifyStatus $ const def
184 printAction = do
185 emitError "Printing not implemented"
186
187 onEvent (tick autoSaveTimer) (const $ saveAction True)
188
189 return saveButton # sink UI.enabled (saveable <$> editorStatus)
190 return printButton # sink UI.enabled (printable <$> editorStatus)
191 return discardButton # sink UI.enabled (discardable <$> editorStatus)
192
193 on UI.click saveButton . const $ saveAction False
194 on UI.click printButton $ const printAction
195 on UI.click discardButton $ const discardAction
196
197 return changeEditorStatus
198
199 saveable s@EditorState{..} = isRight ePrintout && discardable s
200 printable EditorState{..} = isRight ePrintout && not (null eText)
201 discardable EditorState{..} = not (maybe True null eTitle && null eText)
202
203 handleDraftTable changeEditorState = do
204 allowDeletion <- fatal' "Could not find deletion switch" =<< getElementById window "allowDeletion"
205 deletion' <- allowDeletion # get UI.checked
206
207 deletion <- stepper deletion' $ UI.checkedChange allowDeletion
208 let
209 toTable :: Map DraftId (Maybe DraftTitle) -> UI [Element]
210 toTable = mapM toLine . Map.toList
211
212 toLine (id@(show -> tId), fromMaybe "" . fmap T.unpack -> title) = do
213 id' <- UI.td # set TP.text tId
214 title' <- UI.td # set TP.text title
215 delete <- UI.button
216 # set TP.text "Delete"
217 # sink UI.enabled deletion
218 on UI.click delete . const $ draftDelete id
219 load <- UI.button
220 # set TP.text "Load"
221 on UI.click load . const $ loadDraft id
222 actions <- UI.td # set children [load, delete]
223 UI.tr # set children [id', title', actions]
224 loadDraft id = do
225 (title, po) <- draft id
226 time <- liftIO $ getCurrentTime
227 let newState = def
228 { eTitle = fmap T.unpack title
229 , eText = "unimplemented" -- TODO: implement -- drops data as it is
230 , ePrintout = Right po
231 , associatedDraft = Just id
232 , lastSaved = Just (time, newState)
233 }
234 changeEditorState newState
235 table <- fatal' "Could not find draft table" =<< getElementById window "draftListBody"
236 initialContent <- toTable =<< drafts
237 return table # set children initialContent
238
239 onEvent (filterE concernsDrafts dataUpdate) . const $ drafts >>= toTable >>= (\c -> return table # set children c)
240 where
241 concernsDrafts :: URI -> Bool
242 concernsDrafts (uriPath -> p)
243 | p == "drafts" = True
244 | "draft/" `isPrefixOf` p = True
245 | otherwise = False
246
247
248data EditorState = EditorState
249 { eTitle :: Maybe String
250 , eText :: String
251 , ePrintout :: Either BBCodeError Printout
252 , lastSaved :: Maybe (UTCTime, EditorState)
253 , associatedDraft :: Maybe DraftId
254 }
255 deriving (Show)
256
257instance Default EditorState where
258 def = EditorState
259 { eTitle = Nothing
260 , eText = ""
261 , ePrintout = Right mempty
262 , lastSaved = Nothing
263 , associatedDraft = Nothing
264 }
265
266toStatusString :: EditorState -> String
267toStatusString EditorState{..}
268 | null eTitle
269 , null eText = "Draft is empty"
270 | Just (t, _) <- lastSaved = "Last saved: " ++ formatTime defaultTimeLocale "%F %X" t
271 | otherwise = "Draft was never saved"
272
273toStatusString' :: Either BBCodeError a -> String
274toStatusString' (Right _) = ""
275toStatusString' (Left e) = show e
276
108data WebSocketException = ProcessDied | InvalidMessage | Unhandled SomeException 277data WebSocketException = ProcessDied | InvalidMessage | Unhandled SomeException
109 deriving (Show) 278 deriving (Show)
110 279
@@ -114,7 +283,7 @@ withWebSocket :: (Config -> Window -> Event (Either WebSocketException URI) -> U
114withWebSocket setup c@(Config{..}) w = do 283withWebSocket setup c@(Config{..}) w = do
115 (dataUpdate, triggerData) <- liftIO newEvent 284 (dataUpdate, triggerData) <- liftIO newEvent
116 let 285 let
117 rcvEvents = runClient (baseUrlHost server) (baseUrlPort server) "/" $ forever . (triggerData . maybe (Left InvalidMessage) Right . parseURI . T.unpack <=< receiveData) 286 rcvEvents = runClient (baseUrlHost server) (baseUrlPort server) "/" $ forever . (triggerData . maybe (Left InvalidMessage) Right . parseURIReference . T.unpack <=< receiveData)
118 liftIOLater . void $ forkFinally (rcvEvents `catchAll` (triggerData . Left . Unhandled)) (triggerData $ Left ProcessDied) 287 liftIOLater . void $ forkFinally rcvEvents (triggerData . Left . either Unhandled (const ProcessDied))
119 void $ setup c w dataUpdate 288 void $ setup c w dataUpdate
120 289
diff --git a/webgui/thermoprint-webgui.cabal b/webgui/thermoprint-webgui.cabal
index c1f4d19..b76175d 100644
--- a/webgui/thermoprint-webgui.cabal
+++ b/webgui/thermoprint-webgui.cabal
@@ -16,6 +16,11 @@ build-type: Simple
16-- extra-source-files: 16-- extra-source-files:
17cabal-version: >=1.10 17cabal-version: >=1.10
18 18
19data-dir: data
20data-files: index.html
21 , style.css
22 , tabs.js
23
19executable thermoprint-webgui 24executable thermoprint-webgui
20 main-is: Main.hs 25 main-is: Main.hs
21 -- other-modules: 26 -- other-modules:
@@ -31,5 +36,9 @@ executable thermoprint-webgui
31 , network-uri >=2.6.0 && <3 36 , network-uri >=2.6.0 && <3
32 , text >=1.2.2 && <2 37 , text >=1.2.2 && <2
33 , exceptions >=0.8.2 && <1 38 , exceptions >=0.8.2 && <1
39 , containers >=0.5.6 && <1
40 , either >=4.4.1 && <5
41 , time >=1.5.0 && <2
42 , data-default-class >=0.0 && <1
34 hs-source-dirs: src 43 hs-source-dirs: src
35 default-language: Haskell2010 \ No newline at end of file 44 default-language: Haskell2010 \ No newline at end of file
diff --git a/webgui/thermoprint-webgui.nix b/webgui/thermoprint-webgui.nix
index 5eb5912..457f39a 100644
--- a/webgui/thermoprint-webgui.nix
+++ b/webgui/thermoprint-webgui.nix
@@ -1,6 +1,7 @@
1{ mkDerivation, base, bytestring, exceptions, network, network-uri 1{ mkDerivation, base, bytestring, containers, data-default-class
2, optparse-applicative, stdenv, text, thermoprint-bbcode 2, either, exceptions, network, network-uri, optparse-applicative
3, thermoprint-client, threepenny-gui, websockets 3, stdenv, text, thermoprint-bbcode, thermoprint-client
4, threepenny-gui, time, websockets
4}: 5}:
5mkDerivation { 6mkDerivation {
6 pname = "thermoprint-webgui"; 7 pname = "thermoprint-webgui";
@@ -9,9 +10,9 @@ mkDerivation {
9 isLibrary = false; 10 isLibrary = false;
10 isExecutable = true; 11 isExecutable = true;
11 executableHaskellDepends = [ 12 executableHaskellDepends = [
12 base bytestring exceptions network network-uri optparse-applicative 13 base bytestring containers data-default-class either exceptions
13 text thermoprint-bbcode thermoprint-client threepenny-gui 14 network network-uri optparse-applicative text thermoprint-bbcode
14 websockets 15 thermoprint-client threepenny-gui time websockets
15 ]; 16 ];
16 homepage = "http://dirty-haskell.org/tags/thermoprint.html"; 17 homepage = "http://dirty-haskell.org/tags/thermoprint.html";
17 description = "Threepenny interface for thermoprint-spec compliant servers"; 18 description = "Threepenny interface for thermoprint-spec compliant servers";