{-# LANGUAGE OverloadedStrings #-}
module System.Taffybar.Widget.Weather
( WeatherConfig(..)
, WeatherInfo(..)
, WeatherFormatter(WeatherFormatter)
, weatherNew
, weatherCustomNew
, defaultWeatherConfig
) where
import Control.Monad.IO.Class
import qualified Data.ByteString.Lazy as LB
import Data.List (stripPrefix)
import Data.Maybe (fromMaybe)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import GI.GLib(markupEscapeText)
import GI.Gtk
import Network.HTTP.Client
import Network.HTTP.Client.TLS (tlsManagerSettings)
import Network.HTTP.Types.Status
import System.Log.Logger
import Text.Parsec
import Text.Printf
import Text.StringTemplate
import System.Taffybar.Widget.Generic.PollingLabel
data WeatherInfo = WI
{ stationPlace :: String
, stationState :: String
, year :: String
, month :: String
, day :: String
, hour :: String
, wind :: String
, visibility :: String
, skyCondition :: String
, tempC :: Int
, tempF :: Int
, dewPoint :: String
, humidity :: Int
, pressure :: Int
} deriving (Show)
type Parser = Parsec String ()
pTime :: Parser (String, String, String, String)
pTime = do
y <- getNumbersAsString
_ <- char '.'
m <- getNumbersAsString
_ <- char '.'
d <- getNumbersAsString
_ <- char ' '
(h:hh:mi:mimi) <- getNumbersAsString
_ <- char ' '
return (y, m, d , [h]++[hh]++":"++[mi]++mimi)
pTemp :: Parser (Int, Int)
pTemp = do
let num = digit <|> char '-' <|> char '.'
f <- manyTill num $ char ' '
_ <- manyTill anyChar $ char '('
c <- manyTill num $ char ' '
_ <- skipRestOfLine
return (floor (read c :: Double), floor (read f :: Double))
pRh :: Parser Int
pRh = do
s <- manyTill digit $ char '%' <|> char '.'
return $ read s
pPressure :: Parser Int
pPressure = do
_ <- manyTill anyChar $ char '('
s <- manyTill digit $ char ' '
_ <- skipRestOfLine
return $ read s
parseData :: Parser WeatherInfo
parseData = do
st <- getAllBut ","
_ <- space
ss <- getAllBut "("
_ <- skipRestOfLine >> getAllBut "/"
(y,m,d,h) <- pTime
w <- getAfterString "Wind: "
v <- getAfterString "Visibility: "
sk <- getAfterString "Sky conditions: "
_ <- skipTillString "Temperature: "
(tC,tF) <- pTemp
dp <- getAfterString "Dew Point: "
_ <- skipTillString "Relative Humidity: "
rh <- pRh
_ <- skipTillString "Pressure (altimeter): "
p <- pPressure
_ <- manyTill skipRestOfLine eof
return $ WI st ss y m d h w v sk tC tF dp rh p
getAllBut :: String -> Parser String
getAllBut s =
manyTill (noneOf s) (char $ head s)
getAfterString :: String -> Parser String
getAfterString s = pAfter <|> return ("<" ++ s ++ " not found!>")
where
pAfter = do
_ <- try $ manyTill skipRestOfLine $ string s
manyTill anyChar newline
skipTillString :: String -> Parser String
skipTillString s =
manyTill skipRestOfLine $ string s
getNumbersAsString :: Parser String
getNumbersAsString = skipMany space >> many1 digit >>= \n -> return n
skipRestOfLine :: Parser Char
skipRestOfLine = do
_ <- many $ noneOf "\n\r"
newline
downloadURL :: Manager -> Request -> IO (Either String String)
downloadURL mgr request = do
response <- httpLbs request mgr
case responseStatus response of
s | s >= status200 && s < status300 ->
return $ Right (T.unpack . T.decodeUtf8 . LB.toStrict $ responseBody response)
otherStatus ->
return . Left $ "HTTP 2XX status was expected but received " ++ show otherStatus
getWeather :: Manager -> String -> IO (Either String WeatherInfo)
getWeather mgr url = do
request <- parseRequest url
dat <- downloadURL mgr request
case dat of
Right dat' -> case parse parseData url dat' of
Right d -> return (Right d)
Left err -> return (Left (show err))
Left err -> return (Left (show err))
defaultFormatter :: StringTemplate String -> WeatherInfo -> String
defaultFormatter tpl wi = render tpl'
where
tpl' = setManyAttrib [ ("stationPlace", stationPlace wi)
, ("stationState", stationState wi)
, ("year", year wi)
, ("month", month wi)
, ("day", day wi)
, ("hour", hour wi)
, ("wind", wind wi)
, ("visibility", visibility wi)
, ("skyCondition", skyCondition wi)
, ("tempC", show (tempC wi))
, ("tempF", show (tempF wi))
, ("dewPoint", dewPoint wi)
, ("humidity", show (humidity wi))
, ("pressure", show (pressure wi))
] tpl
getCurrentWeather :: IO (Either String WeatherInfo)
-> StringTemplate String
-> StringTemplate String
-> WeatherFormatter
-> IO (T.Text, Maybe T.Text)
getCurrentWeather getter labelTpl tooltipTpl formatter = do
dat <- getter
case dat of
Right wi ->
case formatter of
DefaultWeatherFormatter -> do
let rawLabel = T.pack $ defaultFormatter labelTpl wi
let rawTooltip = T.pack $ defaultFormatter tooltipTpl wi
lbl <- markupEscapeText rawLabel (-1)
tooltip <- markupEscapeText rawTooltip (-1)
return (lbl, Just tooltip)
WeatherFormatter f -> do
let rawLabel = T.pack $ f wi
lbl <- markupEscapeText rawLabel (-1)
return (lbl, Just lbl)
Left err -> do
logM "System.Taffybar.Widget.Weather" ERROR $ "Error in weather: " <> show err
return ("N/A", Nothing)
baseUrl :: String
baseUrl = "https://tgftp.nws.noaa.gov/data/observations/metar/decoded"
data WeatherFormatter
= WeatherFormatter (WeatherInfo -> String)
| DefaultWeatherFormatter
data WeatherConfig = WeatherConfig
{ weatherStation :: String
, weatherTemplate :: String
, weatherTemplateTooltip :: String
, weatherFormatter :: WeatherFormatter
, weatherProxy :: Maybe String
}
defaultWeatherConfig :: String -> WeatherConfig
defaultWeatherConfig station =
WeatherConfig
{ weatherStation = station
, weatherTemplate = "$tempF$ °F"
, weatherTemplateTooltip =
unlines
[ "Station: $stationPlace$"
, "Time: $day$.$month$.$year$ $hour$"
, "Temperature: $tempF$ °F"
, "Pressure: $pressure$ hPa"
, "Wind: $wind$"
, "Visibility: $visibility$"
, "Sky Condition: $skyCondition$"
, "Dew Point: $dewPoint$"
, "Humidity: $humidity$"
]
, weatherFormatter = DefaultWeatherFormatter
, weatherProxy = Nothing
}
weatherNew :: MonadIO m
=> WeatherConfig
-> Double
-> m GI.Gtk.Widget
weatherNew cfg delayMinutes = liftIO $ do
let usedProxy = case weatherProxy cfg of
Nothing -> noProxy
Just str ->
let strToBs = T.encodeUtf8 . T.pack
noHttp = fromMaybe str $ stripPrefix "http://" str
(phost, pport) = case span (':'/=) noHttp of
(h, "") -> (strToBs h, 80)
(h, ':':p) -> (strToBs h, read p)
_ -> error "unreachable: broken span"
in useProxy $ Proxy phost pport
mgr <- newManager $ managerSetProxy usedProxy tlsManagerSettings
let url = printf "%s/%s.TXT" baseUrl (weatherStation cfg)
let getter = getWeather mgr url
weatherCustomNew getter (weatherTemplate cfg) (weatherTemplateTooltip cfg)
(weatherFormatter cfg) delayMinutes
weatherCustomNew
:: MonadIO m
=> IO (Either String WeatherInfo)
-> String
-> String
-> WeatherFormatter
-> Double
-> m GI.Gtk.Widget
weatherCustomNew getter labelTpl tooltipTpl formatter delayMinutes = liftIO $ do
let labelTpl' = newSTMP labelTpl
tooltipTpl' = newSTMP tooltipTpl
l <- pollingLabelNewWithTooltip (delayMinutes * 60)
(getCurrentWeather getter labelTpl' tooltipTpl' formatter)
GI.Gtk.widgetShowAll l
return l