packages feed

uku-0.0.1.0: app/Main.hs

{-|
Description : Main entry point for the CLI app.

Display ukulele fingering charts in your terminal.
-}
module Main where

import Protolude as P (
  Applicative (pure),
  Char,
  Either (..),
  Functor (fmap),
  IO,
  Int,
  Print (putStr),
  Semigroup ((<>)),
  die,
  getArgs,
  length,
  mapM,
  maximum,
  replicate,
  toUpper,
  transpose,
  zipWith,
  ($),
  (&),
  (+),
  (-),
  (.),
  (<$>),
  (<&>),
 )

import Data.Text qualified as Text (
  Text,
  intercalate,
  lines,
  pack,
  replicate,
  unlines,
  unpack,
 )

import Uku.Render (getAnsiArts)


toArt :: [Char] -> IO (Text.Text, Text.Text)
toArt raw = do
  let displayName = Text.pack (capitalizeChord raw)
  case getAnsiArts (Text.pack raw) of
    Left err -> die err
    Right art -> pure (displayName, art)


-- | Capitalize the first letter only (A, B♭, G#m, …)
capitalizeChord :: [Char] -> [Char]
capitalizeChord [] = []
capitalizeChord (x : xs) = toUpper x : xs


-- | Visible length (ignores "\ESC[ … m" sequences).
visibleLen :: Text.Text -> Int
visibleLen = do
  let
    skip [] = []
    skip ('m' : ys) = ys -- End of CSI
    skip (_ : ys) = skip ys

    go n [] = n
    go n ('\x1b' : xs) = go n $ skip xs -- Skip CSI
    go n (_ : xs) = go (n + 1) xs

  go 0 . Text.unpack


combineArts :: [(Text.Text, Text.Text)] -> Text.Text
combineArts arts = do
  let
    -- Prepend the chord name to every column
    colsLines = fmap (\(name, art) -> name : Text.lines art) arts

    -- Pad every column to the same number of rows
    maxRows = P.maximum (P.length <$> colsLines)
    padRows x = x <> P.replicate (maxRows - P.length x) ""
    rowPadded = fmap padRows colsLines

    -- Width of every column
    widths = fmap (P.maximum . fmap visibleLen) rowPadded
    -- Right-pad every cell to the column width
    colPadded =
      P.zipWith
        (\col w -> col <&> (\l -> l <> Text.replicate (w - visibleLen l) " "))
        rowPadded
        widths
    -- Combine the rows horizontally
    joined = colPadded & P.transpose <&> Text.intercalate "  "

  Text.unlines joined


main :: IO ()
main = do
  chords <- getArgs
  case chords of
    [] -> die "Usage: uku <chord> [<chord> ...]"
    cs -> do
      arts <- mapM toArt cs
      putStr $ combineArts arts