diff options
-rw-r--r-- | .envrc | 1 | ||||
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | LICENSE | 30 | ||||
-rw-r--r-- | app/Main.hs | 240 | ||||
-rw-r--r-- | app/Pretty.hs | 89 | ||||
-rw-r--r-- | app/Pretty/Color.hs | 58 | ||||
-rw-r--r-- | app/Sensor.hs | 520 | ||||
-rw-r--r-- | app/Ui.hs | 200 | ||||
-rw-r--r-- | app/Util.hs | 18 | ||||
-rw-r--r-- | astatusbar.cabal | 38 | ||||
-rw-r--r-- | default.nix | 66 | ||||
-rw-r--r-- | nix/sources.json | 20 | ||||
-rw-r--r-- | nix/sources.nix | 198 | ||||
-rw-r--r-- | shell.nix | 1 |
14 files changed, 1480 insertions, 0 deletions
@@ -0,0 +1 @@ +use nix diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8075013 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/dist-newstyle @@ -0,0 +1,30 @@ +Copyright (c) 2024, Alexander Foremny + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Alexander Foremny nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/app/Main.hs b/app/Main.hs new file mode 100644 index 0000000..8dc5d5f --- /dev/null +++ b/app/Main.hs @@ -0,0 +1,240 @@ +{-# LANGUAGE BlockArguments #-} +{-# LANGUAGE MultiWayIf #-} +{-# LANGUAGE OverloadedRecordDot #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE StrictData #-} +{-# LANGUAGE TypeFamilies #-} + +module Main where + +import Control.Concurrent +import Control.Exception +import Control.Monad +import Control.Monad.Identity +import Data.Bits +import Data.List +import Data.Map qualified as M +import GHC.Ptr (Ptr) +import Graphics.X11 qualified as X +import Graphics.X11.Xft qualified as X +import Graphics.X11.Xlib.Extras qualified as X +import Pretty qualified as P +import Sensor +import Ui hiding (Env) +import Ui qualified +import UnliftIO.STM + +data Env = Env + { dpy :: X.Display, + win :: X.Window, + swidth :: Int, + sheight :: Int, + wwidth :: Int, + wheight :: Int, + wleft :: Int, + wtop :: Int, + gc :: X.GC, + pixm :: X.Pixmap, + fnt :: X.XftFont, + drw :: X.XftDraw, + cmap :: X.Colormap, + vis :: X.Visual + } + +type Colors = M.Map (P.Intensity, P.Color) X.XftColor + +data State = State + { dirty :: Bool, + ui :: Ui TVar P.Doc + } + +main :: IO () +main = do + bracket createWindow destroyWindow $ \(env, stateT) -> + withColors env (run env stateT) + +data LastRun = LastRun + { pUi :: Ui Identity P.Doc, + lUi :: Ui' + } + +run :: Env -> TVar State -> Colors -> IO () +run env stateT colors = do + let loop = (loop . Just =<<) . go + loop Nothing + where + go Nothing = do + pUi <- atomically do + state@State {..} <- readTVar stateT + writeTVar stateT state {dirty = False} + pollUi ui + lUi <- paint env colors Nothing pUi + pure (LastRun pUi lUi) + go (Just (LastRun {pUi = pUi', lUi = lUi'})) = do + (pUi, dirty) <- atomically do + state@State {..} <- readTVar stateT + pUi <- pollUi ui + checkSTM (dirty || pUi /= pUi') + writeTVar stateT state {dirty = False} + pure (pUi, dirty) + lUi <- paint env colors (if dirty then Nothing else Just lUi') pUi + pure (LastRun pUi lUi) + +processEvents :: Ptr X.XEvent -> Env -> TVar State -> IO () +processEvents ev env@(Env {..}) stateT = do + timeOut <- X.waitForEvent dpy 1_000_000_000 + unless timeOut do + processEvent ev env stateT + processEvents ev env stateT + +processEvent :: Ptr X.XEvent -> Env -> TVar State -> IO () +processEvent ev (Env {..}) stateT = do + X.nextEvent dpy ev + e <- X.getEvent ev + if + | X.ExposeEvent {} <- e -> atomically do + state <- readTVar stateT + writeTVar stateT state {dirty = True} + | otherwise -> pure () + +paint :: + Env -> + Colors -> + Maybe Ui' -> + Ui Identity P.Doc -> + IO Ui' +paint Env {..} colors Nothing ui = do + let env' = Ui.Env {..} + ui' <- layOutUi env' ui + renderUi env' colors ui' + X.copyArea dpy pixm win gc 0 0 (fi wwidth) (fi wheight) 0 0 + X.sync dpy False + pure ui' +paint Env {..} colors (Just ui') ui = do + let env' = Ui.Env {..} + ui'' <- layOutUi env' ui + renderUi' env' colors ui' ui'' + X.copyArea dpy pixm win gc 0 0 (fi wwidth) (fi wheight) 0 0 + X.sync dpy False + pure ui'' + +destroyWindow :: (Env, TVar State) -> IO () +destroyWindow (Env {..}, _) = do + X.destroyWindow dpy win + +createWindow :: IO (Env, TVar State) +createWindow = do + dpy <- X.openDisplay "" + let scrn = X.defaultScreen dpy + scr = X.defaultScreenOfDisplay dpy + root = X.defaultRootWindow dpy + trueColor = 4 + Just vinfo <- X.matchVisualInfo dpy scrn 32 trueColor + let cls = X.inputOutput + dpth = X.visualInfo_depth vinfo + vis = X.visualInfo_visual vinfo + vmsk = X.cWColormap .|. X.cWBorderPixel .|. X.cWBackingPixel .|. X.cWOverrideRedirect + swidth = fi (X.displayWidth dpy scrn) + sheight = fi (X.displayHeight dpy scrn) + wwidth = swidth - 16 + wheight = 32 + wleft = 8 + wtop = 8 + cmap <- X.createColormap dpy root vis X.allocNone + win <- X.allocaSetWindowAttributes $ \attr -> do + X.set_colormap attr cmap + X.set_border_pixel attr 0 + X.set_background_pixel attr 0 + X.set_override_redirect attr True + X.createWindow dpy root (fi wleft) (fi wtop) (fi wwidth) (fi wheight) 0 dpth cls vis vmsk attr + atom <- X.internAtom dpy "ATOM" True + wmState <- X.internAtom dpy "_NET_WM_STATE" False + wmStateSticky <- X.internAtom dpy "_NET_WM_STATE_STICKY" False + wmStateAbove <- X.internAtom dpy "_NET_WM_STATE_ABOVE" False + wmWindowType <- X.internAtom dpy "_NET_WM_WINDOW_TYPE" False + wmWindowTypeDock <- X.internAtom dpy "_NET_WM_WINDOW_TYPE_DOCK" False + wmStrut <- X.internAtom dpy "_NET_WM_STRUT" False + wmStrutPartial <- X.internAtom dpy "_NET_WM_STRUT_PARTIAL" False + X.changeProperty32 dpy win wmState atom X.propModeReplace $ + [ fi wmStateAbove, + fi wmStateSticky + ] + X.changeProperty32 dpy win wmWindowType atom X.propModeReplace $ + [ fi wmWindowTypeDock + ] + X.changeProperty32 dpy win wmStrut atom X.propModeReplace $ + [0, 0, fi wheight + 8, 0] + X.changeProperty32 dpy win wmStrutPartial atom X.propModeReplace $ + [0, 0, fi wheight + 8, 0, 0, 0, 0, 0, fi wleft, fi wwidth, 0, 0] + pixm <- X.createPixmap dpy win (fi wwidth) (fi wheight) dpth + gc <- X.createGC dpy win + drw <- X.xftDrawCreate dpy pixm vis cmap + fnt <- X.xftFontOpen dpy scr "Free Mono:size=15" + X.mapWindow dpy win + let dirty = True + ui <- + runSensorM . initUi . Ui $ + intercalate [lit " "] $ + [ [sens wmWorkspaces], + [sens wmName, fill], + [lit "cpu ", sens cpu], + [lit "mem ", sens mem], + [lit "disk ", sens disk], + [lit "io ", sens io], + [lit "net ", sens net], + [lit "snd ", sens Sensor.snd], + [lit "bat ", sens bat], + [lit "date ", sens date], + [lit "time ", sens time] + ] + let env = Env {..} + stateT <- newTVarIO State {..} + void $ forkIO $ X.allocaXEvent $ \ev -> forever do + processEvents ev env stateT + pure (env, stateT) + +withColors :: Env -> (Colors -> IO a) -> IO a +withColors Env {..} act = do + X.withXftColorName dpy vis cmap (P.toRgb (P.Vivid, P.Black)) $ \black -> do + X.withXftColorName dpy vis cmap (P.toRgb (P.Vivid, P.Red)) $ \red -> do + X.withXftColorName dpy vis cmap (P.toRgb (P.Vivid, P.Green)) $ \green -> do + X.withXftColorName dpy vis cmap (P.toRgb (P.Vivid, P.Yellow)) $ \yellow -> do + X.withXftColorName dpy vis cmap (P.toRgb (P.Vivid, P.Blue)) $ \blue -> do + X.withXftColorName dpy vis cmap (P.toRgb (P.Vivid, P.Magenta)) $ \magenta -> do + X.withXftColorName dpy vis cmap (P.toRgb (P.Vivid, P.Cyan)) $ \cyan -> do + X.withXftColorName dpy vis cmap (P.toRgb (P.Vivid, P.White)) $ \white -> do + X.withXftColorName dpy vis cmap (P.toRgb (P.Dull, P.Black)) $ \dullBlack -> do + X.withXftColorName dpy vis cmap (P.toRgb (P.Dull, P.Red)) $ \dullRred -> do + X.withXftColorName dpy vis cmap (P.toRgb (P.Dull, P.Green)) $ \dullGreen -> do + X.withXftColorName dpy vis cmap (P.toRgb (P.Dull, P.Yellow)) $ \dullYellow -> do + X.withXftColorName dpy vis cmap (P.toRgb (P.Dull, P.Blue)) $ \dullBlue -> do + X.withXftColorName dpy vis cmap (P.toRgb (P.Dull, P.Magenta)) $ \dullMagenta -> do + X.withXftColorName dpy vis cmap (P.toRgb (P.Dull, P.Cyan)) $ \dullCyan -> do + X.withXftColorName dpy vis cmap (P.toRgb (P.Dull, P.White)) $ \dullWhite -> do + let colors = + M.fromList + [ ((P.Vivid, P.Black), black), + ((P.Vivid, P.Red), red), + ((P.Vivid, P.Green), green), + ((P.Vivid, P.Yellow), yellow), + ((P.Vivid, P.Blue), blue), + ((P.Vivid, P.Magenta), magenta), + ((P.Vivid, P.Cyan), cyan), + ((P.Vivid, P.White), white), + ((P.Dull, P.Black), dullBlack), + ((P.Dull, P.Red), dullRred), + ((P.Dull, P.Green), dullGreen), + ((P.Dull, P.Yellow), dullYellow), + ((P.Dull, P.Blue), dullBlue), + ((P.Dull, P.Magenta), dullMagenta), + ((P.Dull, P.Cyan), dullCyan), + ((P.Dull, P.White), dullWhite) + ] + + act colors + +fi :: (Integral a, Num b) => a -> b +fi = fromIntegral + +clamp :: (Ord a) => a -> a -> a -> a +clamp mi ma = max mi . min ma diff --git a/app/Pretty.hs b/app/Pretty.hs new file mode 100644 index 0000000..8054acc --- /dev/null +++ b/app/Pretty.hs @@ -0,0 +1,89 @@ +{-# OPTIONS_GHC -fno-warn-name-shadowing #-} +{-# OPTIONS_GHC -fno-warn-unused-top-binds #-} + +module Pretty + ( Doc (..), + Pretty, + pretty, + diagram, + module Pretty.Color, + color, + colorDull, + ) +where + +import Pretty.Color + +data Doc + = Col [Doc] + | Lit (Maybe (Intensity, Color)) String + deriving (Show, Eq) + +class Pretty a where + pretty :: a -> Doc + +instance Pretty Char where + pretty = pretty . (: []) + +instance Pretty String where + pretty = Lit Nothing + +diagram :: Int -> [Float] -> Doc +diagram w xs = Col (map chart (discretize xs')) + where + xs' = replicate (2 * w - length xs) 0 ++ reverse (take (2 * w) xs) + + chart :: (Int, Int) -> Doc + chart n = colorize n (pretty (chart' n)) + + chart' (0, 0) = '⠀' + chart' (0, 1) = '⢀' + chart' (0, 2) = '⢠' + chart' (0, 3) = '⢰' + chart' (0, 4) = '⢸' + chart' (1, 0) = '⡀' + chart' (1, 1) = '⣀' + chart' (1, 2) = '⣠' + chart' (1, 3) = '⣰' + chart' (1, 4) = '⣸' + chart' (2, 0) = '⡄' + chart' (2, 1) = '⣄' + chart' (2, 2) = '⣤' + chart' (2, 3) = '⣴' + chart' (2, 4) = '⣼' + chart' (3, 0) = '⡆' + chart' (3, 1) = '⣆' + chart' (3, 2) = '⣦' + chart' (3, 3) = '⣶' + chart' (3, 4) = '⣾' + chart' (4, 0) = '⡇' + chart' (4, 1) = '⣇' + chart' (4, 2) = '⣧' + chart' (4, 3) = '⣷' + chart' (4, 4) = '⣿' + chart' _ = error "chart': argument >4 (or <0)" + + colorize (n, m) = colorize' (max n m) + + colorize' 0 = colorDull Green + colorize' 1 = color Green + colorize' 2 = colorDull Yellow + colorize' 3 = color Yellow + colorize' 4 = color Red + colorize' _ = error "colorize': argument >4 (or <0)" + + discretize :: [Float] -> [(Int, Int)] + discretize [] = [] + discretize (_ : []) = [] + discretize (x1 : x2 : xs) = + (round (x1 * 4), round (x2 * 4)) : discretize xs + +color :: Color -> Doc -> Doc +color c = color' (Vivid, c) + +colorDull :: Color -> Doc -> Doc +colorDull c = color' (Dull, c) + +color' :: (Intensity, Color) -> Doc -> Doc +color' c (Lit _ s) = Lit (Just c) s +color' c (Col ds) = Col (map (color' c) ds) diff --git a/app/Pretty/Color.hs b/app/Pretty/Color.hs new file mode 100644 index 0000000..e8e48dd --- /dev/null +++ b/app/Pretty/Color.hs @@ -0,0 +1,58 @@ +{-# OPTIONS_GHC -fno-warn-unused-top-binds #-} + +module Pretty.Color + ( Color (..), + toRgb, + Intensity (..), + ) +where + +data Color + = Black + | Red + | Green + | Yellow + | Blue + | Magenta + | Cyan + | White + deriving (Show, Eq, Ord) + +toRgb :: (Intensity, Color) -> String +toRgb (Dull, Black) = base00 -- color0 +toRgb (Dull, Red) = base08 -- color1 +toRgb (Dull, Green) = base0B -- color2 +toRgb (Dull, Yellow) = base0A -- color3 +toRgb (Dull, Blue) = base0D -- color4 +toRgb (Dull, Magenta) = base0E -- color5 +toRgb (Dull, Cyan) = base0C -- color6 +toRgb (Dull, White) = base05 -- color7 +toRgb (Vivid, Black) = base03 -- color8 +toRgb (Vivid, Red) = base08 -- color9 +toRgb (Vivid, Green) = base0B -- color10 +toRgb (Vivid, Yellow) = base0A -- color11 +toRgb (Vivid, Blue) = base0D -- color12 +toRgb (Vivid, Magenta) = base0E -- color13 +toRgb (Vivid, Cyan) = base0C -- color14 +toRgb (Vivid, White) = base07 -- color15 + +data Intensity = Vivid | Dull + deriving (Show, Eq, Ord) + +base00, base01, base02, base03, base04, base05, base06, base07, base08, base09, base0A, base0B, base0C, base0D, base0E, base0F :: String +base00 = "#263238" +base01 = "#2E3C43" +base02 = "#314549" +base03 = "#546E7A" +base04 = "#B2CCD6" +base05 = "#EEFFFF" +base06 = "#EEFFFF" +base07 = "#FFFFFF" +base08 = "#F07178" +base09 = "#F78C6C" +base0A = "#FFCB6B" +base0B = "#C3E88D" +base0C = "#89DDFF" +base0D = "#82AAFF" +base0E = "#C792EA" +base0F = "#FF5370" diff --git a/app/Sensor.hs b/app/Sensor.hs new file mode 100644 index 0000000..2342807 --- /dev/null +++ b/app/Sensor.hs @@ -0,0 +1,520 @@ +{-# LANGUAGE BlockArguments #-} +{-# LANGUAGE FunctionalDependencies #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedRecordDot #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE StrictData #-} +{-# LANGUAGE ViewPatterns #-} +{-# OPTIONS_GHC -fno-warn-name-shadowing #-} +{-# OPTIONS_GHC -fno-warn-type-defaults #-} +{-# OPTIONS_GHC -fno-warn-unused-top-binds #-} + +module Sensor + ( SensorM, + runSensorM, + poll, + Sensor (..), + cpu, + io, + net, + mem, + bat, + Sensor.snd, + disk, + date, + time, + wmName, + wmWorkspaces, + ) +where + +import Control.Monad +import Control.Monad.Reader +import Data.Char +import Data.Dynamic +import Data.Functor.WithIndex +import Data.List +import Data.Map qualified as M +import Data.Maybe +import Data.Set qualified as S +import Data.Time.Clock +import Data.Time.Format +import Data.Time.LocalTime +import Graphics.X11 qualified as X +import Graphics.X11.Xlib.Extras qualified as X +import Pretty qualified as P +import Process.Shell (sh) +import Safe +import System.FilePath +import System.IO.Error +import System.Linux.Inotify qualified as I +import System.Posix.StatVFS +import Text.Printf +import UnliftIO.Concurrent +import UnliftIO.Exception +import UnliftIO.Memoize +import UnliftIO.STM +import Witherable (ifilter) + +type SensorM a = + ReaderT (MVar (M.Map String (Memoized Dynamic))) IO a + +runSensorM :: SensorM a -> IO a +runSensorM m = do + g <- newMVar M.empty + runReaderT m g + +poll :: (Typeable a) => Sensor a -> SensorM (TVar a) +poll (Sensor id vM') = do + gM <- ask + g <- takeMVar gM + vM <- case M.lookup id g of + Just vM -> do + putMVar gM g + pure (fromJust . fromDynamic <$> vM) + Nothing -> do + -- liftIO (printf "[%s] start\n" id) + vM <- vM' + putMVar gM (M.insert id (toDyn <$> vM) g) + pure vM + runMemoized vM + +data Sensor a = Sensor String (SensorM (Memoized (TVar a))) + +data CpuStat = CpuStat + { total :: Int, + idle :: Int + } + deriving (Show, Eq) + +cpuStat :: Sensor CpuStat +cpuStat = pollFile "cpuStat" aggregate "/proc/stat" (5 * 10 ^ 5) + where + aggregate fp = liftIO do + stat <- readFile fp + case filter ("cpu " `isPrefixOf`) (lines stat) of + [] -> error "/proc/stat: no cpu line" + (cpu : _) -> + case map read (drop 1 (words cpu)) of + xs@(_ : _ : _ : idle : _) -> + pure $ CpuStat (sum xs) idle + _ -> error "/proc/stat: unexpected cpu line" + +cpu' :: Sensor [CpuStat] +cpu' = histogram "cpu'" [] (\b a -> take 7 (a : b)) cpuStat + +data Cpu = Cpu {unCpu :: [Float]} + +instance P.Pretty Cpu where + pretty = P.diagram 3 . unCpu + +cpu :: Sensor Cpu +cpu = transform "cpu" f cpu' + where + f xs = do + let xs' = + zipWith + ( \next prev -> + let total = next.total - prev.total + idle = next.idle - prev.idle + in fromIntegral (total - idle) / fromIntegral total + ) + (fromMaybe [] (initMay xs)) + (fromMaybe [] (tailMay xs)) + pure (Cpu xs') + +data IoStat = IoStat {unIoStat :: Int} deriving (Show, Eq) + +ioStat :: Sensor IoStat +ioStat = pollFile "ioStat" aggregate "/sys/block/nvme0n1/stat" (5 * 10 ^ 5) + where + aggregate fp = liftIO do + stat <- readFile fp + case words stat of + [_, _, read', _, _, _, write', _, _, _, _, _, _, _, _, _, _] -> + pure (IoStat ((read read') + (read write'))) + _ -> error "/sys/block/nvme0n1/stat: malformed" + +io' :: Sensor [IoStat] +io' = histogram "io'" [] (\b a -> take 7 (a : b)) ioStat + +data Io = Io {unIo :: [Float]} deriving (Show, Eq) + +instance P.Pretty Io where + pretty = P.diagram 3 . unIo + +io :: Sensor Io +io = transform "io" f io' + where + f (map unIoStat -> xs) = do + let xs' = + zipWith + (-) + (fromMaybe [] (initMay xs)) + (fromMaybe [] (tailMay xs)) + x' = max 1 (fromMaybe 0 (maximumMay xs')) + pure . Io $ + map (\x -> fromIntegral x / fromIntegral x') xs' + +data NetStat = NetStat {unNetStat :: Int} deriving (Show, Eq) + +netStat :: Sensor NetStat +netStat = + pollFile + "netStat" + aggregate + "/sys/class/net/wlan0/statistics/rx_bytes" + (5 * 10 ^ 5) + where + aggregate fp = liftIO do + NetStat + <$> ( (+) + <$> (read <$> readFile fp) + <*> (read <$> readFile (takeDirectory fp </> "tx_bytes")) + ) + +net' :: Sensor [NetStat] +net' = histogram "net'" [] (\b a -> take 7 (a : b)) netStat + +data Net = Net {unNet :: [Float]} deriving (Show, Eq) + +instance P.Pretty Net where + pretty = P.diagram 3 . unNet + +net :: Sensor Net +net = transform "net" f net' + where + f (map unNetStat -> xs) = do + let xs' = + zipWith + (-) + (fromMaybe [] (initMay xs)) + (fromMaybe [] (tailMay xs)) + x' = max 1 (fromMaybe 0 (maximumMay xs')) + pure $ Net $ map (\x -> fromIntegral x / fromIntegral x') xs' + +data MemStat = MemStat {unMemStat :: Float} deriving (Show, Eq) + +memStat :: Sensor MemStat +memStat = pollFile "memStat" aggregate "/proc/meminfo" (5 * 10 ^ 5) + where + aggregate fp = liftIO do + meminfo <- readFile fp + case foldl + ( \(total, avail) xs -> + case xs of + ["MemTotal:", v, "kB"] -> (Just (read v), avail) + ["MemAvailable:", v, "kB"] -> (total, Just (read v)) + _ -> (total, avail) + ) + (Nothing, Nothing) + (map words (lines meminfo)) of + (Just total, Just avail) -> pure $ MemStat (1 - avail / total) + (Nothing, _) -> error (printf "%s: MemTotal missing" fp) + (_, Nothing) -> error (printf "%s: MemAvail missing" fp) + +mem' :: Sensor [MemStat] +mem' = histogram "mem'" [] (\b a -> take 7 (a : b)) memStat + +data Mem = Mem {unMem :: [Float]} deriving (Show, Eq) + +instance P.Pretty Mem where + pretty = P.diagram 3 . unMem + +mem :: Sensor Mem +mem = transform "mem" (\(map unMemStat -> xs) -> pure (Mem xs)) mem' + +data BatStat = BatStat {unBatStat :: Float} deriving (Show, Eq) + +batStat :: Sensor BatStat +batStat = pollFile "batStat" aggregate "/sys/class/power_supply/macsmc-battery/charge_now" (5 * 10 ^ 5) + where + aggregate fp = liftIO do + BatStat + <$> ( (\now full -> fromIntegral now / fromIntegral full) + <$> (read <$> readFile fp) + <*> (read <$> readFile (takeDirectory fp </> "charge_full")) + ) + +bat' :: Sensor [BatStat] +bat' = histogram "bat'" [] (\b a -> take 7 (a : b)) batStat + +data Bat = Bat {unBat :: [Float]} deriving (Show, Eq) + +instance P.Pretty Bat where + pretty = P.diagram 1 . unBat + +bat :: Sensor Bat +bat = transform "bat" (\(map unBatStat -> xs) -> pure (Bat xs)) bat' + +data Snd = Snd {unSnd :: Float} deriving (Show, Eq) + +instance P.Pretty Snd where + pretty = P.diagram 1 . (: []) . unSnd + +snd :: Sensor Snd +snd = Sensor "snd" $ memoizeMVar do + stateT <- newTVarIO (Snd 1) + void $ forkIO $ forever do + x <- liftIO aggregate + atomically do writeTVar stateT (Snd x) + threadDelay (2 * 10 ^ 6) + pure stateT + where + aggregate = do + (/ 153) . read <$> [sh|pamixer --get-volume|] + +data Disk = Disk {unDisk :: [Float]} deriving (Show, Eq) + +instance P.Pretty Disk where + pretty = P.diagram 1 . unDisk + +disk :: Sensor Disk +disk = Sensor "disk" $ memoizeMVar do + stateT <- newTVarIO (Disk []) + void $ forkIO $ forever do + x <- liftIO aggregate + atomically do + readTVar stateT >>= \(Disk xs) -> + writeTVar stateT (Disk (x : xs)) + threadDelay (2 * 10 ^ 6) + pure stateT + where + aggregate = do + stat <- statVFS "/" + pure $ + (fromIntegral stat.statVFS_bfree) + / fromIntegral (stat.statVFS_bfree + stat.statVFS_bavail) + +currentTime :: Sensor UTCTime +currentTime = Sensor "currentTime" $ memoizeMVar do + stateT <- newTVarIO =<< liftIO getCurrentTime + void $ forkIO $ forever do + x <- liftIO getCurrentTime + atomically . writeTVar stateT =<< liftIO getCurrentTime + threadDelay (((10 ^ 12) - (fromEnum (utctDayTime x) `mod` (10 ^ 12))) `div` (10 ^ 6)) + pure stateT + +data Date = Date {unDate :: String} deriving (Show, Eq) + +instance P.Pretty Date where + pretty = P.color P.White . P.pretty . unDate + +date :: Sensor Date +date = transform "date" f currentTime + where + f x = do + z <- liftIO getCurrentTimeZone + pure . Date . formatTime defaultTimeLocale "%b %e" $ + utcToLocalTime z x + +data Time = Time {unTime :: String} deriving (Show, Eq) + +instance P.Pretty Time where + pretty = P.color P.White . P.pretty . unTime + +time :: Sensor Time +time = transform "time" f currentTime + where + f x = do + z <- liftIO getCurrentTimeZone + pure . Time . formatTime defaultTimeLocale "%R" $ + utcToLocalTime z x + +data WmName = WmName {unWmName :: String} deriving (Show, Eq) + +instance P.Pretty WmName where + pretty = P.color P.White . P.pretty . unWmName + +wmName' :: Sensor WmName +wmName' = watchXPropertyChanges "wmName'" as aggregate + where + as = ["_NET_ACTIVE_WINDOW"] + aggregate dpy = liftIO do + let root = X.rootWindowOfScreen (X.defaultScreenOfDisplay dpy) + netActiveWindow <- X.internAtom dpy "_NET_ACTIVE_WINDOW" False + X.getWindowProperty32 dpy netActiveWindow root >>= \case + Just (win : _) -> do + netWmName <- X.internAtom dpy "_NET_WM_NAME" False + fmap (WmName . strip . head) . X.wcTextPropertyToTextList dpy + =<< do + let loop 0 = do + error "failed to read _NET_WM_NAME" + loop n = do + catch + (X.getTextProperty dpy (fromIntegral win) netWmName) + ( \(e :: IOError) -> do + if isUserError e + then threadDelay 1000 >> loop (n - 1) + else throwIO e + ) + loop 16 + _ -> pure (WmName "") + strip = reverse . dropWhile isSpace . reverse . dropWhile isSpace + +wmName :: Sensor String +wmName = transform "wmName" (\(WmName x) -> pure x) wmName' + +data WmWorkspaces = WmWorkspaces {unWmWorkspaces :: [Workspace]} + deriving (Show, Eq) + +data Workspace + = Active String + | Inactive String + deriving (Show, Eq) + +instance P.Pretty WmWorkspaces where + pretty = + P.Col . intersperse (P.pretty " ") . map P.pretty . unWmWorkspaces + +instance P.Pretty Workspace where + pretty (Active s) = P.color P.White (P.pretty s) + pretty (Inactive s) = P.colorDull P.White (P.pretty s) + +wmWorkspaces :: Sensor WmWorkspaces +wmWorkspaces = watchXPropertyChanges "wmWorkspaces" as aggregate + where + as = + [ "_NET_CLIENT_LIST", + "_NET_CURRENT_DESKTOP", + "_NET_DESKTOP_NAMES", + "_NET_WM_DESKTOP" + ] + aggregate dpy = liftIO do + let root = X.rootWindowOfScreen (X.defaultScreenOfDisplay dpy) + netClientList <- liftIO $ X.internAtom dpy "_NET_CLIENT_LIST" False + netCurrentDesktop <- liftIO $ X.internAtom dpy "_NET_CURRENT_DESKTOP" False + netDesktopNames <- liftIO $ X.internAtom dpy "_NET_DESKTOP_NAMES" False + netWmDesktop <- liftIO $ X.internAtom dpy "_NET_WM_DESKTOP" False + currentDesktop <- fmap (fromIntegral . head) <$> X.getWindowProperty32 dpy netCurrentDesktop root + occupiedDesktops <- + fmap (S.unions . catMaybes) + . mapM + ( \win -> do + fmap (S.singleton . fromIntegral . head) + <$> X.getWindowProperty32 dpy netWmDesktop win + ) + . map fromIntegral + . fromMaybe [] + =<< X.getWindowProperty32 dpy netClientList root + fmap + ( WmWorkspaces + . ifilter (\i _ -> Just i == currentDesktop || i `S.member` occupiedDesktops) + . imap + ( \i -> + if Just i == currentDesktop + then Active + else Inactive + ) + ) + . X.wcTextPropertyToTextList dpy + =<< X.getTextProperty dpy root netDesktopNames + +watchFile :: String -> (FilePath -> SensorM a) -> FilePath -> Sensor a +watchFile id aggregate fp = Sensor id $ memoizeMVar do + i <- liftIO do + i <- I.init + void (I.addWatch i fp I.in_CLOSE) + pure i + stateT <- newTVarIO =<< aggregate fp + void $ forkIO $ forever do + void (liftIO (I.getEvent i)) + -- liftIO (printf "[%s] aggregate\n" id) + x <- aggregate fp + atomically (writeTVar stateT x) + pure stateT + +pollFile :: String -> (FilePath -> SensorM a) -> FilePath -> Int -> Sensor a +pollFile id aggregate fp delay = Sensor id $ memoizeMVar do + stateT <- newTVarIO =<< aggregate fp + void $ forkIO $ forever do + -- liftIO (printf "[%s] aggregate\n" id) + x <- aggregate fp + threadDelay delay + atomically (writeTVar stateT x) + pure stateT + +watchXPropertyChanges :: + String -> + [String] -> + (X.Display -> SensorM a) -> + Sensor a +watchXPropertyChanges id as' aggregate = Sensor id $ memoizeMVar do + (dpy, as) <- liftIO do + dpy <- X.openDisplay "" + let root = X.rootWindowOfScreen (X.defaultScreenOfDisplay dpy) + X.selectInput dpy root X.propertyChangeMask + as <- S.fromList <$> mapM (\a -> X.internAtom dpy a False) as' + pure (dpy, as) + stateT <- newTVarIO =<< aggregate dpy + void $ forkIO $ forever $ do + as' <- liftIO $ X.allocaXEvent $ \ev -> do + X.nextEvent dpy ev + e <- X.getEvent ev + let loop es = do + isTimeout <- X.waitForEvent dpy 0 + if isTimeout + then pure (S.fromList (map (X.ev_atom) es)) + else do + X.nextEvent dpy ev + e <- X.getEvent ev + loop (e : es) + loop [e] + unless (S.null (as `S.intersection` as')) do + atomically . writeTVar stateT =<< aggregate dpy + pure stateT + +histogram :: + (Eq a, Typeable a) => + String -> + b -> + (b -> a -> b) -> + Sensor a -> + Sensor b +histogram id def acc sensor = transform' id def f sensor + where + f b a = pure (acc b a) + +transform' :: + (Eq a, Typeable a) => + String -> + b -> + (b -> a -> SensorM b) -> + Sensor a -> + Sensor b +transform' id def fM sensor = Sensor id $ memoizeMVar do + xT <- poll sensor + x <- readTVarIO xT + x'T <- newTVarIO x + stateT <- newTVarIO =<< fM def x + void $ forkIO $ forever $ do + (def, x) <- atomically do + x' <- readTVar x'T + x <- readTVar xT + checkSTM (x /= x') + def <- readTVar stateT + writeTVar x'T x + pure (def, x) + atomically . writeTVar stateT =<< fM def x + pure stateT + +transform :: + (Eq a, Typeable a) => + String -> + (a -> SensorM b) -> + Sensor a -> + Sensor b +transform id fM sensor = Sensor id $ memoizeMVar do + xT <- poll sensor + x <- readTVarIO xT + x'T <- newTVarIO x + stateT <- newTVarIO =<< fM x + void $ forkIO $ forever $ do + x <- atomically do + x' <- readTVar x'T + x <- readTVar xT + checkSTM (x /= x') + writeTVar x'T x + pure x + atomically . writeTVar stateT =<< fM x + pure stateT diff --git a/app/Ui.hs b/app/Ui.hs new file mode 100644 index 0000000..fa1b38e --- /dev/null +++ b/app/Ui.hs @@ -0,0 +1,200 @@ +{-# LANGUAGE BlockArguments #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE OverloadedRecordDot #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE StrictData #-} +{-# LANGUAGE ViewPatterns #-} +{-# LANGUAGE NoFieldSelectors #-} +{-# OPTIONS_GHC -fno-warn-name-shadowing #-} +{-# OPTIONS_GHC -fno-warn-type-defaults #-} + +module Ui + ( Ui (Ui), + lit, + fill, + sens, + initUi, + pollUi, + Env (..), + Ui', + layOutUi, + renderUi, + renderUi', + ) +where + +import Control.Concurrent.STM +import Control.Monad.Identity +import Control.Monad.Reader +import Control.Monad.State (evalStateT, get, modify) +import Data.Default +import Data.Dynamic +import Data.Map qualified as M +import Data.Maybe +import GHC.Generics (Generic) +import Graphics.X11 qualified as X +import Graphics.X11.Xft qualified as X +import Graphics.X11.Xrender qualified as X +import Pretty qualified as P +import Sensor hiding (io) + +data Ui f a = Ui [Block f a] + +instance Eq (Ui Identity P.Doc) where + Ui bs == Ui bs' = bs == bs' + +data Block f a + = Lit P.Doc + | Fill + | forall o. (Typeable o) => Sens (o -> a) (f o) + +instance Eq (Block Identity P.Doc) where + (Lit doc) == (Lit doc') = doc == doc' + Fill == Fill = True + Sens toOut (Identity x) == Sens toOut' (Identity x') = toOut x == toOut' x' + _ == _ = False + +lit :: (P.Pretty a) => a -> Block f P.Doc +lit = Lit . P.pretty + +fill :: Block f a +fill = Fill + +sens :: (P.Pretty o, Typeable o) => f o -> Block f P.Doc +sens = Sens P.pretty + +initUi :: + Ui Sensor P.Doc -> + SensorM (Ui TVar P.Doc) +initUi (Ui bs) = Ui <$> mapM go bs + where + go (Lit s) = pure (Lit s) + go Fill = pure Fill + go (Sens toOut s) = Sens toOut <$> poll s + +pollUi :: + Ui TVar P.Doc -> + STM (Ui Identity P.Doc) +pollUi (Ui bs) = Ui <$> mapM go bs + where + go (Lit s) = pure (Lit s) + go Fill = pure Fill + go (Sens toOut vT) = Sens toOut . Identity <$> readTVar vT + +layOutUi :: + Env -> + Ui Identity P.Doc -> + IO Ui' +layOutUi env (Ui bs) = layOut env (Ui' (concatMap go bs)) + where + go (Lit doc) = go' doc + go (Sens toOut (runIdentity -> toOut -> doc)) = go' doc + go Fill = + [ C + { rect = def, + fill = True, + color = Nothing, + string = "" + } + ] + + go' (P.Lit color string) = + [ C + { rect = def, + fill = False, + .. + } + ] + go' (P.Col docs) = + concatMap go' docs + +layOut :: Env -> Ui' -> IO Ui' +layOut env@Env {..} (Ui' cs) = do + cs' <- evalStateT (mapM pack cs) 0 + let rwidth = wwidth - sum (map (.rect.width) cs') + pure (Ui' (expa rwidth cs')) + where + pack c = do + rect <- io (extents env c) + left <- get <* modify (+ rect.width) + pure c {rect = rect {left = left}} + + expa rwidth (c@C {fill = True} : cs) = + do + c {rect = c.rect {width = c.rect.width + rwidth}} + : map (\c -> c {rect = c.rect {left = c.rect.left + rwidth}}) cs + expa rwidth (c : cs) = c : expa rwidth cs + expa _ [] = [] + +extents :: Env -> C -> IO Rect +extents Env {..} (C {..}) = do + glyphInfo <- X.xftTextExtents dpy fnt string + pure + ( Rect + { top = 0, + left = 0, + width = X.xglyphinfo_xOff glyphInfo, + height = X.xglyphinfo_height glyphInfo + } + ) + +data Env = Env + { dpy :: X.Display, + pixm :: X.Pixmap, + gc :: X.GC, + wwidth :: Int, + wheight :: Int, + vis :: X.Visual, + cmap :: X.Colormap, + fnt :: X.XftFont, + drw :: X.XftDraw + } + +type Colors = M.Map (P.Intensity, P.Color) X.XftColor + +renderUi :: Env -> Colors -> Ui' -> IO () +renderUi Env {..} colors (Ui' cs) = do + let bg = colors M.! (P.Dull, P.Black) + X.xftDrawRect drw bg 0 0 wwidth wheight + let h = maximum (map (.rect.height) cs) + mapM_ (go h) cs + where + go h (C {..}) = do + let fg = colors M.! fromMaybe (P.Dull, P.White) color + liftIO (X.xftDrawString drw fg fnt rect.left h string) + +renderUi' :: Env -> Colors -> Ui' -> Ui' -> IO () +renderUi' env@Env {..} colors (Ui' cs') ui@(Ui' cs) + | length cs' /= length cs = renderUi env colors ui + | otherwise = do + let h = maximum (map (.rect.height) cs) + cs'' = catMaybes (zipWith (\c' c -> if c /= c' then Just c else Nothing) cs' cs) + mapM_ (go h) cs'' + where + go h (C {..}) = io do + let bg = colors M.! (P.Dull, P.Black) + let fg = colors M.! (fromMaybe (P.Dull, P.White) color) + X.xftDrawRect drw bg rect.left 0 rect.width wheight + X.xftDrawString drw fg fnt rect.left h string + +data Ui' = Ui' [C] deriving (Show) + +data C = C + { rect :: Rect, + fill :: Bool, + color :: Maybe (P.Intensity, P.Color), + string :: String + } + deriving (Show, Eq) + +data Rect = Rect + { top :: Int, + left :: Int, + width :: Int, + height :: Int + } + deriving (Generic, Default, Show, Eq) + +io :: (MonadIO m) => IO a -> m a +io = liftIO diff --git a/app/Util.hs b/app/Util.hs new file mode 100644 index 0000000..308ef88 --- /dev/null +++ b/app/Util.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE BlockArguments #-} + +module Util + ( stmChanges, + ) +where + +import UnliftIO.STM + +stmChanges :: (Eq a) => STM a -> IO (STM a) +stmChanges mkX = do + x'T <- newTVarIO Nothing + pure do + x' <- readTVar x'T + x <- mkX + checkSTM (Just x /= x') + writeTVar x'T (Just x) + pure x diff --git a/astatusbar.cabal b/astatusbar.cabal new file mode 100644 index 0000000..432c36c --- /dev/null +++ b/astatusbar.cabal @@ -0,0 +1,38 @@ +cabal-version: 3.4 +name: astatusbar +version: 0.1.0.0 +license: BSD-3-Clause +license-file: LICENSE +maintainer: aforemny@posteo.de +author: Alexander Foremny +build-type: Simple + +executable astatusbar + main-is: Main.hs + hs-source-dirs: app + other-modules: + Pretty + Pretty.Color + Sensor + Ui + Util + + default-language: GHC2021 + ghc-options: -Wall -threaded -O1 + build-depends: + base, + containers, + data-default, + filepath, + indexed-traversable, + linux-inotify, + mtl, + safe, + sh, + statvfs, + stm, + time, + unliftio, + witherable, + X11, + X11-xft diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..a0d748b --- /dev/null +++ b/default.nix @@ -0,0 +1,66 @@ +{ pkgs ? import (import ./nix/sources.nix).nixpkgs { } }: +let + haskellPackages = pkgs.haskellPackages.override { + overrides = self: super: { + astatusbar = self.callCabal2nix "astatusbar" ./. { }; + sh = pkgs.haskell.lib.dontCheck (self.callCabal2nix "sh" (import ./nix/sources.nix).sh { }); + statvfs = pkgs.haskell.lib.markUnbroken (super.statvfs.overrideAttrs (oldAtts: { + patches = [ + (pkgs.writers.writeText "statvfs.patch" '' + diff --git a/Setup.hs b/Setup.hs + index 7cf9bfd..54f57d6 100644 + --- a/Setup.hs + +++ b/Setup.hs + @@ -3,4 +3,4 @@ module Main (main) where + import Distribution.Simple + + main :: IO () + -main = defaultMainWithHooks defaultUserHooks + +main = defaultMainWithHooks autoconfUserHooks + '') + ]; + })); + X11 = (self.callCabal2nix "X11" + (pkgs.fetchFromGitHub { + owner = "xmonad"; + repo = "X11"; + rev = "1ead9698c89e78884ac771c93822447999a1dc46"; + hash = "sha256-OAZ3gnUezsumBC8VwhcsKq+Nrq44L4xY/z6exK7InLo="; + }) + { }).overrideAttrs (oldAttrs: { + preConfigure = oldAttrs.preConfigure or "" + '' + ${pkgs.autoconf}/bin/autoreconf''; + patches = oldAttrs.patches or [ ] ++ [ + (pkgs.writers.writeText "X11-safe-select.patch" '' + diff --git a/Graphics/X11/Xlib/Event.hsc b/Graphics/X11/Xlib/Event.hsc + index 842fc2f..869aba8 100644 + --- a/Graphics/X11/Xlib/Event.hsc + +++ b/Graphics/X11/Xlib/Event.hsc + @@ -419,7 +419,7 @@ newtype FdSet = FdSet (Ptr FdSet) + foreign import ccall unsafe "HsXlib.h" fdZero :: Ptr FdSet -> IO () + foreign import ccall unsafe "HsXlib.h" fdSet :: CInt -> Ptr FdSet -> IO () + + -foreign import ccall unsafe "HsXlib.h" select :: + +foreign import ccall safe "HsXlib.h" select :: + CInt -> Ptr FdSet -> Ptr FdSet -> Ptr FdSet -> Ptr TimeVal -> IO CInt + + -- | This function is somewhat compatible with Win32's @TimeGetTime()@ + '') + ]; + }); + }; + }; +in +rec { + inherit haskellPackages; + inherit (haskellPackages) astatusbar; + shell = haskellPackages.shellFor { + packages = _: [ haskellPackages.astatusbar ]; + buildInputs = [ + pkgs.cabal-install + pkgs.niv + pkgs.ormolu + ]; + withHoogle = true; + }; +} diff --git a/nix/sources.json b/nix/sources.json new file mode 100644 index 0000000..3185bc5 --- /dev/null +++ b/nix/sources.json @@ -0,0 +1,20 @@ +{ + "nixpkgs": { + "branch": "release-23.11", + "description": "Nix Packages collection", + "homepage": null, + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "af7e9fb77b8993a7b5f01282c869e503d6cc9e6e", + "sha256": "174r9nhpmvgrjlr46gvs5vvn38nmlzz0hhr27xz5qaabnlwi6b8m", + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/af7e9fb77b8993a7b5f01282c869e503d6cc9e6e.tar.gz", + "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz" + }, + "sh": { + "branch": "main", + "repo": "git@code.nomath.org:~/sh", + "rev": "3ea4e6459333409c60f66a5745bb472d136da741", + "type": "git" + } +} diff --git a/nix/sources.nix b/nix/sources.nix new file mode 100644 index 0000000..fe3dadf --- /dev/null +++ b/nix/sources.nix @@ -0,0 +1,198 @@ +# This file has been generated by Niv. + +let + + # + # The fetchers. fetch_<type> fetches specs of type <type>. + # + + fetch_file = pkgs: name: spec: + let + name' = sanitizeName name + "-src"; + in + if spec.builtin or true then + builtins_fetchurl { inherit (spec) url sha256; name = name'; } + else + pkgs.fetchurl { inherit (spec) url sha256; name = name'; }; + + fetch_tarball = pkgs: name: spec: + let + name' = sanitizeName name + "-src"; + in + if spec.builtin or true then + builtins_fetchTarball { name = name'; inherit (spec) url sha256; } + else + pkgs.fetchzip { name = name'; inherit (spec) url sha256; }; + + fetch_git = name: spec: + let + ref = + spec.ref or ( + if spec ? branch then "refs/heads/${spec.branch}" else + if spec ? tag then "refs/tags/${spec.tag}" else + abort "In git source '${name}': Please specify `ref`, `tag` or `branch`!" + ); + submodules = spec.submodules or false; + submoduleArg = + let + nixSupportsSubmodules = builtins.compareVersions builtins.nixVersion "2.4" >= 0; + emptyArgWithWarning = + if submodules + then + builtins.trace + ( + "The niv input \"${name}\" uses submodules " + + "but your nix's (${builtins.nixVersion}) builtins.fetchGit " + + "does not support them" + ) + { } + else { }; + in + if nixSupportsSubmodules + then { inherit submodules; } + else emptyArgWithWarning; + in + builtins.fetchGit + ({ url = spec.repo; inherit (spec) rev; inherit ref; } // submoduleArg); + + fetch_local = spec: spec.path; + + fetch_builtin-tarball = name: throw + ''[${name}] The niv type "builtin-tarball" is deprecated. You should instead use `builtin = true`. + $ niv modify ${name} -a type=tarball -a builtin=true''; + + fetch_builtin-url = name: throw + ''[${name}] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`. + $ niv modify ${name} -a type=file -a builtin=true''; + + # + # Various helpers + # + + # https://github.com/NixOS/nixpkgs/pull/83241/files#diff-c6f540a4f3bfa4b0e8b6bafd4cd54e8bR695 + sanitizeName = name: + ( + concatMapStrings (s: if builtins.isList s then "-" else s) + ( + builtins.split "[^[:alnum:]+._?=-]+" + ((x: builtins.elemAt (builtins.match "\\.*(.*)" x) 0) name) + ) + ); + + # The set of packages used when specs are fetched using non-builtins. + mkPkgs = sources: system: + let + sourcesNixpkgs = + import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) { inherit system; }; + hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath; + hasThisAsNixpkgsPath = <nixpkgs> == ./.; + in + if builtins.hasAttr "nixpkgs" sources + then sourcesNixpkgs + else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then + import <nixpkgs> { } + else + abort + '' + Please specify either <nixpkgs> (through -I or NIX_PATH=nixpkgs=...) or + add a package called "nixpkgs" to your sources.json. + ''; + + # The actual fetching function. + fetch = pkgs: name: spec: + + if ! builtins.hasAttr "type" spec then + abort "ERROR: niv spec ${name} does not have a 'type' attribute" + else if spec.type == "file" then fetch_file pkgs name spec + else if spec.type == "tarball" then fetch_tarball pkgs name spec + else if spec.type == "git" then fetch_git name spec + else if spec.type == "local" then fetch_local spec + else if spec.type == "builtin-tarball" then fetch_builtin-tarball name + else if spec.type == "builtin-url" then fetch_builtin-url name + else + abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}"; + + # If the environment variable NIV_OVERRIDE_${name} is set, then use + # the path directly as opposed to the fetched source. + replace = name: drv: + let + saneName = stringAsChars (c: if (builtins.match "[a-zA-Z0-9]" c) == null then "_" else c) name; + ersatz = builtins.getEnv "NIV_OVERRIDE_${saneName}"; + in + if ersatz == "" then drv else + # this turns the string into an actual Nix path (for both absolute and + # relative paths) + if builtins.substring 0 1 ersatz == "/" then /. + ersatz else /. + builtins.getEnv "PWD" + "/${ersatz}"; + + # Ports of functions for older nix versions + + # a Nix version of mapAttrs if the built-in doesn't exist + mapAttrs = builtins.mapAttrs or ( + f: set: with builtins; + listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set)) + ); + + # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295 + range = first: last: if first > last then [ ] else builtins.genList (n: first + n) (last - first + 1); + + # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257 + stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1)); + + # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269 + stringAsChars = f: s: concatStrings (map f (stringToCharacters s)); + concatMapStrings = f: list: concatStrings (map f list); + concatStrings = builtins.concatStringsSep ""; + + # https://github.com/NixOS/nixpkgs/blob/8a9f58a375c401b96da862d969f66429def1d118/lib/attrsets.nix#L331 + optionalAttrs = cond: as: if cond then as else { }; + + # fetchTarball version that is compatible between all the versions of Nix + builtins_fetchTarball = { url, name ? null, sha256 }@attrs: + let + inherit (builtins) lessThan nixVersion fetchTarball; + in + if lessThan nixVersion "1.12" then + fetchTarball ({ inherit url; } // (optionalAttrs (name != null) { inherit name; })) + else + fetchTarball attrs; + + # fetchurl version that is compatible between all the versions of Nix + builtins_fetchurl = { url, name ? null, sha256 }@attrs: + let + inherit (builtins) lessThan nixVersion fetchurl; + in + if lessThan nixVersion "1.12" then + fetchurl ({ inherit url; } // (optionalAttrs (name != null) { inherit name; })) + else + fetchurl attrs; + + # Create the final "sources" from the config + mkSources = config: + mapAttrs + ( + name: spec: + if builtins.hasAttr "outPath" spec + then + abort + "The values in sources.json should not have an 'outPath' attribute" + else + spec // { outPath = replace name (fetch config.pkgs name spec); } + ) + config.sources; + + # The "config" used by the fetchers + mkConfig = + { sourcesFile ? if builtins.pathExists ./sources.json then ./sources.json else null + , sources ? if sourcesFile == null then { } else builtins.fromJSON (builtins.readFile sourcesFile) + , system ? builtins.currentSystem + , pkgs ? mkPkgs sources system + }: rec { + # The sources, i.e. the attribute set of spec name to spec + inherit sources; + + # The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers + inherit pkgs; + }; + +in +mkSources (mkConfig { }) // { __functor = _: settings: mkSources (mkConfig settings); } diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..a6bdf20 --- /dev/null +++ b/shell.nix @@ -0,0 +1 @@ +(import ./. { }).shell |