Introducing sectile
Following antikythera and numerus-closus, I've been working for a few days on sectile, which will be released soon, to compose tmux status bar.
Unlike existing libraries and tools, it aims to be both flexible and simple.
A status bar is composed of styled segments with the following syntax:
[bg=red,fg=white]something[fg=black]something else
Usually, status bar frameworks allow chaining segments' colors, which leads to the following design:
newtype Segment m = Segment
{ runSegment :: m (ChunkStyle -> [Chunk])
}
Note: We could have declared ChunkStyle -> m [Chunk], but it would force us to run each segment sequentially, while this design allows parallel runs and sequential application.
By itself, Chunk is defined as follows:
data Chunk = Chunk
{ chunkText :: Text,
chunkStyle :: ChunkStyle
}
data ChunkStyle = ChunkStyle
{ chunkStyleForeground :: Maybe Colour,
chunkStyleBackground :: Maybe Colour,
chunkStyleItalic :: Maybe Bool,
chunkStyleStrikethrough :: Maybe Bool,
chunkStyleSwapForegroundBackground :: Maybe Bool,
chunkStyleConcealed :: Maybe Bool,
chunkStyleOverlined :: Maybe Bool,
chunkStyleConsoleIntensity :: Maybe ConsoleIntensity,
chunkStyleUnderlining :: Maybe Underlining,
chunkStyleBlinking :: Maybe Blinking,
chunkStyleHyperlink :: Maybe Text
}
We can start by creating a trivial hard-coded string, defined as follows:
string :: (Applicative m) => T.Text -> Segment m
string txt =
Segment $
pure $
\style ->
let (_finalStyle, rendered) = Colour.parseAnsiChunks style txt
in rendered
We need to parse the given text, as the input text could contain some style itself.The issue in the previous snippet is that we throw the final style, which is used for chaining. Let's add some types:
newtype Segment m = Segment
{ runSegment :: m (Colour.ChunkStyle -> Formatted)
}
data Formatted = Formatted
{ rendered :: [Colour.Chunk],
finalStyle :: Colour.ChunkStyle
}
Then we should rewrite string as follows:
string :: (Applicative m) => T.Text -> Segment m
string txt =
Segment $
pure $
\style ->
let (finalStyle, rendered) = Colour.parseAnsiChunks style txt
in Formatted {..}
Let's see how to leverage the initial design, with row, turning a list of segments into one, as follows:
type SegmentsRunner m =
(Segment m -> m (Colour.ChunkStyle -> Formatted)) ->
[Segment m] ->
m [Colour.ChunkStyle -> Formatted]
row :: (Monad m) => SegmentsRunner m -> Name -> [Segment m] -> Segment m
row runSegments (Name name) ss =
Segment $ do
formats <- runSegments (.runSegment) ss
pure $
\style ->
let (finalStyle, formatteds) =
let safeLast = foldl' (const Just) Nothing
go (lastStyle, fs) f =
let fmt = f lastStyle
in (maybe lastStyle Colour.chunkStyle $ safeLast fmt.rendered, fmt : fs)
in reverse <$> foldl' go (style, []) formats
rendered = concatMap (.rendered) formatteds
in Formatted {..}
Two things about this implementation:
- We give a function to evaluate the list of segments, which means the evaluation could be parallel, as well as sequential
- After the evaluation, each
Formattedis piped to the next one, creating a biggerFormatted, itself wrapped in aSegment.Beyond this, we can support any shell command, as follows:
sh :: Name -> String -> Maybe [(String, String)] -> Segment IO
sh (Name name) cmd env =
Segment $ do
let proc =
(Process.shell cmd)
{ Process.env = env,
Process.std_in = Process.CreatePipe,
Process.std_out = Process.CreatePipe,
Process.std_err = Process.CreatePipe
}
result <- tryReadProcess proc
let stdout = case result of
Right out -> T.pack out
Left _ -> "Error on " <> TL.toStrict (TLE.decodeUtf8 (B.toLazyByteString name))
pure $
\style ->
let (finalStyle, rendered) = Colour.parseAnsiChunks style stdout
in Formatted {..}
I plan to release sectile both as a binary and as a library; for instance, the time segment is directly based on time and is defined as follows:
time :: Name -> String -> Segment IO
time (Name name) format =
Segment $ do
result <- tryIO $ Time.formatTime Time.defaultTimeLocale format <$> Time.getZonedTime
let txt = case result of
Right t -> T.pack t
Left _ -> "Error on " <> TL.toStrict (TLE.decodeUtf8 (B.toLazyByteString name))
pure $
\style ->
let (finalStyle, rendered) = Colour.parseAnsiChunks style txt
in Formatted {..}
Beyond this, it will also be shipped with helpers to update the color and be defined as follows:
changeStyle :: (Functor m) => (Colour.ChunkStyle -> Colour.ChunkStyle) -> Segment m -> Segment m
changeStyle c (Segment s) = Segment $ (. c) <$> s
forceStyle :: (Functor m) => (Colour.ChunkStyle -> Colour.ChunkStyle) -> Segment m -> Segment m
forceStyle c (Segment s) = Segment $ force <$> s
where
force f style =
let formatted = f style
in formatted
{ rendered = updateChunk <$> formatted.rendered,
finalStyle = c formatted.finalStyle,
explain = \renderer -> formatted.explain $ renderer . map updateChunk
}
updateChunk chunk = chunk {Colour.chunkStyle = c $ Colour.chunkStyle chunk}
changeStyle works on the input style, while forceStyle changes the generated chunks.
There are also helpers around segment size (padding, max size, and fixed size), conditional colors, and themes.
At this time I'm working on giving more flexibility to the format of each segment.