Skip to content

Commit aa2c6aa

Browse files
committed
seito: Strip terminal sequences when output does not go to a tty
1 parent dc78e60 commit aa2c6aa

6 files changed

Lines changed: 115 additions & 26 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ Create a Makefile with the following content:
6565

6666
```Makefile
6767
all:
68-
@seito | sed 's/\x1B\[[0-9;]*[JKmsu]//g'
68+
@seito
6969
```
7070

7171

@@ -75,7 +75,7 @@ Add the following to your Vim configuration (e.g.
7575
`~/.vim/after/ftplugin/haskell.vim`):
7676

7777
```vim
78-
:set makeprg=seito\ \\\|\ sed\ 's/\\x1B\\[[0-9;]*[JKmsu]//g'
78+
:set makeprg=seito
7979
```
8080

8181
### Emacs integration

driver/seito.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
module Main (main) where
22

33
import System.Exit
4+
import System.Environment
45
import Control.Monad
56
import qualified Data.ByteString.Lazy as L
67

78
import Client
89

910
main :: IO ()
1011
main = do
11-
(success, output) <- client ""
12+
(success, output) <- getArgs >>= client ""
1213
L.putStr output
1314
unless success exitFailure

src/Client.hs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module Client (client) where
22

33
import Imports
44

5+
import System.IO
56
import Network.Socket
67
import Network.HTTP.Types
78
import Network.HTTP.Client
@@ -10,12 +11,23 @@ import qualified Data.ByteString.Lazy as L
1011

1112
import HTTP (newSocket, socketName)
1213

13-
client :: FilePath -> IO (Bool, L.ByteString)
14-
client dir = handleSocketFileDoesNotExist name $ do
15-
manager <- newManager defaultManagerSettings {managerRawConnection = return newConnection}
16-
Response{..} <- httpLbs "http://localhost/" manager
17-
return (statusIsSuccessful responseStatus, responseBody)
14+
client :: FilePath -> [String] -> IO (Bool, L.ByteString)
15+
client dir args = case args of
16+
[] -> hIsTerminalDevice stdout >>= run
17+
["--no-color"] -> run False
18+
["--color"] -> run True
19+
_ -> do
20+
hPutStrLn stderr $ "Usage: seito [ --color | --no-color ]"
21+
return (False, "")
1822
where
23+
run color = handleSocketFileDoesNotExist name $ do
24+
manager <- newManager defaultManagerSettings {managerRawConnection = return newConnection}
25+
let
26+
url :: Request
27+
url = fromString $ "http://localhost/?color=" <> map toLower (show color)
28+
Response{..} <- httpLbs url manager
29+
return (statusIsSuccessful responseStatus, responseBody)
30+
1931
name :: FilePath
2032
name = socketName dir
2133

src/HTTP.hs

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ module HTTP (
77

88
#ifdef TEST
99
, app
10+
, stripAnsi
1011
#endif
1112
) where
1213

1314
import Imports
1415

1516
import System.Directory
17+
import qualified Data.ByteString.Lazy as L
1618
import Data.Text.Lazy.Encoding (encodeUtf8)
1719
import Network.Wai
1820
import Network.HTTP.Types
@@ -58,11 +60,46 @@ withThread asyncAction action = do
5860
return r
5961

6062
app :: IO (Trigger.Result, String) -> Application
61-
app trigger _ respond = trigger >>= textPlain
63+
app trigger request respond = trigger >>= textPlain
6264
where
63-
textPlain (result, xs) = respond $ responseLBS status [(hContentType, "text/plain")] (encodeUtf8 . fromString $ xs)
64-
where
65-
status = case result of
66-
Trigger.HookFailed -> internalServerError500
67-
Trigger.Failure -> internalServerError500
68-
Trigger.Success -> ok200
65+
color :: Either ByteString Bool
66+
color = case join $ lookup "color" $ queryString request of
67+
Nothing -> Right True
68+
Just "false" -> Right False
69+
Just "true" -> Right True
70+
Just value -> Left $ "invalid value for color: " <> urlEncode True value
71+
72+
textPlain :: (Trigger.Result, FilePath) -> IO ResponseReceived
73+
textPlain (result, xs) = case color of
74+
Left err -> respond $ responseLBS status400 [(hContentType, "text/plain")] (L.fromStrict err)
75+
Right c -> respond $ responseLBS status [(hContentType, "text/plain")] (encodeUtf8 . fromString $ strip xs)
76+
where
77+
strip :: String -> String
78+
strip
79+
| c = id
80+
| otherwise = stripAnsi
81+
82+
status = case result of
83+
Trigger.HookFailed -> status500
84+
Trigger.Failure -> status500
85+
Trigger.Success -> status200
86+
87+
-- |
88+
-- Remove terminal sequences.
89+
stripAnsi :: String -> String
90+
stripAnsi = go
91+
where
92+
go input = case input of
93+
'\ESC' : '[' : (dropNumericParameters -> c : xs) | isCommand c -> go xs
94+
'\ESC' : '[' : '?' : (dropNumericParameters -> c : xs) | isCommand c -> go xs
95+
x : xs -> x : go xs
96+
[] -> []
97+
98+
dropNumericParameters :: FilePath -> FilePath
99+
dropNumericParameters = dropWhile (`elem` ("0123456789;" :: [Char]))
100+
101+
isCommand :: Char -> Bool
102+
isCommand = (`elem` commands)
103+
104+
commands :: FilePath
105+
commands = ['A'..'Z'] <> ['a'..'z']

