1 -- This file is part of Diohsc
2 -- Copyright (C) 2020-23 Martin Bays <mbays@sdf.org>
4 -- This program is free software: you can redistribute it and/or modify
5 -- it under the terms of version 3 of the GNU General Public License as
6 -- published by the Free Software Foundation, or any later version.
8 -- You should have received a copy of the GNU General Public License
9 -- along with this program. If not, see http://www.gnu.org/licenses/.
12 {-# LANGUAGE FlexibleContexts #-}
13 {-# LANGUAGE LambdaCase #-}
14 {-# LANGUAGE OverloadedStrings #-}
15 {-# LANGUAGE TupleSections #-}
17 module LineClient
(lineClient
) where
19 import qualified Codec
.MIME
.Parse
as MIME
20 import qualified Codec
.MIME
.Type
as MIME
21 import Control
.Applicative
(Alternative
, empty)
22 import Control
.Monad
.Catch
23 import Control
.Monad
.State
24 import Control
.Monad
.Trans
.Maybe (MaybeT
(..), runMaybeT
)
26 import Data
.Bifunctor
(second
)
27 import qualified Data
.ByteString
.Lazy
as BL
28 import Data
.Char (toLower)
29 import Data
.Hashable
(hash
)
30 import Data
.IORef
(modifyIORef
, newIORef
, readIORef
)
31 import Data
.List
(intercalate
, isPrefixOf,
32 isSuffixOf, sort, stripPrefix
)
33 import qualified Data
.Map
as M
35 import qualified Data
.Set
as S
36 import qualified Data
.Text
as TS
37 import qualified Data
.Text
.Encoding
.Error
as T
38 import qualified Data
.Text
.Lazy
as T
39 import qualified Data
.Text
.Lazy
.Encoding
as T
40 import qualified Data
.Text
.Lazy
.IO as T
43 import qualified System
.Console
.Haskeline
as HL
44 import qualified System
.Console
.Terminal
.Size
as Size
45 import System
.Directory
46 import System
.Environment
47 import System
.FilePath
48 import System
.IO.Unsafe
(unsafeInterleaveIO
)
50 import Text
.Regex
(matchRegex
, mkRegexWithOpts
)
51 import Time
.System
(timeCurrentP
)
53 import ActiveIdentities
56 import qualified BStack
57 import ClientCert
(KeyType
(..))
70 import Prompt
hiding (promptYN
)
71 import qualified Prompt
75 import qualified RunExternal
76 import RunExternal
hiding (runRestrictedIO
)
84 import Codec
.Text
.IConv
(convert
)
88 import qualified Magic
91 getTermSize
:: IO (Int,Int)
93 Size
.Window height width
<- fromMaybe (Size
.Window
(2^
(30::Int)) 80) <$> Size
.size
96 lineClient
:: ClientOptions
-> [String] -> Bool -> HL
.InputT ClientM
()
97 lineClient cOpts
@ClientOptions
{ cOptUserDataDir
= userDataDir
98 , cOptInteractive
= interactive
, cOptAnsi
= ansi
, cOptGhost
= ghost
} initialCommands repl
= do
99 (liftIO
. readFileLines
$ userDataDir
</> "diohscrc") >>= mapM_ (handleLine
' . T
.unpack
)
100 lift addToQueuesFromFiles
101 mapM_ handleLine
' initialCommands
102 when repl lineClient
'
103 unless ghost
$ lift appendQueuesToFiles
105 handleLine
' :: String -> HL
.InputT ClientM
Bool
106 handleLine
' line
= lift get
>>= \s
-> handleLine cOpts s line
108 lineClient
' :: HL
.InputT ClientM
()
110 cmd
<- lift getPrompt
>>= promptLineInputT
112 Nothing
-> if interactive
113 then printErrFancy ansi
"Use \"quit\" to quit" >> return False
115 Just Nothing
-> return True
116 Just
(Just line
) -> handleLine
' line
117 lift addToQueuesFromFiles
118 unless quit lineClient
'
120 addToQueuesFromFiles
:: ClientM
()
121 addToQueuesFromFiles | ghost
= return ()
123 qfs
<- ignoreIOErr
$ liftIO findQueueFiles
124 forM_ qfs
$ \(qfile
, qname
) ->
125 modifyQueues
. enqueue
(QueueSpec qname Nothing
) <=<
126 ignoreIOErr
. liftIO
$
127 mapMaybe queueLine
<$> readFileLines qfile
<* removeFile qfile
128 ignoreIOErr
. liftIO
$ removeDirectory queuesDir
130 findQueueFiles
:: IO [(FilePath,String)]
132 qf
<- (\e
-> [(queueFile
, "") | e
]) <$> doesFileExist queueFile
133 qfs
<- ((\qn
-> (queuesDir
</> qn
, qn
)) <$>) <$> listDirectory queuesDir
135 queueLine
:: T
.Text
-> Maybe QueueItem
136 queueLine s
= QueueURI Nothing
<$> (parseUriAsAbsolute
. escapeIRI
$ T
.unpack s
)
138 appendQueuesToFiles
:: ClientM
()
139 appendQueuesToFiles
= do
140 queues
<- gets
$ M
.toList
. clientQueues
141 liftIO
$ createDirectoryIfMissing
True queuesDir
142 liftIO
$ forM_ queues appendQueue
144 appendQueue
(_
, []) = pure
()
145 appendQueue
(qname
, queue
) =
146 let qfile
= case qname
of
149 in warnIOErr
$ BL
.appendFile qfile
.
150 T
.encodeUtf8
. T
.unlines $ T
.pack
. show . queueUri
<$> queue
152 queueFile
, queuesDir
:: FilePath
153 queueFile
= userDataDir
</> "queue"
154 queuesDir
= userDataDir
</> "queues"
156 getPrompt
:: ClientM
String
158 queue
<- gets
$ M
.findWithDefault
[] "" . clientQueues
159 curr
<- gets clientCurrent
160 proxies
<- gets
$ clientConfProxies
. clientConfig
161 ais
<- gets clientActiveIdentities
162 let queueStatus
:: Maybe String
163 queueStatus
= guard (not $ null queue
) >> return (show (length queue
) ++ "~")
164 colour
= applyIf ansi
. withColourStr
165 bold
= applyIf ansi withBoldStr
166 uriStatus
:: Int -> URI
-> String
168 let fullUriStr
= stripGem
$ show uri
169 stripGem s
= fromMaybe s
$ stripPrefix
"gemini://" s
170 mIdName
= (identityName
<$>) $ findIdentity ais
=<< requestOfProxiesAndUri proxies uri
171 idStr
= flip (maybe "") mIdName
$ \idName
->
172 let abbrId
= length idName
> 8 && length fullUriStr
+ 2 + length idName
> w
- 2
173 in "[" ++ (if abbrId
then ".." ++ take 6 idName
else idName
) ++ "]"
174 abbrUri
= length fullUriStr
+ length idStr
> w
- 2
175 uriFormat
= colour BoldMagenta
178 let abbrUriChars
= w
- 4 - length idStr
179 preChars
= abbrUriChars `
div`
2
180 postChars
= abbrUriChars
- preChars
181 in uriFormat
(take preChars fullUriStr
) <>
183 uriFormat
(drop (length fullUriStr
- postChars
) fullUriStr
)
184 else uriFormat fullUriStr
186 (if null idStr
then "" else colour Green idStr
)
187 prompt
:: Int -> String
188 prompt maxPromptWidth
=
189 ((applyIf ansi withReverseStr
$ colour BoldCyan
"%%%") ++)
190 . (" " ++) . (++ bold
"> ") . unwords $ catMaybes
192 , uriStatus
(maxPromptWidth
- 5 - maybe 0 ((+1) . length) queueStatus
)
193 . historyUri
<$> curr
195 prompt
. min 40 . (`
div`
2) . snd <$> liftIO getTermSize
197 handleLine
:: ClientOptions
-> ClientState
-> String -> HL
.InputT ClientM
Bool
198 handleLine cOpts
@ClientOptions
{ cOptAnsi
= ansi
} s line
= handle backupHandler
. catchInterrupts
$ case parseCommandLine line
of
199 Left err
-> printErrFancy ansi err
>> return False
200 Right
(CommandLine Nothing
(Just
(c
,_
))) | c `
isPrefixOf`
"quit" -> return True
201 Right cline
-> handleCommandLine cOpts s
False cline
>> return False
203 catchInterrupts
= HL
.handleInterrupt
(printErrFancy ansi
"Interrupted." >> return False) . HL
.withInterrupt
. lift
204 backupHandler
:: SomeException
-> HL
.InputT ClientM
Bool
205 backupHandler
= (>> return False) . printErrFancy ansi
. ("Unhandled exception: " <>) . show
207 handleCommandLine
:: ClientOptions
-> ClientState
-> Bool -> CommandLine
-> ClientM
()
209 cOpts
@(ClientOptions userDataDir interactive ansi ghost restrictedMode requestContext logH
)
210 cState
@(ClientState curr jumpBack cLog visited queues _ marks sessionMarks aliases
211 (ClientConfig defaultAction proxies geminators renderFilter preOpt linkDescFirst maxLogLen maxWrapWidth confNoConfirm verboseConnection
))
213 = \(CommandLine mt mcas
) -> case mcas
of
214 Just
(c
,args
) | Just
(Alias _
(CommandLine mt
' mcas
')) <- lookupAlias c aliases
->
215 let mcas
'' = (, drop 1 args
) . commandArgArg
<$> headMay args
216 appendCArgs
(c
',as') = (c
', (appendTail
<$> as') ++ args
)
218 appendTail arg
@(CommandArg a
' t
') = case args
of
220 (CommandArg _ t
: _
) -> CommandArg a
' $ t
' <> " " <> t
221 in handleCommandLine
' (mt
' `mplus` mt
) $
222 (appendCArgs
<$> mcas
') `mplus` mcas
''
223 _
-> handleCommandLine
' mt mcas
227 -- Remark: we handle haskell's "configurations problem" by having the below all within the scope
228 -- of the ClientOptions parameter. This is nicer to my mind than the heavier approaches of
229 -- simulating global variables or threading a Reader monad throughout. The downside is that this
230 -- module can't be split as much as it ought to be.
231 -- Similar remarks go for `GeminiProtocol.makeRequest`.
233 onRestriction
:: IO ()
234 onRestriction
= printErr
"This is not allowed in restricted mode."
236 doRestricted
:: Monoid a
=> RestrictedIO a
-> IO a
237 doRestricted m | restrictedMode
= onRestriction
>> return mempty
238 |
otherwise = RunExternal
.runRestrictedIO m
240 doRestrictedAlt
:: Alternative f
=> RestrictedIO
(f a
) -> IO (f a
)
241 doRestrictedAlt m | restrictedMode
= onRestriction
>> return empty
242 |
otherwise = RunExternal
.runRestrictedIO m
244 doRestrictedFilter
:: (a
-> RestrictedIO a
) -> (a
-> IO a
)
245 doRestrictedFilter f | restrictedMode
= \a -> do
248 |
otherwise = RunExternal
.runRestrictedIO
. f
250 printErr
, printInfo
:: MonadIO m
=> String -> m
()
251 printErr
= printErrFancy ansi
252 printInfo
= printInfoFancy ansi
254 printIOErr
:: IOError -> IO ()
255 printIOErr
= printErr
. show
258 noConfirm
= confNoConfirm ||
not interactive
260 confirm
:: Applicative m
=> m
Bool -> m
Bool
261 confirm | noConfirm
= const $ pure
True
264 promptYN
= Prompt
.promptYN interactive
266 colour
:: MetaString a
=> Colour
-> a
-> a
267 colour
= applyIf ansi
. withColourStr
268 bold
:: MetaString a
=> a
-> a
269 bold
= applyIf ansi withBoldStr
271 isVisited
:: URI
-> Bool
272 isVisited uri
= S
.member
(hash
. T
.pack
$ show uri
) visited
274 requestOfUri
= requestOfProxiesAndUri proxies
276 idAtUri
:: ActiveIdentities
-> URI
-> Maybe Identity
277 idAtUri ais uri
= findIdentity ais
=<< requestOfUri uri
279 showUriRefFull
:: MetaString a
=> Bool -> ActiveIdentities
-> URI
-> URIRef
-> a
280 showUriRefFull ansi
' ais base ref
= showUriFull ansi
' ais
(Just base
) $ relativeTo ref base
282 showUriFull
:: MetaString a
=> Bool -> ActiveIdentities
-> Maybe URI
-> URI
-> a
283 showUriFull ansi
' ais base uri
=
284 let scheme
= uriScheme uri
285 handled
= scheme `
elem`
["gemini","file"] || M
.member scheme proxies
286 inHistory
= isJust $ curr
>>= flip pathItemByUri uri
287 activeId
= isJust $ idAtUri ais uri
288 col
= if inHistory
&& not activeId
then BoldBlue
else case (isVisited uri
,handled
) of
289 (True,True) -> Yellow
290 (False,True) -> BoldYellow
292 (False,False) -> BoldRed
295 Just b
-> show $ relativeFrom uri b
296 in fromString
(applyIf ansi
' (withColourStr col
) s
) <> case idAtUri ais uri
of
298 Just ident
-> showIdentity ansi
' ident
300 displayUri
:: MetaString a
=> URI
-> a
301 displayUri
= colour Yellow
. fromString
. show
303 showUri
:: URI
-> ClientM
()
305 ais
<- gets clientActiveIdentities
306 liftIO
. putStrLn $ showUriFull ansi ais Nothing uri
308 addToLog
:: URI
-> ClientM
()
310 let t
= T
.pack
$ show uri
312 { clientLog
= BStack
.push maxLogLen t
$ clientLog s
313 , clientVisited
= S
.insert (hash
. T
.pack
$ show uri
) $ clientVisited s
}
314 unless ghost
. liftIO
$ maybe (return ()) (ignoreIOErr
. (`T
.hPutStrLn` t
)) logH
316 preprocessQuery
:: String -> ClientM
String
317 preprocessQuery
= (escapeQuery
<$>) . liftIO
. maybeEdit
319 maybeEdit
:: String -> IO String
320 maybeEdit s | lastMay s
== Just
'\\' =
321 handle
(\e
-> printIOErr e
>> pure s
)
322 . doRestrictedFilter
(editInteractively ansi userDataDir
)
326 expand
:: String -> String
327 expand
= expandHelp ansi
(fst <$> aliases
) userDataDir
329 idsPath
= userDataDir
</> "identities"
330 savesDir
= userDataDir
</> "saves"
331 marksDir
= userDataDir
</> "marks"
333 historyEnv
:: HistoryItem
-> ClientM
[(String,String)]
334 historyEnv
item = gets clientActiveIdentities
>>= \ais
-> pure
$
335 [ ("URI", show $ historyUri
item)
336 , ("MIMETYPE", showMimeType
$ historyMimedData
item) ] <>
337 (maybe [] (identityEnvironment idsPath
) . idAtUri ais
$ historyUri
item)
339 setMark
:: String -> URIWithIdName
-> ClientM
()
340 setMark mark uriId | markNameValid mark
= do
341 modify
$ \s
-> s
{ clientMarks
= insertMark mark uriId
$ clientMarks s
}
342 unless (mark `
elem` tempMarks
) . liftIO
.
343 handle printIOErr
$ saveMark marksDir mark uriId
344 setMark mark _
= printErr
$ "Invalid mark name " ++ mark
346 promptInput
= if ghost
then promptLine
else promptLineWithHistoryFile inputHistPath
347 where inputHistPath
= userDataDir
</> "inputHistory"
349 handleCommandLine
' :: Maybe PTarget
-> Maybe (String, [CommandArg
]) -> ClientM
()
350 handleCommandLine
' mt mcas
= void
. runMaybeT
$ do
353 Just pt
-> either ((>> mzero
) . printErr
)
354 (\ts
-> mapM_ addTargetId ts
>> return ts
) $
355 resolveTarget cState pt
357 Nothing
-> lift
$ handleBareTargets ts
359 c
' <- maybe (printErr
(unknownCommand s
) >> mzero
)
360 return $ normaliseCommand s
361 lift
$ handleCommand ts
(c
',as)
363 unknownCommand s
= "Unknown command \"" <> s
<> "\". Type \"help\" for help."
364 addTargetId
:: Target
-> MaybeT ClientM
()
365 addTargetId
(TargetIdUri idName uri
) =
366 liftIO
(loadIdentity idsPath idName
) >>= (\case
367 (Nothing
, _
) -> printErr
("Bad URI: " ++ show uri
) >> mzero
368 (_
, Nothing
) -> printErr
("Unknown identity: " ++ showIdentityName ansi idName
) >> mzero
369 (Just req
, Just ident
) -> lift
$ addIdentity req ident
) . (requestOfUri uri
,)
370 addTargetId _
= return ()
372 doPage
:: [T
.Text
] -> ClientM
()
375 (height
,width
) <- liftIO getTermSize
376 let pageWidth
= min maxWrapWidth
(width
- 4)
377 perPage
= height
- min 3 (height `
div`
4)
378 doCmd str
= get
>>= \s
-> doSubCommand s
True str
379 printLinesPaged pageWidth width perPage doCmd ls
380 |
otherwise = liftIO
$ mapM_ T
.putStrLn ls
382 handleBareTargets
:: [Target
] -> ClientM
()
383 handleBareTargets
[] = return ()
384 handleBareTargets
(_
:_
:_
) =
385 printErr
"Can only go to one place at a time. Try \"show\" or \"page\"?"
386 handleBareTargets
[TargetHistory
item] = goHistory
item
387 handleBareTargets
[TargetFrom origin uri
] = goUri
False (Just origin
) uri
388 handleBareTargets
[t
] = goUri
False Nothing
$ targetUri t
391 handleCommand
:: [Target
] -> (String, [CommandArg
]) -> ClientM
()
392 handleCommand _
(c
,_
) | restrictedMode
&& notElem c
(commands
True) =
393 printErr
"Command disabled in restricted mode"
394 handleCommand
[] ("help", args
) = case args
of
395 [] -> doPage
. map (T
.pack
. expand
) $ helpText
396 CommandArg s _
: _
-> doPage
. map (T
.pack
. expand
) $ helpOn s
397 handleCommand
[] ("commands",_
) = doPage
$ T
.pack
. expand
<$> commandHelpText
399 commandHelpText
= ["Aliases:"] ++ (showAlias
<$> aliases
) ++
400 ["","Commands:"] ++ (('{' :) . (<> "}") <$> commands
False)
401 showAlias
(a
, Alias s _
) = "{" <> a
<> "}: " <> s
403 handleCommand
[] ("mark", []) =
404 let ms
= M
.toAscList marks
405 markLine
(m
, Just uriId
) = let m
' = showMinPrefix ansi
(fst <$> ms
) m
406 in T
.pack
$ "'" <> m
' <>
407 replicate (max 1 $ 16 - visibleLength
(T
.pack m
')) ' ' <>
409 markLine
(m
, Nothing
) = T
.pack
$ "'" <> m
<> " [Failed to read mark]"
410 in doPage
$ markLine
<$> ms
411 handleCommand
[] ("inventory",_
) = do
412 ais
<- gets clientActiveIdentities
413 let showNumberedUri
:: Bool -> T
.Text
-> (Int,URI
) -> T
.Text
414 showNumberedUri iter s
(n
,uri
) = s
<>
415 (if iter
&& n
== 1 then " "
416 else if iter
&& n
== 2 then T
.takeEnd
1 s
417 else T
.pack
(show n
)) <>
418 " " <> showUriFull ansi ais Nothing uri
419 showIteratedItem s
(n
,item) = showNumberedUri
True s
(n
, historyUri
item)
420 showNumberedItem s
(n
,item) = showNumberedUri
False s
(n
, historyUri
item)
421 showIteratedQueueItem s
(n
, QueueURI _ uri
) =
422 showNumberedUri
True s
(n
, uri
)
423 showIteratedQueueItem s
(n
, QueueHistory
item) =
424 showNumberedUri
True s
(n
, historyUri
item) <> " {fetched}"
425 showJumpBack
:: [T
.Text
]
426 showJumpBack
= maybeToList $ ("'' " <>) . showUriFull ansi ais Nothing
. historyUri
<$> jumpBack
427 doPage
. intercalate
[""] . filter (not . null) $
429 [ showIteratedQueueItem
(T
.pack
$ qname
<> "~") <$> zip [1..] queue
430 |
(qname
, queue
) <- M
.toList queues
]
431 ++ [ showNumberedItem
"'" <$> M
.toAscList sessionMarks
432 , (showIteratedItem
"<" <$> zip [1..] (maybe [] (initSafe
. historyAncestors
) curr
))
433 ++ (("@ " <>) . showUriFull ansi ais Nothing
. historyUri
<$>
434 maybeToList (curr
>>= lastMay
. historyAncestors
))
435 , showIteratedItem
">" <$> zip [1..] (maybe [] historyDescendants curr
)
437 handleCommand
[] ("log",_
) =
438 let showLog
(n
,t
) = "$" <> T
.pack
(show n
) <> " " <> colour Yellow t
439 in doPage
$ showLog
<$> zip [(1::Int)..] (BStack
.toList cLog
)
440 handleCommand
[] ("alias", CommandArg a _
: CommandArg _ str
: _
) = void
. runMaybeT
$ do
441 c
<- either ((>>mzero
) . printErr
) return $ parseCommand a
442 when (c
/= a
) $ printErr
("Bad alias: " <> a
) >> mzero
443 cl
<- either ((>>mzero
) . printErr
) return $ parseCommandLine str
445 s
{ clientAliases
= insertAlias a
(Alias str cl
) . deleteAlias a
$ clientAliases s
}
446 handleCommand
[] ("alias", [CommandArg a _
]) =
447 modify
$ \s
-> s
{ clientAliases
= deleteAlias a
$ clientAliases s
}
448 handleCommand
[] ("set", []) = liftIO
$ do
449 let (c
,args
) = defaultAction
450 putStrLn $ expand
"{default_action}: " <> c
<>
451 maybe "" ((" "<>) . commandArgLiteralTail
) (headMay args
)
452 putStrLn $ expand
"{proxies}:"
453 printMap showHost
$ M
.toAscList proxies
454 putStrLn $ expand
"{geminators}:"
455 printMap
id $ second
snd <$> geminators
456 putStrLn $ expand
"{render_filter}: " <> fromMaybe "" renderFilter
457 putStrLn $ expand
"{pre_display}: " <> showPreOpt preOpt
458 putStrLn $ expand
"{link_desc_first}: " <> show linkDescFirst
459 putStrLn $ expand
"{log_length}: " <> show maxLogLen
460 putStrLn $ expand
"{max_wrap_width}: " <> show maxWrapWidth
461 putStrLn $ expand
"{no_confirm}: " <> show noConfirm
462 putStrLn $ expand
"{verbose_connection}: " <> show verboseConnection
464 printMap
:: (a
-> String) -> [(String,a
)] -> IO ()
465 printMap f
as = mapM_ putStrLn $
466 (\(k
,e
) -> " " <> k
<> " " <> f e
) <$> as
467 handleCommand
[] ("set", CommandArg opt _
: val
)
468 | opt `
isPrefixOf`
"default_action" = case val
of
469 (CommandArg cmd _
: args
) -> case actionOfCommand
(cmd
,args
) of
470 Just _
-> modifyCConf
$ \c
-> c
{ clientConfDefaultAction
= (cmd
,args
) }
471 Nothing
-> printErr
"Invalid action"
472 _
-> printErr
"Require value for option."
473 | opt `
isPrefixOf`
"proxies" || opt `
isPrefixOf`
"proxy" = case val
of
474 (CommandArg scheme _
: val
') ->
475 let f
= maybe (M
.delete scheme
) (M
.insert scheme
) $
476 parseHost
. commandArgLiteralTail
=<< headMay val
'
477 in modifyCConf
$ \c
-> c
{ clientConfProxies
= f
$ clientConfProxies c
}
478 -- if only I'd allowed myself to use lenses, eh?
479 [] -> printErr
"Require mimetype to set geminator for."
480 | opt `
isPrefixOf`
"geminators" = case val
of
481 (CommandArg patt _
: val
') ->
482 let f
= maybe (filter $ (/= patt
) . fst)
483 (\v -> (++ [(patt
, (mkRegexWithOpts patt
True True,
484 commandArgLiteralTail v
))])) $
486 in modifyCConf
$ \c
-> c
{ clientConfGeminators
= f
$ clientConfGeminators c
}
487 [] -> printErr
"Require mimetype to set geminator for."
488 | opt `
isPrefixOf`
"render_filter" =
489 modifyCConf
$ \c
-> c
{ clientConfRenderFilter
=
490 commandArgLiteralTail
<$> headMay val
}
491 | opt `
isPrefixOf`
"pre_display" = case val
of
492 [CommandArg s _
] |
map toLower s `
isPrefixOf`
"both" ->
493 modifyCConf
$ \c
-> c
{ clientConfPreOpt
= PreOptBoth
}
494 [CommandArg s _
] |
map toLower s `
isPrefixOf`
"pre" ->
495 modifyCConf
$ \c
-> c
{ clientConfPreOpt
= PreOptPre
}
496 [CommandArg s _
] |
map toLower s `
isPrefixOf`
"alt" ->
497 modifyCConf
$ \c
-> c
{ clientConfPreOpt
= PreOptAlt
}
498 _
-> printErr
"Require \"both\" or \"pre\" or \"alt\" for pre_display"
499 | opt `
isPrefixOf`
"link_desc_first" = case val
of
500 [CommandArg s _
] |
map toLower s `
isPrefixOf`
"true" ->
501 modifyCConf
$ \c
-> c
{ clientConfLinkDescFirst
= True }
502 [CommandArg s _
] |
map toLower s `
isPrefixOf`
"false" ->
503 modifyCConf
$ \c
-> c
{ clientConfLinkDescFirst
= False }
504 _
-> printErr
"Require \"true\" or \"false\" as value for link_desc_first"
505 | opt `
isPrefixOf`
"log_length" = case val
of
506 [CommandArg s _
] | Just n
<- readMay s
, n
>= 0 -> do
507 modifyCConf
$ \c
-> c
{ clientConfMaxLogLen
= n
}
508 modify
$ \st
-> st
{ clientLog
= BStack
.truncate n
$ clientLog st
}
509 _
-> printErr
"Require non-negative integer value for log_length"
510 | opt `
isPrefixOf`
"max_wrap_width" = case val
of
511 [CommandArg s _
] | Just n
<- readMay s
, n
> 0 ->
512 modifyCConf
$ \c
-> c
{ clientConfMaxWrapWidth
= n
}
513 _
-> printErr
"Require positive integer value for max_wrap_width"
514 | opt `
isPrefixOf`
"no_confirm" = case val
of
515 [CommandArg s _
] |
map toLower s `
isPrefixOf`
"true" ->
516 modifyCConf
$ \c
-> c
{ clientConfNoConfirm
= True }
517 [CommandArg s _
] |
map toLower s `
isPrefixOf`
"false" ->
518 modifyCConf
$ \c
-> c
{ clientConfNoConfirm
= False }
519 _
-> printErr
"Require \"true\" or \"false\" as value for no_confirm"
520 | opt `
isPrefixOf`
"verbose_connection" = case val
of
521 [CommandArg s _
] |
map toLower s `
isPrefixOf`
"true" ->
522 modifyCConf
$ \c
-> c
{ clientConfVerboseConnection
= True }
523 [CommandArg s _
] |
map toLower s `
isPrefixOf`
"false" ->
524 modifyCConf
$ \c
-> c
{ clientConfVerboseConnection
= False }
525 _
-> printErr
"Require \"true\" or \"false\" as value for verbose_connection"
526 |
otherwise = printErr
$ "No such option \"" <> opt
<> "\"."
527 handleCommand
[] cargs
=
529 Just
item -> handleCommand
[TargetHistory
item] cargs
530 Nothing
-> printErr
"No current location. Enter an URI, or type \"help\"."
532 handleCommand ts
("add", args
) = case parseQueueSpec
$ commandArgArg
<$> args
of
533 Nothing
-> printErr
"Bad arguments to 'add'."
534 Just qs
-> modifyQueues
. enqueue qs
$ targetQueueItem
<$> ts
536 handleCommand ts
("fetch", args
) = case parseQueueSpec
$ commandArgArg
<$> args
of
537 Nothing
-> printErr
"Bad arguments to 'fetch."
539 -- XXX: we have to use an IORef to store the items, since
540 -- CommandAction doesn't allow a return value.
541 lRef
<- liftIO
$ newIORef
[]
542 let add
item = liftIO
$ slurpItem
item >> modifyIORef lRef
(item:)
543 forM_ ts
$ \t -> case t
of
544 TargetHistory
item -> add
item
545 _
-> modifyQueues
(unqueue uri
) >> doRequestUri uri add
546 where uri
= targetUri t
547 l
<- liftIO
$ reverse <$> readIORef lRef
548 modifyQueues
. enqueue qs
$ QueueHistory
<$> l
550 handleCommand ts cargs
=
551 mapM_ handleTargetCommand ts
553 handleTargetCommand
(TargetHistory
item) | Just action
<- actionOfCommand cargs
=
555 handleTargetCommand t | Just action
<- actionOfCommand cargs
=
556 let uri
= targetUri t
557 in modifyQueues
(unqueue uri
) >> doRequestUri uri action
558 handleTargetCommand
(TargetHistory
item) |
("repeat",_
) <- cargs
=
559 goUri
True (recreateOrigin
<$> historyParent
item) $ historyUri
item
560 handleTargetCommand
(TargetHistory
item) |
("repl",_
) <- cargs
=
561 repl
(recreateOrigin
<$> historyParent
item) $ historyUri
item
562 handleTargetCommand t |
("query", CommandArg _ str
: _
) <- cargs
= do
563 let origin
= case t
of
564 TargetHistory
item -> Just
$ HistoryOrigin
item Nothing
565 TargetFrom o _
-> Just o
567 str
' <- preprocessQuery str
568 goUri
True origin
. setQuery
('?
':str
') $ targetUri t
569 handleTargetCommand t
=
570 handleUriCommand
(targetUri t
) cargs
572 recreateOrigin
:: HistoryItem
-> HistoryOrigin
573 recreateOrigin parent
= HistoryOrigin parent
$ childLink
=<< historyChild parent
575 handleUriCommand uri
("delete",[]) =
576 modifyQueues
$ unqueueFrom
"" uri
577 handleUriCommand uri
("delete",CommandArg qname _
: _
) =
578 modifyQueues
$ unqueueFrom qname uri
579 handleUriCommand uri
("repeat",_
) = goUri
True Nothing uri
580 handleUriCommand uri
("uri",_
) = showUri uri
581 handleUriCommand uri
("mark", CommandArg mark _
: _
)
582 | Just _
<- readMay mark
:: Maybe Int = error "Bad mark for uri"
584 ais
<- gets clientActiveIdentities
585 let mIdName
= case findIdentity ais
=<< requestOfUri uri
of
586 Just ident |
not $ isTemporary ident
-> Just
$ identityName ident
588 setMark mark
$ URIWithIdName uri mIdName
589 handleUriCommand uri
("identify", args
) = case requestOfUri uri
of
590 Nothing
-> printErr
"Bad URI"
591 Just req
-> gets
((`findIdentityRoot` req
) . clientActiveIdentities
) >>= \case
592 Just
(root
,(ident
,_
)) |
null args
-> endIdentityPrompted root ident
593 _
-> void
. runMaybeT
$ do
594 ident
<- MaybeT
. liftIO
$ case args
of
595 CommandArg idName _
: args
' ->
596 let tp
= case args
' of
597 CommandArg
('e
':'d
':_
) _
: _
-> KeyEd25519
599 in getIdentity interactive ansi idsPath tp idName
601 then getIdentityRequesting ansi idsPath
602 else getIdentity interactive ansi idsPath KeyRSA
""
603 lift
$ addIdentity req ident
604 handleUriCommand uri
("browse", args
) = do
605 ais
<- gets clientActiveIdentities
606 let envir
= maybe [] (identityEnvironment idsPath
) .
607 idAtUri ais
. setSchemeDefault
$ uri
608 void
. liftIO
. runMaybeT
$ do
611 (\s
-> if null s
then notSet
else return $ parseBrowser s
) =<<
612 lift
(lookupEnv
"BROWSER")
614 notSet
= printErr
"Please set $BROWSER or give a command to run" >> mzero
615 -- |based on specification for $BROWSER in 'man 1 man'
616 parseBrowser
:: String -> String
617 parseBrowser
= subPercentOrAppend
(show uri
) . takeWhile (/=':')
618 (CommandArg _ c
: _
) -> return $ subPercentOrAppend
(show uri
) c
619 lift
$ confirm
(confirmShell
"Run" cmd
) >>? doRestricted
$ void
(runShellCmd cmd envir
)
620 handleUriCommand uri
("repl",_
) = repl Nothing uri
621 handleUriCommand uri
("log",_
) = addToLog uri
>> modifyQueues
(unqueue uri
)
623 handleUriCommand _
(c
,_
) = printErr
$ "Bad arguments to command " <> c
625 repl
:: Maybe HistoryOrigin
-> URI
-> ClientM
()
626 repl origin uri
= repl
' where
627 repl
' = liftIO
(join <$> promptInput
">> ") >>= \case
630 query
' <- preprocessQuery query
631 goUri
True origin
. setQuery
('?
':query
') $ uri
634 slurpItem
:: HistoryItem
-> IO ()
635 slurpItem
item = slurpNoisily
(historyRequestTime
item) . mimedBody
$ historyMimedData
item
637 actionOnRendered
:: Bool -> ([T
.Text
] -> ClientM
()) -> CommandAction
638 actionOnRendered ansi
' m
item = do
639 ais
<- gets clientActiveIdentities
640 liftIO
(renderMimed ansi
' (historyUri
item) ais
(historyGeminatedMimedData
item)) >>=
643 actionOfCommand
:: (String, [CommandArg
]) -> Maybe CommandAction
644 actionOfCommand
(c
,_
) | restrictedMode
&& notElem c
(commands
True) = Nothing
645 actionOfCommand
("show",_
) = Just
. actionOnRendered ansi
$ liftIO
. mapM_ T
.putStrLn
646 actionOfCommand
("page",_
) = Just
$ actionOnRendered ansi doPage
647 actionOfCommand
("links",_
) = Just
$ \item -> do
648 ais
<- gets clientActiveIdentities
649 let cl
= childLink
=<< historyChild
item
650 linkLine n
(Link uri desc
) =
651 applyIf
(cl
== Just
(n
-1)) (bold
"* " <>) $
652 T
.pack
('[' : show n
++ "] ")
653 <> showUriRefFull ansi ais
(historyUri
item) uri
654 <> if T
.null desc
then "" else " " <>
655 applyIf ansi
(withColourStr Cyan
) desc
656 doPage
. zipWith linkLine
[1..] . historyLinks
$ item
658 actionOfCommand
("mark", CommandArg mark _
: _
) |
659 Just n
<- readMay mark
:: Maybe Int = Just
$ \item -> do
660 liftIO
$ slurpItem
item
661 modify
$ \s
-> s
{ clientSessionMarks
= M
.insert n
item $ clientSessionMarks s
}
662 actionOfCommand
("mime",_
) = Just
$ liftIO
. putStrLn . showMimeType
. historyMimedData
663 actionOfCommand
("save", []) = actionOfCommand
("save", [CommandArg
"" savesDir
])
664 actionOfCommand
("save", CommandArg _ path
: _
) = Just
$ \item -> liftIO
. doRestricted
. RestrictedIO
$ do
665 createDirectoryIfMissing
True savesDir
666 homePath
<- getHomeDirectory
668 |
take 2 path
== "~/" = homePath
</> drop 2 path
669 |
take 1 path
== "/" ||
take 2 path
== "./" ||
take 3 path
== "../" = path
670 |
otherwise = savesDir
</> path
671 body
= mimedBody
$ historyMimedData
item
672 uri
= historyUri
item
673 name
= fromMaybe (fromMaybe "" $ uriRegName uri
) . lastMay
.
674 filter (not . null) $ pathSegments uri
675 handle printIOErr
. void
. runMaybeT
$ do
676 lift
$ mkdirhierto path
'
677 isDir
<- lift
$ doesDirectoryExist path
'
678 let fullpath
= if isDir
then path
' </> name
else path
'
679 lift
(doesDirectoryExist fullpath
) >>?
do
680 lift
. printErr
$ "Path " ++ show fullpath
++ " exists and is directory"
682 lift
(doesFileExist fullpath
) >>?
683 guard =<< lift
(promptYN
False $ "Overwrite " ++ show fullpath
++ "?")
685 putStrLn $ "Saving to " ++ fullpath
687 BL
.writeFile fullpath
=<< interleaveProgress t0 body
689 actionOfCommand
("!", CommandArg _ cmd
: _
) = Just
$ \item -> do
690 env
<- historyEnv
item
691 liftIO
. handle printIOErr
. doRestricted
.
692 shellOnData noConfirm cmd userDataDir env
. mimedBody
$ historyMimedData
item
694 actionOfCommand
("view",_
) = Just
$ \item ->
695 let mimed
= historyMimedData
item
696 mimetype
= showMimeType mimed
697 body
= mimedBody mimed
698 in liftIO
. handle printIOErr
. doRestricted
$ runMailcap noConfirm
"view" userDataDir mimetype body
699 actionOfCommand
("|", CommandArg _ cmd
: _
) = Just
$ \item -> do
700 env
<- historyEnv
item
701 liftIO
. handle printIOErr
. doRestricted
$
702 pipeToShellLazily cmd env
. mimedBody
$ historyMimedData
item
703 actionOfCommand
("||", args
) = Just
$ pipeRendered ansi args
704 actionOfCommand
("||-", args
) = Just
$ pipeRendered
False args
705 actionOfCommand
("cat",_
) = Just
$ liftIO
. BL
.putStr . mimedBody
. historyMimedData
706 actionOfCommand
("at", CommandArg _ str
: _
) = Just
$ \item ->
707 doSubCommand
(cState
{ clientCurrent
= Just
item }) blockGo str
708 actionOfCommand _
= Nothing
710 pipeRendered
:: Bool -> [CommandArg
] -> CommandAction
711 pipeRendered ansi
' args
item = (\action
-> actionOnRendered ansi
' action
item) $ \ls
-> do
712 env
<- historyEnv
item
713 liftIO
. void
. runMaybeT
$ do
716 (\s
-> if null s
then notSet
else return s
) =<<
717 liftIO
(lookupEnv
"PAGER")
719 notSet
= printErr
"Please set $PAGER or give a command to run" >> mzero
720 (CommandArg _ cmd
: _
) -> return cmd
721 lift
. doRestricted
. pipeToShellLazily cmd env
. T
.encodeUtf8
$ T
.unlines ls
723 doSubCommand
:: ClientState
-> Bool -> String -> ClientM
()
724 doSubCommand s block str
= void
. runMaybeT
$ do
725 cl
<- either ((>>mzero
) . printErr
) return $ parseCommandLine str
726 lift
$ handleCommandLine cOpts s block cl
728 setCurr
:: HistoryItem
-> ClientM
()
730 let isJump
= isNothing $ curr
>>= pathItemByUri i
. historyUri
732 when isJump
$ modify
$ \s
-> s
{ clientJumpBack
= curr
}
733 modify
$ \s
-> s
{ clientCurrent
= Just i
}
735 doDefault
:: HistoryItem
-> ClientM
()
737 maybe (printErr
"Bad default action!") ($ item) $ actionOfCommand defaultAction
739 goHistory
:: HistoryItem
-> ClientM
()
740 goHistory _ | blockGo
= printErr
"Can't go anywhere now."
742 modifyQueues
$ unqueue uri
746 where uri
= historyUri
item
748 goUri
:: Bool -> Maybe HistoryOrigin
-> URI
-> ClientM
()
749 goUri _ _ _ | blockGo
= printErr
"Can't go anywhere now."
750 goUri forceRequest origin uri
= do
751 modifyQueues
$ unqueue uri
752 activeId
<- gets
$ isJust . (`idAtUri` uri
) . clientActiveIdentities
753 case curr
>>= flip pathItemByUri uri
of
754 Just i
' |
not (activeId || forceRequest
) -> goHistory i
'
755 _
-> doRequestUri uri
$ \item -> do
757 -- Lazily recursively update the links in the doubly linked list
758 let i
' = i
{ historyParent
= updateParent
. updateChild i
' <$> historyParent i
}
760 updateChild i
' i
= i
{ historyChild
= setChild
<$> historyChild i
}
761 where setChild c
= c
{ childItem
= i
' }
762 glueOrigin
(HistoryOrigin o l
) = updateParent
$ o
{ historyChild
= Just
$ HistoryChild
item' l
}
763 item' = item { historyParent
= glueOrigin
<$> origin
}
766 liftIO
$ slurpItem
item
768 doRequestUri
:: URI
-> CommandAction
-> ClientM
()
769 doRequestUri uri0 action
= doRequestUri
' 0 uri0
771 doRequestUri
' redirs uri
772 | Just req
<- requestOfUri uri
= addToLog uri
>> doRequest redirs req
773 |
otherwise = printErr
$ "Bad URI: " ++ displayUri uri
++ (
774 let scheme
= uriScheme uri
775 in if scheme
/= "gemini" && isNothing (M
.lookup scheme proxies
)
776 then " : No proxy set for non-gemini scheme " ++ scheme
++ "; use \"browse\"?"
779 doRequest
:: Int -> Request
-> ClientM
()
780 doRequest redirs _ | redirs
> 5 =
781 printErr
"Too many redirections!"
782 doRequest redirs req
@(NetworkRequest _ uri
) = do
783 (mId
, ais
) <- liftIO
. useActiveIdentity noConfirm ansi req
=<< gets clientActiveIdentities
784 modify
$ \s
-> s
{ clientActiveIdentities
= ais
}
785 printInfo
$ ">>> " ++ showUriFull ansi ais Nothing uri
786 let respBuffSize
= 2 ^
(15::Int) -- 32KB max cache for response stream
787 liftIO
(makeRequest requestContext mId respBuffSize verboseConnection req
)
788 `
bracket`
either (\_
-> return ()) (liftIO
. snd) $
790 (printErr
. displayException
)
791 (handleResponse
. fst)
793 handleResponse
:: Response
-> ClientM
()
794 handleResponse
(Input isPass prompt
) = do
795 let defaultPrompt
= "[" ++ (if isPass
then "PASSWORD" else "INPUT") ++ "]"
796 (liftIO
. (join <$>) . promptInput
$
797 (if null prompt
then defaultPrompt
else prompt
) ++ " > ") >>= \case
800 query
' <- preprocessQuery query
801 doRequestUri
' redirs
. setQuery
('?
':query
') $ uri
803 handleResponse
(Success mimedData
) = doAction req mimedData
805 handleResponse
(Redirect isPerm to
) = do
806 ais
<- gets clientActiveIdentities
807 let uri
' = to `relativeTo` uri
808 crossSite
= uriRegName uri
' /= uriRegName uri
809 crossScheme
= uriScheme uri
' /= uriScheme uri
810 crossScope
= case idAtUri ais
<$> [uri
,uri
'] of
811 [fromId
,toId
] -> isJust toId
&& fromId
/= toId
813 warningStr
= colour BoldRed
814 proceed
<- (isJust <$>) . lift
. runMaybeT
$ do
815 when crossSite
$ guard <=< (liftIO
. promptYN
False) $
816 warningStr
"Follow cross-site redirect to " ++ showUriRefFull ansi ais uri to
++ warningStr
"?"
817 when crossScheme
$ guard <=< (liftIO
. promptYN
False) $
818 warningStr
"Follow cross-protocol redirect to " ++ showUriRefFull ansi ais uri to
++ warningStr
"?"
819 when crossScope
$ guard <=< (liftIO
. promptYN
False) $
820 warningStr
"Follow redirect with identity " ++ showUriRefFull ansi ais uri to
++ warningStr
"?"
822 when (isPerm
&& not ghost
) . mapM_ (updateMark uri
') . marksWithUri uri
=<< gets clientMarks
823 doRequestUri
' (redirs
+ 1) uri
'
824 where updateMark uri
' (mark
,uriId
) = do
825 conf
<- confirm
. liftIO
. promptYN
True $ "Update mark '" <> mark
<> " to " <> show uri
' <> " ?"
826 when conf
. setMark mark
$ uriId
{ uriIdUri
= uri
' }
827 handleResponse
(Failure code info
) |
60 <= code
&& code
<= 69 = void
. runMaybeT
$ do
829 liftIO
. putStrLn $ (case code
of
830 60 -> "Server requests identification"
831 _
-> "Server rejects provided identification certificate" ++
832 (if code
== 61 then " as unauthorised" else if code
== 62 then " as invalid" else ""))
833 ++ if null info
then "" else ": " ++ info
835 MaybeT
. liftIO
$ getIdentityRequesting ansi idsPath
837 addIdentity req identity
840 handleResponse
(Failure code info
) =
841 printErr
$ "Server returns failure: " ++ show code
++ " " ++ info
842 handleResponse
(MalformedResponse malformation
) =
843 printErr
$ "Malformed response from server: " ++ show malformation
845 doRequest redirs
(LocalFileRequest path
) | redirs
> 0 = printErr
"Ignoring redirect to local file."
846 |
otherwise = void
. runMaybeT
$ do
847 (path
', mimedData
) <- MaybeT
. liftIO
. doRestrictedAlt
. RestrictedIO
. warnIOErrAlt
$ do
848 let detectExtension
= case takeExtension path
of
849 -- |certain crucial filetypes we can't rely on magic to detect:
850 s | s `
elem`
[".gmi", ".gem", ".gemini"] -> Just
"text/gemini"
851 ".md" -> Just
"text/markdown"
852 ".html" -> Just
"text/html"
855 detectPlain
"text/plain" = fromMaybe "text/plain" detectExtension
857 magic
<- Magic
.magicOpen
[Magic
.MagicMimeType
]
858 Magic
.magicLoadDefault magic
859 s
<- detectPlain
<$> Magic
.magicFile magic path
861 let s
= if "/" `
isSuffixOf` path
then "inode/directory"
862 else fromMaybe "application/octet-stream" detectExtension
864 case MIME
.parseMIMEType
$ TS
.pack s
of
865 Nothing
-> printErr
("Failed to parse mimetype string: " <> s
) >> return Nothing
866 _ | s
== "inode/directory" -> Just
. (slashedPath
,) . MimedData gemTextMimeType
.
867 T
.encodeUtf8
. T
.unlines .
868 ((("=> " <>) . T
.pack
. escapePathString
) <$>) . sort <$>
869 getDirectoryContents path
870 where slashedPath |
"/" `
isSuffixOf` path
= path
871 |
otherwise = path
<> "/"
872 Just mimetype
-> Just
. (path
,) . MimedData mimetype
<$> BL
.readFile path
873 lift
$ doAction
(LocalFileRequest path
') mimedData
875 doAction req mimedData
= do
876 t0
<- liftIO timeCurrentP
877 geminated
<- geminate mimedData
878 action
$ HistoryItem req t0 mimedData geminated Nothing Nothing
880 -- |returns MimedData with lazy IO
881 geminate
:: MimedData
-> ClientM MimedData
883 let geminator
= lookupGeminator
$ showMimeType mimed
884 in liftIO
. unsafeInterleaveIO
$ applyGeminator geminator
886 lookupGeminator mimetype
=
887 listToMaybe [ gem |
(_
, (regex
, gem
)) <- geminators
888 , isJust $ matchRegex regex mimetype
]
889 applyGeminator Nothing
= return mimed
890 applyGeminator
(Just cmd
) =
891 printInfo
("| " <> cmd
) >>
892 MimedData gemTextMimeType
<$>
893 doRestrictedFilter
(filterShell cmd
[("URI", show $ requestUri req
)]) (mimedBody mimed
)
895 gemTextMimeType
:: MIME
.Type
896 gemTextMimeType
= MIME
.Type
(MIME
.Text
"gemini") []
899 addIdentity
:: Request
-> Identity
-> ClientM
()
900 addIdentity req identity
= do
901 ais
<- gets clientActiveIdentities
>>= liftIO
. insertIdentity req identity
902 modify
$ \s
-> s
{clientActiveIdentities
= ais
}
903 endIdentityPrompted
:: Request
-> Identity
-> ClientM
()
904 endIdentityPrompted root ident
= do
905 conf
<- confirm
$ liftIO
. promptYN
False $ "Stop using " ++
906 (if isTemporary ident
then "temporary anonymous identity" else showIdentity ansi ident
) ++
907 " at " ++ displayUri
(requestUri root
) ++ "?"
908 when conf
. modify
$ \s
->
909 s
{ clientActiveIdentities
= deleteIdentity root
$ clientActiveIdentities s
}
911 renderMimed
:: Bool -> URI
-> ActiveIdentities
-> MimedData
-> IO (Either String [T
.Text
])
912 renderMimed ansi
' uri ais
(MimedData mime body
) = case MIME
.mimeType mime
of
913 MIME
.Text textType
-> do
914 let extractCharsetParam
(MIME
.MIMEParam
"charset" v
) = Just v
915 extractCharsetParam _
= Nothing
916 charset
= TS
.unpack
. fromMaybe "utf-8" . msum . map extractCharsetParam
$ MIME
.mimeParams mime
917 isUtf8
= map toLower charset `
elem`
["utf-8", "utf8"]
919 reencoder
= if isUtf8
then id else
920 convert charset
"UTF-8"
923 unless isUtf8
. printErr
$
924 "Warning: Treating unsupported charset " ++ show charset
++ " as utf-8"
926 (_
,width
) <- getTermSize
927 let pageWidth
= if interactive
928 then min maxWrapWidth
(width
- 4)
930 let bodyText
= T
.decodeUtf8With T
.lenientDecode
$ reencoder body
931 applyFilter
:: [T
.Text
] -> IO [T
.Text
]
932 applyFilter
= case renderFilter
of
934 Just cmd
-> (T
.lines . T
.decodeUtf8With T
.lenientDecode
<$>) .
935 doRestrictedFilter
(filterShell cmd
[]) . BL
.concat . (appendNewline
. T
.encodeUtf8
<$>)
936 where appendNewline
= (`BL
.snoc`
10)
937 (Right
<$>) . applyFilter
$ case textType
of
939 let opts
= GemRenderOpts ansi
' preOpt pageWidth linkDescFirst
940 in printGemDoc opts
(showUriRefFull ansi
' ais uri
) $ parseGemini bodyText
941 _
-> T
.stripEnd
. stripControl
<$> T
.lines bodyText
943 return . Left
$ "No geminator for " ++ TS
.unpack
(MIME
.showMIMEType mimeType
) ++ " configured. Try \"save\", \"view\", \"!\", or \"|\"?"