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 Control
.Applicative
(Alternative
, empty)
20 import Control
.Monad
.Catch
21 import Control
.Monad
.State
22 import Control
.Monad
.Trans
.Maybe (MaybeT
(..), runMaybeT
)
23 import Data
.Bifunctor
(second
)
24 import Data
.Char (isAlpha, toLower)
25 import Data
.Hash
(hash
)
26 import Data
.IORef
(modifyIORef
, newIORef
, readIORef
)
27 import Data
.List
(intercalate
, isPrefixOf,
28 isSuffixOf, sort, stripPrefix
)
31 import System
.Directory
32 import System
.Environment
34 import System
.FilePath
36 import System
.IO.Unsafe
(unsafeInterleaveIO
)
37 import Text
.Regex
(matchRegex
, mkRegexWithOpts
)
38 import Time
.System
(timeCurrentP
)
40 import qualified Data
.ByteString
.Lazy
as BL
42 import qualified Codec
.MIME
.Parse
as MIME
43 import qualified Codec
.MIME
.Type
as MIME
44 import qualified Data
.Map
as M
45 import qualified Data
.Set
as S
46 import qualified Data
.Text
as TS
47 import qualified Data
.Text
.Encoding
.Error
as T
48 import qualified Data
.Text
.Lazy
as T
49 import qualified Data
.Text
.Lazy
.Encoding
as T
50 import qualified Data
.Text
.Lazy
.IO as T
51 import qualified System
.Console
.Haskeline
as HL
52 import qualified System
.Console
.Terminal
.Size
as Size
54 import ActiveIdentities
57 import qualified BStack
58 import ClientCert
(KeyType
(..))
72 import Prompt
hiding (promptYN
)
73 import qualified Prompt
77 import qualified RunExternal
78 import RunExternal
hiding (runRestrictedIO
)
87 import System
.Posix
.Files
(ownerModes
, setFileMode
)
91 import Codec
.Text
.IConv
(convert
)
95 import qualified Magic
98 getTermSize
:: IO (Int,Int)
100 Size
.Window height width
<- fromMaybe (Size
.Window
(2^
(30::Int)) 80) <$> Size
.size
101 return (height
,width
)
103 lineClient
:: ClientOptions
-> [String] -> Bool -> HL
.InputT ClientM
()
104 lineClient cOpts
@ClientOptions
{ cOptUserDataDir
= userDataDir
105 , cOptInteractive
= interactive
, cOptAnsi
= ansi
, cOptGhost
= ghost
} initialCommands repl
= do
106 (liftIO
. readFileLines
$ userDataDir
</> "diohscrc") >>= mapM_ (handleLine
' . T
.unpack
)
107 lift addToQueuesFromFiles
108 mapM_ handleLine
' initialCommands
109 when repl lineClient
'
110 unless ghost
$ lift appendQueuesToFiles
112 handleLine
' :: String -> HL
.InputT ClientM
Bool
113 handleLine
' line
= lift get
>>= \s
-> handleLine cOpts s line
115 lineClient
' :: HL
.InputT ClientM
()
117 cmd
<- lift getPrompt
>>= promptLineInputT
119 Nothing
-> if interactive
120 then printErrFancy ansi
"Use \"quit\" to quit" >> return False
122 Just Nothing
-> return True
123 Just
(Just line
) -> handleLine
' line
124 lift addToQueuesFromFiles
125 unless quit lineClient
'
127 addToQueuesFromFiles
:: ClientM
()
128 addToQueuesFromFiles | ghost
= return ()
130 qfs
<- ignoreIOErr
$ liftIO findQueueFiles
131 forM_ qfs
$ \(qfile
, qname
) ->
132 modifyQueues
. enqueue
(QueueSpec qname Nothing
) <=<
133 ignoreIOErr
. liftIO
$
134 mapMaybe queueLine
<$> readFileLines qfile
<* removeFile qfile
135 ignoreIOErr
. liftIO
$ removeDirectory queuesDir
137 findQueueFiles
:: IO [(FilePath,String)]
139 qf
<- (\e
-> [(queueFile
, "") | e
]) <$> doesFileExist queueFile
140 qfs
<- ((\qn
-> (queuesDir
</> qn
, qn
)) <$>) <$> listDirectory queuesDir
142 queueLine
:: T
.Text
-> Maybe QueueItem
143 queueLine s
= QueueURI Nothing
<$> (parseUriAsAbsolute
. escapeIRI
$ T
.unpack s
)
145 appendQueuesToFiles
:: ClientM
()
146 appendQueuesToFiles
= do
147 queues
<- gets
$ M
.toList
. clientQueues
148 liftIO
$ createDirectoryIfMissing
True queuesDir
149 liftIO
$ forM_ queues appendQueue
151 appendQueue
(_
, []) = pure
()
152 appendQueue
(qname
, queue
) =
153 let qfile
= case qname
of
156 in warnIOErr
$ BL
.appendFile qfile
.
157 T
.encodeUtf8
. T
.unlines $ T
.pack
. show . queueUri
<$> queue
159 queueFile
, queuesDir
:: FilePath
160 queueFile
= userDataDir
</> "queue"
161 queuesDir
= userDataDir
</> "queues"
163 getPrompt
:: ClientM
String
165 queue
<- gets
$ M
.findWithDefault
[] "" . clientQueues
166 curr
<- gets clientCurrent
167 proxies
<- gets
$ clientConfProxies
. clientConfig
168 ais
<- gets clientActiveIdentities
169 let queueStatus
:: Maybe String
170 queueStatus
= guard (not $ null queue
) >> return (show (length queue
) ++ "~")
171 colour
= applyIf ansi
. withColourStr
172 bold
= applyIf ansi withBoldStr
173 uriStatus
:: Int -> URI
-> String
175 let fullUriStr
= stripGem
$ show uri
176 stripGem s
= fromMaybe s
$ stripPrefix
"gemini://" s
177 mIdName
= (identityName
<$>) $ findIdentity ais
=<< requestOfProxiesAndUri proxies uri
178 idStr
= flip (maybe "") mIdName
$ \idName
->
179 let abbrId
= length idName
> 8 && length fullUriStr
+ 2 + length idName
> w
- 2
180 in "[" ++ (if abbrId
then ".." ++ take 6 idName
else idName
) ++ "]"
181 abbrUri
= length fullUriStr
+ length idStr
> w
- 2
182 uriFormat
= colour BoldMagenta
185 let abbrUriChars
= w
- 4 - length idStr
186 preChars
= abbrUriChars `
div`
2
187 postChars
= abbrUriChars
- preChars
188 in uriFormat
(take preChars fullUriStr
) <>
190 uriFormat
(drop (length fullUriStr
- postChars
) fullUriStr
)
191 else uriFormat fullUriStr
193 (if null idStr
then "" else colour Green idStr
)
194 prompt
:: Int -> String
195 prompt maxPromptWidth
=
196 ((applyIf ansi withReverseStr
$ colour BoldCyan
"%%%") ++)
197 . (" " ++) . (++ bold
"> ") . unwords $ catMaybes
199 , uriStatus
(maxPromptWidth
- 5 - maybe 0 ((+1) . length) queueStatus
)
200 . historyUri
<$> curr
202 prompt
. min 40 . (`
div`
2) . snd <$> liftIO getTermSize
204 handleLine
:: ClientOptions
-> ClientState
-> String -> HL
.InputT ClientM
Bool
205 handleLine cOpts
@ClientOptions
{ cOptAnsi
= ansi
} s line
= handle backupHandler
. catchInterrupts
$ case parseCommandLine line
of
206 Left err
-> printErrFancy ansi err
>> return False
207 Right
(CommandLine Nothing
(Just
(c
,_
))) | c `
isPrefixOf`
"quit" -> return True
208 Right cline
-> handleCommandLine cOpts s
False cline
>> return False
210 catchInterrupts
= HL
.handleInterrupt
(printErrFancy ansi
"Interrupted." >> return False) . HL
.withInterrupt
. lift
211 backupHandler
:: SomeException
-> HL
.InputT ClientM
Bool
212 backupHandler
= (>> return False) . printErrFancy ansi
. ("Unhandled exception: " <>) . show
214 handleCommandLine
:: ClientOptions
-> ClientState
-> Bool -> CommandLine
-> ClientM
()
216 cOpts
@(ClientOptions userDataDir interactive ansi ghost restrictedMode requestContext logH
)
217 cState
@(ClientState curr jumpBack cLog visited queues _ marks sessionMarks aliases
218 (ClientConfig defaultAction proxies geminators renderFilter preOpt linkDescFirst maxLogLen maxWrapWidth confNoConfirm verboseConnection
))
220 = \(CommandLine mt mcas
) -> case mcas
of
221 Just
(c
,args
) | Just
(Alias _
(CommandLine mt
' mcas
')) <- lookupAlias c aliases
->
222 let mcas
'' = (, drop 1 args
) . commandArgArg
<$> headMay args
223 appendCArgs
(c
',as') = (c
', (appendTail
<$> as') ++ args
)
225 appendTail arg
@(CommandArg a
' t
') = case args
of
227 (CommandArg _ t
: _
) -> CommandArg a
' $ t
' <> " " <> t
228 in handleCommandLine
' (mt
' `mplus` mt
) $
229 (appendCArgs
<$> mcas
') `mplus` mcas
''
230 _
-> handleCommandLine
' mt mcas
234 -- Remark: we handle haskell's "configurations problem" by having the below all within the scope
235 -- of the ClientOptions parameter. This is nicer to my mind than the heavier approaches of
236 -- simulating global variables or threading a Reader monad throughout. The downside is that this
237 -- module can't be split as much as it ought to be.
238 -- Similar remarks go for `GeminiProtocol.makeRequest`.
240 onRestriction
:: IO ()
241 onRestriction
= printErr
"This is not allowed in restricted mode."
243 doRestricted
:: Monoid a
=> RestrictedIO a
-> IO a
244 doRestricted m | restrictedMode
= onRestriction
>> return mempty
245 |
otherwise = RunExternal
.runRestrictedIO m
247 doRestrictedAlt
:: Alternative f
=> RestrictedIO
(f a
) -> IO (f a
)
248 doRestrictedAlt m | restrictedMode
= onRestriction
>> return empty
249 |
otherwise = RunExternal
.runRestrictedIO m
251 doRestrictedFilter
:: (a
-> RestrictedIO a
) -> (a
-> IO a
)
252 doRestrictedFilter f | restrictedMode
= \a -> do
255 |
otherwise = RunExternal
.runRestrictedIO
. f
257 printErr
, printInfo
:: MonadIO m
=> String -> m
()
258 printErr
= printErrFancy ansi
259 printInfo
= printInfoFancy ansi
261 printIOErr
:: IOError -> IO ()
262 printIOErr
= printErr
. show
265 noConfirm
= confNoConfirm ||
not interactive
267 confirm
:: Applicative m
=> m
Bool -> m
Bool
268 confirm | noConfirm
= const $ pure
True
271 promptYN
= Prompt
.promptYN interactive
273 colour
:: MetaString a
=> Colour
-> a
-> a
274 colour
= applyIf ansi
. withColourStr
275 bold
:: MetaString a
=> a
-> a
276 bold
= applyIf ansi withBoldStr
278 isVisited
:: URI
-> Bool
279 isVisited uri
= S
.member
(hash
$ show uri
) visited
281 requestOfUri
= requestOfProxiesAndUri proxies
283 idAtUri
:: ActiveIdentities
-> URI
-> Maybe Identity
284 idAtUri ais uri
= findIdentity ais
=<< requestOfUri uri
286 showUriRefFull
:: MetaString a
=> Bool -> ActiveIdentities
-> URI
-> URIRef
-> a
287 showUriRefFull ansi
' ais base ref
= showUriFull ansi
' ais
(Just base
) $ relativeTo ref base
289 showUriFull
:: MetaString a
=> Bool -> ActiveIdentities
-> Maybe URI
-> URI
-> a
290 showUriFull ansi
' ais base uri
=
291 let scheme
= uriScheme uri
292 handled
= scheme `
elem`
["gemini","file"] || M
.member scheme proxies
293 inHistory
= isJust $ curr
>>= flip pathItemByUri uri
294 activeId
= isJust $ idAtUri ais uri
295 col
= if inHistory
&& not activeId
then BoldBlue
else case (isVisited uri
,handled
) of
296 (True,True) -> Yellow
297 (False,True) -> BoldYellow
299 (False,False) -> BoldRed
302 Just b
-> show $ relativeFrom uri b
303 in fromString
(applyIf ansi
' (withColourStr col
) s
) <> case idAtUri ais uri
of
305 Just ident
-> showIdentity ansi
' ident
307 displayUri
:: MetaString a
=> URI
-> a
308 displayUri
= colour Yellow
. fromString
. show
310 showUri
:: URI
-> ClientM
()
312 ais
<- gets clientActiveIdentities
313 liftIO
. putStrLn $ showUriFull ansi ais Nothing uri
315 addToLog
:: URI
-> ClientM
()
317 let t
= T
.pack
$ show uri
319 { clientLog
= BStack
.push maxLogLen t
$ clientLog s
320 , clientVisited
= S
.insert (hash
$ show uri
) $ clientVisited s
}
321 unless ghost
. liftIO
$ maybe (return ()) (ignoreIOErr
. (`T
.hPutStrLn` t
)) logH
323 preprocessQuery
:: String -> ClientM
String
324 preprocessQuery
= (escapeQuery
<$>) . liftIO
. maybeEdit
326 maybeEdit
:: String -> IO String
327 maybeEdit s | lastMay s
== Just
'\\' =
328 handle
(\e
-> printIOErr e
>> pure s
)
329 . doRestrictedFilter
(editInteractively ansi userDataDir
)
333 expand
:: String -> String
334 expand
= expandHelp ansi
(fst <$> aliases
) userDataDir
336 idsPath
= userDataDir
</> "identities"
337 savesDir
= userDataDir
</> "saves"
338 marksDir
= userDataDir
</> "marks"
340 historyEnv
:: HistoryItem
-> ClientM
[(String,String)]
341 historyEnv
item = gets clientActiveIdentities
>>= \ais
-> pure
$
342 [ ("URI", show $ historyUri
item)
343 , ("MIMETYPE", showMimeType
$ historyMimedData
item) ] <>
344 (maybe [] (identityEnvironment idsPath
) . idAtUri ais
$ historyUri
item)
346 setMark
:: String -> URIWithIdName
-> ClientM
()
347 setMark mark uriId | markNameValid mark
= do
348 modify
$ \s
-> s
{ clientMarks
= insertMark mark uriId
$ clientMarks s
}
349 unless (mark `
elem` tempMarks
) . liftIO
.
350 handle printIOErr
$ saveMark marksDir mark uriId
351 setMark mark _
= printErr
$ "Invalid mark name " ++ mark
353 promptInput
= if ghost
then promptLine
else promptLineWithHistoryFile inputHistPath
354 where inputHistPath
= userDataDir
</> "inputHistory"
356 handleCommandLine
' :: Maybe PTarget
-> Maybe (String, [CommandArg
]) -> ClientM
()
357 handleCommandLine
' mt mcas
= void
. runMaybeT
$ do
360 Just pt
-> either ((>> mzero
) . printErr
)
361 (\ts
-> mapM_ addTargetId ts
>> return ts
) $
362 resolveTarget cState pt
364 Nothing
-> lift
$ handleBareTargets ts
366 c
' <- maybe (printErr
(unknownCommand s
) >> mzero
)
367 return $ normaliseCommand s
368 lift
$ handleCommand ts
(c
',as)
370 unknownCommand s
= "Unknown command \"" <> s
<> "\". Type \"help\" for help."
371 addTargetId
:: Target
-> MaybeT ClientM
()
372 addTargetId
(TargetIdUri idName uri
) =
373 liftIO
(loadIdentity idsPath idName
) >>= (\case
374 (Nothing
, _
) -> printErr
("Bad URI: " ++ show uri
) >> mzero
375 (_
, Nothing
) -> printErr
("Unknown identity: " ++ showIdentityName ansi idName
) >> mzero
376 (Just req
, Just ident
) -> lift
$ addIdentity req ident
) . (requestOfUri uri
,)
377 addTargetId _
= return ()
379 doPage
:: [T
.Text
] -> ClientM
()
382 (height
,width
) <- liftIO getTermSize
383 let pageWidth
= min maxWrapWidth
(width
- 4)
384 perPage
= height
- min 3 (height `
div`
4)
385 doCmd str
= get
>>= \s
-> doSubCommand s
True str
386 printLinesPaged pageWidth width perPage doCmd ls
387 |
otherwise = liftIO
$ mapM_ T
.putStrLn ls
389 handleBareTargets
:: [Target
] -> ClientM
()
390 handleBareTargets
[] = return ()
391 handleBareTargets
(_
:_
:_
) =
392 printErr
"Can only go to one place at a time. Try \"show\" or \"page\"?"
393 handleBareTargets
[TargetHistory
item] = goHistory
item
394 handleBareTargets
[TargetFrom origin uri
] = goUri
False (Just origin
) uri
395 handleBareTargets
[t
] = goUri
False Nothing
$ targetUri t
398 handleCommand
:: [Target
] -> (String, [CommandArg
]) -> ClientM
()
399 handleCommand _
(c
,_
) | restrictedMode
&& notElem c
(commands
True) =
400 printErr
"Command disabled in restricted mode"
401 handleCommand
[] ("help", args
) = case args
of
402 [] -> doPage
. map (T
.pack
. expand
) $ helpText
403 CommandArg s _
: _
-> doPage
. map (T
.pack
. expand
) $ helpOn s
404 handleCommand
[] ("commands",_
) = doPage
$ T
.pack
. expand
<$> commandHelpText
406 commandHelpText
= ["Aliases:"] ++ (showAlias
<$> aliases
) ++
407 ["","Commands:"] ++ (('{' :) . (<> "}") <$> commands
False)
408 showAlias
(a
, Alias s _
) = "{" <> a
<> "}: " <> s
410 handleCommand
[] ("mark", []) =
411 let ms
= M
.toAscList marks
412 markLine
(m
, uriId
) = let m
' = showMinPrefix ansi
(fst <$> ms
) m
413 in T
.pack
$ "'" <> m
' <>
414 replicate (max 1 $ 16 - visibleLength
(T
.pack m
')) ' ' <>
416 in doPage
$ markLine
<$> ms
417 handleCommand
[] ("inventory",_
) = do
418 ais
<- gets clientActiveIdentities
419 let showNumberedUri
:: Bool -> T
.Text
-> (Int,URI
) -> T
.Text
420 showNumberedUri iter s
(n
,uri
) = s
<>
421 (if iter
&& n
== 1 then " "
422 else if iter
&& n
== 2 then T
.takeEnd
1 s
423 else T
.pack
(show n
)) <>
424 " " <> showUriFull ansi ais Nothing uri
425 showIteratedItem s
(n
,item) = showNumberedUri
True s
(n
, historyUri
item)
426 showNumberedItem s
(n
,item) = showNumberedUri
False s
(n
, historyUri
item)
427 showIteratedQueueItem s
(n
, QueueURI _ uri
) =
428 showNumberedUri
True s
(n
, uri
)
429 showIteratedQueueItem s
(n
, QueueHistory
item) =
430 showNumberedUri
True s
(n
, historyUri
item) <> " {fetched}"
431 showJumpBack
:: [T
.Text
]
432 showJumpBack
= maybeToList $ ("'' " <>) . showUriFull ansi ais Nothing
. historyUri
<$> jumpBack
433 doPage
. intercalate
[""] . filter (not . null) $
435 [ showIteratedQueueItem
(T
.pack
$ qname
<> "~") <$> zip [1..] queue
436 |
(qname
, queue
) <- M
.toList queues
]
437 ++ [ showNumberedItem
"'" <$> M
.toAscList sessionMarks
438 , (showIteratedItem
"<" <$> zip [1..] (maybe [] (initSafe
. historyAncestors
) curr
))
439 ++ (("@ " <>) . showUriFull ansi ais Nothing
. historyUri
<$>
440 maybeToList (curr
>>= lastMay
. historyAncestors
))
441 , showIteratedItem
">" <$> zip [1..] (maybe [] historyDescendants curr
)
443 handleCommand
[] ("log",_
) =
444 let showLog
(n
,t
) = "$" <> T
.pack
(show n
) <> " " <> colour Yellow t
445 in doPage
$ showLog
<$> zip [(1::Int)..] (BStack
.toList cLog
)
446 handleCommand
[] ("alias", CommandArg a _
: CommandArg _ str
: _
) = void
. runMaybeT
$ do
447 c
<- either ((>>mzero
) . printErr
) return $ parseCommand a
448 when (c
/= a
) $ printErr
("Bad alias: " <> a
) >> mzero
449 cl
<- either ((>>mzero
) . printErr
) return $ parseCommandLine str
451 s
{ clientAliases
= insertAlias a
(Alias str cl
) . deleteAlias a
$ clientAliases s
}
452 handleCommand
[] ("alias", [CommandArg a _
]) =
453 modify
$ \s
-> s
{ clientAliases
= deleteAlias a
$ clientAliases s
}
454 handleCommand
[] ("set", []) = liftIO
$ do
455 let (c
,args
) = defaultAction
456 putStrLn $ expand
"{default_action}: " <> c
<>
457 maybe "" ((" "<>) . commandArgLiteralTail
) (headMay args
)
458 putStrLn $ expand
"{proxies}:"
459 printMap showHost
$ M
.toAscList proxies
460 putStrLn $ expand
"{geminators}:"
461 printMap
id $ second
snd <$> geminators
462 putStrLn $ expand
"{render_filter}: " <> fromMaybe "" renderFilter
463 putStrLn $ expand
"{pre_display}: " <> showPreOpt preOpt
464 putStrLn $ expand
"{link_desc_first}: " <> show linkDescFirst
465 putStrLn $ expand
"{log_length}: " <> show maxLogLen
466 putStrLn $ expand
"{max_wrap_width}: " <> show maxWrapWidth
467 putStrLn $ expand
"{no_confirm}: " <> show noConfirm
468 putStrLn $ expand
"{verbose_connection}: " <> show verboseConnection
470 printMap
:: (a
-> String) -> [(String,a
)] -> IO ()
471 printMap f
as = mapM_ putStrLn $
472 (\(k
,e
) -> " " <> k
<> " " <> f e
) <$> as
473 handleCommand
[] ("set", CommandArg opt _
: val
)
474 | opt `
isPrefixOf`
"default_action" = case val
of
475 (CommandArg cmd _
: args
) -> case actionOfCommand
(cmd
,args
) of
476 Just _
-> modifyCConf
$ \c
-> c
{ clientConfDefaultAction
= (cmd
,args
) }
477 Nothing
-> printErr
"Invalid action"
478 _
-> printErr
"Require value for option."
479 | opt `
isPrefixOf`
"proxies" || opt `
isPrefixOf`
"proxy" = case val
of
480 (CommandArg scheme _
: val
') ->
481 let f
= maybe (M
.delete scheme
) (M
.insert scheme
) $
482 parseHost
. commandArgLiteralTail
=<< headMay val
'
483 in modifyCConf
$ \c
-> c
{ clientConfProxies
= f
$ clientConfProxies c
}
484 -- if only I'd allowed myself to use lenses, eh?
485 [] -> printErr
"Require mimetype to set geminator for."
486 | opt `
isPrefixOf`
"geminators" = case val
of
487 (CommandArg patt _
: val
') ->
488 let f
= maybe (filter $ (/= patt
) . fst)
489 (\v -> (++ [(patt
, (mkRegexWithOpts patt
True True,
490 commandArgLiteralTail v
))])) $
492 in modifyCConf
$ \c
-> c
{ clientConfGeminators
= f
$ clientConfGeminators c
}
493 [] -> printErr
"Require mimetype to set geminator for."
494 | opt `
isPrefixOf`
"render_filter" =
495 modifyCConf
$ \c
-> c
{ clientConfRenderFilter
=
496 commandArgLiteralTail
<$> headMay val
}
497 | opt `
isPrefixOf`
"pre_display" = case val
of
498 [CommandArg s _
] |
map toLower s `
isPrefixOf`
"both" ->
499 modifyCConf
$ \c
-> c
{ clientConfPreOpt
= PreOptBoth
}
500 [CommandArg s _
] |
map toLower s `
isPrefixOf`
"pre" ->
501 modifyCConf
$ \c
-> c
{ clientConfPreOpt
= PreOptPre
}
502 [CommandArg s _
] |
map toLower s `
isPrefixOf`
"alt" ->
503 modifyCConf
$ \c
-> c
{ clientConfPreOpt
= PreOptAlt
}
504 _
-> printErr
"Require \"both\" or \"pre\" or \"alt\" for pre_display"
505 | opt `
isPrefixOf`
"link_desc_first" = case val
of
506 [CommandArg s _
] |
map toLower s `
isPrefixOf`
"true" ->
507 modifyCConf
$ \c
-> c
{ clientConfLinkDescFirst
= True }
508 [CommandArg s _
] |
map toLower s `
isPrefixOf`
"false" ->
509 modifyCConf
$ \c
-> c
{ clientConfLinkDescFirst
= False }
510 _
-> printErr
"Require \"true\" or \"false\" as value for link_desc_first"
511 | opt `
isPrefixOf`
"log_length" = case val
of
512 [CommandArg s _
] | Just n
<- readMay s
, n
>= 0 -> do
513 modifyCConf
$ \c
-> c
{ clientConfMaxLogLen
= n
}
514 modify
$ \st
-> st
{ clientLog
= BStack
.truncate n
$ clientLog st
}
515 _
-> printErr
"Require non-negative integer value for log_length"
516 | opt `
isPrefixOf`
"max_wrap_width" = case val
of
517 [CommandArg s _
] | Just n
<- readMay s
, n
> 0 ->
518 modifyCConf
$ \c
-> c
{ clientConfMaxWrapWidth
= n
}
519 _
-> printErr
"Require positive integer value for max_wrap_width"
520 | opt `
isPrefixOf`
"no_confirm" = case val
of
521 [CommandArg s _
] |
map toLower s `
isPrefixOf`
"true" ->
522 modifyCConf
$ \c
-> c
{ clientConfNoConfirm
= True }
523 [CommandArg s _
] |
map toLower s `
isPrefixOf`
"false" ->
524 modifyCConf
$ \c
-> c
{ clientConfNoConfirm
= False }
525 _
-> printErr
"Require \"true\" or \"false\" as value for no_confirm"
526 | opt `
isPrefixOf`
"verbose_connection" = case val
of
527 [CommandArg s _
] |
map toLower s `
isPrefixOf`
"true" ->
528 modifyCConf
$ \c
-> c
{ clientConfVerboseConnection
= True }
529 [CommandArg s _
] |
map toLower s `
isPrefixOf`
"false" ->
530 modifyCConf
$ \c
-> c
{ clientConfVerboseConnection
= False }
531 _
-> printErr
"Require \"true\" or \"false\" as value for verbose_connection"
532 |
otherwise = printErr
$ "No such option \"" <> opt
<> "\"."
533 handleCommand
[] cargs
=
535 Just
item -> handleCommand
[TargetHistory
item] cargs
536 Nothing
-> printErr
"No current location. Enter an URI, or type \"help\"."
538 handleCommand ts
("add", args
) = case parseQueueSpec
$ commandArgArg
<$> args
of
539 Nothing
-> printErr
"Bad arguments to 'add'."
540 Just qs
-> modifyQueues
. enqueue qs
$ targetQueueItem
<$> ts
542 handleCommand ts
("fetch", args
) = case parseQueueSpec
$ commandArgArg
<$> args
of
543 Nothing
-> printErr
"Bad arguments to 'fetch."
545 -- XXX: we have to use an IORef to store the items, since
546 -- CommandAction doesn't allow a return value.
547 lRef
<- liftIO
$ newIORef
[]
548 let add
item = liftIO
$ slurpItem
item >> modifyIORef lRef
(item:)
549 forM_ ts
$ \t -> case t
of
550 TargetHistory
item -> add
item
551 _
-> modifyQueues
(unqueue uri
) >> doRequestUri uri add
552 where uri
= targetUri t
553 l
<- liftIO
$ reverse <$> readIORef lRef
554 modifyQueues
. enqueue qs
$ QueueHistory
<$> l
556 handleCommand ts cargs
=
557 mapM_ handleTargetCommand ts
559 handleTargetCommand
(TargetHistory
item) | Just action
<- actionOfCommand cargs
=
561 handleTargetCommand t | Just action
<- actionOfCommand cargs
=
562 let uri
= targetUri t
563 in modifyQueues
(unqueue uri
) >> doRequestUri uri action
564 handleTargetCommand
(TargetHistory
item) |
("repeat",_
) <- cargs
=
565 goUri
True (recreateOrigin
<$> historyParent
item) $ historyUri
item
566 handleTargetCommand
(TargetHistory
item) |
("repl",_
) <- cargs
=
567 repl
(recreateOrigin
<$> historyParent
item) $ historyUri
item
568 handleTargetCommand t |
("query", CommandArg _ str
: _
) <- cargs
= do
569 let origin
= case t
of
570 TargetHistory
item -> Just
$ HistoryOrigin
item Nothing
571 TargetFrom o _
-> Just o
573 str
' <- preprocessQuery str
574 goUri
True origin
. setQuery
('?
':str
') $ targetUri t
575 handleTargetCommand t
=
576 handleUriCommand
(targetUri t
) cargs
578 recreateOrigin
:: HistoryItem
-> HistoryOrigin
579 recreateOrigin parent
= HistoryOrigin parent
$ childLink
=<< historyChild parent
581 handleUriCommand uri
("delete",[]) =
582 modifyQueues
$ unqueueFrom
"" uri
583 handleUriCommand uri
("delete",CommandArg qname _
: _
) =
584 modifyQueues
$ unqueueFrom qname uri
585 handleUriCommand uri
("repeat",_
) = goUri
True Nothing uri
586 handleUriCommand uri
("uri",_
) = showUri uri
587 handleUriCommand uri
("mark", CommandArg mark _
: _
)
588 | Just _
<- readMay mark
:: Maybe Int = error "Bad mark for uri"
590 ais
<- gets clientActiveIdentities
591 let mIdName
= case findIdentity ais
=<< requestOfUri uri
of
592 Just ident |
not $ isTemporary ident
-> Just
$ identityName ident
594 setMark mark
$ URIWithIdName uri mIdName
595 handleUriCommand uri
("identify", args
) = case requestOfUri uri
of
596 Nothing
-> printErr
"Bad URI"
597 Just req
-> gets
((`findIdentityRoot` req
) . clientActiveIdentities
) >>= \case
598 Just
(root
,(ident
,_
)) |
null args
-> endIdentityPrompted root ident
599 _
-> void
. runMaybeT
$ do
600 ident
<- MaybeT
. liftIO
$ case args
of
601 CommandArg idName _
: args
' ->
602 let tp
= case args
' of
603 CommandArg
('e
':'d
':_
) _
: _
-> KeyEd25519
605 in getIdentity interactive ansi idsPath tp idName
607 then getIdentityRequesting ansi idsPath
608 else getIdentity interactive ansi idsPath KeyRSA
""
609 lift
$ addIdentity req ident
610 handleUriCommand uri
("browse", args
) = do
611 ais
<- gets clientActiveIdentities
612 let envir
= maybe [] (identityEnvironment idsPath
) .
613 idAtUri ais
. setSchemeDefault
$ uri
614 void
. liftIO
. runMaybeT
$ do
617 (\s
-> if null s
then notSet
else return $ parseBrowser s
) =<<
618 lift
(lookupEnv
"BROWSER")
620 notSet
= printErr
"Please set $BROWSER or give a command to run" >> mzero
621 -- |based on specification for $BROWSER in 'man 1 man'
622 parseBrowser
:: String -> String
623 parseBrowser
= subPercentOrAppend
(show uri
) . takeWhile (/=':')
624 (CommandArg _ c
: _
) -> return $ subPercentOrAppend
(show uri
) c
625 lift
$ confirm
(confirmShell
"Run" cmd
) >>? doRestricted
$ void
(runShellCmd cmd envir
)
626 handleUriCommand uri
("repl",_
) = repl Nothing uri
627 handleUriCommand uri
("log",_
) = addToLog uri
>> modifyQueues
(unqueue uri
)
629 handleUriCommand _
(c
,_
) = printErr
$ "Bad arguments to command " <> c
631 repl
:: Maybe HistoryOrigin
-> URI
-> ClientM
()
632 repl origin uri
= repl
' where
633 repl
' = liftIO
(join <$> promptInput
">> ") >>= \case
636 query
' <- preprocessQuery query
637 goUri
True origin
. setQuery
('?
':query
') $ uri
640 slurpItem
:: HistoryItem
-> IO ()
641 slurpItem
item = slurpNoisily
(historyRequestTime
item) . mimedBody
$ historyMimedData
item
643 actionOnRendered
:: Bool -> ([T
.Text
] -> ClientM
()) -> CommandAction
644 actionOnRendered ansi
' m
item = do
645 ais
<- gets clientActiveIdentities
646 liftIO
(renderMimed ansi
' (historyUri
item) ais
(historyGeminatedMimedData
item)) >>=
649 actionOfCommand
:: (String, [CommandArg
]) -> Maybe CommandAction
650 actionOfCommand
(c
,_
) | restrictedMode
&& notElem c
(commands
True) = Nothing
651 actionOfCommand
("show",_
) = Just
. actionOnRendered ansi
$ liftIO
. mapM_ T
.putStrLn
652 actionOfCommand
("page",_
) = Just
$ actionOnRendered ansi doPage
653 actionOfCommand
("links",_
) = Just
$ \item -> do
654 ais
<- gets clientActiveIdentities
655 let cl
= childLink
=<< historyChild
item
656 linkLine n
(Link uri desc
) =
657 applyIf
(cl
== Just
(n
-1)) (bold
"* " <>) $
658 T
.pack
('[' : show n
++ "] ")
659 <> showUriRefFull ansi ais
(historyUri
item) uri
660 <> if T
.null desc
then "" else " " <>
661 applyIf ansi
(withColourStr Cyan
) desc
662 doPage
. zipWith linkLine
[1..] . historyLinks
$ item
664 actionOfCommand
("mark", CommandArg mark _
: _
) |
665 Just n
<- readMay mark
:: Maybe Int = Just
$ \item -> do
666 liftIO
$ slurpItem
item
667 modify
$ \s
-> s
{ clientSessionMarks
= M
.insert n
item $ clientSessionMarks s
}
668 actionOfCommand
("mime",_
) = Just
$ liftIO
. putStrLn . showMimeType
. historyMimedData
669 actionOfCommand
("save", []) = actionOfCommand
("save", [CommandArg
"" savesDir
])
670 actionOfCommand
("save", CommandArg _ path
: _
) = Just
$ \item -> liftIO
. doRestricted
. RestrictedIO
$ do
671 createDirectoryIfMissing
True savesDir
672 homePath
<- getHomeDirectory
674 |
take 2 path
== "~/" = homePath
</> drop 2 path
675 |
take 1 path
== "/" ||
take 2 path
== "./" ||
take 3 path
== "../" = path
676 |
otherwise = savesDir
</> path
677 body
= mimedBody
$ historyMimedData
item
678 uri
= historyUri
item
679 name
= fromMaybe (fromMaybe "" $ uriRegName uri
) . lastMay
.
680 filter (not . null) $ pathSegments uri
681 handle printIOErr
. void
. runMaybeT
$ do
682 lift
$ mkdirhierto path
'
683 isDir
<- lift
$ doesDirectoryExist path
'
684 let fullpath
= if isDir
then path
' </> name
else path
'
685 lift
(doesDirectoryExist fullpath
) >>?
do
686 lift
. printErr
$ "Path " ++ show fullpath
++ " exists and is directory"
688 lift
(doesFileExist fullpath
) >>?
689 guard =<< lift
(promptYN
False $ "Overwrite " ++ show fullpath
++ "?")
691 putStrLn $ "Saving to " ++ fullpath
693 BL
.writeFile fullpath
=<< interleaveProgress t0 body
695 actionOfCommand
("!", CommandArg _ cmd
: _
) = Just
$ \item -> do
696 env
<- historyEnv
item
697 liftIO
. handle printIOErr
. doRestricted
.
698 shellOnData noConfirm cmd userDataDir env
. mimedBody
$ historyMimedData
item
700 actionOfCommand
("view",_
) = Just
$ \item ->
701 let mimed
= historyMimedData
item
702 mimetype
= showMimeType mimed
703 body
= mimedBody mimed
704 in liftIO
. handle printIOErr
. doRestricted
$ runMailcap noConfirm
"view" userDataDir mimetype body
705 actionOfCommand
("|", CommandArg _ cmd
: _
) = Just
$ \item -> do
706 env
<- historyEnv
item
707 liftIO
. handle printIOErr
. doRestricted
$
708 pipeToShellLazily cmd env
. mimedBody
$ historyMimedData
item
709 actionOfCommand
("||", args
) = Just
$ pipeRendered ansi args
710 actionOfCommand
("||-", args
) = Just
$ pipeRendered
False args
711 actionOfCommand
("cat",_
) = Just
$ liftIO
. BL
.putStr . mimedBody
. historyMimedData
712 actionOfCommand
("at", CommandArg _ str
: _
) = Just
$ \item ->
713 doSubCommand
(cState
{ clientCurrent
= Just
item }) blockGo str
714 actionOfCommand _
= Nothing
716 pipeRendered
:: Bool -> [CommandArg
] -> CommandAction
717 pipeRendered ansi
' args
item = (\action
-> actionOnRendered ansi
' action
item) $ \ls
-> do
718 env
<- historyEnv
item
719 liftIO
. void
. runMaybeT
$ do
722 (\s
-> if null s
then notSet
else return s
) =<<
723 liftIO
(lookupEnv
"PAGER")
725 notSet
= printErr
"Please set $PAGER or give a command to run" >> mzero
726 (CommandArg _ cmd
: _
) -> return cmd
727 lift
. doRestricted
. pipeToShellLazily cmd env
. T
.encodeUtf8
$ T
.unlines ls
729 doSubCommand
:: ClientState
-> Bool -> String -> ClientM
()
730 doSubCommand s block str
= void
. runMaybeT
$ do
731 cl
<- either ((>>mzero
) . printErr
) return $ parseCommandLine str
732 lift
$ handleCommandLine cOpts s block cl
734 setCurr
:: HistoryItem
-> ClientM
()
736 let isJump
= isNothing $ curr
>>= pathItemByUri i
. historyUri
738 when isJump
$ modify
$ \s
-> s
{ clientJumpBack
= curr
}
739 modify
$ \s
-> s
{ clientCurrent
= Just i
}
741 doDefault
:: HistoryItem
-> ClientM
()
743 maybe (printErr
"Bad default action!") ($ item) $ actionOfCommand defaultAction
745 goHistory
:: HistoryItem
-> ClientM
()
746 goHistory _ | blockGo
= printErr
"Can't go anywhere now."
748 modifyQueues
$ unqueue uri
752 where uri
= historyUri
item
754 goUri
:: Bool -> Maybe HistoryOrigin
-> URI
-> ClientM
()
755 goUri _ _ _ | blockGo
= printErr
"Can't go anywhere now."
756 goUri forceRequest origin uri
= do
757 modifyQueues
$ unqueue uri
758 activeId
<- gets
$ isJust . (`idAtUri` uri
) . clientActiveIdentities
759 case curr
>>= flip pathItemByUri uri
of
760 Just i
' |
not (activeId || forceRequest
) -> goHistory i
'
761 _
-> doRequestUri uri
$ \item -> do
763 -- Lazily recursively update the links in the doubly linked list
764 let i
' = i
{ historyParent
= updateParent
. updateChild i
' <$> historyParent i
}
766 updateChild i
' i
= i
{ historyChild
= setChild
<$> historyChild i
}
767 where setChild c
= c
{ childItem
= i
' }
768 glueOrigin
(HistoryOrigin o l
) = updateParent
$ o
{ historyChild
= Just
$ HistoryChild
item' l
}
769 item' = item { historyParent
= glueOrigin
<$> origin
}
772 liftIO
$ slurpItem
item
774 doRequestUri
:: URI
-> CommandAction
-> ClientM
()
775 doRequestUri uri0 action
= doRequestUri
' 0 uri0
777 doRequestUri
' redirs uri
778 | Just req
<- requestOfUri uri
= addToLog uri
>> doRequest redirs req
779 |
otherwise = printErr
$ "Bad URI: " ++ displayUri uri
++ (
780 let scheme
= uriScheme uri
781 in if scheme
/= "gemini" && isNothing (M
.lookup scheme proxies
)
782 then " : No proxy set for non-gemini scheme " ++ scheme
++ "; use \"browse\"?"
785 doRequest
:: Int -> Request
-> ClientM
()
786 doRequest redirs _ | redirs
> 5 =
787 printErr
"Too many redirections!"
788 doRequest redirs req
@(NetworkRequest _ uri
) = do
789 (mId
, ais
) <- liftIO
. useActiveIdentity noConfirm ansi req
=<< gets clientActiveIdentities
790 modify
$ \s
-> s
{ clientActiveIdentities
= ais
}
791 printInfo
$ ">>> " ++ showUriFull ansi ais Nothing uri
792 let respBuffSize
= 2 ^
(15::Int) -- 32KB max cache for response stream
793 liftIO
(makeRequest requestContext mId respBuffSize verboseConnection req
)
794 `
bracket`
either (\_
-> return ()) (liftIO
. snd) $
796 (printErr
. displayException
)
797 (handleResponse
. fst)
799 handleResponse
:: Response
-> ClientM
()
800 handleResponse
(Input isPass prompt
) = do
801 let defaultPrompt
= "[" ++ (if isPass
then "PASSWORD" else "INPUT") ++ "]"
802 (liftIO
. (join <$>) . promptInput
$
803 (if null prompt
then defaultPrompt
else prompt
) ++ " > ") >>= \case
806 query
' <- preprocessQuery query
807 doRequestUri
' redirs
. setQuery
('?
':query
') $ uri
809 handleResponse
(Success mimedData
) = doAction req mimedData
811 handleResponse
(Redirect isPerm to
) = do
812 ais
<- gets clientActiveIdentities
813 let uri
' = to `relativeTo` uri
814 crossSite
= uriRegName uri
' /= uriRegName uri
815 crossScheme
= uriScheme uri
' /= uriScheme uri
816 crossScope
= case idAtUri ais
<$> [uri
,uri
'] of
817 [fromId
,toId
] -> isJust toId
&& fromId
/= toId
819 warningStr
= colour BoldRed
820 proceed
<- (isJust <$>) . lift
. runMaybeT
$ do
821 when crossSite
$ guard <=< (liftIO
. promptYN
False) $
822 warningStr
"Follow cross-site redirect to " ++ showUriRefFull ansi ais uri to
++ warningStr
"?"
823 when crossScheme
$ guard <=< (liftIO
. promptYN
False) $
824 warningStr
"Follow cross-protocol redirect to " ++ showUriRefFull ansi ais uri to
++ warningStr
"?"
825 when crossScope
$ guard <=< (liftIO
. promptYN
False) $
826 warningStr
"Follow redirect with identity " ++ showUriRefFull ansi ais uri to
++ warningStr
"?"
828 when (isPerm
&& not ghost
) . mapM_ (updateMark uri
') . marksWithUri uri
=<< gets clientMarks
829 doRequestUri
' (redirs
+ 1) uri
'
830 where updateMark uri
' (mark
,uriId
) = do
831 conf
<- confirm
. liftIO
. promptYN
True $ "Update mark '" <> mark
<> " to " <> show uri
' <> " ?"
832 when conf
. setMark mark
$ uriId
{ uriIdUri
= uri
' }
833 handleResponse
(Failure code info
) |
60 <= code
&& code
<= 69 = void
. runMaybeT
$ do
835 liftIO
. putStrLn $ (case code
of
836 60 -> "Server requests identification"
837 _
-> "Server rejects provided identification certificate" ++
838 (if code
== 61 then " as unauthorised" else if code
== 62 then " as invalid" else ""))
839 ++ if null info
then "" else ": " ++ info
841 MaybeT
. liftIO
$ getIdentityRequesting ansi idsPath
843 addIdentity req identity
846 handleResponse
(Failure code info
) =
847 printErr
$ "Server returns failure: " ++ show code
++ " " ++ info
848 handleResponse
(MalformedResponse malformation
) =
849 printErr
$ "Malformed response from server: " ++ show malformation
851 doRequest redirs
(LocalFileRequest path
) | redirs
> 0 = printErr
"Ignoring redirect to local file."
852 |
otherwise = void
. runMaybeT
$ do
853 (path
', mimedData
) <- MaybeT
. liftIO
. doRestrictedAlt
. RestrictedIO
. warnIOErrAlt
$ do
854 let detectExtension
= case takeExtension path
of
855 -- |certain crucial filetypes we can't rely on magic to detect:
856 s | s `
elem`
[".gmi", ".gem", ".gemini"] -> Just
"text/gemini"
857 ".md" -> Just
"text/markdown"
858 ".html" -> Just
"text/html"
861 detectPlain
"text/plain" = fromMaybe "text/plain" detectExtension
863 magic
<- Magic
.magicOpen
[Magic
.MagicMimeType
]
864 Magic
.magicLoadDefault magic
865 s
<- detectPlain
<$> Magic
.magicFile magic path
867 let s
= if "/" `
isSuffixOf` path
then "inode/directory"
868 else fromMaybe "application/octet-stream" detectExtension
870 case MIME
.parseMIMEType
$ TS
.pack s
of
871 Nothing
-> printErr
("Failed to parse mimetype string: " <> s
) >> return Nothing
872 _ | s
== "inode/directory" -> Just
. (slashedPath
,) . MimedData gemTextMimeType
.
873 T
.encodeUtf8
. T
.unlines .
874 ((("=> " <>) . T
.pack
. escapePathString
) <$>) . sort <$>
875 getDirectoryContents path
876 where slashedPath |
"/" `
isSuffixOf` path
= path
877 |
otherwise = path
<> "/"
878 Just mimetype
-> Just
. (path
,) . MimedData mimetype
<$> BL
.readFile path
879 lift
$ doAction
(LocalFileRequest path
') mimedData
881 doAction req mimedData
= do
882 t0
<- liftIO timeCurrentP
883 geminated
<- geminate mimedData
884 action
$ HistoryItem req t0 mimedData geminated Nothing Nothing
886 -- |returns MimedData with lazy IO
887 geminate
:: MimedData
-> ClientM MimedData
889 let geminator
= lookupGeminator
$ showMimeType mimed
890 in liftIO
. unsafeInterleaveIO
$ applyGeminator geminator
892 lookupGeminator mimetype
=
893 listToMaybe [ gem |
(_
, (regex
, gem
)) <- geminators
894 , isJust $ matchRegex regex mimetype
]
895 applyGeminator Nothing
= return mimed
896 applyGeminator
(Just cmd
) =
897 printInfo
("| " <> cmd
) >>
898 MimedData gemTextMimeType
<$>
899 doRestrictedFilter
(filterShell cmd
[("URI", show $ requestUri req
)]) (mimedBody mimed
)
901 gemTextMimeType
:: MIME
.Type
902 gemTextMimeType
= MIME
.Type
(MIME
.Text
"gemini") []
905 addIdentity
:: Request
-> Identity
-> ClientM
()
906 addIdentity req identity
= do
907 ais
<- gets clientActiveIdentities
>>= liftIO
. insertIdentity req identity
908 modify
$ \s
-> s
{clientActiveIdentities
= ais
}
909 endIdentityPrompted
:: Request
-> Identity
-> ClientM
()
910 endIdentityPrompted root ident
= do
911 conf
<- confirm
$ liftIO
. promptYN
False $ "Stop using " ++
912 (if isTemporary ident
then "temporary anonymous identity" else showIdentity ansi ident
) ++
913 " at " ++ displayUri
(requestUri root
) ++ "?"
914 when conf
. modify
$ \s
->
915 s
{ clientActiveIdentities
= deleteIdentity root
$ clientActiveIdentities s
}
917 renderMimed
:: Bool -> URI
-> ActiveIdentities
-> MimedData
-> IO (Either String [T
.Text
])
918 renderMimed ansi
' uri ais
(MimedData mime body
) = case MIME
.mimeType mime
of
919 MIME
.Text textType
-> do
920 let extractCharsetParam
(MIME
.MIMEParam
"charset" v
) = Just v
921 extractCharsetParam _
= Nothing
922 charset
= TS
.unpack
. fromMaybe "utf-8" . msum . map extractCharsetParam
$ MIME
.mimeParams mime
923 isUtf8
= map toLower charset `
elem`
["utf-8", "utf8"]
925 reencoder
= if isUtf8
then id else
926 convert charset
"UTF-8"
929 unless isUtf8
. printErr
$
930 "Warning: Treating unsupported charset " ++ show charset
++ " as utf-8"
932 (_
,width
) <- getTermSize
933 let pageWidth
= if interactive
934 then min maxWrapWidth
(width
- 4)
936 let bodyText
= T
.decodeUtf8With T
.lenientDecode
$ reencoder body
937 applyFilter
:: [T
.Text
] -> IO [T
.Text
]
938 applyFilter
= case renderFilter
of
940 Just cmd
-> (T
.lines . T
.decodeUtf8With T
.lenientDecode
<$>) .
941 doRestrictedFilter
(filterShell cmd
[]) . BL
.concat . (appendNewline
. T
.encodeUtf8
<$>)
942 where appendNewline
= (`BL
.snoc`
10)
943 (Right
<$>) . applyFilter
$ case textType
of
945 let opts
= GemRenderOpts ansi
' preOpt pageWidth linkDescFirst
946 in printGemDoc opts
(showUriRefFull ansi
' ais uri
) $ parseGemini bodyText
947 _
-> T
.stripEnd
. stripControl
<$> T
.lines bodyText
949 return . Left
$ "No geminator for " ++ TS
.unpack
(MIME
.showMIMEType mimeType
) ++ " configured. Try \"save\", \"view\", \"!\", or \"|\"?"