test/ClientSpec.hs

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,39 @@ module ClientSpec (spec) where
22

33
import Helper
44

5-
import HTTP
5+
import HTTP (socketName)
6+
import qualified HTTP
67
import Client
78
import qualified Trigger
89

10+
withSuccess :: (FilePath -> IO a) -> IO a
11+
withSuccess = withServer Trigger.Success (withColor Green "success")
12+
13+
withFailure :: (FilePath -> IO a) -> IO a
14+
withFailure = withServer Trigger.Failure (withColor Red "failure")
15+
16+
withServer :: Trigger.Result -> String -> (FilePath -> IO a) -> IO a
17+
withServer result text action = do
18+
withTempDirectory $ \ dir -> do
19+
HTTP.withServer dir (return (result, text)) $ do
20+
action dir
21+
922
spec :: Spec
1023
spec = do
1124
describe "client" $ do
12-
it "does a HTTP request via a Unix domain socket" $ do
13-
withTempDirectory $ \ dir -> do
14-
withServer dir (return (Trigger.Success, "hello")) $ do
15-
client dir `shouldReturn` (True, "hello")
25+
it "accepts --color" $ do
26+
withSuccess $ \ dir -> do
27+
client dir ["--color"] `shouldReturn` (True, fromString $ withColor Green "success")
28+
29+
it "accepts --no-color" $ do
30+
withSuccess $ \ dir -> do
31+
client dir ["--no-color"] `shouldReturn` (True, "success")
1632

1733
it "indicates failure" $ do
18-
withTempDirectory $ \ dir -> do
19-
withServer dir (return (Trigger.Failure, "hello")) $ do
20-
client dir `shouldReturn` (False, "hello")
34+
withFailure $ \ dir -> do
35+
client dir [] `shouldReturn` (False, "failure")
2136

2237
context "when server socket is missing" $ do
2338
it "reports error" $ do
2439
withTempDirectory $ \ dir -> do
25-
client dir `shouldReturn` (False, "could not connect to " <> fromString (socketName dir) <> "\n")
40+
client dir [] `shouldReturn` (False, "could not connect to " <> fromString (socketName dir) <> "\n")

test/HTTPSpec.hs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,41 @@ module HTTPSpec (spec) where
33
import Helper
44

55
import Test.Hspec.Wai
6+
import qualified System.Console.ANSI as Ansi
67

78
import HTTP
89
import qualified Trigger
910

1011
spec :: Spec
1112
spec = do
1213
describe "app" $ do
13-
with (return $ app $ return (Trigger.Success, "hello")) $ do
14+
with (return $ app $ return (Trigger.Success, withColor Green "hello")) $ do
1415
it "returns 200 on success" $ do
15-
get "/" `shouldRespondWith` 200
16+
get "/" `shouldRespondWith` fromString (withColor Green "hello")
17+
18+
context "with ?color" $ do
19+
it "keeps terminal sequences" $ do
20+
get "/?color" `shouldRespondWith` fromString (withColor Green "hello")
21+
22+
context "with ?color=true" $ do
23+
it "keeps terminal sequences" $ do
24+
get "/?color=true" `shouldRespondWith` fromString (withColor Green "hello")
25+
26+
context "with ?color=false" $ do
27+
it "removes terminal sequences" $ do
28+
get "/?color=false" `shouldRespondWith` "hello"
29+
30+
context "with an in invalid value for ?color" $ do
31+
it "returns status 400" $ do
32+
get "/?color=some%20value" `shouldRespondWith` 400 { matchBody = "invalid value for color: some%20value" }
1633

1734
with (return $ app $ return (Trigger.Failure, "hello")) $ do
1835
it "return 500 on failure" $ do
1936
get "/" `shouldRespondWith` 500
37+
38+
describe "stripAnsi" $ do
39+
it "removes ANSI color sequences" $ do
40+
stripAnsi ("some " <> withColor Green "colorized" <> " text") `shouldBe` "some colorized text"
41+
42+
it "removes DEC private mode sequences" $ do
43+
stripAnsi (Ansi.hideCursorCode <> "some text" <> Ansi.showCursorCode) `shouldBe` "some text"

0 commit comments

Comments
 (0)